ui: improve Agent Health layout and fix misaligned controls

- Move Update Agent button to Subsystem Configuration header
- Remove duplicate Compact Summary box with misaligned refresh
- Reduce visual separation between sections (same card styling)
- Make security status details visible instead of hidden in tooltips
- Fix enforced status colors (blue instead of red)
- Consolidate enabled/auto-run counts in header
- Reduce spacing between sections for cohesive interface

The enabled/auto-run toggles now properly align with their
subsystems in the table, and critical security information
is immediately visible without hover interactions.
This commit is contained in:
Fimeg
2025-11-10 23:08:17 -05:00
parent 3f0838affc
commit 8b9a314200
5 changed files with 1008 additions and 32 deletions

View File

@@ -13,12 +13,14 @@ import {
CheckCircle,
AlertCircle,
XCircle,
Upload,
} from 'lucide-react';
import { formatRelativeTime } from '@/lib/utils';
import { agentApi, securityApi } from '@/lib/api';
import toast from 'react-hot-toast';
import { cn } from '@/lib/utils';
import { AgentSubsystem } from '@/types';
import { AgentUpdate } from './AgentUpdate';
interface AgentScannersProps {
agentId: string;
@@ -53,6 +55,7 @@ const subsystemConfig: Record<string, { icon: React.ReactNode; name: string; des
};
export function AgentScanners({ agentId }: AgentScannersProps) {
const [showUpdateModal, setShowUpdateModal] = useState(false);
const queryClient = useQueryClient();
// Fetch subsystems from API
@@ -65,6 +68,13 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
refetchInterval: 30000, // Refresh every 30 seconds
});
// Fetch agent data for Update Agent button
const { data: agent } = useQuery({
queryKey: ['agent', agentId],
queryFn: () => agentApi.getAgent(agentId),
refetchInterval: 30000,
});
// Fetch security health status
const { data: securityOverview, isLoading: securityLoading } = useQuery({
queryKey: ['security-overview'],
@@ -79,12 +89,16 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
const getSecurityStatusDisplay = (status: string) => {
switch (status) {
case 'healthy':
case 'enforced':
case 'operational':
return {
color: 'text-green-600 bg-green-100 border-green-200',
icon: <CheckCircle className="h-4 w-4 text-green-600" />
};
case 'enforced':
return {
color: 'text-blue-600 bg-blue-100 border-blue-200',
icon: <Shield className="h-4 w-4 text-blue-600" />
};
case 'degraded':
return {
color: 'text-amber-600 bg-amber-100 border-amber-200',
@@ -227,11 +241,25 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
const autoRunCount = subsystems.filter(s => s.auto_run && s.enabled).length;
return (
<div className="space-y-6">
<div className="space-y-4">
{/* Subsystem Configuration Table */}
<div className="card">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-gray-900">Subsystem Configuration</h3>
<div className="flex items-center space-x-4">
<h3 className="text-sm font-medium text-gray-900">Subsystem Configuration</h3>
<div className="flex items-center space-x-3 text-xs text-gray-600">
<span>{enabledCount} enabled</span>
<span>{autoRunCount} auto-running</span>
<span className="text-gray-500"> {subsystems.length} total</span>
</div>
</div>
<button
onClick={() => setShowUpdateModal(true)}
className="flex items-center space-x-1 px-3 py-1 text-xs text-blue-600 hover:text-blue-800 border border-blue-300 hover:bg-blue-50 rounded-md transition-colors"
>
<Upload className="h-3 w-3" />
<span>Update Agent</span>
</button>
</div>
{isLoading ? (
@@ -376,37 +404,13 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
)}
</div>
{/* Compact note */}
<div className="text-xs text-gray-500">
Subsystems report specific metrics to the server on scheduled intervals. Enable auto-run to schedule automatic scans, or trigger manual scans as needed.
</div>
{/* Compact Summary */}
<div className="card">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center space-x-6">
<div>
<span className="text-gray-600">Enabled:</span>
<span className="ml-2 font-medium text-green-600">{enabledCount}/{subsystems.length}</span>
</div>
<div>
<span className="text-gray-600">Auto-Run:</span>
<span className="ml-2 font-medium text-blue-600">{autoRunCount}</span>
</div>
</div>
<button
onClick={() => refetch()}
disabled={isLoading}
className="flex items-center space-x-1 px-3 py-1 text-xs text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded transition-colors"
>
<RefreshCw className={cn('h-3 w-3', isLoading && 'animate-spin')} />
<span>Refresh</span>
</button>
</div>
{/* Note */}
<div className="text-xs text-gray-500 text-center py-2">
Subsystems report specific metrics on scheduled intervals. Enable auto-run to schedule automatic scans, or use Actions to trigger manual scans.
</div>
{/* Security Health */}
<div className="bg-white/90 backdrop-blur-md rounded-lg border border-gray-200/50 shadow-sm">
<div className="card">
<div className="flex items-center justify-between p-4 border-b border-gray-200/50">
<div className="flex items-center space-x-2">
<Shield className="h-5 w-5 text-blue-600" />
@@ -516,6 +520,25 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
}
};
const getDetailedSecurityInfo = (subsystemType: string, subsystem: any) => {
if (!securityOverview?.subsystems[subsystemType]) return '';
const subsystemData = securityOverview.subsystems[subsystemType];
const checks = subsystemData.checks || {};
const metrics = subsystemData.metrics || {};
switch (subsystemType) {
case 'nonce_validation':
return `Nonces: ${metrics.total_pending_commands || 0} pending. Max age: ${checks.max_age_minutes || 5}min. Failures: ${checks.validation_failures || 0}. Format: ${checks.nonce_format || 'UUID:Timestamp'}`;
case 'machine_binding':
return `Machine ID: ${checks.machine_id_type || 'Hardware fingerprint'}. Bound agents: ${checks.bound_agents || 'Unknown'}. Violations: ${checks.recent_violations || 0}. Min version: ${checks.min_agent_version || 'v0.1.22'}`;
case 'ed25519_signing':
return `Key: ${checks.public_key_fingerprint?.substring(0, 16) || 'Not available'}... Algorithm: ${checks.algorithm || 'Ed25519'}. Valid since: ${new Date(securityOverview.timestamp).toLocaleDateString()}`;
default:
return `Status: ${subsystem.status}. Last check: ${new Date(securityOverview.timestamp).toLocaleString()}`;
}
};
return (
<div
key={key}
@@ -528,7 +551,7 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
{getSecurityIcon(key)}
</div>
</div>
<div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900 flex items-center gap-2">
{getSecurityDisplayName(key)}
<CheckCircle className="w-3 h-3 text-gray-400" />
@@ -536,15 +559,24 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
<p className="text-xs text-gray-600 mt-0.5">
{getEnhancedSubtitle(key, subsystem.status)}
</p>
{(key === 'nonce_validation' || key === 'machine_binding' || key === 'ed25519_signing') && (
<div className="mt-2 p-2 bg-gray-50/70 rounded border border-gray-200/50">
<p className="text-xs text-gray-700 font-mono break-all">
{getDetailedSecurityInfo(key, subsystem)}
</p>
</div>
)}
</div>
</div>
<div className={cn(
'inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium border',
subsystem.status === 'healthy' ? 'bg-green-100 text-green-700 border-green-200' :
subsystem.status === 'enforced' ? 'bg-blue-100 text-blue-700 border-blue-200' :
subsystem.status === 'degraded' ? 'bg-amber-100 text-amber-700 border-amber-200' :
'bg-red-100 text-red-700 border-red-200'
)}>
{subsystem.status === 'healthy' && <CheckCircle className="w-3 h-3" />}
{subsystem.status === 'enforced' && <Shield className="w-3 h-3" />}
{subsystem.status === 'degraded' && <AlertCircle className="w-3 h-3" />}
{subsystem.status === 'unhealthy' && <XCircle className="w-3 h-3" />}
{subsystem.status.toUpperCase()}
@@ -603,6 +635,35 @@ export function AgentScanners({ agentId }: AgentScannersProps) {
</div>
)}
</div>
{/* Update Agent Modal */}
{showUpdateModal && agent && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm" onClick={() => setShowUpdateModal(false)} />
<div className="relative z-10 w-full max-w-lg mx-4">
<div className="bg-white rounded-lg shadow-xl border border-gray-200">
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">Update Agent: {agent.hostname}</h3>
<button
onClick={() => setShowUpdateModal(false)}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<XCircle className="h-5 w-5" />
</button>
</div>
<div className="p-6">
<AgentUpdate
agent={agent}
onUpdateComplete={() => {
setShowUpdateModal(false);
queryClient.invalidateQueries({ queryKey: ['agent', agentId] });
}}
/>
</div>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -190,6 +190,36 @@ export const agentApi = {
return response.data;
},
// Get agent metrics
getAgentMetrics: async (agentId: string): Promise<any> => {
const response = await api.get(`/agents/${agentId}/metrics`);
return response.data;
},
// Get agent storage metrics
getAgentStorageMetrics: async (agentId: string): Promise<any> => {
const response = await api.get(`/agents/${agentId}/metrics/storage`);
return response.data;
},
// Get agent system metrics
getAgentSystemMetrics: async (agentId: string): Promise<any> => {
const response = await api.get(`/agents/${agentId}/metrics/system`);
return response.data;
},
// Get agent Docker images
getAgentDockerImages: async (agentId: string, params?: any): Promise<any> => {
const response = await api.get(`/agents/${agentId}/docker-images`, { params });
return response.data;
},
// Get agent Docker info
getAgentDockerInfo: async (agentId: string): Promise<any> => {
const response = await api.get(`/agents/${agentId}/docker-info`);
return response.data;
},
// Update multiple agents (bulk)
updateMultipleAgents: async (updateData: {
agent_ids: string[];