fix(scheduler): исключить поздние ответы после смены фильтра и источника
This commit is contained in:
1 parent
1b51d2af01
commit
933a4eaf57
5 files changed
+79
-41
No files matched your search
@@ -1,9 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { FormToggle } from "@/components/form-kit"
|
||||
import { Badge } from "@/components/reui/badge"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
|
||||
@@ -554,6 +553,12 @@ function applySchedulerJobsToDrafts(
|
||||
}
|
||||
|
||||
export default function DataCollectionPage() {
|
||||
const { mode, backendUrl, prefsHydrated } = useDataSource()
|
||||
if (!prefsHydrated) return <p role="status" className="p-6 text-sm text-muted-foreground">Загрузка настроек источника…</p>
|
||||
return <DataCollectionContent key={JSON.stringify([mode, backendUrl])} />
|
||||
}
|
||||
|
||||
function DataCollectionContent() {
|
||||
const { mode, backendUrl, prefsHydrated } = useDataSource()
|
||||
const isLive = prefsHydrated && mode === "live"
|
||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||
@@ -587,6 +592,13 @@ export default function DataCollectionPage() {
|
||||
const [collectorError, setCollectorError] = useState<string | null>(null)
|
||||
const [openRunIds, setOpenRunIds] = useState<Set<string>>(() => new Set())
|
||||
|
||||
const reader = useRef<{ active: boolean; revision: number; controller?: AbortController }>({ active: true, revision: 0 })
|
||||
useEffect(() => {
|
||||
reader.current.active = true
|
||||
const state = reader.current
|
||||
return () => { state.active = false; state.revision++; state.controller?.abort() }
|
||||
}, [])
|
||||
|
||||
const toggleRunOpen = useCallback((id: string, open: boolean) => {
|
||||
setOpenRunIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
@@ -602,7 +614,12 @@ export default function DataCollectionPage() {
|
||||
}, [uptimeCollector?.scheduler?.jobs])
|
||||
|
||||
const loadCollectors = useCallback(async () => {
|
||||
if (!isLive) return
|
||||
if (!isLive || !reader.current.active) return
|
||||
const revision = ++reader.current.revision
|
||||
reader.current.controller?.abort()
|
||||
const controller = new AbortController(); reader.current.controller = controller
|
||||
const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(30_000)])
|
||||
const current = () => reader.current.active && revision === reader.current.revision && !controller.signal.aborted
|
||||
setCollectorError(null)
|
||||
setPageBusy(true)
|
||||
try {
|
||||
@@ -611,14 +628,15 @@ export default function DataCollectionPage() {
|
||||
? `?limit=80&jobKey=${encodeURIComponent(runFilterJobKey)}`
|
||||
: "?limit=80"
|
||||
const [trafficRes, serversApiRes, uptimeRes, internetPathRes, certRenewRes, runsRes] = await Promise.allSettled([
|
||||
apiFetch<CollectorSettingsDto>("/api/traffic/settings"),
|
||||
apiFetch<CollectorSettingsDto>("/api/servers-api-ping/settings"),
|
||||
apiFetch<UptimeSettingsDto>("/api/uptime/settings"),
|
||||
apiFetch<CollectorSettingsDto>("/api/internet-path/settings"),
|
||||
apiFetch<{ enabled: boolean; intervalSec: number; renewBeforeDays: number }>("/api/certificates/renew-settings"),
|
||||
apiFetch<{ runs: SchedulerRunRowDto[] }>(`/api/scheduler/runs${runsQuery}`),
|
||||
apiFetch<CollectorSettingsDto>("/api/traffic/settings", { signal }),
|
||||
apiFetch<CollectorSettingsDto>("/api/servers-api-ping/settings", { signal }),
|
||||
apiFetch<UptimeSettingsDto>("/api/uptime/settings", { signal }),
|
||||
apiFetch<CollectorSettingsDto>("/api/internet-path/settings", { signal }),
|
||||
apiFetch<{ enabled: boolean; intervalSec: number; renewBeforeDays: number }>("/api/certificates/renew-settings", { signal }),
|
||||
apiFetch<{ runs: SchedulerRunRowDto[] }>(`/api/scheduler/runs${runsQuery}`, { signal }),
|
||||
])
|
||||
|
||||
if (!current()) return
|
||||
const loadErrors = collectSettledErrors(
|
||||
[trafficRes, serversApiRes, uptimeRes, internetPathRes, certRenewRes, runsRes],
|
||||
["трафик", "серверы REST API", "uptime", "internet path", "сертификаты", "журнал планировщика"],
|
||||
@@ -684,9 +702,9 @@ export default function DataCollectionPage() {
|
||||
setSchedulerRuns(runsPayload.runs ?? [])
|
||||
}
|
||||
} catch (e) {
|
||||
setCollectorError(e instanceof Error ? e.message : "Не удалось загрузить данные")
|
||||
if (current()) setCollectorError(e instanceof Error ? e.message : "Не удалось загрузить данные")
|
||||
} finally {
|
||||
setPageBusy(false)
|
||||
if (current()) setPageBusy(false)
|
||||
}
|
||||
}, [apiFetch, isLive, runFilterJobKey])
|
||||
|
||||
@@ -823,21 +841,10 @@ export default function DataCollectionPage() {
|
||||
}, [loadCollectors, persistSchedulerDrafts])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLive) {
|
||||
setTrafficCollector(null)
|
||||
setServersApiCollector(null)
|
||||
setUptimeCollector(null)
|
||||
setInternetPathCollector(null)
|
||||
setSchedulerRuns([])
|
||||
return
|
||||
}
|
||||
void loadCollectors()
|
||||
if (!isLive) return
|
||||
queueMicrotask(() => { void loadCollectors() })
|
||||
}, [isLive, loadCollectors])
|
||||
|
||||
useEffect(() => {
|
||||
setOpenRunIds(new Set())
|
||||
}, [runFilterJobKey])
|
||||
|
||||
const handleRefreshScheduler = async () => {
|
||||
if (!isLive) return
|
||||
setCollectorError(null)
|
||||
@@ -1005,8 +1012,7 @@ export default function DataCollectionPage() {
|
||||
|
||||
const errorRunsInView = useMemo(() => schedulerRuns.filter((r) => r.status === "error").length, [schedulerRuns])
|
||||
|
||||
const stats = useMemo(
|
||||
() => [
|
||||
const stats = [
|
||||
{
|
||||
label: "Включено задач",
|
||||
value: `${enabledJobsCount} / ${schedulerJobCount}`,
|
||||
@@ -1039,18 +1045,7 @@ export default function DataCollectionPage() {
|
||||
sub: errorRunsInView ? `${errorRunsInView} с ошибкой` : "ошибок в показанных — нет",
|
||||
icon: <DatabaseIcon className="size-4" />,
|
||||
},
|
||||
],
|
||||
[
|
||||
enabledJobsCount,
|
||||
schedulerJobCount,
|
||||
runningJobsCount,
|
||||
trafficCollector?.lastCollectedAt,
|
||||
trafficCollector?.lastDurationMs,
|
||||
trafficCollector?.lastError,
|
||||
schedulerRuns.length,
|
||||
errorRunsInView,
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
@@ -1275,7 +1270,7 @@ export default function DataCollectionPage() {
|
||||
id="run-filter"
|
||||
className="h-8 rounded-md border border-input bg-background px-2 text-xs min-w-[180px]"
|
||||
value={runFilterJobKey}
|
||||
onChange={(e) => setRunFilterJobKey(e.target.value)}
|
||||
onChange={(e) => { setRunFilterJobKey(e.target.value); setOpenRunIds(new Set()) }}
|
||||
>
|
||||
<option value="">Все</option>
|
||||
{SCHEDULER_JOB_KEYS.map((k) => (
|
||||
|
||||
Reference in new issue
Block a user