feat: tab based changes in ui and and labeling of client system disks
This commit is contained in:
163
src/App.jsx
163
src/App.jsx
@@ -56,6 +56,7 @@ function App() {
|
||||
const [toast, setToast] = useState(null);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [filterClient, setFilterClient] = useState('all');
|
||||
const [nodeFilter, setNodeFilter] = useState('all'); // 'all' | 'online' | 'offline'
|
||||
const [searchQuery, setSearchQuery] = useState(''); // Live search filter for clients
|
||||
const [expandedClients, setExpandedClients] = useState({}); // Expanded telemetry details for offline nodes
|
||||
const terminalRef = useRef(null);
|
||||
@@ -249,10 +250,15 @@ function App() {
|
||||
const activeClients = Object.values(clients).filter(c => c.active).length;
|
||||
const totalClients = Object.keys(clients).length;
|
||||
|
||||
// Filter clients based on search query
|
||||
const filteredClientEntries = Object.entries(clients).filter(([clientId]) =>
|
||||
clientId.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
// Filter clients based on search query AND node status tab
|
||||
const filteredClientEntries = Object.entries(clients).filter(([clientId, info]) => {
|
||||
const matchesSearch = clientId.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesFilter =
|
||||
nodeFilter === 'all' ||
|
||||
(nodeFilter === 'online' && info.active) ||
|
||||
(nodeFilter === 'offline' && !info.active);
|
||||
return matchesSearch && matchesFilter;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-16 flex flex-col font-sans antialiased text-slate-100 bg-[#050811]">
|
||||
@@ -342,40 +348,87 @@ function App() {
|
||||
{/* Dashboard Vitals: Grid of Cards (Left/Center Column) */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
|
||||
{/* Central Metrics Board */}
|
||||
{/* Central Metrics Board — clickable filter tabs */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden group">
|
||||
|
||||
{/* ALL tab */}
|
||||
<button
|
||||
onClick={() => setNodeFilter('all')}
|
||||
className={`glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden group text-left transition-all duration-200 ${
|
||||
nodeFilter === 'all'
|
||||
? 'ring-2 ring-violet-500/60 shadow-[0_0_20px_rgba(139,92,246,0.15)]'
|
||||
: 'hover:ring-1 hover:ring-violet-500/20 hover:shadow-[0_0_12px_rgba(139,92,246,0.08)]'
|
||||
}`}
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-violet-600/5 rounded-full blur-2xl group-hover:bg-violet-600/10 transition-all"></div>
|
||||
<div className="w-12 h-12 rounded-xl bg-violet-500/10 border border-violet-500/20 flex items-center justify-center text-violet-400 shrink-0">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-all ${
|
||||
nodeFilter === 'all'
|
||||
? 'bg-violet-500/25 border-2 border-violet-500/60 text-violet-300'
|
||||
: 'bg-violet-500/10 border border-violet-500/20 text-violet-400'
|
||||
}`}>
|
||||
<Server className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Total Fleet Nodes</span>
|
||||
<span className="text-3xl font-extrabold text-white mt-0.5 block">{totalClients}</span>
|
||||
{nodeFilter === 'all' && (
|
||||
<span className="text-[9px] text-violet-400 font-bold uppercase tracking-wider mt-0.5 block">● Active Filter</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden border-l-2 border-l-emerald-500 group">
|
||||
{/* ONLINE tab */}
|
||||
<button
|
||||
onClick={() => setNodeFilter('online')}
|
||||
className={`glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden border-l-2 border-l-emerald-500 group text-left transition-all duration-200 ${
|
||||
nodeFilter === 'online'
|
||||
? 'ring-2 ring-emerald-500/60 shadow-[0_0_20px_rgba(16,185,129,0.15)]'
|
||||
: 'hover:ring-1 hover:ring-emerald-500/20 hover:shadow-[0_0_12px_rgba(16,185,129,0.08)]'
|
||||
}`}
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-emerald-500/5 rounded-full blur-2xl group-hover:bg-emerald-500/10 transition-all"></div>
|
||||
<div className="w-12 h-12 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-400 shrink-0">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-all ${
|
||||
nodeFilter === 'online'
|
||||
? 'bg-emerald-500/25 border-2 border-emerald-500/60 text-emerald-300'
|
||||
: 'bg-emerald-500/10 border border-emerald-500/20 text-emerald-400'
|
||||
}`}>
|
||||
<Activity className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider block">🟢 Online Nodes</span>
|
||||
<span className="text-3xl font-extrabold text-emerald-400 mt-0.5 block">{activeClients}</span>
|
||||
{nodeFilter === 'online' && (
|
||||
<span className="text-[9px] text-emerald-400 font-bold uppercase tracking-wider mt-0.5 block">● Active Filter</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden border-l-2 border-l-rose-500 group">
|
||||
{/* OFFLINE tab */}
|
||||
<button
|
||||
onClick={() => setNodeFilter('offline')}
|
||||
className={`glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden border-l-2 border-l-rose-500 group text-left transition-all duration-200 ${
|
||||
nodeFilter === 'offline'
|
||||
? 'ring-2 ring-rose-500/60 shadow-[0_0_20px_rgba(244,63,94,0.15)]'
|
||||
: 'hover:ring-1 hover:ring-rose-500/20 hover:shadow-[0_0_12px_rgba(244,63,94,0.08)]'
|
||||
}`}
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-rose-500/5 rounded-full blur-2xl group-hover:bg-rose-500/10 transition-all"></div>
|
||||
<div className="w-12 h-12 rounded-xl bg-rose-500/10 border border-rose-500/20 flex items-center justify-center text-rose-400 shrink-0">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-all ${
|
||||
nodeFilter === 'offline'
|
||||
? 'bg-rose-500/25 border-2 border-rose-500/60 text-rose-300'
|
||||
: 'bg-rose-500/10 border border-rose-500/20 text-rose-400'
|
||||
}`}>
|
||||
<AlertTriangle className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider block">🔴 Offline Nodes</span>
|
||||
<span className="text-3xl font-extrabold text-rose-400 mt-0.5 block">{totalClients - activeClients}</span>
|
||||
{nodeFilter === 'offline' && (
|
||||
<span className="text-[9px] text-rose-400 font-bold uppercase tracking-wider mt-0.5 block">● Active Filter</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Vitals Node Grid Cards */}
|
||||
@@ -384,8 +437,27 @@ function App() {
|
||||
<h2 className="text-xs font-bold text-slate-300 tracking-wider uppercase flex items-center gap-2">
|
||||
<Sliders className="w-4 h-4 text-violet-400" />
|
||||
Remote Fleet Systems Grid ({filteredClientEntries.length})
|
||||
{nodeFilter !== 'all' && (
|
||||
<span className={`text-[9px] font-bold px-2 py-0.5 rounded-full uppercase tracking-wider ${
|
||||
nodeFilter === 'online'
|
||||
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
|
||||
: 'bg-rose-500/10 text-rose-400 border border-rose-500/20'
|
||||
}`}>
|
||||
{nodeFilter === 'online' ? '🟢 Online' : '🔴 Offline'}
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<div className="flex items-center gap-2 w-full sm:w-auto">
|
||||
{/* Clear filter shortcut */}
|
||||
{nodeFilter !== 'all' && (
|
||||
<button
|
||||
onClick={() => setNodeFilter('all')}
|
||||
className="text-[10px] font-bold text-slate-400 hover:text-slate-200 uppercase tracking-wider bg-white/5 hover:bg-white/10 px-2.5 py-1.5 rounded-lg border border-white/5 transition-all whitespace-nowrap"
|
||||
>
|
||||
✕ Clear Filter
|
||||
</button>
|
||||
)}
|
||||
{/* Node Search Bar */}
|
||||
<div className="relative w-full sm:w-64">
|
||||
<Search className="w-4 h-4 text-slate-500 absolute left-3 top-2.5" />
|
||||
@@ -398,6 +470,7 @@ function App() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredClientEntries.length === 0 ? (
|
||||
<div className="glass-panel p-12 rounded-2xl text-center text-slate-400">
|
||||
@@ -580,23 +653,67 @@ function App() {
|
||||
const diskTotal = d.total_gb || 0;
|
||||
const diskFree = d.free_gb || 0;
|
||||
const diskUsed = (diskTotal - diskFree).toFixed(2);
|
||||
|
||||
// Get friendly label
|
||||
const diskLabel = d.label || (d.mount === '/' ? 'Computer' : (d.mount.split(/[/\\]/).pop() || 'Volume'));
|
||||
|
||||
return (
|
||||
<div key={index} className="p-3 rounded-xl bg-slate-950/30 border border-white/5 space-y-2">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="font-bold text-slate-300">Disk [ {d.mount} ]</span>
|
||||
<span className="font-mono text-slate-400 text-[10px]">
|
||||
Used: <b className="text-slate-300">{diskUsed}G</b> / {diskTotal}G
|
||||
<div key={index} className="p-4 rounded-xl bg-slate-950/40 border border-white/5 hover:border-violet-500/15 transition-all duration-200 space-y-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
{/* Left side: Icon, Label, Device badge, Mount path */}
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="p-2 rounded-lg bg-slate-900 border border-white/5 text-indigo-400 shrink-0">
|
||||
<HardDrive className="w-4 h-4 text-indigo-400" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<span className="font-extrabold text-white text-xs block truncate">
|
||||
{diskLabel}
|
||||
</span>
|
||||
<span className="text-[9px] text-slate-400 font-mono flex flex-wrap items-center gap-1 mt-0.5">
|
||||
{d.device && (
|
||||
<span className="px-1.5 py-0.5 rounded bg-slate-900 border border-white/5 font-semibold text-[8px] text-slate-300">
|
||||
{d.device}
|
||||
</span>
|
||||
)}
|
||||
{d.device && <span className="text-slate-600">on</span>}
|
||||
<span className="text-slate-400 truncate max-w-[120px]" title={d.mount}>
|
||||
{d.mount}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-1.5 rounded-full bg-slate-900 overflow-hidden">
|
||||
</div>
|
||||
|
||||
{/* Right side: Free Space / Total */}
|
||||
<div className="text-right shrink-0">
|
||||
<span className="text-[11px] font-bold text-slate-300 font-mono block">
|
||||
{diskFree} GB free
|
||||
</span>
|
||||
<span className="text-[9px] text-slate-500 font-mono block mt-0.5">
|
||||
of {diskTotal} GB
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="w-full h-1.5 rounded-full bg-slate-900 overflow-hidden relative border border-white/5">
|
||||
<div
|
||||
className={`h-full rounded-full ${d.percent > 85 ? 'bg-rose-505' : 'bg-indigo-500'}`}
|
||||
className={`h-full rounded-full transition-all duration-500 ${
|
||||
d.percent > 85
|
||||
? 'bg-gradient-to-r from-rose-500 to-red-500'
|
||||
: d.percent > 70
|
||||
? 'bg-gradient-to-r from-amber-500 to-yellow-500'
|
||||
: 'bg-gradient-to-r from-indigo-500 to-violet-500'
|
||||
}`}
|
||||
style={{ width: `${d.percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between text-[9px] text-slate-500 font-mono">
|
||||
<span>Free: <b className="text-slate-400">{diskFree} GB</b></span>
|
||||
<span>{d.percent}% Used</span>
|
||||
<span>{diskUsed} GB Used</span>
|
||||
<span className={d.percent > 85 ? "text-rose-400 font-bold" : ""}>
|
||||
{d.percent}% Used
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user