Add alert rule editing UI
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import type {
|
||||
AlertRule,
|
||||
AlertRuleUpdate,
|
||||
Asset,
|
||||
CheckResult,
|
||||
Incident,
|
||||
@@ -85,6 +87,12 @@ export const api = {
|
||||
}),
|
||||
monitorResults: (token: string, monitorId: number, limit = 1) =>
|
||||
request<CheckResult[]>(`/monitors/${monitorId}/results?limit=${limit}`, token),
|
||||
alertRules: (token: string) => request<AlertRule[]>("/alerts/rules", token),
|
||||
updateAlertRule: (token: string, ruleId: number, payload: AlertRuleUpdate) =>
|
||||
request<AlertRule>(`/alerts/rules/${ruleId}`, token, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
incidents: (token: string) => request<Incident[]>("/incidents", token),
|
||||
acknowledgeIncident: (token: string, incidentId: number) =>
|
||||
request<Incident>(`/incidents/${incidentId}/acknowledge`, token, {
|
||||
|
||||
@@ -84,7 +84,7 @@ export function App() {
|
||||
<NetworkChecksPage token={auth.token} monitors={monitors} onChanged={refreshData} />
|
||||
) : null}
|
||||
{page === "alerts" ? (
|
||||
<AlertsPage token={auth.token} incidents={incidents} selectedIncidentId={selectedIncidentId} onChanged={refreshData} />
|
||||
<AlertsPage token={auth.token} monitors={monitors} incidents={incidents} selectedIncidentId={selectedIncidentId} onChanged={refreshData} />
|
||||
) : null}
|
||||
{page === "discovery" ? <ListPage title="Discovery" description="Guided target discovery with monitor and alert choices." /> : null}
|
||||
{page === "graphs" ? <ListPage title="Graphs" description="Metric history and dashboard-ready charts." /> : null}
|
||||
|
||||
@@ -1,22 +1,52 @@
|
||||
import { useState } from "react";
|
||||
import { AlertTriangle, BellOff, CheckCheck, RefreshCw } from "lucide-react";
|
||||
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||
import { AlertTriangle, BellOff, CheckCheck, Pencil, RefreshCw, Save, ShieldAlert, X } from "lucide-react";
|
||||
|
||||
import { api } from "../api/client";
|
||||
import { Button } from "../components/Button";
|
||||
import type { Incident } from "../types/api";
|
||||
import type { AlertRule, Incident, Monitor } from "../types/api";
|
||||
|
||||
interface AlertsPageProps {
|
||||
token: string;
|
||||
monitors: Monitor[];
|
||||
incidents: Incident[];
|
||||
selectedIncidentId?: number | null;
|
||||
onChanged: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function AlertsPage({ token, incidents, selectedIncidentId, onChanged }: AlertsPageProps) {
|
||||
const [busyId, setBusyId] = useState<number | null>(null);
|
||||
export function AlertsPage({ token, monitors, incidents, selectedIncidentId, onChanged }: AlertsPageProps) {
|
||||
const [rules, setRules] = useState<AlertRule[]>([]);
|
||||
const [busyIncidentId, setBusyIncidentId] = useState<number | null>(null);
|
||||
const [busyRuleId, setBusyRuleId] = useState<number | null>(null);
|
||||
const [editingRuleId, setEditingRuleId] = useState<number | null>(null);
|
||||
const [ruleName, setRuleName] = useState("");
|
||||
const [severity, setSeverity] = useState("warning");
|
||||
const [failureThreshold, setFailureThreshold] = useState(3);
|
||||
const [cooldownSeconds, setCooldownSeconds] = useState(300);
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [tlsCheckEnabled, setTlsCheckEnabled] = useState(false);
|
||||
const [tlsWarningDays, setTlsWarningDays] = useState(30);
|
||||
const [savingRule, setSavingRule] = useState(false);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
const monitorById = useMemo(() => new Map(monitors.map((monitor) => [monitor.id, monitor])), [monitors]);
|
||||
const editingRule = rules.find((rule) => rule.id === editingRuleId) ?? null;
|
||||
const editingMonitor = editingRule ? (monitorById.get(editingRule.monitor_id) ?? null) : null;
|
||||
const editingHttpsMonitor = editingMonitor && isHttpsWebsiteMonitor(editingMonitor) ? editingMonitor : null;
|
||||
|
||||
async function refreshRules() {
|
||||
setRules(await api.alertRules(token));
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
await Promise.all([onChanged(), refreshRules()]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshRules().catch(() => setRules([]));
|
||||
}, [token]);
|
||||
|
||||
async function runAction(incidentId: number, action: "ack" | "silence") {
|
||||
setBusyId(incidentId);
|
||||
setBusyIncidentId(incidentId);
|
||||
try {
|
||||
if (action === "ack") {
|
||||
await api.acknowledgeIncident(token, incidentId);
|
||||
@@ -25,7 +55,78 @@ export function AlertsPage({ token, incidents, selectedIncidentId, onChanged }:
|
||||
}
|
||||
await onChanged();
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
setBusyIncidentId(null);
|
||||
}
|
||||
}
|
||||
|
||||
function startEdit(rule: AlertRule) {
|
||||
setEditingRuleId(rule.id);
|
||||
setRuleName(rule.name);
|
||||
setSeverity(rule.severity);
|
||||
setFailureThreshold(rule.failure_threshold);
|
||||
setCooldownSeconds(rule.cooldown_seconds);
|
||||
setEnabled(rule.is_enabled);
|
||||
const monitor = monitorById.get(rule.monitor_id);
|
||||
setTlsCheckEnabled(Boolean(monitor?.config?.check_tls_expiry ?? false));
|
||||
setTlsWarningDays(Number(monitor?.config?.tls_warning_days ?? 30));
|
||||
setMessage(null);
|
||||
}
|
||||
|
||||
function resetRuleForm() {
|
||||
setEditingRuleId(null);
|
||||
setRuleName("");
|
||||
setSeverity("warning");
|
||||
setFailureThreshold(3);
|
||||
setCooldownSeconds(300);
|
||||
setEnabled(true);
|
||||
setTlsCheckEnabled(false);
|
||||
setTlsWarningDays(30);
|
||||
}
|
||||
|
||||
async function saveRule(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
if (!editingRule) return;
|
||||
|
||||
setSavingRule(true);
|
||||
setMessage(null);
|
||||
try {
|
||||
await Promise.all([
|
||||
api.updateAlertRule(token, editingRule.id, {
|
||||
name: ruleName,
|
||||
severity,
|
||||
failure_threshold: failureThreshold,
|
||||
cooldown_seconds: cooldownSeconds,
|
||||
is_enabled: enabled,
|
||||
}),
|
||||
editingHttpsMonitor
|
||||
? api.updateMonitor(token, editingHttpsMonitor.id, {
|
||||
config: {
|
||||
...(editingHttpsMonitor.config ?? {}),
|
||||
check_tls_expiry: tlsCheckEnabled,
|
||||
tls_warning_days: tlsWarningDays,
|
||||
},
|
||||
})
|
||||
: Promise.resolve(),
|
||||
]);
|
||||
resetRuleForm();
|
||||
await refreshAll();
|
||||
} catch (err) {
|
||||
setMessage(err instanceof Error ? err.message : "Could not save alert rule");
|
||||
} finally {
|
||||
setSavingRule(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleRule(rule: AlertRule) {
|
||||
setBusyRuleId(rule.id);
|
||||
setMessage(null);
|
||||
try {
|
||||
await api.updateAlertRule(token, rule.id, { is_enabled: !rule.is_enabled });
|
||||
await refreshRules();
|
||||
} catch (err) {
|
||||
setMessage(err instanceof Error ? err.message : "Could not update alert rule");
|
||||
} finally {
|
||||
setBusyRuleId(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +135,129 @@ export function AlertsPage({ token, incidents, selectedIncidentId, onChanged }:
|
||||
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-end">
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold">Alerts</h1>
|
||||
<p className="mt-2 text-sm text-slate-400">Open incidents, acknowledgements, silences, recoveries, and notification history.</p>
|
||||
<p className="mt-2 text-sm text-slate-400">Alert rules, open incidents, acknowledgements, silences, recoveries, and notification history.</p>
|
||||
</div>
|
||||
<Button variant="ghost" onClick={onChanged}>
|
||||
<Button variant="ghost" onClick={refreshAll}>
|
||||
<RefreshCw size={16} />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<section className="grid gap-5 xl:grid-cols-[420px_minmax(0,1fr)]">
|
||||
<form className="space-y-4 rounded-md border border-line bg-[#0d131c] p-5" onSubmit={saveRule}>
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldAlert size={18} className="text-pulse" />
|
||||
<h2 className="text-base font-semibold">{editingRule ? "Edit Alert Rule" : "Alert Rule Settings"}</h2>
|
||||
</div>
|
||||
|
||||
{editingRule ? (
|
||||
<>
|
||||
<div className="rounded-md border border-line bg-slate-950 p-3 text-sm text-slate-300">
|
||||
<div className="font-medium">{editingMonitor?.name ?? `Monitor ${editingRule.monitor_id}`}</div>
|
||||
<div className="mt-1 text-xs text-slate-500">{describeCondition(editingRule.condition, editingMonitor)}</div>
|
||||
</div>
|
||||
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm text-slate-300">Rule Name</span>
|
||||
<input className="h-10 w-full rounded-md border border-line bg-slate-950 px-3 text-sm outline-none ring-pulse/40 focus:ring-2" value={ruleName} onChange={(event) => setRuleName(event.target.value)} required />
|
||||
</label>
|
||||
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm text-slate-300">Severity</span>
|
||||
<select className="h-10 w-full rounded-md border border-line bg-slate-950 px-3 text-sm outline-none ring-pulse/40 focus:ring-2" value={severity} onChange={(event) => setSeverity(event.target.value)}>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="critical">Critical</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm text-slate-300">Failure Threshold</span>
|
||||
<input className="h-10 w-full rounded-md border border-line bg-slate-950 px-3 text-sm outline-none ring-pulse/40 focus:ring-2" value={failureThreshold} onChange={(event) => setFailureThreshold(Number(event.target.value))} min={1} max={20} type="number" />
|
||||
</label>
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm text-slate-300">Cooldown Seconds</span>
|
||||
<input className="h-10 w-full rounded-md border border-line bg-slate-950 px-3 text-sm outline-none ring-pulse/40 focus:ring-2" value={cooldownSeconds} onChange={(event) => setCooldownSeconds(Number(event.target.value))} min={0} max={86400} type="number" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-md border border-line bg-slate-950 px-3 py-2">
|
||||
<span className="text-sm text-slate-300">Enabled</span>
|
||||
<input className="h-5 w-5 accent-teal-400" checked={enabled} onChange={(event) => setEnabled(event.target.checked)} type="checkbox" />
|
||||
</div>
|
||||
|
||||
{editingHttpsMonitor ? (
|
||||
<div className="space-y-3 rounded-md border border-line bg-slate-950 p-3">
|
||||
<div className="text-sm font-medium text-slate-300">TLS Certificate</div>
|
||||
<div className="grid gap-3 sm:grid-cols-[1fr_140px]">
|
||||
<div className="flex items-center justify-between rounded-md border border-line bg-[#0d131c] px-3 py-2">
|
||||
<span className="text-sm text-slate-300">Expiry Check</span>
|
||||
<input className="h-5 w-5 accent-teal-400" checked={tlsCheckEnabled} onChange={(event) => setTlsCheckEnabled(event.target.checked)} type="checkbox" />
|
||||
</div>
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm text-slate-300">Warn Days</span>
|
||||
<input className="h-10 w-full rounded-md border border-line bg-[#0d131c] px-3 text-sm outline-none ring-pulse/40 focus:ring-2" disabled={!tlsCheckEnabled} value={tlsWarningDays} onChange={(event) => setTlsWarningDays(Number(event.target.value))} min={1} max={365} type="number" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{message ? <div className="rounded-md border border-line bg-slate-950 p-3 text-sm text-slate-300">{message}</div> : null}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button className="flex-1" onClick={resetRuleForm} type="button" variant="ghost">
|
||||
<X size={16} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="flex-1" disabled={savingRule} type="submit">
|
||||
<Save size={16} />
|
||||
{savingRule ? "Saving..." : "Save Rule"}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="rounded-md border border-line bg-slate-950 p-4 text-sm text-slate-400">Select a rule from the list to edit severity, threshold, cooldown, or enabled state.</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<div className="rounded-md border border-line bg-[#0d131c]">
|
||||
<div className="border-b border-line p-4">
|
||||
<h2 className="text-base font-semibold">Alert Rules</h2>
|
||||
</div>
|
||||
<div className="divide-y divide-line">
|
||||
{rules.length ? (
|
||||
rules.map((rule) => {
|
||||
const monitor = monitorById.get(rule.monitor_id);
|
||||
return (
|
||||
<div key={rule.id} className={`grid gap-3 p-4 md:grid-cols-[1fr_100px_115px_165px] md:items-center ${editingRuleId === rule.id ? "bg-slate-800/60" : ""}`}>
|
||||
<div>
|
||||
<div className="font-medium">{rule.name}</div>
|
||||
<div className="text-sm text-slate-400">{monitor?.name ?? `Monitor ${rule.monitor_id}`}</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
{describeCondition(rule.condition, monitor)} after {rule.failure_threshold} failed {rule.failure_threshold === 1 ? "check" : "checks"}, cooldown {formatDuration(rule.cooldown_seconds)}
|
||||
</div>
|
||||
</div>
|
||||
<Badge value={rule.severity} tone={rule.severity === "critical" ? "critical" : "warning"} />
|
||||
<Badge value={rule.is_enabled ? "enabled" : "disabled"} tone={rule.is_enabled ? "ok" : "neutral"} />
|
||||
<div className="flex gap-2 md:justify-end">
|
||||
<Button className="h-8 px-3" disabled={busyRuleId === rule.id} onClick={() => toggleRule(rule)} title={rule.is_enabled ? "Disable rule" : "Enable rule"} type="button" variant="ghost">
|
||||
{rule.is_enabled ? "Disable" : "Enable"}
|
||||
</Button>
|
||||
<Button aria-label={`Edit ${rule.name}`} className="h-8 px-3" onClick={() => startEdit(rule)} title="Edit rule" type="button" variant="ghost">
|
||||
<Pencil className="text-slate-100" size={15} />
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="p-6 text-sm text-slate-400">No alert rules yet.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="rounded-md border border-line bg-[#0d131c]">
|
||||
<div className="grid grid-cols-[1fr_100px_130px_220px] gap-3 border-b border-line px-4 py-3 text-xs uppercase text-slate-500 max-lg:hidden">
|
||||
<div>Incident</div>
|
||||
@@ -78,11 +294,11 @@ export function AlertsPage({ token, incidents, selectedIncidentId, onChanged }:
|
||||
{incident.silenced_until ? <div className="text-xs text-slate-500">Silenced</div> : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button disabled={!open || Boolean(incident.acknowledged_at) || busyId === incident.id} onClick={() => runAction(incident.id, "ack")} variant="ghost">
|
||||
<Button disabled={!open || Boolean(incident.acknowledged_at) || busyIncidentId === incident.id} onClick={() => runAction(incident.id, "ack")} variant="ghost">
|
||||
<CheckCheck size={16} />
|
||||
Ack
|
||||
</Button>
|
||||
<Button disabled={!open || busyId === incident.id} onClick={() => runAction(incident.id, "silence")} variant="ghost">
|
||||
<Button disabled={!open || busyIncidentId === incident.id} onClick={() => runAction(incident.id, "silence")} variant="ghost">
|
||||
<BellOff size={16} />
|
||||
Silence
|
||||
</Button>
|
||||
@@ -99,6 +315,25 @@ export function AlertsPage({ token, incidents, selectedIncidentId, onChanged }:
|
||||
);
|
||||
}
|
||||
|
||||
function describeCondition(condition: Record<string, unknown>, monitor?: Monitor | null) {
|
||||
if (condition.type === "status_not_up" && monitor?.monitor_type === "http" && monitor.config?.check_tls_expiry) {
|
||||
return `Website failure or TLS certificate inside ${String(monitor.config.tls_warning_days ?? 30)}-day warning window`;
|
||||
}
|
||||
if (condition.type === "status_not_up") return "Monitor is not up";
|
||||
return "Custom condition";
|
||||
}
|
||||
|
||||
function isHttpsWebsiteMonitor(monitor: Monitor) {
|
||||
return monitor.monitor_type === "http" && monitor.target.trim().toLowerCase().startsWith("https://");
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number) {
|
||||
if (seconds === 0) return "off";
|
||||
if (seconds % 3600 === 0) return `${seconds / 3600}h`;
|
||||
if (seconds % 60 === 0) return `${seconds / 60}m`;
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
function Badge({ value, tone }: { value: string; tone: "critical" | "warning" | "ok" | "neutral" }) {
|
||||
const classes = {
|
||||
critical: "border-red-500/40 bg-red-950/40 text-red-200",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormEvent, useState } from "react";
|
||||
import { Activity, Edit3, PlugZap, Plus, RefreshCw, Trash2, X } from "lucide-react";
|
||||
import { Activity, Pencil, PlugZap, Plus, RefreshCw, Trash2, X } from "lucide-react";
|
||||
|
||||
import { api } from "../api/client";
|
||||
import { Button } from "../components/Button";
|
||||
@@ -205,20 +205,22 @@ export function NetworkChecksPage({ token, monitors, onChanged }: NetworkChecksP
|
||||
<div className="divide-y divide-line">
|
||||
{networkChecks.length ? (
|
||||
networkChecks.map((monitor) => (
|
||||
<div key={monitor.id} className="grid gap-2 p-4 md:grid-cols-[1fr_90px_110px_170px] md:items-center">
|
||||
<div key={monitor.id} className="grid gap-2 p-4 md:grid-cols-[1fr_90px_110px_250px] md:items-center">
|
||||
<div>
|
||||
<div className="font-medium">{monitor.name}</div>
|
||||
<div className="truncate text-sm text-slate-400">{monitor.target}</div>
|
||||
</div>
|
||||
<span className="text-sm uppercase text-slate-400">{monitor.monitor_type}</span>
|
||||
<Status status={monitor.status} />
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap items-center justify-start gap-2 md:justify-end">
|
||||
<div className="text-sm text-slate-400">{monitor.last_checked_at ? new Date(monitor.last_checked_at).toLocaleTimeString() : "Not checked"}</div>
|
||||
<Button aria-label={`Edit ${monitor.name}`} className="h-8 w-8 px-0" onClick={() => startEdit(monitor)} title="Edit check" type="button" variant="ghost">
|
||||
<Edit3 size={15} />
|
||||
<Button aria-label={`Edit ${monitor.name}`} className="h-8 px-3" onClick={() => startEdit(monitor)} title="Edit check" type="button" variant="ghost">
|
||||
<Pencil className="text-slate-100" size={15} />
|
||||
Edit
|
||||
</Button>
|
||||
<Button aria-label={`Delete ${monitor.name}`} className="h-8 w-8 px-0" disabled={deletingId === monitor.id} onClick={() => deleteMonitor(monitor.id)} title="Delete check" type="button" variant="ghost">
|
||||
<Trash2 size={15} />
|
||||
<Button aria-label={`Delete ${monitor.name}`} className="h-8 px-3 text-red-100" disabled={deletingId === monitor.id} onClick={() => deleteMonitor(monitor.id)} title="Delete check" type="button" variant="ghost">
|
||||
<Trash2 className="text-red-200" size={15} />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { Bell, Edit3, RefreshCw, Send, Trash2, X } from "lucide-react";
|
||||
import { Bell, Pencil, RefreshCw, Send, Trash2, X } from "lucide-react";
|
||||
|
||||
import { api } from "../api/client";
|
||||
import { Button } from "../components/Button";
|
||||
@@ -177,7 +177,7 @@ export function NotificationsPage({ token }: NotificationsPageProps) {
|
||||
<div className="divide-y divide-line">
|
||||
{channels.length ? (
|
||||
channels.map((channel) => (
|
||||
<div key={channel.id} className="grid gap-3 p-4 md:grid-cols-[1fr_140px_90px_150px] md:items-center">
|
||||
<div key={channel.id} className="grid gap-3 p-4 md:grid-cols-[1fr_140px_90px_260px] md:items-center">
|
||||
<div>
|
||||
<div className="font-medium">{channel.name}</div>
|
||||
<div className="text-sm text-slate-400">{String(channel.settings.username || "OrbitalWard")}</div>
|
||||
@@ -185,15 +185,18 @@ export function NotificationsPage({ token }: NotificationsPageProps) {
|
||||
</div>
|
||||
<div className="text-sm text-slate-300">{channel.channel_type}</div>
|
||||
<Status enabled={channel.is_enabled} />
|
||||
<div className="flex gap-2">
|
||||
<Button className="h-8 w-8 px-0" disabled={busyId === channel.id} onClick={() => testChannel(channel.id)} title="Test channel" type="button" variant="ghost">
|
||||
<Send size={15} />
|
||||
<div className="flex flex-wrap gap-2 md:justify-end">
|
||||
<Button className="h-8 px-3" disabled={busyId === channel.id} onClick={() => testChannel(channel.id)} title="Test channel" type="button" variant="ghost">
|
||||
<Send className="text-slate-100" size={15} />
|
||||
Test
|
||||
</Button>
|
||||
<Button className="h-8 w-8 px-0" disabled={busyId === channel.id} onClick={() => startEdit(channel)} title="Edit channel" type="button" variant="ghost">
|
||||
<Edit3 size={15} />
|
||||
<Button className="h-8 px-3" disabled={busyId === channel.id} onClick={() => startEdit(channel)} title="Edit channel" type="button" variant="ghost">
|
||||
<Pencil className="text-slate-100" size={15} />
|
||||
Edit
|
||||
</Button>
|
||||
<Button className="h-8 w-8 px-0" disabled={busyId === channel.id} onClick={() => deleteChannel(channel.id)} title="Delete channel" type="button" variant="ghost">
|
||||
<Trash2 size={15} />
|
||||
<Button className="h-8 px-3 text-red-100" disabled={busyId === channel.id} onClick={() => deleteChannel(channel.id)} title="Delete channel" type="button" variant="ghost">
|
||||
<Trash2 className="text-red-200" size={15} />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { Edit3, Globe2, Plus, RefreshCw, Trash2, X } from "lucide-react";
|
||||
import { Globe2, Pencil, Plus, RefreshCw, Trash2, X } from "lucide-react";
|
||||
|
||||
import { api } from "../api/client";
|
||||
import { Button } from "../components/Button";
|
||||
@@ -233,7 +233,7 @@ export function WebsitesPage({ token, monitors, onCreated }: WebsitesPageProps)
|
||||
<div className="divide-y divide-line">
|
||||
{websites.length ? (
|
||||
websites.map((monitor) => (
|
||||
<div key={monitor.id} className="grid gap-2 p-4 md:grid-cols-[1fr_110px_170px] md:items-center">
|
||||
<div key={monitor.id} className="grid gap-2 p-4 md:grid-cols-[1fr_110px_250px] md:items-center">
|
||||
<div>
|
||||
<div className="font-medium">{monitor.name}</div>
|
||||
<div className="truncate text-sm text-slate-400">{monitor.target}</div>
|
||||
@@ -241,13 +241,15 @@ export function WebsitesPage({ token, monitors, onCreated }: WebsitesPageProps)
|
||||
{monitor.config?.check_tls_expiry ? <div className="text-xs text-slate-500">TLS warning at {String(monitor.config.tls_warning_days ?? 30)} days</div> : null}
|
||||
</div>
|
||||
<Status status={monitor.status} />
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap items-center justify-start gap-2 md:justify-end">
|
||||
<div className="text-sm text-slate-400">{monitor.last_checked_at ? new Date(monitor.last_checked_at).toLocaleTimeString() : "Not checked"}</div>
|
||||
<Button aria-label={`Edit ${monitor.name}`} className="h-8 w-8 px-0" onClick={() => startEdit(monitor)} title="Edit monitor" type="button" variant="ghost">
|
||||
<Edit3 size={15} />
|
||||
<Button aria-label={`Edit ${monitor.name}`} className="h-8 px-3" onClick={() => startEdit(monitor)} title="Edit monitor" type="button" variant="ghost">
|
||||
<Pencil className="text-slate-100" size={15} />
|
||||
Edit
|
||||
</Button>
|
||||
<Button aria-label={`Delete ${monitor.name}`} className="h-8 w-8 px-0" disabled={deletingId === monitor.id} onClick={() => deleteMonitor(monitor.id)} title="Delete monitor" type="button" variant="ghost">
|
||||
<Trash2 size={15} />
|
||||
<Button aria-label={`Delete ${monitor.name}`} className="h-8 px-3 text-red-100" disabled={deletingId === monitor.id} onClick={() => deleteMonitor(monitor.id)} title="Delete monitor" type="button" variant="ghost">
|
||||
<Trash2 className="text-red-200" size={15} />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,6 +57,27 @@ export interface Incident {
|
||||
details: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AlertRule {
|
||||
id: number;
|
||||
monitor_id: number;
|
||||
name: string;
|
||||
severity: string;
|
||||
condition: Record<string, unknown>;
|
||||
failure_threshold: number;
|
||||
cooldown_seconds: number;
|
||||
is_enabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface AlertRuleUpdate {
|
||||
name?: string;
|
||||
severity?: string;
|
||||
failure_threshold?: number;
|
||||
cooldown_seconds?: number;
|
||||
is_enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface NotificationChannel {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user