146 lines
4.9 KiB
TypeScript
146 lines
4.9 KiB
TypeScript
import type { LucideIcon } from 'lucide-react';
|
|
import { Card, CardContent } from './ui/card';
|
|
import { Button } from './ui/button';
|
|
import { Alert, AlertDescription } from './ui/alert';
|
|
import { Badge } from './ui/badge';
|
|
import { Textarea } from './ui/textarea';
|
|
import { Loader2, CheckCircle, AlertCircle, Play, RotateCw } from 'lucide-react';
|
|
import { useCentralStatus } from '../hooks/useCentralStatus';
|
|
|
|
interface MetadataItem {
|
|
label: string;
|
|
value: string | number | undefined;
|
|
}
|
|
|
|
interface SubsystemPageProps {
|
|
title: string;
|
|
description: string;
|
|
icon: LucideIcon;
|
|
daemonKey: string;
|
|
|
|
configContent?: string;
|
|
isLoadingConfig?: boolean;
|
|
|
|
rawStatusLabel?: string;
|
|
rawStatusContent?: string;
|
|
|
|
onGenerate?: () => void;
|
|
isGenerating?: boolean;
|
|
generateLabel?: string;
|
|
|
|
onRestart?: () => void;
|
|
isRestarting?: boolean;
|
|
restartLabel?: string;
|
|
|
|
successMessage?: string | null;
|
|
errorMessage?: string | null;
|
|
|
|
metadata?: MetadataItem[];
|
|
}
|
|
|
|
export function SubsystemPage({
|
|
title, description, icon: Icon, daemonKey,
|
|
configContent, isLoadingConfig,
|
|
rawStatusLabel, rawStatusContent,
|
|
onGenerate, isGenerating, generateLabel = 'Generate & Apply',
|
|
onRestart, isRestarting, restartLabel = 'Restart',
|
|
successMessage, errorMessage,
|
|
metadata,
|
|
}: SubsystemPageProps) {
|
|
const { data: centralStatus } = useCentralStatus();
|
|
const active = centralStatus?.daemons?.[daemonKey]?.active;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-4">
|
|
<div className="p-2 bg-primary/10 rounded-lg">
|
|
<Icon className="h-6 w-6 text-primary" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-2xl font-semibold">{title}</h2>
|
|
<p className="text-muted-foreground">{description}</p>
|
|
</div>
|
|
</div>
|
|
{active !== undefined && (
|
|
<Badge variant={active ? 'success' : 'destructive'} className="gap-1">
|
|
{active ? <CheckCircle className="h-3 w-3" /> : <AlertCircle className="h-3 w-3" />}
|
|
{active ? 'Active' : 'Inactive'}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
{/* Alerts */}
|
|
{successMessage && (
|
|
<Alert className="border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/20">
|
|
<CheckCircle className="h-4 w-4 text-green-600" />
|
|
<AlertDescription className="text-green-700 dark:text-green-300">{successMessage}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
{errorMessage && (
|
|
<Alert variant="error">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>{errorMessage}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Actions + Metadata */}
|
|
{(onGenerate || onRestart || metadata?.length) && (
|
|
<Card>
|
|
<CardContent className="p-4 space-y-4">
|
|
{(onGenerate || onRestart) && (
|
|
<div className="flex gap-2">
|
|
{onGenerate && (
|
|
<Button onClick={onGenerate} disabled={isGenerating} size="sm" className="gap-1">
|
|
{isGenerating ? <Loader2 className="h-3 w-3 animate-spin" /> : <Play className="h-3 w-3" />}
|
|
{generateLabel}
|
|
</Button>
|
|
)}
|
|
{onRestart && (
|
|
<Button variant="outline" onClick={onRestart} disabled={isRestarting} size="sm" className="gap-1">
|
|
{isRestarting ? <Loader2 className="h-3 w-3 animate-spin" /> : <RotateCw className="h-3 w-3" />}
|
|
{restartLabel}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)}
|
|
{metadata && metadata.length > 0 && (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
{metadata.map(m => (
|
|
<div key={m.label}>
|
|
<span className="text-muted-foreground">{m.label}</span>
|
|
<div className="font-mono mt-0.5">{m.value ?? '--'}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Config */}
|
|
{isLoadingConfig ? (
|
|
<Card className="p-8 text-center">
|
|
<Loader2 className="h-8 w-8 text-primary mx-auto mb-2 animate-spin" />
|
|
<p className="text-sm text-muted-foreground">Loading configuration...</p>
|
|
</Card>
|
|
) : configContent !== undefined ? (
|
|
<Textarea value={configContent} readOnly className="font-mono text-xs min-h-[300px]" />
|
|
) : null}
|
|
|
|
{/* Raw status */}
|
|
{rawStatusLabel && rawStatusContent && (
|
|
<Card>
|
|
<CardContent className="p-4 space-y-2">
|
|
<span className="text-sm font-medium">{rawStatusLabel}</span>
|
|
<pre className="bg-muted p-3 rounded-lg overflow-x-auto text-xs font-mono">
|
|
{rawStatusContent}
|
|
</pre>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|