feat: implement swapper metrics and node cabinet ui
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useNodeSwapper, SwapperModel } from '@/hooks/useNodeSwapper';
|
||||
import { useState } from 'react';
|
||||
import { useNodeSwapper } from '@/hooks/useNodeSwapper';
|
||||
|
||||
interface NodeSwapperCardProps {
|
||||
nodeId: string;
|
||||
@@ -9,95 +9,111 @@ interface NodeSwapperCardProps {
|
||||
|
||||
export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
|
||||
const { swapper, isLoading, error, mutate } = useNodeSwapper(nodeId);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 animate-pulse">
|
||||
<div className="h-6 bg-white/10 rounded w-1/3 mb-4" />
|
||||
<div className="h-4 bg-white/10 rounded w-1/2" />
|
||||
<div className="h-6 w-32 bg-white/10 rounded mb-4" />
|
||||
<div className="h-4 w-full bg-white/5 rounded" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-red-500/10 backdrop-blur-md rounded-2xl border border-red-500/20 p-6 text-red-400">
|
||||
<h3 className="font-semibold flex items-center gap-2">
|
||||
<span>🧠</span> Swapper Service
|
||||
</h3>
|
||||
<p className="text-sm mt-2">Failed to load status</p>
|
||||
<div className="bg-red-500/10 backdrop-blur-md rounded-2xl border border-red-500/20 p-6">
|
||||
<h3 className="text-red-400 font-medium mb-2">Swapper Service Unavailable</h3>
|
||||
<p className="text-white/50 text-sm">Failed to load swapper details.</p>
|
||||
<button
|
||||
onClick={() => mutate()}
|
||||
className="mt-4 text-xs text-white/40 hover:text-white hover:underline"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!swapper) return null;
|
||||
|
||||
const statusColor = swapper.healthy ? 'text-green-400' : 'text-red-400';
|
||||
const statusIcon = swapper.healthy ? '🟢' : '🔴';
|
||||
const statusText = swapper.healthy ? 'Healthy' : 'Down';
|
||||
|
||||
return (
|
||||
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 transition-all hover:border-cyan-500/30">
|
||||
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<span>🧠</span> Swapper Service
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => mutate()}
|
||||
className="text-white/30 hover:text-white/70 transition-colors"
|
||||
title="Refresh"
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => mutate()}
|
||||
className="p-1.5 rounded-lg hover:bg-white/5 text-white/30 hover:text-white transition-colors"
|
||||
title="Refresh"
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Status Row */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-white/50">Status:</span>
|
||||
<span className={`font-medium flex items-center gap-1.5 ${statusColor}`}>
|
||||
{statusIcon} {statusText}
|
||||
</span>
|
||||
<div className="space-y-4">
|
||||
{/* Status */}
|
||||
<div className="bg-white/5 rounded-xl p-4 border border-white/5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-white/50 text-sm">Status</span>
|
||||
<div className={`flex items-center gap-2 px-2 py-1 rounded-full text-xs font-medium ${
|
||||
swapper.healthy ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
||||
}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${
|
||||
swapper.healthy ? 'bg-green-400' : 'bg-red-400'
|
||||
}`} />
|
||||
{swapper.healthy ? 'Healthy' : 'Unhealthy'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white/50 text-sm">Models Loaded</span>
|
||||
<span className="text-white font-mono">
|
||||
{swapper.models_loaded} <span className="text-white/30">/ {swapper.models_total}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Row */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-white/50">Models:</span>
|
||||
<span className="text-white font-medium">
|
||||
{swapper.models_loaded} / {swapper.models_total} loaded
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Details Toggle */}
|
||||
{/* Models List Toggle */}
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="w-full mt-2 py-2 text-xs font-medium text-white/40 hover:text-white/70 bg-white/5 hover:bg-white/10 rounded-lg transition-colors flex items-center justify-center gap-1"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="w-full flex items-center justify-between px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 transition-colors text-sm text-white/70"
|
||||
>
|
||||
{expanded ? 'Hide Models' : 'View Models'}
|
||||
<span>{expanded ? '▲' : '▼'}</span>
|
||||
<span>Available Models</span>
|
||||
<span className={`transition-transform ${isExpanded ? 'rotate-180' : ''}`}>▼</span>
|
||||
</button>
|
||||
|
||||
{/* Models List */}
|
||||
{expanded && (
|
||||
<div className="mt-3 space-y-2 border-t border-white/10 pt-3">
|
||||
{isExpanded && (
|
||||
<div className="space-y-2 mt-2">
|
||||
{swapper.models.length === 0 ? (
|
||||
<p className="text-white/30 text-xs text-center py-2">No models found</p>
|
||||
<p className="text-center text-white/30 text-sm py-2">No models found</p>
|
||||
) : (
|
||||
swapper.models.map((model, idx) => (
|
||||
<div key={`${model.name}-${idx}`} className="flex items-center justify-between text-xs bg-black/20 p-2 rounded border border-white/5">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-white font-medium">{model.name}</span>
|
||||
<span className="text-white/40 uppercase text-[10px]">{model.type || 'unknown'}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-0.5">
|
||||
<span className={model.loaded ? 'text-green-400' : 'text-white/30'}>
|
||||
{model.loaded ? '● LOADED' : '○ UNLOADED'}
|
||||
</span>
|
||||
{model.vram_gb && (
|
||||
<span className="text-white/30">{model.vram_gb.toFixed(1)} GB</span>
|
||||
)}
|
||||
<div
|
||||
key={`${model.name}-${idx}`}
|
||||
className="flex items-center justify-between p-3 rounded-lg bg-white/5 border border-white/5"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-white text-sm font-medium">{model.name}</span>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs text-white/40 uppercase px-1.5 py-0.5 rounded bg-white/5">
|
||||
{model.type || 'unknown'}
|
||||
</span>
|
||||
{model.vram_gb && (
|
||||
<span className="text-xs text-white/30">
|
||||
{model.vram_gb.toFixed(1)} GB VRAM
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`w-2 h-2 rounded-full ${
|
||||
model.loaded ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.5)]' : 'bg-white/10'
|
||||
}`} title={model.loaded ? 'Loaded in VRAM' : 'Not loaded'} />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -107,4 +123,3 @@ export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
export { StatusBadge } from './StatusBadge';
|
||||
export { ProgressBar } from './ProgressBar';
|
||||
export { NodeSummaryCard } from './NodeSummaryCard';
|
||||
export { InfraCard } from './InfraCard';
|
||||
export { AIServicesCard } from './AIServicesCard';
|
||||
export { AgentsCard } from './AgentsCard';
|
||||
export { MatrixCard } from './MatrixCard';
|
||||
export { ModulesCard } from './ModulesCard';
|
||||
export { NodeStandardComplianceCard } from './NodeStandardComplianceCard';
|
||||
export { DAGIRouterCard } from './DAGIRouterCard';
|
||||
export { NodeMetricsCard } from './NodeMetricsCard';
|
||||
import { NodeSummaryCard } from './NodeSummaryCard';
|
||||
import { InfraCard } from './InfraCard';
|
||||
import { AIServicesCard } from './AIServicesCard';
|
||||
import { AgentsCard } from './AgentsCard';
|
||||
import { MatrixCard } from './MatrixCard';
|
||||
import { ModulesCard } from './ModulesCard';
|
||||
import { NodeStandardComplianceCard } from './NodeStandardComplianceCard';
|
||||
import { DAGIRouterCard } from './DAGIRouterCard';
|
||||
import { NodeMetricsCard } from './NodeMetricsCard';
|
||||
import { NodeSwapperCard } from './NodeSwapperCard';
|
||||
|
||||
export {
|
||||
NodeSummaryCard,
|
||||
InfraCard,
|
||||
AIServicesCard,
|
||||
AgentsCard,
|
||||
MatrixCard,
|
||||
ModulesCard,
|
||||
NodeStandardComplianceCard,
|
||||
DAGIRouterCard,
|
||||
NodeMetricsCard,
|
||||
NodeSwapperCard
|
||||
};
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import useSWR from "swr";
|
||||
|
||||
const fetcher = (url: string) => fetch(url, { credentials: "include" }).then(r => {
|
||||
if (!r.ok) throw new Error(`Failed to load ${url}`);
|
||||
return r.json();
|
||||
});
|
||||
import useSWR from 'swr';
|
||||
|
||||
export interface SwapperModel {
|
||||
name: string;
|
||||
@@ -20,7 +15,9 @@ export interface NodeSwapperDetail {
|
||||
models: SwapperModel[];
|
||||
}
|
||||
|
||||
export function useNodeSwapper(nodeId?: string) {
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
|
||||
export function useNodeSwapper(nodeId: string) {
|
||||
const { data, error, isLoading, mutate } = useSWR<NodeSwapperDetail>(
|
||||
nodeId ? `/api/v1/internal/node/${nodeId}/swapper` : null,
|
||||
fetcher
|
||||
@@ -33,4 +30,3 @@ export function useNodeSwapper(nodeId?: string) {
|
||||
mutate,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user