feat(ui): add type filter toggles

This commit is contained in:
2026-02-18 23:13:28 -08:00
commit a4cff9894c
14457 changed files with 2204835 additions and 0 deletions

141
src/App.tsx Normal file
View File

@@ -0,0 +1,141 @@
import { useEffect, useRef, useCallback, useState } from 'react';
import { ReactFlowProvider } from '@xyflow/react';
import { useTopologyStore } from './store/topologyStore';
import {
defaultNetworkInfo,
defaultHosts,
discoverHosts,
convertToTopology
} from './services/discovery';
import Header from './components/Header';
import LeftPanel from './components/LeftPanel';
import RightPanel from './components/RightPanel';
import TopologyGraph from './components/Graph/TopologyGraph';
const POLLING_INTERVAL_MS = 30000;
function App() {
const {
setNodes,
setEdges,
setNetworkInfo,
setHosts,
setLastUpdated,
setIsLoading,
leftPanelOpen,
rightPanelOpen,
isLoading
} = useTopologyStore();
const isLoadingRef = useRef(isLoading);
isLoadingRef.current = isLoading;
const loadData = useCallback(async () => {
if (isLoadingRef.current) return;
setIsLoading(true);
try {
const hostNames = ['ubuntu', 'grizzley', 'truenas', 'ice', 'panda', 'proxmox'];
const discoveryResult = await discoverHosts(hostNames);
const { nodes, edges } = convertToTopology(
discoveryResult.hosts,
defaultNetworkInfo
);
const hosts = discoveryResult.hosts.map(h => ({
name: h.name,
ip: h.ip,
type: (h.name === 'ubuntu' ? 'vm' :
h.name === 'proxmox' || h.name === 'truenas' ? 'physical' : 'rpi5') as 'vm' | 'physical' | 'rpi5' | 'container',
role: h.name === 'ubuntu' ? 'Primary Docker Host' :
h.name === 'grizzley' ? 'Edge Services' :
h.name === 'truenas' ? 'Storage (NAS)' :
h.name === 'proxmox' ? 'Hypervisor' : 'Host',
containers: h.containers.map(c => c.name)
}));
setNodes(nodes);
setEdges(edges);
setNetworkInfo(defaultNetworkInfo);
setHosts(hosts);
setLastUpdated(new Date());
} catch (error) {
console.error('Discovery failed:', error);
setNodes([]);
setEdges([]);
setNetworkInfo(defaultNetworkInfo);
setHosts(defaultHosts);
setLastUpdated(new Date());
} finally {
setIsLoading(false);
}
}, [setNodes, setEdges, setNetworkInfo, setHosts, setLastUpdated, setIsLoading]);
useEffect(() => {
loadData();
const intervalId = setInterval(loadData, POLLING_INTERVAL_MS);
return () => clearInterval(intervalId);
}, [loadData]);
return (
<ReactFlowProvider>
<div className="h-screen w-screen flex flex-col bg-slate-900">
<Header onRefresh={loadData} isLoading={isLoading} />
<div className="flex-1 flex overflow-hidden">
{leftPanelOpen && (
<LeftPanel />
)}
<div className="flex-1">
<TopologyGraph />
</div>
{rightPanelOpen && (
<RightPanel />
)}
</div>
<Footer />
</div>
</ReactFlowProvider>
);
}
function Footer() {
const { lastUpdated, nodes } = useTopologyStore();
const [countdown, setCountdown] = useState(30);
const formatTime = (date: Date | null) => {
if (!date) return 'Never';
return date.toLocaleTimeString();
};
useEffect(() => {
setCountdown(30);
const intervalId = setInterval(() => {
setCountdown(prev => {
if (prev <= 1) return 30;
return prev - 1;
});
}, 1000);
return () => clearInterval(intervalId);
}, [lastUpdated]);
return (
<div className="h-8 bg-slate-800 border-t border-slate-700 px-4 flex items-center justify-between text-xs text-slate-400">
<span>Nodes: {nodes.length}</span>
<div className="flex items-center gap-4">
<span>Next refresh: {countdown}s</span>
<span>Last updated: {formatTime(lastUpdated)}</span>
</div>
</div>
);
}
export default App;