fix(ui): открывать GRE-туннель с контекстом сервера

This commit is contained in:
Denozordec committed 2026-09-20 17:09:33 +07:00
1 parent 6c8e66693b
commit 67a152b7c8
7 files changed
+188 -14

No files matched your search

+53 -7
View File
@@ -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, AlertTitle, AlertDescription } from "@/components/reui/alert"
import { PageHeader } from "@/components/page-header"
import { DataPageCard } from "@/components/data-page-card"
import { DataPageToolbar, DataPageToolbarFrame } from "@/components/data-page-toolbar"
@@ -248,20 +251,25 @@ type TabFilter = "all" | "up" | "ipsec" | "plain"
type PageTab = "tunnels" | "pools"
// ════════════════════════════════════════════════════════════════════════════
export default function GrePage() {
function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, onSelectServer }: {
selectedServerId: string
selectedTunnelId: string | null
allTunnelsHref: string
onSelectServer: (id: string) => void
}) {
const { mode, backendUrl } = useDataSource()
const isLive = mode === "live"
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
const [liveServers, setLiveServers] = useState<Server[]>([])
const [liveTunnels, setLiveTunnels] = useState<GreTunnel[]>([])
const [dataLoading, setDataLoading] = useState(false)
const [dataLoading, setDataLoading] = useState(true)
const [dataError, setDataError] = useState<string | null>(null)
const [failedServerIds, setFailedServerIds] = useState<string[]>([])
const [pageTab, setPageTab] = useState<PageTab>("tunnels")
const [tabFilter, setTabFilter] = useState<TabFilter>("all")
const [search, setSearch] = useState("")
const [selectedServerId, setSelectedServerId] = useState(ALL_SERVERS_ID)
const [tunnelOpen, setTunnelOpen] = useState(false)
const [tunnelMode, setTunnelMode] = useState<"create" | "edit">("create")
@@ -293,6 +301,7 @@ export default function GrePage() {
])
setLiveServers(backendServers.map(mapBackendToServer))
setLiveTunnels(greRes.tunnels)
setFailedServerIds((greRes.failures ?? []).map(f => String(f.serverId)))
setLiveStale(false)
if (greRes.failures?.length) {
toast.warning(
@@ -344,9 +353,13 @@ export default function GrePage() {
), [displayServers, displayTunnels])
const scopedTunnels = useMemo(() => {
if (selectedTunnelId !== null) return displayTunnels.filter(t => t.serverId === selectedServerId && t.id === selectedTunnelId)
if (selectedServerId === ALL_SERVERS_ID) return displayTunnels
return displayTunnels.filter((t) => t.serverId === selectedServerId)
}, [displayTunnels, selectedServerId])
}, [displayTunnels, selectedServerId, selectedTunnelId])
const selectedTunnel = selectedTunnelId === null ? undefined : scopedTunnels[0]
const selectedTunnelError = isLive && (dataError !== null || failedServerIds.includes(selectedServerId))
const serverById = useMemo(
() => Object.fromEntries(displayServers.map((s) => [s.id, s])),
@@ -358,7 +371,7 @@ export default function GrePage() {
)
const historyServerId = selectedServerId === ALL_SERVERS_ID ? null : selectedServerId
const mutationsLocked = isLive && (mutateBusy || liveStale || historyRestoring)
const mutationsLocked = isLive && (mutateBusy || liveStale || selectedTunnelError || historyRestoring)
const loadRevisions = useCallback(async () => {
if (!isLive || !historyServerId) return
@@ -580,7 +593,8 @@ export default function GrePage() {
<ServerRailLayout
items={greRailItems}
selectedId={selectedServerId}
onSelect={setSelectedServerId}
preserveSelection
onSelect={onSelectServer}
showAll
allCount={displayServers.length}
loading={isLive && dataLoading && displayServers.length === 0}
@@ -630,6 +644,18 @@ export default function GrePage() {
}
>
<div className="flex flex-col gap-5">
{selectedTunnelId !== null && (
<Alert variant={selectedTunnelError || ((!isLive || !dataLoading) && !selectedTunnel) ? "warning" : "info"} role="status" aria-label="GRE-туннель по ссылке">
<AlertTitle className="line-clamp-none wrap-anywhere">{selectedTunnel ? "Выбран GRE-туннель: " + selectedTunnel.name : "GRE-туннель по ссылке"}</AlertTitle>
<AlertDescription>
<p className="wrap-anywhere">{isLive && dataLoading ? "Загрузка туннеля…" : selectedTunnelError ? "Не удалось получить данные выбранного сервера. Повторите обновление; сохранённые данные могут быть устаревшими." : selectedTunnel ? (serverById[selectedServerId]?.name ?? selectedServerId) + " · " + selectedTunnel.remoteAddress + ". Список ограничен выбранным туннелем; сводка относится ко всем серверам." : "Туннель не найден или недоступен на указанном сервере. Проверьте ссылку и доступ к устройству."}</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>
</AlertDescription>
</Alert>
)}
{/* Legacy banner */}
<div className="flex items-start gap-3 rounded-lg bg-amber-500/5 border border-amber-500/20 px-4 py-3 text-sm">
@@ -1079,3 +1105,23 @@ export default function GrePage() {
</>
)
}
function GreRoute() {
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 = "/gre" + (rest.size ? "?" + rest.toString() : "")
return <GrePageContent 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("/gre" + (next.size ? "?" + next.toString() : ""))
}} />
}
export default function GrePage() {
return <Suspense fallback={<div role="status">Загрузка GRE…</div>}><GreRoute /></Suspense>
}