fix(ui): показывать свежесть GRE и закреплять экспорт
This commit is contained in:
1 parent
ceb2113209
commit
a21252fb02
5 files changed
+94
-8
No files matched your search
+31
-7
@@ -231,6 +231,9 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
const [failures, setFailures] = useState<z.infer<typeof greResponseSchema>["failures"]>([])
|
||||
const failedServerIds = useMemo(() => failures.map(f => f.serverId), [failures])
|
||||
const [hasLoaded, setHasLoaded] = useState(false)
|
||||
const [readReceipt, setReadReceipt] = useState<Record<string, string>>({})
|
||||
const [responseReceipt, setResponseReceipt] = useState<string | null>(null)
|
||||
const [writeReceipt, setWriteReceipt] = useState<{ serverId: string; at: string } | null>(null)
|
||||
const loadEpoch = useRef(0)
|
||||
const mounted = useRef(true)
|
||||
const operationLock = useRef(false)
|
||||
@@ -264,6 +267,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
const [historyRestoring, setHistoryRestoring] = useState(false)
|
||||
const [poolOpen, setPoolOpen] = useState(false)
|
||||
const [codePreviewTunnel, setCodePreviewTunnel] = useState<GreTunnel | null>(null)
|
||||
const [exportContext, setExportContext] = useState<{ servers: Record<string, Server>; at: string | null; stale: boolean; mock: boolean } | null>(null)
|
||||
|
||||
const [tForm, setTForm] = useState(defaultTunnelForm)
|
||||
const [pForm, setPForm] = useState(defaultPoolForm)
|
||||
@@ -289,6 +293,9 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
setLiveTunnels(previous => [...response.data.tunnels, ...previous.filter(t => failedIds.has(t.serverId) && enabledIds.has(t.serverId))])
|
||||
setFailures(response.data.failures)
|
||||
setHasLoaded(true)
|
||||
const at = new Date().toISOString()
|
||||
setResponseReceipt(at)
|
||||
setReadReceipt(previous => Object.fromEntries([...enabledIds].map(id => [id, failedIds.has(id) ? previous[id] : at]).filter((entry): entry is [string, string] => !!entry[1])))
|
||||
setDataError(null)
|
||||
setLiveStale(false)
|
||||
} catch (e) {
|
||||
@@ -400,6 +407,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
)
|
||||
if (!mounted.current) return
|
||||
if (res?.ok !== true) throw new Error("Сервер не подтвердил восстановление")
|
||||
setWriteReceipt({ serverId: historyServerId, at: new Date().toISOString() })
|
||||
toast.success("Версия применена на роутер")
|
||||
await loadLive(true)
|
||||
if (mounted.current) await loadRevisions()
|
||||
@@ -464,6 +472,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
})
|
||||
if (!mounted.current) return
|
||||
if (result?.ok !== true) throw new Error("Сервер не подтвердил сохранение туннеля")
|
||||
setWriteReceipt({ serverId: tForm.serverId, at: new Date().toISOString() })
|
||||
toast.success(`Туннель ${tForm.name} обновлён`)
|
||||
} else {
|
||||
const result = await apiFetch<unknown>("/api/gre/tunnels", {
|
||||
@@ -479,6 +488,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
"enabled" in result && result.enabled === tForm.enabled)
|
||||
)
|
||||
if (!confirmed) throw new Error("Сервер не подтвердил создание туннеля")
|
||||
setWriteReceipt({ serverId: tForm.serverId, at: new Date().toISOString() })
|
||||
toast.success(`Туннель ${tForm.name} создан`)
|
||||
}
|
||||
setTunnelOpen(false)
|
||||
@@ -510,6 +520,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
})
|
||||
if (!mounted.current) return
|
||||
if (result?.ok !== true) throw new Error("Сервер не подтвердил изменение туннеля")
|
||||
setWriteReceipt({ serverId: t.serverId, at: new Date().toISOString() })
|
||||
toast.success(t.enabled ? `Выключен ${t.name}` : `Включён ${t.name}`)
|
||||
await loadLive(true)
|
||||
} catch (err) {
|
||||
@@ -533,6 +544,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
})
|
||||
if (!mounted.current) return
|
||||
if (result?.ok !== true) throw new Error("Сервер не подтвердил удаление туннеля")
|
||||
setWriteReceipt({ serverId: t.serverId, at: new Date().toISOString() })
|
||||
toast.success(`Удалён ${t.name}`)
|
||||
setPendingDelete(null)
|
||||
await loadLive(true)
|
||||
@@ -615,9 +627,16 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
{ value: "plain", label: "Без IPsec", count: scopedTunnels.length - scopedIpsecCount },
|
||||
]
|
||||
|
||||
function openCodePreview(t: GreTunnel) {
|
||||
setCodePreviewTunnel(t)
|
||||
setExportContext({ servers: serverById, at: readReceipt[t.serverId] ?? null, stale: isLive && (!hasLoaded || !!dataError || failedServerIds.includes(t.serverId)), mock: !isLive })
|
||||
}
|
||||
const scopeReceipt = selectedServerId === ALL_SERVERS_ID ? responseReceipt : readReceipt[selectedServerId]
|
||||
const formatReceipt = (at: string) => new Date(at).toLocaleString("ru-RU")
|
||||
|
||||
const greExportCode = useMemo(
|
||||
() => (codePreviewTunnel ? generateRosCommands(codePreviewTunnel, serverById) : ""),
|
||||
[codePreviewTunnel, serverById],
|
||||
() => (codePreviewTunnel ? generateRosCommands(codePreviewTunnel, exportContext?.servers ?? {}) : ""),
|
||||
[codePreviewTunnel, exportContext],
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -744,6 +763,11 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
]}
|
||||
/>
|
||||
|
||||
<div role="group" aria-label="Свежесть GRE" className="flex flex-wrap gap-x-5 gap-y-1 text-xs text-muted-foreground">
|
||||
<span data-source-time="read">{isLive ? selectedServerId === ALL_SERVERS_ID ? "Ответ каталога: " : "Опрос устройства: " : "Моковые данные · "}{isLive ? scopeReceipt ? <time dateTime={scopeReceipt} title="Время получения ответа браузером; не время измерения RouterOS">{formatReceipt(scopeReceipt)}</time> : "неизвестно" : "не получены с RouterOS"}</span>
|
||||
{isLive && writeReceipt && (selectedServerId === ALL_SERVERS_ID || writeReceipt.serverId === selectedServerId) && <span data-source-time="write">API подтвердил изменение · {serverById[writeReceipt.serverId]?.name ?? writeReceipt.serverId}: <time dateTime={writeReceipt.at}>{formatReceipt(writeReceipt.at)}</time></span>}
|
||||
</div>
|
||||
|
||||
{toggleStatus && <p role="status" className="text-sm text-muted-foreground">{toggleStatus}</p>}
|
||||
{toggleError && <p role="alert" className="text-sm text-destructive">{toggleError}</p>}
|
||||
|
||||
@@ -780,7 +804,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
tunnels={filtered}
|
||||
servers={displayServers}
|
||||
pools={displayPools}
|
||||
onCodePreview={setCodePreviewTunnel}
|
||||
onCodePreview={openCodePreview}
|
||||
onEdit={openEditTunnel}
|
||||
onToggle={(t) => { void toggleTunnel(t) }}
|
||||
onDelete={setPendingDelete}
|
||||
@@ -864,7 +888,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
open={!!codePreviewTunnel}
|
||||
onClose={() => setCodePreviewTunnel(null)}
|
||||
title={codePreviewTunnel?.name ?? "GRE"}
|
||||
description="Команды RouterOS 7.20+ для создания туннеля"
|
||||
description={exportContext?.mock ? "Моковый снимок · данные не получены с RouterOS" : `Снимок на момент открытия · ${exportContext?.at ? formatReceipt(exportContext.at) : "время получения неизвестно"} · ${exportContext?.stale ? "актуальность не подтверждена" : "последний успешный опрос"}`}
|
||||
formats={[
|
||||
{
|
||||
id: "rsc",
|
||||
@@ -877,11 +901,11 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
codePreviewTunnel ? (
|
||||
<div className="flex flex-wrap gap-3 text-xs shrink-0">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className={`size-1.5 rounded-full ${greStatusMeta(codePreviewTunnel.status).dot}`} />
|
||||
{greStatusMeta(codePreviewTunnel.status).label}
|
||||
<span className={`size-1.5 rounded-full ${exportContext?.stale ? "bg-muted-foreground" : greStatusMeta(codePreviewTunnel.status).dot}`} />
|
||||
{exportContext?.stale ? "Неизвестно" : greStatusMeta(codePreviewTunnel.status).label}
|
||||
</span>
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span>{serverById[codePreviewTunnel.serverId]?.name}</span>
|
||||
<span>{exportContext?.servers[codePreviewTunnel.serverId]?.name}</span>
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span className="font-mono">
|
||||
{codePreviewTunnel.localAddress === "0.0.0.0" ? "авто" : codePreviewTunnel.localAddress}
|
||||
|
||||
Reference in new issue
Block a user