feat(alerting): server-side state+severity filters, ButtonGroup filter UI
Backend: `GET /environments/{envSlug}/alerts` now accepts optional multi-value
`state=…` and `severity=…` query params. Filters are pushed down to
PostgresAlertInstanceRepository, which appends `AND state::text = ANY(?)` /
`AND severity::text = ANY(?)` to the inbox query (null/empty = no filter).
`AlertInstanceRepository.listForInbox` gained a 7-arg overload; the old 5-arg
form is preserved as a default delegate so existing callers (evaluator,
AlertingFullLifecycleIT, PostgresAlertInstanceRepositoryIT) compile unchanged.
`InAppInboxQuery.listInbox` also has a new filtered overload.
UI: InboxPage severity filter migrated from `SegmentedTabs` (single-select,
no color cues) to `ButtonGroup` (multi-select with severity-coloured dots),
matching the topnavbar status-filter pattern. `useAlerts` forwards the
filters as query params and cache-keys on the filter tuple so each combo
is independently cached.
Unit + hook tests updated to the new contract (5 UI tests + 8 Java unit
tests passing). OpenAPI types regenerated from the fresh local backend.
This commit is contained in:
@@ -8,6 +8,8 @@ import com.cameleer.server.app.web.EnvPath;
|
|||||||
import com.cameleer.server.core.alerting.AlertInstance;
|
import com.cameleer.server.core.alerting.AlertInstance;
|
||||||
import com.cameleer.server.core.alerting.AlertInstanceRepository;
|
import com.cameleer.server.core.alerting.AlertInstanceRepository;
|
||||||
import com.cameleer.server.core.alerting.AlertReadRepository;
|
import com.cameleer.server.core.alerting.AlertReadRepository;
|
||||||
|
import com.cameleer.server.core.alerting.AlertSeverity;
|
||||||
|
import com.cameleer.server.core.alerting.AlertState;
|
||||||
import com.cameleer.server.core.runtime.Environment;
|
import com.cameleer.server.core.runtime.Environment;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
@@ -54,10 +56,12 @@ public class AlertController {
|
|||||||
@GetMapping
|
@GetMapping
|
||||||
public List<AlertDto> list(
|
public List<AlertDto> list(
|
||||||
@EnvPath Environment env,
|
@EnvPath Environment env,
|
||||||
@RequestParam(defaultValue = "50") int limit) {
|
@RequestParam(defaultValue = "50") int limit,
|
||||||
|
@RequestParam(required = false) List<AlertState> state,
|
||||||
|
@RequestParam(required = false) List<AlertSeverity> severity) {
|
||||||
String userId = currentUserId();
|
String userId = currentUserId();
|
||||||
int effectiveLimit = Math.min(limit, 200);
|
int effectiveLimit = Math.min(limit, 200);
|
||||||
return inboxQuery.listInbox(env.id(), userId, effectiveLimit)
|
return inboxQuery.listInbox(env.id(), userId, state, severity, effectiveLimit)
|
||||||
.stream().map(AlertDto::from).toList();
|
.stream().map(AlertDto::from).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.cameleer.server.app.alerting.dto.UnreadCountResponse;
|
|||||||
import com.cameleer.server.core.alerting.AlertInstance;
|
import com.cameleer.server.core.alerting.AlertInstance;
|
||||||
import com.cameleer.server.core.alerting.AlertInstanceRepository;
|
import com.cameleer.server.core.alerting.AlertInstanceRepository;
|
||||||
import com.cameleer.server.core.alerting.AlertSeverity;
|
import com.cameleer.server.core.alerting.AlertSeverity;
|
||||||
|
import com.cameleer.server.core.alerting.AlertState;
|
||||||
import com.cameleer.server.core.rbac.RbacService;
|
import com.cameleer.server.core.rbac.RbacService;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -54,9 +55,22 @@ public class InAppInboxQuery {
|
|||||||
* or target a role the user holds. Empty target lists mean "broadcast to all".
|
* or target a role the user holds. Empty target lists mean "broadcast to all".
|
||||||
*/
|
*/
|
||||||
public List<AlertInstance> listInbox(UUID envId, String userId, int limit) {
|
public List<AlertInstance> listInbox(UUID envId, String userId, int limit) {
|
||||||
|
return listInbox(envId, userId, null, null, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filtered variant of {@link #listInbox(UUID, String, int)}: optional {@code states}
|
||||||
|
* and {@code severities} narrow the result set. {@code null} or empty lists mean
|
||||||
|
* "no filter on that dimension".
|
||||||
|
*/
|
||||||
|
public List<AlertInstance> listInbox(UUID envId,
|
||||||
|
String userId,
|
||||||
|
List<AlertState> states,
|
||||||
|
List<AlertSeverity> severities,
|
||||||
|
int limit) {
|
||||||
List<String> groupIds = resolveGroupIds(userId);
|
List<String> groupIds = resolveGroupIds(userId);
|
||||||
List<String> roleNames = resolveRoleNames(userId);
|
List<String> roleNames = resolveRoleNames(userId);
|
||||||
return instanceRepo.listForInbox(envId, groupIds, userId, roleNames, limit);
|
return instanceRepo.listForInbox(envId, groupIds, userId, roleNames, states, severities, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -98,12 +98,14 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository
|
|||||||
List<String> userGroupIdFilter,
|
List<String> userGroupIdFilter,
|
||||||
String userId,
|
String userId,
|
||||||
List<String> userRoleNames,
|
List<String> userRoleNames,
|
||||||
|
List<AlertState> states,
|
||||||
|
List<AlertSeverity> severities,
|
||||||
int limit) {
|
int limit) {
|
||||||
// Build arrays for group UUIDs and role names
|
// Build arrays for group UUIDs and role names
|
||||||
Array groupArray = toUuidArrayFromStrings(userGroupIdFilter);
|
Array groupArray = toUuidArrayFromStrings(userGroupIdFilter);
|
||||||
Array roleArray = toTextArray(userRoleNames);
|
Array roleArray = toTextArray(userRoleNames);
|
||||||
|
|
||||||
String sql = """
|
StringBuilder sql = new StringBuilder("""
|
||||||
SELECT * FROM alert_instances
|
SELECT * FROM alert_instances
|
||||||
WHERE environment_id = ?
|
WHERE environment_id = ?
|
||||||
AND (
|
AND (
|
||||||
@@ -111,10 +113,24 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository
|
|||||||
OR target_group_ids && ?
|
OR target_group_ids && ?
|
||||||
OR target_role_names && ?
|
OR target_role_names && ?
|
||||||
)
|
)
|
||||||
ORDER BY fired_at DESC
|
""");
|
||||||
LIMIT ?
|
List<Object> args = new ArrayList<>(List.of(environmentId, userId, groupArray, roleArray));
|
||||||
""";
|
|
||||||
return jdbc.query(sql, rowMapper(), environmentId, userId, groupArray, roleArray, limit);
|
if (states != null && !states.isEmpty()) {
|
||||||
|
Array stateArray = toTextArray(states.stream().map(Enum::name).toList());
|
||||||
|
sql.append(" AND state::text = ANY(?)");
|
||||||
|
args.add(stateArray);
|
||||||
|
}
|
||||||
|
if (severities != null && !severities.isEmpty()) {
|
||||||
|
Array severityArray = toTextArray(severities.stream().map(Enum::name).toList());
|
||||||
|
sql.append(" AND severity::text = ANY(?)");
|
||||||
|
args.add(severityArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
sql.append(" ORDER BY fired_at DESC LIMIT ?");
|
||||||
|
args.add(limit);
|
||||||
|
|
||||||
|
return jdbc.query(sql.toString(), rowMapper(), args.toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -75,13 +75,31 @@ class InAppInboxQueryTest {
|
|||||||
.thenReturn(List.of(new RoleSummary(roleId, "OPERATOR", true, "direct")));
|
.thenReturn(List.of(new RoleSummary(roleId, "OPERATOR", true, "direct")));
|
||||||
|
|
||||||
when(instanceRepo.listForInbox(eq(ENV_ID), eq(List.of(groupId.toString())),
|
when(instanceRepo.listForInbox(eq(ENV_ID), eq(List.of(groupId.toString())),
|
||||||
eq(USER_ID), eq(List.of("OPERATOR")), eq(20)))
|
eq(USER_ID), eq(List.of("OPERATOR")), isNull(), isNull(), eq(20)))
|
||||||
.thenReturn(List.of());
|
.thenReturn(List.of());
|
||||||
|
|
||||||
List<AlertInstance> result = query.listInbox(ENV_ID, USER_ID, 20);
|
List<AlertInstance> result = query.listInbox(ENV_ID, USER_ID, 20);
|
||||||
assertThat(result).isEmpty();
|
assertThat(result).isEmpty();
|
||||||
verify(instanceRepo).listForInbox(ENV_ID, List.of(groupId.toString()),
|
verify(instanceRepo).listForInbox(ENV_ID, List.of(groupId.toString()),
|
||||||
USER_ID, List.of("OPERATOR"), 20);
|
USER_ID, List.of("OPERATOR"), null, null, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listInbox_forwardsStateAndSeverityFilters() {
|
||||||
|
when(rbacService.getEffectiveGroupsForUser(USER_ID)).thenReturn(List.of());
|
||||||
|
when(rbacService.getEffectiveRolesForUser(USER_ID)).thenReturn(List.of());
|
||||||
|
|
||||||
|
List<com.cameleer.server.core.alerting.AlertState> states =
|
||||||
|
List.of(com.cameleer.server.core.alerting.AlertState.FIRING);
|
||||||
|
List<AlertSeverity> severities = List.of(AlertSeverity.CRITICAL, AlertSeverity.WARNING);
|
||||||
|
|
||||||
|
when(instanceRepo.listForInbox(eq(ENV_ID), eq(List.of()), eq(USER_ID), eq(List.of()),
|
||||||
|
eq(states), eq(severities), eq(25)))
|
||||||
|
.thenReturn(List.of());
|
||||||
|
|
||||||
|
query.listInbox(ENV_ID, USER_ID, states, severities, 25);
|
||||||
|
verify(instanceRepo).listForInbox(ENV_ID, List.of(), USER_ID, List.of(),
|
||||||
|
states, severities, 25);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -10,10 +10,30 @@ public interface AlertInstanceRepository {
|
|||||||
AlertInstance save(AlertInstance instance); // upsert by id
|
AlertInstance save(AlertInstance instance); // upsert by id
|
||||||
Optional<AlertInstance> findById(UUID id);
|
Optional<AlertInstance> findById(UUID id);
|
||||||
Optional<AlertInstance> findOpenForRule(UUID ruleId); // state IN ('PENDING','FIRING','ACKNOWLEDGED')
|
Optional<AlertInstance> findOpenForRule(UUID ruleId); // state IN ('PENDING','FIRING','ACKNOWLEDGED')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unfiltered inbox listing. Convenience overload that delegates to the filtered
|
||||||
|
* variant with {@code states}/{@code severities} set to {@code null} (no filter).
|
||||||
|
*/
|
||||||
|
default List<AlertInstance> listForInbox(UUID environmentId,
|
||||||
|
List<String> userGroupIdFilter,
|
||||||
|
String userId,
|
||||||
|
List<String> userRoleNames,
|
||||||
|
int limit) {
|
||||||
|
return listForInbox(environmentId, userGroupIdFilter, userId, userRoleNames, null, null, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inbox listing with optional state + severity filters. {@code null} or empty lists mean
|
||||||
|
* "no filter on that field". When both lists are non-empty the row must match at least one
|
||||||
|
* value from each list (AND between dimensions, OR within).
|
||||||
|
*/
|
||||||
List<AlertInstance> listForInbox(UUID environmentId,
|
List<AlertInstance> listForInbox(UUID environmentId,
|
||||||
List<String> userGroupIdFilter,
|
List<String> userGroupIdFilter,
|
||||||
String userId,
|
String userId,
|
||||||
List<String> userRoleNames,
|
List<String> userRoleNames,
|
||||||
|
List<AlertState> states,
|
||||||
|
List<AlertSeverity> severities,
|
||||||
int limit);
|
int limit);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -22,16 +22,32 @@ describe('useAlerts', () => {
|
|||||||
useEnvironmentStore.setState({ environment: 'dev' });
|
useEnvironmentStore.setState({ environment: 'dev' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetches up to 200 alerts for selected env (no server-side filter params)', async () => {
|
it('forwards state + severity filters to the server as query 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 });
|
(apiClient.GET as any).mockResolvedValue({ data: [], error: null });
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => useAlerts({ state: 'FIRING', severity: ['CRITICAL', 'WARNING'] }),
|
() => useAlerts({ state: 'FIRING', severity: ['CRITICAL', 'WARNING'] }),
|
||||||
{ wrapper },
|
{ wrapper },
|
||||||
);
|
);
|
||||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||||
|
expect(apiClient.GET).toHaveBeenCalledWith(
|
||||||
|
'/environments/{envSlug}/alerts',
|
||||||
|
expect.objectContaining({
|
||||||
|
params: expect.objectContaining({
|
||||||
|
path: { envSlug: 'dev' },
|
||||||
|
query: {
|
||||||
|
limit: 200,
|
||||||
|
state: ['FIRING'],
|
||||||
|
severity: ['CRITICAL', 'WARNING'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('omits state + severity when no filter is set', async () => {
|
||||||
|
(apiClient.GET as any).mockResolvedValue({ data: [], error: null });
|
||||||
|
const { result } = renderHook(() => useAlerts(), { wrapper });
|
||||||
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||||
expect(apiClient.GET).toHaveBeenCalledWith(
|
expect(apiClient.GET).toHaveBeenCalledWith(
|
||||||
'/environments/{envSlug}/alerts',
|
'/environments/{envSlug}/alerts',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -43,21 +59,19 @@ describe('useAlerts', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies state + severity filters client-side via select', async () => {
|
it('still applies ruleId client-side via select', async () => {
|
||||||
const dataset = [
|
const dataset = [
|
||||||
{ id: '1', state: 'FIRING', severity: 'CRITICAL', title: 'a' },
|
{ id: '1', ruleId: 'R1', state: 'FIRING', severity: 'WARNING', title: 'a' },
|
||||||
{ id: '2', state: 'FIRING', severity: 'WARNING', title: 'b' },
|
{ id: '2', ruleId: 'R2', 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 });
|
(apiClient.GET as any).mockResolvedValue({ data: dataset, error: null });
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => useAlerts({ state: ['FIRING'], severity: ['CRITICAL', 'WARNING'] }),
|
() => useAlerts({ ruleId: 'R2' }),
|
||||||
{ wrapper },
|
{ wrapper },
|
||||||
);
|
);
|
||||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||||
const ids = (result.current.data ?? []).map((a: any) => a.id);
|
const ids = (result.current.data ?? []).map((a: any) => a.id);
|
||||||
expect(ids).toEqual(['1', '2']);
|
expect(ids).toEqual(['2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not fetch when no env is selected', () => {
|
it('does not fetch when no env is selected', () => {
|
||||||
|
|||||||
@@ -30,51 +30,45 @@ function toArray<T>(v: T | T[] | undefined): T[] | undefined {
|
|||||||
|
|
||||||
/** 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` /
|
* State + severity filters are server-side (`state=FIRING&state=ACKNOWLEDGED&severity=CRITICAL`).
|
||||||
* `severity` query params are dropped. We fetch up to 200 alerts once per
|
* `ruleId` is not a backend param and is still applied via react-query `select`.
|
||||||
* env (cached under a stable key) and apply filters client-side via
|
* Each unique (state, severity) combo gets its own cache entry so the server
|
||||||
* react-query's `select` so filter switches on the All / History / Inbox
|
* honors the filter and stays as the source of truth.
|
||||||
* 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 = {}) {
|
export function useAlerts(filter: AlertsFilter = {}) {
|
||||||
const env = useSelectedEnv();
|
const env = useSelectedEnv();
|
||||||
const fetchLimit = 200;
|
const fetchLimit = Math.min(filter.limit ?? 200, 200);
|
||||||
const stateSet = filter.state === undefined
|
const stateArr = toArray(filter.state);
|
||||||
? undefined
|
const severityArr = toArray(filter.severity);
|
||||||
: new Set(toArray(filter.state));
|
|
||||||
const severitySet = filter.severity === undefined
|
|
||||||
? undefined
|
|
||||||
: new Set(toArray(filter.severity));
|
|
||||||
const applyLimit = filter.limit;
|
|
||||||
const ruleIdFilter = filter.ruleId;
|
const ruleIdFilter = filter.ruleId;
|
||||||
|
|
||||||
|
// Stable, serialisable key — arrays must be sorted so order doesn't create cache misses.
|
||||||
|
const stateKey = stateArr ? [...stateArr].sort() : null;
|
||||||
|
const severityKey = severityArr ? [...severityArr].sort() : null;
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['alerts', env, 'list', fetchLimit],
|
queryKey: ['alerts', env, 'list', fetchLimit, stateKey, severityKey],
|
||||||
enabled: !!env,
|
enabled: !!env,
|
||||||
refetchInterval: 30_000,
|
refetchInterval: 30_000,
|
||||||
refetchIntervalInBackground: false,
|
refetchIntervalInBackground: false,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!env) throw new Error('no env');
|
if (!env) throw new Error('no env');
|
||||||
|
const query: Record<string, unknown> = { limit: fetchLimit };
|
||||||
|
if (stateArr && stateArr.length > 0) query.state = stateArr;
|
||||||
|
if (severityArr && severityArr.length > 0) query.severity = severityArr;
|
||||||
const { data, error } = await apiClient.GET(
|
const { data, error } = await apiClient.GET(
|
||||||
'/environments/{envSlug}/alerts',
|
'/environments/{envSlug}/alerts',
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
path: { envSlug: env },
|
path: { envSlug: env },
|
||||||
query: { limit: fetchLimit },
|
query,
|
||||||
},
|
},
|
||||||
} as any,
|
} as any,
|
||||||
);
|
);
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
return data as AlertDto[];
|
return data as AlertDto[];
|
||||||
},
|
},
|
||||||
select: (all) => {
|
select: (all) => ruleIdFilter ? all.filter((a) => a.ruleId === ruleIdFilter) : 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;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
ui/src/api/schema.d.ts
vendored
2
ui/src/api/schema.d.ts
vendored
@@ -7213,6 +7213,8 @@ export interface operations {
|
|||||||
query: {
|
query: {
|
||||||
env: components["schemas"]["Environment"];
|
env: components["schemas"]["Environment"];
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
state?: ("PENDING" | "FIRING" | "ACKNOWLEDGED" | "RESOLVED")[];
|
||||||
|
severity?: ("CRITICAL" | "WARNING" | "INFO")[];
|
||||||
};
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { useMemo, useState } from 'react';
|
|||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { Inbox } from 'lucide-react';
|
import { Inbox } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Button, DataTable, EmptyState, SegmentedTabs, useToast,
|
Button, ButtonGroup, DataTable, EmptyState, useToast,
|
||||||
} from '@cameleer/design-system';
|
} from '@cameleer/design-system';
|
||||||
import type { Column } from '@cameleer/design-system';
|
import type { ButtonGroupItem, Column } from '@cameleer/design-system';
|
||||||
import { PageLoader } from '../../components/PageLoader';
|
import { PageLoader } from '../../components/PageLoader';
|
||||||
import { SeverityBadge } from '../../components/SeverityBadge';
|
import { SeverityBadge } from '../../components/SeverityBadge';
|
||||||
import { AlertStateChip } from '../../components/AlertStateChip';
|
import { AlertStateChip } from '../../components/AlertStateChip';
|
||||||
@@ -20,20 +20,21 @@ import tableStyles from '../../styles/table-section.module.css';
|
|||||||
|
|
||||||
type Severity = NonNullable<AlertDto['severity']>;
|
type Severity = NonNullable<AlertDto['severity']>;
|
||||||
|
|
||||||
const SEVERITY_FILTERS: Record<string, { label: string; values: Severity[] | undefined }> = {
|
const SEVERITY_ITEMS: ButtonGroupItem[] = [
|
||||||
all: { label: 'All severities', values: undefined },
|
{ value: 'CRITICAL', label: 'Critical', color: 'var(--error)' },
|
||||||
critical: { label: 'Critical', values: ['CRITICAL'] },
|
{ value: 'WARNING', label: 'Warning', color: 'var(--warning)' },
|
||||||
warning: { label: 'Warning', values: ['WARNING'] },
|
{ value: 'INFO', label: 'Info', color: 'var(--text-muted)' },
|
||||||
info: { label: 'Info', values: ['INFO'] },
|
];
|
||||||
};
|
|
||||||
|
|
||||||
export default function InboxPage() {
|
export default function InboxPage() {
|
||||||
const [severityKey, setSeverityKey] = useState<string>('all');
|
const [severitySel, setSeveritySel] = useState<Set<string>>(new Set());
|
||||||
const severityFilter = SEVERITY_FILTERS[severityKey];
|
const severityValues: Severity[] | undefined = severitySel.size === 0
|
||||||
|
? undefined
|
||||||
|
: [...severitySel] as Severity[];
|
||||||
|
|
||||||
const { data, isLoading, error } = useAlerts({
|
const { data, isLoading, error } = useAlerts({
|
||||||
state: ['FIRING', 'ACKNOWLEDGED'],
|
state: ['FIRING', 'ACKNOWLEDGED'],
|
||||||
severity: severityFilter.values,
|
severity: severityValues,
|
||||||
limit: 200,
|
limit: 200,
|
||||||
});
|
});
|
||||||
const bulkRead = useBulkReadAlerts();
|
const bulkRead = useBulkReadAlerts();
|
||||||
@@ -179,10 +180,10 @@ export default function InboxPage() {
|
|||||||
<span className={css.pageSubtitle}>{subtitle}</span>
|
<span className={css.pageSubtitle}>{subtitle}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={css.pageActions}>
|
<div className={css.pageActions}>
|
||||||
<SegmentedTabs
|
<ButtonGroup
|
||||||
tabs={Object.entries(SEVERITY_FILTERS).map(([value, f]) => ({ value, label: f.label }))}
|
items={SEVERITY_ITEMS}
|
||||||
active={severityKey}
|
value={severitySel}
|
||||||
onChange={setSeverityKey}
|
onChange={setSeveritySel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
Reference in New Issue
Block a user