@orchestrator-ui/orchestrator-ui-components 8.5.0 → 8.6.0

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 (30) hide show
  1. package/.turbo/turbo-build.log +10 -14
  2. package/.turbo/turbo-lint.log +2 -4
  3. package/.turbo/turbo-test.log +7 -9
  4. package/CHANGELOG.md +6 -0
  5. package/dist/index.d.ts +84 -74
  6. package/dist/index.js +2568 -2252
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/WfoAutoExpandableTextArea/WfoAutoExpandableTextArea.tsx +54 -0
  10. package/src/components/WfoAutoExpandableTextArea/index.ts +1 -0
  11. package/src/components/WfoAutoExpandableTextArea/styles.ts +18 -0
  12. package/src/components/WfoPageTemplate/paths.ts +1 -0
  13. package/src/components/WfoTable/WfoStructuredSearchTable/WfoAddGroupAction.tsx +1 -1
  14. package/src/components/WfoTable/WfoStructuredSearchTable/WfoCombinatorSelector.tsx +3 -0
  15. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFieldSelector.tsx +58 -4
  16. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFilterBuilder.tsx +37 -28
  17. package/src/components/WfoTable/WfoStructuredSearchTable/WfoInlineCombinator.tsx +17 -0
  18. package/src/components/WfoTable/WfoStructuredSearchTable/WfoRemoveRuleAction.tsx +3 -3
  19. package/src/components/WfoTable/WfoStructuredSearchTable/WfoRuleGroup.tsx +6 -3
  20. package/src/components/WfoTable/WfoStructuredSearchTable/WfoSearchFieldWithActions.tsx +56 -0
  21. package/src/components/WfoTable/WfoStructuredSearchTable/WfoStructuredSearchTable.tsx +84 -81
  22. package/src/components/WfoTable/WfoStructuredSearchTable/styles.ts +52 -16
  23. package/src/components/WfoTable/WfoStructuredSearchTable/utils.ts +45 -0
  24. package/src/components/WfoTable/WfoTableSettingsModal/WfoTableSettingsModal.tsx +9 -0
  25. package/src/components/index.ts +1 -0
  26. package/src/configuration/version.ts +1 -1
  27. package/src/messages/en-GB.json +1 -0
  28. package/src/messages/nl-NL.json +1 -0
  29. package/src/pages/WfoSearchPocPage.tsx +151 -25
  30. package/src/types/search.ts +3 -0
@@ -1,4 +1,4 @@
1
- import React, { ReactNode, useEffect, useMemo, useState } from 'react';
1
+ import React, { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import type { RuleGroupType } from 'react-querybuilder';
3
3
  import { formatQuery } from 'react-querybuilder/formatQuery';
4
4
  import { parseCEL } from 'react-querybuilder/parseCEL';
@@ -32,16 +32,22 @@ import {
32
32
  WfoTableColumnConfig,
33
33
  subscriptionListTabs,
34
34
  } from '@/components';
35
+ import { parseCelToRuleGroup } from '@/components/WfoTable/WfoStructuredSearchTable/utils';
35
36
  import { ColumnType, WfoTableProps } from '@/components/WfoTable/WfoTable';
36
37
  import { useStoredTableConfig } from '@/hooks';
37
- import { SearchPayload, useSearchQuery } from '@/rtk';
38
- import { EntityKind, PaginatedSearchResults, RetrieverType, SortOrder } from '@/types';
39
- import { parseDateToLocaleDateTimeString } from '@/utils';
38
+ import { SearchPayload, useLazySearchQuery, useSearchQuery } from '@/rtk';
39
+ import {
40
+ EntityKind,
41
+ FieldToOperatorMap,
42
+ PaginatedSearchResults,
43
+ ResultColumToPropertyMap,
44
+ RetrieverType,
45
+ SortOrder,
46
+ } from '@/types';
47
+ import { getCsvFileNameWithDate, initiateCsvFileDownload, parseDateToLocaleDateTimeString } from '@/utils';
40
48
 
41
49
  const SEARCH_TABLE_LOCAL_STORAGE_KEY = 'SEARCH_TABLE_LOCAL_STORAGE_KEY';
42
50
 
43
- type ResultColumToPropertyMap<T> = Map<string, keyof T>;
44
-
45
51
  const getKeyByValueFromMap = <T,>(resultColumToPropertyMap: ResultColumToPropertyMap<T>, field: keyof T) => {
46
52
  return [...resultColumToPropertyMap.entries()].find(([, v]) => v === field)?.[0] || '';
47
53
  };
@@ -116,6 +122,21 @@ const resultColumToPropertyMap: ResultColumToPropertyMap<SubscriptionListItem> =
116
122
  ['subscription.metadata', 'metadata'],
117
123
  ]);
