Advanced pages. Model-driven controls.
This commit is contained in:
145
web/src/components/SubsystemPage.tsx
Normal file
145
web/src/components/SubsystemPage.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user