diff --git a/ui/src/components/health/health-card.tsx b/ui/src/components/health/health-card.tsx new file mode 100644 index 00000000..c3097596 --- /dev/null +++ b/ui/src/components/health/health-card.tsx @@ -0,0 +1,68 @@ +import { Card, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { CheckCircle, AlertTriangle, XCircle, Wrench } from 'lucide-react'; +import { useFixHealth } from '@/hooks/use-health'; + +interface HealthCheck { + id: string; + name: string; + status: 'ok' | 'warning' | 'error'; + message: string; + details?: string; + fixable?: boolean; +} + +const statusConfig = { + ok: { + icon: CheckCircle, + color: 'text-green-600', + bg: 'bg-green-50 dark:bg-green-900/20', + border: 'border-green-200 dark:border-green-800', + }, + warning: { + icon: AlertTriangle, + color: 'text-yellow-500', + bg: 'bg-yellow-50 dark:bg-yellow-900/20', + border: 'border-yellow-200 dark:border-yellow-800', + }, + error: { + icon: XCircle, + color: 'text-red-500', + bg: 'bg-red-50 dark:bg-red-900/20', + border: 'border-red-200 dark:border-red-800', + }, +}; + +export function HealthCard({ check }: { check: HealthCheck }) { + const fixMutation = useFixHealth(); + const config = statusConfig[check.status]; + const Icon = config.icon; + + return ( + + +
+
+ + {check.name} +
+ {check.fixable && check.status !== 'ok' && ( + + )} +
+

{check.message}

+ {check.details && ( +

{check.details}

+ )} +
+
+ ); +} diff --git a/ui/src/components/health/health-check-item.tsx b/ui/src/components/health/health-check-item.tsx new file mode 100644 index 00000000..88268d64 --- /dev/null +++ b/ui/src/components/health/health-check-item.tsx @@ -0,0 +1,162 @@ +import { ChevronRight, Copy, Terminal, Wrench } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { useFixHealth, type HealthCheck } from '@/hooks/use-health'; +import { cn } from '@/lib/utils'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +const statusConfig = { + ok: { dot: 'bg-green-500', label: 'OK', labelColor: 'text-green-500' }, + warning: { dot: 'bg-yellow-500', label: 'WARN', labelColor: 'text-yellow-500' }, + error: { dot: 'bg-red-500', label: 'ERR', labelColor: 'text-red-500' }, + info: { dot: 'bg-blue-500', label: 'INFO', labelColor: 'text-blue-500' }, +}; + +export function HealthCheckItem({ check }: { check: HealthCheck }) { + const fixMutation = useFixHealth(); + const config = statusConfig[check.status]; + const [isOpen, setIsOpen] = useState(false); + const hasExpandableContent = check.details || check.fix; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + toast.success('Copied to clipboard'); + }; + + // Compact single-line display for items without expandable content + if (!hasExpandableContent) { + return ( +
+ {/* Status dot with pulse animation */} +
+
+ {check.status !== 'ok' && ( +
+ )} +
+ + {/* Check name */} + {check.name} + + {/* Status label */} + + [{config.label}] + + + {/* Fix button for fixable non-ok items */} + {check.fixable && check.status !== 'ok' && ( + + )} +
+ ); + } + + // Expandable display for items with details or fix commands + return ( + +
+ + + + + +
+ {/* Message */} +

{check.message}

