fix(ui): отделить ошибки статистики от пустых данных
This commit is contained in:
1 parent
3898881d8a
commit
167e19cd91
4 files changed
+135
-25
No files matched your search
@@ -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-фактов. Куб заполняется с момента деплоя, без бэкфилла за год."
|
||||
|
||||
Reference in new issue
Block a user