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:
@@ -22,7 +22,10 @@ describe('useAlerts', () => {
|
||||
useEnvironmentStore.setState({ environment: 'dev' });
|
||||
});
|
||||
|
||||
it('fetches alerts for selected env and passes filter query params', async () => {
|
||||
it('fetches up to 200 alerts for selected env (no server-side filter params)', async () => {
|
||||
// Backend AlertController.list accepts only `limit`; state/severity are
|
||||
// dropped server-side. We therefore fetch once per env and filter
|
||||
// client-side via react-query `select`.
|
||||
(apiClient.GET as any).mockResolvedValue({ data: [], error: null });
|
||||
const { result } = renderHook(
|
||||
() => useAlerts({ state: 'FIRING', severity: ['CRITICAL', 'WARNING'] }),
|
||||
@@ -34,16 +37,29 @@ describe('useAlerts', () => {
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
path: { envSlug: 'dev' },
|
||||
query: expect.objectContaining({
|
||||
state: ['FIRING'],
|
||||
severity: ['CRITICAL', 'WARNING'],
|
||||
limit: 100,
|
||||
}),
|
||||
query: { limit: 200 },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('applies state + severity filters client-side via select', async () => {
|
||||
const dataset = [
|
||||
{ id: '1', state: 'FIRING', severity: 'CRITICAL', title: 'a' },
|
||||
{ id: '2', state: 'FIRING', severity: 'WARNING', title: 'b' },
|
||||
{ id: '3', state: 'ACKNOWLEDGED', severity: 'CRITICAL', title: 'c' },
|
||||
{ id: '4', state: 'RESOLVED', severity: 'INFO', title: 'd' },
|
||||
];
|
||||
(apiClient.GET as any).mockResolvedValue({ data: dataset, error: null });
|
||||
const { result } = renderHook(
|
||||
() => useAlerts({ state: ['FIRING'], severity: ['CRITICAL', 'WARNING'] }),
|
||||
{ wrapper },
|
||||
);
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
const ids = (result.current.data ?? []).map((a: any) => a.id);
|
||||
expect(ids).toEqual(['1', '2']);
|
||||
});
|
||||
|
||||
it('does not fetch when no env is selected', () => {
|
||||
useEnvironmentStore.setState({ environment: undefined });
|
||||
const { result } = renderHook(() => useAlerts(), { wrapper });
|
||||
|
||||
@@ -28,11 +28,28 @@ function toArray<T>(v: T | T[] | undefined): T[] | undefined {
|
||||
// openapi-fetch regardless of what the TS types say; we therefore cast the
|
||||
// call options to `any` to bypass the generated type oddity.
|
||||
|
||||
/** List alert instances in the current env. Polls every 30s (pauses in background). */
|
||||
/** List alert instances in the current env. Polls every 30s (pauses in background).
|
||||
*
|
||||
* The backend's `AlertController.list` accepts only `limit` — `state` /
|
||||
* `severity` query params are dropped. We fetch up to 200 alerts once per
|
||||
* env (cached under a stable key) and apply filters client-side via
|
||||
* react-query's `select` so filter switches on the All / History / Inbox
|
||||
* pages are instant and don't each fire their own request. True server-side
|
||||
* filtering needs a backend change (follow-up).
|
||||
*/
|
||||
export function useAlerts(filter: AlertsFilter = {}) {
|
||||
const env = useSelectedEnv();
|
||||
const fetchLimit = 200;
|
||||
const stateSet = filter.state === undefined
|
||||
? undefined
|
||||
: new Set(toArray(filter.state));
|
||||
const severitySet = filter.severity === undefined
|
||||
? undefined
|
||||
: new Set(toArray(filter.severity));
|
||||
const applyLimit = filter.limit;
|
||||
const ruleIdFilter = filter.ruleId;
|
||||
return useQuery({
|
||||
queryKey: ['alerts', env, filter],
|
||||
queryKey: ['alerts', env, 'list', fetchLimit],
|
||||
enabled: !!env,
|
||||
refetchInterval: 30_000,
|
||||
refetchIntervalInBackground: false,
|
||||
@@ -43,18 +60,21 @@ export function useAlerts(filter: AlertsFilter = {}) {
|
||||
{
|
||||
params: {
|
||||
path: { envSlug: env },
|
||||
query: {
|
||||
state: toArray(filter.state),
|
||||
severity: toArray(filter.severity),
|
||||
ruleId: filter.ruleId,
|
||||
limit: filter.limit ?? 100,
|
||||
},
|
||||
query: { limit: fetchLimit },
|
||||
},
|
||||
} as any,
|
||||
);
|
||||
if (error) throw error;
|
||||
return data as AlertDto[];
|
||||
},
|
||||
select: (all) => {
|
||||
let out = all;
|
||||
if (stateSet) out = out.filter((a) => a.state && stateSet.has(a.state));
|
||||
if (severitySet) out = out.filter((a) => a.severity && severitySet.has(a.severity));
|
||||
if (ruleIdFilter) out = out.filter((a) => a.ruleId === ruleIdFilter);
|
||||
if (applyLimit !== undefined) out = out.slice(0, applyLimit);
|
||||
return out;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user