118
124
 
125
+ /* These options will be added as the first options in the field dropdown in the FieldSelector */
126
+ const prefilledFieldOptions: FieldToOperatorMap = new Map([
127
+ ['subscription.subscription_id', ['eq', 'neq', 'like']],
128
+ ['subscription.description', ['eq', 'neq', 'like']],
129
+ ['subscription.status', ['eq', 'neq', 'like']],
130
+ ['subscription.insync', ['eq', 'neq']],
131
+ ['subscription.product.name', ['eq', 'neq', 'like']],
132
+ ['subscription.product.tag', ['eq', 'neq', 'like']],
133
+ ['subscription.customer_name', ['eq', 'neq', 'like']],
134
+ ['subscription.customer_abbreviation', ['eq', 'neq', 'like']],
135
+ ['subscription.start_date', ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'between']],
136
+ ['subscription.end_date', ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'between']],
137
+ ['subscription.note', ['eq', 'neq', 'like']],
138
+ ]);
139
+
119
140
  export const WfoSearchPocPage = () => {
120
141
  const t = useTranslations('subscriptions.index');
121
142
  const [activeTab, setActiveTab] = useQueryParam('activeTab', withDefault(StringParam, WfoSubscriptionListTab.ACTIVE));
@@ -125,26 +146,60 @@ export const WfoSearchPocPage = () => {
125
146
 
126
147
  // Part of the search endpoint payload that is passed in the q parameter
127
148
  const [queryText, setQueryText] = useState<string>('');
128
- const [committedSearchQuery, setCommittedSearchQuery] = useState<string>('');
149
+ // The committed query and filter live in the URL so a link reproduces the search and browser
150
+ // back/forward re-runs it. The filter is stored as a CEL string and parsed back to a rule group.
151
+ const [committedSearchQuery, setCommittedSearchQuery] = useQueryParam('queryString', withDefault(StringParam, ''));
152
+ const [committedFilterString, setCommittedFilterString] = useQueryParam('filterString', withDefault(StringParam, ''));
153
+ // Track the last value this page committed, so the URL->state sync effects below only rebuild the
154
+ // inputs for external changes (page load, back/forward). Rebuilding on own commits would revert
155
+ // characters typed right after committing (search bar) or visibly restructure builder rules the
156
+ // CEL round trip cannot preserve, such as 'between'. The refs hold the value as the params decode
157
+ // it (absent param -> ''); the setters receive undefined to remove an empty param from the URL.
158
+ const lastSelfCommittedFilter = useRef('');
159
+ const commitFilterString = (celString: string) => {
160
+ lastSelfCommittedFilter.current = celString;
161
+ setCommittedFilterString(celString || undefined);
162
+ };
163
+ const lastSelfCommittedQuery = useRef('');
164
+ const commitSearchQuery = (queryText: string) => {
165
+ lastSelfCommittedQuery.current = queryText;
166
+ setCommittedSearchQuery(queryText || undefined);
167
+ };
129
168
 
130
169
  // String that is displayed in the filter textarea. This is transformed and if valid passed to the search endpoint in the filter parameter
131
- const [filterString, setFilterString] = useState<string>();
170
+ const [filterString, setFilterString] = useState<string>('');
132
171
  const [queryBuilderRuleGroup, setQueryBuilderRuleGroup] = useState<RuleGroupType | undefined>();
133
- const [committedRuleGroup, setCommittedRuleGroup] = useState<RuleGroupType | undefined>();
172
+ const committedRuleGroup = useMemo<RuleGroupType | undefined>(
173
+ () => parseCelToRuleGroup(committedFilterString),
174
+ [committedFilterString],
175
+ );
134
176
  const [isValidFilterString, setIsValidFilterString] = useState<boolean>(true);
135
177
  const [tableDefaults, setTableDefaults] = useState<StoredTableConfig<SubscriptionListItem>>();
136
178
  const [pageSize, setPageSize] = useState<number>(tableDefaults?.selectedPageSize || DEFAULT_PAGE_SIZE);
137
179
  const [limit, setLimit] = useState<number>(pageSize);
138
- const [cursor, setCursor] = useState<string | undefined>(undefined);
180
+ const [pageCursor, setPageCursor] = useState<{ cursor: string; searchKey: string } | undefined>(undefined);
139
181
  const [dataSorting, setDataSorting] = useState<WfoDataSorting<SubscriptionListItem>>({
140
182
  field: 'subscriptionId',
141
183
  sortOrder: SortOrder.DESC,
142
184
  });
143
185
 
144
- // The search payload is derived entirely from the committed search state. Changing any of these
145
- // values including the active tab, whether via a tab click or a browser back/forward that
146
- // updates the activeTab URL param produces a new payload and RTK Query re-runs the search
147
- // automatically. No imperative trigger or effect (and no eslint-disable) is needed.
186
+ // A next-page cursor is only valid for the committed search that produced it. Binding it to a key
187
+ // of that search covers commits that bypass the handlers below, e.g. browser back/forward changing
188
+ // the queryString/filterString/activeTab URL params: the key no longer matches, so the stale
189
+ // cursor is not sent along with the new search.
190
+ const committedSearchKey = JSON.stringify([
191
+ committedSearchQuery,
192
+ committedFilterString,
193
+ selectedTab,
194
+ retrieverType,
195
+ dataSorting,
196
+ ]);
197
+ const cursor = pageCursor?.searchKey === committedSearchKey ? pageCursor.cursor : undefined;
198
+
199
+ // The search payload is derived entirely from the committed search state, which lives in the URL
200
+ // (queryString, filterString and activeTab params). Changing any of these values — whether via the
201
+ // UI or a browser back/forward that updates the URL — produces a new payload and RTK Query re-runs
202
+ // the search automatically. No imperative trigger or effect (and no eslint-disable) is needed.
148
203
  // Cursor is stripped from the RTK cache key, so a cursor change appends to the accumulated
149
204
  // result set instead of creating a new cache entry — see search endpoint's merge() logic.
150
205
  const searchPayload: SearchPayload = useMemo(() => {
@@ -167,6 +222,9 @@ export const WfoSearchPocPage = () => {
167
222
 
168
223
  const { data, isFetching } = useSearchQuery(searchPayload);
169
224
 
225
+ const [getSubscriptionListTrigger] = useLazySearchQuery();
226
+ const getSubscriptionListForExport = () => getSubscriptionListTrigger(searchPayload).unwrap();
227
+
170
228
  useEffect(() => {
171
229
  const storedConfig = getStoredTableConfig();
172
230
  if (storedConfig) {
@@ -188,6 +246,7 @@ export const WfoSearchPocPage = () => {
188
246
  renderDetails: (value) => value,
189
247
  renderTooltip: (value) => value,
190
248
  isSortable: true,
249
+ isFilterable: true,
191
250
  },
192
251
  description: {
193
252
  columnType: ColumnType.DATA,
@@ -195,37 +254,44 @@ export const WfoSearchPocPage = () => {
195
254
  width: '500px',
196
255
  renderData: (value, record) => <Link href={`/subscriptions/${record.subscriptionId}`}>{value}</Link>,
197
256
  renderTooltip: (value) => value,
257
+ isFilterable: true,
198
258
  },
199
259
  status: {
200
260
  columnType: ColumnType.DATA,
201
261
  label: t('status'),
202
262
  width: '120px',
203
263
  renderData: (value) => <WfoSubscriptionStatusBadge status={value} />,
264
+ isFilterable: true,
204
265
  },
205
266
  insync: {
206
267
  columnType: ColumnType.DATA,
207
268
  label: t('insync'),
208
269
  width: '75px',
209
270
  renderData: (value) => <WfoInsyncIcon inSync={value} />,
271
+ isFilterable: true,
210
272
  },
211
273
  productName: {
212
274
  columnType: ColumnType.DATA,
213
275
  width: '260px',
214
276
  label: t('product'),
277
+ isFilterable: true,
215
278
  },
216
279
  tag: {
217
280
  columnType: ColumnType.DATA,
218
281
  label: t('tag'),
219
282
  width: '100px',
283
+ isFilterable: true,
220
284
  },
221
285
  customerFullname: {
222
286
  columnType: ColumnType.DATA,
223
287
  label: t('customerFullname'),
288
+ isFilterable: true,
224
289
  },
225
290
  customerShortcode: {
226
291
  columnType: ColumnType.DATA,
227
292
  label: t('customerShortcode'),
228
293
  width: '150px',
294
+ isFilterable: true,
229
295
  },
230
296
  startDate: {
231
297
  columnType: ColumnType.DATA,
@@ -235,6 +301,7 @@ export const WfoSearchPocPage = () => {
235
301
  renderDetails: parseDateToLocaleDateTimeString,
236
302
  clipboardText: parseDateToLocaleDateTimeString,
237
303
  renderTooltip: (cellValue) => cellValue?.toString(),
304
+ isFilterable: true,
238
305
  },
239
306
  endDate: {
240
307
  columnType: ColumnType.DATA,
@@ -244,6 +311,7 @@ export const WfoSearchPocPage = () => {
244
311
  renderDetails: parseDateToLocaleDateTimeString,
245
312
  clipboardText: parseDateToLocaleDateTimeString,
246
313
  renderTooltip: (cellValue) => cellValue?.toString(),
314
+ isFilterable: true,
247
315
  },
248
316
  note: {
249
317
  columnType: ColumnType.DATA,
@@ -260,6 +328,7 @@ export const WfoSearchPocPage = () => {
260
328
  />
261
329
  );
262
330
  },
331
+ isFilterable: true,
263
332
  },
264
333
  metadata: {
265
334
  columnType: ColumnType.DATA,
@@ -268,12 +337,28 @@ export const WfoSearchPocPage = () => {
268
337
  renderData: (value) => <WfoInlineJson data={value} />,
269
338
  renderDetails: (value) => value && <WfoJsonCodeBlock data={value} isBasicStyle />,
270
339
  renderTooltip: (value) => value && <WfoJsonCodeBlock data={value} isBasicStyle={false} />,
340
+ isFilterable: true,
271
341
  },
272
342
  };
273
343
 
274
344
  const handleApplyFilter = (searchParams?: SearchParams) => {
275
- setCommittedRuleGroup(searchParams?.ruleGroup === false ? undefined : queryBuilderRuleGroup);
276
- setCursor(undefined);
345
+ const ruleGroupParam = searchParams?.ruleGroup;
346
+ // Use an explicitly passed rule group when provided (e.g. a column-header search), a cleared filter
347
+ // when `false`, and otherwise the current query builder state (the "Apply filter" button).
348
+ const effectiveRuleGroup = ruleGroupParam === false ? undefined : (ruleGroupParam ?? queryBuilderRuleGroup);
349
+ // '' (no rule group, or only placeholder rules) commits an empty filter, clearing the URL param.
350
+ const celQuery =
351
+ effectiveRuleGroup ? formatQuery(effectiveRuleGroup, { format: 'cel', fallbackExpression: '' }) : '';
352
+ // A non-empty CEL string must survive the round trip through the URL: formatQuery escapes double
353
+ // quotes in values but parseCEL has no escape support, so such a filter would silently be dropped
354
+ // after committing. Refuse the commit and flag the filter instead, keeping the URL and the search
355
+ // results consistent.
356
+ if (celQuery && !parseCelToRuleGroup(celQuery)) {
357
+ setIsValidFilterString(false);
358
+ return;
359
+ }
360
+ commitFilterString(celQuery);
361
+ setPageCursor(undefined);
277
362
  };
278
363
 
279
364
  const onChangeQueryText = (queryText: string) => {
@@ -282,22 +367,22 @@ export const WfoSearchPocPage = () => {
282
367
 
283
368
  const onSearchQueryText = (queryText: string) => {
284
369
  setQueryText(queryText);
285
- setCommittedSearchQuery(queryText);
286
- setCursor(undefined);
370
+ commitSearchQuery(queryText);
371
+ setPageCursor(undefined);
287
372
  };
288
373
 
289
374
  const onUpdateRetrieverType = (retrieverType: RetrieverType) => {
290
375
  setRetrieverType(retrieverType);
291
- setCursor(undefined);
376
+ setPageCursor(undefined);
292
377
  };
293
378
 
294
379
  const handleChangeTab = (updatedTab: WfoSubscriptionListTab) => {
295
380
  setActiveTab(updatedTab);
296
381
  setLimit(pageSize);
297
- setCursor(undefined);
382
+ setPageCursor(undefined);
298
383
  };
299
384
 
300
- const safeCelParse = (celString: string) => {
385
+ const safeCelParse = useCallback((celString: string) => {
301
386
  try {
302
387
  const ruleGroup = parseCEL(celString);
303
388
  if (celString === '') {
@@ -315,7 +400,32 @@ export const WfoSearchPocPage = () => {
315
400
  } catch {
316
401
  setIsValidFilterString(false);
317
402
  }
318
- };
403
+ }, []);
404
+
405
+ // Populate the search bar and the filter builder from the URL, both on page load and when
406
+ // back/forward navigation changes the committed search. Commits made by this page are skipped —
407
+ // see the lastSelfCommitted refs above.
408
+ useEffect(() => {
409
+ if (committedSearchQuery === lastSelfCommittedQuery.current) {
410
+ return;
411
+ }
412
+ lastSelfCommittedQuery.current = committedSearchQuery;
413
+ setQueryText(committedSearchQuery);
414
+ }, [committedSearchQuery]);
415
+
416
+ useEffect(() => {
417
+ if (committedFilterString === lastSelfCommittedFilter.current) {
418
+ return;
419
+ }
420
+ lastSelfCommittedFilter.current = committedFilterString;
421
+ setFilterString(committedFilterString);
422
+ if (committedFilterString) {
423
+ safeCelParse(committedFilterString);
424
+ } else {
425
+ setQueryBuilderRuleGroup(undefined);
426
+ setIsValidFilterString(true);
427
+ }
428
+ }, [committedFilterString, safeCelParse]);
319
429
 
320
430
  const onUpdateQueryBuilder = (ruleGroup: RuleGroupType | false) => {
321
431
  if (ruleGroup === false) {
@@ -349,21 +459,34 @@ export const WfoSearchPocPage = () => {
349
459
  const hasNextPage = data?.page_info?.has_next_page ?? false;
350
460
  const nextPageCursor = data?.page_info?.next_page_cursor ?? undefined;
351
461
 
462
+ const exportData = async () => {
463
+ const exportResult = await getSubscriptionListForExport();
464
+ const { items: exportItems } = getDataFromResponse<SubscriptionListItem>(
465
+ exportResult,
466
+ resultColumToPropertyMap,
467
+ 'subscriptionId',
468
+ );
469
+ if (!exportItems.length) {
470
+ return;
471
+ }
472
+ initiateCsvFileDownload(exportItems, Object.keys(tableColumnConfig), getCsvFileNameWithDate('Subscriptions'));
473
+ };
474
+
352
475
  const onShowMore = () => {
353
476
  if (!isFetching && nextPageCursor) {
354
- setCursor(nextPageCursor);
477
+ setPageCursor({ cursor: nextPageCursor, searchKey: committedSearchKey });
355
478
  }
356
479
  };
357
480
 
358
481
  const onUpdateDataSorting = ({ field, sortOrder }: WfoDataSorting<SubscriptionListItem>) => {
359
482
  setDataSorting({ field, sortOrder });
360
483
  setLimit(pageSize);
361
- setCursor(undefined);
484
+ setPageCursor(undefined);
362
485
  };
363
486
 
364
487
  return (
365
488
  <>
366
- <WfoContentHeader title="Subscriptions (POC)" />
489
+ <WfoContentHeader title="Subscriptions (Beta)" />
367
490
  <WfoFilterTabs
368
491
  tabs={subscriptionListTabs}
369
492
  selectedTab={selectedTab}
@@ -391,11 +514,14 @@ export const WfoSearchPocPage = () => {
391
514
  queryText={queryText}
392
515
  retrieverType={retrieverType}
393
516
  tableColumnConfig={tableColumnConfig}
517
+ getColumnSearchFieldName={(field) => getKeyByValueFromMap(resultColumToPropertyMap, field)}
394
518
  pageSize={pageSize}
395
519
  onUpdateDataSorting={onUpdateDataSorting}
396
520
  setPageSize={setPageSize}
397
521
  totalItems={totalItems}
398
522
  hasNextPage={hasNextPage}
523
+ prefilledFieldOptions={prefilledFieldOptions}
524
+ onExportData={exportData}
399
525
  />
400
526
  </>
401
527
  );
@@ -211,3 +211,6 @@ export type ExportArtifact = {
211
211
  download_url: string;
212
212
  description: string;
213
213
  };
214
+
215
+ export type ResultColumToPropertyMap<T> = Map<string, keyof T>;
216
+ export type FieldToOperatorMap = Map<string, string[]>;