import React, { useState, useCallback } from 'react'; import { useApi } from '../hooks/useApi'; import { getAlertRules, getAlertHistory, createAlertRule, updateAlertRule, deleteAlertRule, type AlertRuleData, type AlertHistoryEntry, type CreateAlertRuleData, } from '../api/client'; // ─── Constants ────────────────────────────────────────────────── const CONDITION_OPTIONS = [ { value: 'error_rate_exceeds', label: 'Error rate exceeds', unit: '(ratio 0-1)' }, { value: 'cost_exceeds', label: 'Cost exceeds', unit: '(USD)' }, { value: 'latency_exceeds', label: 'Latency exceeds', unit: '(ms)' }, { value: 'event_count_exceeds', label: 'Event count exceeds', unit: '(count)' }, { value: 'no_events_for', label: 'No events for', unit: '(window)' }, ] as const; const WINDOW_OPTIONS = [ { value: 5, label: '5 minutes' }, { value: 15, label: '15 minutes' }, { value: 30, label: '30 minutes' }, { value: 60, label: '1 hour' }, { value: 360, label: '6 hours' }, { value: 1440, label: '24 hours' }, ]; type Tab = 'active' | 'rules' | 'history'; // ─── Sub-components ───────────────────────────────────────────── function conditionLabel(condition: string): string { const found = CONDITION_OPTIONS.find((c) => c.value === condition); return found ? found.label : condition; } function formatTimestamp(ts: string): string { try { return new Date(ts).toLocaleString(); } catch { return ts; } } function formatValue(condition: string, value: number): string { switch (condition) { case 'error_rate_exceeds': return `${(value * 100).toFixed(1)}%`; case 'cost_exceeds': return `$${value.toFixed(2)}`; case 'latency_exceeds': return `${value.toFixed(0)}ms`; default: return value.toString(); } } // ─── Create Rule Form ─────────────────────────────────────────── interface CreateRuleFormProps { onCreated: () => void; onCancel: () => void; } function CreateRuleForm({ onCreated, onCancel }: CreateRuleFormProps): React.ReactElement { const [name, setName] = useState(''); const [condition, setCondition] = useState('error_rate_exceeds'); const [threshold, setThreshold] = useState('0.1'); const [windowMinutes, setWindowMinutes] = useState(60); const [webhookUrl, setWebhookUrl] = useState(''); const [agentId, setAgentId] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setSaving(true); try { const data: CreateAlertRuleData = { name, condition, threshold: parseFloat(threshold), windowMinutes, notifyChannels: webhookUrl ? [webhookUrl] : [], scope: agentId ? { agentId } : {}, }; await createAlertRule(data); onCreated(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create rule'); } finally { setSaving(false); } }; return (

Create Alert Rule

{error && (
{error}
)}
setName(e.target.value)} placeholder="e.g., High Error Rate Alert" />
setThreshold(e.target.value)} />
setAgentId(e.target.value)} placeholder="my-agent" />
setWebhookUrl(e.target.value)} placeholder="https://hooks.slack.com/services/..." />
); } // ─── Rule Row ─────────────────────────────────────────────────── interface RuleRowProps { rule: AlertRuleData; onToggle: (id: string, enabled: boolean) => void; onDelete: (id: string) => void; } function RuleRow({ rule, onToggle, onDelete }: RuleRowProps): React.ReactElement { return ( {rule.name} {conditionLabel(rule.condition)} {formatValue(rule.condition, rule.threshold)} {rule.windowMinutes}m {rule.notifyChannels.length > 0 ? `${rule.notifyChannels.length} channel(s)` : '—'} ); } // ─── Main Alerts Component ────────────────────────────────────── export function Alerts(): React.ReactElement { const [tab, setTab] = useState('rules'); const [showCreate, setShowCreate] = useState(false); const rules = useApi(() => getAlertRules(), []); const history = useApi( () => getAlertHistory({ limit: 50 }), [], ); const handleToggle = useCallback( async (id: string, enabled: boolean) => { try { await updateAlertRule(id, { enabled }); rules.refetch(); } catch (err) { console.error('Failed to toggle rule:', err); } }, [rules], ); const handleDelete = useCallback( async (id: string) => { if (!confirm('Delete this alert rule?')) return; try { await deleteAlertRule(id); rules.refetch(); } catch (err) { console.error('Failed to delete rule:', err); } }, [rules], ); const handleCreated = useCallback(() => { setShowCreate(false); rules.refetch(); }, [rules]); // Compute active alerts (recent history entries that are unresolved) const activeAlerts = history.data?.entries.filter((e) => !e.resolvedAt) ?? []; return (

Alerts

{/* Active Alerts Banner */} {activeAlerts.length > 0 && (

🔴 Active Alerts ({activeAlerts.length})

{activeAlerts.slice(0, 5).map((alert) => (
{alert.message} {formatTimestamp(alert.triggeredAt)}
))}
)} {/* Create Rule Form */} {showCreate && ( setShowCreate(false)} /> )} {/* Tabs */}
{/* Rules Tab */} {tab === 'rules' && (
{rules.loading ? (
Loading rules...
) : rules.error ? (
Error: {rules.error}
) : (rules.data ?? []).length === 0 ? (
🔔

No alert rules configured.

) : ( {(Array.isArray(rules.data) ? rules.data : []).map((rule) => ( ))}
Name Condition Threshold Window Status Channels Actions
)}
)} {/* History Tab */} {tab === 'history' && (
{history.loading ? (
Loading history...
) : history.error ? (
Error: {history.error}
) : (history.data?.entries ?? []).length === 0 ? (
📜

No alert history yet.

) : ( {(history.data?.entries ?? []).map((entry) => ( ))}
Triggered Message Value Threshold Status
{formatTimestamp(entry.triggeredAt)} {entry.message} {entry.currentValue.toFixed(4)} {entry.threshold.toFixed(4)} {entry.resolvedAt ? 'Resolved' : 'Active'}
)}
)}
); }