55 * 2.0.
66 */
77
8+ import React from 'react' ;
9+ import useToggle from 'react-use/lib/useToggle' ;
810import {
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' ;
1721import {
1822 BinaryFilterCondition ,
1923 Condition ,
2024 FilterCondition ,
25+ isCondition ,
2126 isNeverCondition ,
2227} from '@kbn/streams-schema' ;
23- import React , { useEffect } from 'react' ;
2428import { i18n } from '@kbn/i18n' ;
25- import { css } from '@emotion/css' ;
2629import { CodeEditor } from '@kbn/code-editor' ;
30+ import { isPlainObject } from 'lodash' ;
2731import {
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+
176165function 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