Skip to content

Commit 0c8a0b2

Browse files
[8.x] [Streams 🌊] Update condition editor enabling and fixes (#218055) (#218106)
# Backport This will backport the following commits from `main` to `8.x`: - [[Streams 🌊] Update condition editor enabling and fixes (#218055)](#218055) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Marco Antonio Ghiani","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-04-14T12:25:46Z","message":"[Streams 🌊] Update condition editor enabling and fixes (#218055)\n\n## πŸ““ Summary\n\nCloses #217884 \n\n- Updates the condition editor to have a more consistent behaviour for\nenabled/disabled routing.\n- A more explicit tooltip is added to describe how the status flag\naffects the routing behaviour.\n- The status switch is visible by default, while before it was shown\nonly in edit mode for a routing condition.\n - Fixed crashes when manually working on the syntax editor.\n- Removes the routing status flag from the condition editor in the\nprocessors' config.\n\n<img width=\"763\" alt=\"Screenshot 2025-04-14 at 10 23 42\"\nsrc=\"https://github.com/user-attachments/assets/8521739a-ac53-4751-9ad3-4400a84c5a8d\"\n/>","sha":"d46a89e4a547054077d31de1a4b281615734c6a4","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:obs-ux-logs","backport:version","Feature:Streams","v9.1.0","v8.19.0"],"title":"[Streams 🌊] Update condition editor enabling and fixes","number":218055,"url":"https://github.com/elastic/kibana/pull/218055","mergeCommit":{"message":"[Streams 🌊] Update condition editor enabling and fixes (#218055)\n\n## πŸ““ Summary\n\nCloses #217884 \n\n- Updates the condition editor to have a more consistent behaviour for\nenabled/disabled routing.\n- A more explicit tooltip is added to describe how the status flag\naffects the routing behaviour.\n- The status switch is visible by default, while before it was shown\nonly in edit mode for a routing condition.\n - Fixed crashes when manually working on the syntax editor.\n- Removes the routing status flag from the condition editor in the\nprocessors' config.\n\n<img width=\"763\" alt=\"Screenshot 2025-04-14 at 10 23 42\"\nsrc=\"https://github.com/user-attachments/assets/8521739a-ac53-4751-9ad3-4400a84c5a8d\"\n/>","sha":"d46a89e4a547054077d31de1a4b281615734c6a4"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/218055","number":218055,"mergeCommit":{"message":"[Streams 🌊] Update condition editor enabling and fixes (#218055)\n\n## πŸ““ Summary\n\nCloses #217884 \n\n- Updates the condition editor to have a more consistent behaviour for\nenabled/disabled routing.\n- A more explicit tooltip is added to describe how the status flag\naffects the routing behaviour.\n- The status switch is visible by default, while before it was shown\nonly in edit mode for a routing condition.\n - Fixed crashes when manually working on the syntax editor.\n- Removes the routing status flag from the condition editor in the\nprocessors' config.\n\n<img width=\"763\" alt=\"Screenshot 2025-04-14 at 10 23 42\"\nsrc=\"https://github.com/user-attachments/assets/8521739a-ac53-4751-9ad3-4400a84c5a8d\"\n/>","sha":"d46a89e4a547054077d31de1a4b281615734c6a4"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Marco Antonio Ghiani <[email protected]>
1 parent 161e3d5 commit 0c8a0b2

File tree

8 files changed

+210
-205
lines changed

8 files changed

+210
-205
lines changed

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

Lines changed: 162 additions & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -5,153 +5,71 @@
55
* 2.0.
66
*/
77

8+
import React from 'react';
9+
import useToggle from 'react-use/lib/useToggle';
810
import {
11+
EuiCodeBlock,
912
EuiFieldText,
1013
EuiFlexGroup,
11-
EuiFlexItem,
14+
EuiForm,
15+
EuiFormRow,
16+
EuiIconTip,
1217
EuiSelect,
18+
EuiSelectOption,
1319
EuiSwitch,
14-
EuiText,
15-
EuiToolTip,
1620
} from '@elastic/eui';
1721
import {
1822
BinaryFilterCondition,
1923
Condition,
2024
FilterCondition,
25+
isCondition,
2126
isNeverCondition,
2227
} from '@kbn/streams-schema';
23-
import React, { useEffect } from 'react';
2428
import { i18n } from '@kbn/i18n';
25-
import { css } from '@emotion/css';
2629
import { CodeEditor } from '@kbn/code-editor';
30+
import { isPlainObject } from 'lodash';
2731
import {
28-
EMPTY_EQUALS_CONDITION,
32+
ALWAYS_CONDITION,
33+
NEVER_CONDITION,
2934
alwaysToEmptyEquals,
3035
emptyEqualsToAlways,
3136
} from '../../../util/condition';
3237

33-
export function ConditionEditor(props: {
34-
condition: Condition;
35-
onConditionChange?: (condition: Condition) => void;
36-
isNew?: boolean;
37-
}) {
38-
const normalizedCondition = alwaysToEmptyEquals(props.condition);
39-
40-
const handleConditionChange = (condition: Condition) => {
41-
props.onConditionChange?.(emptyEqualsToAlways(condition));
42-
};
38+
export type RoutingConditionEditorProps = ConditionEditorProps;
4339

44-
return (
45-
<ConditionForm
46-
condition={normalizedCondition}
47-
onConditionChange={handleConditionChange}
48-
isNew={props.isNew}
49-
/>
50-
);
51-
}
40+
export function RoutingConditionEditor(props: RoutingConditionEditorProps) {
41+
const isEnabled = !isNeverCondition(props.condition);
5242

53-
export function ConditionForm(props: {
54-
condition: Condition;
55-
onConditionChange: (condition: Condition) => void;
56-
isNew?: boolean;
57-
}) {
58-
const [syntaxEditor, setSyntaxEditor] = React.useState(() =>
59-
Boolean(props.condition && !('operator' in props.condition))
60-
);
61-
const [jsonCondition, setJsonCondition] = React.useState<string | null>(() =>
62-
JSON.stringify(props.condition, null, 2)
63-
);
64-
useEffect(() => {
65-
if (!syntaxEditor && props.condition) {
66-
setJsonCondition(JSON.stringify(props.condition, null, 2));
67-
}
68-
}, [syntaxEditor, props.condition]);
6943
return (
70-
<EuiFlexGroup direction="column" gutterSize="s">
71-
{!props.isNew && (
72-
<>
73-
<EuiFlexItem grow>
74-
<EuiText
75-
className={css`
76-
font-weight: bold;
77-
`}
78-
size="xs"
79-
>
80-
{i18n.translate('xpack.streams.conditionEditor.title', { defaultMessage: 'Status' })}
81-
</EuiText>
82-
</EuiFlexItem>
83-
<EuiToolTip
84-
content={i18n.translate('xpack.streams.conditionEditor.disableTooltip', {
85-
defaultMessage: 'Route no documents to this stream without deleting existing data',
44+
<EuiForm fullWidth>
45+
<EuiFormRow
46+
label={
47+
<EuiFlexGroup gutterSize="xs" alignItems="center">
48+
{i18n.translate('xpack.streams.conditionEditor.title', {
49+
defaultMessage: 'Status',
8650
})}
87-
>
88-
<EuiSwitch
89-
label={i18n.translate('xpack.streams.conditionEditor.switch', {
90-
defaultMessage: 'Enabled',
91-
})}
92-
compressed
93-
checked={!isNeverCondition(props.condition)}
94-
onChange={() => {
95-
props.onConditionChange(
96-
isNeverCondition(props.condition) ? EMPTY_EQUALS_CONDITION : { never: {} }
97-
);
98-
setSyntaxEditor(false);
99-
}}
100-
/>
101-
</EuiToolTip>
102-
</>
103-
)}
104-
{(props.isNew || !isNeverCondition(props.condition)) && (
105-
<>
106-
<EuiFlexGroup alignItems="center" gutterSize="xs">
107-
<EuiFlexItem grow>
108-
<EuiText
109-
className={css`
110-
font-weight: bold;
111-
`}
112-
size="xs"
113-
>
114-
{i18n.translate('xpack.streams.conditionEditor.title', {
115-
defaultMessage: 'Condition',
116-
})}
117-
</EuiText>
118-
</EuiFlexItem>
119-
120-
<EuiSwitch
121-
label={i18n.translate('xpack.streams.conditionEditor.switch', {
122-
defaultMessage: 'Syntax editor',
51+
<EuiIconTip
52+
content={i18n.translate('xpack.streams.conditionEditor.disableTooltip', {
53+
defaultMessage:
54+
'When disabled, the routing rule stops sending documents to this stream. It does not remove existing data.',
12355
})}
124-
compressed
125-
checked={syntaxEditor}
126-
onChange={() => setSyntaxEditor(!syntaxEditor)}
12756
/>
12857
</EuiFlexGroup>
129-
{syntaxEditor ? (
130-
<CodeEditor
131-
height={200}
132-
languageId="json"
133-
value={jsonCondition || ''}
134-
onChange={(e) => {
135-
setJsonCondition(e);
136-
try {
137-
const condition = JSON.parse(e);
138-
props.onConditionChange(condition);
139-
} catch (error: unknown) {
140-
// do nothing
141-
}
142-
}}
143-
/>
144-
) : !props.condition || 'operator' in props.condition ? (
145-
<FilterForm
146-
condition={(props.condition as FilterCondition) || EMPTY_EQUALS_CONDITION}
147-
onConditionChange={props.onConditionChange}
148-
/>
149-
) : (
150-
<pre>{JSON.stringify(props.condition, null, 2)}</pre>
151-
)}
152-
</>
153-
)}
154-
</EuiFlexGroup>
58+
}
59+
>
60+
<EuiSwitch
61+
label={i18n.translate('xpack.streams.conditionEditor.switch', {
62+
defaultMessage: 'Enabled',
63+
})}
64+
compressed
65+
checked={isEnabled}
66+
onChange={(event) => {
67+
props.onConditionChange(event.target.checked ? ALWAYS_CONDITION : NEVER_CONDITION);
68+
}}
69+
/>
70+
</EuiFormRow>
71+
{isEnabled && <ConditionEditor {...props} />}
72+
</EuiForm>
15573
);
15674
}
15775

@@ -173,77 +91,141 @@ const operatorMap = {
17391
notExists: i18n.translate('xpack.streams.filter.notExists', { defaultMessage: 'not exists' }),
17492
};
17593

94+
const operatorOptions: EuiSelectOption[] = Object.entries(operatorMap).map(([value, text]) => ({
95+
value,
96+
text,
97+
}));
98+
99+
export interface ConditionEditorProps {
100+
condition: Condition;
101+
onConditionChange: (condition: Condition) => void;
102+
}
103+
104+
export function ConditionEditor(props: ConditionEditorProps) {
105+
const isInvalidCondition = !isCondition(props.condition);
106+
107+
const condition = alwaysToEmptyEquals(props.condition);
108+
109+
const isFilterCondition = isPlainObject(condition) && 'operator' in condition;
110+
111+
const [usingSyntaxEditor, toggleSyntaxEditor] = useToggle(!isFilterCondition);
112+
113+
const handleConditionChange = (updatedCondition: Condition) => {
114+
props.onConditionChange(emptyEqualsToAlways(updatedCondition));
115+
};
116+
117+
return (
118+
<EuiFormRow
119+
label={i18n.translate('xpack.streams.conditionEditor.title', {
120+
defaultMessage: 'Condition',
121+
})}
122+
labelAppend={
123+
<EuiSwitch
124+
label={i18n.translate('xpack.streams.conditionEditor.switch', {
125+
defaultMessage: 'Syntax editor',
126+
})}
127+
compressed
128+
checked={usingSyntaxEditor}
129+
onChange={toggleSyntaxEditor}
130+
/>
131+
}
132+
isInvalid={isInvalidCondition}
133+
error={
134+
isInvalidCondition
135+
? i18n.translate('xpack.streams.conditionEditor.error', {
136+
defaultMessage: 'The condition is invalid or in unrecognized format.',
137+
})
138+
: undefined
139+
}
140+
>
141+
{usingSyntaxEditor ? (
142+
<CodeEditor
143+
height={200}
144+
languageId="json"
145+
value={JSON.stringify(condition, null, 2)}
146+
onChange={(value) => {
147+
try {
148+
handleConditionChange(JSON.parse(value));
149+
} catch (error: unknown) {
150+
// do nothing
151+
}
152+
}}
153+
/>
154+
) : isFilterCondition ? (
155+
<FilterForm condition={condition} onConditionChange={handleConditionChange} />
156+
) : (
157+
<EuiCodeBlock language="json" paddingSize="m" isCopyable>
158+
{JSON.stringify(condition, null, 2)}
159+
</EuiCodeBlock>
160+
)}
161+
</EuiFormRow>
162+
);
163+
}
164+
176165
function FilterForm(props: {
177166
condition: FilterCondition;
178167
onConditionChange: (condition: FilterCondition) => void;
179168
}) {
169+
const handleConditionChange = (updatedCondition: Partial<FilterCondition>) => {
170+
props.onConditionChange({
171+
...props.condition,
172+
...updatedCondition,
173+
} as FilterCondition);
174+
};
175+
176+
const handleOperatorChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
177+
const newCondition: Partial<FilterCondition> = { ...props.condition };
178+
179+
const newOperator = event.target.value;
180+
if (newOperator === 'exists' || newOperator === 'notExists') {
181+
if ('value' in newCondition) delete newCondition.value;
182+
} else if (!('value' in newCondition)) {
183+
(newCondition as BinaryFilterCondition).value = '';
184+
}
185+
186+
props.onConditionChange({
187+
...newCondition,
188+
operator: newOperator,
189+
} as FilterCondition);
190+
};
191+
180192
return (
181193
<EuiFlexGroup gutterSize="s" alignItems="center">
182-
<EuiFlexItem grow>
194+
<EuiFieldText
195+
data-test-subj="streamsAppFilterFormFieldText"
196+
aria-label={i18n.translate('xpack.streams.filter.field', { defaultMessage: 'Field' })}
197+
compressed
198+
placeholder={i18n.translate('xpack.streams.filter.fieldPlaceholder', {
199+
defaultMessage: 'Field',
200+
})}
201+
value={props.condition.field}
202+
onChange={(e) => {
203+
handleConditionChange({ field: e.target.value });
204+
}}
205+
/>
206+
<EuiSelect
207+
aria-label={i18n.translate('xpack.streams.filter.operator', {
208+
defaultMessage: 'Operator',
209+
})}
210+
data-test-subj="streamsAppFilterFormSelect"
211+
options={operatorOptions}
212+
value={props.condition.operator}
213+
compressed
214+
onChange={handleOperatorChange}
215+
/>
216+
{'value' in props.condition && (
183217
<EuiFieldText
184-
data-test-subj="streamsAppFilterFormFieldText"
185-
aria-label={i18n.translate('xpack.streams.filter.field', { defaultMessage: 'Field' })}
186-
compressed
187-
placeholder={i18n.translate('xpack.streams.filter.fieldPlaceholder', {
188-
defaultMessage: 'Field',
218+
aria-label={i18n.translate('xpack.streams.filter.value', { defaultMessage: 'Value' })}
219+
placeholder={i18n.translate('xpack.streams.filter.valuePlaceholder', {
220+
defaultMessage: 'Value',
189221
})}
190-
value={props.condition.field}
191-
onChange={(e) => {
192-
props.onConditionChange({ ...props.condition, field: e.target.value });
193-
}}
194-
/>
195-
</EuiFlexItem>
196-
<EuiFlexItem grow>
197-
<EuiSelect
198-
aria-label={i18n.translate('xpack.streams.filter.operator', {
199-
defaultMessage: 'Operator',
200-
})}
201-
data-test-subj="streamsAppFilterFormSelect"
202-
options={
203-
Object.entries(operatorMap).map(([value, text]) => ({
204-
value,
205-
text,
206-
})) as Array<{ value: FilterCondition['operator']; text: string }>
207-
}
208-
value={props.condition.operator}
209222
compressed
223+
value={String(props.condition.value)}
224+
data-test-subj="streamsAppFilterFormValueText"
210225
onChange={(e) => {
211-
const newCondition: Partial<FilterCondition> = {
212-
...props.condition,
213-
};
214-
215-
const newOperator = e.target.value as FilterCondition['operator'];
216-
if (newOperator === 'exists' || newOperator === 'notExists') {
217-
if ('value' in newCondition) delete newCondition.value;
218-
} else if (!('value' in newCondition)) {
219-
(newCondition as BinaryFilterCondition).value = '';
220-
}
221-
props.onConditionChange({
222-
...newCondition,
223-
operator: newOperator,
224-
} as FilterCondition);
226+
handleConditionChange({ value: e.target.value });
225227
}}
226228
/>
227-
</EuiFlexItem>
228-
229-
{'value' in props.condition && (
230-
<EuiFlexItem grow>
231-
<EuiFieldText
232-
aria-label={i18n.translate('xpack.streams.filter.value', { defaultMessage: 'Value' })}
233-
placeholder={i18n.translate('xpack.streams.filter.valuePlaceholder', {
234-
defaultMessage: 'Value',
235-
})}
236-
compressed
237-
value={String(props.condition.value)}
238-
data-test-subj="streamsAppFilterFormValueText"
239-
onChange={(e) => {
240-
props.onConditionChange({
241-
...props.condition,
242-
value: e.target.value,
243-
} as BinaryFilterCondition);
244-
}}
245-
/>
246-
</EuiFlexItem>
247229
)}
248230
</EuiFlexGroup>
249231
);

0 commit comments

Comments
Β (0)