fix(alerts): разделить загрузку каталогов целей и правил
This commit is contained in:
1 parent
a8a3eb59fb
commit
526f305aa6
11 files changed
+153
-77
No files matched your search
+22
-74
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user