import { type FormEvent, useState } from 'react'; import { ChevronDown, Plus, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import type { Monitor, MonitorInput, MonitorMethod } from '../../api/monitors'; import { useCreateMonitorMutation, useUpdateMonitorMutation } from '../../queries/monitors'; export const DEFAULT_MONITOR_INPUT: MonitorInput = { name: '', url: 'https://', method: 'GET', expectedStatus: 200, intervalSeconds: 300, timeoutMs: 10_000, retryCount: 1, failureThreshold: 2, expectKeyword: null, keywordInverted: false, requestHeaders: null, requestBody: null, degradedLatencyMs: null, enabled: true, alertsEnabled: true, }; export const INTERVAL_OPTIONS = [ { value: '300', label: '5 minutes' }, { value: '900', label: '15 minutes' }, { value: '1800', label: '30 minutes' }, { value: '3600', label: '1 hour' }, { value: '86400', label: '24 hours' }, ]; type MonitorFormDialogProps = { editing: Monitor | null; onClose: () => void; }; type HeaderRow = { id: number; name: string; value: string }; function headerRows(requestHeaders: string | null): HeaderRow[] { if (!requestHeaders) return []; try { return Object.entries(JSON.parse(requestHeaders) as Record).map(([name, value], index) => ({ id: index, name, value })); } catch { return []; } } function errorMessage(error: unknown, fallback: string) { return error instanceof Error ? error.message : fallback; } function monitorInput(monitor: Monitor | null): MonitorInput { if (!monitor) return DEFAULT_MONITOR_INPUT; return { name: monitor.name, url: monitor.url, method: monitor.method, expectedStatus: monitor.expectedStatus, intervalSeconds: monitor.intervalSeconds, timeoutMs: monitor.timeoutMs, retryCount: monitor.retryCount, failureThreshold: monitor.failureThreshold, expectKeyword: monitor.expectKeyword, keywordInverted: monitor.keywordInverted, requestHeaders: monitor.requestHeaders ? (JSON.parse(monitor.requestHeaders) as Record) : null, requestBody: monitor.requestBody, degradedLatencyMs: monitor.degradedLatencyMs, enabled: monitor.enabled, alertsEnabled: monitor.alertsEnabled, }; } export function MonitorFormDialog({ editing, onClose }: MonitorFormDialogProps) { const createMutation = useCreateMonitorMutation(); const updateMutation = useUpdateMonitorMutation(); const [form, setForm] = useState(() => monitorInput(editing)); const [headers, setHeaders] = useState(() => headerRows(editing?.requestHeaders ?? null)); const [nextHeaderId, setNextHeaderId] = useState(() => headers.length); const formMutation = editing ? updateMutation : createMutation; function closeForm() { if (formMutation.isPending) return; onClose(); } function handleSubmit(event: FormEvent) { event.preventDefault(); const requestHeaders = Object.fromEntries( headers.filter((header) => header.name.trim()).map((header) => [header.name.trim(), header.value]), ); const input: MonitorInput = { ...form, expectKeyword: form.expectKeyword?.trim() || null, requestHeaders: Object.keys(requestHeaders).length > 0 ? requestHeaders : null, requestBody: form.method === 'POST' ? form.requestBody || null : null, }; if (editing) { updateMutation.mutate({ id: editing.id, input }, { onSuccess: onClose }); } else { createMutation.mutate(input, { onSuccess: onClose }); } } function addHeader() { setHeaders((current) => [...current, { id: nextHeaderId, name: '', value: '' }]); setNextHeaderId((current) => current + 1); } function updateHeader(id: number, field: 'name' | 'value', value: string) { setHeaders((current) => current.map((header) => (header.id === id ? { ...header, [field]: value } : header))); } return ( { if (!nextOpen) closeForm(); }} > { if (formMutation.isPending) event.preventDefault(); }} onInteractOutside={(event) => { if (formMutation.isPending) event.preventDefault(); }} >

Configuration

{editing ? `Edit ${editing.name}` : 'Add a monitor'} Checks run at least every five minutes. Failures are retried immediately and must repeat before an incident is published.
Method
Interval
Retries after a failure Retried immediately, so a sub-second blip never reaches the status page.
Confirmations before alerting Confirmed after about {Math.round(((form.failureThreshold ?? 2) * form.intervalSeconds) / 60)} minutes.
setForm({ ...form, enabled })} />
setForm({ ...form, alertsEnabled })} />
Advanced request and response checks
setForm({ ...form, keywordInverted })} disabled={!form.expectKeyword} />
Request headers Up to 10 headers. Restricted transport headers are blocked.
{headers.length > 0 && (
{headers.map((header) => (
updateHeader(header.id, 'name', event.target.value)} maxLength={64} placeholder="Authorization" /> updateHeader(header.id, 'value', event.target.value)} maxLength={512} placeholder="Bearer …" />
))}
)}
{form.method === 'POST' && (