fix(ui): сохранять контекст и свежесть опроса BGP
This commit is contained in:
1 parent
8fdc4b917b
commit
2e17cbe926
8 files changed
+182
-19
No files matched your search
+43
-15
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useMemo, useEffect } from "react"
|
||||
import { Suspense, useState, useMemo, useEffect } from "react"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { DataPageToolbar } from "@/components/data-page-toolbar"
|
||||
import { BgpSessionsDataGrid } from "@/components/data-grids/bgp-sessions-data-grid"
|
||||
@@ -258,7 +259,7 @@ const BGP_FILTER_ACCESSORS = {
|
||||
afi: (s: BgpSession) => s.afi,
|
||||
}
|
||||
|
||||
function SessionsTab({ sessions, emptyTitle }: { sessions: BgpSession[]; emptyTitle?: string }) {
|
||||
function SessionsTab({ sessions, emptyTitle, selectedSessionId, onSelectSession }: { sessions: BgpSession[]; emptyTitle?: string; selectedSessionId: string | null; onSelectSession: (id: string | null) => void }) {
|
||||
const [search, setSearch] = useState("")
|
||||
const [stateFilter, setStateFilter] = useState<StateFilter>("all")
|
||||
const [typeFilter, setTypeFilter] = useState<TypeFilter>("all")
|
||||
@@ -324,7 +325,7 @@ function SessionsTab({ sessions, emptyTitle }: { sessions: BgpSession[]; emptyTi
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<BgpSessionsDataGrid sessions={filtered} emptyTitle={emptyTitle} />
|
||||
<BgpSessionsDataGrid sessions={filtered} emptyTitle={emptyTitle} selectedSessionId={selectedSessionId} onSelectSession={onSelectSession} />
|
||||
</DataPageCard>
|
||||
</div>
|
||||
)
|
||||
@@ -611,14 +612,33 @@ const TABS: Array<{ id: BgpTab; label: string; icon: React.ReactNode }> = [
|
||||
]
|
||||
|
||||
export default function BgpPage() {
|
||||
const { mode, backendUrl } = useDataSource()
|
||||
return <BgpPageContent key={JSON.stringify([mode, backendUrl])} />
|
||||
return <Suspense fallback={<div role="status">Загрузка BGP…</div>}><BgpRoute /></Suspense>
|
||||
}
|
||||
|
||||
function BgpPageContent() {
|
||||
const [activeTab, setActiveTab] = useState<BgpTab>("sessions")
|
||||
const [selectedServerId, setSelectedServerId] = useState(ALL_SERVERS_ID)
|
||||
|
||||
function BgpRoute() {
|
||||
const { mode, backendUrl } = useDataSource()
|
||||
const params = useSearchParams()
|
||||
const router = useRouter()
|
||||
const selectedServerId = params.get("server") ?? ALL_SERVERS_ID
|
||||
const selectedSessionId = params.get("session")
|
||||
const tab = params.get("tab")
|
||||
const activeTab: BgpTab = tab === "routers" || tab === "analytics" ? tab : "sessions"
|
||||
function navigate(values: Record<string, string | null>) {
|
||||
const next = new URLSearchParams(params.toString())
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (value === null) next.delete(key)
|
||||
else next.set(key, value)
|
||||
}
|
||||
router.push("/bgp" + (next.size ? "?" + next.toString() : ""))
|
||||
}
|
||||
return <BgpPageContent key={JSON.stringify([mode, backendUrl])} selectedServerId={selectedServerId} selectedSessionId={selectedSessionId} activeTab={activeTab}
|
||||
onSelectServer={id => navigate({ server: id === ALL_SERVERS_ID ? null : id, session: null })}
|
||||
onSelectSession={id => navigate({ session: id })}
|
||||
onSelectTab={tab => navigate({ tab: tab === "sessions" ? null : tab })} />
|
||||
}
|
||||
function BgpPageContent({ selectedServerId, selectedSessionId, activeTab, onSelectServer, onSelectSession, onSelectTab }: {
|
||||
selectedServerId: string; selectedSessionId: string | null; activeTab: BgpTab;
|
||||
onSelectServer: (id: string) => void; onSelectSession: (id: string | null) => void; onSelectTab: (tab: BgpTab) => void;
|
||||
}) {
|
||||
const { mode, backendUrl } = useDataSource()
|
||||
const isLive = mode === "live"
|
||||
|
||||
@@ -629,6 +649,7 @@ function BgpPageContent() {
|
||||
const [liveError, setLiveError] = useState<string | null>(null)
|
||||
const [failures, setFailures] = useState<BgpFailure[]>([])
|
||||
const [hasLoaded, setHasLoaded] = useState(false)
|
||||
const [readReceipts, setReadReceipts] = useState<Record<string, string>>({})
|
||||
const [fetchTick, setFetchTick] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -660,7 +681,9 @@ function BgpPageContent() {
|
||||
setFailures(catalog.failures)
|
||||
setLiveError(null)
|
||||
setHasLoaded(true)
|
||||
setFetchedAt(new Date())
|
||||
const receivedAt = new Date()
|
||||
setFetchedAt(receivedAt)
|
||||
setReadReceipts(previous => ({ ...Object.fromEntries(Object.entries(previous).filter(([id]) => enabled.has(Number(id)))), ...Object.fromEntries([...enabled].filter(id => !failed.has(String(id))).map(id => [String(id), receivedAt.toISOString()])) }))
|
||||
setLoading(false)
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
@@ -674,7 +697,8 @@ function BgpPageContent() {
|
||||
|
||||
const unknownIds = useMemo(() => new Set(isLive ? (liveError ? liveServers.map(s => s.id) : failures.map(f => String(f.serverId))) : []), [isLive, liveError, liveServers, failures])
|
||||
const allSessions: BgpSession[] = isLive ? liveSessions.map(s => unknownIds.has(s.serverId) ? { ...s, state: "Unknown" } : s) : SESSIONS
|
||||
const incomplete = isLive && (!hasLoaded || Boolean(liveError) || (selectedServerId === ALL_SERVERS_ID ? failures.length > 0 : unknownIds.has(selectedServerId)))
|
||||
const unavailableContext = isLive && hasLoaded && selectedServerId !== ALL_SERVERS_ID && !liveServers.some(s => s.id === selectedServerId)
|
||||
const incomplete = isLive && (unavailableContext || !hasLoaded || Boolean(liveError) || (selectedServerId === ALL_SERVERS_ID ? failures.length > 0 : unknownIds.has(selectedServerId)))
|
||||
const displayServers = isLive ? liveServers : mockServers.filter((s) => s.enabled)
|
||||
|
||||
const effectiveServerId = selectedServerId
|
||||
@@ -711,9 +735,10 @@ function BgpPageContent() {
|
||||
|
||||
return (
|
||||
<ServerRailLayout
|
||||
preserveSelection
|
||||
items={railItems}
|
||||
selectedId={effectiveServerId}
|
||||
onSelect={setSelectedServerId}
|
||||
onSelect={onSelectServer}
|
||||
showAll
|
||||
allCount={displayServers.length}
|
||||
loading={isLive && loading && displayServers.length === 0}
|
||||
@@ -735,7 +760,7 @@ function BgpPageContent() {
|
||||
<div className="border-b bg-background shrink-0">
|
||||
<div className="flex items-center px-6">
|
||||
{TABS.map(t => (
|
||||
<button key={t.id} aria-pressed={activeTab === t.id} onClick={() => setActiveTab(t.id)}
|
||||
<button key={t.id} aria-pressed={activeTab === t.id} onClick={() => onSelectTab(t.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors -mb-px",
|
||||
activeTab === t.id
|
||||
@@ -751,6 +776,8 @@ function BgpPageContent() {
|
||||
>
|
||||
<div className="flex flex-col gap-5">
|
||||
|
||||
{unavailableContext && <Alert variant="warning"><AlertCircleIcon /><AlertTitle>Устройство BGP недоступно в этом каталоге</AlertTitle><AlertDescription>Контекст {selectedServerId} сохранён. Выберите доступное устройство.<Button variant="outline" size="sm" onClick={() => onSelectServer(ALL_SERVERS_ID)}>Все устройства BGP</Button></AlertDescription></Alert>}
|
||||
{selectedSessionId && hasLoaded && !sessions.some(s => s.id === selectedSessionId) && <Alert variant="warning"><AlertCircleIcon /><AlertDescription>Сессия из ссылки отсутствует в выбранном снимке. Контекст ссылки сохранён.<Button variant="outline" size="sm" onClick={() => onSelectSession(null)}>Все сессии BGP</Button></AlertDescription></Alert>}
|
||||
{/* data source banner */}
|
||||
{isLive && loading && (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
@@ -762,6 +789,7 @@ function BgpPageContent() {
|
||||
<div role="group" aria-label="Свежесть BGP" className="text-xs text-muted-foreground">
|
||||
Ответ каталога получен <time dateTime={fetchedAt.toISOString()}>{fetchedAt.toLocaleTimeString("ru")}</time>.
|
||||
Это время получения браузером, не время измерения RouterOS.
|
||||
{displayServers.filter(s => effectiveServerId === ALL_SERVERS_ID || s.id === effectiveServerId).map(s => <p key={s.id}>{s.name}: {readReceipts[s.id] ? <time data-bgp-server={s.id} dateTime={readReceipts[s.id]}>{new Date(readReceipts[s.id]).toLocaleTimeString("ru")}</time> : "успешный опрос не получен"}{unknownIds.has(s.id) ? " · данные устарели" : ""}</p>)}
|
||||
</div>
|
||||
)}
|
||||
{isLive && (liveError || failures.length > 0) && (
|
||||
@@ -830,7 +858,7 @@ function BgpPageContent() {
|
||||
)}
|
||||
|
||||
{/* tab content */}
|
||||
{activeTab === "sessions" && <SessionsTab sessions={sessions} emptyTitle={incomplete && sessions.length === 0 ? "Данные BGP неизвестны" : undefined} />}
|
||||
{activeTab === "sessions" && <SessionsTab sessions={sessions} emptyTitle={incomplete && sessions.length === 0 ? "Данные BGP неизвестны" : undefined} selectedSessionId={selectedSessionId} onSelectSession={onSelectSession} />}
|
||||
{activeTab === "routers" && (incomplete ? <p role="status">Сводка по роутерам неизвестна до успешного опроса. Последние сессии доступны в таблице.</p> : <RoutersTab sessions={sessions} />)}
|
||||
{activeTab === "analytics" && (incomplete ? <p role="status">Аналитика неизвестна до успешного опроса. Последние сессии доступны в таблице.</p> : <AnalyticsTab sessions={sessions} />)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user