diff --git a/ui/src/pages/Alerts/HistoryPage.tsx b/ui/src/pages/Alerts/HistoryPage.tsx index 27f01bb0..d80bdb7c 100644 --- a/ui/src/pages/Alerts/HistoryPage.tsx +++ b/ui/src/pages/Alerts/HistoryPage.tsx @@ -1,26 +1,116 @@ -import { SectionHeader } from '@cameleer/design-system'; +import { useState } from 'react'; +import { Link } from 'react-router'; +import { History } from 'lucide-react'; +import { + SectionHeader, DataTable, EmptyState, DateRangePicker, +} from '@cameleer/design-system'; +import type { Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; -import { useAlerts } from '../../api/queries/alerts'; -import { AlertRow } from './AlertRow'; +import { SeverityBadge } from '../../components/SeverityBadge'; +import { + useAlerts, type AlertDto, +} from '../../api/queries/alerts'; +import { severityToAccent } from './severity-utils'; +import { formatRelativeTime } from './time-utils'; +import { renderAlertExpanded } from './alert-expanded'; import css from './alerts-page.module.css'; +import tableStyles from '../../styles/table-section.module.css'; + +/** Duration in s/m/h/d. Pure, best-effort. */ +function formatDuration(from?: string | null, to?: string | null): string { + if (!from || !to) return '—'; + const ms = new Date(to).getTime() - new Date(from).getTime(); + if (ms < 0 || Number.isNaN(ms)) return '—'; + const sec = Math.floor(ms / 1000); + if (sec < 60) return `${sec}s`; + if (sec < 3600) return `${Math.floor(sec / 60)}m`; + if (sec < 86_400) return `${Math.floor(sec / 3600)}h`; + return `${Math.floor(sec / 86_400)}d`; +} export default function HistoryPage() { + const [dateRange, setDateRange] = useState({ + start: new Date(Date.now() - 7 * 24 * 3600_000), + end: new Date(), + }); + + // useAlerts doesn't accept a time range today; filter client-side. const { data, isLoading, error } = useAlerts({ state: 'RESOLVED', limit: 200 }); + const filtered = (data ?? []).filter((a) => { + if (!a.firedAt) return false; + const t = new Date(a.firedAt).getTime(); + return t >= dateRange.start.getTime() && t <= dateRange.end.getTime(); + }); + + const columns: Column[] = [ + { + key: 'severity', header: 'Severity', width: '110px', + render: (_, row) => row.severity ? : null, + }, + { + key: 'title', header: 'Title', + render: (_, row) => ( +
+ {row.title ?? '(untitled)'} + {row.message && {row.message}} +
+ ), + }, + { + key: 'firedAt', header: 'Fired at', width: '140px', sortable: true, + render: (_, row) => + row.firedAt ? ( + + {formatRelativeTime(row.firedAt)} + + ) : '—', + }, + { + key: 'resolvedAt', header: 'Resolved at', width: '140px', sortable: true, + render: (_, row) => + row.resolvedAt ? ( + + {formatRelativeTime(row.resolvedAt)} + + ) : '—', + }, + { + key: 'duration', header: 'Duration', width: '90px', + render: (_, row) => formatDuration(row.firedAt, row.resolvedAt), + }, + ]; + if (isLoading) return ; if (error) return
Failed to load history: {String(error)}
; - const rows = data ?? []; - return (
History
- {rows.length === 0 ? ( -
No resolved alerts in retention window.
+ +
+ +
+ + {filtered.length === 0 ? ( + } + title="No resolved alerts" + description="Nothing in the selected date range. Try widening it." + /> ) : ( - rows.map((a) => ) +
+ + columns={columns as Column[]} + data={filtered as Array} + sortable + flush + rowAccent={(row) => row.severity ? severityToAccent(row.severity) : undefined} + expandedContent={renderAlertExpanded} + /> +
)}
);