fix(ui): открывать интерфейс WireGuard с контекстом сервера
This commit is contained in:
1 parent
67a152b7c8
commit
af84f0135d
7 files changed
+219
-29
No files matched your search
@@ -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>
|
||||
}
|
||||
Reference in new issue
Block a user