fix(ui): сохранять устройство диагностики в URL

This commit is contained in:
Denozordec committed 2026-10-01 08:31:50 +07:00
1 parent 1b91ba4e1d
commit d0c715e93c
7 files changed
+89 -28

No files matched your search

+22 -11
View File
@@ -1,6 +1,8 @@
"use client"
import { useEffect, useRef, useState, useMemo, useCallback } from "react"
import { Suspense, useEffect, useRef, useState, useMemo, useCallback } from "react"
import { useSearchParams } from "next/navigation"
import { readProbeUrl, patchProbeUrl } from "@/lib/probe-url"
import { PageHeader } from "@/components/page-header"
import { FormToggle } from "@/components/form-kit"
import { Frame, FramePanel } from "@/components/reui/frame"
@@ -567,12 +569,23 @@ function ScheduleTab({
// ─── page ─────────────────────────────────────────────────────────────────────
export default function ProbesPage() {
return <Suspense fallback={<p role="status">Загрузка диагностики…</p>}><ProbesRoute /></Suspense>
}
function ProbesRoute() {
const params = useSearchParams()
const url = readProbeUrl(new URLSearchParams(params.toString()))
const { mode, backendUrl, prefsHydrated } = useDataSource()
if (url.error) return <div role="alert" aria-label="Ссылка диагностики" className="p-4 space-y-2"><p>{url.error}</p><Button type="button" variant="outline" onClick={() => window.history.replaceState(null, "", patchProbeUrl(new URLSearchParams(params.toString()), null) + window.location.hash)}>Сбросить контекст диагностики</Button></div>
if (!prefsHydrated) return <p role="status">Загрузка настроек диагностики…</p>
return <ProbesContent key={JSON.stringify([mode, backendUrl])} />
}
function ProbesContent() {
const params = useSearchParams()
const { server } = readProbeUrl(new URLSearchParams(params.toString()))
const srcId = server ?? ""
const selectServer = (id: string) => window.history.pushState(null, "", patchProbeUrl(new URLSearchParams(params.toString()), id || null) + window.location.hash)
const { mode, backendUrl } = useDataSource()
const canRead = can("mm:servers:read")
const canRun = can("mm:servers:write")
@@ -590,7 +603,6 @@ function ProbesContent() {
const [greByServer, setGreByServer] = useState<Record<string, GreTunnel[]>>({})
const [tool, setTool] = useState<DiagTool>("ping")
const [srcId, setSrcId] = useState(servers[0]?.id ?? "srv1")
const [target, setTarget] = useState("8.8.8.8")
const [pingCount, setPingCount] = useState(5)
const [pingSize, setPingSize] = useState(64)
@@ -653,8 +665,8 @@ function ProbesContent() {
), [allServers])
useEffect(() => {
if (!isLive) {
setRosSrcV4(undefined)
if (!isLive || !canRead || liveLoad !== "idle" || !allServers.some(s => s.id === srcId && s.enabled)) {
queueMicrotask(() => setRosSrcV4(undefined))
return
}
const n = Number.parseInt(srcId, 10)
@@ -672,7 +684,7 @@ function ProbesContent() {
if (!cancelled) setRosSrcV4(null)
})
return () => { cancelled = true }
}, [isLive, srcId, apiFetch])
}, [isLive, canRead, liveLoad, allServers, srcId, apiFetch])
useEffect(() => {
if (!isLive || !canReadGre || liveLoad !== "idle" || !allServers.some(s => s.id === srcId && s.enabled)) return
@@ -699,11 +711,6 @@ function ProbesContent() {
return greTunnels.filter(t => t.serverId === srcId)
}, [isLive, greByServer, srcId])
useEffect(() => {
const first = allServers[0]?.id
if (!first) return
if (!allServers.some(s => s.id === srcId)) setSrcId(first)
}, [allServers, srcId])
useEffect(() => {
const list = isLive ? (greByServer[srcId] ?? []) : greTunnels.filter(t => t.serverId === srcId)
@@ -939,7 +946,8 @@ function ProbesContent() {
<ServerRailLayout
items={probeRailItems}
selectedId={srcId}
onSelect={setSrcId}
onSelect={selectServer}
preserveSelection
showAll={false}
loading={isLive && liveLoad === "loading" && allServers.length === 0}
header={
@@ -963,6 +971,9 @@ function ProbesContent() {
>
<div className="flex flex-col gap-4">
{!server && <p role="status">Выберите устройство для диагностики. Выбор сохраняется в URL.</p>}
{server && (!isLive || catalogReadAt !== null) && !srcServer && <div role="alert" aria-label="Устройство диагностики"><p>Указанное устройство отсутствует в текущем каталоге. Другое устройство автоматически не выбирается.</p><Button type="button" variant="outline" onClick={() => selectServer("")}>Сбросить выбор устройства</Button></div>}
{srcServer && !srcServer.enabled && <p role="status">Выбранное устройство выключено. Диагностика недоступна.</p>}
{isLive && liveLoad === "error" && (
<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>