@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.
Files changed (28) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/.turbo/turbo-test.log +14 -13
  4. package/CHANGELOG.md +17 -0
  5. package/dist/index.d.ts +48 -26
  6. package/dist/index.js +2202 -2081
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/WfoBadges/WfoProductStatusBadge/WfoProductStatusBadge.tsx +2 -5
  10. package/src/components/WfoStartButton/WfoStartButtonComboBox.tsx +6 -11
  11. package/src/components/WfoStartButton/WfoStartWorkflowComboBox.tsx +1 -1
  12. package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActions.tsx +6 -16
  13. package/src/components/WfoSubscription/utils/utils.spec.ts +57 -0
  14. package/src/components/WfoSubscription/utils/utils.ts +15 -0
  15. package/src/components/WfoTable/WfoStructuredSearchTable/WfoSearchFieldWithActions.tsx +9 -9
  16. package/src/components/WfoTable/WfoStructuredSearchTable/WfoSearchHelpModal.tsx +59 -0
  17. package/src/components/WfoTable/WfoStructuredSearchTable/WfoStructuredSearchTable.tsx +13 -27
  18. package/src/configuration/version.ts +1 -1
  19. package/src/messages/en-GB.json +19 -0
  20. package/src/messages/nl-NL.json +20 -1
  21. package/src/pages/WfoSearchPocPage.tsx +68 -36
  22. package/src/rtk/endpoints/metadata/productBlocks.ts +9 -1
  23. package/src/rtk/endpoints/metadata/products.ts +5 -2
  24. package/src/rtk/endpoints/startOptions.ts +4 -3
  25. package/src/types/types.ts +3 -3
  26. package/src/utils/getProductLifecycleStatus.spec.ts +26 -0
  27. package/src/utils/getProductLifecycleStatus.ts +13 -0
  28. 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 q parameter
153
- const [queryText, setQueryText] = useState<string>('');
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 [committedSearchQuery, setCommittedSearchQuery] = useQueryParam('queryString', withDefault(StringParam, ''));
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 lastSelfCommittedQuery = useRef('');
169
- const commitSearchQuery = (queryText: string) => {
170
- lastSelfCommittedQuery.current = queryText;
171
- setCommittedSearchQuery(queryText || undefined);
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
- committedSearchQuery,
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: committedSearchQuery,
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
- }, [committedSearchQuery, committedRuleGroup, selectedTab, retrieverType, pageSize, dataSorting, cursor]);
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 = queryText === '';
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
- const handleApplyFilter = (searchParams?: SearchParams) => {
347
- const ruleGroupParam = searchParams?.ruleGroup;
348
- // Use an explicitly passed rule group when provided (e.g. a column-header search), a cleared filter
349
- // when `false`, and otherwise the current query builder state (the "Apply filter" button).
350
- const effectiveRuleGroup = ruleGroupParam === false ? undefined : (ruleGroupParam ?? queryBuilderRuleGroup);
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
- return;
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 onChangeQueryText = (queryText: string) => {
367
- setQueryText(queryText);
382
+ const onChangeQueryString = (queryString: string) => {
383
+ setQueryString(queryString);
368
384
  };
369
385
 
370
- const onSearchQueryText = (queryText: string) => {
371
- setQueryText(queryText);
372
- commitSearchQuery(queryText);
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 (committedSearchQuery === lastSelfCommittedQuery.current) {
435
+ if (committedQueryString === lastSelfCommittedQueryString.current) {
409
436
  return;
410
437
  }
411
- lastSelfCommittedQuery.current = committedSearchQuery;
412
- setQueryText(committedSearchQuery);
413
- }, [committedSearchQuery]);
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
- onChangeQueryText={onChangeQueryText}
525
- onSearchQueryText={onSearchQueryText}
556
+ onChangeQueryString={onChangeQueryString}
557
+ onSearchQueryString={onSearchQueryString}
526
558
  onShowMore={onShowMore}
527
559
  onUpdateRetrieverType={onUpdateRetrieverType}
528
560
  queryBuilderRuleGroup={queryBuilderRuleGroup}
529
- queryText={queryText}
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 | string>({
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 = (productStatus ?? ProductLifecycleStatus.ACTIVE).toLowerCase();
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.toLowerCase() === statusToMatch)
83
+ .filter((product) => getProductLifecycleStatus(product.status) === statusToMatch)
83
84
  .forEach((product) => {
84
85
  startOptions.push({
85
86
  workflowName,
@@ -83,9 +83,9 @@ export interface ProductBlockDefinition {
83
83
 
84
84
  export enum ProductLifecycleStatus {
85
85
  ACTIVE = 'active',
86
- PRE_PRODUCTION = 'pre_production',
87
- PHASE_OUT = 'phase_out',
88
- END_OF_LIFE = '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
+ };
@@ -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';