Skip to content

Commit 5eb07b6

Browse files
authored
[Streams] Add KI subtype filter and extract shared filter component (elastic#268963)
## Summary - Adds a **subtype filter** to the Knowledge Indicators table toolbar (between type and stream filters) - Extracts a generic `KnowledgeIndicatorSelectableFilter` component that all three filters (type, stream, subtype) now delegate to, eliminating duplicated popover/selectable/memo logic - Subtype filter is disabled (not hidden) when no subtypes are available for the current filter selection - Cross-filter pruning: changing the type filter clears invalid subtype selections Net result: new feature with less code than before (-162 lines). ## Test plan - [x] Jest tests for `KnowledgeIndicatorSelectableFilter` (12 tests, covers rendering, popover interaction, filtering logic, custom labels, disabled state) - [ ] Verify type, stream, and subtype filters render and function on the KI page - [ ] Verify subtype filter shows disabled with "0" when a type with no subtypes is selected - [ ] Verify selecting a type prunes invalid subtype selections <img width="800" height="439" alt="CleanShot 2026-05-12 at 11 46 40" src="https://github.com/user-attachments/assets/b0c6669b-dbac-451a-9fee-45295b9bcf53" />
1 parent d4a1217 commit 5eb07b6

11 files changed

Lines changed: 660 additions & 305 deletions

File tree

x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,12 @@ export function KnowledgeIndicatorsTable() {
124124
debouncedSearchTerm,
125125
statusFilter,
126126
selectedTypes,
127+
selectedSubtypes,
127128
selectedStreams,
128129
hideComputedTypes,
129130
handleStatusFilterChange,
130131
handleSelectedTypesChange,
132+
handleSelectedSubtypesChange,
131133
handleSelectedStreamsChange,
132134
handleComputedToggleChange,
133135
handleSearchChange,
@@ -236,6 +238,7 @@ export function KnowledgeIndicatorsTable() {
236238
debouncedSearchTerm={debouncedSearchTerm}
237239
statusFilter={statusFilter}
238240
selectedTypes={selectedTypes}
241+
selectedSubtypes={selectedSubtypes}
239242
selectedStreams={selectedStreams}
240243
hideComputedTypes={hideComputedTypes}
241244
pagination={pagination}
@@ -249,6 +252,7 @@ export function KnowledgeIndicatorsTable() {
249252
onSearchChange={handleSearchChange}
250253
onStatusFilterChange={handleStatusFilterChange}
251254
onSelectedTypesChange={handleSelectedTypesChange}
255+
onSelectedSubtypesChange={handleSelectedSubtypesChange}
252256
onSelectedStreamsChange={handleSelectedStreamsChange}
253257
onComputedToggleChange={handleComputedToggleChange}
254258
onClearSelection={() => setSelectedKnowledgeIndicators([])}

x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_toolbar.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type { KnowledgeIndicator } from '@kbn/streams-ai';
1919
import React from 'react';
2020
import { TableTitle } from '../../../stream_detail_systems/table_title';
2121
import { KnowledgeIndicatorsTypeFilter } from '../../../stream_detail_significant_events_view/knowledge_indicators_type_filter';
22+
import { KnowledgeIndicatorsSubtypeFilter } from '../../../stream_detail_significant_events_view/knowledge_indicators_subtype_filter';
2223
import { MATCH_QUERY_TYPE } from '../../../stream_detail_significant_events_view/utils/get_knowledge_indicator_type';
2324
import { KnowledgeIndicatorsStatusFilter } from '../../../stream_detail_significant_events_view/knowledge_indicators_status_filter';
2425
import { StreamFilter } from '../stream_filter';
@@ -47,6 +48,7 @@ interface KnowledgeIndicatorsToolbarProps {
4748
debouncedSearchTerm: string;
4849
statusFilter: 'active' | 'excluded';
4950
selectedTypes: string[];
51+
selectedSubtypes: string[];
5052
selectedStreams: string[];
5153
hideComputedTypes: boolean;
5254
pagination: { pageIndex: number; pageSize: number };
@@ -60,6 +62,7 @@ interface KnowledgeIndicatorsToolbarProps {
6062
onSearchChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
6163
onStatusFilterChange: (filter: 'active' | 'excluded') => void;
6264
onSelectedTypesChange: (types: string[]) => void;
65+
onSelectedSubtypesChange: (subtypes: string[]) => void;
6366
onSelectedStreamsChange: (streams: string[]) => void;
6467
onComputedToggleChange: (checked: boolean) => void;
6568
onClearSelection: () => void;
@@ -76,6 +79,7 @@ export function KnowledgeIndicatorsToolbar({
7679
debouncedSearchTerm,
7780
statusFilter,
7881
selectedTypes,
82+
selectedSubtypes,
7983
selectedStreams,
8084
hideComputedTypes,
8185
pagination,
@@ -89,6 +93,7 @@ export function KnowledgeIndicatorsToolbar({
8993
onSearchChange,
9094
onStatusFilterChange,
9195
onSelectedTypesChange,
96+
onSelectedSubtypesChange,
9297
onSelectedStreamsChange,
9398
onComputedToggleChange,
9499
onClearSelection,
@@ -131,6 +136,18 @@ export function KnowledgeIndicatorsToolbar({
131136
selectedStreams={selectedStreams}
132137
/>
133138
</EuiFlexItem>
139+
<EuiFlexItem grow={false}>
140+
<KnowledgeIndicatorsSubtypeFilter
141+
knowledgeIndicators={knowledgeIndicators}
142+
searchTerm={debouncedSearchTerm}
143+
statusFilter={statusFilter}
144+
selectedTypes={selectedTypes}
145+
selectedSubtypes={selectedSubtypes}
146+
onSelectedSubtypesChange={onSelectedSubtypesChange}
147+
hideComputedTypes={hideComputedTypes}
148+
selectedStreams={selectedStreams}
149+
/>
150+
</EuiFlexItem>
134151
<EuiFlexItem grow={false}>
135152
<StreamFilter
136153
knowledgeIndicators={knowledgeIndicators}

x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/use_knowledge_indicators_table.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { getFormattedError } from '../../../../../util/errors';
2222
import { KI_ROW_ACTION_MUTATION_KEY } from '../../../stream_detail_significant_events_view/knowledge_indicator_actions_cell';
2323
import { getKnowledgeIndicatorItemId } from '../../../stream_detail_significant_events_view/utils/get_knowledge_indicator_item_id';
2424
import { getKnowledgeIndicatorStreamName } from '../../../stream_detail_significant_events_view/utils/get_knowledge_indicator_stream_name';
25+
import { getKnowledgeIndicatorSubtype } from '../../../stream_detail_significant_events_view/utils/get_knowledge_indicator_subtype';
2526
import { matchesKnowledgeIndicatorFilters } from '../../../stream_detail_significant_events_view/utils/matches_knowledge_indicator_filters';
2627
import { getKnowledgeIndicatorType } from '../../../stream_detail_significant_events_view/utils/get_knowledge_indicator_type';
2728
import {
@@ -60,6 +61,7 @@ export function useKnowledgeIndicatorsTable() {
6061
.toLowerCase();
6162
const [statusFilter, setStatusFilter] = useState<'active' | 'excluded'>('active');
6263
const [selectedTypes, setSelectedTypes] = useState<string[]>([]);
64+
const [selectedSubtypes, setSelectedSubtypes] = useState<string[]>([]);
6365
const [selectedStreams, setSelectedStreams] = useState<string[]>([]);
6466
const [hideComputedTypes, setHideComputedTypes] = useState(true);
6567

@@ -113,6 +115,7 @@ export function useKnowledgeIndicatorsTable() {
113115
// but *with* the other filters applied.
114116
useEffect(() => {
115117
const availableTypes = new Set<string>();
118+
const availableSubtypes = new Set<string>();
116119
const availableStreams = new Set<string>();
117120

118121
for (const ki of knowledgeIndicators) {
@@ -125,6 +128,17 @@ export function useKnowledgeIndicatorsTable() {
125128
) {
126129
availableTypes.add(getKnowledgeIndicatorType(ki));
127130
}
131+
if (
132+
matchesKnowledgeIndicatorFilters(ki, {
133+
statusFilter,
134+
selectedTypes,
135+
selectedStreams,
136+
hideComputedTypes,
137+
})
138+
) {
139+
const subtype = getKnowledgeIndicatorSubtype(ki);
140+
if (subtype) availableSubtypes.add(subtype);
141+
}
128142
if (
129143
matchesKnowledgeIndicatorFilters(ki, {
130144
statusFilter,
@@ -140,6 +154,10 @@ export function useKnowledgeIndicatorsTable() {
140154
const pruned = current.filter((t) => availableTypes.has(t));
141155
return pruned.length === current.length ? current : pruned;
142156
});
157+
setSelectedSubtypes((current) => {
158+
const pruned = current.filter((s) => availableSubtypes.has(s));
159+
return pruned.length === current.length ? current : pruned;
160+
});
143161
setSelectedStreams((current) => {
144162
const pruned = current.filter((s) => availableStreams.has(s));
145163
return pruned.length === current.length ? current : pruned;
@@ -151,6 +169,7 @@ export function useKnowledgeIndicatorsTable() {
151169
matchesKnowledgeIndicatorFilters(ki, {
152170
statusFilter,
153171
selectedTypes,
172+
selectedSubtypes,
154173
selectedStreams,
155174
hideComputedTypes,
156175
searchTerm: debouncedSearchTerm,
@@ -167,6 +186,7 @@ export function useKnowledgeIndicatorsTable() {
167186
debouncedSearchTerm,
168187
statusFilter,
169188
selectedTypes,
189+
selectedSubtypes,
170190
selectedStreams,
171191
hideComputedTypes,
172192
]);
@@ -190,6 +210,15 @@ export function useKnowledgeIndicatorsTable() {
190210
const handleSelectedTypesChange = useCallback(
191211
(types: string[]) => {
192212
setSelectedTypes(types);
213+
setSelectedSubtypes([]);
214+
resetPagination();
215+
},
216+
[resetPagination]
217+
);
218+
219+
const handleSelectedSubtypesChange = useCallback(
220+
(subtypes: string[]) => {
221+
setSelectedSubtypes(subtypes);
193222
resetPagination();
194223
},
195224
[resetPagination]
@@ -324,6 +353,7 @@ export function useKnowledgeIndicatorsTable() {
324353
matchesKnowledgeIndicatorFilters(ki, {
325354
statusFilter,
326355
selectedTypes,
356+
selectedSubtypes,
327357
selectedStreams,
328358
hideComputedTypes: false,
329359
searchTerm: debouncedSearchTerm,
@@ -336,6 +366,7 @@ export function useKnowledgeIndicatorsTable() {
336366
filteredKnowledgeIndicators,
337367
statusFilter,
338368
selectedTypes,
369+
selectedSubtypes,
339370
selectedStreams,
340371
debouncedSearchTerm,
341372
]);
@@ -396,10 +427,12 @@ export function useKnowledgeIndicatorsTable() {
396427
debouncedSearchTerm,
397428
statusFilter,
398429
selectedTypes,
430+
selectedSubtypes,
399431
selectedStreams,
400432
hideComputedTypes,
401433
handleStatusFilterChange,
402434
handleSelectedTypesChange,
435+
handleSelectedSubtypesChange,
403436
handleSelectedStreamsChange,
404437
handleComputedToggleChange,
405438
handleSearchChange,

x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/stream_filter/stream_filter.tsx

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

8-
import type { EuiSelectableOption } from '@elastic/eui';
9-
import {
10-
EuiBadge,
11-
EuiFilterButton,
12-
EuiFilterGroup,
13-
EuiPanel,
14-
EuiPopover,
15-
EuiSelectable,
16-
useGeneratedHtmlId,
17-
} from '@elastic/eui';
18-
import { css } from '@emotion/react';
198
import { i18n } from '@kbn/i18n';
209
import type { KnowledgeIndicator } from '@kbn/streams-ai';
2110
import React, { useMemo } from 'react';
22-
import useToggle from 'react-use/lib/useToggle';
11+
import { KnowledgeIndicatorSelectableFilter } from '../../../stream_detail_significant_events_view/knowledge_indicator_selectable_filter';
2312
import { getKnowledgeIndicatorStreamName } from '../../../stream_detail_significant_events_view/utils/get_knowledge_indicator_stream_name';
24-
import { matchesKnowledgeIndicatorFilters } from '../../../stream_detail_significant_events_view/utils/matches_knowledge_indicator_filters';
25-
26-
const popoverPanelStyle = css`
27-
min-width: 260px;
28-
`;
2913

3014
interface StreamFilterProps {
3115
knowledgeIndicators: KnowledgeIndicator[];
@@ -46,128 +30,35 @@ export function StreamFilter({
4630
selectedStreams,
4731
onSelectedStreamsChange,
4832
}: StreamFilterProps) {
49-
const [isPopoverOpen, togglePopover] = useToggle(false);
50-
const popoverId = useGeneratedHtmlId({
51-
prefix: 'streamFilterPopover',
52-
});
53-
54-
const hasActiveFilters = selectedStreams.length > 0;
55-
56-
const { availableStreams, streamCounts } = useMemo(() => {
57-
const streams = new Set<string>();
58-
const counts: Record<string, number> = {};
59-
60-
for (const ki of knowledgeIndicators) {
61-
if (
62-
!matchesKnowledgeIndicatorFilters(ki, {
63-
statusFilter,
64-
selectedTypes,
65-
hideComputedTypes,
66-
})
67-
) {
68-
continue;
69-
}
70-
71-
const streamName = getKnowledgeIndicatorStreamName(ki);
72-
streams.add(streamName);
73-
74-
if (!searchTerm || matchesKnowledgeIndicatorFilters(ki, { searchTerm })) {
75-
counts[streamName] = (counts[streamName] ?? 0) + 1;
76-
}
77-
}
78-
79-
return {
80-
availableStreams: Array.from(streams).sort((left, right) => left.localeCompare(right)),
81-
streamCounts: counts,
82-
};
83-
}, [knowledgeIndicators, searchTerm, statusFilter, selectedTypes, hideComputedTypes]);
84-
85-
const options = useMemo<EuiSelectableOption[]>(() => {
86-
const selectedSet = new Set(selectedStreams);
87-
return [
88-
{
89-
label: STREAM_FILTER_GROUP_LABEL,
90-
isGroupLabel: true,
91-
},
92-
...availableStreams.map((stream) => ({
93-
key: stream,
94-
checked: selectedSet.has(stream) ? ('on' as const) : undefined,
95-
label: stream,
96-
append: <EuiBadge>{streamCounts[stream] ?? 0}</EuiBadge>,
97-
})),
98-
];
99-
}, [availableStreams, selectedStreams, streamCounts]);
33+
const filterCriteria = useMemo(
34+
() => ({ statusFilter, selectedTypes, hideComputedTypes }),
35+
[statusFilter, selectedTypes, hideComputedTypes]
36+
);
10037

10138
return (
102-
<EuiFilterGroup>
103-
<EuiPopover
104-
id={popoverId}
105-
aria-label={STREAM_FILTER_POPOVER_ARIA_LABEL}
106-
button={
107-
<EuiFilterButton
108-
iconType="arrowDown"
109-
iconSide="right"
110-
isSelected={isPopoverOpen}
111-
hasActiveFilters={hasActiveFilters}
112-
numFilters={availableStreams.length}
113-
numActiveFilters={selectedStreams.length}
114-
onClick={togglePopover}
115-
>
116-
{STREAM_FILTER_LABEL}
117-
</EuiFilterButton>
118-
}
119-
isOpen={isPopoverOpen}
120-
closePopover={() => togglePopover(false)}
121-
panelPaddingSize="none"
122-
>
123-
<EuiSelectable
124-
aria-label={STREAM_FILTER_SELECTABLE_ARIA_LABEL}
125-
options={options}
126-
onChange={(nextOptions) => {
127-
onSelectedStreamsChange(
128-
nextOptions
129-
.filter((option) => option.checked === 'on')
130-
.map((option) => String(option.key ?? option.label))
131-
);
132-
}}
133-
>
134-
{(list) => (
135-
<EuiPanel
136-
hasShadow={false}
137-
hasBorder={false}
138-
paddingSize="none"
139-
css={popoverPanelStyle}
140-
>
141-
{list}
142-
</EuiPanel>
143-
)}
144-
</EuiSelectable>
145-
</EuiPopover>
146-
</EuiFilterGroup>
39+
<KnowledgeIndicatorSelectableFilter
40+
knowledgeIndicators={knowledgeIndicators}
41+
searchTerm={searchTerm}
42+
getValue={getKnowledgeIndicatorStreamName}
43+
selected={selectedStreams}
44+
onSelectedChange={onSelectedStreamsChange}
45+
labels={{
46+
button: i18n.translate('xpack.streams.knowledgeIndicators.streamFilterLabel', {
47+
defaultMessage: 'Stream',
48+
}),
49+
groupLabel: i18n.translate('xpack.streams.knowledgeIndicators.streamFilterGroupLabel', {
50+
defaultMessage: 'Filter by stream',
51+
}),
52+
popoverAriaLabel: i18n.translate(
53+
'xpack.streams.knowledgeIndicators.streamFilterPopoverLabel',
54+
{ defaultMessage: 'Stream filter' }
55+
),
56+
selectableAriaLabel: i18n.translate(
57+
'xpack.streams.knowledgeIndicators.streamFilterSelectableAriaLabel',
58+
{ defaultMessage: 'Filter knowledge indicators by stream' }
59+
),
60+
}}
61+
filterCriteria={filterCriteria}
62+
/>
14763
);
14864
}
149-
150-
const STREAM_FILTER_GROUP_LABEL = i18n.translate(
151-
'xpack.streams.knowledgeIndicators.streamFilterGroupLabel',
152-
{
153-
defaultMessage: 'Filter by stream',
154-
}
155-
);
156-
157-
const STREAM_FILTER_POPOVER_ARIA_LABEL = i18n.translate(
158-
'xpack.streams.knowledgeIndicators.streamFilterPopoverLabel',
159-
{
160-
defaultMessage: 'Stream filter',
161-
}
162-
);
163-
164-
const STREAM_FILTER_LABEL = i18n.translate('xpack.streams.knowledgeIndicators.streamFilterLabel', {
165-
defaultMessage: 'Stream',
166-
});
167-
168-
const STREAM_FILTER_SELECTABLE_ARIA_LABEL = i18n.translate(
169-
'xpack.streams.knowledgeIndicators.streamFilterSelectableAriaLabel',
170-
{
171-
defaultMessage: 'Filter knowledge indicators by stream',
172-
}
173-
);

0 commit comments

Comments
 (0)