fix(alerts/ui): bell position, content tabs hidden, filters, novice labels
Surfaced during second smoke:
1. Notification bell moved — was first child of TopBar (left of
breadcrumb); now rendered inside the `environment` slot so it
sits between the env selector and the user menu, matching user
expectations.
2. Content tabs (Exchanges/Dashboard/Runtime/Deployments) hidden on
`/alerts/*` — the operational tabs don't apply there.
3. Inbox / All alerts filters now actually filter. `AlertController.list`
accepts only `limit` — `state`/`severity` query params are dropped
server-side. Move `useAlerts` to fetch once per env (limit 200) and
apply filters client-side via react-query `select`, with a stable
queryKey so filter toggles are instant and don't re-request. True
server-side filter needs a backend change (follow-up).
4. Novice-friendly labels:
- Inbox subtitle: "99 firing · 100 total" → "99 need attention ·
100 total in inbox"
- All alerts filter: Open/Firing/Acked/All →
"Currently open"/"Firing now"/"Acknowledged"/"All states"
- All alerts subtitle: "N shown" → "N matching your filter"
- History subtitle: "N resolved" → "N resolved alert(s) in range"
- Rules subtitle: "N total" → "N rule(s) configured"
- Silences subtitle: "N active" → "N active silence(s)" or
"Nothing silenced right now"
- Column headers: "State" → "Status", rules "Kind" → "Type",
rules "Targets" → "Notifies"
- Buttons: "Ack" → "Acknowledge", silence "End" → "End early"
Updated alerts.test.tsx and e2e selector to match new behavior/labels.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,10 +21,10 @@ import tableStyles from '../../styles/table-section.module.css';
|
||||
type AlertState = NonNullable<AlertDto['state']>;
|
||||
|
||||
const STATE_FILTERS: Record<string, { label: string; values: AlertState[] }> = {
|
||||
open: { label: 'Open', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED'] },
|
||||
firing: { label: 'Firing', values: ['FIRING'] },
|
||||
acked: { label: 'Acked', values: ['ACKNOWLEDGED'] },
|
||||
all: { label: 'All', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED', 'RESOLVED'] },
|
||||
open: { label: 'Currently open', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED'] },
|
||||
firing: { label: 'Firing now', values: ['FIRING'] },
|
||||
acked: { label: 'Acknowledged', values: ['ACKNOWLEDGED'] },
|
||||
all: { label: 'All states', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED', 'RESOLVED'] },
|
||||
};
|
||||
|
||||
export default function AllAlertsPage() {
|
||||
@@ -41,7 +41,7 @@ export default function AllAlertsPage() {
|
||||
render: (_, row) => row.severity ? <SeverityBadge severity={row.severity} /> : null,
|
||||
},
|
||||
{
|
||||
key: 'state', header: 'State', width: '140px',
|
||||
key: 'state', header: 'Status', width: '140px',
|
||||
render: (_, row) => row.state ? <AlertStateChip state={row.state} silenced={row.silenced} /> : null,
|
||||
},
|
||||
{
|
||||
@@ -74,7 +74,7 @@ export default function AllAlertsPage() {
|
||||
<header className={css.pageHeader}>
|
||||
<div className={css.pageTitleGroup}>
|
||||
<h2 className={css.pageTitle}>All alerts</h2>
|
||||
<span className={css.pageSubtitle}>{rows.length} shown</span>
|
||||
<span className={css.pageSubtitle}>{rows.length} matching your filter</span>
|
||||
</div>
|
||||
<div className={css.pageActions}>
|
||||
<SegmentedTabs
|
||||
|
||||
@@ -88,8 +88,12 @@ export default function HistoryPage() {
|
||||
<div className={css.page}>
|
||||
<header className={css.pageHeader}>
|
||||
<div className={css.pageTitleGroup}>
|
||||
<h2 className={css.pageTitle}>History</h2>
|
||||
<span className={css.pageSubtitle}>{filtered.length} resolved</span>
|
||||
<h2 className={css.pageTitle}>Alert history</h2>
|
||||
<span className={css.pageSubtitle}>
|
||||
{filtered.length === 0
|
||||
? 'No resolved alerts in range'
|
||||
: `${filtered.length} resolved alert${filtered.length === 1 ? '' : 's'} in range`}
|
||||
</span>
|
||||
</div>
|
||||
<div className={css.pageActions}>
|
||||
<DateRangePicker value={dateRange} onChange={setDateRange} />
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function InboxPage() {
|
||||
row.severity ? <SeverityBadge severity={row.severity} /> : null,
|
||||
},
|
||||
{
|
||||
key: 'state', header: 'State', width: '140px',
|
||||
key: 'state', header: 'Status', width: '140px',
|
||||
render: (_, row) =>
|
||||
row.state ? <AlertStateChip state={row.state} silenced={row.silenced} /> : null,
|
||||
},
|
||||
@@ -108,11 +108,11 @@ export default function InboxPage() {
|
||||
) : '—',
|
||||
},
|
||||
{
|
||||
key: 'ack', header: '', width: '70px',
|
||||
key: 'ack', header: '', width: '120px',
|
||||
render: (_, row) =>
|
||||
row.state === 'FIRING' ? (
|
||||
<Button size="sm" variant="secondary" onClick={() => onAck(row.id, row.title ?? undefined)}>
|
||||
Ack
|
||||
Acknowledge
|
||||
</Button>
|
||||
) : null,
|
||||
},
|
||||
@@ -126,7 +126,7 @@ export default function InboxPage() {
|
||||
const subtitle =
|
||||
selectedIds.length > 0
|
||||
? `${selectedIds.length} selected`
|
||||
: `${unreadIds.length} firing · ${rows.length} total`;
|
||||
: `${unreadIds.length} need attention · ${rows.length} total in inbox`;
|
||||
|
||||
return (
|
||||
<div className={css.page}>
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function RulesListPage() {
|
||||
render: (_, r) => <Link to={`/alerts/rules/${r.id}`}>{r.name}</Link>,
|
||||
},
|
||||
{
|
||||
key: 'conditionKind', header: 'Kind', width: '160px',
|
||||
key: 'conditionKind', header: 'Type', width: '160px',
|
||||
render: (_, r) => <Badge label={r.conditionKind ?? ''} color="auto" variant="outlined" />,
|
||||
},
|
||||
{
|
||||
@@ -85,7 +85,7 @@ export default function RulesListPage() {
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'targets', header: 'Targets', width: '90px',
|
||||
key: 'targets', header: 'Notifies', width: '90px',
|
||||
render: (_, r) => String(r.targets?.length ?? 0),
|
||||
},
|
||||
{
|
||||
@@ -114,7 +114,9 @@ export default function RulesListPage() {
|
||||
<header className={css.pageHeader}>
|
||||
<div className={css.pageTitleGroup}>
|
||||
<h2 className={css.pageTitle}>Alert rules</h2>
|
||||
<span className={css.pageSubtitle}>{rows.length} total</span>
|
||||
<span className={css.pageSubtitle}>
|
||||
{rows.length === 0 ? 'No rules yet' : `${rows.length} rule${rows.length === 1 ? '' : 's'} configured`}
|
||||
</span>
|
||||
</div>
|
||||
<div className={css.pageActions}>
|
||||
<Link to="/alerts/rules/new">
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function SilencesPage() {
|
||||
key: 'actions', header: '', width: '90px',
|
||||
render: (_, s) => (
|
||||
<Button variant="ghost" size="sm" onClick={() => setPendingEnd(s)}>
|
||||
End
|
||||
End early
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
@@ -95,7 +95,11 @@ export default function SilencesPage() {
|
||||
<header className={css.pageHeader}>
|
||||
<div className={css.pageTitleGroup}>
|
||||
<h2 className={css.pageTitle}>Alert silences</h2>
|
||||
<span className={css.pageSubtitle}>{rows.length} active</span>
|
||||
<span className={css.pageSubtitle}>
|
||||
{rows.length === 0
|
||||
? 'Nothing silenced right now'
|
||||
: `${rows.length} active silence${rows.length === 1 ? '' : 's'}`}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user