Skip to content

Commit 45c335e

Browse files
[Streams] Add documents filter options for routing preview (#233437)
## πŸ“ Summary closes #217964 This PR adds a new filter control for the preview table to allow switching between `Matched` and `Unmatched` documents. ## πŸŽ₯ Demo https://github.com/user-attachments/assets/fb716c6e-47ee-410a-98da-889e8626ac81
1 parent 792ed55 commit 45c335e

File tree

11 files changed

+445
-123
lines changed

11 files changed

+445
-123
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import {
9+
EuiFilterButton,
10+
EuiFilterGroup,
11+
EuiFlexGroup,
12+
EuiFlexItem,
13+
EuiIconTip,
14+
useEuiTheme,
15+
} from '@elastic/eui';
16+
import { i18n } from '@kbn/i18n';
17+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
18+
import { css } from '@emotion/react';
19+
import {
20+
useStreamsRoutingSelector,
21+
type DocumentMatchFilterOptions,
22+
} from './state_management/stream_routing_state_machine';
23+
24+
export interface DocumentMatchFilterControlsProps {
25+
initialFilter: DocumentMatchFilterOptions;
26+
onFilterChange: (filter: DocumentMatchFilterOptions) => void;
27+
matchedDocumentPercentage: number;
28+
isDisabled?: boolean;
29+
}
30+
31+
export const DocumentMatchFilterControls = ({
32+
initialFilter,
33+
onFilterChange,
34+
matchedDocumentPercentage,
35+
isDisabled = false,
36+
}: DocumentMatchFilterControlsProps) => {
37+
const { euiTheme } = useEuiTheme();
38+
39+
const [selectedFilter, setSelectedFilter] = useState<DocumentMatchFilterOptions>(initialFilter);
40+
41+
const isIdleState = useStreamsRoutingSelector((snapshot) => snapshot).matches({
42+
ready: 'idle',
43+
});
44+
45+
const handleFilterChanged = useCallback(
46+
(value: DocumentMatchFilterOptions) => {
47+
if (value === selectedFilter) return;
48+
49+
const newFilter = selectedFilter === 'matched' ? 'unmatched' : 'matched';
50+
onFilterChange(newFilter);
51+
setSelectedFilter(newFilter);
52+
},
53+
[selectedFilter, onFilterChange]
54+
);
55+
56+
useEffect(() => {
57+
if (isIdleState) {
58+
handleFilterChanged('matched');
59+
}
60+
}, [isIdleState, handleFilterChanged]);
61+
62+
const filterButtonCss = useMemo(
63+
() => css`
64+
background-color: transparent !important;
65+
border: 0px !important;
66+
67+
&[aria-pressed='true']:not(:disabled) {
68+
color: ${euiTheme.colors.textParagraph} !important;
69+
}
70+
`,
71+
[euiTheme]
72+
);
73+
74+
return (
75+
<EuiFlexItem grow={false} data-test-subj="routingPreviewFilterControls">
76+
<EuiFlexGroup gutterSize="s" alignItems="center">
77+
<EuiFlexItem grow={false}>
78+
<EuiFilterGroup compressed fullWidth>
79+
<EuiFilterButton
80+
aria-label={i18n.translate(
81+
'xpack.streams.streamDetail.preview.filter.matchedAriaLabel',
82+
{ defaultMessage: 'Filter for matched documents.' }
83+
)}
84+
data-test-subj="routingPreviewMatchedFilterButton"
85+
hasActiveFilters={selectedFilter === 'matched'}
86+
onClick={() => handleFilterChanged('matched')}
87+
isDisabled={isDisabled || isNaN(matchedDocumentPercentage)}
88+
isSelected={selectedFilter === 'matched'}
89+
badgeColor="success"
90+
grow={false}
91+
isToggle
92+
numActiveFilters={
93+
isNaN(matchedDocumentPercentage) ? '' : `${matchedDocumentPercentage}%`
94+
}
95+
css={filterButtonCss}
96+
>
97+
{i18n.translate('xpack.streams.streamDetail.preview.filter.matched', {
98+
defaultMessage: 'Matched',
99+
})}
100+
</EuiFilterButton>
101+
<EuiFilterButton
102+
aria-label={i18n.translate(
103+
'xpack.streams.streamDetail.preview.filter.unmatchedAriaLabel',
104+
{ defaultMessage: 'Filter for unmatched documents.' }
105+
)}
106+
data-test-subj="routingPreviewUnmatchedFilterButton"
107+
hasActiveFilters={selectedFilter === 'unmatched'}
108+
onClick={() => handleFilterChanged('unmatched')}
109+
isDisabled={isDisabled || isNaN(matchedDocumentPercentage)}
110+
isSelected={selectedFilter === 'unmatched'}
111+
badgeColor="accent"
112+
grow={false}
113+
isToggle
114+
numActiveFilters={
115+
isNaN(matchedDocumentPercentage) ? '' : `${100 - matchedDocumentPercentage}%`
116+
}
117+
css={filterButtonCss}
118+
>
119+
{i18n.translate('xpack.streams.streamDetail.preview.filter.unmatched', {
120+
defaultMessage: 'Unmatched',
121+
})}
122+
</EuiFilterButton>
123+
</EuiFilterGroup>
124+
</EuiFlexItem>
125+
<EuiFlexItem grow={false} data-test-subj="routingPreviewFilterControlsTooltip">
126+
<EuiIconTip
127+
aria-label={i18n.translate(
128+
'xpack.streams.streamRouting.previewMatchesTooltipAriaLabel',
129+
{
130+
defaultMessage: 'Additional information',
131+
}
132+
)}
133+
type={'question'}
134+
content={i18n.translate('xpack.streams.streamDetail.previewMatchesTooltipText', {
135+
defaultMessage:
136+
'Approximate percentage of documents matching/unmatching the condition over a random sample of documents.',
137+
})}
138+
iconProps={{ style: { verticalAlign: 'text-bottom', marginLeft: 2 } }}
139+
/>
140+
</EuiFlexItem>
141+
</EuiFlexGroup>
142+
</EuiFlexItem>
143+
);
144+
};

β€Žx-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_routing/preview_matches.tsxβ€Ž

Lines changed: 0 additions & 73 deletions
This file was deleted.

β€Žx-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_routing/preview_panel.tsxβ€Ž

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@ import {
1717
import { i18n } from '@kbn/i18n';
1818
import { isEmpty } from 'lodash';
1919
import React from 'react';
20+
import { isCondition } from '@kbn/streamlang';
2021
import { AssetImage } from '../../asset_image';
2122
import { StreamsAppSearchBar } from '../../streams_app_search_bar';
2223
import { PreviewTable } from '../preview_table';
23-
import { PreviewMatches } from './preview_matches';
2424
import {
2525
selectPreviewDocuments,
26+
useStreamRoutingEvents,
2627
useStreamSamplesSelector,
2728
useStreamsRoutingSelector,
2829
} from './state_management/stream_routing_state_machine';
30+
import { DocumentMatchFilterControls } from './document_match_filter_controls';
31+
import { processCondition } from './utils';
2932

3033
export function PreviewPanel() {
3134
const routingSnapshot = useStreamsRoutingSelector((snapshot) => snapshot);
@@ -96,21 +99,25 @@ const EditingPanel = () => (
9699

97100
const SamplePreviewPanel = () => {
98101
const samplesSnapshot = useStreamSamplesSelector((snapshot) => snapshot);
102+
const { setDocumentMatchFilter } = useStreamRoutingEvents();
99103
const isLoadingDocuments = samplesSnapshot.matches({ fetching: { documents: 'loading' } });
100104
const isUpdating =
101105
samplesSnapshot.matches('debouncingCondition') ||
102106
samplesSnapshot.matches({ fetching: { documents: 'loading' } });
103-
const isLoadingDocumentCounts = samplesSnapshot.matches({
104-
fetching: { documentCounts: 'loading' },
105-
});
106-
const { documentsError, approximateMatchingPercentage, approximateMatchingPercentageError } =
107-
samplesSnapshot.context;
108107

108+
const { documentsError, approximateMatchingPercentage } = samplesSnapshot.context;
109109
const documents = useStreamSamplesSelector((snapshot) =>
110110
selectPreviewDocuments(snapshot.context)
111111
);
112+
113+
const condition = processCondition(samplesSnapshot.context.condition);
114+
const isProcessedCondition = condition ? isCondition(condition) : true;
112115
const hasDocuments = !isEmpty(documents);
113116

117+
const matchedDocumentPercentage = isNaN(parseFloat(approximateMatchingPercentage ?? ''))
118+
? Number.NaN
119+
: parseFloat(approximateMatchingPercentage!);
120+
114121
let content: React.ReactNode | null = null;
115122

116123
if (isLoadingDocuments && !hasDocuments) {
@@ -147,7 +154,7 @@ const SamplePreviewPanel = () => {
147154
body={documentsError.message}
148155
/>
149156
);
150-
} else if (!hasDocuments) {
157+
} else if (!hasDocuments || !isProcessedCondition) {
151158
content = (
152159
<EuiEmptyPrompt
153160
icon={<AssetImage type="noResults" />}
@@ -164,24 +171,23 @@ const SamplePreviewPanel = () => {
164171
} else if (hasDocuments) {
165172
content = (
166173
<EuiFlexItem grow data-test-subj="routingPreviewPanelWithResults">
167-
<EuiFlexGroup direction="column">
168-
<EuiFlexItem grow={false}>
169-
<PreviewMatches
170-
approximateMatchingPercentage={approximateMatchingPercentage}
171-
error={approximateMatchingPercentageError}
172-
isLoading={isLoadingDocumentCounts}
173-
/>
174-
</EuiFlexItem>
175-
<PreviewTable documents={documents} />
176-
</EuiFlexGroup>
174+
<PreviewTable documents={documents} />
177175
</EuiFlexItem>
178176
);
179177
}
180178

181179
return (
182180
<>
183181
{isUpdating && <EuiProgress size="xs" color="accent" position="absolute" />}
184-
{content}
182+
<EuiFlexGroup gutterSize="m" direction="column">
183+
<DocumentMatchFilterControls
184+
initialFilter={samplesSnapshot.context.documentMatchFilter}
185+
onFilterChange={setDocumentMatchFilter}
186+
matchedDocumentPercentage={Math.round(matchedDocumentPercentage)}
187+
isDisabled={!!documentsError || !condition}
188+
/>
189+
{content}
190+
</EuiFlexGroup>
185191
</>
186192
);
187193
};

β€Žx-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_routing/state_management/stream_routing_state_machine/index.tsβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77

88
export * from './selectors';
99
export * from './use_stream_routing';
10+
export * from './routing_samples_state_machine';
1011
export type * from './types';

0 commit comments

Comments
Β (0)