diff --git a/ui/src/components/sidebar-utils.test.ts b/ui/src/components/sidebar-utils.test.ts index 74cc9a37..11b9c36c 100644 --- a/ui/src/components/sidebar-utils.test.ts +++ b/ui/src/components/sidebar-utils.test.ts @@ -2,16 +2,14 @@ import { describe, it, expect } from 'vitest'; import { buildAlertsTreeNodes } from './sidebar-utils'; describe('buildAlertsTreeNodes', () => { - it('returns 5 entries with inbox/all/rules/silences/history paths', () => { + it('returns 3 entries with inbox/rules/silences paths', () => { const nodes = buildAlertsTreeNodes(); - expect(nodes).toHaveLength(5); + expect(nodes).toHaveLength(3); const paths = nodes.map((n) => n.path); expect(paths).toEqual([ '/alerts/inbox', - '/alerts/all', '/alerts/rules', '/alerts/silences', - '/alerts/history', ]); }); }); diff --git a/ui/src/components/sidebar-utils.ts b/ui/src/components/sidebar-utils.ts index 12aad938..9da2e922 100644 --- a/ui/src/components/sidebar-utils.ts +++ b/ui/src/components/sidebar-utils.ts @@ -1,6 +1,6 @@ import { createElement, type ReactNode } from 'react'; import type { SidebarTreeNode } from '@cameleer/design-system'; -import { AlertTriangle, Inbox, List, ScrollText, BellOff } from 'lucide-react'; +import { AlertTriangle, Inbox, BellOff } from 'lucide-react'; /* ------------------------------------------------------------------ */ /* Domain types (moved out of DS — no longer exported there) */ @@ -117,15 +117,13 @@ export function buildAdminTreeNodes(opts?: { infrastructureEndpoints?: boolean } /** * Alerts tree — static nodes for the alerting section. - * Paths: /alerts/{inbox|all|rules|silences|history} + * Paths: /alerts/{inbox|rules|silences} */ export function buildAlertsTreeNodes(): SidebarTreeNode[] { const icon = (el: ReactNode) => el; return [ { id: 'alerts-inbox', label: 'Inbox', path: '/alerts/inbox', icon: icon(createElement(Inbox, { size: 14 })) }, - { id: 'alerts-all', label: 'All', path: '/alerts/all', icon: icon(createElement(List, { size: 14 })) }, { id: 'alerts-rules', label: 'Rules', path: '/alerts/rules', icon: icon(createElement(AlertTriangle, { size: 14 })) }, { id: 'alerts-silences', label: 'Silences', path: '/alerts/silences', icon: icon(createElement(BellOff, { size: 14 })) }, - { id: 'alerts-history', label: 'History', path: '/alerts/history', icon: icon(createElement(ScrollText, { size: 14 })) }, ]; } diff --git a/ui/src/pages/Alerts/AllAlertsPage.tsx b/ui/src/pages/Alerts/AllAlertsPage.tsx deleted file mode 100644 index a398109a..00000000 --- a/ui/src/pages/Alerts/AllAlertsPage.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useState } from 'react'; -import { Link } from 'react-router'; -import { Bell } from 'lucide-react'; -import { - ButtonGroup, DataTable, EmptyState, -} from '@cameleer/design-system'; -import type { ButtonGroupItem, Column } from '@cameleer/design-system'; -import { PageLoader } from '../../components/PageLoader'; -import { SeverityBadge } from '../../components/SeverityBadge'; -import { AlertStateChip } from '../../components/AlertStateChip'; -import { - useAlerts, useMarkAlertRead, - 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'; - -type AlertState = NonNullable; - -const STATE_ITEMS: ButtonGroupItem[] = [ - { value: 'FIRING', label: 'Firing', color: 'var(--error)' }, - { value: 'ACKNOWLEDGED', label: 'Acknowledged', color: 'var(--warning)' }, - { value: 'PENDING', label: 'Pending', color: 'var(--text-muted)' }, - { value: 'RESOLVED', label: 'Resolved', color: 'var(--success)' }, -]; - -const DEFAULT_OPEN_STATES = new Set(['PENDING', 'FIRING', 'ACKNOWLEDGED']); - -export default function AllAlertsPage() { - const [stateSel, setStateSel] = useState>(() => new Set(DEFAULT_OPEN_STATES)); - const stateValues: AlertState[] | undefined = stateSel.size === 0 - ? undefined - : [...stateSel] as AlertState[]; - - const { data, isLoading, error } = useAlerts({ state: stateValues, limit: 200 }); - const markRead = useMarkAlertRead(); - - const rows = data ?? []; - - const columns: Column[] = [ - { - key: 'severity', header: 'Severity', width: '110px', - render: (_, row) => row.severity ? : null, - }, - { - key: 'state', header: 'Status', width: '140px', - render: (_, row) => row.state ? : null, - }, - { - key: 'title', header: 'Title', - render: (_, row) => ( -
- row.id && markRead.mutate(row.id)}> - {row.title ?? '(untitled)'} - - {row.message && {row.message}} -
- ), - }, - { - key: 'firedAt', header: 'Fired at', width: '140px', sortable: true, - render: (_, row) => - row.firedAt ? ( - - {formatRelativeTime(row.firedAt)} - - ) : '—', - }, - ]; - - if (isLoading) return ; - if (error) return
Failed to load alerts: {String(error)}
; - - return ( -
-
-
-

All alerts

- {rows.length} matching your filter -
-
- -
-
- - {rows.length === 0 ? ( - } - title="No alerts match this filter" - description="Try switching to a different state or widening your criteria." - /> - ) : ( -
- - columns={columns as Column[]} - data={rows as Array} - sortable - flush - fillHeight - pageSize={200} - rowAccent={(row) => row.severity ? severityToAccent(row.severity) : undefined} - expandedContent={renderAlertExpanded} - /> -
- )} -
- ); -} diff --git a/ui/src/pages/Alerts/HistoryPage.tsx b/ui/src/pages/Alerts/HistoryPage.tsx deleted file mode 100644 index a7e22922..00000000 --- a/ui/src/pages/Alerts/HistoryPage.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { Link } from 'react-router'; -import { History } from 'lucide-react'; -import { - DataTable, EmptyState, useGlobalFilters, -} from '@cameleer/design-system'; -import type { Column } from '@cameleer/design-system'; -import { PageLoader } from '../../components/PageLoader'; -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 { timeRange } = useGlobalFilters(); - - // useAlerts doesn't accept a time range today; filter client-side - // against the global TimeRangeDropdown in the top bar. - 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 >= timeRange.start.getTime() && t <= timeRange.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)}
; - - return ( -
-
-
-

Alert history

- - {filtered.length === 0 - ? 'No resolved alerts in range' - : `${filtered.length} resolved alert${filtered.length === 1 ? '' : 's'} in range`} - -
-
- - {filtered.length === 0 ? ( - } - title="No resolved alerts" - description="Nothing in the selected date range. Try widening it." - /> - ) : ( -
- - columns={columns as Column[]} - data={filtered as Array} - sortable - flush - fillHeight - pageSize={200} - rowAccent={(row) => row.severity ? severityToAccent(row.severity) : undefined} - expandedContent={renderAlertExpanded} - /> -
- )} -
- ); -} diff --git a/ui/src/router.tsx b/ui/src/router.tsx index 7bee0798..0fbd88e6 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -24,8 +24,6 @@ const SensitiveKeysPage = lazy(() => import('./pages/Admin/SensitiveKeysPage')); const AppsTab = lazy(() => import('./pages/AppsTab/AppsTab')); const SwaggerPage = lazy(() => import('./pages/Swagger/SwaggerPage')); const InboxPage = lazy(() => import('./pages/Alerts/InboxPage')); -const AllAlertsPage = lazy(() => import('./pages/Alerts/AllAlertsPage')); -const HistoryPage = lazy(() => import('./pages/Alerts/HistoryPage')); const RulesListPage = lazy(() => import('./pages/Alerts/RulesListPage')); const RuleEditorWizard = lazy(() => import('./pages/Alerts/RuleEditor/RuleEditorWizard')); const SilencesPage = lazy(() => import('./pages/Alerts/SilencesPage')); @@ -84,8 +82,6 @@ export const router = createBrowserRouter([ // Alerts { path: 'alerts', element: }, { path: 'alerts/inbox', element: }, - { path: 'alerts/all', element: }, - { path: 'alerts/history', element: }, { path: 'alerts/rules', element: }, { path: 'alerts/rules/new', element: }, { path: 'alerts/rules/:id', element: },