feat(ui): add type filter toggles
This commit is contained in:
256
src/components/RightPanel.tsx
Normal file
256
src/components/RightPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user