Spaces:
Paused
Paused
| 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; | |