Kraft102's picture
fix: sql.js Docker/Alpine compatibility layer for PatternMemory and FailureMemory
5a81b95
import { useState } from 'react';
import {
AlertTriangle, Plus, Trash2, Edit2, Power, PowerOff,
Shield, Cpu, HardDrive, Network, UserX, Users, Clock, AlertCircle,
ChevronDown, Activity
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import { toast } from '@/hooks/use-toast';
import {
useAlertRules,
AlertRule,
MetricType,
Operator,
METRIC_CONFIG,
OPERATOR_CONFIG
} from '@/hooks/useAlertRules';
import { NotificationSeverity } from '@/contexts/NotificationContext';
import { cn } from '@/lib/utils';
const metricIcons: Record<MetricType, any> = {
threat_count: Shield,
cpu_usage: Cpu,
memory_usage: HardDrive,
network_traffic: Network,
failed_logins: UserX,
active_connections: Users,
response_time: Clock,
error_rate: AlertCircle,
};
const severityColors: Record<NotificationSeverity, string> = {
critical: 'text-destructive',
warning: 'text-orange-400',
info: 'text-primary',
};
const AlertRulesManager = () => {
const { rules, currentMetrics, createRule, updateRule, deleteRule, toggleRule } = useAlertRules();
const [isOpen, setIsOpen] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const [editingRule, setEditingRule] = useState<AlertRule | null>(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [ruleToDelete, setRuleToDelete] = useState<AlertRule | null>(null);
// Form state
const [formName, setFormName] = useState('');
const [formDescription, setFormDescription] = useState('');
const [formMetric, setFormMetric] = useState<MetricType>('threat_count');
const [formOperator, setFormOperator] = useState<Operator>('gt');
const [formThreshold, setFormThreshold] = useState('50');
const [formSeverity, setFormSeverity] = useState<NotificationSeverity>('warning');
const [formCooldown, setFormCooldown] = useState('5');
const resetForm = () => {
setFormName('');
setFormDescription('');
setFormMetric('threat_count');
setFormOperator('gt');
setFormThreshold('50');
setFormSeverity('warning');
setFormCooldown('5');
setEditingRule(null);
};
const openCreateDialog = () => {
resetForm();
setDialogOpen(true);
};
const openEditDialog = (rule: AlertRule) => {
setEditingRule(rule);
setFormName(rule.name);
setFormDescription(rule.description || '');
setFormMetric(rule.metric);
setFormOperator(rule.operator);
setFormThreshold(String(rule.threshold));
setFormSeverity(rule.severity);
setFormCooldown(String(rule.cooldownMinutes));
setDialogOpen(true);
};
const handleSaveRule = () => {
if (!formName.trim()) {
toast({ title: "Navn påkrævet", variant: "destructive" });
return;
}
const threshold = parseFloat(formThreshold);
if (isNaN(threshold)) {
toast({ title: "Ugyldig threshold værdi", variant: "destructive" });
return;
}
const cooldown = parseInt(formCooldown);
if (isNaN(cooldown) || cooldown < 1) {
toast({ title: "Cooldown skal være mindst 1 minut", variant: "destructive" });
return;
}
if (editingRule) {
updateRule(editingRule.id, {
name: formName.trim(),
description: formDescription.trim() || undefined,
metric: formMetric,
operator: formOperator,
threshold,
severity: formSeverity,
cooldownMinutes: cooldown,
});
toast({ title: "Regel opdateret" });
} else {
createRule({
name: formName.trim(),
description: formDescription.trim() || undefined,
enabled: true,
metric: formMetric,
operator: formOperator,
threshold,
severity: formSeverity,
cooldownMinutes: cooldown,
});
toast({ title: "Regel oprettet" });
}
setDialogOpen(false);
resetForm();
};
const confirmDelete = (rule: AlertRule) => {
setRuleToDelete(rule);
setDeleteDialogOpen(true);
};
const handleDelete = () => {
if (ruleToDelete) {
deleteRule(ruleToDelete.id);
toast({ title: "Regel slettet" });
setRuleToDelete(null);
setDeleteDialogOpen(false);
}
};
const activeRules = rules.filter(r => r.enabled).length;
return (
<>
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger asChild>
<Button variant="outline" size="sm" className="gap-2">
<Activity className="w-4 h-4" />
Alert Rules
{activeRules > 0 && (
<span className="ml-1 px-1.5 py-0.5 bg-primary/20 text-primary text-xs rounded">
{activeRules}
</span>
)}
<ChevronDown className={cn("w-3 h-3 transition-transform", isOpen && "rotate-180")} />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="absolute top-full left-0 right-0 mt-2 z-40">
<div className="bg-card border border-border rounded-lg shadow-lg p-4 mx-4">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="font-display text-lg text-primary">Alert Rules</h3>
<p className="text-xs text-muted-foreground">Definer custom alerts baseret på metrics</p>
</div>
<Button variant="cyber" size="sm" onClick={openCreateDialog}>
<Plus className="w-4 h-4 mr-2" />
Ny regel
</Button>
</div>
{/* Current Metrics Overview */}
<div className="grid grid-cols-4 md:grid-cols-8 gap-2 mb-4 p-3 bg-secondary/20 rounded-lg">
{(Object.keys(METRIC_CONFIG) as MetricType[]).map((metric) => {
const config = METRIC_CONFIG[metric];
const Icon = metricIcons[metric];
const value = currentMetrics[metric];
return (
<div key={metric} className="text-center">
<Icon className="w-4 h-4 mx-auto text-muted-foreground mb-1" />
<div className="text-sm font-mono text-foreground">
{value ?? '-'}{config.unit}
</div>
<div className="text-xs text-muted-foreground truncate">{config.label}</div>
</div>
);
})}
</div>
{/* Rules List */}
{rules.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<AlertTriangle className="w-10 h-10 mx-auto mb-2 opacity-50" />
<p className="text-sm">Ingen regler defineret endnu</p>
</div>
) : (
<div className="space-y-2 max-h-64 overflow-y-auto">
{rules.map((rule) => {
const Icon = metricIcons[rule.metric];
const config = METRIC_CONFIG[rule.metric];
const opConfig = OPERATOR_CONFIG[rule.operator];
return (
<div
key={rule.id}
className={cn(
"flex items-center justify-between p-3 rounded-lg border transition-all",
rule.enabled
? "bg-secondary/30 border-border/50"
: "bg-secondary/10 border-border/30 opacity-60"
)}
>
<div className="flex items-center gap-3">
<Icon className={cn("w-5 h-5", severityColors[rule.severity])} />
<div>
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{rule.name}</span>
<span className={cn(
"px-1.5 py-0.5 text-xs rounded",
rule.severity === 'critical' && "bg-destructive/20 text-destructive",
rule.severity === 'warning' && "bg-orange-400/20 text-orange-400",
rule.severity === 'info' && "bg-primary/20 text-primary"
)}>
{rule.severity}
</span>
</div>
<div className="text-xs text-muted-foreground">
{config.label} {opConfig.symbol} {rule.threshold}{config.unit}
{rule.triggerCount > 0 && (
<span className="ml-2">• Triggered {rule.triggerCount}x</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Switch
checked={rule.enabled}
onCheckedChange={() => toggleRule(rule.id)}
/>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => openEditDialog(rule)}
>
<Edit2 className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
onClick={() => confirmDelete(rule)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
);
})}
</div>
)}
</div>
</CollapsibleContent>
</Collapsible>
{/* Create/Edit Dialog */}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="bg-card border-border">
<DialogHeader>
<DialogTitle className="text-primary font-display">
{editingRule ? 'Rediger regel' : 'Opret ny regel'}
</DialogTitle>
<DialogDescription>
Definer betingelser for hvornår du vil modtage alerts
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Navn</label>
<Input
placeholder="High CPU Alert"
value={formName}
onChange={(e) => setFormName(e.target.value)}
className="bg-secondary/30 border-border"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Beskrivelse (valgfri)</label>
<Input
placeholder="Alert når CPU forbrug er for højt"
value={formDescription}
onChange={(e) => setFormDescription(e.target.value)}
className="bg-secondary/30 border-border"
/>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="space-y-2">
<label className="text-sm font-medium">Metric</label>
<Select value={formMetric} onValueChange={(v) => setFormMetric(v as MetricType)}>
<SelectTrigger className="bg-secondary/30 border-border">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-card border-border">
{(Object.keys(METRIC_CONFIG) as MetricType[]).map((metric) => (
<SelectItem key={metric} value={metric}>
{METRIC_CONFIG[metric].label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Operator</label>
<Select value={formOperator} onValueChange={(v) => setFormOperator(v as Operator)}>
<SelectTrigger className="bg-secondary/30 border-border">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-card border-border">
{(Object.keys(OPERATOR_CONFIG) as Operator[]).map((op) => (
<SelectItem key={op} value={op}>
{OPERATOR_CONFIG[op].symbol} {OPERATOR_CONFIG[op].label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">
Threshold ({METRIC_CONFIG[formMetric].unit || 'værdi'})
</label>
<Input
type="number"
value={formThreshold}
onChange={(e) => setFormThreshold(e.target.value)}
className="bg-secondary/30 border-border"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<label className="text-sm font-medium">Severity</label>
<Select value={formSeverity} onValueChange={(v) => setFormSeverity(v as NotificationSeverity)}>
<SelectTrigger className="bg-secondary/30 border-border">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-card border-border">
<SelectItem value="info">Info</SelectItem>
<SelectItem value="warning">Warning</SelectItem>
<SelectItem value="critical">Critical</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Cooldown (minutter)</label>
<Input
type="number"
min="1"
value={formCooldown}
onChange={(e) => setFormCooldown(e.target.value)}
className="bg-secondary/30 border-border"
/>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setDialogOpen(false)}>
Annuller
</Button>
<Button variant="cyber" onClick={handleSaveRule}>
{editingRule ? 'Gem ændringer' : 'Opret regel'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent className="bg-card border-border">
<DialogHeader>
<DialogTitle className="text-destructive font-display">Slet regel</DialogTitle>
<DialogDescription>
Er du sikker på at du vil slette "{ruleToDelete?.name}"?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
Annuller
</Button>
<Button variant="destructive" onClick={handleDelete}>
Slet regel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};
export default AlertRulesManager;