fix(alerts): разделить загрузку каталогов целей и правил

This commit is contained in:
Denozordec committed 2026-10-03 17:50:23 +07:00
1 parent a8a3eb59fb
commit 526f305aa6
11 files changed
+153 -77

No files matched your search

+22 -74
View File
@@ -1,6 +1,8 @@
"use client"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import {useAlertsTargets} from "@/hooks/use-alerts-targets"
import {AlertsTargetsStatus} from "@/components/alerts-targets-status"
import { useDataSource } from "@/lib/data-source"
import { PageHeader } from "@/components/page-header"
import { FormToggle } from "@/components/form-kit"
@@ -171,12 +173,6 @@ interface AlertsMeta {
greTunnelDetail?: AlertsGreTunnelRow[]
}
/** Минимальный ответ BGP для подписей «роутер · сосед». */
interface BgpSessionListRow {
serverName: string
name: string
}
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
return requestJson<T>(backendUrl, path, init)
@@ -2364,7 +2360,19 @@ function AlertsContent() {
const [history, setHistory] = useState<HistoryEntry[]>(isLive?[]:INIT_HISTORY)
const [tg, setTg] = useState<TelegramConfig>(isLive?{token:"",chatId:"",messageThreadId:"",connected:false}:INIT_TG)
const [tokenConfigured, setTokenConfigured] = useState(false)
const [meta, setMeta] = useState<AlertsMeta | null>(null)
const [baseMeta, setMeta] = useState<AlertsMeta | null>(null)
const catalogs=useAlertsTargets(backendUrl,isLive)
const meta=useMemo(()=>{
if(!baseMeta)return null
const serverMap=new Map(catalogs.servers.data?.map(server=>[String(server.id),server]))
const greTunnelDetail:AlertsGreTunnelRow[]|undefined=catalogs.gre.data?.tunnels.map(tunnel=>{
const server=serverMap.get(tunnel.serverId)
const routerName=server?.name.trim()||tunnel.serverId
const tunnelName=tunnel.name.trim()||tunnel.id?.trim()||"GRE"
return {targetLabel:tunnelName+" / "+routerName,tunnelName,routerName,country:server?.country||"—",site:server?.site||"—",remoteAddress:tunnel.remoteAddress||"—",status:tunnel.status?.trim()||"Неизвестно",comment:tunnel.comment}
})
return {...baseMeta,bgpPeers:catalogs.bgp.data?.map(row=>row.serverName+": "+row.name),greTunnelDetail,greTunnelTargets:greTunnelDetail?.map(row=>row.targetLabel)}
},[baseMeta,catalogs.bgp.data,catalogs.servers.data,catalogs.gre.data])
const [known,setKnown]=useState(!isLive)
const [receivedAt,setReceivedAt]=useState<string|null>(null)
@@ -2446,74 +2454,13 @@ function AlertsContent() {
setRules(mapApiRules(data.rules))
setGroups(mapApiGroups(data.groups ?? []))
setHistory(mapApiHistory(data.history))
let mergedMeta: AlertsMeta = data.meta
try {
const bgpRows = await apiFetch<BgpSessionListRow[]>("/api/bgp/sessions",{signal:controller.signal})
const bgpPeers = uniqStrings(
bgpRows.map((s) => `${String(s.serverName ?? "").trim()}: ${String(s.name ?? "").trim()}`.trim()),
)
mergedMeta = { ...data.meta, bgpPeers }
} catch {
mergedMeta = { ...data.meta, bgpPeers: [] }
}
try {
const [srvList, greRes] = await Promise.all([
apiFetch<
Array<{ id: number; name: string; site?: string; country?: string; host?: string }>
>("/api/servers",{signal:controller.signal}),
apiFetch<{
tunnels: Array<{
id?: string
name: string
serverId: string
remoteAddress?: string
status?: string
comment?: string
}>
}>("/api/filters/gre-tunnels",{signal:controller.signal}),
])
const sidToSrv = new Map(
srvList.map((s) => [
String(s.id),
{
name: String(s.name ?? "").trim() || String(s.id),
site: String(s.site ?? "").trim(),
country: String(s.country ?? "").trim(),
host: String(s.host ?? "").trim(),
},
]),
)
const greTunnelDetail: AlertsGreTunnelRow[] = greRes.tunnels.map((t) => {
const srv = sidToSrv.get(String(t.serverId))
const sn = srv?.name ?? String(t.serverId)
const tn = (String(t.name ?? "").trim() || String(t.id ?? "").trim() || "gre").trim()
const label = `${tn} / ${sn}`.trim()
const rawSt = (t.status ?? "down").toLowerCase()
const status: AlertsGreTunnelRow["status"] =
rawSt === "up" || rawSt === "down" || rawSt === "degraded" ? rawSt : "degraded"
return {
targetLabel: label,
tunnelName: tn,
routerName: sn,
country: srv?.country ? srv.country : "—",
site: srv?.site ? srv.site : "—",
remoteAddress: (String(t.remoteAddress ?? "").trim() || "—"),
status,
comment: (t.comment ?? "").trim() || undefined,
}
})
const greTunnelTargets = uniqStrings(greTunnelDetail.map((r) => r.targetLabel))
mergedMeta = { ...mergedMeta, greTunnelTargets, greTunnelDetail }
} catch {
mergedMeta = { ...mergedMeta, greTunnelTargets: [], greTunnelDetail: [] }
}
if(pageActive.current&&requestRevision===revision.current)setMeta(mergedMeta)
setMeta(data.meta)
} catch (e) {
if(pageActive.current&&requestRevision===revision.current)setLoadError(e instanceof Error ? e.message : "Ошибка загрузки")
} finally {
if(pageActive.current&&requestRevision===revision.current)setLoadBusy(false)
}
}, [apiFetch, isLive, backendUrl])
}, [isLive, backendUrl])
useEffect(() => {
let current=true
@@ -2806,13 +2753,13 @@ function AlertsContent() {
const chipActive = (sev: SeverityFilter) => sevFilter === sev
return (
<div className="flex flex-col h-full">
<div className="flex min-h-0 flex-col h-full">
<PageHeader
crumbs={[{ label: "Система" }, { label: "Оповещения" }]}
actions={
<>
<Button variant="outline" size="sm" disabled={loadBusy || (isLive && backendStatus === false)}
onClick={() => { void load() }}>
onClick={() => { void load(); catalogs.refresh() }}>
{loadBusy ? <LoaderCircleIcon className="size-4 animate-spin" /> : <RefreshCwIcon className="size-4" />}
Обновить
</Button>
@@ -2823,7 +2770,7 @@ function AlertsContent() {
}
/>
<div className="flex-1 overflow-y-auto p-6">
<div className="min-h-0 flex-1 overflow-y-auto p-4 sm:p-6">
<div className="flex flex-col gap-5">
{isLive && backendStatus === false && (
@@ -2839,12 +2786,13 @@ function AlertsContent() {
)}
{loadError && (
<p className="text-xs text-destructive rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2">
<p role="alert" className="text-xs text-destructive rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2">
{loadError}
</p>
)}
{isLive?<p role="status" className="text-xs text-muted-foreground">{known?"Последние данные получены: "+new Date(receivedAt!).toLocaleString("ru-RU"):loadBusy?"Загрузка оповещений…":"Данные оповещений ещё не получены."}</p>:null}
{isLive?<AlertsTargetsStatus catalogs={catalogs}/>:null}
{known?<>
<Collapsible open={hintsExpanded} onOpenChange={setHintsExpanded} className="rounded-lg border border-dashed border-border bg-muted/15">
<CollapsibleTrigger className="flex w-full items-center gap-2 px-3 py-2 text-left text-xs font-medium text-foreground hover:bg-muted/40 rounded-lg">