feat: integrate sonner for toast notifications and enhance UI feedback
Added the sonner library for toast notifications across various components, improving user feedback for actions such as saving settings, syncing rules, and handling errors. Updated the layout to include a Toaster component for consistent notification display. Refactored alert messages in the backups, gre, and filters pages to utilize the new notification system, enhancing overall user experience.
This commit is contained in:
@@ -13,7 +13,6 @@ import { BandwidthChart } from "@/components/dashboard/bandwidth-chart"
|
||||
import {
|
||||
servers as mockServers,
|
||||
pingProbes,
|
||||
systemEvents,
|
||||
dashLatency,
|
||||
traffic,
|
||||
serverFilterRulesets,
|
||||
@@ -26,6 +25,8 @@ import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import { buildLatencySeriesByProbeSource } from "@/lib/dashboard-latency"
|
||||
import { listEvents } from "@/shared/api/events"
|
||||
import type { EventItem } from "@/packages/contracts/src/events"
|
||||
|
||||
function makeApiFetch(backendUrl: string) {
|
||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
@@ -71,6 +72,19 @@ function fmtIntRu(n: number): string {
|
||||
return n.toLocaleString("ru-RU")
|
||||
}
|
||||
|
||||
function formatEventAge(iso: string): string {
|
||||
const ts = Date.parse(iso)
|
||||
if (!Number.isFinite(ts)) return "—"
|
||||
const diffMs = Math.max(0, Date.now() - ts)
|
||||
const minutes = Math.floor(diffMs / 60_000)
|
||||
if (minutes < 1) return "сейчас"
|
||||
if (minutes < 60) return `${minutes}м`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
if (hours < 24) return `${hours}ч`
|
||||
const days = Math.floor(hours / 24)
|
||||
return `${days}д`
|
||||
}
|
||||
|
||||
interface LiveKpiSnapshot {
|
||||
filters: { ruleTotal: number; serversWithRules: number } | null
|
||||
bgp: { prefixSum: number; establishedCount: number } | null
|
||||
@@ -198,6 +212,9 @@ export default function DashboardPage() {
|
||||
const [liveKpi, setLiveKpi] = useState<LiveKpiSnapshot | null>(null)
|
||||
const [probesLoading, setProbesLoading] = useState(false)
|
||||
const [probesError, setProbesError] = useState<string | null>(null)
|
||||
const [recentEvents, setRecentEvents] = useState<EventItem[]>([])
|
||||
const [eventsLoading, setEventsLoading] = useState(false)
|
||||
const [eventsError, setEventsError] = useState<string | null>(null)
|
||||
|
||||
const probeServerCatalog = useMemo(() => {
|
||||
if (!isLive) return mockServers
|
||||
@@ -259,6 +276,25 @@ export default function DashboardPage() {
|
||||
}
|
||||
}, [apiFetch, isLive])
|
||||
|
||||
const fetchRecentEvents = useCallback(async (silent: boolean) => {
|
||||
if (!isLive) {
|
||||
setRecentEvents([])
|
||||
setEventsError(null)
|
||||
return
|
||||
}
|
||||
if (!silent) setEventsLoading(true)
|
||||
try {
|
||||
const rows = await listEvents(backendUrl, { limit: 8 })
|
||||
setRecentEvents(rows)
|
||||
setEventsError(null)
|
||||
} catch (error) {
|
||||
setRecentEvents([])
|
||||
setEventsError(error instanceof Error ? error.message : "Не удалось загрузить события")
|
||||
} finally {
|
||||
if (!silent) setEventsLoading(false)
|
||||
}
|
||||
}, [backendUrl, isLive])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLive) {
|
||||
queueMicrotask(() => {
|
||||
@@ -277,6 +313,19 @@ export default function DashboardPage() {
|
||||
return () => { cancelled = true }
|
||||
}, [isLive, fetchProbes])
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
void fetchRecentEvents(false)
|
||||
})
|
||||
}, [fetchRecentEvents])
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
queueMicrotask(() => { void fetchRecentEvents(true) })
|
||||
}, 20_000)
|
||||
return () => clearInterval(id)
|
||||
}, [fetchRecentEvents])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLive) return
|
||||
const id = setInterval(() => {
|
||||
@@ -628,24 +677,38 @@ export default function DashboardPage() {
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">Последние события</CardTitle>
|
||||
<Button variant="ghost" size="sm" className="text-xs h-7">Все →</Button>
|
||||
<Link
|
||||
href="/alerts"
|
||||
className={cn(buttonVariants({ variant: "ghost", size: "sm" }), "text-xs h-7")}
|
||||
>
|
||||
Все →
|
||||
</Link>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Система и BGP-активность</p>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 px-0">
|
||||
<div className="divide-y divide-border">
|
||||
{systemEvents.map((e) => (
|
||||
{eventsLoading && recentEvents.length === 0 && (
|
||||
<div className="px-5 py-6 text-sm text-muted-foreground">Загрузка событий...</div>
|
||||
)}
|
||||
{eventsError && recentEvents.length === 0 && (
|
||||
<div className="px-5 py-6 text-sm text-destructive">{eventsError}</div>
|
||||
)}
|
||||
{!eventsLoading && !eventsError && recentEvents.length === 0 && (
|
||||
<div className="px-5 py-6 text-sm text-muted-foreground">Событий пока нет.</div>
|
||||
)}
|
||||
{recentEvents.map((e) => (
|
||||
<div key={e.id} className="grid grid-cols-[20px_1fr_auto] gap-3 px-5 py-3 items-start">
|
||||
<div className="mt-0.5">
|
||||
{e.sev === "destructive" && <AlertCircleIcon className="size-4 text-destructive" />}
|
||||
{e.sev === "warning" && <AlertTriangleIcon className="size-4 text-[var(--status-degraded)]" />}
|
||||
{e.sev === "info" && <InfoIcon className="size-4 text-[var(--chart-1)]" />}
|
||||
{e.level === "critical" && <AlertCircleIcon className="size-4 text-destructive" />}
|
||||
{e.level === "warning" && <AlertTriangleIcon className="size-4 text-[var(--status-degraded)]" />}
|
||||
{e.level === "info" && <InfoIcon className="size-4 text-[var(--chart-1)]" />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[13px] font-medium leading-tight">{e.title}</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5 leading-snug">{e.message}</p>
|
||||
</div>
|
||||
<span className="text-[11px] font-mono text-muted-foreground">{e.when}</span>
|
||||
<span className="text-[11px] font-mono text-muted-foreground">{formatEventAge(e.createdAt)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user