fix(ui): сохранять контекст карты сети в ссылке
This commit is contained in:
1 parent
0cdb43a29d
commit
8bc4f22af7
6 files changed
+114
-24
No files matched your search
@@ -1,6 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useRef, useEffect, useMemo, useCallback } from "react"
|
||||
import { useState, useRef, useEffect, useMemo, useCallback, Suspense } from "react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import {readNetworkMapUrl,patchNetworkMapUrl,type NetworkMapFilter} from "@/lib/network-map-url-state"
|
||||
import {Alert,AlertTitle,AlertDescription} from "@/components/ui/alert"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import {
|
||||
servers as mockServers,
|
||||
@@ -319,14 +322,7 @@ const MOCK_COUNTRY_SERVICE_GROUPS: FlowMapCountryServiceGroup[] = [
|
||||
const DEST_MODE_KEY = "mm-network-map-dest-mode"
|
||||
type DestMode = "services" | "countries"
|
||||
|
||||
function readDestMode(): DestMode {
|
||||
if (typeof window === "undefined") return "services"
|
||||
try {
|
||||
return sessionStorage.getItem(DEST_MODE_KEY) === "countries" ? "countries" : "services"
|
||||
} catch {
|
||||
return "services"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function destDisplayLabel(node: FlowMapService | undefined, mode: DestMode, fallback = ""): string {
|
||||
if (!node) return fallback
|
||||
@@ -475,7 +471,7 @@ function edgeBadgePosition(
|
||||
|
||||
// ─── Filter ───────────────────────────────────────────────────────────────────
|
||||
|
||||
type FilterKey = "all" | "online" | "degraded" | "offline" | "unknown" | "jump-host" | "exit-node" | "home-router"
|
||||
type FilterKey = NetworkMapFilter
|
||||
|
||||
function greEdgeKey(e: GreMapEdge): string {
|
||||
const la = (e.tunnel.localAddress ?? "").trim()
|
||||
@@ -1138,10 +1134,23 @@ function ZoomControls({ zoom, onZoomIn, onZoomOut, onFit }: {
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export default function NetworkMapPage() {
|
||||
return <Suspense fallback={<p role="status">Загрузка контекста карты…</p>}><NetworkMapRoute /></Suspense>
|
||||
}
|
||||
function NetworkMapRoute(){
|
||||
const {mode,backendUrl}=useDataSource()
|
||||
const params=useSearchParams()
|
||||
const state=readNetworkMapUrl(new URLSearchParams(params.toString()))
|
||||
if(state.error)return <div><PageHeader crumbs={[{label:"Обзор"},{label:"Карта сети"}]} /><Alert variant="destructive"><AlertTitle>Контекст карты недоступен</AlertTitle><AlertDescription>{state.error}<Button type="button" variant="outline" onClick={()=>window.history.replaceState(null,"",patchNetworkMapUrl(new URLSearchParams(window.location.search),{server:null,q:null,filter:null,dest:null,wan:null})+window.location.hash)}>Сбросить контекст карты</Button></AlertDescription></Alert></div>
|
||||
return <NetworkMapContent key={JSON.stringify([mode,backendUrl])} />
|
||||
}
|
||||
function NetworkMapContent() {
|
||||
const params=useSearchParams()
|
||||
const serializedParams=params.toString()
|
||||
const urlState=useMemo(()=>readNetworkMapUrl(new URLSearchParams(serializedParams)),[serializedParams])
|
||||
const updateUrl=useCallback((values:Record<string,string|null>,replace=false)=>{
|
||||
const href=patchNetworkMapUrl(new URLSearchParams(window.location.search),values)+window.location.hash
|
||||
if(href!==window.location.pathname+window.location.search+window.location.hash)(replace?window.history.replaceState:window.history.pushState).call(window.history,null,"",href)
|
||||
},[])
|
||||
const { mode, backendUrl } = useDataSource()
|
||||
const useLiveData = mode === "live"
|
||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||
@@ -1298,17 +1307,17 @@ function NetworkMapContent() {
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
|
||||
// ── Interaction ─────────────────────────────────────────────────────────────
|
||||
const [selectedStored, setSelected] = useState<Server | null>(null)
|
||||
const selected=selectedStored?mapServers.find(server=>server.id===selectedStored.id)??null:null
|
||||
const selected=urlState.server?mapServers.find(server=>server.id===urlState.server)??null:null
|
||||
const setSelected=useCallback((value:React.SetStateAction<Server|null>)=>{
|
||||
const next=typeof value==="function"?value(selected):value
|
||||
updateUrl({server:next?.id??null,wan:null})
|
||||
},[selected,updateUrl])
|
||||
const [selectedService, setSelectedService] = useState<FlowMapService | null>(null)
|
||||
/** Раскрытая страна (режим «Страны»): справа столбец её сервисов. Не персистится. */
|
||||
const [expandedCountryId, setExpandedCountryId] = useState<string | null>(null)
|
||||
const [destMode, setDestModeState] = useState<DestMode>("services")
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => setDestModeState(readDestMode()))
|
||||
}, [])
|
||||
const destMode=urlState.dest
|
||||
function setDestMode(mode: DestMode) {
|
||||
setDestModeState(mode)
|
||||
updateUrl({dest:mode==="services"?null:mode})
|
||||
setSelectedService(null)
|
||||
setExpandedCountryId(null)
|
||||
setHighlightedPath(null)
|
||||
@@ -1337,7 +1346,8 @@ function NetworkMapContent() {
|
||||
? mapCountries.find((c) => c.id === expandedCountryId) ?? null
|
||||
: null
|
||||
const [highlightedPath, setHighlightedPath] = useState<{ viaId: string; enId: string; serviceId: string } | null>(null)
|
||||
const [selWanIdx, setSelWanIdx] = useState<number | null>(null)
|
||||
const selWanIdx=urlState.wan
|
||||
function setSelWanIdx(value:React.SetStateAction<number|null>){const next=typeof value==="function"?value(selWanIdx):value;updateUrl({wan:next==null?null:String(next)})}
|
||||
const [hoveredId, setHoveredId] = useState<string | null>(null)
|
||||
|
||||
// ── Node positions (overrides POS defaults) ─────────────────────────────────
|
||||
@@ -1367,8 +1377,10 @@ function NetworkMapContent() {
|
||||
const [selectedGreStored, setSelectedGreEdge] = useState<GreMapEdge | null>(null)
|
||||
|
||||
// ── Toggles ─────────────────────────────────────────────────────────────────
|
||||
const [filter, setFilter] = useState<FilterKey>("all")
|
||||
const [search, setSearch] = useState("")
|
||||
const filter=urlState.filter
|
||||
const search=urlState.search
|
||||
const setFilter=(value:FilterKey)=>updateUrl({filter:value==="all"?null:value})
|
||||
const setSearch=(value:string)=>updateUrl({q:value||null},true)
|
||||
const [showPingBadges, setShowPingBadges] = useState(true)
|
||||
const [showNetflow, setShowNetflow] = useState(true)
|
||||
const [showServices, setShowServices] = useState(true)
|
||||
@@ -1775,7 +1787,7 @@ function NetworkMapContent() {
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
|
||||
if (e.key === "Escape") {
|
||||
setSelected(null)
|
||||
setSelWanIdx(null)
|
||||
updateUrl({wan:null})
|
||||
setSelectedGreEdge(null)
|
||||
setSelectedService(null)
|
||||
setHighlightedPath(null)
|
||||
@@ -1789,7 +1801,7 @@ function NetworkMapContent() {
|
||||
}
|
||||
window.addEventListener("keydown", onKey)
|
||||
return () => window.removeEventListener("keydown", onKey)
|
||||
}, [applyZoomCenter,fitView])
|
||||
}, [applyZoomCenter,fitView,setSelected,updateUrl])
|
||||
|
||||
// ── Drag ─────────────────────────────────────────────────────────────────
|
||||
function onSvgMouseDown(e: React.MouseEvent<SVGSVGElement>) {
|
||||
@@ -2051,6 +2063,7 @@ function NetworkMapContent() {
|
||||
}
|
||||
/>
|
||||
|
||||
{urlState.server&&((hasSnapshot||!useLiveData)&&!selected||selected&&selWanIdx!==null&&!selected.wanUplinks?.[selWanIdx])&&<Alert variant="destructive"><AlertTitle>Объект карты недоступен</AlertTitle><AlertDescription>{!selected?"Устройство "+urlState.server+" отсутствует в текущем снимке.":"Запрошенный WAN отсутствует у выбранного устройства."}<Button type="button" variant="outline" onClick={()=>updateUrl({server:null,wan:null})}>Сбросить выбранный объект</Button></AlertDescription></Alert>}
|
||||
{/* ── Toolbar ── */}
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b bg-background/60 shrink-0 flex-wrap">
|
||||
|
||||
@@ -2075,7 +2088,7 @@ function NetworkMapContent() {
|
||||
<input
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="Поиск узла…"
|
||||
aria-label="Поиск узла" maxLength={256} placeholder="Поиск узла…"
|
||||
className="h-7 pl-6 pr-3 text-xs rounded-md border border-border bg-muted/40
|
||||
placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-ring w-44"
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user