Skip to content

Commit 2be2f71

Browse files
[SigEvents][Discovery] Add severity filter to Significant Events tab (#278891)
closes elastic/nightshift-program#662 ## Summary Adds a **Severity** filter to the Significant Events tab in Discovery so users can narrow the list to the severities they care about. **Critical** and **High** are selected by default; clearing all checkboxes returns every severity (same behavior as the Status filter). Server-side filtering applies on the **latest** document per event (post-`pickLatestPerGroup`, alongside status). The UI reuses the existing `FilterPopover` pattern and wires severity through `useFetchSignificantEvents`. <img width="1479" height="499" alt="image" src="https://github.com/user-attachments/assets/072236b2-5ce5-42af-836a-41a1b7c94a9f" /> <img width="1485" height="541" alt="image" src="https://github.com/user-attachments/assets/7ab88c64-1ece-48c3-b986-e62335227d1a" /> ### How to test 1. Open **Streams → Discovery → Significant Events** and confirm a **Severity** filter appears next to Status and Stream. 2. On load, only **Critical** and **High** events should appear; the network request should include `severity=80-critical&severity=60-high` (or equivalent array form). 3. Change severity selection and confirm the table refetches and pagination resets to page 1. 4. Clear all severity checkboxes and confirm all severities are returned (no `severity` param sent). 5. Confirm Status, Stream, search, time range, pagination, and flyout deeplink behavior are unchanged. --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6073296 commit 2be2f71

9 files changed

Lines changed: 145 additions & 39 deletions

File tree

x-pack/platform/packages/shared/kbn-significant-events-schema/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export {
100100
type SignalEntry,
101101
type Severity,
102102
severitySchema,
103+
SEVERITY_OPTIONS,
103104
getSeverityLabel,
104105
detectionSchema,
105106
discoverySchema,

x-pack/platform/packages/shared/kbn-significant-events-schema/src/significant_events/common_schemas.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,11 @@ const detectionSignalSchema = signalBaseSchema.extend({
171171
export const signalEntrySchema = z.discriminatedUnion('type', [detectionSignalSchema]);
172172
export type SignalEntry = z.infer<typeof signalEntrySchema>;
173173

174+
/** Canonical severity values in descending severity order (critical → low). */
175+
export const SEVERITY_OPTIONS = ['80-critical', '60-high', '40-medium', '20-low'] as const;
176+
174177
/** Canonical sortable severity used by storage, APIs, and tools. */
175-
export const severitySchema = z.enum(['20-low', '40-medium', '60-high', '80-critical'])
176-
.describe(dedent`
178+
export const severitySchema = z.enum(SEVERITY_OPTIONS).describe(dedent`
177179
Sortable severity keyword. Higher prefixes indicate greater severity:
178180
"80-critical" = the most severe outage. Any ONE qualifies independently:
179181
- a site-wide/global outage affecting most customers;
@@ -190,16 +192,16 @@ export type Severity = z.infer<typeof severitySchema>;
190192

191193
const SEVERITY_LABELS: Record<Severity, string> = {
192194
'20-low': i18n.translate('xpack.significantEvents.severity.lowLabel', {
193-
defaultMessage: 'low',
195+
defaultMessage: 'Low',
194196
}),
195197
'40-medium': i18n.translate('xpack.significantEvents.severity.mediumLabel', {
196-
defaultMessage: 'medium',
198+
defaultMessage: 'Medium',
197199
}),
198200
'60-high': i18n.translate('xpack.significantEvents.severity.highLabel', {
199-
defaultMessage: 'high',
201+
defaultMessage: 'High',
200202
}),
201203
'80-critical': i18n.translate('xpack.significantEvents.severity.criticalLabel', {
202-
defaultMessage: 'critical',
204+
defaultMessage: 'Critical',
203205
}),
204206
};
205207

x-pack/platform/packages/shared/kbn-significant-events-schema/src/significant_events/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export {
1919
causalFeatureSchema,
2020
signalEntrySchema,
2121
severitySchema,
22+
SEVERITY_OPTIONS,
2223
getSeverityLabel,
2324
type BlastRadiusEntry,
2425
type CausalFeature,

x-pack/platform/plugins/shared/significant_events/server/lib/significant_events/events/event_client.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,24 @@ describe('EventClient', () => {
151151
expect(result.hits[0].status).toBe('closed');
152152
expect(result.total).toBe(1);
153153
});
154+
155+
it('filters severity after latest-per-slug reduction', async () => {
156+
const { client, query } = createSearchClient({
157+
hits: [],
158+
total: 0,
159+
});
160+
161+
await client.findLatestByCurrentStatePaginated({
162+
severity: ['80-critical', '60-high'],
163+
});
164+
165+
const dataQuery = query.mock.calls
166+
.map((call) => (call[0] as { query: string }).query)
167+
.find((q) => !q.includes('STATS total'));
168+
expect(dataQuery).toContain('severity IN');
169+
expect(dataQuery?.indexOf('INLINE STATS latest_ts')).toBeLessThan(
170+
dataQuery!.indexOf('severity IN')
171+
);
172+
});
154173
});
155174
});

x-pack/platform/plugins/shared/significant_events/server/lib/significant_events/events/event_client.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { IDataStreamClient } from '@kbn/data-streams';
99
import { esql, type ComposerSortShorthand } from '@elastic/esql';
1010
import type { ESQLAstExpression } from '@elastic/esql/types';
1111
import type { ElasticsearchClient } from '@kbn/core/server';
12-
import type { SignificantEvent } from '@kbn/significant-events-schema';
12+
import type { SignificantEvent, Severity } from '@kbn/significant-events-schema';
1313
import {
1414
type BulkCreateOptions,
1515
type CommonSearchOptions,
@@ -43,6 +43,7 @@ export type EventDataStreamClient = IDataStreamClient<typeof eventsMappings, Sto
4343

4444
export interface EventsFilterOptions {
4545
status?: SignificantEvent['status'][];
46+
severity?: Severity[];
4647
stream?: string[];
4748
search?: string;
4849
}
@@ -130,6 +131,10 @@ export class EventClient {
130131
? esql.exp`${esql.col('status')} IN (${options.status.map((s) => esql.str(s))})`
131132
: undefined;
132133

134+
const severityWhere = options.severity
135+
? esql.exp`${esql.col('severity')} IN (${options.severity.map((s) => esql.str(s))})`
136+
: undefined;
137+
133138
// ComposerQuery is mutable — each chaining call mutates the same object and returns `this`.
134139
// Build the base query twice via a factory so the data branch and count branch get independent
135140
// instances; sharing a single reference causes the count pipeline to corrupt the data query.
@@ -143,10 +148,11 @@ export class EventClient {
143148
from: options.from,
144149
to: options.to,
145150
});
146-
// stream + search filters run pre-latest; state filter runs post-latest
151+
// stream + search filters run pre-latest; status + severity filters run post-latest
147152
q = withWhere(q, this.buildWhere({ stream: options.stream, search: options.search }));
148153
q = pickLatestPerGroup(q, FIELD_EVENT_ID);
149154
q = withWhere(q, statusWhere);
155+
q = withWhere(q, severityWhere);
150156
return q;
151157
};
152158

x-pack/platform/plugins/shared/significant_events/server/routes/internal/events/route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
significantEventSchema,
1010
significantEventInvestigationSchema,
1111
significantEventStatusSchema,
12+
severitySchema,
1213
type Detection,
1314
type SignificantEvent,
1415
type Discovery,
@@ -79,6 +80,7 @@ const eventsSearchRoute = createServerRoute({
7980
.optional(),
8081
stream: z.union([z.string().max(255), z.array(z.string().max(255)).max(50)]).optional(),
8182
search: z.string().max(500).optional(),
83+
severity: z.union([severitySchema, z.array(severitySchema).max(4)]).optional(),
8284
}),
8385
}),
8486
handler: async ({
@@ -91,12 +93,13 @@ const eventsSearchRoute = createServerRoute({
9193

9294
await assertSignificantEventsAccess({ server, licensing });
9395

94-
const { status, stream, search, ...rest } = params.query;
96+
const { status, stream, search, severity, ...rest } = params.query;
9597

9698
return getEventClient().findLatestByCurrentStatePaginated({
9799
...rest,
98100
status: toArray(status),
99101
stream: toArray(stream),
102+
severity: toArray(severity),
100103
search: search || undefined,
101104
});
102105
},

x-pack/platform/plugins/shared/streams_app/public/components/significant_events/significant_events_discovery/components/knowledge_indicators_table/use_knowledge_indicators_url_state.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* 2.0.
66
*/
77

8+
import { castArray } from 'lodash';
89
import { useDebouncedValue } from '@kbn/react-hooks';
910
import { COMPUTED_FEATURE_TYPES } from '@kbn/significant-events-schema';
1011
import type { KnowledgeIndicator } from '@kbn/streams-ai';
@@ -21,9 +22,6 @@ import { getKnowledgeIndicatorType } from '../../../stream_detail_significant_ev
2122
const SEARCH_DEBOUNCE_MS = 300;
2223
const COMPUTED_FEATURE_TYPES_SET = new Set<string>(COMPUTED_FEATURE_TYPES);
2324

24-
const toArray = (v: string | string[] | undefined): string[] =>
25-
v == null ? [] : Array.isArray(v) ? v : [v];
26-
2725
interface UseKnowledgeIndicatorsUrlStateParams {
2826
knowledgeIndicators: KnowledgeIndicator[];
2927
isLoading: boolean;
@@ -53,10 +51,16 @@ export function useKnowledgeIndicatorsUrlState({
5351
const [statusFilter, setStatusFilter] = useState<'active' | 'excluded'>(() =>
5452
query?.status === 'excluded' ? 'excluded' : 'active'
5553
);
56-
const [selectedTypes, setSelectedTypes] = useState<string[]>(() => toArray(query?.type));
57-
const [selectedSubtypes, setSelectedSubtypes] = useState<string[]>(() => toArray(query?.subtype));
58-
const [selectedStreams, setSelectedStreams] = useState<string[]>(() => toArray(query?.stream));
59-
const initialUrlStreamsRef = useRef<string[]>(toArray(query?.stream));
54+
const [selectedTypes, setSelectedTypes] = useState<string[]>(() =>
55+
query?.type ? castArray(query.type) : []
56+
);
57+
const [selectedSubtypes, setSelectedSubtypes] = useState<string[]>(() =>
58+
query?.subtype ? castArray(query.subtype) : []
59+
);
60+
const [selectedStreams, setSelectedStreams] = useState<string[]>(() =>
61+
query?.stream ? castArray(query.stream) : []
62+
);
63+
const initialUrlStreamsRef = useRef<string[]>(query?.stream ? castArray(query.stream) : []);
6064
const [hideComputedTypes, setHideComputedTypes] = useState(() =>
6165
query?.showComputed === 'true' ? false : true
6266
);

x-pack/platform/plugins/shared/streams_app/public/components/significant_events/significant_events_discovery/components/significant_events_tab/index.tsx

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,17 @@ import { css } from '@emotion/react';
2323
import { capitalize } from 'lodash';
2424
import useInterval from 'react-use/lib/useInterval';
2525
import { i18n } from '@kbn/i18n';
26-
import { getSeverityLabel, SIGNIFICANT_EVENT_STATUS_OPTIONS } from '@kbn/significant-events-schema';
27-
import type { SignificantEvent, SignificantEventStatus } from '@kbn/significant-events-schema';
26+
import {
27+
getSeverityLabel,
28+
severitySchema,
29+
SIGNIFICANT_EVENT_STATUS_OPTIONS,
30+
SEVERITY_OPTIONS,
31+
} from '@kbn/significant-events-schema';
32+
import type {
33+
SignificantEvent,
34+
SignificantEventStatus,
35+
Severity,
36+
} from '@kbn/significant-events-schema';
2837
import { useSignificantEventsUrlState } from './use_significant_events_url_state';
2938
import { useFetchSignificantEventLifecycle } from '../../../../../hooks/significant_events/use_fetch_significant_event_lifecycle';
3039
import { RUNNING_POLL_INTERVAL_MS } from '../../../constants';
@@ -43,6 +52,8 @@ import { SIGNIFICANT_EVENT_STATUS_LABELS } from '../shared/translations';
4352
import { useTriggerInvestigation } from '../../../../../hooks/significant_events/use_trigger_investigation';
4453
import { useUpdateSignificantEvent } from '../../../../../hooks/significant_events/use_update_significant_event';
4554

55+
export const DEFAULT_SIGNIFICANT_EVENT_SEVERITY_FILTER: Severity[] = ['80-critical', '60-high'];
56+
4657
const RUN_ARIA_LABEL = i18n.translate(
4758
'xpack.streams.sigEventsTab.runInvestigationButton.ariaLabel',
4859
{
@@ -104,8 +115,6 @@ const CloseEventCell = ({ event }: { event: SignificantEvent }) => {
104115
);
105116
};
106117

107-
const MAX_VISIBLE_STREAMS = 3;
108-
109118
const clickableRowCss = css`
110119
cursor: pointer;
111120
`;
@@ -125,10 +134,6 @@ const LOADING_MESSAGE = i18n.translate('xpack.streams.sigEventsTab.loadingMessag
125134
const EMPTY_MESSAGE = i18n.translate('xpack.streams.sigEventsTab.emptyBody', {
126135
defaultMessage: 'No significant events found.',
127136
});
128-
const MORE_LABEL = i18n.translate('xpack.streams.sigEventsTab.moreLabel', {
129-
defaultMessage: 'more',
130-
});
131-
132137
const columns: Array<EuiBasicTableColumn<SignificantEvent>> = [
133138
{
134139
field: '@timestamp',
@@ -163,22 +168,43 @@ const columns: Array<EuiBasicTableColumn<SignificantEvent>> = [
163168
defaultMessage: 'Streams',
164169
}),
165170
width: '160px',
171+
// Required for the column's `width` to actually constrain the cell — EUI's
172+
// `truncateText` only kicks in when the cell is bounded (see tableLayout="fixed" below).
173+
truncateText: true,
166174
render: (streamNames: string[]) => {
167175
const names = streamNames ?? [];
168-
const visible = names.slice(0, MAX_VISIBLE_STREAMS);
169-
const remaining = names.length - visible.length;
176+
const [first, ...rest] = names;
177+
if (!first) return null;
178+
const overflowCount = rest.length;
170179
return (
171-
<EuiFlexGroup gutterSize="xs" wrap responsive={false}>
172-
{visible.map((name, idx) => (
173-
<EuiFlexItem grow={false} key={`${name}-${idx}`}>
174-
<EuiBadge color="hollow">{name}</EuiBadge>
175-
</EuiFlexItem>
176-
))}
177-
{remaining > 0 && (
180+
<EuiFlexGroup
181+
gutterSize="xs"
182+
alignItems="center"
183+
responsive={false}
184+
css={css`
185+
flex-wrap: nowrap;
186+
min-width: 0;
187+
`}
188+
>
189+
<EuiFlexItem
190+
grow={1}
191+
css={css`
192+
min-width: 0;
193+
`}
194+
>
195+
<EuiToolTip content={first}>
196+
<EuiBadge tabIndex={0} color="hollow">
197+
{first}
198+
</EuiBadge>
199+
</EuiToolTip>
200+
</EuiFlexItem>
201+
{overflowCount > 0 && (
178202
<EuiFlexItem grow={false}>
179-
<EuiText size="xs" color="subdued">
180-
+{remaining} {MORE_LABEL}
181-
</EuiText>
203+
<EuiToolTip content={rest.join(', ')}>
204+
<EuiText tabIndex={0} size="xs" color="subdued">
205+
+{overflowCount}
206+
</EuiText>
207+
</EuiToolTip>
182208
</EuiFlexItem>
183209
)}
184210
</EuiFlexGroup>
@@ -218,6 +244,8 @@ const extractCheckedKeys = (options: EuiSelectableOption[]): string[] =>
218244
const isSignificantEventStatus = (value: string): value is SignificantEventStatus =>
219245
SIGNIFICANT_EVENT_STATUS_OPTIONS.some((status) => status === value);
220246

247+
const isSeverity = (value: string): value is Severity => severitySchema.safeParse(value).success;
248+
221249
const buildSelectableOptions = <T extends string>({
222250
values,
223251
selected,
@@ -241,6 +269,9 @@ export const SigEventsTab = () => {
241269
const [statusFilter, setStatusFilter] = useState<SignificantEventStatus[]>(() =>
242270
SIGNIFICANT_EVENT_STATUS_OPTIONS.filter((status) => status === 'open')
243271
);
272+
const [severityFilter, setSeverityFilter] = useState<Severity[]>(() => [
273+
...DEFAULT_SIGNIFICANT_EVENT_SEVERITY_FILTER,
274+
]);
244275
const [streamFilter, setStreamFilter] = useState<string[]>([]);
245276
const [searchQuery, setSearchQuery] = useState('');
246277
const debouncedSearch = useDebouncedValue(searchQuery, 300);
@@ -258,6 +289,7 @@ export const SigEventsTab = () => {
258289
from: timeState.start,
259290
to: timeState.end,
260291
status: statusFilter.length > 0 ? statusFilter : undefined,
292+
severity: severityFilter.length > 0 ? severityFilter : undefined,
261293
stream: streamFilter.length > 0 ? streamFilter : undefined,
262294
search: debouncedSearch || undefined,
263295
});
@@ -291,6 +323,11 @@ export const SigEventsTab = () => {
291323
[]
292324
);
293325

326+
const onSeverityChange = useCallback(
327+
(opts: EuiSelectableOption[]) => setSeverityFilter(extractCheckedKeys(opts).filter(isSeverity)),
328+
[]
329+
);
330+
294331
const filters = useMemo(
295332
() => [
296333
{
@@ -308,6 +345,22 @@ export const SigEventsTab = () => {
308345
numActiveFilters: statusFilter.length,
309346
onChange: onStatusChange,
310347
},
348+
{
349+
label: i18n.translate('xpack.streams.sigEventsTab.filter.severity', {
350+
defaultMessage: 'Severity',
351+
}),
352+
ariaLabel: i18n.translate('xpack.streams.sigEventsTab.filter.severityAriaLabel', {
353+
defaultMessage: 'Filter by severity',
354+
}),
355+
options: buildSelectableOptions({
356+
values: SEVERITY_OPTIONS,
357+
selected: severityFilter,
358+
getLabel: getSeverityLabel,
359+
}),
360+
numFilters: SEVERITY_OPTIONS.length,
361+
numActiveFilters: severityFilter.length,
362+
onChange: onSeverityChange,
363+
},
311364
{
312365
label: i18n.translate('xpack.streams.sigEventsTab.filter.stream', {
313366
defaultMessage: 'Stream',
@@ -325,7 +378,15 @@ export const SigEventsTab = () => {
325378
onChange: onStreamChange,
326379
},
327380
],
328-
[statusFilter, streamFilter, streamOptions, onStatusChange, onStreamChange]
381+
[
382+
statusFilter,
383+
severityFilter,
384+
streamFilter,
385+
streamOptions,
386+
onStatusChange,
387+
onSeverityChange,
388+
onStreamChange,
389+
]
329390
);
330391

331392
const onTableChange = ({ page }: { page?: { index: number; size: number } }) => {
@@ -397,6 +458,7 @@ export const SigEventsTab = () => {
397458
)}
398459
<EuiFlexItem grow={false}>
399460
<EuiBasicTable<SignificantEvent>
461+
tableLayout="fixed"
400462
tableCaption={TABLE_CAPTION}
401463
items={data?.hits ?? []}
402464
columns={columns}

0 commit comments

Comments
 (0)