@orchestrator-ui/orchestrator-ui-components 8.7.0 → 8.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestrator-ui/orchestrator-ui-components",
3
- "version": "8.7.0",
3
+ "version": "8.7.2",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Library of UI Components used to display the workflow orchestrator frontend",
6
6
  "author": {
@@ -1,7 +1,12 @@
1
1
  import React from 'react';
2
2
 
3
3
  import { capitalize } from 'lodash';
4
- import type { PydanticFormElement } from 'pydantic-forms';
4
+ import {
5
+ type PydanticFormElement,
6
+ type PydanticFormLabelProviderResponse,
7
+ useGetConfig,
8
+ useLabelProvider,
9
+ } from 'pydantic-forms';
5
10
 
6
11
  import { EuiFlexItem, EuiFormRow, EuiText } from '@elastic/eui';
7
12
 
@@ -14,24 +19,53 @@ import { getNestedSummaryLabel } from './wfoPydanticFormUtils';
14
19
  export const WfoSummary: PydanticFormElement = ({ pydanticFormField }) => {
15
20
  const { summaryFieldStyle } = useWithOrchestratorTheme(summaryFieldStyles);
16
21
  const { formRowStyle } = useWithOrchestratorTheme(getCommonFormFieldStyles);
22
+ const config = useGetConfig();
23
+ const { data }: { data: PydanticFormLabelProviderResponse | undefined } = useLabelProvider(
24
+ config.labelProvider,
25
+ 'temp',
26
+ 'test',
27
+ );
28
+ const rawLabels = data?.labels || { summary: {} };
29
+ const labelTranslations: Record<string, string> = {
30
+ ...(rawLabels as Record<string, string>),
31
+ ...(rawLabels?.summary as Record<string, string>),
32
+ };
33
+
34
+ const translateSummaryField = (value: string) => {
35
+ if (value in labelTranslations) {
36
+ return labelTranslations[value];
37
+ }
38
+
39
+ const match = value.match(/^(.+)_(\d+)$/);
40
+ if (!match) {
41
+ return value;
42
+ }
43
+ const [, base, suffix] = match;
44
+
45
+ if (base in labelTranslations) {
46
+ return `${labelTranslations[base]} ${suffix}`;
47
+ }
48
+
49
+ return snakeToHuman(capitalize(value));
50
+ };
17
51
 
18
52
  const { id, title, description } = pydanticFormField;
19
53
  const uniforms = pydanticFormField.schema.uniforms;
20
54
  const summaryData = uniforms?.data as unknown as {
21
- headers: string[][];
55
+ headers: string[];
22
56
  labels: string[];
23
57
  columns: string[][];
24
58
  };
25
59
 
26
- const headers = summaryData?.headers as string[][];
27
- const labels = summaryData?.labels as string[];
60
+ const headers = summaryData?.headers;
61
+ const labels = summaryData?.labels;
28
62
  const columns = summaryData?.columns || [];
29
63
 
30
64
  const extraColumnsData = columns.filter((_, index) => index !== 0);
31
65
 
32
66
  const rows = columns[0].map((row, index) => (
33
67
  <tr key={index}>
34
- {labels && <td className={`label`}>{getNestedSummaryLabel(labels, index)}</td>}
68
+ {labels && <td className={`label`}>{translateSummaryField(getNestedSummaryLabel(labels, index))}</td>}
35
69
  <td className={`value`}>
36
70
  {typeof row === 'string' && row.includes('<!doctype html>') ?
37
71
  <div className="emailMessage" dangerouslySetInnerHTML={{ __html: row }}></div>
@@ -52,11 +86,12 @@ export const WfoSummary: PydanticFormElement = ({ pydanticFormField }) => {
52
86
  : <tr>
53
87
  {labels && <th />}
54
88
  {headers.map((header, idx) => (
55
- <th key={idx}>{header}</th>
89
+ <th key={idx}>{translateSummaryField(header)}</th>
56
90
  ))}
57
91
  </tr>;
58
92
 
59
- const formattedTitle = snakeToHuman(capitalize(title ?? ''));
93
+ const formattedTitle =
94
+ title === 'MigrationSummaryValue' ? translateSummaryField(id) : snakeToHuman(capitalize(title ?? ''));
60
95
 
61
96
  return (
62
97
  <EuiFlexItem data-testid={id} css={[summaryFieldStyle, formRowStyle]}>
@@ -113,7 +113,7 @@ export const FilterGroup: FC<FilterGroupProps> = ({
113
113
  <EuiFlexGroup gutterSize="s" alignItems="center">
114
114
  <EuiFlexItem grow={false}>
115
115
  <EuiButton size="s" iconType="plusInCircle" onClick={addCondition}>
116
- {t('addCondition')}
116
+ {t('addRule')}
117
117
  </EuiButton>
118
118
  </EuiFlexItem>
119
119
  <EuiFlexItem grow={false}>
@@ -85,7 +85,7 @@ export const WfoFieldSelector = ({ handleOnChange, disabled, rule, context }: Fi
85
85
  ?? paths.find((path) => path.availablePaths?.includes(selectedValue));
86
86
  const operators = matchingPath?.operators ?? prefilledFieldOptions.get(selectedValue) ?? [];
87
87
 
88
- context?.onFieldSelected?.(selectedValue, operators);
88
+ context?.onFieldSelected?.(selectedValue, operators, matchingPath);
89
89
  };
90
90
 
91
91
  const handleFieldSelection = (selectedOptions: EuiComboBoxOptionOption<string>[]) => {
@@ -10,7 +10,7 @@ import { SearchParams, WfoAutoExpandableTextArea, WfoTextAnchor } from '@/compon
10
10
  import { WfoCombinatorSelector } from '@/components/WfoTable/WfoStructuredSearchTable/WfoCombinatorSelector';
11
11
  import { useWithOrchestratorTheme } from '@/hooks';
12
12
  import { OperatorDisplay } from '@/types';
13
- import type { FieldToOperatorMap } from '@/types';
13
+ import type { FieldToOperatorMap, PathInfo } from '@/types';
14
14
 
15
15
  import { WfoFieldSelector } from './WfoFieldSelector';
16
16
  import { WfoInlineCombinator } from './WfoInlineCombinator';
@@ -23,6 +23,9 @@ import { getWfoStructuredSearchTableStyles } from './styles';
23
23
 
24
24
  // Maps PathInfo operator names to react-querybuilder's native operator names,
25
25
  // which is what parseCEL produces and formatQuery(cel) expects.
26
+ // has_component/not_has_component ride on notNull/null: they survive the CEL round trip
27
+ // (`field != null` / `field == null`) and formatQuery(elasticsearch) turns them into
28
+ // exists / must_not-exists, which the backend translates to component-presence filters.
26
29
  const SEARCH_OPERATOR_TO_RQB_OPERATOR_MAP: Record<string, string> = {
27
30
  eq: '=',
28
31
  neq: '!=',
@@ -32,8 +35,13 @@ const SEARCH_OPERATOR_TO_RQB_OPERATOR_MAP: Record<string, string> = {
32
35
  gte: '>=',
33
36
  between: 'between',
34
37
  like: 'contains',
38
+ has_component: 'notNull',
39
+ not_has_component: 'null',
35
40
  };
36
41
 
42
+ // Operators without a value; marking them unary makes react-querybuilder's Rule hide the value editor.
43
+ const RQB_UNARY_OPERATORS = ['null', 'notNull'];
44
+
37
45
  const OPERATOR_MAP: Record<string, OperatorDisplay> = {
38
46
  eq: { symbol: '=', description: 'equals' },
39
47
  neq: { symbol: '≠', description: 'not equals' },
@@ -47,10 +55,6 @@ const OPERATOR_MAP: Record<string, OperatorDisplay> = {
47
55
  like: { symbol: '∋', description: 'contains' },
48
56
  };
49
57
 
50
- /* TODO: Add the missing operators
51
- ['has_component', 'not_has_component'];
52
- */
53
-
54
58
  interface WfoFilterBuilderProps {
55
59
  filterString?: string;
56
60
  onUpdateFilterString: (filterString: string) => void;
@@ -87,13 +91,21 @@ export const WfoFilterBuilder = ({
87
91
  return (operators ?? []).map((operator) => {
88
92
  const { symbol, description } = OPERATOR_MAP[operator] || { symbol: operator, description: operator };
89
93
  const rqbOperator = SEARCH_OPERATOR_TO_RQB_OPERATOR_MAP[operator] ?? operator;
90
- return { name: rqbOperator, label: `${symbol} ${description}`, value: rqbOperator };
94
+ return {
95
+ name: rqbOperator,
96
+ label: `${symbol} ${description}`,
97
+ value: rqbOperator,
98
+ ...(RQB_UNARY_OPERATORS.includes(rqbOperator) && { arity: 'unary' }),
99
+ };
91
100
  });
92
101
  };
93
102
 
94
103
  const t = useTranslations('common');
95
104
  const { queryBuilderContainerStyles } = useWithOrchestratorTheme(getWfoStructuredSearchTableStyles);
96
105
  const [fieldToOperatorMap, setFieldToOperatorMap] = useState<FieldToOperatorMap>(prefilledFieldOptions);
106
+ // Path info per selected field, so the value editor can pick a typed editor (date picker,
107
+ // number input, boolean toggle or range editor)
108
+ const [fieldPathInfoMap, setFieldPathInfoMap] = useState<Map<string, PathInfo>>(new Map());
97
109
 
98
110
  // Enter in a value editor commits its value on blur, and that state update has not
99
111
  // flushed yet when the search runs in the same keydown. onQueryChange fires
@@ -106,10 +118,15 @@ export const WfoFilterBuilder = ({
106
118
  handleSearch({ ruleGroup: latestRuleGroupRef.current });
107
119
  };
108
120
 
109
- const handleFieldSelected = (field: string, operators: string[]) => {
121
+ const handleFieldSelected = (field: string, operators: string[], pathInfo?: PathInfo) => {
110
122
  setFieldToOperatorMap((previousMap) => {
111
123
  return new Map(previousMap).set(field, operators);
112
124
  });
125
+ if (pathInfo) {
126
+ setFieldPathInfoMap((previousMap) => {
127
+ return new Map(previousMap).set(field, pathInfo);
128
+ });
129
+ }
113
130
  };
114
131
 
115
132
  return (
@@ -126,6 +143,7 @@ export const WfoFilterBuilder = ({
126
143
  context={{
127
144
  onFieldSelected: handleFieldSelected,
128
145
  prefilledFieldOptions,
146
+ fieldPathInfoMap,
129
147
  onValueEditorEnter: handleValueEditorEnter,
130
148
  }}
131
149
  getOperators={(field) => {
@@ -7,6 +7,14 @@ import { EuiSelect } from '@elastic/eui';
7
7
  const isOptionGroup = (operator: FullOperator | OptionGroup<FullOperator>): operator is OptionGroup<FullOperator> =>
8
8
  'options' in operator;
9
9
 
10
+ // The search backend has no is-null operator: null/notNull only occur here as the
11
+ // react-querybuilder encoding of the component-presence operators, so a restored rule
12
+ // should label them accordingly instead of using defaultOperators' "is (not) null".
13
+ const FALLBACK_OPERATOR_LABELS: Record<string, string> = {
14
+ notNull: '✓ has component',
15
+ null: '✗ does not have component',
16
+ };
17
+
10
18
  export const WfoOperatorSelector = (props: OperatorSelectorProps) => {
11
19
  const { value, handleOnChange } = props;
12
20
 
@@ -44,7 +52,16 @@ export const WfoOperatorSelector = (props: OperatorSelectorProps) => {
44
52
  const currentValueIsListed = !value || selectOptions.some((option) => option.value === value);
45
53
  const displayOptions =
46
54
  currentValueIsListed ? selectOptions : (
47
- [...selectOptions, { value, text: defaultOperators.find((operator) => operator.name === value)?.label ?? value }]
55
+ [
56
+ ...selectOptions,
57
+ {
58
+ value,
59
+ text:
60
+ FALLBACK_OPERATOR_LABELS[value]
61
+ ?? defaultOperators.find((operator) => operator.name === value)?.label
62
+ ?? value,
63
+ },
64
+ ]
48
65
  );
49
66
 
50
67
  return (
@@ -1,64 +1,52 @@
1
1
  import React, { useEffect, useState } from 'react';
2
+ import type { ValueEditorProps } from 'react-querybuilder';
2
3
 
3
4
  import { EuiFlexGroup } from '@elastic/eui';
4
5
 
5
- export interface WfoRangeElementProps {
6
- handleOnChange: (value: string | number | undefined, rangeIndex?: number) => void;
7
- value: string;
8
- operator?: string;
9
- rangeIndex?: number;
10
- }
6
+ import type { EditorComponent } from './WfoValueEditor';
11
7
 
12
8
  interface WfoRangeEditorProps {
13
- handleOnChange: (value: string | number | undefined, rangeIndex?: number) => void;
14
- operator: string;
9
+ handleOnChange: ValueEditorProps['handleOnChange'];
15
10
  value: string;
16
- Element: React.ComponentType<WfoRangeElementProps>;
11
+ InputElement: EditorComponent;
17
12
  }
18
13
 
19
- export const WfoRangeEditor = ({ handleOnChange, operator, Element, value: currentValue }: WfoRangeEditorProps) => {
20
- const [currentOperator, setCurrentOperator] = useState(operator);
14
+ export const WfoRangeEditor = ({ handleOnChange, InputElement, value: currentValue }: WfoRangeEditorProps) => {
21
15
  const startValue = currentValue ? currentValue?.toString().split(',') : [];
22
- const [value, setValue] = useState<string[]>(startValue);
16
+ const [value, setValue] = useState<(string | undefined)[]>(startValue);
17
+
18
+ const handleRangeChange = (newValue: string | number | boolean | undefined, rangeIndex: number) => {
19
+ setValue((currentValues) => {
20
+ const next = [...currentValues];
21
+ const isCleared = newValue === undefined || (typeof newValue === 'number' && Number.isNaN(newValue));
22
+ next[rangeIndex] = isCleared ? undefined : String(newValue);
23
+ return next;
24
+ });
25
+ };
23
26
 
27
+ // Notify the parent only when both ends of the range hold a value
24
28
  useEffect(() => {
25
- if (operator !== currentOperator && (operator === 'between' || currentOperator === 'between')) {
26
- setValue([]);
27
- handleOnChange('');
28
- setCurrentOperator(operator);
29
- }
30
- }, [currentOperator, handleOnChange, operator]);
31
-
32
- const handleChange = (value: string | number | undefined, rangeIndex: number = 0) => {
33
- if (operator === 'between') {
34
- setValue((currentDates) => {
35
- // remove value if set to undefined
36
- if (value === null) {
37
- return currentDates.filter((_, index) => index !== rangeIndex);
38
- }
39
- // add value at supplied index
40
- currentDates[rangeIndex] = value as string;
41
-
42
- // call the parent if 2 values are present
43
- if (currentDates.length === 2) {
44
- handleOnChange(`${currentDates[0]},${currentDates[1]}`);
45
- }
46
-
47
- return currentDates;
48
- });
49
- } else {
50
- handleOnChange(value);
29
+ if (value[0] !== undefined && value[1] !== undefined) {
30
+ handleOnChange(`${value[0]},${value[1]}`);
51
31
  }
52
- };
53
-
54
- if (operator === 'between') {
55
- return (
56
- <EuiFlexGroup direction="row" gutterSize="s">
57
- <Element handleOnChange={handleChange} rangeIndex={0} value={value[0]} />
58
- <Element handleOnChange={handleChange} rangeIndex={1} value={value[1]} />
59
- </EuiFlexGroup>
60
- );
61
- }
62
-
63
- return <Element handleOnChange={handleOnChange} value={value[0]} />;
32
+ // handleOnChange comes from react-querybuilder and is not referentially stable
33
+ // eslint-disable-next-line react-hooks/exhaustive-deps
34
+ }, [value]);
35
+
36
+ return (
37
+ <EuiFlexGroup direction="row" gutterSize="s">
38
+ <InputElement
39
+ handleOnChange={(value) => {
40
+ handleRangeChange(value, 0);
41
+ }}
42
+ value={value[0]}
43
+ />
44
+ <InputElement
45
+ handleOnChange={(value) => {
46
+ handleRangeChange(value, 1);
47
+ }}
48
+ value={value[1]}
49
+ />
50
+ </EuiFlexGroup>
51
+ );
64
52
  };
@@ -7,10 +7,15 @@ import { useTranslations } from 'next-intl';
7
7
 
8
8
  import { EuiButtonGroup, EuiDatePicker, EuiFieldNumber, EuiFieldText } from '@elastic/eui';
9
9
 
10
+ import { WfoRangeEditor } from '@/components/WfoTable/WfoStructuredSearchTable/WfoRangeEditor';
10
11
  import { getWfoStructuredSearchTableStyles } from '@/components/WfoTable/WfoStructuredSearchTable/styles';
11
12
  import { useWithOrchestratorTheme } from '@/hooks';
12
13
 
13
- import { WfoRangeEditor, WfoRangeElementProps } from './WfoRangeEditor';
14
+ export interface EditorInputFieldProps<T = string> {
15
+ handleOnChange: ValueEditorProps['handleOnChange'];
16
+ value: T;
17
+ }
18
+ export type EditorComponent = React.ComponentType<EditorInputFieldProps<ValueEditorProps['value']>>;
14
19
 
15
20
  enum UiFieldType {
16
21
  text = 'text',
@@ -19,15 +24,7 @@ enum UiFieldType {
19
24
  datetime = 'datetime',
20
25
  }
21
26
 
22
- export type HandleOnChange<T> = (value: T | undefined, rangeIndex?: number) => void;
23
-
24
- export interface EditorProps<T> {
25
- handleOnChange: HandleOnChange<T>;
26
- value: T;
27
- operator?: string;
28
- }
29
-
30
- const BooleanEditor = ({ handleOnChange, value: currentValue = true }: EditorProps<boolean>) => {
27
+ const BooleanEditor = ({ handleOnChange, value: currentValue = true }: EditorInputFieldProps<boolean>) => {
31
28
  const [value, setValue] = useState<string>(currentValue.toString());
32
29
 
33
30
  useEffect(() => {
@@ -65,7 +62,7 @@ const BooleanEditor = ({ handleOnChange, value: currentValue = true }: EditorPro
65
62
  );
66
63
  };
67
64
 
68
- const TextEditor = ({ handleOnChange, value: currentValue = '' }: EditorProps<string>) => {
65
+ const TextEditor = ({ handleOnChange, value: currentValue = '' }: EditorInputFieldProps<string>) => {
69
66
  const [value, setValue] = useState<string>(currentValue);
70
67
 
71
68
  const handleTextChange: ChangeEventHandler<HTMLInputElement> = (e) => {
@@ -85,7 +82,7 @@ const TextEditor = ({ handleOnChange, value: currentValue = '' }: EditorProps<st
85
82
  return <EuiFieldText value={value} onChange={handleTextChange} onBlur={handleOnBlur} onKeyDown={handleOnKeyDown} />;
86
83
  };
87
84
 
88
- const NumberEditor = ({ handleOnChange, rangeIndex, value: currentValue }: WfoRangeElementProps) => {
85
+ const NumberEditor = ({ handleOnChange, value: currentValue }: EditorInputFieldProps<number>) => {
89
86
  const [value, setValue] = useState<string>(currentValue?.toString() || '');
90
87
 
91
88
  const handleNumberChange: ChangeEventHandler<HTMLInputElement> = (e) => {
@@ -94,7 +91,7 @@ const NumberEditor = ({ handleOnChange, rangeIndex, value: currentValue }: WfoRa
94
91
 
95
92
  const handleOnBlur = () => {
96
93
  const numberValue = parseFloat(value);
97
- handleOnChange(numberValue, rangeIndex);
94
+ handleOnChange(numberValue);
98
95
  };
99
96
 
100
97
  const handleOnKeyDown: KeyboardEventHandler<HTMLInputElement> = (e) => {
@@ -113,7 +110,7 @@ const NumberEditor = ({ handleOnChange, rangeIndex, value: currentValue }: WfoRa
113
110
  );
114
111
  };
115
112
 
116
- const DatePicker = ({ handleOnChange, rangeIndex, value: currentValue }: WfoRangeElementProps) => {
113
+ const DatePicker = ({ handleOnChange, value: currentValue }: EditorInputFieldProps<string>) => {
117
114
  const [date, setDate] = useState<string>(currentValue || '');
118
115
  const t = useTranslations('search.page');
119
116
 
@@ -123,9 +120,8 @@ const DatePicker = ({ handleOnChange, rangeIndex, value: currentValue }: WfoRang
123
120
  onChange={(date) => {
124
121
  const utcDate = date ? moment.utc(date) : undefined;
125
122
  setDate(utcDate?.toISOString() || '');
126
- handleOnChange(utcDate?.toISOString(), rangeIndex);
123
+ handleOnChange(utcDate?.toISOString());
127
124
  }}
128
- id={rangeIndex ? `date-range-${rangeIndex}` : 'date-range'}
129
125
  css={{ width: '330px' }}
130
126
  showTimeSelect
131
127
  dateFormat="yyyy-MM-dd HH:mm"
@@ -145,27 +141,36 @@ export const WfoValueEditor = ({
145
141
  value,
146
142
  className,
147
143
  }: ValueEditorProps) => {
144
+ // For components that don't take a value in addition to an operator - for example where you
145
+ // only choose 'Has component' or 'Does not have component' - the WfoValueEditor should not be rendered.
146
+ // React-query-builder handles this by default by setting the unary constant in the getOperators
147
+ // property of the QueryBuilder component (see WfoFilterBuilder)
148
+ // Because this check might not have run yet when the query is rebuild from an URL
149
+ // we make the check explicitly here aswell.
150
+ if (operator === 'null' || operator === 'notNull') {
151
+ return null;
152
+ }
153
+
154
+ const getComponentByType = (): EditorComponent => {
155
+ if (uiFieldType === UiFieldType.boolean) return BooleanEditor;
156
+ if (uiFieldType === UiFieldType.datetime) return DatePicker;
157
+ if (uiFieldType === UiFieldType.number) return NumberEditor;
158
+ return TextEditor;
159
+ };
160
+
148
161
  const fieldPathInfoMap = context?.fieldPathInfoMap;
149
162
 
150
163
  const fieldInfo = fieldPathInfoMap && fieldPathInfoMap.has(fieldName) ? fieldPathInfoMap.get(fieldName) : undefined;
151
- const uiFieldType = fieldInfo?.ui_types[0] || UiFieldType.text;
164
+ const uiFieldType = fieldInfo?.ui_types?.[0] || UiFieldType.text;
152
165
 
153
166
  const getEditor = () => {
154
- if (uiFieldType === UiFieldType.boolean) {
155
- return <BooleanEditor handleOnChange={handleOnChange} value={value} />;
156
- }
157
-
158
- if (uiFieldType === UiFieldType.datetime) {
159
- return <WfoRangeEditor handleOnChange={handleOnChange} operator={operator} Element={DatePicker} value={value} />;
160
- }
167
+ const InputElement = getComponentByType();
161
168
 
162
- if (uiFieldType === UiFieldType.number) {
163
- return (
164
- <WfoRangeEditor handleOnChange={handleOnChange} operator={operator} Element={NumberEditor} value={value} />
165
- );
169
+ if (operator === 'between') {
170
+ return <WfoRangeEditor handleOnChange={handleOnChange} value={value} InputElement={InputElement} />;
166
171
  }
167
172
 
168
- return <TextEditor handleOnChange={handleOnChange} value={value} />;
173
+ return <InputElement handleOnChange={handleOnChange} value={value} />;
169
174
  };
170
175
 
171
176
  const handleWrapperKeyDown: KeyboardEventHandler<HTMLDivElement> = (event) => {
@@ -1 +1 @@
1
- export const ORCHESTRATOR_UI_LIBRARY_VERSION = '8.7.0';
1
+ export const ORCHESTRATOR_UI_LIBRARY_VERSION = '8.7.2';