fix(ui): открывать интерфейс WireGuard с контекстом сервера

This commit is contained in:
Denozordec committed 2026-09-20 21:59:57 +07:00
1 parent 67a152b7c8
commit af84f0135d
7 files changed
+219 -29

No files matched your search

+71 -20
View File
@@ -1,6 +1,8 @@
"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 { PageHeader } from "@/components/page-header"
import { servers as mockServers } from "@/lib/data"
import type { Server } from "@/lib/data"
@@ -169,17 +171,22 @@ function peerRowId(iface: WgIfaceWithServer, peer: WgIfaceWithServer["peers"][nu
return peer.id ?? peer.rosId ?? `${iface.id}-${peer.publicKey}-${index}`
}
export default function WireGuardPage() {
function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterfacesHref, onSelectServer }: {
selectedServerId: string
selectedInterfaceId: string | null
allInterfacesHref: string
onSelectServer: (id: string) => void
}) {
const { mode, backendUrl } = useDataSource()
const isLive = mode === "live"
const [liveIfaces, setLiveIfaces] = useState<WgIfaceWithServer[]>([])
const [liveServers, setLiveServers] = useState<Server[]>([])
const [failures, setFailures] = useState<WgFailure[]>([])
const [loading, setLoading] = useState(false)
const [loading, setLoading] = useState(true)
const [loadError, setLoadError] = useState(false)
const [busy, setBusy] = useState(false)
const [selectedServerId, setSelectedServerId] = useState(ALL_SERVERS_ID)
const [workspaceTab, setWorkspaceTab] = useState<WgWorkspaceTab>("interfaces")
const [statusFilter, setStatusFilter] = useState<WgStatusFilter>("all")
const [search, setSearch] = useState("")
@@ -212,6 +219,7 @@ export default function WireGuardPage() {
setLiveIfaces(wg.interfaces.map(dtoToRow))
setLiveServers(servers.filter((s) => s.enabled).map(mapBackendServer))
setFailures(wg.failures ?? [])
setLoadError(false)
if (wg.failures?.length) {
toast.warning(
`Не удалось опросить: ${wg.failures.map((f) => f.serverName ?? f.serverId).join(", ")}`,
@@ -219,8 +227,7 @@ export default function WireGuardPage() {
}
} catch (e) {
toast.error(e instanceof Error ? e.message : "Ошибка загрузки WireGuard")
setLiveIfaces([])
setFailures([])
setLoadError(true)
} finally {
setLoading(false)
}
@@ -243,10 +250,8 @@ export default function WireGuardPage() {
const displayIfaces = isLive ? liveIfaces : collectMockInterfaces()
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 selectedSourceError = isLive && (loadError || failures.some(f => f.serverId === selectedServerId))
const historyServerId = effectiveServerId === ALL_SERVERS_ID ? null : effectiveServerId
@@ -287,9 +292,12 @@ export default function WireGuardPage() {
}, [isLive, historyServerId, backendUrl, loadLive, loadRevisions])
const scopedIfaces = useMemo(() => {
if (selectedInterfaceId !== null) return displayIfaces.filter(i => i.serverId === effectiveServerId && i.id === selectedInterfaceId)
if (effectiveServerId === ALL_SERVERS_ID) return displayIfaces
return displayIfaces.filter((i) => i.serverId === effectiveServerId)
}, [displayIfaces, effectiveServerId])
}, [displayIfaces, effectiveServerId, selectedInterfaceId])
const selectedInterface = selectedInterfaceId === null ? undefined : scopedIfaces[0]
const statusCounts = useMemo(() => {
let up = 0
@@ -458,7 +466,14 @@ export default function WireGuardPage() {
}
}
function sourceUnavailable(serverId: string | undefined) {
const unavailable = isLive && (loadError || failures.some(f => f.serverId === serverId))
if (unavailable) toast.error("Обновите данные сервера перед изменением объекта")
return unavailable
}
async function handleToggle(iface: WgIfaceWithServer) {
if (sourceUnavailable(iface.serverId)) return
if (!isLive || !iface.rosId) {
toast.info("Доступно только в live-режиме")
return
@@ -476,6 +491,7 @@ export default function WireGuardPage() {
async function confirmDelete() {
if (!pendingDelete) return
if (sourceUnavailable(pendingDelete.iface.serverId)) return
if (!isLive) {
toast.info("Доступно только в live-режиме")
setPendingDelete(null)
@@ -501,6 +517,7 @@ export default function WireGuardPage() {
}
async function handleAddPeer(form: WgPeerFormState) {
if (sourceUnavailable(peerIface?.serverId)) return
if (!isLive || !peerIface) {
toast.info("Доступно только в live-режиме")
return
@@ -577,7 +594,8 @@ export default function WireGuardPage() {
<ServerRailLayout
items={railItems}
selectedId={effectiveServerId}
onSelect={setSelectedServerId}
preserveSelection
onSelect={onSelectServer}
showAll
allCount={displayIfaces.length}
loading={isLive && loading && displayServers.length === 0}
@@ -627,6 +645,19 @@ export default function WireGuardPage() {
}
>
<div className="flex flex-col gap-4">
{isLive && loadError && selectedInterfaceId === null && <Alert variant="warning"><AlertTitle>Не удалось обновить WireGuard</AlertTitle><AlertDescription>Показаны последние полученные данные. Повторите обновление перед изменением объектов.</AlertDescription></Alert>}
{selectedInterfaceId !== null && (
<Alert variant={selectedSourceError || ((!isLive || !loading) && !selectedInterface) ? "warning" : "info"} role="status" aria-label="WireGuard по ссылке">
<AlertTitle className="line-clamp-none wrap-anywhere">{selectedInterface ? "Выбран интерфейс: " + selectedInterface.name : "WireGuard по ссылке"}</AlertTitle>
<AlertDescription>
<p className="wrap-anywhere">{isLive && loading ? "Загрузка интерфейса…" : selectedSourceError ? "Не удалось получить данные выбранного сервера. Повторите обновление; сохранённые данные могут быть устаревшими." : selectedInterface ? selectedInterface.serverName + " · " + (selectedInterface.address ?? selectedInterface.id) + ". Список, пиры и сводка ограничены выбранным интерфейсом." : "Интерфейс не найден или недоступен на указанном сервере. Проверьте ссылку и доступ к устройству."}</p>
<div className="flex flex-wrap gap-4">
<Link href={allInterfacesHref} className="underline underline-offset-2">Все интерфейсы сервера</Link>
<Link href="/dashboard" className="underline underline-offset-2">К дашборду</Link>
</div>
</AlertDescription>
</Alert>
)}
<KpiStatGrid
aria-label="Сводка WireGuard"
items={[
@@ -723,10 +754,10 @@ export default function WireGuardPage() {
compactServer={compactServer}
emptyAction={emptyCreateAction}
onExport={(iface) => openExport(iface, "rsc")}
onAddPeer={setPeerIface}
onToggle={handleToggle}
onDelete={(iface) => setPendingDelete({ kind: "iface", iface })}
onDeletePeer={(iface, peerId) => setPendingDelete({ kind: "peer", iface, peerId })}
onAddPeer={selectedSourceError ? undefined : setPeerIface}
onToggle={selectedSourceError ? undefined : handleToggle}
onDelete={selectedSourceError ? undefined : (iface) => setPendingDelete({ kind: "iface", iface })}
onDeletePeer={selectedSourceError ? undefined : (iface, peerId) => setPendingDelete({ kind: "peer", iface, peerId })}
onExportPeer={(iface, peerId) => openExport(iface, "peer", peerId)}
/>
</DataPageCard>
@@ -745,19 +776,19 @@ export default function WireGuardPage() {
compactServer={compactServer}
emptyAction={
scopedIfaces.length === 1 ? (
<Button size="sm" onClick={() => setPeerIface(scopedIfaces[0])}>
<Button size="sm" disabled={selectedSourceError} onClick={() => setPeerIface(scopedIfaces[0])}>
<PlusIcon className="size-4" />
Добавить пира
</Button>
) : emptyCreateAction
}
onDeletePeer={(row) => {
const iface = scopedIfaces.find((i) => i.id === row.ifaceId)
onDeletePeer={selectedSourceError ? undefined : (row) => {
const iface = scopedIfaces.find((i) => i.id === row.ifaceId && i.serverId === row.serverId)
if (!iface) return
setPendingDelete({ kind: "peer", iface, peerId: row.id })
}}
onExportPeer={(row) => {
const iface = scopedIfaces.find((i) => i.id === row.ifaceId)
const iface = scopedIfaces.find((i) => i.id === row.ifaceId && i.serverId === row.serverId)
if (!iface) return
openExport(iface, "peer", row.id)
}}
@@ -895,3 +926,23 @@ export default function WireGuardPage() {
</>
)
}
function WireGuardRoute() {
const params = useSearchParams()
const router = useRouter()
const serverId = params.get("server") ?? ALL_SERVERS_ID
const interfaceId = params.get("interface")
const rest = new URLSearchParams(params.toString())
rest.delete("interface")
const allInterfacesHref = "/wireguard" + (rest.size ? "?" + rest.toString() : "")
return <WireGuardPageContent key={JSON.stringify([serverId, interfaceId])} selectedServerId={serverId} selectedInterfaceId={interfaceId} allInterfacesHref={allInterfacesHref} onSelectServer={id => {
const next = new URLSearchParams(rest)
if (id === ALL_SERVERS_ID) next.delete("server")
else next.set("server", id)
router.push("/wireguard" + (next.size ? "?" + next.toString() : ""))
}} />
}
export default function WireGuardPage() {
return <Suspense fallback={<div role="status">Загрузка WireGuard…</div>}><WireGuardRoute /></Suspense>
}