fix(ui): отделить ошибки статистики от пустых данных

This commit is contained in:
Denozordec committed 2026-09-23 20:22:18 +07:00
1 parent 3898881d8a
commit 167e19cd91
4 files changed
+135 -25

No files matched your search

+28 -24
View File
@@ -9,6 +9,7 @@ import {
ServerIcon,
UsersIcon,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { PageHeader } from "@/components/page-header"
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
import { DataPageCard } from "@/components/data-page-card"
@@ -243,10 +244,16 @@ function StatisticsPageInner() {
const pivotRow = useMemo(() => readPivotDim(searchParams, "pivotRow", "country"), [searchParams])
const pivotCol = useMemo(() => readPivotDim(searchParams, "pivotCol", "service"), [searchParams])
const [data, setData] = useState<StatisticsDto>(EMPTY)
const [pivot, setPivot] = useState<StatisticsPivotDto>(EMPTY_PIVOT)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [retry, setRetry] = useState(0)
const requestKey = JSON.stringify({ backendUrl, isLive, range, slices, view, pivotRow, pivotCol, planes, retry })
const [result, setResult] = useState<{
key: string; data: StatisticsDto; pivot: StatisticsPivotDto; error: string | null
} | null>(null)
const current = result?.key === requestKey ? result : null
const loading = isLive && current === null
const error = current?.error ?? null
const dataReady = isLive && current !== null && !error
const pivot = dataReady ? current.pivot : EMPTY_PIVOT
const replaceParams = useCallback(
(patch: Record<string, string | undefined>) => {
@@ -287,37 +294,32 @@ function StatisticsPageInner() {
if (!prefsHydrated || !isLive) return
let cancelled = false
void (async () => {
setLoading(true)
setError(null)
try {
const query = toQuery(range, slices, planes)
const dto = await getStatistics(backendUrl, query)
if (!cancelled) setData(dto)
if (cancelled) return
let matrix = EMPTY_PIVOT
if (view === "pivot" && pivotRow !== pivotCol) {
const matrix = await getStatisticsPivot(backendUrl, {
matrix = await getStatisticsPivot(backendUrl, {
...query,
row: pivotRow,
col: pivotCol,
metric: "bytes",
})
if (!cancelled) setPivot(matrix)
}
if (!cancelled) setResult({ key: requestKey, data: dto, pivot: matrix, error: null })
} catch (e: unknown) {
if (!cancelled) {
setData(EMPTY)
setPivot(EMPTY_PIVOT)
setError(e instanceof Error ? e.message : "Не удалось загрузить статистику")
setResult({ key: requestKey, data: EMPTY, pivot: EMPTY_PIVOT, error: e instanceof Error ? e.message : "Не удалось загрузить статистику" })
}
} finally {
if (!cancelled) setLoading(false)
}
})()
return () => {
cancelled = true
}
}, [backendUrl, isLive, prefsHydrated, range, slices, view, pivotRow, pivotCol, planes])
}, [backendUrl, isLive, prefsHydrated, range, slices, view, pivotRow, pivotCol, planes, requestKey])
const viewData = isLive ? data : EMPTY
const viewData = dataReady ? current.data : EMPTY
const sliced = hasAnySlice(slices)
const emptyCube = !isLive || (!loading && viewData.kpis.bytes === 0 && viewData.interfaces.length === 0)
@@ -369,13 +371,13 @@ function StatisticsPageInner() {
) : null}
{error ? (
<Alert variant="destructive">
<Alert variant="destructive" role="alert">
<AlertTitle>Ошибка загрузки</AlertTitle>
<AlertDescription>{error}</AlertDescription>
<AlertDescription>{error}<Button type="button" variant="outline" size="sm" onClick={() => setRetry(n => n + 1)}>Повторить загрузку</Button></AlertDescription>
</Alert>
) : null}
{!slices.serverId && isLive && !emptyCube ? (
{!slices.serverId && dataReady && !emptyCube ? (
<Alert>
<AlertTitle>Уникальный объём</AlertTitle>
<AlertDescription>
@@ -384,7 +386,7 @@ function StatisticsPageInner() {
</Alert>
) : null}
<KpiStatGrid
{(dataReady || loading) && <KpiStatGrid
aria-label="Сводка трафика"
isLoading={loading}
skeletonCount={5}
@@ -432,9 +434,9 @@ function StatisticsPageInner() {
iconClassName: "text-muted-foreground",
},
]}
/>
/>}
<StatisticsVolumeChart series={viewData.series} grain={viewData.grain} />
{dataReady && <StatisticsVolumeChart series={viewData.series} grain={viewData.grain} />}
<DataPageCard>
<DataPageToolbar
@@ -482,7 +484,7 @@ function StatisticsPageInner() {
</div>
}
filterControl={<StatisticsFilters key={JSON.stringify(slices)} slices={slices} onApply={setSlices} />}
countLabel={countLabel}
countLabel={dataReady ? countLabel : undefined}
/>
<SliceChips
chips={chips}
@@ -492,7 +494,9 @@ function StatisticsPageInner() {
setSlices(next)
}}
/>
{emptyCube ? (
{isLive && !dataReady ? (
<div role="status" className="p-6 text-sm text-muted-foreground">{loading ? "Загрузка статистики…" : "Статистика недоступна. Повторите загрузку."}</div>
) : emptyCube ? (
<EmptyState
title="Нет данных куба"
description="За выбранный период нет IPFIX-фактов. Куб заполняется с момента деплоя, без бэкфилла за год."