fix(ui): завершить переходы к объектам дашборда через VXLAN
This commit is contained in:
1 parent
af84f0135d
commit
d25afa4b9e
6 files changed
+190
-23
No files matched your search
+53
-13
@@ -1,6 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { Alert as ContextAlert, AlertTitle as ContextTitle, AlertDescription as ContextDescription } from "@/components/reui/alert"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { vxlanTunnels as mockVxlanTunnels, servers as mockServers } from "@/lib/data"
|
||||
import type { Server, VxlanTunnel } from "@/lib/data"
|
||||
@@ -121,17 +124,21 @@ function ExportSheet({
|
||||
)
|
||||
}
|
||||
|
||||
export default function VxlanPage() {
|
||||
function VxlanPageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, onSelectServer }: {
|
||||
selectedServerId: string
|
||||
selectedTunnelId: string | null
|
||||
allTunnelsHref: string
|
||||
onSelectServer: (id: string) => void
|
||||
}) {
|
||||
const { mode, backendUrl } = useDataSource()
|
||||
const isLive = mode === "live"
|
||||
|
||||
const [search, setSearch] = useState("")
|
||||
const [exportTunnel, setExportTunnel] = useState<VxlanTunnel | null>(null)
|
||||
const [selectedServerId, setSelectedServerId] = useState(ALL_SERVERS_ID)
|
||||
|
||||
const [liveTunnels, setLiveTunnels] = useState<VxlanTunnel[]>([])
|
||||
const [liveServers, setLiveServers] = useState<Server[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [liveError, setLiveError] = useState<string | null>(null)
|
||||
|
||||
const loadLive = useCallback(async () => {
|
||||
@@ -147,7 +154,6 @@ export default function VxlanPage() {
|
||||
setLiveServers(serversRes.filter((s) => s.enabled).map(mapBackendServer))
|
||||
} catch (e) {
|
||||
setLiveError(e instanceof Error ? e.message : "Ошибка загрузки")
|
||||
setLiveTunnels([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -170,15 +176,15 @@ export default function VxlanPage() {
|
||||
const displayTunnels = isLive ? liveTunnels : mockVxlanTunnels
|
||||
const displayServers = isLive ? liveServers : mockServers.filter((s) => s.enabled)
|
||||
|
||||
const effectiveServerId =
|
||||
selectedServerId === ALL_SERVERS_ID || displayServers.some((s) => s.id === selectedServerId)
|
||||
? selectedServerId
|
||||
: ALL_SERVERS_ID
|
||||
const effectiveServerId = selectedServerId
|
||||
|
||||
const scopedTunnels = useMemo(() => {
|
||||
if (selectedTunnelId !== null) return displayTunnels.filter(t => t.serverId === effectiveServerId && t.id === selectedTunnelId)
|
||||
if (effectiveServerId === ALL_SERVERS_ID) return displayTunnels
|
||||
return displayTunnels.filter((t) => t.serverId === effectiveServerId)
|
||||
}, [displayTunnels, effectiveServerId])
|
||||
}, [displayTunnels, effectiveServerId, selectedTunnelId])
|
||||
|
||||
const selectedTunnel = selectedTunnelId === null ? undefined : scopedTunnels[0]
|
||||
|
||||
const serverById = useMemo(
|
||||
() => Object.fromEntries(displayServers.map((s) => [s.id, s])),
|
||||
@@ -218,7 +224,8 @@ export default function VxlanPage() {
|
||||
<ServerRailLayout
|
||||
items={railItems}
|
||||
selectedId={effectiveServerId}
|
||||
onSelect={setSelectedServerId}
|
||||
preserveSelection
|
||||
onSelect={onSelectServer}
|
||||
showAll
|
||||
allCount={displayServers.length}
|
||||
loading={isLive && loading && displayServers.length === 0}
|
||||
@@ -246,14 +253,27 @@ export default function VxlanPage() {
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-5">
|
||||
{selectedTunnelId !== null && (
|
||||
<ContextAlert variant={liveError || ((!isLive || !loading) && !selectedTunnel) ? "warning" : "info"} role="status" aria-label="VXLAN по ссылке">
|
||||
<ContextTitle className="line-clamp-none wrap-anywhere">{selectedTunnel ? "Выбран VXLAN: " + selectedTunnel.name : "VXLAN по ссылке"}</ContextTitle>
|
||||
<ContextDescription>
|
||||
<p className="wrap-anywhere">{isLive && loading ? "Загрузка туннеля…" : isLive && liveError ? "Не удалось обновить VXLAN. Повторите обновление; сохранённые данные могут быть устаревшими." : selectedTunnel ? (serverById[selectedServerId]?.name ?? selectedServerId) + " · VNI " + selectedTunnel.vni + ". Список и сводка ограничены выбранным туннелем." : "Туннель не получен для указанного сервера. Проверьте ссылку и доступ к устройству: туннель может отсутствовать или устройство не ответило на опрос."}</p>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link href={allTunnelsHref} className="underline underline-offset-2">Все туннели сервера</Link>
|
||||
<Link href="/dashboard" className="underline underline-offset-2">К дашборду</Link>
|
||||
</div>
|
||||
</ContextDescription>
|
||||
</ContextAlert>
|
||||
)}
|
||||
|
||||
{isLive && liveError && (
|
||||
|
||||
{isLive && liveError && selectedTunnelId === null && (
|
||||
<Alert variant="warning" className="py-2">
|
||||
<AlertCircleIcon />
|
||||
<AlertDescription className="text-xs">Ошибка загрузки: {liveError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{isLive && !loading && displayTunnels.length === 0 && !liveError && (
|
||||
{selectedTunnelId === null && isLive && !loading && displayTunnels.length === 0 && !liveError && (
|
||||
<div className="rounded-md border border-border bg-muted/30 px-4 py-6 text-center text-sm text-muted-foreground">
|
||||
На опрошенных серверах нет VXLAN-интерфейсов
|
||||
</div>
|
||||
@@ -387,3 +407,23 @@ export default function VxlanPage() {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function VxlanRoute() {
|
||||
const params = useSearchParams()
|
||||
const router = useRouter()
|
||||
const serverId = params.get("server") ?? ALL_SERVERS_ID
|
||||
const tunnelId = params.get("tunnel")
|
||||
const rest = new URLSearchParams(params.toString())
|
||||
rest.delete("tunnel")
|
||||
const allTunnelsHref = "/vxlan" + (rest.size ? "?" + rest.toString() : "")
|
||||
return <VxlanPageContent key={JSON.stringify([serverId, tunnelId])} selectedServerId={serverId} selectedTunnelId={tunnelId} allTunnelsHref={allTunnelsHref} onSelectServer={id => {
|
||||
const next = new URLSearchParams(rest)
|
||||
if (id === ALL_SERVERS_ID) next.delete("server")
|
||||
else next.set("server", id)
|
||||
router.push("/vxlan" + (next.size ? "?" + next.toString() : ""))
|
||||
}} />
|
||||
}
|
||||
|
||||
export default function VxlanPage() {
|
||||
return <Suspense fallback={<div role="status">Загрузка VXLAN…</div>}><VxlanRoute /></Suspense>
|
||||
}
|
||||
Reference in new issue
Block a user