@orchestrator-ui/orchestrator-ui-components 8.6.0 → 8.7.1
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/.turbo/turbo-build.log +8 -8
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +15 -12
- package/CHANGELOG.md +18 -0
- package/dist/index.d.ts +416 -1183
- package/dist/index.js +3457 -3072
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/components/WfoPydanticForm/fields/WfoCron.spec.ts +88 -0
- package/src/components/WfoPydanticForm/fields/WfoCron.tsx +233 -0
- package/src/components/WfoPydanticForm/fields/WfoSummary.tsx +42 -7
- package/src/components/WfoPydanticForm/fields/index.ts +1 -0
- package/src/components/WfoPydanticForm/fields/styles.ts +92 -0
- package/src/components/WfoSearchPage/WfoFilterGroup/WfoFilterGroup.tsx +1 -1
- package/src/components/WfoSearchPage/WfoSearchResults/WfoSearchResultItem.tsx +2 -1
- package/src/components/WfoSearchPage/utils.ts +37 -1
- package/src/components/WfoSubscription/WfoInUseByRelations.tsx +6 -2
- package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActionExpandableMenuItem.tsx +19 -14
- package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActions.tsx +13 -1
- package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActionsMenuItem.tsx +3 -0
- package/src/components/WfoSubscription/WfoSubscriptionProductBlock/WfoSubscriptionProductBlock.tsx +1 -1
- package/src/components/WfoSubscriptionsList/WfoSubscriptionsList.tsx +5 -0
- package/src/components/WfoSubscriptionsList/subscriptionResultMappers.ts +3 -1
- package/src/components/WfoTable/WfoAdvancedTable/WfoAdvancedTable.tsx +2 -12
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoExpandingSearchRow.tsx +21 -14
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoFieldSelector.tsx +27 -1
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoFilterBuilder.tsx +97 -91
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoOperatorSelector.tsx +32 -7
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoRuleGroup.tsx +5 -4
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoStructuredSearchTable.tsx +32 -30
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoValueEditor.tsx +41 -11
- package/src/components/WfoTable/WfoStructuredSearchTable/styles.ts +22 -5
- package/src/components/WfoTable/WfoTable/WfoTable.tsx +1 -0
- package/src/components/WfoTable/WfoTable/WfoTableDataRows.tsx +16 -4
- package/src/components/WfoTable/WfoTable/styles.ts +8 -0
- package/src/components/WfoTable/WfoTable/utils.spec.ts +62 -1
- package/src/components/WfoTable/WfoTable/utils.ts +4 -1
- package/src/components/WfoTable/WfoTableSettingsModal/WfoTableSettingsModal.spec.ts +45 -0
- package/src/components/WfoTable/WfoTableSettingsModal/index.ts +1 -0
- package/src/components/WfoTable/WfoTableSettingsModal/utils.ts +17 -0
- package/src/configuration/version.ts +1 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useGetPydanticFormsConfig.tsx +12 -0
- package/src/hooks/useLanguageCode.ts +11 -0
- package/src/messages/en-GB.json +442 -549
- package/src/messages/getTranslationMessages.spec.ts +47 -30
- package/src/messages/nl-NL.json +450 -411
- package/src/pages/WfoSearchPocPage.tsx +14 -5
- package/src/rtk/endpoints/subscriptionList.ts +1 -0
- package/src/theme/baseStyles/formFieldsBaseStyle.ts +0 -1
- package/src/types/search.ts +1 -1
- package/src/types/types.ts +1 -1
- package/src/utils/getDefaultTableConfig.ts +6 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/integer.spec.ts +20 -0
- package/src/utils/integer.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orchestrator-ui/orchestrator-ui-components",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.7.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Library of UI Components used to display the workflow orchestrator frontend",
|
|
6
6
|
"author": {
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
"@monaco-editor/react": "^4.7.0",
|
|
40
40
|
"@rtk-query/graphql-request-base-query": "^2.3.1",
|
|
41
41
|
"@tanstack/react-virtual": "^3.13.24",
|
|
42
|
+
"cron-parser": "^5.6.2",
|
|
43
|
+
"cronstrue": "^3.24.0",
|
|
42
44
|
"graphql-request": "^6.1.0",
|
|
43
45
|
"invariant": "^2.2.4",
|
|
44
46
|
"lodash": "^4.17.21",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { getCronFieldIndexAtCursor, getCronFieldLayout, getCronFieldSelectionRange } from './WfoCron';
|
|
2
|
+
|
|
3
|
+
describe('getCronFieldSelectionRange', () => {
|
|
4
|
+
it('returns the character range of the requested field', () => {
|
|
5
|
+
expect(getCronFieldSelectionRange('5 4 * JAN *', 0)).toEqual([0, 1]);
|
|
6
|
+
expect(getCronFieldSelectionRange('5 4 * JAN *', 1)).toEqual([2, 3]);
|
|
7
|
+
expect(getCronFieldSelectionRange('5 4 * JAN *', 3)).toEqual([6, 9]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('handles extra whitespace', () => {
|
|
11
|
+
expect(getCronFieldSelectionRange(' 5 4 *', 1)).toEqual([6, 7]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('returns null when the field is not present in the expression', () => {
|
|
15
|
+
expect(getCronFieldSelectionRange('5 4', 2)).toEqual(null);
|
|
16
|
+
expect(getCronFieldSelectionRange('', 0)).toEqual(null);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('getCronFieldLayout', () => {
|
|
21
|
+
it('returns the 5 field layout starting with minute for an empty expression', () => {
|
|
22
|
+
expect(getCronFieldLayout('')).toEqual(['minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek']);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns the 5 field layout starting with minute for a 5 field expression', () => {
|
|
26
|
+
expect(getCronFieldLayout('5 4 * JAN *')).toEqual(['minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek']);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns the 6 field layout starting with second for a 6 field expression', () => {
|
|
30
|
+
expect(getCronFieldLayout('0 5 4 * JAN *')).toEqual([
|
|
31
|
+
'second',
|
|
32
|
+
'minute',
|
|
33
|
+
'hour',
|
|
34
|
+
'dayOfMonth',
|
|
35
|
+
'month',
|
|
36
|
+
'dayOfWeek',
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('getCronFieldIndexAtCursor', () => {
|
|
42
|
+
const expression = '*/5 8-17 * JAN mon-fri';
|
|
43
|
+
|
|
44
|
+
it('returns the first field for an empty expression', () => {
|
|
45
|
+
expect(getCronFieldIndexAtCursor('', 0)).toEqual(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns the field the cursor is inside', () => {
|
|
49
|
+
expect(getCronFieldIndexAtCursor(expression, 0)).toEqual(0); // |*/5
|
|
50
|
+
expect(getCronFieldIndexAtCursor(expression, 2)).toEqual(0); // */|5
|
|
51
|
+
expect(getCronFieldIndexAtCursor(expression, 3)).toEqual(0); // */5|
|
|
52
|
+
expect(getCronFieldIndexAtCursor(expression, 5)).toEqual(1); // 8|-17
|
|
53
|
+
expect(getCronFieldIndexAtCursor(expression, 9)).toEqual(2); // |*
|
|
54
|
+
expect(getCronFieldIndexAtCursor(expression, 12)).toEqual(3); // J|AN
|
|
55
|
+
expect(getCronFieldIndexAtCursor(expression, 22)).toEqual(4); // mon-fri|
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns the next field when the cursor is after a separating space', () => {
|
|
59
|
+
expect(getCronFieldIndexAtCursor(expression, 4)).toEqual(1); // */5 |8-17
|
|
60
|
+
expect(getCronFieldIndexAtCursor(expression, 15)).toEqual(4); // JAN |mon-fri
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('handles multiple consecutive spaces', () => {
|
|
64
|
+
expect(getCronFieldIndexAtCursor('* *', 1)).toEqual(0);
|
|
65
|
+
expect(getCronFieldIndexAtCursor('* *', 2)).toEqual(1);
|
|
66
|
+
expect(getCronFieldIndexAtCursor('* *', 3)).toEqual(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('never exceeds the last field of the active layout', () => {
|
|
70
|
+
expect(getCronFieldIndexAtCursor('1 2 3 4 5 ', 10)).toEqual(4);
|
|
71
|
+
expect(getCronFieldIndexAtCursor('* * * * * * extra', 17)).toEqual(5);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('handles leading whitespace', () => {
|
|
75
|
+
expect(getCronFieldIndexAtCursor(' * * * * *', 1)).toEqual(0);
|
|
76
|
+
expect(getCronFieldIndexAtCursor(' * * * * *', 3)).toEqual(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('maps the first field to minute for 5 fields and to second for 6 fields', () => {
|
|
80
|
+
const fiveFieldExpression = '5 4 * JAN *';
|
|
81
|
+
expect(getCronFieldLayout(fiveFieldExpression)[getCronFieldIndexAtCursor(fiveFieldExpression, 1)]).toEqual(
|
|
82
|
+
'minute',
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const sixFieldExpression = '0 5 4 * JAN *';
|
|
86
|
+
expect(getCronFieldLayout(sixFieldExpression)[getCronFieldIndexAtCursor(sixFieldExpression, 1)]).toEqual('second');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import React, { useMemo, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { CronExpressionParser } from 'cron-parser';
|
|
4
|
+
import cronstrue from 'cronstrue/i18n';
|
|
5
|
+
import _ from 'lodash';
|
|
6
|
+
import moment from 'moment';
|
|
7
|
+
import { useTranslations } from 'next-intl';
|
|
8
|
+
import type { PydanticFormControlledElement } from 'pydantic-forms';
|
|
9
|
+
import { getFormFieldIdWithPath } from 'pydantic-forms';
|
|
10
|
+
|
|
11
|
+
import { EuiFieldText, EuiText } from '@elastic/eui';
|
|
12
|
+
|
|
13
|
+
import { useLanguageCode, useWithOrchestratorTheme } from '@/hooks';
|
|
14
|
+
import { getFormFieldsBaseStyle } from '@/theme';
|
|
15
|
+
|
|
16
|
+
import { getWfoCronFieldStyles } from './styles';
|
|
17
|
+
|
|
18
|
+
const CRON_FIELD_KEYS = ['second', 'minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek'] as const;
|
|
19
|
+
|
|
20
|
+
type CronFieldKey = (typeof CRON_FIELD_KEYS)[number];
|
|
21
|
+
|
|
22
|
+
type CronHintRow = {
|
|
23
|
+
symbol: string;
|
|
24
|
+
translationKey: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const SPECIAL_CHARACTER_HINTS: CronHintRow[] = [
|
|
28
|
+
{ symbol: '*', translationKey: 'anyValue' },
|
|
29
|
+
{ symbol: ',', translationKey: 'valueListSeparator' },
|
|
30
|
+
{ symbol: '-', translationKey: 'rangeOfValues' },
|
|
31
|
+
{ symbol: '/', translationKey: 'stepValues' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const CRON_FIELD_HINTS: Record<CronFieldKey, CronHintRow[]> = {
|
|
35
|
+
second: [{ symbol: '0-59', translationKey: 'allowedValues' }],
|
|
36
|
+
minute: [{ symbol: '0-59', translationKey: 'allowedValues' }],
|
|
37
|
+
hour: [{ symbol: '0-23', translationKey: 'allowedValues' }],
|
|
38
|
+
dayOfMonth: [{ symbol: '1-31', translationKey: 'allowedValues' }],
|
|
39
|
+
month: [
|
|
40
|
+
{ symbol: '1-12', translationKey: 'allowedValues' },
|
|
41
|
+
{ symbol: 'JAN-DEC', translationKey: 'alternativeSingleValues' },
|
|
42
|
+
],
|
|
43
|
+
dayOfWeek: [
|
|
44
|
+
{ symbol: '0-6', translationKey: 'allowedValues' },
|
|
45
|
+
{ symbol: 'SUN-SAT', translationKey: 'alternativeSingleValues' },
|
|
46
|
+
{ symbol: '7', translationKey: 'sundayNonStandard' },
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// cron-parser and cronstrue treat a 5 field expression as starting with minute;
|
|
51
|
+
// only with 6 fields does the first field become second
|
|
52
|
+
export const getCronFieldLayout = (expression: string): readonly CronFieldKey[] => {
|
|
53
|
+
const fieldCount = expression.trim().split(/\s+/).filter(Boolean).length;
|
|
54
|
+
return fieldCount >= CRON_FIELD_KEYS.length ? CRON_FIELD_KEYS : CRON_FIELD_KEYS.slice(1);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const getCronFieldIndexAtCursor = (expression: string, cursorPosition: number): number => {
|
|
58
|
+
const textBeforeCursor = expression.slice(0, cursorPosition);
|
|
59
|
+
const fieldsBeforeCursor = textBeforeCursor.split(/\s+/).filter(Boolean);
|
|
60
|
+
const cursorIsBetweenFields = /\s$/.test(textBeforeCursor) || fieldsBeforeCursor.length === 0;
|
|
61
|
+
const fieldIndex = cursorIsBetweenFields ? fieldsBeforeCursor.length : fieldsBeforeCursor.length - 1;
|
|
62
|
+
return Math.min(fieldIndex, getCronFieldLayout(expression).length - 1);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const getCronFieldSelectionRange = (expression: string, fieldIndex: number): [number, number] | null => {
|
|
66
|
+
const fieldPattern = /\S+/g;
|
|
67
|
+
let match = fieldPattern.exec(expression);
|
|
68
|
+
let index = 0;
|
|
69
|
+
while (match) {
|
|
70
|
+
if (index === fieldIndex) {
|
|
71
|
+
return [match.index, match.index + match[0].length];
|
|
72
|
+
}
|
|
73
|
+
index++;
|
|
74
|
+
match = fieldPattern.exec(expression);
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const WfoCron: PydanticFormControlledElement = ({ onChange, value, disabled, pydanticFormField }) => {
|
|
80
|
+
const { formFieldBaseStyle } = useWithOrchestratorTheme(getFormFieldsBaseStyle);
|
|
81
|
+
const {
|
|
82
|
+
cronFieldWrapperStyle,
|
|
83
|
+
cronPossibleValuesStyle,
|
|
84
|
+
cronLegendStyle,
|
|
85
|
+
cronLegendItemStyle,
|
|
86
|
+
cronActiveLegendItemStyle,
|
|
87
|
+
cronInactiveLegendItemStyle,
|
|
88
|
+
cronDescriptionStyle,
|
|
89
|
+
cronHintStyle,
|
|
90
|
+
cronHintListStyle,
|
|
91
|
+
cronHintSymbolStyle,
|
|
92
|
+
cronErrorStyle,
|
|
93
|
+
cronDescriptionContainerStyle,
|
|
94
|
+
} = useWithOrchestratorTheme(getWfoCronFieldStyles);
|
|
95
|
+
const t = useTranslations('pydanticForms.widgets.cron');
|
|
96
|
+
const cronstrueLocale = useLanguageCode();
|
|
97
|
+
const [activeFieldIndex, setActiveFieldIndex] = useState<number | null>(null);
|
|
98
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
99
|
+
|
|
100
|
+
// If the field is part of an array the value is passed in as an object with the field name as key
|
|
101
|
+
// this is imposed by react-hook-form. We try to detect this and extract the actual value
|
|
102
|
+
const fieldName = getFormFieldIdWithPath(pydanticFormField.id);
|
|
103
|
+
const fieldValue = _.isObject(value) && _.has(value, fieldName) ? _.get(value, fieldName) : value;
|
|
104
|
+
|
|
105
|
+
const { description, parseError, nextOccurrences } = useMemo(() => {
|
|
106
|
+
const expression = typeof fieldValue === 'string' ? fieldValue.trim() : '';
|
|
107
|
+
if (!expression) {
|
|
108
|
+
return { description: undefined, parseError: undefined, nextOccurrences: [] };
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
// cron-parser validates the expression, cronstrue turns it into a human readable description
|
|
112
|
+
const cronExpression = CronExpressionParser.parse(expression);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
description: cronstrue.toString(expression, {
|
|
116
|
+
locale: cronstrueLocale,
|
|
117
|
+
throwExceptionOnParseError: true,
|
|
118
|
+
}),
|
|
119
|
+
parseError: undefined,
|
|
120
|
+
nextOccurrences: cronExpression.take(1).map((cronDate) => cronDate.toDate()),
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
description: undefined,
|
|
125
|
+
parseError: error instanceof Error ? error.message : String(error),
|
|
126
|
+
nextOccurrences: [],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}, [fieldValue, cronstrueLocale]);
|
|
130
|
+
|
|
131
|
+
const updateActiveField = (event: React.SyntheticEvent<HTMLInputElement>) => {
|
|
132
|
+
const { selectionStart, value: expression } = event.currentTarget;
|
|
133
|
+
setActiveFieldIndex(selectionStart === null ? null : getCronFieldIndexAtCursor(expression, selectionStart));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const fieldLayout = getCronFieldLayout(typeof fieldValue === 'string' ? fieldValue : '');
|
|
137
|
+
const activeFieldKey =
|
|
138
|
+
activeFieldIndex !== null ? fieldLayout[Math.min(activeFieldIndex, fieldLayout.length - 1)] : null;
|
|
139
|
+
const hintFieldKey = activeFieldKey ?? fieldLayout[0];
|
|
140
|
+
|
|
141
|
+
const focusCronField = (fieldKey: CronFieldKey) => {
|
|
142
|
+
const input = inputRef.current;
|
|
143
|
+
const fieldIndex = fieldLayout.indexOf(fieldKey);
|
|
144
|
+
// The second field is not part of the layout when the expression has 5 fields
|
|
145
|
+
if (!input || fieldIndex === -1) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const expression = typeof fieldValue === 'string' ? fieldValue : '';
|
|
149
|
+
input.focus();
|
|
150
|
+
const selectionRange = getCronFieldSelectionRange(expression, fieldIndex);
|
|
151
|
+
if (selectionRange) {
|
|
152
|
+
input.setSelectionRange(selectionRange[0], selectionRange[1]);
|
|
153
|
+
} else {
|
|
154
|
+
// The selection can't be found so the caret is placed at the end of the selection while maintaining focus on the field
|
|
155
|
+
input.setSelectionRange(expression.length, expression.length);
|
|
156
|
+
}
|
|
157
|
+
setActiveFieldIndex(fieldIndex);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div css={cronFieldWrapperStyle}>
|
|
162
|
+
<EuiFieldText
|
|
163
|
+
data-testid={pydanticFormField.id}
|
|
164
|
+
css={formFieldBaseStyle}
|
|
165
|
+
inputRef={inputRef}
|
|
166
|
+
disabled={disabled}
|
|
167
|
+
isInvalid={!!parseError}
|
|
168
|
+
placeholder="* * * * *"
|
|
169
|
+
onChange={(event) => {
|
|
170
|
+
onChange(event.target.value);
|
|
171
|
+
}}
|
|
172
|
+
onSelect={updateActiveField}
|
|
173
|
+
onFocus={updateActiveField}
|
|
174
|
+
onBlur={() => setActiveFieldIndex(null)}
|
|
175
|
+
value={fieldValue}
|
|
176
|
+
fullWidth
|
|
177
|
+
/>
|
|
178
|
+
<div css={[cronLegendStyle]}>
|
|
179
|
+
{CRON_FIELD_KEYS.map((fieldKey) => {
|
|
180
|
+
const isInLayout = fieldLayout.includes(fieldKey);
|
|
181
|
+
return (
|
|
182
|
+
<button
|
|
183
|
+
key={fieldKey}
|
|
184
|
+
type="button"
|
|
185
|
+
disabled={disabled || !isInLayout}
|
|
186
|
+
aria-pressed={fieldKey === activeFieldKey}
|
|
187
|
+
onClick={() => focusCronField(fieldKey)}
|
|
188
|
+
css={[
|
|
189
|
+
cronLegendItemStyle,
|
|
190
|
+
fieldKey === activeFieldKey && cronActiveLegendItemStyle,
|
|
191
|
+
!isInLayout && cronInactiveLegendItemStyle,
|
|
192
|
+
]}
|
|
193
|
+
>
|
|
194
|
+
{t(fieldKey)}
|
|
195
|
+
</button>
|
|
196
|
+
);
|
|
197
|
+
})}
|
|
198
|
+
</div>
|
|
199
|
+
{hintFieldKey && (
|
|
200
|
+
<>
|
|
201
|
+
<div css={cronPossibleValuesStyle}>{t('possibleValues')}</div>
|
|
202
|
+
<div css={cronHintListStyle}>
|
|
203
|
+
{[...SPECIAL_CHARACTER_HINTS, ...CRON_FIELD_HINTS[hintFieldKey]].map(({ symbol, translationKey }) => (
|
|
204
|
+
<React.Fragment key={symbol}>
|
|
205
|
+
<span css={cronHintSymbolStyle}>{symbol}</span>
|
|
206
|
+
<span>{t(translationKey)}</span>
|
|
207
|
+
</React.Fragment>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
</>
|
|
211
|
+
)}
|
|
212
|
+
{parseError && (
|
|
213
|
+
<EuiText size="s" css={cronErrorStyle}>
|
|
214
|
+
{t('invalidExpression', { error: parseError })}
|
|
215
|
+
</EuiText>
|
|
216
|
+
)}
|
|
217
|
+
{description && (
|
|
218
|
+
<div css={cronDescriptionContainerStyle}>
|
|
219
|
+
<EuiText size="s" css={cronDescriptionStyle}>
|
|
220
|
+
{description}
|
|
221
|
+
</EuiText>
|
|
222
|
+
{nextOccurrences.length > 0 && (
|
|
223
|
+
<EuiText size="xs" css={cronHintStyle}>
|
|
224
|
+
{t('nextOccurrences', {
|
|
225
|
+
dates: nextOccurrences.map((date) => moment(date).format('YYYY-MM-DD HH:mm:ss')).join(', '),
|
|
226
|
+
})}
|
|
227
|
+
</EuiText>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
3
|
import { capitalize } from 'lodash';
|
|
4
|
-
import
|
|
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
|
|
27
|
-
const labels = summaryData?.labels
|
|
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 =
|
|
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]}>
|
|
@@ -14,6 +14,98 @@ export const getWfoObjectFieldStyles = () => {
|
|
|
14
14
|
};
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
export const getWfoCronFieldStyles = ({ theme }: WfoThemeHelpers) => {
|
|
18
|
+
const cronFieldWrapperStyle = css({
|
|
19
|
+
width: '100%',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const cronLegendStyle = css({
|
|
23
|
+
display: 'flex',
|
|
24
|
+
flexWrap: 'wrap',
|
|
25
|
+
gap: theme.size.xs,
|
|
26
|
+
marginTop: theme.size.s,
|
|
27
|
+
fontSize: theme.size.m,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const cronLegendItemStyle = css({
|
|
31
|
+
color: theme.colors.textSubdued,
|
|
32
|
+
padding: `0 ${theme.size.xs}`,
|
|
33
|
+
borderRadius: theme.border.radius.small,
|
|
34
|
+
border: 'none',
|
|
35
|
+
backgroundColor: 'transparent',
|
|
36
|
+
font: 'inherit',
|
|
37
|
+
cursor: 'pointer',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const cronPossibleValuesStyle = css({
|
|
41
|
+
marginTop: theme.size.m,
|
|
42
|
+
marginLeft: theme.size.m,
|
|
43
|
+
fontSize: theme.size.m,
|
|
44
|
+
color: theme.colors.textSubdued,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const cronActiveLegendItemStyle = css({
|
|
48
|
+
color: theme.colors.textPrimary,
|
|
49
|
+
backgroundColor: theme.colors.backgroundBasePrimary,
|
|
50
|
+
fontWeight: theme.font.weight.bold,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const cronInactiveLegendItemStyle = css({
|
|
54
|
+
color: theme.colors.textDisabled,
|
|
55
|
+
cursor: 'default',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const cronDescriptionStyle = css({
|
|
59
|
+
color: theme.colors.textParagraph,
|
|
60
|
+
fontWeight: theme.font.weight.semiBold,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const cronHintStyle = css({
|
|
64
|
+
marginTop: theme.size.xs,
|
|
65
|
+
color: theme.colors.textSubdued,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const cronHintListStyle = css({
|
|
69
|
+
display: 'grid',
|
|
70
|
+
gridTemplateColumns: 'max-content 1fr',
|
|
71
|
+
columnGap: theme.size.m,
|
|
72
|
+
rowGap: theme.size.xxs,
|
|
73
|
+
margin: theme.size.m,
|
|
74
|
+
marginTop: theme.size.xs,
|
|
75
|
+
fontSize: theme.size.m,
|
|
76
|
+
color: theme.colors.textSubdued,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const cronHintSymbolStyle = css({
|
|
80
|
+
fontFamily: theme.font.familyCode,
|
|
81
|
+
color: theme.colors.textPrimary,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const cronErrorStyle = css({
|
|
85
|
+
marginTop: theme.size.xs,
|
|
86
|
+
color: theme.colors.textDanger,
|
|
87
|
+
});
|
|
88
|
+
const cronDescriptionContainerStyle = css({
|
|
89
|
+
border: `thin dotted ${theme.colors.borderBasePlain}`,
|
|
90
|
+
borderRadius: theme.border.radius.small,
|
|
91
|
+
padding: theme.size.m,
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
cronFieldWrapperStyle,
|
|
95
|
+
cronLegendStyle,
|
|
96
|
+
cronLegendItemStyle,
|
|
97
|
+
cronActiveLegendItemStyle,
|
|
98
|
+
cronInactiveLegendItemStyle,
|
|
99
|
+
cronDescriptionStyle,
|
|
100
|
+
cronHintStyle,
|
|
101
|
+
cronHintListStyle,
|
|
102
|
+
cronHintSymbolStyle,
|
|
103
|
+
cronErrorStyle,
|
|
104
|
+
cronPossibleValuesStyle,
|
|
105
|
+
cronDescriptionContainerStyle,
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
17
109
|
export const getCommonFormFieldStyles = ({ theme }: WfoThemeHelpers) => {
|
|
18
110
|
const formRowStyle = css({
|
|
19
111
|
marginBottom: theme.base * 2,
|
|
@@ -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('
|
|
116
|
+
{t('addRule')}
|
|
117
117
|
</EuiButton>
|
|
118
118
|
</EuiFlexItem>
|
|
119
119
|
<EuiFlexItem grow={false}>
|
|
@@ -28,7 +28,8 @@ export const WfoSearchResultItem: FC<WfoSearchResultItemProps> = ({
|
|
|
28
28
|
index,
|
|
29
29
|
}) => {
|
|
30
30
|
const t = useTranslations('search.page');
|
|
31
|
-
const
|
|
31
|
+
const matchingFields = result.matching_fields;
|
|
32
|
+
const matchingField = matchingFields?.[0];
|
|
32
33
|
const { theme } = useOrchestratorTheme();
|
|
33
34
|
const baseUrl = `${window.location.protocol}//${window.location.host}`;
|
|
34
35
|
const detailUrl = getDetailUrl(result, baseUrl);
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
Condition,
|
|
8
8
|
EntityKind,
|
|
9
9
|
Filter,
|
|
10
|
+
MatchingField,
|
|
10
11
|
OperatorDisplay,
|
|
11
12
|
PathInfo,
|
|
12
13
|
RetrieverType,
|
|
@@ -194,7 +195,42 @@ const buildSubscriptionStatusFilter = (tab: WfoSubscriptionListTab) => {
|
|
|
194
195
|
};
|
|
195
196
|
};
|
|
196
197
|
|
|
197
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Search-API field path holding the status of each entity kind. Only the SUBSCRIPTION path is in
|
|
200
|
+
* use; the others follow the same convention but should be verified against the backend paths
|
|
201
|
+
* endpoint before relying on them.
|
|
202
|
+
*/
|
|
203
|
+
const STATUS_FIELD_PATHS: Record<EntityKind, string> = {
|
|
204
|
+
[EntityKind.SUBSCRIPTION]: 'subscription.status',
|
|
205
|
+
[EntityKind.PROCESS]: 'process.last_status',
|
|
206
|
+
[EntityKind.PRODUCT]: 'product.status',
|
|
207
|
+
[EntityKind.WORKFLOW]: 'workflow.status',
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The tab implicitly adds a status filter to the search (see addStatusFilterFromTab). The backend
|
|
212
|
+
* reports every matched filter in matching_fields, so those implicit matches are removed here to
|
|
213
|
+
* only show the user matches for filters they provided themselves. Matching on path AND value:
|
|
214
|
+
* a status match that cannot come from the tab filter is kept.
|
|
215
|
+
*/
|
|
216
|
+
export const removeTabStatusMatchingFields = (
|
|
217
|
+
matchingFields: MatchingField[] | null | undefined,
|
|
218
|
+
tab: WfoSubscriptionListTab,
|
|
219
|
+
entityKind: EntityKind,
|
|
220
|
+
): MatchingField[] => {
|
|
221
|
+
const statusFieldPath = STATUS_FIELD_PATHS[entityKind];
|
|
222
|
+
const tabStatuses = getSubscriptionStatusesFromTab(tab);
|
|
223
|
+
return (
|
|
224
|
+
matchingFields?.filter((field) => {
|
|
225
|
+
const isTabStatusMatch =
|
|
226
|
+
field.path === statusFieldPath
|
|
227
|
+
&& tabStatuses.some((status) => status.toLowerCase() === field.text.toLowerCase());
|
|
228
|
+
return !isTabStatusMatch;
|
|
229
|
+
}) ?? []
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export const addStatusFilterFromTab = (ruleGroup: RuleGroupType | false | undefined, tab: WfoSubscriptionListTab) => {
|
|
198
234
|
const userRuleGroup = ruleGroup === false ? undefined : ruleGroup;
|
|
199
235
|
const ruleGroups = [buildSubscriptionStatusFilter(tab), userRuleGroup].filter(Boolean) as RuleGroupType[];
|
|
200
236
|
|
|
@@ -14,9 +14,13 @@ import { getSubscriptionDetailStyles } from './styles';
|
|
|
14
14
|
|
|
15
15
|
interface WfoInUseByRelationsProps {
|
|
16
16
|
inUseByRelations: InUseByRelation[];
|
|
17
|
+
subscriptionPath?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
export const WfoInUseByRelations = ({
|
|
20
|
+
export const WfoInUseByRelations = ({
|
|
21
|
+
inUseByRelations,
|
|
22
|
+
subscriptionPath = PATH_SUBSCRIPTIONS,
|
|
23
|
+
}: WfoInUseByRelationsProps) => {
|
|
20
24
|
const t = useTranslations('subscriptions.detail');
|
|
21
25
|
const { inUseByRelationDetailsStyle } = useWithOrchestratorTheme(getSubscriptionDetailStyles);
|
|
22
26
|
const subscriptionIds = inUseByRelations.map((relation) => relation.subscription_id).join('|');
|
|
@@ -42,7 +46,7 @@ export const WfoInUseByRelations = ({ inUseByRelations }: WfoInUseByRelationsPro
|
|
|
42
46
|
{
|
|
43
47
|
key: t('description'),
|
|
44
48
|
value: (
|
|
45
|
-
<Link href={`${
|
|
49
|
+
<Link href={`${subscriptionPath}/${inUseByRelationDetails.subscriptionId}`} target="_blank">
|
|
46
50
|
{inUseByRelationDetails.description}
|
|
47
51
|
</Link>
|
|
48
52
|
),
|