fix(ui): выбирать неактивные серверы для месячного отчёта

This commit is contained in:
Denozordec committed 2026-09-28 16:59:40 +07:00
1 parent 7dd377d7c3
commit 460cf67313
6 files changed
+76 -33

No files matched your search

+27 -12
View File
@@ -797,7 +797,7 @@ export default function TrafficPage() {
const [flowScope, setFlowScope] = useState<FlowScope>("servers")
const [flowRetry, setFlowRetry] = useState(0)
const flowKey = JSON.stringify([isLive, groupMode, backendUrl, range, flowScope, flowRetry])
const [flowResult, setFlowResult] = useState<{ key: string; receivedAt?: number; stats?: FlowStatsDto; exporters?: FlowEntityCard[]; clients?: FlowEntityCard[]; error?: string } | null>(null)
const [flowResult, setFlowResult] = useState<{ key: string; receivedAt?: number; monthlyServers?: ServerRead[]; stats?: FlowStatsDto; exporters?: FlowEntityCard[]; clients?: FlowEntityCard[]; error?: string } | null>(null)
const currentFlows = flowResult?.key === flowKey ? flowResult : null
const liveBusy = isLive && groupMode === "flows" && !currentFlows
const liveError = currentFlows?.error
@@ -879,10 +879,20 @@ export default function TrafficPage() {
useEffect(() => {
if (!isLive || effectiveMode !== "flows") return
let cancelled = false
if (range === "30d") {
void listServers(backendUrl).then(servers => {
if (cancelled) return
setFlowResult({ key: flowKey, receivedAt: Date.now(), monthlyServers: servers })
setSelectedId(prev => servers.some(server => String(server.id) === prev) ? prev : String(servers[0]?.id ?? ""))
}).catch(error => {
if (!cancelled) setFlowResult({ key: flowKey, error: error instanceof Error ? error.message : "Не удалось загрузить каталог серверов" })
})
return () => { cancelled = true }
}
void Promise.all([
getTrafficFlows(backendUrl, range === "30d" ? "24h" : range),
getFlowExporters(backendUrl, range === "30d" ? "24h" : range),
getFlowClients(backendUrl, range === "30d" ? "24h" : range),
getTrafficFlows(backendUrl, range),
getFlowExporters(backendUrl, range),
getFlowClients(backendUrl, range),
]).then(([stats, exporters, clients]) => {
if (cancelled) return
setFlowResult({ key: flowKey, receivedAt: Date.now(), stats, exporters: exporters.exporters, clients: clients.clients })
@@ -1049,7 +1059,12 @@ export default function TrafficPage() {
return sortDir === "desc" ? -v : v
})
}, [flowCards, q, sortField, sortDir])
const selFlowCard = sortedFlowCards.find((c) => c.id === selectedId) ?? sortedFlowCards[0] ?? null
const monthlyServers = currentFlows?.monthlyServers ?? []
const visibleMonthlyServers = monthlyServers.filter(server => !q || server.name.toLowerCase().includes(q)).sort((a, b) => a.name.localeCompare(b.name))
const monthlyServer = monthlyServers.find(server => String(server.id) === selectedId)
const selFlowCard = range === "30d"
? (monthlyServer ? { id: String(monthlyServer.id), name: monthlyServer.name, country: monthlyServer.country, site: monthlyServer.site } : null)
: flowCards.find((c) => c.id === selectedId) ?? null
const displayedFlow = currentFlows && !liveError ? (flowLiveSample ?? currentAnalytics?.data ?? null) : null
const selServer = useMemo(() => activeServerTraffic.find(s => s.id === selectedId) ?? activeServerTraffic[0], [selectedId, activeServerTraffic])
@@ -1242,7 +1257,7 @@ export default function TrafficPage() {
className="w-full pl-8 pr-3 h-8 text-xs bg-muted/50 border border-border rounded-md outline-none focus:border-primary transition-colors"
/>
</div>
<div className="flex gap-1 flex-wrap">
<div className={cn("flex gap-1 flex-wrap", range === "30d" && "hidden")}>
<span className="text-[10px] text-muted-foreground self-center mr-0.5">Сортировка:</span>
{visibleSortFields.map(({ field, label }) => (
<button
@@ -1260,9 +1275,9 @@ export default function TrafficPage() {
</button>
))}
</div>
{range === "30d" && <p className="text-xs text-muted-foreground">Выбор сервера: активность за последние 24 часа. Месячный отчёт — в деталях сервера.</p>}
{range === "30d" && <p className="text-xs text-muted-foreground">Серверы из каталога, включая неактивные. Удалённые серверы в список не входят.</p>}
<div className="flex flex-col gap-2">
{sortedFlowCards.map((card) => (
{range === "30d" ? visibleMonthlyServers.map(server => <Button key={server.id} variant="outline" className="justify-start whitespace-normal break-all h-auto py-2" aria-pressed={String(server.id) === selectedId} onClick={() => setSelectedId(String(server.id))}>{server.name}</Button>) : sortedFlowCards.map((card) => (
<FlowEntityCardView
key={card.id}
card={card}
@@ -1270,9 +1285,9 @@ export default function TrafficPage() {
onClick={() => { setFlowIface("__all__"); setSelectedId(card.id) }}
/>
))}
{!liveBusy && !liveError && sortedFlowCards.length === 0 ? (
{!liveBusy && !liveError && (range === "30d" ? visibleMonthlyServers.length : sortedFlowCards.length) === 0 ? (
<p className="text-xs text-muted-foreground">
{flowEmptyHint(flowStats, collectorAlive) ?? "Нет экспортёров IPFIX. Подключите jump-host."}
{range === "30d" ? (q ? "Нет серверов по этому запросу" : "В каталоге нет серверов") : flowEmptyHint(flowStats, collectorAlive) ?? "Нет экспортёров IPFIX. Подключите jump-host."}
</p>
) : null}
</div>
@@ -1281,11 +1296,11 @@ export default function TrafficPage() {
<FramePanel className="flex-1 min-w-0 p-3 sm:p-5">
{analyticsLoading && <p role="status">Загрузка аналитики…</p>}
{currentAnalytics?.error && <Alert variant="destructive" role="alert"><AlertTitle>Аналитика недоступна</AlertTitle><AlertDescription>{currentAnalytics.error}<Button size="sm" variant="outline" onClick={() => setAnalyticsRetry(value => value + 1)}>Повторить аналитику</Button></AlertDescription></Alert>}
{isLive && (displayedFlow || currentAnalytics?.monthly) && <TrafficDataTime label="Получение аналитики потоков" receivedAt={flowLiveSample ? flowLiveReceivedAt : currentAnalytics?.receivedAt} source={flowLiveSample ? "live-поток" : "API"} />}
{isLive && (displayedFlow || (monthlyServer && currentAnalytics?.monthly)) && <TrafficDataTime label="Получение аналитики потоков" receivedAt={flowLiveSample ? flowLiveReceivedAt : currentAnalytics?.receivedAt} source={flowLiveSample ? "live-поток" : "API"} />}
<FlowAnalyticsDetail
card={selFlowCard}
analytics={displayedFlow}
monthly={currentAnalytics?.monthly}
monthly={monthlyServer ? currentAnalytics?.monthly : undefined}
monthlyAvailable={flowScope === "servers"}
month={flowMonth}
onMonth={setFlowMonth}