feat: System Information grid primitives — auto-placing 2-column grid
This commit is contained in:
parent
2084019be7
commit
1fa76bf665
6 changed files with 348 additions and 209 deletions
55
web/src/components/primitives/MetricItem.tsx
Normal file
55
web/src/components/primitives/MetricItem.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* MetricItem — a single system-info metric cell.
|
||||
*
|
||||
* Renders a label, value, optional icon, optional sub-text, and optional
|
||||
* progress bar. Designed to be an independent grid item inside SystemInfoGrid.
|
||||
*
|
||||
* Usage:
|
||||
* <MetricItem label="CPU" value="Intel i7" icon={Cpu} sub="4 cores" />
|
||||
* <MetricItem label="Disk" value="120 / 500 GB" icon={HardDrive}
|
||||
* progress={{ used: 120, total: 500 }} />
|
||||
*/
|
||||
interface MetricItemProps {
|
||||
label: string;
|
||||
value: string | number;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
sub?: string;
|
||||
progress?: { used: number; total: number };
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const MetricItem: React.FC<MetricItemProps> = ({
|
||||
label,
|
||||
value,
|
||||
icon: Icon,
|
||||
sub,
|
||||
progress,
|
||||
className,
|
||||
}) => (
|
||||
<div className={cn('min-w-0', className)}>
|
||||
<p className="text-xs text-gray-500 flex items-center gap-1">
|
||||
{Icon && <Icon className="h-3 w-3" />}
|
||||
{label}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-900">{value}</p>
|
||||
{sub && <p className="text-xs text-gray-500">{sub}</p>}
|
||||
{progress && progress.total > 0 && (
|
||||
<>
|
||||
<div className="w-full bg-gray-200 rounded-full h-1.5 mt-1">
|
||||
<div
|
||||
className="bg-blue-600 h-1.5 rounded-full"
|
||||
style={{ width: `${Math.round((progress.used / progress.total) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
{Math.round((progress.used / progress.total) * 100)}% used
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default MetricItem;
|
||||
77
web/src/components/primitives/ProcessTable.tsx
Normal file
77
web/src/components/primitives/ProcessTable.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import React from 'react';
|
||||
import { Activity } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* ProcessTable — top processes display for the System Information grid.
|
||||
*
|
||||
* Renders a compact table (Name, PID, CPU%, Mem%) when process data is
|
||||
* available, or a fallback message with the process count.
|
||||
*
|
||||
* Designed as an independent grid item inside SystemInfoGrid.
|
||||
*/
|
||||
interface ProcessInfo {
|
||||
name: string;
|
||||
pid: number;
|
||||
cpu: number | null;
|
||||
mem: number | null;
|
||||
}
|
||||
|
||||
interface ProcessTableProps {
|
||||
processes?: ProcessInfo[];
|
||||
processCount?: number | string;
|
||||
onSeeMore?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ProcessTable: React.FC<ProcessTableProps> = ({
|
||||
processes,
|
||||
processCount,
|
||||
onSeeMore,
|
||||
className,
|
||||
}) => (
|
||||
<div className={cn('min-w-0', className)}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-xs font-medium text-gray-900 flex items-center gap-1">
|
||||
<Activity className="h-3 w-3" /> Top Processes
|
||||
</p>
|
||||
{onSeeMore && (
|
||||
<button onClick={onSeeMore} className="text-[10px] text-blue-600 hover:text-blue-800">
|
||||
See More →
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{processes && processes.length > 0 ? (
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-gray-500 border-b border-gray-100">
|
||||
<th className="text-left py-1 font-medium">Name</th>
|
||||
<th className="text-right py-1 font-medium">PID</th>
|
||||
<th className="text-right py-1 font-medium">CPU%</th>
|
||||
<th className="text-right py-1 font-medium">Mem%</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{processes.slice(0, 5).map((proc, i) => (
|
||||
<tr key={proc.pid || i} className="border-b border-gray-50 last:border-0">
|
||||
<td className="py-1 text-gray-900 font-medium truncate max-w-[160px]">{proc.name}</td>
|
||||
<td className="py-1 text-right text-gray-600">{proc.pid}</td>
|
||||
<td className="py-1 text-right text-gray-600">
|
||||
{proc.cpu != null ? `${proc.cpu.toFixed(1)}%` : '—'}
|
||||
</td>
|
||||
<td className="py-1 text-right text-gray-600">
|
||||
{proc.mem != null ? `${proc.mem.toFixed(1)}%` : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<p className="text-[10px] text-gray-400 italic">
|
||||
Process details not reported. {processCount != null && `Count: ${processCount}`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ProcessTable;
|
||||
114
web/src/components/primitives/ScreenshotCard.tsx
Normal file
114
web/src/components/primitives/ScreenshotCard.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import React from 'react';
|
||||
import { MonitorPlay, RefreshCw } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* ScreenshotCard — screenshot thumbnail with sunshine overlay and capture state.
|
||||
*
|
||||
* Renders the agent's screenshot (or capture prompt), live/stream badges,
|
||||
* and timestamp. Designed as a grid item inside SystemInfoGrid with
|
||||
* col-start-1 row-span-2 to anchor top-left.
|
||||
*
|
||||
* Handlers are passed in as props — this component doesn't own mutations.
|
||||
*/
|
||||
interface SunshineInfo {
|
||||
web_ui?: string;
|
||||
version?: string;
|
||||
state: string; // 'active' | 'running' | 'stopped' | ...
|
||||
}
|
||||
|
||||
interface ScreenshotStatus {
|
||||
status: string;
|
||||
completed_at?: string;
|
||||
}
|
||||
|
||||
interface ScreenshotCardProps {
|
||||
image?: string; // base64 PNG
|
||||
isCapturing: boolean;
|
||||
isPolling: boolean;
|
||||
sunshine?: SunshineInfo;
|
||||
screenshotStatus?: ScreenshotStatus;
|
||||
onCapture: () => void;
|
||||
formatRelativeTime?: (t: string) => string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ScreenshotCard: React.FC<ScreenshotCardProps> = ({
|
||||
image,
|
||||
isCapturing,
|
||||
isPolling,
|
||||
sunshine,
|
||||
screenshotStatus,
|
||||
onCapture,
|
||||
formatRelativeTime,
|
||||
className,
|
||||
}) => {
|
||||
const live = sunshine?.state === 'active';
|
||||
const sunshineReady = live || sunshine?.state === 'running';
|
||||
const hasImage = !!image;
|
||||
const canClick = !isCapturing && !isPolling;
|
||||
|
||||
return (
|
||||
<div className={cn('min-w-0', className)}>
|
||||
<div
|
||||
className={cn(
|
||||
'relative aspect-video w-full overflow-hidden rounded border',
|
||||
'bg-gradient-to-br from-slate-800 to-slate-900 border-slate-700',
|
||||
'flex flex-col items-center justify-center gap-1 text-slate-400',
|
||||
canClick && 'cursor-pointer hover:from-slate-700 hover:to-slate-800 transition-colors'
|
||||
)}
|
||||
onClick={() => {
|
||||
if (sunshineReady && sunshine?.web_ui) {
|
||||
window.open(sunshine.web_ui, '_blank', 'noreferrer');
|
||||
} else if (canClick) {
|
||||
onCapture();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{hasImage ? (
|
||||
<img
|
||||
src={`data:image/png;base64,${image}`}
|
||||
alt="Agent screenshot"
|
||||
className="absolute inset-0 w-full h-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<MonitorPlay className="h-6 w-6 opacity-70" />
|
||||
<span className="text-[10px]">
|
||||
{isCapturing ? 'Requesting…'
|
||||
: isPolling ? 'Capturing…'
|
||||
: sunshineReady ? 'Open stream'
|
||||
: 'Click to capture'}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{live && (
|
||||
<span className="absolute top-1 left-1 flex items-center gap-1 rounded bg-red-600/90 px-1 py-0.5 text-[9px] font-medium text-white">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-white animate-pulse" />
|
||||
LIVE
|
||||
</span>
|
||||
)}
|
||||
{sunshineReady && !live && (
|
||||
<span className="absolute top-1 right-1 rounded bg-black/50 px-1 py-0.5 text-[9px] text-white">
|
||||
Sunshine
|
||||
</span>
|
||||
)}
|
||||
{(isCapturing || isPolling) && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
||||
<RefreshCw className="h-5 w-5 text-white animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-1 text-[10px] text-gray-500">
|
||||
{sunshine?.version ? <span>Sunshine v{sunshine.version}</span> : <span />}
|
||||
{screenshotStatus?.status === 'completed' && screenshotStatus?.completed_at && formatRelativeTime
|
||||
? <span>{formatRelativeTime(screenshotStatus.completed_at)}</span>
|
||||
: screenshotStatus?.status === 'failed'
|
||||
? <span className="text-red-500">Failed</span>
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScreenshotCard;
|
||||
36
web/src/components/primitives/SystemInfoGrid.tsx
Normal file
36
web/src/components/primitives/SystemInfoGrid.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* SystemInfoGrid — CSS Grid container for system information primitives.
|
||||
*
|
||||
* Items auto-place into a 2-column grid that collapses to 1 on narrow
|
||||
* viewports. The screenshot (or any anchor item) gets col-start-1
|
||||
* row-span-2 via its own className prop.
|
||||
*
|
||||
* Usage:
|
||||
* <SystemInfoGrid>
|
||||
* <ScreenshotCard className="col-start-1 row-span-2" ... />
|
||||
* <MetricItem label="CPU" ... />
|
||||
* <MetricItem label="Memory" ... />
|
||||
* <ProcessTable ... />
|
||||
* </SystemInfoGrid>
|
||||
*/
|
||||
interface SystemInfoGridProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SystemInfoGrid: React.FC<SystemInfoGridProps> = ({ children, className }) => (
|
||||
<div
|
||||
className={cn('grid gap-6', className)}
|
||||
style={{
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
|
||||
gridAutoRows: 'min-content',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SystemInfoGrid;
|
||||
|
|
@ -24,6 +24,10 @@ import {
|
|||
Upload,
|
||||
} from 'lucide-react';
|
||||
import { SearchInput, FilterDropdown, PageState } from '@/components/primitives';
|
||||
import SystemInfoGrid from '@/components/primitives/SystemInfoGrid';
|
||||
import ScreenshotCard from '@/components/primitives/ScreenshotCard';
|
||||
import MetricItem from '@/components/primitives/MetricItem';
|
||||
import ProcessTable from '@/components/primitives/ProcessTable';
|
||||
import { useDebounce } from '@/hooks/useDebounce';
|
||||
import { useAgents, useAgent, useScanMultipleAgents, useUnregisterAgent } from '@/hooks/useAgents';
|
||||
import { useActiveCommands, useCancelCommand, useCaptureScreenshot, useCommand } from '@/hooks/useCommands';
|
||||
|
|
@ -821,218 +825,65 @@ const Agents: React.FC = () => {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* System info — screenshot left, stats right, top processes below */}
|
||||
{/* System info — independent primitives in a CSS Grid */}
|
||||
<div className="card">
|
||||
<h2 className="text-lg font-medium text-gray-900 mb-4">System Information</h2>
|
||||
|
||||
<div className="flex gap-6">
|
||||
{/* Screenshot — left, fixed width */}
|
||||
{(() => {
|
||||
const integrations = readIntegrations(selectedAgent.metadata);
|
||||
const sunshine = integrations.sunshine;
|
||||
const state = resolveState(sunshine);
|
||||
const live = state === 'active';
|
||||
const sunshineReady = live || state === 'running';
|
||||
const isCapturing = captureScreenshotMutation.isPending;
|
||||
const isPolling = screenshotCommand && !['completed', 'failed', 'timed_out', 'cancelled'].includes(screenshotCommand.status);
|
||||
const hasImage = !!screenshotImage;
|
||||
const canClick = !isCapturing && !isPolling;
|
||||
{(() => {
|
||||
const integrations = readIntegrations(selectedAgent.metadata);
|
||||
const sunshine = integrations.sunshine;
|
||||
const osInfo = parseOSInfo(selectedAgent);
|
||||
const meta = getSystemMetadata(selectedAgent);
|
||||
const topProcesses = selectedAgent.metadata?.top_processes;
|
||||
|
||||
return (
|
||||
<div className="shrink-0 w-[260px]">
|
||||
<div
|
||||
className={cn(
|
||||
'relative aspect-video w-full overflow-hidden rounded border',
|
||||
'bg-gradient-to-br from-slate-800 to-slate-900 border-slate-700',
|
||||
'flex flex-col items-center justify-center gap-1 text-slate-400',
|
||||
canClick && 'cursor-pointer hover:from-slate-700 hover:to-slate-800 transition-colors'
|
||||
)}
|
||||
onClick={() => {
|
||||
if (sunshineReady && sunshine?.web_ui) {
|
||||
window.open(sunshine.web_ui, '_blank', 'noreferrer');
|
||||
} else if (canClick) {
|
||||
handleCaptureScreenshot(selectedAgent.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{hasImage ? (
|
||||
<img
|
||||
src={`data:image/png;base64,${screenshotImage}`}
|
||||
alt="Agent screenshot"
|
||||
className="absolute inset-0 w-full h-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<MonitorPlay className="h-6 w-6 opacity-70" />
|
||||
<span className="text-[10px]">
|
||||
{isCapturing ? 'Requesting…'
|
||||
: isPolling ? 'Capturing…'
|
||||
: sunshineReady ? 'Open stream'
|
||||
: 'Click to capture'}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{live && (
|
||||
<span className="absolute top-1 left-1 flex items-center gap-1 rounded bg-red-600/90 px-1 py-0.5 text-[9px] font-medium text-white">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-white animate-pulse" />
|
||||
LIVE
|
||||
</span>
|
||||
)}
|
||||
{sunshineReady && !live && (
|
||||
<span className="absolute top-1 right-1 rounded bg-black/50 px-1 py-0.5 text-[9px] text-white">
|
||||
Sunshine
|
||||
</span>
|
||||
)}
|
||||
{(isCapturing || isPolling) && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
||||
<RefreshCw className="h-5 w-5 text-white animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-1 text-[10px] text-gray-500">
|
||||
{sunshine?.version ? <span>Sunshine v{sunshine.version}</span> : <span />}
|
||||
{screenshotCommand?.status === 'completed' && screenshotCommand?.completed_at
|
||||
? <span>{formatRelativeTime(screenshotCommand.completed_at)}</span>
|
||||
: screenshotCommand?.status === 'failed'
|
||||
? <span className="text-red-500">Failed</span>
|
||||
: null}
|
||||
</div>
|
||||
{/* Platform info — under screenshot */}
|
||||
{(() => {
|
||||
const osInfo = parseOSInfo(selectedAgent);
|
||||
return (
|
||||
<div className="mt-2 space-y-1">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Platform</p>
|
||||
<p className="text-sm font-medium text-gray-900">{osInfo.platform}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Distribution</p>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{osInfo.distribution}
|
||||
{osInfo.version && <span className="text-xs text-gray-500 ml-1">({osInfo.version})</span>}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Architecture</p>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{selectedAgent.os_architecture || selectedAgent.architecture}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* All stats — right column, stacked flush top */}
|
||||
<div className="flex-1 space-y-3 min-w-0">
|
||||
{(() => {
|
||||
const meta = getSystemMetadata(selectedAgent);
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 flex items-center gap-1">
|
||||
<Cpu className="h-3 w-3" /> CPU
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-900">{meta.cpuModel}</p>
|
||||
<p className="text-xs text-gray-500">{meta.cpuCores} cores</p>
|
||||
</div>
|
||||
{meta.memoryTotal > 0 && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 flex items-center gap-1">
|
||||
<MemoryStick className="h-3 w-3" /> Memory
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-900">{formatBytes(meta.memoryTotal)}</p>
|
||||
</div>
|
||||
)}
|
||||
{meta.diskTotal > 0 && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 flex items-center gap-1">
|
||||
<HardDrive className="h-3 w-3" /> Disk ({meta.diskMount})
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{formatBytes(meta.diskUsed)} / {formatBytes(meta.diskTotal)}
|
||||
</p>
|
||||
<div className="w-full bg-gray-200 rounded-full h-1.5 mt-1">
|
||||
<div
|
||||
className="bg-blue-600 h-1.5 rounded-full"
|
||||
style={{ width: `${Math.round((meta.diskUsed / meta.diskTotal) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">{Math.round((meta.diskUsed / meta.diskTotal) * 100)}% used</p>
|
||||
</div>
|
||||
)}
|
||||
{meta.processes !== 'Unknown' && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 flex items-center gap-1">
|
||||
<GitBranch className="h-3 w-3" /> Running Processes
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-900">{meta.processes}</p>
|
||||
</div>
|
||||
)}
|
||||
{meta.uptime !== 'Unknown' && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" /> Uptime
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-900">{meta.uptime}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Top Processes */}
|
||||
<div className="pt-3 border-t border-gray-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-xs font-medium text-gray-900 flex items-center gap-1">
|
||||
<Activity className="h-3 w-3" /> Top Processes
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate(`/updates?agent=${selectedAgent.id}`)}
|
||||
className="text-[10px] text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
See More →
|
||||
</button>
|
||||
</div>
|
||||
{(() => {
|
||||
const meta = getSystemMetadata(selectedAgent);
|
||||
const topProcesses = selectedAgent.metadata?.top_processes;
|
||||
if (topProcesses && Array.isArray(topProcesses) && topProcesses.length > 0) {
|
||||
return (
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-gray-500 border-b border-gray-100">
|
||||
<th className="text-left py-1 font-medium">Name</th>
|
||||
<th className="text-right py-1 font-medium">PID</th>
|
||||
<th className="text-right py-1 font-medium">CPU%</th>
|
||||
<th className="text-right py-1 font-medium">Mem%</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{topProcesses.slice(0, 5).map((proc: any, i: number) => (
|
||||
<tr key={proc.pid || i} className="border-b border-gray-50 last:border-0">
|
||||
<td className="py-1 text-gray-900 font-medium truncate max-w-[160px]">{proc.name}</td>
|
||||
<td className="py-1 text-right text-gray-600">{proc.pid}</td>
|
||||
<td className="py-1 text-right text-gray-600">{proc.cpu != null ? `${proc.cpu.toFixed(1)}%` : '—'}</td>
|
||||
<td className="py-1 text-right text-gray-600">{proc.mem != null ? `${proc.mem.toFixed(1)}%` : '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p className="text-[10px] text-gray-400 italic">
|
||||
Process details not reported. Count: {meta.processes}
|
||||
</p>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<SystemInfoGrid>
|
||||
<ScreenshotCard
|
||||
className="col-start-1 row-span-2"
|
||||
image={screenshotImage}
|
||||
isCapturing={captureScreenshotMutation.isPending}
|
||||
isPolling={screenshotCommand && !['completed', 'failed', 'timed_out', 'cancelled'].includes(screenshotCommand.status)}
|
||||
sunshine={sunshine ? { ...sunshine, state: resolveState(sunshine) } : undefined}
|
||||
screenshotStatus={screenshotCommand}
|
||||
onCapture={() => handleCaptureScreenshot(selectedAgent.id)}
|
||||
formatRelativeTime={formatRelativeTime}
|
||||
/>
|
||||
<MetricItem label="Platform" value={osInfo.platform} />
|
||||
<MetricItem
|
||||
label="Distribution"
|
||||
value={osInfo.distribution}
|
||||
sub={osInfo.version ? `(${osInfo.version})` : undefined}
|
||||
/>
|
||||
<MetricItem
|
||||
label="Architecture"
|
||||
value={selectedAgent.os_architecture || selectedAgent.architecture}
|
||||
/>
|
||||
<MetricItem label="CPU" value={meta.cpuModel} icon={Cpu} sub={`${meta.cpuCores} cores`} />
|
||||
{meta.memoryTotal > 0 && (
|
||||
<MetricItem label="Memory" value={formatBytes(meta.memoryTotal)} icon={MemoryStick} />
|
||||
)}
|
||||
{meta.diskTotal > 0 && (
|
||||
<MetricItem
|
||||
label={`Disk (${meta.diskMount})`}
|
||||
value={`${formatBytes(meta.diskUsed)} / ${formatBytes(meta.diskTotal)}`}
|
||||
icon={HardDrive}
|
||||
progress={{ used: meta.diskUsed, total: meta.diskTotal }}
|
||||
/>
|
||||
)}
|
||||
{meta.processes !== 'Unknown' && (
|
||||
<MetricItem label="Running Processes" value={meta.processes} icon={GitBranch} />
|
||||
)}
|
||||
{meta.uptime !== 'Unknown' && (
|
||||
<MetricItem label="Uptime" value={meta.uptime} icon={Clock} />
|
||||
)}
|
||||
<ProcessTable
|
||||
processes={topProcesses}
|
||||
processCount={meta.processes}
|
||||
onSeeMore={() => navigate(`/updates?agent=${selectedAgent.id}`)}
|
||||
/>
|
||||
</SystemInfoGrid>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue