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

View File

@@ -0,0 +1,256 @@
import { useState } from 'react';
import { Info, FileCode, FolderOpen, BarChart3, Star, X } from 'lucide-react';
import { useTopologyStore } from '../store/topologyStore';
import { getNodeColor, getStatusColor, getImportanceLabel, getImportanceColor } from '../utils/colors';
type TabId = 'details' | 'config' | 'files' | 'usage' | 'importance';
const tabs: { id: TabId; label: string; icon: React.ReactNode }[] = [
{ id: 'details', label: 'Details', icon: <Info className="w-4 h-4" /> },
{ id: 'config', label: 'Config', icon: <FileCode className="w-4 h-4" /> },
{ id: 'files', label: 'Files', icon: <FolderOpen className="w-4 h-4" /> },
{ id: 'usage', label: 'Usage', icon: <BarChart3 className="w-4 h-4" /> },
{ id: 'importance', label: 'Importance', icon: <Star className="w-4 h-4" /> },
];
export default function RightPanel() {
const { nodes, selectedNodeId, setSelectedNode } = useTopologyStore();
const [activeTab, setActiveTab] = useState<TabId>('details');
const selectedNode = nodes.find(n => n.id === selectedNodeId);
if (!selectedNode) {
return (
<div className="w-80 bg-slate-800 border-l border-slate-700 flex flex-col items-center justify-center p-4">
<div className="text-slate-500 text-sm text-center">
Select a node to view its details
</div>
</div>
);
}
const nodeColor = getNodeColor(selectedNode.type, selectedNode.data.category);
const statusColor = getStatusColor(selectedNode.data.status);
return (
<div className="w-80 bg-slate-800 border-l border-slate-700 flex flex-col">
<div className="h-12 px-4 flex items-center justify-between border-b border-slate-700">
<h2 className="text-sm font-semibold text-white truncate">{selectedNode.name}</h2>
<button
onClick={() => setSelectedNode(null)}
className="p-1 hover:bg-slate-700 rounded transition-colors"
>
<X className="w-4 h-4 text-slate-400" />
</button>
</div>
<div className="flex border-b border-slate-700">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex-1 flex items-center justify-center gap-1 py-3 text-xs transition-colors ${
activeTab === tab.id
? 'text-indigo-400 border-b-2 border-indigo-400 bg-indigo-500/10'
: 'text-slate-400 hover:text-white'
}`}
>
{tab.icon}
</button>
))}
</div>
<div className="flex-1 overflow-y-auto p-4">
{activeTab === 'details' && <DetailsTab node={selectedNode} nodeColor={nodeColor} statusColor={statusColor} />}
{activeTab === 'config' && <ConfigTab node={selectedNode} />}
{activeTab === 'files' && <FilesTab node={selectedNode} />}
{activeTab === 'usage' && <UsageTab node={selectedNode} />}
{activeTab === 'importance' && <ImportanceTab node={selectedNode} />}
</div>
</div>
);
}
function DetailsTab({ node, nodeColor, statusColor }: { node: any; nodeColor: string; statusColor: string }) {
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
<div
className="w-12 h-12 rounded-xl flex items-center justify-center"
style={{ backgroundColor: `${nodeColor}20` }}
>
<div style={{ color: nodeColor }} className="text-lg font-bold">
{node.name.charAt(0).toUpperCase()}
</div>
</div>
<div>
<div className="text-white font-medium">{node.type.replace(/_/g, ' ')}</div>
<div className="flex items-center gap-2 text-sm">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: statusColor }}
/>
<span className="text-slate-400 capitalize">{node.data.status}</span>
</div>
</div>
</div>
<div className="space-y-3">
<div>
<div className="text-xs text-slate-500 uppercase tracking-wide mb-1">IP Address</div>
<div className="font-mono text-sm text-white">{node.data.ip || 'N/A'}</div>
</div>
{node.data.description && (
<div>
<div className="text-xs text-slate-500 uppercase tracking-wide mb-1">Description</div>
<div className="text-sm text-slate-300">{node.data.description}</div>
</div>
)}
<div>
<div className="text-xs text-slate-500 uppercase tracking-wide mb-1">Metadata</div>
<div className="bg-slate-900 rounded-lg p-3 font-mono text-xs text-slate-300 overflow-x-auto">
{JSON.stringify(node.data.metadata, null, 2)}
</div>
</div>
</div>
</div>
);
}
function ConfigTab({ node }: { node: any }) {
const hasConfig = node.data.config;
if (!hasConfig) {
return (
<div className="text-center text-slate-500 py-8">
<FileCode className="w-8 h-8 mx-auto mb-2 opacity-50" />
<div className="text-sm">No configuration available</div>
</div>
);
}
return (
<div>
<pre className="bg-slate-900 rounded-lg p-3 font-mono text-xs text-slate-300 overflow-x-auto">
{node.data.config}
</pre>
</div>
);
}
function FilesTab({ node }: { node: any }) {
const files = node.data.files || [
'/etc/docker-compose.yml',
'/etc/traefik/dynamic.yml',
'/var/log/container.log'
];
return (
<div className="space-y-1">
{files.map((file: string, idx: number) => (
<button
key={idx}
className="w-full px-3 py-2 flex items-center gap-2 bg-slate-700/50 hover:bg-slate-700 rounded-lg text-left transition-colors"
>
<FolderOpen className="w-4 h-4 text-slate-400" />
<span className="font-mono text-xs text-slate-300 truncate">{file}</span>
</button>
))}
</div>
);
}
function UsageTab({ node }: { node: any }) {
const isService = node.type === 'service';
if (!isService) {
return (
<div className="text-center text-slate-500 py-8">
<BarChart3 className="w-8 h-8 mx-auto mb-2 opacity-50" />
<div className="text-sm">Usage data available for services only</div>
</div>
);
}
return (
<div className="space-y-4">
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-slate-400">CPU</span>
<span className="text-white">12.4%</span>
</div>
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
<div className="h-full w-[12.4%] bg-indigo-500 rounded-full" />
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-slate-400">Memory</span>
<span className="text-white">256 MB / 1 GB</span>
</div>
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
<div className="h-full w-[25.6%] bg-purple-500 rounded-full" />
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-slate-400">Network I/O</span>
<span className="text-white">1.2 MB/s 0.8 MB/s </span>
</div>
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
<div className="h-full w-[40%] bg-cyan-500 rounded-full" />
</div>
</div>
</div>
);
}
function ImportanceTab({ node }: { node: any }) {
const importance = node.data.importance || 3;
const importanceLabel = getImportanceLabel(importance);
const importanceColor = getImportanceColor(importance);
const reasons = {
5: ['Critical infrastructure', 'Single point of failure', 'Required for other services'],
4: ['Important service', 'Used frequently', 'Difficult to replace'],
3: ['Standard service', 'Can be rebuilt', 'Not critical'],
2: ['Optional service', 'Rarely used', 'Easy to recreate'],
1: ['Development only', 'Non-critical', 'Can be disabled'],
};
return (
<div className="space-y-4">
<div className="flex items-center justify-center gap-2">
{[1, 2, 3, 4, 5].map(star => (
<Star
key={star}
className={`w-8 h-8 ${star <= importance ? 'fill-yellow-500 text-yellow-500' : 'text-slate-600'}`}
/>
))}
</div>
<div className="text-center">
<div className="text-lg font-semibold" style={{ color: importanceColor }}>
{importanceLabel}
</div>
<div className="text-sm text-slate-400">Importance Level {importance}/5</div>
</div>
<div className="bg-slate-700/50 rounded-lg p-3">
<div className="text-xs text-slate-500 uppercase tracking-wide mb-2">Why this level?</div>
<ul className="space-y-1">
{reasons[importance as keyof typeof reasons]?.map((reason, idx) => (
<li key={idx} className="text-sm text-slate-300 flex items-center gap-2">
<div className="w-1 h-1 bg-slate-500 rounded-full" />
{reason}
</li>
))}
</ul>
</div>
</div>
);
}