fix(ui): сохранять каталог устройств диагностики при отказе
This commit is contained in:
1 parent
6fa8e5bfd1
commit
cba3db1508
7 files changed
+78
-46
No files matched your search
+32
-43
@@ -20,7 +20,7 @@ import { Input } from "@/components/ui/input"
|
||||
import { ServerRailLayout, ServerRailMobileButton } from "@/components/server-rail-layout"
|
||||
import { type ServerTileItem } from "@/components/server-tile-rail"
|
||||
import { servers, greTunnels, type GreTunnel, type Server } from "@/lib/data"
|
||||
import { readProbeOutput } from "@/lib/probe-response"
|
||||
import { readProbeOutput, probeServersSchema } from "@/lib/probe-response"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import {
|
||||
PlayIcon, SquareIcon, CopyIcon, Trash2Icon, PlusIcon,
|
||||
@@ -94,18 +94,6 @@ function parseProbeOutput(text: string): OutputLine[] {
|
||||
return text.split("\n").map(t => ({ text: t || " ", kind: inferProbeLineKind(t) }))
|
||||
}
|
||||
|
||||
interface BackendServerRow {
|
||||
id: number
|
||||
name: string
|
||||
host: string
|
||||
site: string
|
||||
country: string
|
||||
asn: string
|
||||
type: Server["type"]
|
||||
enabled: boolean
|
||||
status?: Server["status"]
|
||||
latency?: number | null
|
||||
}
|
||||
|
||||
// SpeedProbeApiRow imported from probes-speed-probes-data-grid
|
||||
|
||||
@@ -589,6 +577,9 @@ function ProbesContent() {
|
||||
|
||||
const [liveServers, setLiveServers] = useState<Server[]>([])
|
||||
const [liveLoad, setLiveLoad] = useState<"idle" | "loading" | "error">(() => (isLive ? "loading" : "idle"))
|
||||
const [catalogError, setCatalogError] = useState<string | null>(null)
|
||||
const [catalogRevision, setCatalogRevision] = useState(0)
|
||||
const [catalogReadAt, setCatalogReadAt] = useState<number | null>(null)
|
||||
const [greByServer, setGreByServer] = useState<Record<string, GreTunnel[]>>({})
|
||||
|
||||
const [tool, setTool] = useState<DiagTool>("ping")
|
||||
@@ -609,36 +600,31 @@ function ProbesContent() {
|
||||
const [rosSrcV4, setRosSrcV4] = useState<string | null | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLive) {
|
||||
queueMicrotask(() => setLiveLoad("idle"))
|
||||
return
|
||||
}
|
||||
setLiveLoad("loading")
|
||||
void apiFetch<BackendServerRow[]>("/api/servers")
|
||||
.then(rows => {
|
||||
const mapped: Server[] = rows.map(s => ({
|
||||
id: String(s.id),
|
||||
name: s.name || s.host,
|
||||
host: s.host,
|
||||
model: "—",
|
||||
os: "—",
|
||||
site: s.site,
|
||||
country: s.country || "UN",
|
||||
asn: s.asn,
|
||||
type: s.type,
|
||||
enabled: s.enabled,
|
||||
status: (s.status ?? "offline") as Server["status"],
|
||||
latency: s.latency != null ? Math.round(s.latency) : null,
|
||||
sessions: 0,
|
||||
}))
|
||||
setLiveServers(mapped)
|
||||
if (!isLive) return
|
||||
let cancelled = false
|
||||
const ctrl = new AbortController()
|
||||
queueMicrotask(() => { if (!cancelled) { setLiveLoad("loading"); setCatalogError(null) } })
|
||||
void apiFetch<unknown>("/api/servers", { signal: ctrl.signal })
|
||||
.then(raw => {
|
||||
const result = probeServersSchema.safeParse(raw)
|
||||
if (!result.success) throw new Error("Некорректный ответ API: каталог устройств не подтверждён.")
|
||||
if (cancelled) return
|
||||
setLiveServers(result.data.map(s => ({
|
||||
id: String(s.id), name: s.name || s.host, host: s.host, model: "—", os: "—",
|
||||
site: s.site ?? "", country: s.country ?? "UN", asn: s.asn ?? "", type: s.type,
|
||||
enabled: s.enabled, status: s.status ?? "unknown",
|
||||
latency: s.latency != null ? Math.round(s.latency) : null, sessions: 0,
|
||||
})))
|
||||
setCatalogReadAt(Date.now())
|
||||
setLiveLoad("idle")
|
||||
})
|
||||
.catch(() => {
|
||||
setLiveServers([])
|
||||
.catch(error => {
|
||||
if (cancelled) return
|
||||
setCatalogError(error instanceof Error ? error.message : "Не удалось прочитать устройства.")
|
||||
setLiveLoad("error")
|
||||
})
|
||||
}, [isLive, apiFetch])
|
||||
return () => { cancelled = true; ctrl.abort() }
|
||||
}, [isLive, apiFetch, catalogRevision])
|
||||
|
||||
const allServers = useMemo(() => {
|
||||
if (!isLive) return servers
|
||||
@@ -921,7 +907,7 @@ function ProbesContent() {
|
||||
|
||||
const running = tests.filter(t => t.status === "running")
|
||||
|
||||
if (isLive && liveLoad === "loading") {
|
||||
if (isLive && liveLoad === "loading" && catalogReadAt === null) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<PageHeader crumbs={[{ label: "Инструменты" }, { label: "Диагностика" }]} />
|
||||
@@ -946,6 +932,7 @@ function ProbesContent() {
|
||||
actions={
|
||||
<>
|
||||
<ServerRailMobileButton />
|
||||
{isLive && <Button type="button" size="sm" variant="outline" aria-label="Обновить устройства диагностики" disabled={liveLoad === "loading"} onClick={() => setCatalogRevision(n => n + 1)}>Обновить</Button>}
|
||||
{running.length > 0
|
||||
? <Button variant="outline" size="sm" onClick={() => {
|
||||
running.forEach(test => stopTest(test.id))
|
||||
@@ -961,11 +948,13 @@ function ProbesContent() {
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
{isLive && liveLoad === "error" && (
|
||||
<div className="rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-2.5 text-xs text-destructive flex items-center gap-2">
|
||||
<AlertCircleIcon className="size-3.5 shrink-0" />
|
||||
Бекенд недоступен — переключитесь в режим «Демо» в настройках источника данных или проверьте URL API.
|
||||
<div role="alert" aria-label="Каталог диагностики" className="rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-2.5 text-sm text-destructive">
|
||||
<p>{catalogReadAt === null ? "Устройства пока неизвестны." : "Не удалось обновить устройства. Последний список сохранён; запуск приостановлен."}</p>
|
||||
<p>{catalogError}</p>
|
||||
</div>
|
||||
)}
|
||||
{isLive && catalogReadAt !== null && <p role="status" className="text-xs text-muted-foreground">Каталог получен: {new Date(catalogReadAt).toLocaleTimeString("ru-RU")}{liveLoad === "loading" ? " · обновляется" : ""}</p>}
|
||||
{isLive && liveLoad === "idle" && allServers.length === 0 && <p role="status">В каталоге нет устройств. Добавьте устройство в разделе «Серверы».</p>}
|
||||
|
||||
{isLive && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
|
||||
Reference in new issue
Block a user