From 468132d1dddeda7c6b31474d0028574423d6a3f8 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 12:10:20 +0200 Subject: [PATCH] fix(ui/alerts): bell spacing, rule editor width, inbox bulk controls Round 4 smoke feedback on /alerts: - Bell now has consistent 12px gap from env selector and user name (wrap env + bell in flex container inside TopBar's environment prop) - RuleEditorWizard constrained to max-width 840px (centered) and upgraded the page title from SectionHeader to h2 pattern used by the list pages - Inbox: added select-all checkbox, severity SegmentedTabs filter (All / Critical / Warning / Info), and bulk-ack actions (Acknowledge selected + Acknowledge all firing) alongside the existing mark-read actions --- ui/src/components/LayoutShell.tsx | 4 +- ui/src/pages/Alerts/InboxPage.tsx | 114 +++++++++++++++--- .../Alerts/RuleEditor/RuleEditorWizard.tsx | 4 +- .../pages/Alerts/RuleEditor/wizard.module.css | 11 ++ 4 files changed, 111 insertions(+), 22 deletions(-) diff --git a/ui/src/components/LayoutShell.tsx b/ui/src/components/LayoutShell.tsx index 4ae0be9f..f2c49ea7 100644 --- a/ui/src/components/LayoutShell.tsx +++ b/ui/src/components/LayoutShell.tsx @@ -957,14 +957,14 @@ function LayoutContent() { +
- +
} user={username ? { name: username } : undefined} userMenuItems={userMenuItems} diff --git a/ui/src/pages/Alerts/InboxPage.tsx b/ui/src/pages/Alerts/InboxPage.tsx index 2ce460a0..99be8d12 100644 --- a/ui/src/pages/Alerts/InboxPage.tsx +++ b/ui/src/pages/Alerts/InboxPage.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'; import { Link } from 'react-router'; import { Inbox } from 'lucide-react'; import { - Button, DataTable, EmptyState, useToast, + Button, DataTable, EmptyState, SegmentedTabs, useToast, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; @@ -18,8 +18,24 @@ import { renderAlertExpanded } from './alert-expanded'; import css from './alerts-page.module.css'; import tableStyles from '../../styles/table-section.module.css'; +type Severity = NonNullable; + +const SEVERITY_FILTERS: Record = { + all: { label: 'All severities', values: undefined }, + critical: { label: 'Critical', values: ['CRITICAL'] }, + warning: { label: 'Warning', values: ['WARNING'] }, + info: { label: 'Info', values: ['INFO'] }, +}; + export default function InboxPage() { - const { data, isLoading, error } = useAlerts({ state: ['FIRING', 'ACKNOWLEDGED'], limit: 200 }); + const [severityKey, setSeverityKey] = useState('all'); + const severityFilter = SEVERITY_FILTERS[severityKey]; + + const { data, isLoading, error } = useAlerts({ + state: ['FIRING', 'ACKNOWLEDGED'], + severity: severityFilter.values, + limit: 200, + }); const bulkRead = useBulkReadAlerts(); const markRead = useMarkAlertRead(); const ack = useAckAlert(); @@ -33,6 +49,11 @@ export default function InboxPage() { [rows], ); + const firingIds = unreadIds; // FIRING alerts are the ones that can be ack'd + + const allSelected = rows.length > 0 && rows.every((r) => selected.has(r.id)); + const someSelected = selected.size > 0 && !allSelected; + const toggleSelected = (id: string) => { setSelected((prev) => { const next = new Set(prev); @@ -41,6 +62,14 @@ export default function InboxPage() { }); }; + const toggleSelectAll = () => { + if (allSelected) { + setSelected(new Set()); + } else { + setSelected(new Set(rows.map((r) => r.id))); + } + }; + const onAck = async (id: string, title?: string) => { try { await ack.mutateAsync(id); @@ -50,6 +79,17 @@ export default function InboxPage() { } }; + const onBulkAck = async (ids: string[]) => { + if (ids.length === 0) return; + try { + await Promise.all(ids.map((id) => ack.mutateAsync(id))); + setSelected(new Set()); + toast({ title: `Acknowledged ${ids.length} alert${ids.length === 1 ? '' : 's'}`, variant: 'success' }); + } catch (e) { + toast({ title: 'Bulk ack failed', description: String(e), variant: 'error' }); + } + }; + const onBulkRead = async (ids: string[]) => { if (ids.length === 0) return; try { @@ -122,6 +162,9 @@ export default function InboxPage() { if (error) return
Failed to load alerts: {String(error)}
; const selectedIds = Array.from(selected); + const selectedFiringIds = rows + .filter((r) => selected.has(r.id) && r.state === 'FIRING') + .map((r) => r.id); const subtitle = selectedIds.length > 0 @@ -136,25 +179,60 @@ export default function InboxPage() { {subtitle}
- - + ({ value, label: f.label }))} + active={severityKey} + onChange={setSeverityKey} + />
+
+ + + + + + +
+ {rows.length === 0 ? ( } diff --git a/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx b/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx index 6e8ca575..71f12450 100644 --- a/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx +++ b/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router'; -import { Alert, Button, SectionHeader, useToast } from '@cameleer/design-system'; +import { Alert, Button, useToast } from '@cameleer/design-system'; import { PageLoader } from '../../../components/PageLoader'; import { useAlertRule, @@ -148,7 +148,7 @@ export default function RuleEditorWizard() { return (
- {isEdit ? `Edit rule: ${form.name}` : 'New alert rule'} +

{isEdit ? `Edit rule: ${form.name}` : 'New alert rule'}

{promoteFrom && ( diff --git a/ui/src/pages/Alerts/RuleEditor/wizard.module.css b/ui/src/pages/Alerts/RuleEditor/wizard.module.css index 4aee7a9a..3d99608c 100644 --- a/ui/src/pages/Alerts/RuleEditor/wizard.module.css +++ b/ui/src/pages/Alerts/RuleEditor/wizard.module.css @@ -3,6 +3,17 @@ display: flex; flex-direction: column; gap: var(--space-md); + max-width: 840px; + margin: 0 auto; + width: 100%; +} + +.pageTitle { + font-size: 20px; + font-weight: 600; + color: var(--text-primary); + margin: 0; + letter-spacing: -0.01em; } .header {