fix(ui): сохранять и проверять контекст отчёта статистики
This commit is contained in:
1 parent
94ad73212b
commit
e05b3a38c8
5 files changed
+117
-20
No files matched your search
@@ -33,6 +33,7 @@ import {
|
||||
isStatisticsSliceKind,
|
||||
STATISTICS_DIMS,
|
||||
} from "@/lib/statistics-dims"
|
||||
import { statisticsContextError, STATISTICS_CONTEXT_KEYS } from "@/lib/statistics-context"
|
||||
import { readStatisticsRange } from "@/lib/statistics-range"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { fmtBps, formatBytes } from "@/lib/fmt-rate"
|
||||
@@ -231,6 +232,7 @@ function StatisticsPageInner() {
|
||||
const isLive = mode === "live"
|
||||
|
||||
const { range, error: rangeError } = useMemo(() => readStatisticsRange(searchParams, rangeForPreset("7d")), [searchParams])
|
||||
const paramsError = useMemo(() => statisticsContextError(searchParams), [searchParams])
|
||||
const slices = useMemo(() => readSlices(searchParams), [searchParams])
|
||||
const dim = useMemo(() => readDim(searchParams), [searchParams])
|
||||
const view = useMemo(() => readView(searchParams), [searchParams])
|
||||
@@ -239,17 +241,17 @@ function StatisticsPageInner() {
|
||||
const pivotCol = useMemo(() => readPivotDim(searchParams, "pivotCol", "service"), [searchParams])
|
||||
|
||||
const [retry, setRetry] = useState(0)
|
||||
const requestKey = JSON.stringify({ backendUrl, isLive, range, slices, view, pivotRow, pivotCol, planes, retry, rangeError })
|
||||
const requestKey = JSON.stringify({ backendUrl, isLive, range, slices, view, pivotRow, pivotCol, planes, retry, rangeError, paramsError })
|
||||
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 && !rangeError && current === null
|
||||
const error = rangeError ?? current?.error ?? null
|
||||
const loading = isLive && !rangeError && !paramsError && current === null
|
||||
const error = rangeError ?? paramsError ?? current?.error ?? null
|
||||
const dataReady = isLive && current !== null && !error
|
||||
const pivot = dataReady ? current.pivot : EMPTY_PIVOT
|
||||
|
||||
const replaceParams = useCallback(
|
||||
const updateParams = useCallback(
|
||||
(patch: Record<string, string | undefined>) => {
|
||||
const sp = new URLSearchParams(window.location.search)
|
||||
for (const [k, v] of Object.entries(patch)) {
|
||||
@@ -258,21 +260,21 @@ function StatisticsPageInner() {
|
||||
}
|
||||
const qs = sp.toString()
|
||||
if (qs === searchParams.toString()) return
|
||||
window.history.replaceState(null, "", qs ? `/statistics?${qs}` : "/statistics")
|
||||
window.history.pushState(null, "", (qs ? `/statistics?${qs}` : "/statistics") + window.location.hash)
|
||||
},
|
||||
[searchParams],
|
||||
)
|
||||
|
||||
const setRange = useCallback(
|
||||
(next: DateRangeYmd) => {
|
||||
replaceParams({ from: next.from, to: next.to })
|
||||
updateParams({ from: next.from, to: next.to })
|
||||
},
|
||||
[replaceParams],
|
||||
[updateParams],
|
||||
)
|
||||
|
||||
const setSlices = useCallback(
|
||||
(next: CubeSlices) => {
|
||||
replaceParams({
|
||||
updateParams({
|
||||
country: next.country,
|
||||
service: next.service,
|
||||
asn: next.asn,
|
||||
@@ -281,11 +283,11 @@ function StatisticsPageInner() {
|
||||
iface: next.iface,
|
||||
})
|
||||
},
|
||||
[replaceParams],
|
||||
[updateParams],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!prefsHydrated || !isLive || rangeError) return
|
||||
if (!prefsHydrated || !isLive || rangeError || paramsError) return
|
||||
let cancelled = false
|
||||
void (async () => {
|
||||
try {
|
||||
@@ -311,7 +313,7 @@ function StatisticsPageInner() {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [backendUrl, isLive, prefsHydrated, range, slices, view, pivotRow, pivotCol, planes, requestKey, rangeError])
|
||||
}, [backendUrl, isLive, prefsHydrated, range, slices, view, pivotRow, pivotCol, planes, requestKey, rangeError, paramsError])
|
||||
|
||||
const viewData = dataReady ? current.data : EMPTY
|
||||
const sliced = hasAnySlice(slices)
|
||||
@@ -327,7 +329,7 @@ function StatisticsPageInner() {
|
||||
if (rowId === "__other__" || colId === "__other__") return
|
||||
let next = applyPivotDim(slices, pivotRow, rowId)
|
||||
next = applyPivotDim(next, pivotCol, colId)
|
||||
replaceParams({
|
||||
updateParams({
|
||||
country: next.country,
|
||||
service: next.service,
|
||||
asn: next.asn,
|
||||
@@ -366,8 +368,8 @@ function StatisticsPageInner() {
|
||||
|
||||
{error ? (
|
||||
<Alert variant="destructive" role="alert">
|
||||
<AlertTitle>{rangeError ? "Некорректный период" : "Ошибка загрузки"}</AlertTitle>
|
||||
<AlertDescription>{error}{!rangeError && <Button type="button" variant="outline" size="sm" onClick={() => setRetry(n => n + 1)}>Повторить загрузку</Button>}</AlertDescription>
|
||||
<AlertTitle>{rangeError ? "Некорректный период" : paramsError ? "Параметры отчёта требуют внимания" : "Ошибка загрузки"}</AlertTitle>
|
||||
<AlertDescription>{error}{!rangeError && !paramsError && <Button type="button" variant="outline" size="sm" onClick={() => setRetry(n => n + 1)}>Повторить загрузку</Button>}{paramsError && <Button type="button" variant="outline" size="sm" onClick={() => updateParams(Object.fromEntries(STATISTICS_CONTEXT_KEYS.map(key => [key, undefined])))}>Сбросить параметры отчёта</Button>}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
@@ -438,7 +440,7 @@ function StatisticsPageInner() {
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<SegmentedControl
|
||||
value={view}
|
||||
onChange={(next) => replaceParams({ view: next === "pivot" ? "pivot" : "explore" })}
|
||||
onChange={(next) => updateParams({ view: next === "pivot" ? "pivot" : "explore" })}
|
||||
options={[
|
||||
{ value: "explore", label: "Разрез" },
|
||||
{ value: "pivot", label: "Сводка" },
|
||||
@@ -446,7 +448,7 @@ function StatisticsPageInner() {
|
||||
/>
|
||||
<SegmentedControl
|
||||
value={planes}
|
||||
onChange={(next) => replaceParams({ planes: next === "all" ? "all" : undefined })}
|
||||
onChange={(next) => updateParams({ planes: next === "all" ? "all" : undefined })}
|
||||
options={[
|
||||
{ value: "unique", label: "Уникальный" },
|
||||
{ value: "all", label: "Все плоскости" },
|
||||
@@ -456,7 +458,7 @@ function StatisticsPageInner() {
|
||||
<DimensionSelect
|
||||
label="Критерий"
|
||||
value={dim}
|
||||
onChange={(next) => replaceParams({ dim: next })}
|
||||
onChange={(next) => updateParams({ dim: next, tab: undefined })}
|
||||
/>
|
||||
) : null}
|
||||
{view === "pivot" ? (
|
||||
@@ -465,13 +467,13 @@ function StatisticsPageInner() {
|
||||
label="Строки"
|
||||
value={pivotRow}
|
||||
exclude={pivotCol}
|
||||
onChange={(next) => replaceParams({ pivotRow: next })}
|
||||
onChange={(next) => updateParams({ pivotRow: next })}
|
||||
/>
|
||||
<PivotDimSelect
|
||||
label="Колонки"
|
||||
value={pivotCol}
|
||||
exclude={pivotRow}
|
||||
onChange={(next) => replaceParams({ pivotCol: next })}
|
||||
onChange={(next) => updateParams({ pivotCol: next })}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
@@ -489,7 +491,7 @@ function StatisticsPageInner() {
|
||||
}}
|
||||
/>
|
||||
{isLive && !dataReady ? (
|
||||
<div role="status" className="p-6 text-sm text-muted-foreground">{loading ? "Загрузка статистики…" : rangeError ? "Выберите корректный период для загрузки статистики." : "Статистика недоступна. Повторите загрузку."}</div>
|
||||
<div role="status" className="p-6 text-sm text-muted-foreground">{loading ? "Загрузка статистики…" : rangeError || paramsError ? "Исправьте параметры отчёта для загрузки статистики." : "Статистика недоступна. Повторите загрузку."}</div>
|
||||
) : emptyCube ? (
|
||||
<EmptyState
|
||||
title="Нет данных куба"
|
||||
|
||||
Reference in new issue
Block a user