mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 23:24:44 +02:00
feat(dashboard): mesh topology graph + geographic map + node table
- D3 force-directed topology graph with flowing particle animations - Leaflet geographic map with CartoDB Dark tiles - Drag-to-reorganize with visible settling (matches Meshview behavior) - SNR-based edge coloring: excellent/good/fair/marginal/poor - Node detail panel with neighbor list and external map links - Sortable/filterable node table - Region-colored nodes, infrastructure vs client distinction - Click-to-select synced across graph, map, table, and detail panel Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7704923b8c
commit
8273913c1a
6 changed files with 1384 additions and 11 deletions
|
|
@ -1,15 +1,142 @@
|
|||
import { Radio } from 'lucide-react'
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react'
|
||||
import { Map, Network } from 'lucide-react'
|
||||
import {
|
||||
fetchNodes,
|
||||
fetchEdges,
|
||||
fetchRegions,
|
||||
type NodeInfo,
|
||||
type EdgeInfo,
|
||||
type RegionInfo,
|
||||
} from '@/lib/api'
|
||||
import TopologyGraph from '@/components/TopologyGraph'
|
||||
import GeoMap from '@/components/GeoMap'
|
||||
import NodeDetail from '@/components/NodeDetail'
|
||||
import NodeTable from '@/components/NodeTable'
|
||||
|
||||
type ViewMode = 'topo' | 'geo'
|
||||
|
||||
export default function Mesh() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] text-center">
|
||||
<div className="w-16 h-16 rounded-full bg-bg-card border border-border flex items-center justify-center mb-6">
|
||||
<Radio size={32} className="text-slate-500" />
|
||||
const [nodes, setNodes] = useState<NodeInfo[]>([])
|
||||
const [edges, setEdges] = useState<EdgeInfo[]>([])
|
||||
const [_regions, setRegions] = useState<RegionInfo[]>([])
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<number | null>(null)
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('topo')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Fetch data on mount
|
||||
useEffect(() => {
|
||||
Promise.all([fetchNodes(), fetchEdges(), fetchRegions()])
|
||||
.then(([n, e, r]) => {
|
||||
setNodes(n)
|
||||
setEdges(e)
|
||||
setRegions(r)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(err.message)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Get selected node
|
||||
const selectedNode = useMemo(
|
||||
() => nodes.find((n) => n.node_num === selectedNodeId) || null,
|
||||
[nodes, selectedNodeId]
|
||||
)
|
||||
|
||||
// Handle node selection
|
||||
const handleSelectNode = useCallback((nodeId: number | null) => {
|
||||
setSelectedNodeId(nodeId)
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-slate-400">Loading mesh data...</div>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-slate-300 mb-2">Mesh</h2>
|
||||
<p className="text-slate-500 max-w-md">
|
||||
Topology graph and geographic map coming in Phase 6
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-red-400">Error: {error}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header with view toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-slate-400">
|
||||
{nodes.length} nodes • {edges.length} edges
|
||||
</div>
|
||||
|
||||
{/* View toggle */}
|
||||
<div className="flex items-center bg-bg-card border border-border rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setViewMode('topo')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm transition-colors ${
|
||||
viewMode === 'topo'
|
||||
? 'bg-accent text-white'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
<Network size={14} />
|
||||
Topology
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('geo')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm transition-colors ${
|
||||
viewMode === 'geo'
|
||||
? 'bg-accent text-white'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
<Map size={14} />
|
||||
Geographic
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main view area */}
|
||||
<div className="flex gap-0">
|
||||
{/* Graph/Map */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{viewMode === 'topo' ? (
|
||||
<TopologyGraph
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
selectedNodeId={selectedNodeId}
|
||||
onSelectNode={handleSelectNode}
|
||||
/>
|
||||
) : (
|
||||
<GeoMap
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
selectedNodeId={selectedNodeId}
|
||||
onSelectNode={handleSelectNode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Detail panel */}
|
||||
<NodeDetail
|
||||
node={selectedNode}
|
||||
edges={edges}
|
||||
nodes={nodes}
|
||||
onSelectNode={handleSelectNode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Node table */}
|
||||
<NodeTable
|
||||
nodes={nodes}
|
||||
selectedNodeId={selectedNodeId}
|
||||
onSelectNode={handleSelectNode}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue