@orchestrator-ui/orchestrator-ui-components 8.8.2 → 8.9.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 +7 -7
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +14 -13
- package/CHANGELOG.md +17 -0
- package/dist/index.d.ts +48 -26
- package/dist/index.js +2202 -2081
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/WfoBadges/WfoProductStatusBadge/WfoProductStatusBadge.tsx +2 -5
- package/src/components/WfoStartButton/WfoStartButtonComboBox.tsx +6 -11
- package/src/components/WfoStartButton/WfoStartWorkflowComboBox.tsx +1 -1
- package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActions.tsx +6 -16
- package/src/components/WfoSubscription/utils/utils.spec.ts +57 -0
- package/src/components/WfoSubscription/utils/utils.ts +15 -0
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoSearchFieldWithActions.tsx +9 -9
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoSearchHelpModal.tsx +59 -0
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoStructuredSearchTable.tsx +13 -27
- package/src/configuration/version.ts +1 -1
- package/src/messages/en-GB.json +19 -0
- package/src/messages/nl-NL.json +20 -1
- package/src/pages/WfoSearchPocPage.tsx +68 -36
- package/src/rtk/endpoints/metadata/productBlocks.ts +9 -1
- package/src/rtk/endpoints/metadata/products.ts +5 -2
- package/src/rtk/endpoints/startOptions.ts +4 -3
- package/src/types/types.ts +3 -3
- package/src/utils/getProductLifecycleStatus.spec.ts +26 -0
- package/src/utils/getProductLifecycleStatus.ts +13 -0
- package/src/utils/index.ts +1 -0
|
@@ -149,11 +149,12 @@ export const WfoSearchPocPage = () => {
|
|
|
149
149
|
const getStoredTableConfig = useStoredTableConfig<SubscriptionListItem>(SEARCH_TABLE_LOCAL_STORAGE_KEY);
|
|
150
150
|
const [retrieverType, setRetrieverType] = useState<RetrieverType>(RetrieverType.Auto);
|
|
151
151
|
|
|
152
|
-
// Part of the search endpoint payload that is passed in the
|
|
153
|
-
const [
|
|
152
|
+
// Part of the search endpoint payload that is passed in the queryString parameter
|
|
153
|
+
const [queryString, setQueryString] = useState<string>('');
|
|
154
|
+
|
|
154
155
|
// The committed query and filter live in the URL so a link reproduces the search and browser
|
|
155
156
|
// back/forward re-runs it. The filter is stored as a CEL string and parsed back to a rule group.
|
|
156
|
-
const [
|
|
157
|
+
const [committedQueryString, setCommittedQueryString] = useQueryParam('queryString', withDefault(StringParam, ''));
|
|
157
158
|
const [committedFilterString, setCommittedFilterString] = useQueryParam('filterString', withDefault(StringParam, ''));
|
|
158
159
|
// Track the last value this page committed, so the URL->state sync effects below only rebuild the
|
|
159
160
|
// inputs for external changes (page load, back/forward). Rebuilding on own commits would revert
|
|
@@ -165,10 +166,10 @@ export const WfoSearchPocPage = () => {
|
|
|
165
166
|
lastSelfCommittedFilter.current = celString;
|
|
166
167
|
setCommittedFilterString(celString || undefined);
|
|
167
168
|
};
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
const lastSelfCommittedQueryString = useRef('');
|
|
170
|
+
const commitQueryString = (queryString: string) => {
|
|
171
|
+
lastSelfCommittedQueryString.current = queryString;
|
|
172
|
+
setCommittedQueryString(queryString || undefined);
|
|
172
173
|
};
|
|
173
174
|
|
|
174
175
|
// String that is displayed in the filter textarea. This is transformed and if valid passed to the search endpoint in the filter parameter
|
|
@@ -179,6 +180,9 @@ export const WfoSearchPocPage = () => {
|
|
|
179
180
|
[committedFilterString],
|
|
180
181
|
);
|
|
181
182
|
const [isValidFilterString, setIsValidFilterString] = useState<boolean>(true);
|
|
183
|
+
// Set when a commit attempt (Apply filter, or enter in the search bar) is refused because the
|
|
184
|
+
// filter draft is invalid; used to hide the search results — see isSearchBlocked below.
|
|
185
|
+
const [isCommitRefused, setIsCommitRefused] = useState<boolean>(false);
|
|
182
186
|
const [tableDefaults, setTableDefaults] = useState<StoredTableConfig<SubscriptionListItem>>();
|
|
183
187
|
const [pageSize, setPageSize] = useState<number>(DEFAULT_PAGE_SIZE);
|
|
184
188
|
const [pageCursor, setPageCursor] = useState<{ cursor: string; searchKey: string } | undefined>(undefined);
|
|
@@ -192,7 +196,7 @@ export const WfoSearchPocPage = () => {
|
|
|
192
196
|
// the queryString/filterString/activeTab URL params: the key no longer matches, so the stale
|
|
193
197
|
// cursor is not sent along with the new search.
|
|
194
198
|
const committedSearchKey = JSON.stringify([
|
|
195
|
-
|
|
199
|
+
committedQueryString,
|
|
196
200
|
committedFilterString,
|
|
197
201
|
selectedTab,
|
|
198
202
|
retrieverType,
|
|
@@ -213,7 +217,7 @@ export const WfoSearchPocPage = () => {
|
|
|
213
217
|
direction: dataSorting.sortOrder.toLowerCase(),
|
|
214
218
|
};
|
|
215
219
|
return {
|
|
216
|
-
query:
|
|
220
|
+
query: committedQueryString,
|
|
217
221
|
limit: pageSize,
|
|
218
222
|
entity_type: EntityKind.SUBSCRIPTION,
|
|
219
223
|
response_columns: Array.from(resultColumToPropertyMap.keys()),
|
|
@@ -222,7 +226,7 @@ export const WfoSearchPocPage = () => {
|
|
|
222
226
|
...(filters && { filters }),
|
|
223
227
|
...(cursor && { cursor }),
|
|
224
228
|
};
|
|
225
|
-
}, [
|
|
229
|
+
}, [committedQueryString, committedRuleGroup, selectedTab, retrieverType, pageSize, dataSorting, cursor]);
|
|
226
230
|
|
|
227
231
|
const { data, isFetching } = useSearchQuery(searchPayload);
|
|
228
232
|
|
|
@@ -335,7 +339,7 @@ export const WfoSearchPocPage = () => {
|
|
|
335
339
|
};
|
|
336
340
|
|
|
337
341
|
const sortableAndFilterableFieldNames = Object.keys(tableColumnConfig).filter((fieldName) => fieldName !== 'actions');
|
|
338
|
-
const isSortingAllowed =
|
|
342
|
+
const isSortingAllowed = queryString === '';
|
|
339
343
|
const tableColumnConfigWithSortingAndFiltering =
|
|
340
344
|
mapSortableAndFilterableValuesToTableColumnConfig<SubscriptionListItem>(
|
|
341
345
|
tableColumnConfig,
|
|
@@ -343,33 +347,56 @@ export const WfoSearchPocPage = () => {
|
|
|
343
347
|
sortableAndFilterableFieldNames,
|
|
344
348
|
);
|
|
345
349
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
350
|
+
// Formats the given rule group to CEL and commits it. Returns false without committing when
|
|
351
|
+
// the CEL would not survive the URL round trip: formatQuery escapes double quotes in values
|
|
352
|
+
// but parseCEL has no escape support, so such a filter would silently be dropped after
|
|
353
|
+
// committing. Refusing keeps the URL and the search results consistent.
|
|
354
|
+
const commitFilterDraft = (effectiveRuleGroup: RuleGroupType | undefined): boolean => {
|
|
351
355
|
// '' (no rule group, or only placeholder rules) commits an empty filter, clearing the URL param.
|
|
352
356
|
const celQuery =
|
|
353
357
|
effectiveRuleGroup ? formatQuery(effectiveRuleGroup, { format: 'cel', fallbackExpression: '' }) : '';
|
|
354
|
-
// A non-empty CEL string must survive the round trip through the URL: formatQuery escapes double
|
|
355
|
-
// quotes in values but parseCEL has no escape support, so such a filter would silently be dropped
|
|
356
|
-
// after committing. Refuse the commit and flag the filter instead, keeping the URL and the search
|
|
357
|
-
// results consistent.
|
|
358
358
|
if (celQuery && !parseCelToRuleGroup(celQuery)) {
|
|
359
359
|
setIsValidFilterString(false);
|
|
360
|
-
|
|
360
|
+
setIsCommitRefused(true);
|
|
361
|
+
return false;
|
|
361
362
|
}
|
|
362
363
|
commitFilterString(celQuery);
|
|
364
|
+
setIsCommitRefused(false);
|
|
365
|
+
return true;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const handleApplyFilter = (searchParams?: SearchParams) => {
|
|
369
|
+
const ruleGroupParam = searchParams?.ruleGroup;
|
|
370
|
+
// Use an explicitly passed rule group when provided (e.g. a column-header search), a cleared filter
|
|
371
|
+
// when `false`, and otherwise the current query builder state (the "Apply filter" button).
|
|
372
|
+
const effectiveRuleGroup = ruleGroupParam === false ? undefined : (ruleGroupParam ?? queryBuilderRuleGroup);
|
|
373
|
+
if (!commitFilterDraft(effectiveRuleGroup)) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
// Also commit the draft search text, so applying a filter picks up text typed in the
|
|
377
|
+
// search bar without pressing enter.
|
|
378
|
+
commitQueryString(queryString);
|
|
363
379
|
setPageCursor(undefined);
|
|
364
380
|
};
|
|
365
381
|
|
|
366
|
-
const
|
|
367
|
-
|
|
382
|
+
const onChangeQueryString = (queryString: string) => {
|
|
383
|
+
setQueryString(queryString);
|
|
368
384
|
};
|
|
369
385
|
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
386
|
+
const onSearchQueryString = (queryString: string) => {
|
|
387
|
+
setQueryString(queryString);
|
|
388
|
+
// Mirror handleApplyFilter: searching also commits the pending filter draft, and an invalid
|
|
389
|
+
// draft refuses the whole search — nothing commits and the results are hidden until the
|
|
390
|
+
// draft is fixed. The isValidFilterString check guards the textarea state, which the
|
|
391
|
+
// queryBuilderRuleGroup (holding the last *valid* parse) does not reflect while invalid.
|
|
392
|
+
if (!isValidFilterString) {
|
|
393
|
+
setIsCommitRefused(true);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (!commitFilterDraft(queryBuilderRuleGroup)) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
commitQueryString(queryString);
|
|
373
400
|
setPageCursor(undefined);
|
|
374
401
|
};
|
|
375
402
|
|
|
@@ -405,12 +432,12 @@ export const WfoSearchPocPage = () => {
|
|
|
405
432
|
// back/forward navigation changes the committed search. Commits made by this page are skipped —
|
|
406
433
|
// see the lastSelfCommitted refs above.
|
|
407
434
|
useEffect(() => {
|
|
408
|
-
if (
|
|
435
|
+
if (committedQueryString === lastSelfCommittedQueryString.current) {
|
|
409
436
|
return;
|
|
410
437
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}, [
|
|
438
|
+
lastSelfCommittedQueryString.current = committedQueryString;
|
|
439
|
+
setQueryString(committedQueryString);
|
|
440
|
+
}, [committedQueryString]);
|
|
414
441
|
|
|
415
442
|
useEffect(() => {
|
|
416
443
|
if (committedFilterString === lastSelfCommittedFilter.current) {
|
|
@@ -455,13 +482,18 @@ export const WfoSearchPocPage = () => {
|
|
|
455
482
|
safeCelParse(filterString);
|
|
456
483
|
};
|
|
457
484
|
|
|
485
|
+
// A refused commit hides the search results for as long as the filter draft stays invalid:
|
|
486
|
+
// showing them would suggest the attempted search ran. Editing the draft back to valid CEL
|
|
487
|
+
// (or a later successful commit) lifts the block.
|
|
488
|
+
const isSearchBlocked = isCommitRefused && !isValidFilterString;
|
|
489
|
+
|
|
458
490
|
const { items: subscriptionListItems, rowExpandingConfiguration } =
|
|
459
|
-
data ?
|
|
491
|
+
data && !isSearchBlocked ?
|
|
460
492
|
getDataFromResponse<SubscriptionListItem>(data, resultColumToPropertyMap, 'subscriptionId', selectedTab)
|
|
461
493
|
: { items: [] };
|
|
462
494
|
|
|
463
|
-
const totalItems = getTotalItemsFromResponse(data);
|
|
464
|
-
const hasNextPage = data?.page_info?.has_next_page ?? false;
|
|
495
|
+
const totalItems = !isSearchBlocked && getTotalItemsFromResponse(data);
|
|
496
|
+
const hasNextPage = !isSearchBlocked && (data?.page_info?.has_next_page ?? false);
|
|
465
497
|
const nextPageCursor = data?.page_info?.next_page_cursor ?? undefined;
|
|
466
498
|
|
|
467
499
|
const exportData = async () => {
|
|
@@ -521,12 +553,12 @@ export const WfoSearchPocPage = () => {
|
|
|
521
553
|
localStorageKey={SEARCH_TABLE_LOCAL_STORAGE_KEY}
|
|
522
554
|
onUpdateFilterString={onUpdateFilterString}
|
|
523
555
|
onUpdateQueryBuilder={onUpdateQueryBuilder}
|
|
524
|
-
|
|
525
|
-
|
|
556
|
+
onChangeQueryString={onChangeQueryString}
|
|
557
|
+
onSearchQueryString={onSearchQueryString}
|
|
526
558
|
onShowMore={onShowMore}
|
|
527
559
|
onUpdateRetrieverType={onUpdateRetrieverType}
|
|
528
560
|
queryBuilderRuleGroup={queryBuilderRuleGroup}
|
|
529
|
-
|
|
561
|
+
queryString={queryString}
|
|
530
562
|
retrieverType={retrieverType}
|
|
531
563
|
tableColumnConfig={tableColumnConfigWithSortingAndFiltering}
|
|
532
564
|
getColumnSearchFieldName={(field) => getKeyByValueFromMap(resultColumToPropertyMap, field)}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ProductBlockDefinition,
|
|
8
8
|
ProductBlockDefinitionsResult,
|
|
9
9
|
} from '@/types';
|
|
10
|
+
import { getProductLifecycleStatus } from '@/utils';
|
|
10
11
|
|
|
11
12
|
export const productBlocksQuery = `
|
|
12
13
|
query MetadataProductBlocks(
|
|
@@ -69,7 +70,14 @@ const productBlocksApi = orchestratorApi.injectEndpoints({
|
|
|
69
70
|
variables,
|
|
70
71
|
}),
|
|
71
72
|
transformResponse: (response: ProductBlockDefinitionsResult): ProductBlocksResponse => {
|
|
72
|
-
const productBlocks = response.productBlocks.page || []
|
|
73
|
+
const productBlocks = (response.productBlocks.page || []).map((productBlock) => ({
|
|
74
|
+
...productBlock,
|
|
75
|
+
status: getProductLifecycleStatus(productBlock.status),
|
|
76
|
+
dependsOn: productBlock.dependsOn.map((dependsOnBlock) => ({
|
|
77
|
+
...dependsOnBlock,
|
|
78
|
+
status: getProductLifecycleStatus(dependsOnBlock.status),
|
|
79
|
+
})),
|
|
80
|
+
}));
|
|
73
81
|
const pageInfo = response.productBlocks.pageInfo || {};
|
|
74
82
|
|
|
75
83
|
return {
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
ProductDefinition,
|
|
10
10
|
ProductDefinitionsResult,
|
|
11
11
|
} from '@/types';
|
|
12
|
-
import { getCacheTag } from '@/utils';
|
|
12
|
+
import { getCacheTag, getProductLifecycleStatus } from '@/utils';
|
|
13
13
|
|
|
14
14
|
export const products = `
|
|
15
15
|
query MetadataProducts(
|
|
@@ -61,7 +61,10 @@ const productsApi = orchestratorApi.injectEndpoints({
|
|
|
61
61
|
variables,
|
|
62
62
|
}),
|
|
63
63
|
transformResponse: (response: ProductDefinitionsResult): ProductsResponse => {
|
|
64
|
-
const products = response.products.page || []
|
|
64
|
+
const products = (response.products.page || []).map((product) => ({
|
|
65
|
+
...product,
|
|
66
|
+
status: getProductLifecycleStatus(product.status),
|
|
67
|
+
}));
|
|
65
68
|
const pageInfo = response.products.pageInfo || {};
|
|
66
69
|
|
|
67
70
|
return {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
WorkflowDefinition,
|
|
6
6
|
WorkflowTarget,
|
|
7
7
|
} from '@/types';
|
|
8
|
+
import { getProductLifecycleStatus } from '@/utils';
|
|
8
9
|
|
|
9
10
|
import { orchestratorApi } from '../api';
|
|
10
11
|
|
|
@@ -68,18 +69,18 @@ type TaskOptionsResult = StartOptionsResult<TaskOption>;
|
|
|
68
69
|
|
|
69
70
|
const startButtonOptionsApi = orchestratorApi.injectEndpoints({
|
|
70
71
|
endpoints: (build) => ({
|
|
71
|
-
getWorkflowOptions: build.query<StartOptionsResponse<WorkflowOption>, ProductLifecycleStatus
|
|
72
|
+
getWorkflowOptions: build.query<StartOptionsResponse<WorkflowOption>, ProductLifecycleStatus>({
|
|
72
73
|
query: () => ({
|
|
73
74
|
document: workflowOptionsQuery,
|
|
74
75
|
}),
|
|
75
76
|
transformResponse: (response: WorkflowOptionsResult | undefined, _, productStatus) => {
|
|
76
|
-
const statusToMatch =
|
|
77
|
+
const statusToMatch = productStatus ?? ProductLifecycleStatus.ACTIVE;
|
|
77
78
|
const startOptions: WorkflowOption[] = [];
|
|
78
79
|
const workflows = response?.workflows?.page || [];
|
|
79
80
|
workflows.forEach((workflow) => {
|
|
80
81
|
const workflowName = workflow.name;
|
|
81
82
|
workflow.products
|
|
82
|
-
.filter((product) => product.status
|
|
83
|
+
.filter((product) => getProductLifecycleStatus(product.status) === statusToMatch)
|
|
83
84
|
.forEach((product) => {
|
|
84
85
|
startOptions.push({
|
|
85
86
|
workflowName,
|
package/src/types/types.ts
CHANGED
|
@@ -83,9 +83,9 @@ export interface ProductBlockDefinition {
|
|
|
83
83
|
|
|
84
84
|
export enum ProductLifecycleStatus {
|
|
85
85
|
ACTIVE = 'active',
|
|
86
|
-
PRE_PRODUCTION = '
|
|
87
|
-
PHASE_OUT = '
|
|
88
|
-
END_OF_LIFE = '
|
|
86
|
+
PRE_PRODUCTION = 'pre production',
|
|
87
|
+
PHASE_OUT = 'phase out',
|
|
88
|
+
END_OF_LIFE = 'end of life',
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
export enum BadgeType {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ProductLifecycleStatus } from '@/types';
|
|
2
|
+
|
|
3
|
+
import { getProductLifecycleStatus } from './getProductLifecycleStatus';
|
|
4
|
+
|
|
5
|
+
describe('getProductLifecycleStatus()', () => {
|
|
6
|
+
it('converts an uppercase GraphQL key to its ProductLifecycleStatus value', () => {
|
|
7
|
+
expect(getProductLifecycleStatus('ACTIVE')).toBe(ProductLifecycleStatus.ACTIVE);
|
|
8
|
+
expect(getProductLifecycleStatus('PRE_PRODUCTION')).toBe(ProductLifecycleStatus.PRE_PRODUCTION);
|
|
9
|
+
expect(getProductLifecycleStatus('PHASE_OUT')).toBe(ProductLifecycleStatus.PHASE_OUT);
|
|
10
|
+
expect(getProductLifecycleStatus('END_OF_LIFE')).toBe(ProductLifecycleStatus.END_OF_LIFE);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('converts a lowercase key to its ProductLifecycleStatus value', () => {
|
|
14
|
+
expect(getProductLifecycleStatus('pre_production')).toBe(ProductLifecycleStatus.PRE_PRODUCTION);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('converts a mixed case key to its ProductLifecycleStatus value', () => {
|
|
18
|
+
expect(getProductLifecycleStatus('Phase_Out')).toBe(ProductLifecycleStatus.PHASE_OUT);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns undefined for a status that does not match any ProductLifecycleStatus key', () => {
|
|
22
|
+
expect(getProductLifecycleStatus('not_a_status')).toBeUndefined();
|
|
23
|
+
expect(getProductLifecycleStatus('')).toBeUndefined();
|
|
24
|
+
expect(getProductLifecycleStatus('active_pending')).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ProductLifecycleStatus } from '@/types';
|
|
2
|
+
|
|
3
|
+
const productStatusAsString = Object.values(ProductLifecycleStatus) as string[];
|
|
4
|
+
|
|
5
|
+
export const getProductLifecycleStatus = (rawStatus: string): ProductLifecycleStatus => {
|
|
6
|
+
const trimmed = rawStatus.trim();
|
|
7
|
+
if (productStatusAsString.includes(trimmed)) {
|
|
8
|
+
return trimmed as ProductLifecycleStatus;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const normalizedKey = trimmed.replace(/\s+/g, '_').toUpperCase();
|
|
12
|
+
return ProductLifecycleStatus[normalizedKey as keyof typeof ProductLifecycleStatus];
|
|
13
|
+
};
|
package/src/utils/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './getDefaultTableConfig';
|
|
|
6
6
|
export * from './getEnvironmentVariables';
|
|
7
7
|
export * from './getObjectKeys';
|
|
8
8
|
export * from './getQueryUrl';
|
|
9
|
+
export * from './getProductLifecycleStatus';
|
|
9
10
|
export * from './getProductNamesFromProcess';
|
|
10
11
|
export * from './getQueryVariablesForExport';
|
|
11
12
|
export * from './getStatusBadgeColor';
|