+ + {/* Details block */} + {check.details && ( +
+                {check.details}
+              
+ )} + + {/* Fix command block */} + {check.fix && ( +
+
+ + {check.fix} + +
+ + {check.fixable && check.status !== 'ok' && ( + + )} +
+ )} +
+
+
+
+ ); +} diff --git a/ui/src/components/health/health-gauge.tsx b/ui/src/components/health/health-gauge.tsx new file mode 100644 index 00000000..2936b726 --- /dev/null +++ b/ui/src/components/health/health-gauge.tsx @@ -0,0 +1,91 @@ +import { cn } from '@/lib/utils'; + +interface HealthGaugeProps { + passed: number; + total: number; + status: 'ok' | 'warning' | 'error'; + size?: 'sm' | 'md' | 'lg'; +} + +const sizeConfig = { + sm: { dimension: 80, strokeWidth: 6, fontSize: 'text-lg', labelSize: 'text-[10px]' }, + md: { dimension: 120, strokeWidth: 8, fontSize: 'text-3xl', labelSize: 'text-xs' }, + lg: { dimension: 160, strokeWidth: 10, fontSize: 'text-4xl', labelSize: 'text-sm' }, +}; + +const statusColors = { + ok: { stroke: '#22C55E', glow: 'rgba(34, 197, 94, 0.4)' }, + warning: { stroke: '#EAB308', glow: 'rgba(234, 179, 8, 0.4)' }, + error: { stroke: '#EF4444', glow: 'rgba(239, 68, 68, 0.4)' }, +}; + +export function HealthGauge({ passed, total, status, size = 'md' }: HealthGaugeProps) { + const config = sizeConfig[size]; + const colors = statusColors[status]; + const percentage = total > 0 ? Math.round((passed / total) * 100) : 0; + + const radius = (config.dimension - config.strokeWidth) / 2; + const circumference = 2 * Math.PI * radius; + const strokeDashoffset = circumference - (percentage / 100) * circumference; + const center = config.dimension / 2; + + return ( +
+ + {/* Background track */} + + {/* Progress arc */} + + {/* Animated glow dot at end of arc */} + {percentage > 0 && ( + + )} + + {/* Center content */} +
+ + {percentage} + + + health + +
+
+ ); +} diff --git a/ui/src/components/health/health-group-section.tsx b/ui/src/components/health/health-group-section.tsx new file mode 100644 index 00000000..1f7f1740 --- /dev/null +++ b/ui/src/components/health/health-group-section.tsx @@ -0,0 +1,111 @@ +import { ChevronDown, Monitor, Settings, Users, Shield, Zap } from 'lucide-react'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { HealthCheckItem } from './health-check-item'; +import { type HealthGroup } from '@/hooks/use-health'; +import { cn } from '@/lib/utils'; +import { useState } from 'react'; + +const groupIcons: Record = { + Monitor, + Settings, + Users, + Shield, + Zap, +}; + +interface HealthGroupSectionProps { + group: HealthGroup; + defaultOpen?: boolean; +} + +export function HealthGroupSection({ group, defaultOpen = true }: HealthGroupSectionProps) { + const [isOpen, setIsOpen] = useState(defaultOpen); + const Icon = groupIcons[group.icon] || Monitor; + + const passed = group.checks.filter((c) => c.status === 'ok').length; + const total = group.checks.length; + const hasErrors = group.checks.some((c) => c.status === 'error'); + const hasWarnings = group.checks.some((c) => c.status === 'warning'); + const percentage = Math.round((passed / total) * 100); + + // Determine status color + const statusColor = hasErrors + ? 'text-red-500' + : hasWarnings + ? 'text-yellow-500' + : 'text-green-500'; + const progressColor = hasErrors ? 'bg-red-500' : hasWarnings ? 'bg-yellow-500' : 'bg-green-500'; + + return ( + +
+ {/* Group header */} + + + + + {/* Checks list */} + +
+ {group.checks.map((check) => ( + + ))} +
+
+
+
+ ); +} diff --git a/ui/src/components/health/health-stats-bar.tsx b/ui/src/components/health/health-stats-bar.tsx new file mode 100644 index 00000000..042a3f89 --- /dev/null +++ b/ui/src/components/health/health-stats-bar.tsx @@ -0,0 +1,85 @@ +import { cn } from '@/lib/utils'; + +interface HealthStatsBarProps { + total: number; + passed: number; + warnings: number; + errors: number; + info: number; +} + +interface StatItemProps { + label: string; + value: number; + color: string; + bgColor: string; +} + +function StatItem({ label, value, color, bgColor }: StatItemProps) { + return ( +
+
+ + {label} + + {value} +
+ ); +} + +export function HealthStatsBar({ total, passed, warnings, errors, info }: HealthStatsBarProps) { + // Calculate percentages for the progress bar + const passedPct = (passed / total) * 100; + const warningPct = (warnings / total) * 100; + const errorPct = (errors / total) * 100; + const infoPct = (info / total) * 100; + + return ( +
+ {/* Progress bar visualization */} +
+ {errorPct > 0 && ( +
+ )} + {warningPct > 0 && ( +
+ )} + {infoPct > 0 && ( +
+ )} + {passedPct > 0 && ( +
+ )} +
+ + {/* Stats row */} +
+
+ + Checks + + {total} +
+ +
+ + + + +
+
+
+ ); +} diff --git a/ui/src/components/health/index.ts b/ui/src/components/health/index.ts new file mode 100644 index 00000000..16eac15e --- /dev/null +++ b/ui/src/components/health/index.ts @@ -0,0 +1,9 @@ +/** + * Health Components Barrel Export + */ + +export { HealthCard } from './health-card'; +export { HealthCheckItem } from './health-check-item'; +export { HealthGauge } from './health-gauge'; +export { HealthGroupSection } from './health-group-section'; +export { HealthStatsBar } from './health-stats-bar';