Files
IT_Tool/Frontend/src/pages/AlertsPage.tsx

498 lines
19 KiB
TypeScript
Raw Normal View History

import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Bell, Plus, Trash2, Check, AlertTriangle, Info, AlertCircle } from 'lucide-react'
import { alertsApi, alertRulesApi } from '../api/client'
import type { AlertSeverity, AlertRule, CreateAlertRuleRequest } from '../api/types'
import { cn } from '../lib/utils'
function getSeverityStyle(severity: AlertSeverity) {
return {
Critical: 'bg-red-500/20 text-red-400 border border-red-500/30',
Warning: 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/30',
Info: 'bg-blue-500/20 text-blue-400 border border-blue-500/30',
}[severity]
}
function getSeverityIcon(severity: AlertSeverity) {
return {
Critical: <AlertCircle size={14} />,
Warning: <AlertTriangle size={14} />,
Info: <Info size={14} />,
}[severity]
}
const metricPathOptions = [
{ value: 'cpu_usage_percent', label: 'CPU-Auslastung (%)' },
{ value: 'memory_usage_percent', label: 'RAM-Auslastung (%)' },
]
const operatorOptions = [
{ value: '>', label: '>' },
{ value: '>=', label: '>=' },
{ value: '<', label: '<' },
{ value: '<=', label: '<=' },
{ value: '==', label: '==' },
]
const severityOptions = [
{ value: 'Info' as AlertSeverity, label: 'Info' },
{ value: 'Warning' as AlertSeverity, label: 'Warning' },
{ value: 'Critical' as AlertSeverity, label: 'Critical' },
]
interface CreateRuleForm {
name: string
metricPath: string
operator: string
threshold: string
severity: AlertSeverity
}
export default function AlertsPage() {
const [activeTab, setActiveTab] = useState<'alerts' | 'rules'>('alerts')
const [alertFilter, setAlertFilter] = useState<'all' | 'unacknowledged' | 'acknowledged'>('unacknowledged')
const [showCreateRuleModal, setShowCreateRuleModal] = useState(false)
const [formData, setFormData] = useState<CreateRuleForm>({
name: '',
metricPath: 'cpu_usage_percent',
operator: '>',
threshold: '',
severity: 'Warning',
})
const queryClient = useQueryClient()
// Queries
const alertsQuery = useQuery({
queryKey: ['alerts'],
queryFn: () => alertsApi.list(),
})
const rulesQuery = useQuery({
queryKey: ['alert-rules'],
queryFn: () => alertRulesApi.list(),
})
// Mutations
const acknowledgeMutation = useMutation({
mutationFn: (id: number) => alertsApi.acknowledge(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['alerts'] })
},
})
const createRuleMutation = useMutation({
mutationFn: (data: CreateAlertRuleRequest) => alertRulesApi.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['alert-rules'] })
setShowCreateRuleModal(false)
setFormData({
name: '',
metricPath: 'cpu_usage_percent',
operator: '>',
threshold: '',
severity: 'Warning',
})
},
})
const updateRuleMutation = useMutation({
mutationFn: ({ id, enabled }: { id: number; enabled: boolean }) =>
alertRulesApi.update(id, { enabled }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['alert-rules'] })
},
})
const deleteRuleMutation = useMutation({
mutationFn: (id: number) => alertRulesApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['alert-rules'] })
},
})
// Filter alerts
const filteredAlerts = alertsQuery.data?.filter((alert) => {
if (alertFilter === 'acknowledged') return alert.acknowledged
if (alertFilter === 'unacknowledged') return !alert.acknowledged
return true
}) || []
// Handlers
const handleCreateRule = async () => {
if (!formData.name.trim() || !formData.threshold) {
alert('Bitte alle erforderlichen Felder ausfüllen')
return
}
try {
await createRuleMutation.mutateAsync({
name: formData.name,
metricPath: formData.metricPath,
operator: formData.operator,
threshold: parseFloat(formData.threshold),
severity: formData.severity,
})
} catch (err) {
console.error('Fehler beim Erstellen der Regel:', err)
}
}
const handleDeleteRule = (id: number) => {
if (window.confirm('Möchten Sie diese Regel wirklich löschen?')) {
deleteRuleMutation.mutate(id)
}
}
const handleToggleRuleEnabled = (rule: AlertRule) => {
updateRuleMutation.mutate({ id: rule.id, enabled: !rule.enabled })
}
return (
<div className="p-6 max-w-7xl mx-auto">
{/* Header */}
<div className="flex items-center gap-3 mb-6">
<Bell size={24} className="text-primary" />
<h1 className="text-3xl font-bold text-foreground">Alerts</h1>
</div>
{/* Tabs */}
<div className="flex gap-2 border-b border-border mb-6">
<button
onClick={() => setActiveTab('alerts')}
className={cn(
'px-4 py-2 font-medium transition-colors',
activeTab === 'alerts'
? 'border-b-2 border-primary text-primary'
: 'text-muted-foreground hover:text-foreground',
)}
>
Aktive Alerts
</button>
<button
onClick={() => setActiveTab('rules')}
className={cn(
'px-4 py-2 font-medium transition-colors',
activeTab === 'rules'
? 'border-b-2 border-primary text-primary'
: 'text-muted-foreground hover:text-foreground',
)}
>
Regeln
</button>
</div>
{/* Tab 1: Aktive Alerts */}
{activeTab === 'alerts' && (
<div className="space-y-4">
{/* Filter */}
<div className="flex gap-2">
<button
onClick={() => setAlertFilter('all')}
className={cn(
'px-3 py-1 rounded text-sm transition-colors',
alertFilter === 'all'
? 'bg-primary text-primary-foreground'
: 'bg-accent text-foreground hover:bg-accent/80',
)}
>
Alle
</button>
<button
onClick={() => setAlertFilter('unacknowledged')}
className={cn(
'px-3 py-1 rounded text-sm transition-colors',
alertFilter === 'unacknowledged'
? 'bg-primary text-primary-foreground'
: 'bg-accent text-foreground hover:bg-accent/80',
)}
>
Unbestätigt
</button>
<button
onClick={() => setAlertFilter('acknowledged')}
className={cn(
'px-3 py-1 rounded text-sm transition-colors',
alertFilter === 'acknowledged'
? 'bg-primary text-primary-foreground'
: 'bg-accent text-foreground hover:bg-accent/80',
)}
>
Bestätigt
</button>
</div>
{/* Loading / Error */}
{alertsQuery.isLoading && <p className="text-muted-foreground">Lädt...</p>}
{alertsQuery.isError && (
<p className="text-red-400">Fehler beim Laden der Alerts</p>
)}
{/* Empty State */}
{alertsQuery.isSuccess && filteredAlerts.length === 0 && (
<p className="text-muted-foreground text-center py-8">
Keine aktiven Alerts
</p>
)}
{/* Table */}
{alertsQuery.isSuccess && filteredAlerts.length > 0 && (
<div className="overflow-x-auto border border-border rounded-lg">
<table className="w-full text-sm">
<thead>
<tr className="bg-accent border-b border-border">
<th className="px-4 py-2 text-left font-medium">Erstellt</th>
<th className="px-4 py-2 text-left font-medium">Agent</th>
<th className="px-4 py-2 text-left font-medium">Regel</th>
<th className="px-4 py-2 text-left font-medium">Nachricht</th>
<th className="px-4 py-2 text-left font-medium">Schweregrad</th>
<th className="px-4 py-2 text-left font-medium">Aktionen</th>
</tr>
</thead>
<tbody>
{filteredAlerts.map((alert) => (
<tr key={alert.id} className="border-b border-border hover:bg-accent/50">
<td className="px-4 py-2 text-xs text-muted-foreground">
{new Date(alert.createdAt).toLocaleString('de-DE')}
</td>
<td className="px-4 py-2">
<div>
<p className="font-medium">{alert.agentHostname}</p>
<p className="text-xs text-muted-foreground">{alert.agentId}</p>
</div>
</td>
<td className="px-4 py-2">{alert.ruleName}</td>
<td className="px-4 py-2 text-xs">{alert.message}</td>
<td className="px-4 py-2">
<div className={cn('flex items-center gap-1 px-2 py-1 rounded w-fit text-xs font-medium', getSeverityStyle(alert.severity))}>
{getSeverityIcon(alert.severity)}
{alert.severity}
</div>
</td>
<td className="px-4 py-2">
<button
onClick={() => acknowledgeMutation.mutate(alert.id)}
disabled={alert.acknowledged || acknowledgeMutation.isPending}
className={cn(
'flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors',
alert.acknowledged
? 'bg-green-500/20 text-green-400 cursor-default'
: 'bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 disabled:opacity-50',
)}
>
<Check size={12} />
{alert.acknowledged ? 'Bestätigt' : 'Bestätigen'}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{/* Tab 2: Regeln */}
{activeTab === 'rules' && (
<div className="space-y-4">
{/* Create Button */}
<button
onClick={() => setShowCreateRuleModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
>
<Plus size={16} />
Neue Regel
</button>
{/* Loading / Error */}
{rulesQuery.isLoading && <p className="text-muted-foreground">Lädt...</p>}
{rulesQuery.isError && (
<p className="text-red-400">Fehler beim Laden der Regeln</p>
)}
{/* Empty State */}
{rulesQuery.isSuccess && rulesQuery.data.length === 0 && (
<p className="text-muted-foreground text-center py-8">
Keine Regeln vorhanden
</p>
)}
{/* Table */}
{rulesQuery.isSuccess && rulesQuery.data.length > 0 && (
<div className="overflow-x-auto border border-border rounded-lg">
<table className="w-full text-sm">
<thead>
<tr className="bg-accent border-b border-border">
<th className="px-4 py-2 text-left font-medium">Name</th>
<th className="px-4 py-2 text-left font-medium">Metrik</th>
<th className="px-4 py-2 text-left font-medium">Bedingung</th>
<th className="px-4 py-2 text-left font-medium">Schweregrad</th>
<th className="px-4 py-2 text-center font-medium">Aktiv</th>
<th className="px-4 py-2 text-left font-medium">Aktionen</th>
</tr>
</thead>
<tbody>
{rulesQuery.data.map((rule) => {
const metricLabel = metricPathOptions.find(
(opt) => opt.value === rule.metricPath,
)?.label || rule.metricPath
return (
<tr key={rule.id} className="border-b border-border hover:bg-accent/50">
<td className="px-4 py-2 font-medium">{rule.name}</td>
<td className="px-4 py-2 text-xs">{metricLabel}</td>
<td className="px-4 py-2 text-xs font-mono">
{metricLabel} {rule.operator} {rule.threshold}
</td>
<td className="px-4 py-2">
<div className={cn('flex items-center gap-1 px-2 py-1 rounded w-fit text-xs font-medium', getSeverityStyle(rule.severity))}>
{getSeverityIcon(rule.severity)}
{rule.severity}
</div>
</td>
<td className="px-4 py-2 text-center">
<button
onClick={() => handleToggleRuleEnabled(rule)}
disabled={updateRuleMutation.isPending}
className={cn(
'px-2 py-1 rounded text-xs font-medium transition-colors',
rule.enabled
? 'bg-green-500/20 text-green-400 hover:bg-green-500/30'
: 'bg-red-500/20 text-red-400 hover:bg-red-500/30',
)}
>
{rule.enabled ? 'Ja' : 'Nein'}
</button>
</td>
<td className="px-4 py-2">
<button
onClick={() => handleDeleteRule(rule.id)}
disabled={deleteRuleMutation.isPending}
className="flex items-center gap-1 px-2 py-1 rounded text-xs bg-red-500/20 text-red-400 hover:bg-red-500/30 transition-colors disabled:opacity-50"
>
<Trash2 size={12} />
Löschen
</button>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
</div>
)}
{/* Create Rule Modal */}
{showCreateRuleModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-card border border-border rounded-lg p-6 max-w-md w-full">
<h2 className="text-xl font-bold mb-4">Neue Regel erstellen</h2>
<div className="space-y-4">
{/* Name */}
<div>
<label className="block text-sm font-medium mb-1">
Name <span className="text-red-400">*</span>
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-3 py-2 bg-background border border-border rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="z.B. CPU über 90%"
/>
</div>
{/* MetricPath */}
<div>
<label className="block text-sm font-medium mb-1">
Metrik <span className="text-red-400">*</span>
</label>
<select
value={formData.metricPath}
onChange={(e) => setFormData({ ...formData, metricPath: e.target.value })}
className="w-full px-3 py-2 bg-background border border-border rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
>
{metricPathOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
{/* Operator */}
<div>
<label className="block text-sm font-medium mb-1">
Operator <span className="text-red-400">*</span>
</label>
<select
value={formData.operator}
onChange={(e) => setFormData({ ...formData, operator: e.target.value })}
className="w-full px-3 py-2 bg-background border border-border rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
>
{operatorOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
{/* Threshold */}
<div>
<label className="block text-sm font-medium mb-1">
Schwellenwert <span className="text-red-400">*</span>
</label>
<input
type="number"
value={formData.threshold}
onChange={(e) => setFormData({ ...formData, threshold: e.target.value })}
className="w-full px-3 py-2 bg-background border border-border rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="z.B. 90"
/>
</div>
{/* Severity */}
<div>
<label className="block text-sm font-medium mb-1">
Schweregrad <span className="text-red-400">*</span>
</label>
<select
value={formData.severity}
onChange={(e) => setFormData({ ...formData, severity: e.target.value as AlertSeverity })}
className="w-full px-3 py-2 bg-background border border-border rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
>
{severityOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
</div>
{/* Buttons */}
<div className="flex gap-3 mt-6">
<button
onClick={() => setShowCreateRuleModal(false)}
className="flex-1 px-4 py-2 bg-accent text-foreground rounded-md hover:bg-accent/80 transition-colors"
>
Abbrechen
</button>
<button
onClick={handleCreateRule}
disabled={createRuleMutation.isPending}
className="flex-1 px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors disabled:opacity-50"
>
{createRuleMutation.isPending ? 'Wird erstellt...' : 'Erstellen'}
</button>
</div>
</div>
</div>
)}
</div>
)
}