This commit is contained in:
Denozordec
2026-05-03 11:16:07 +07:00
parent ce00c4c671
commit bdb9b72fac
66 changed files with 9553 additions and 1547 deletions
+42 -28
View File
@@ -345,10 +345,10 @@ function moveInArray<T>(arr: T[], from: number, to: number): T[] {
function stateClass(state: OspfNeighbor["state"] | BfdSession["state"]) {
if (state === "Full" || state === "Up")
return "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/25"
return "bg-[var(--status-online-bg)] text-[var(--status-online-fg)] border-current/25"
if (state === "2-Way" || state === "Init")
return "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/25"
return "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/25"
return "bg-[var(--status-degraded-bg)] text-[var(--status-degraded-fg)] border-current/25"
return "bg-[var(--status-offline-bg)] text-[var(--status-offline-fg)] border-current/25"
}
function routeTypeClass(type: OspfRoute["type"]) {
@@ -573,8 +573,10 @@ function NodeDetailPanel({
<span className="size-2 rounded-full shrink-0" style={{ background: theme.stroke }} />
<span className="text-xs font-mono font-semibold" style={{ color: theme.stroke }}>{panelState}</span>
<div className="flex gap-1.5 ml-auto">
{fullCnt > 0 && <span className="text-[10px] px-1.5 py-0.5 rounded-full border border-emerald-500/25 bg-emerald-500/10 text-emerald-400 font-medium">Full ×{fullCnt}</span>}
{partCnt > 0 && <span className="text-[10px] px-1.5 py-0.5 rounded-full border border-amber-500/25 bg-amber-500/10 text-amber-400 font-medium">2-Way ×{partCnt}</span>}
{fullCnt > 0 && <span className="text-[10px] px-1.5 py-0.5 rounded-full border border-current/25 font-medium"
style={{ background: "var(--status-online-bg)", color: "var(--status-online-fg)" }}>Full ×{fullCnt}</span>}
{partCnt > 0 && <span className="text-[10px] px-1.5 py-0.5 rounded-full border border-current/25 font-medium"
style={{ background: "var(--status-degraded-bg)", color: "var(--status-degraded-fg)" }}>2-Way ×{partCnt}</span>}
</div>
</div>
@@ -630,7 +632,9 @@ function InterfacesTab({ items: initialItems, isLive }: { items: OspfItem[]; isL
const [toast, setToast] = useState<string | null>(null)
// Sync with live data when it changes
useEffect(() => { setItems(initialItems) }, [initialItems])
useEffect(() => {
queueMicrotask(() => setItems(initialItems))
}, [initialItems])
function showToast(msg: string) { setToast(msg); setTimeout(() => setToast(null), 2500) }
@@ -708,7 +712,8 @@ function InterfacesTab({ items: initialItems, isLive }: { items: OspfItem[]; isL
</div>
{toast && (
<div className="flex items-center gap-2 rounded-lg border border-emerald-500/30 bg-emerald-500/10 px-4 py-2.5 text-sm text-emerald-600 dark:text-emerald-400">
<div className="flex items-center gap-2 rounded-lg border border-current/20 px-4 py-2.5 text-sm"
style={{ background: "var(--status-online-bg)", color: "var(--status-online-fg)" }}>
<CheckIcon className="size-4 shrink-0" />{toast}
</div>
)}
@@ -760,7 +765,7 @@ function InterfacesTab({ items: initialItems, isLive }: { items: OspfItem[]; isL
dragging !== item.key && dragOver !== item.key && "hover:bg-muted/40",
)}>
<GripVerticalIcon className={cn("size-3.5 shrink-0", isLive ? "text-muted-foreground/10" : "text-muted-foreground/30")} />
<span className={cn("inline-block size-1.5 rounded-full shrink-0", item.active ? "bg-emerald-500" : "bg-muted-foreground/40")} />
<span className={cn("inline-block size-1.5 rounded-full shrink-0", item.active ? "bg-[var(--status-online)]" : "bg-muted-foreground/40")} />
<code className="text-xs font-mono flex-1 min-w-0 truncate">{item.interfaceName}</code>
{item.type && item.type !== "broadcast" && (
<Chip color="bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20">{item.type}</Chip>
@@ -828,8 +833,8 @@ function NeighborsTab({
<div className="grid grid-cols-3 gap-3">
{[
{ label: "Всего соседей", value: neighbors.length, color: "" },
{ label: "Full", value: fullCount, color: "text-emerald-600 dark:text-emerald-400" },
{ label: "Не Full", value: neighbors.length - fullCount, color: neighbors.length - fullCount > 0 ? "text-amber-500" : "text-muted-foreground" },
{ label: "Full", value: fullCount, color: "text-[var(--status-online-fg)]" },
{ label: "Не Full", value: neighbors.length - fullCount, color: neighbors.length - fullCount > 0 ? "text-[var(--status-degraded-fg)]" : "text-muted-foreground" },
].map(s => (
<Card key={s.label}>
<CardContent className="pt-4 pb-3 px-4">
@@ -861,7 +866,7 @@ function NeighborsTab({
? "bg-black/60 border-white/15 text-white/70 hover:text-white"
: "bg-black/60 border-white/10 text-white/30 hover:text-white/60",
)}>
<span className={cn("size-1.5 rounded-full", showDots ? "bg-emerald-400" : "bg-white/20")} />
<span className={cn("size-1.5 rounded-full", showDots ? "bg-[var(--status-online)]" : "bg-white/20")} />
Анимация
</button>
{!selectedId && (
@@ -1024,9 +1029,9 @@ function BfdTab({ sessions }: { sessions: BfdSession[] }) {
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{[
{ label: "Сессий BFD", value: sessions.length, color: "" },
{ label: "Up", value: upCount, color: "text-emerald-600 dark:text-emerald-400" },
{ label: "Down / Admin", value: downCount, color: downCount > 0 ? "text-red-500" : "text-muted-foreground" },
{ label: "Init / другие", value: initCount, color: initCount > 0 ? "text-amber-500" : "text-muted-foreground" },
{ label: "Up", value: upCount, color: "text-[var(--status-online-fg)]" },
{ label: "Down / Admin", value: downCount, color: downCount > 0 ? "text-[var(--status-offline-fg)]" : "text-muted-foreground" },
{ label: "Init / другие", value: initCount, color: initCount > 0 ? "text-[var(--status-degraded-fg)]" : "text-muted-foreground" },
].map(s => (
<Card key={s.label}>
<CardContent className="pt-4 pb-3 px-4">
@@ -1094,7 +1099,7 @@ function BfdTab({ sessions }: { sessions: BfdSession[] }) {
{b.packetsTx.toLocaleString()}
</td>
<td className="px-3 py-2.5 font-mono tabular-nums text-center">
<span className={cn(b.stateChanges > 3 ? "text-amber-500" : "text-muted-foreground")}>
<span className={cn(b.stateChanges > 3 ? "text-[var(--status-degraded-fg)]" : "text-muted-foreground")}>
{b.stateChanges}
</span>
</td>
@@ -1142,22 +1147,31 @@ export default function OspfPage() {
const [fetchTick, setFetchTick] = useState(0)
// Reset server filter when switching live ↔ mock
useEffect(() => { setFilterServerId("all") }, [isLive])
useEffect(() => {
queueMicrotask(() => setFilterServerId("all"))
}, [isLive])
useEffect(() => {
if (!isLive) { setLiveData(null); return }
if (!isLive) {
queueMicrotask(() => setLiveData(null))
return
}
let cancelled = false
setLoading(true); setLiveError(null)
fetch(`${backendUrl}/api/ospf/all`)
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json() as Promise<BackendOspfAll> })
.then(data => {
if (cancelled) return
setLiveData(data); setFetchedAt(new Date()); setLoading(false)
})
.catch((err: unknown) => {
if (cancelled) return
setLiveError(err instanceof Error ? err.message : String(err)); setLoading(false)
})
queueMicrotask(() => {
if (cancelled) return
setLoading(true)
setLiveError(null)
fetch(`${backendUrl}/api/ospf/all`)
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json() as Promise<BackendOspfAll> })
.then(data => {
if (cancelled) return
setLiveData(data); setFetchedAt(new Date()); setLoading(false)
})
.catch((err: unknown) => {
if (cancelled) return
setLiveError(err instanceof Error ? err.message : String(err)); setLoading(false)
})
})
return () => { cancelled = true }
}, [isLive, backendUrl, fetchTick])