@orchestrator-ui/orchestrator-ui-components 8.7.2 → 8.8.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 (52) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/.turbo/turbo-test.log +17 -10
  4. package/CHANGELOG.md +28 -0
  5. package/dist/index.d.ts +506 -19
  6. package/dist/index.js +614 -399
  7. package/dist/index.js.map +1 -1
  8. package/jest.config.cjs +9 -8
  9. package/package.json +1 -1
  10. package/src/components/WfoBadges/WfoWorkflowTargetBadge/WfoWorkflowTargetBadge.tsx +2 -2
  11. package/src/components/WfoContentHeader/WfoContentHeader.tsx +1 -1
  12. package/src/components/WfoInlineNoteEdit/WfoProcessListNoteEdit.spec.tsx +35 -0
  13. package/src/components/WfoInlineNoteEdit/WfoProcessListNoteEdit.tsx +7 -9
  14. package/src/components/WfoProcessList/WfoProcessListDeltaPopover.tsx +1 -1
  15. package/src/components/WfoProcessList/WfoProcessesList.tsx +7 -5
  16. package/src/components/WfoSubscription/WfoProcessesTimeline.tsx +21 -7
  17. package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActions.tsx +25 -9
  18. package/src/components/WfoSubscription/WfoSubscriptionGeneralSections/WfoSubscriptionDetailSection.tsx +1 -1
  19. package/src/components/WfoSubscription/styles.ts +6 -0
  20. package/src/components/WfoSubscription/utils/utils.ts +2 -4
  21. package/src/components/WfoTable/WfoStructuredSearchTable/WfoExpandingSearchRow.tsx +0 -1
  22. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFilterBuilder.tsx +30 -3
  23. package/src/components/WfoTable/WfoStructuredSearchTable/WfoOperatorSelector.spec.tsx +143 -0
  24. package/src/components/WfoTable/WfoStructuredSearchTable/WfoOperatorSelector.tsx +33 -11
  25. package/src/components/WfoTable/WfoStructuredSearchTable/WfoRestoreLoop.spec.tsx +130 -0
  26. package/src/components/WfoTable/WfoStructuredSearchTable/WfoStructuredSearchTable.tsx +40 -1
  27. package/src/components/WfoTable/WfoStructuredSearchTable/WfoValueEditor.spec.tsx +60 -0
  28. package/src/components/WfoTable/WfoStructuredSearchTable/WfoValueEditor.tsx +22 -4
  29. package/src/components/WfoTable/WfoStructuredSearchTable/styles.ts +8 -9
  30. package/src/components/WfoTable/WfoStructuredSearchTable/utils.spec.ts +56 -0
  31. package/src/components/WfoTable/WfoStructuredSearchTable/utils.ts +40 -1
  32. package/src/components/WfoTable/WfoTable/WfoTable.tsx +5 -1
  33. package/src/components/WfoTable/WfoTable/WfoTableDataRows.tsx +11 -2
  34. package/src/components/WfoTable/WfoTable/WfoTableExpandedRowReveal.spec.tsx +120 -0
  35. package/src/components/WfoTable/WfoTable/styles.ts +6 -0
  36. package/src/components/WfoTable/utils/tableConfigPersistence.ts +1 -0
  37. package/src/components/WfoTimeline/styles.ts +1 -1
  38. package/src/components/WfoWorkflowUserGuide/WfoPageWithUserGuide.tsx +8 -2
  39. package/src/components/WfoWorkflowUserGuide/WfoWorkflowGuideExpandablePanel.tsx +39 -30
  40. package/src/components/WfoWorkflowUserGuide/styles.ts +55 -7
  41. package/src/configuration/constants.ts +1 -1
  42. package/src/configuration/version.ts +1 -1
  43. package/src/hooks/usePathAutoComplete.spec.tsx +64 -0
  44. package/src/hooks/usePathAutoComplete.ts +98 -41
  45. package/src/hooks/useStoredTableConfig.ts +1 -0
  46. package/src/messages/en-GB.json +3 -1
  47. package/src/messages/nl-NL.json +4 -2
  48. package/src/pages/WfoSearchPocPage.tsx +19 -18
  49. package/src/pages/tasks/WfoTasksListPage.tsx +1 -0
  50. package/src/rtk/endpoints/search.ts +1 -0
  51. package/src/types/types.ts +11 -11
  52. package/src/utils/getDefaultTableConfig.ts +2 -0
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The expanded ("match details") row under each data row hides itself with display: none and
3
+ * is revealed by sibling selectors that WfoTable puts on the data row: always when
4
+ * showExpandedRows is set, and on hover/focus otherwise. These tests pin the whole mechanism
5
+ * down at the DOM level: the detail row must be the data row's next sibling, the reveal rules
6
+ * must be emitted, and their selectors must actually match the detail row.
7
+ */
8
+ import React from 'react';
9
+
10
+ import { css } from '@emotion/react';
11
+ import '@testing-library/jest-dom';
12
+ import { render, screen } from '@testing-library/react';
13
+
14
+ import { ColumnType, WfoTable, WfoTableColumnConfig } from './WfoTable';
15
+
16
+ jest.mock('next-intl', () => ({
17
+ useTranslations: () => (key: string) => key,
18
+ }));
19
+
20
+ type Item = { id: string; name: string };
21
+
22
+ const data: Item[] = [
23
+ { id: 'row-1', name: 'First row' },
24
+ { id: 'row-2', name: 'Second row' },
25
+ ];
26
+
27
+ const columnConfig: WfoTableColumnConfig<Item> = {
28
+ name: {
29
+ columnType: ColumnType.DATA,
30
+ label: 'Name',
31
+ },
32
+ };
33
+
34
+ // Mimics WfoExpandingSearchRow: a <tr> that hides itself
35
+ const hideStyle = css({ display: 'none' });
36
+ const DetailRow = ({ text }: { text: string }) => (
37
+ <tr css={hideStyle}>
38
+ <td>{text}</td>
39
+ </tr>
40
+ );
41
+
42
+ const rowExpandingConfiguration = {
43
+ uniqueRowId: 'id' as keyof WfoTableColumnConfig<Item>,
44
+ uniqueRowIdToExpandedRowMap: {
45
+ 'row-1': <DetailRow text="detail-1" />,
46
+ 'row-2': <DetailRow text="detail-2" />,
47
+ },
48
+ };
49
+
50
+ // Emotion may insert rules through the CSSOM, leaving <style> tags without text content, so
51
+ // read the rules from document.styleSheets. Returns [selectorText, display] pairs, with the
52
+ // selector whitespace stripped since the CSSOM normalizes it.
53
+ const getDisplayRules = () =>
54
+ Array.from(document.styleSheets)
55
+ .flatMap((sheet) => Array.from(sheet.cssRules))
56
+ .filter((rule): rule is CSSStyleRule => 'selectorText' in rule)
57
+ .map((rule): [string, string] => [rule.selectorText.replace(/\s+/g, ''), rule.style.display])
58
+ .filter(([, display]) => Boolean(display));
59
+
60
+ const getRowPair = () => {
61
+ const dataRow = screen.getByText('First row').closest('tr');
62
+ const detailRow = dataRow?.nextElementSibling;
63
+ return { dataRow, detailRow };
64
+ };
65
+
66
+ // The reveal selectors resolve '&' to the emotion class composed on the data row
67
+ const getDataRowEmotionClass = (dataRow: HTMLTableRowElement) => {
68
+ const emotionClass = Array.from(dataRow.classList).find((className) => className.startsWith('css-'));
69
+ expect(emotionClass).toBeDefined();
70
+ return emotionClass;
71
+ };
72
+
73
+ describe('WfoTable expanded row reveal', () => {
74
+ it('renders the detail row as next sibling of its data row, hidden by its own style', () => {
75
+ render(
76
+ <WfoTable<Item> data={data} columnConfig={columnConfig} rowExpandingConfiguration={rowExpandingConfiguration} />,
77
+ );
78
+
79
+ const { dataRow, detailRow } = getRowPair();
80
+ expect(dataRow).toBeInTheDocument();
81
+ expect(detailRow).toHaveTextContent('detail-1');
82
+
83
+ const detailRowClass = Array.from(detailRow!.classList).find((className) => className.startsWith('css-'));
84
+ expect(getDisplayRules()).toContainEqual([`.${detailRowClass}`, 'none']);
85
+ });
86
+
87
+ it('reveals every detail row through the sibling selector when showExpandedRows is set', () => {
88
+ render(
89
+ <WfoTable<Item>
90
+ data={data}
91
+ columnConfig={columnConfig}
92
+ rowExpandingConfiguration={rowExpandingConfiguration}
93
+ showExpandedRows
94
+ />,
95
+ );
96
+
97
+ const { dataRow, detailRow } = getRowPair();
98
+ const emotionClass = getDataRowEmotionClass(dataRow!);
99
+
100
+ expect(getDisplayRules()).toContainEqual([`.${emotionClass}+tr`, 'table-row']);
101
+ expect(detailRow!.matches(`.${emotionClass} + tr`)).toBe(true);
102
+ });
103
+
104
+ it('reveals a detail row on hover/focus of the data row or of the detail row itself when showExpandedRows is not set', () => {
105
+ render(
106
+ <WfoTable<Item> data={data} columnConfig={columnConfig} rowExpandingConfiguration={rowExpandingConfiguration} />,
107
+ );
108
+
109
+ const { dataRow, detailRow } = getRowPair();
110
+ const emotionClass = getDataRowEmotionClass(dataRow!);
111
+ const displayRules = getDisplayRules();
112
+
113
+ expect(displayRules).toContainEqual([`.${emotionClass}:hover+tr,.${emotionClass}:focus-within+tr`, 'table-row']);
114
+ // The rule that keeps the row open while hovering the revealed row itself (flicker fix)
115
+ expect(displayRules).toContainEqual([`.${emotionClass}+tr:hover,.${emotionClass}+tr:focus-within`, 'table-row']);
116
+
117
+ // :hover cannot be simulated in jsdom; verify the selectors structurally instead
118
+ expect(detailRow!.matches(`.${emotionClass} + tr`)).toBe(true);
119
+ });
120
+ });
@@ -160,8 +160,13 @@ export const getWfoTableStyles = ({ theme, isDarkModeActive }: WfoThemeHelpers)
160
160
  '.eui-xScroll': { display: 'flex', justifyContent: 'flex-start' },
161
161
  });
162
162
 
163
+ const showExpandedRowStyle = css({
164
+ '& + tr': { display: 'table-row' },
165
+ });
166
+
163
167
  const toggleExpandedRowOnHoverStyle = css({
164
168
  '&:hover + tr, &:focus-within + tr': { display: 'table-row' },
169
+ '& + tr:hover, & + tr:focus-within': { display: 'table-row' },
165
170
  });
166
171
 
167
172
  return {
@@ -181,6 +186,7 @@ export const getWfoTableStyles = ({ theme, isDarkModeActive }: WfoThemeHelpers)
181
186
  dragAndDropStyle,
182
187
  paginationStyle,
183
188
  setWidth,
189
+ showExpandedRowStyle,
184
190
  toggleExpandedRowOnHoverStyle,
185
191
  };
186
192
  };
@@ -3,6 +3,7 @@ import { TableColumnKeys } from './columns';
3
3
  export type StoredTableConfig<T> = {
4
4
  hiddenColumns: TableColumnKeys<T>;
5
5
  selectedPageSize: number;
6
+ showMatchDetails?: boolean;
6
7
  };
7
8
 
8
9
  export const isValidLocalStorageTableConfig = <T>(object: StoredTableConfig<T>): object is StoredTableConfig<T> =>
@@ -57,7 +57,7 @@ export const getTimelineStyles = ({ theme }: WfoThemeHelpers) => {
57
57
  paddingLeft: theme.font.baseline * 4,
58
58
  paddingRight: theme.font.baseline * 4,
59
59
  position: 'sticky',
60
- top: timelineOutlineWidthPx,
60
+ top: theme.base / 4,
61
61
  zIndex: 2, // Some EUI components have a zIndex
62
62
  display: 'flex',
63
63
 
@@ -29,9 +29,15 @@ export const WfoPageWithUserGuide = ({ workflowName, children }: WfoFormWithUser
29
29
  );
30
30
 
31
31
  return (
32
- <EuiFlexGroup gutterSize="s" direction={isBigScreen ? 'row' : 'column'}>
32
+ <EuiFlexGroup
33
+ gutterSize="s"
34
+ direction={isBigScreen ? 'row' : 'column'}
35
+ alignItems={isBigScreen ? 'flexStart' : 'stretch'}
36
+ >
33
37
  {!isBigScreen && workflowGuideExpandablePanel}
34
- <EuiFlexItem grow={true}>{children}</EuiFlexItem>
38
+ <EuiFlexItem grow={true} css={{ minWidth: 0 }}>
39
+ {children}
40
+ </EuiFlexItem>
35
41
  {isBigScreen && workflowGuideExpandablePanel}
36
42
  </EuiFlexGroup>
37
43
  );
@@ -29,6 +29,7 @@ interface WfoUserGuideToggleStripProps {
29
29
  ariaLabel: string;
30
30
  isBigScreen: boolean;
31
31
  noLeadingBorder?: boolean;
32
+ sticky?: boolean;
32
33
  children: ReactNode;
33
34
  }
34
35
 
@@ -37,37 +38,51 @@ const WfoWorkflowGuideToggleStrip = ({
37
38
  ariaLabel,
38
39
  isBigScreen,
39
40
  noLeadingBorder,
41
+ sticky,
40
42
  children,
41
43
  }: WfoUserGuideToggleStripProps) => {
42
44
  const {
43
45
  fullHeightStyle,
46
+ stickyStripContainerStyle,
47
+ stripIconStyle,
48
+ stickyPanelFillStyle,
44
49
  toggleStripContainerStyle,
45
50
  toggleStripContainerHorizontalStyle,
51
+ stickyHorizontalStripContainerStyle,
46
52
  toggleStripPanelStyle,
47
53
  noLeftBorderStyle,
48
54
  noBottomBorderStyle,
49
55
  } = useWithOrchestratorTheme(getStyles);
50
56
 
57
+ const containerStyle =
58
+ isBigScreen ?
59
+ sticky ? stickyStripContainerStyle
60
+ : toggleStripContainerStyle
61
+ : sticky ? stickyHorizontalStripContainerStyle
62
+ : toggleStripContainerHorizontalStyle;
63
+
51
64
  return (
52
- <EuiFlexItem
53
- grow={false}
54
- onClick={onToggle}
55
- aria-label={ariaLabel}
56
- css={isBigScreen ? toggleStripContainerStyle : toggleStripContainerHorizontalStyle}
57
- >
65
+ <EuiFlexItem grow={false} onClick={onToggle} aria-label={ariaLabel} css={containerStyle}>
58
66
  <EuiPanel
59
67
  hasShadow={false}
60
- css={[toggleStripPanelStyle, noLeadingBorder && (isBigScreen ? noLeftBorderStyle : noBottomBorderStyle)]}
68
+ css={[
69
+ toggleStripPanelStyle,
70
+ sticky && stickyPanelFillStyle,
71
+ noLeadingBorder && (isBigScreen ? noLeftBorderStyle : noBottomBorderStyle),
72
+ ]}
61
73
  >
62
- <EuiFlexGroup
63
- direction={isBigScreen ? 'column' : 'row'}
64
- alignItems="center"
65
- gutterSize="s"
66
- justifyContent="center"
67
- css={fullHeightStyle}
68
- >
69
- {children}
70
- </EuiFlexGroup>
74
+ {isBigScreen ?
75
+ <div css={stripIconStyle}>{children}</div>
76
+ : <EuiFlexGroup
77
+ direction="row"
78
+ alignItems="center"
79
+ gutterSize="s"
80
+ justifyContent="center"
81
+ css={fullHeightStyle}
82
+ >
83
+ {children}
84
+ </EuiFlexGroup>
85
+ }
71
86
  </EuiPanel>
72
87
  </EuiFlexItem>
73
88
  );
@@ -82,12 +97,13 @@ const WfoWorkflowGuideMarkdown = ({ workflowName, isBigScreen }: { workflowName:
82
97
 
83
98
  return (
84
99
  <div css={isBigScreen ? guideBodyStyle : guideStackedBodyStyle}>
85
- {(isLoading && <EuiLoadingSpinner size="m" />)
86
- || ((isError || !content) && <EuiText color="subdued">{t('noGuideAvailable')}</EuiText>) || (
87
- <EuiPanel paddingSize="m" hasShadow css={isBigScreen ? guidePanelStyle : guideStackedPanelStyle}>
100
+ {(isLoading && <EuiLoadingSpinner size="m" />) || (
101
+ <EuiPanel paddingSize="m" hasShadow css={isBigScreen ? guidePanelStyle : guideStackedPanelStyle}>
102
+ {((isError || !content) && <EuiText color="subdued">{t('noGuideAvailable')}</EuiText>) || (
88
103
  <EuiMarkdownFormat>{content}</EuiMarkdownFormat>
89
- </EuiPanel>
90
- )}
104
+ )}
105
+ </EuiPanel>
106
+ )}
91
107
  </div>
92
108
  );
93
109
  };
@@ -97,14 +113,7 @@ export const WfoWorkflowGuideExpandablePanel = ({ workflowName, isExpanded, onTo
97
113
  const { fullHeightStyle, guideExpandedItemStyle, guideExpandedFillStyle } = useWithOrchestratorTheme(getStyles);
98
114
  const isBigScreen = useIsWithinBreakpoints(['xl', 'xxl']);
99
115
 
100
- const OpenGuideButton = () => (
101
- <EuiFlexItem grow={false}>
102
- <EuiFlexGroup gutterSize="xs" alignItems="center" direction={isBigScreen ? 'column' : 'row'}>
103
- <EuiIcon type={isBigScreen ? 'arrowLeft' : 'arrowDown'} size="xxl" color="primary" />
104
- <EuiIcon type={'info'} size="xxl" color="primary" />
105
- </EuiFlexGroup>
106
- </EuiFlexItem>
107
- );
116
+ const OpenGuideButton = () => <EuiIcon type={'info'} size="xxl" color="primary" />;
108
117
 
109
118
  const CloseGuideButton = () => (
110
119
  <EuiFlexItem grow={false}>
@@ -114,7 +123,7 @@ export const WfoWorkflowGuideExpandablePanel = ({ workflowName, isExpanded, onTo
114
123
 
115
124
  if (!isExpanded) {
116
125
  return (
117
- <WfoWorkflowGuideToggleStrip onToggle={onToggle} ariaLabel={t('show')} isBigScreen={isBigScreen}>
126
+ <WfoWorkflowGuideToggleStrip onToggle={onToggle} ariaLabel={t('show')} isBigScreen={isBigScreen} sticky>
118
127
  <OpenGuideButton />
119
128
  </WfoWorkflowGuideToggleStrip>
120
129
  );
@@ -3,6 +3,11 @@ import { css } from '@emotion/react';
3
3
  import { WfoThemeHelpers } from '@/hooks';
4
4
 
5
5
  export const getStyles = ({ theme }: WfoThemeHelpers) => {
6
+ const navigationHeight = theme.base * 3;
7
+ const timelineStickyHeight = theme.base * 4;
8
+ const stripBottomGap = theme.base;
9
+ const stripMinHeight = theme.base * 40;
10
+
6
11
  const fullHeightStyle = css({
7
12
  height: '100%',
8
13
  });
@@ -12,25 +17,56 @@ export const getStyles = ({ theme }: WfoThemeHelpers) => {
12
17
  cursor: 'pointer',
13
18
  });
14
19
 
20
+ const stickyStripContainerStyle = css({
21
+ width: theme.base * 4,
22
+ cursor: 'pointer',
23
+ alignSelf: 'stretch',
24
+ position: 'sticky',
25
+ top: timelineStickyHeight,
26
+ minHeight: stripMinHeight,
27
+ maxHeight: `calc(100vh - ${navigationHeight + timelineStickyHeight + stripBottomGap}px)`,
28
+ });
29
+
30
+ const stripIconStyle = css({
31
+ display: 'flex',
32
+ justifyContent: 'center',
33
+ });
34
+
35
+ const stickyPanelFillStyle = css({
36
+ flexGrow: 1,
37
+ });
38
+
15
39
  const toggleStripContainerHorizontalStyle = css({
16
40
  width: '100%',
17
41
  height: theme.base * 4,
18
42
  cursor: 'pointer',
19
43
  });
20
44
 
45
+ const stickyHorizontalStripContainerStyle = css({
46
+ width: '100%',
47
+ height: theme.base * 4,
48
+ cursor: 'pointer',
49
+ position: 'sticky',
50
+ top: timelineStickyHeight,
51
+ zIndex: 2,
52
+ });
53
+
21
54
  const toggleStripPanelStyle = css({
22
55
  height: '100%',
23
56
  backgroundColor: theme.colors.backgroundBasePrimary,
24
- border: `2px dashed ${theme.colors.backgroundLightPrimary}`,
25
- transition: 'box-shadow 100ms ease-in-out, border-style 100ms ease-in-out',
57
+ border: `2px solid ${theme.colors.backgroundLightPrimary}`,
58
+ transition: 'border-color 100ms ease-in-out',
26
59
  '&:hover': {
27
- borderStyle: 'solid',
28
- boxShadow: `0 0 0 2px ${theme.colors.backgroundBasePrimary}`,
60
+ borderColor: theme.colors.borderBasePrimary,
29
61
  },
30
62
  });
31
63
 
32
64
  const guideExpandedItemStyle = css({
33
- position: 'relative',
65
+ alignSelf: 'stretch',
66
+ position: 'sticky',
67
+ top: timelineStickyHeight,
68
+ minHeight: stripMinHeight,
69
+ maxHeight: `calc(100vh - ${navigationHeight + timelineStickyHeight + stripBottomGap}px)`,
34
70
  });
35
71
 
36
72
  const guideExpandedFillStyle = css({
@@ -41,8 +77,10 @@ export const getStyles = ({ theme }: WfoThemeHelpers) => {
41
77
  const guidePanelStyle = css({
42
78
  height: '100%',
43
79
  overflowY: 'auto',
44
- border: `2px dashed ${theme.colors.backgroundLightPrimary}`,
80
+ border: `2px solid ${theme.colors.backgroundLightPrimary}`,
45
81
  borderRightWidth: 0,
82
+ borderTopRightRadius: 0,
83
+ borderBottomRightRadius: 0,
46
84
  });
47
85
 
48
86
  const guideBodyStyle = css({
@@ -54,10 +92,14 @@ export const getStyles = ({ theme }: WfoThemeHelpers) => {
54
92
 
55
93
  const noLeftBorderStyle = css({
56
94
  borderLeftWidth: 0,
95
+ borderTopLeftRadius: 0,
96
+ borderBottomLeftRadius: 0,
57
97
  });
58
98
 
59
99
  const noBottomBorderStyle = css({
60
100
  borderBottomWidth: 0,
101
+ borderBottomLeftRadius: 0,
102
+ borderBottomRightRadius: 0,
61
103
  });
62
104
 
63
105
  const guideStackedBodyStyle = css({
@@ -66,14 +108,20 @@ export const getStyles = ({ theme }: WfoThemeHelpers) => {
66
108
  });
67
109
 
68
110
  const guideStackedPanelStyle = css({
69
- border: `2px dashed ${theme.colors.backgroundLightPrimary}`,
111
+ border: `2px solid ${theme.colors.backgroundLightPrimary}`,
70
112
  borderTopWidth: 0,
113
+ borderTopLeftRadius: 0,
114
+ borderTopRightRadius: 0,
71
115
  });
72
116
 
73
117
  return {
74
118
  fullHeightStyle,
75
119
  toggleStripContainerStyle,
120
+ stickyStripContainerStyle,
121
+ stripIconStyle,
122
+ stickyPanelFillStyle,
76
123
  toggleStripContainerHorizontalStyle,
124
+ stickyHorizontalStripContainerStyle,
77
125
  toggleStripPanelStyle,
78
126
  noLeftBorderStyle,
79
127
  noBottomBorderStyle,
@@ -34,7 +34,7 @@ export const METADATA_PRODUCT_ENDPOINT = 'products';
34
34
  export const METADATA_PRODUCT_BLOCK_ENDPOINT = 'product_blocks';
35
35
  export const METADATA_RESOURCE_TYPE_ENDPOINT = 'resource_types';
36
36
  export const METADATA_WORKFLOWS_ENDPOINT = 'workflows';
37
- export const METADATA_SCHEDULES_ENDPOINT = 'schedules';
37
+ export const METADATA_SCHEDULES_ENDPOINT = 'schedules/';
38
38
 
39
39
  //search
40
40
  export const SEARCH_QUERY_RESULTS_ENDPOINT = 'search/queries';
@@ -1 +1 @@
1
- export const ORCHESTRATOR_UI_LIBRARY_VERSION = '8.7.2';
1
+ export const ORCHESTRATOR_UI_LIBRARY_VERSION = '8.8.1';
@@ -0,0 +1,64 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+
3
+ import { EntityKind } from '@/types';
4
+
5
+ import { useFieldsPathInfo } from './usePathAutoComplete';
6
+
7
+ const fetchPathsMock = jest.fn();
8
+
9
+ jest.mock('@/rtk/endpoints', () => ({
10
+ useSearchPathsQuery: jest.fn(),
11
+ useLazySearchPathsQuery: () => [fetchPathsMock],
12
+ useSearchDefinitionsQuery: () => ({
13
+ data: {
14
+ boolean: {
15
+ operators: ['eq', 'neq'],
16
+ value_schema: { eq: { kind: 'boolean' }, neq: { kind: 'boolean' } },
17
+ },
18
+ },
19
+ isError: false,
20
+ }),
21
+ }));
22
+
23
+ describe('useFieldsPathInfo', () => {
24
+ beforeEach(() => {
25
+ fetchPathsMock.mockImplementation(({ q }: { q: string }) => ({
26
+ unwrap: () =>
27
+ Promise.resolve(
28
+ q === 'lldp' ?
29
+ { leaves: [{ name: 'lldp', ui_types: ['boolean'], paths: [] }], components: [] }
30
+ : { leaves: [], components: [] },
31
+ ),
32
+ }));
33
+ });
34
+
35
+ it('resolves the path info of an exactly matching leaf', async () => {
36
+ const { result } = renderHook(() => useFieldsPathInfo(['lldp'], EntityKind.SUBSCRIPTION));
37
+
38
+ await waitFor(() => expect(result.current.get('lldp')).toBeTruthy());
39
+ expect(result.current.get('lldp')).toMatchObject({
40
+ path: 'lldp',
41
+ ui_types: ['boolean'],
42
+ operators: ['eq', 'neq'],
43
+ });
44
+ });
45
+
46
+ it('stores null for a field the backend does not know', async () => {
47
+ const { result } = renderHook(() => useFieldsPathInfo(['nonexistent'], EntityKind.SUBSCRIPTION));
48
+
49
+ await waitFor(() => expect(result.current.has('nonexistent')).toBe(true));
50
+ expect(result.current.get('nonexistent')).toBeNull();
51
+ });
52
+
53
+ it('looks up each field at most once across rerenders', async () => {
54
+ const { result, rerender } = renderHook(({ fields }) => useFieldsPathInfo(fields, EntityKind.SUBSCRIPTION), {
55
+ initialProps: { fields: ['lldp'] },
56
+ });
57
+
58
+ await waitFor(() => expect(result.current.has('lldp')).toBe(true));
59
+ rerender({ fields: ['lldp'] });
60
+ rerender({ fields: ['lldp'] });
61
+
62
+ expect(fetchPathsMock).toHaveBeenCalledTimes(1);
63
+ });
64
+ });
@@ -1,7 +1,7 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
2
 
3
- import { useSearchDefinitionsQuery, useSearchPathsQuery } from '@/rtk/endpoints';
4
- import { EntityKind, PathInfo, value_schema } from '@/types';
3
+ import { useLazySearchPathsQuery, useSearchDefinitionsQuery, useSearchPathsQuery } from '@/rtk/endpoints';
4
+ import { EntityKind, PathAutocompleteResponse, PathInfo, value_schema } from '@/types';
5
5
 
6
6
  import { useDebounce } from './useDebounce';
7
7
 
@@ -50,6 +50,52 @@ const FALLBACK_DEFINITIONS: Record<
50
50
  },
51
51
  };
52
52
 
53
+ type SearchDefinitions = typeof FALLBACK_DEFINITIONS;
54
+
55
+ const mapPathAutocompleteResponseToPathInfos = (
56
+ pathData: PathAutocompleteResponse,
57
+ definitions: SearchDefinitions,
58
+ ): PathInfo[] => {
59
+ const enrichedPaths: PathInfo[] = [];
60
+
61
+ // Process leaves first
62
+ (pathData.leaves || []).forEach((leaf) => {
63
+ const primaryType = leaf.ui_types[0] || 'string';
64
+ const typeDefinition = definitions[primaryType];
65
+
66
+ enrichedPaths.push({
67
+ path: leaf.name,
68
+ type: primaryType as 'string' | 'number' | 'datetime' | 'boolean',
69
+ operators: typeDefinition?.operators || [],
70
+ value_schema: typeDefinition?.value_schema || {},
71
+ group: 'leaf',
72
+ displayLabel: leaf.name,
73
+ ui_types: leaf.ui_types,
74
+ availablePaths: leaf.paths || [],
75
+ pathCount: leaf.paths ? leaf.paths?.length : 0,
76
+ });
77
+ });
78
+
79
+ (pathData.components || []).forEach((component) => {
80
+ const primaryType = component.ui_types[0] || 'string';
81
+ const typeDefinition = definitions[primaryType];
82
+
83
+ enrichedPaths.push({
84
+ path: component.name,
85
+ type: 'component',
86
+ operators: typeDefinition?.operators || [],
87
+ value_schema: typeDefinition?.value_schema || {},
88
+ group: 'component',
89
+ displayLabel: component.name,
90
+ ui_types: component.ui_types,
91
+ availablePaths: component.paths || [],
92
+ pathCount: component.paths ? component.paths?.length : 0,
93
+ });
94
+ });
95
+
96
+ return enrichedPaths;
97
+ };
98
+
53
99
  export const usePathAutocomplete = (prefix: string, entityType: EntityKind) => {
54
100
  const [paths, setPaths] = useState<PathInfo[]>([]);
55
101
  const debouncedPrefix = useDebounce(prefix, 300);
@@ -71,44 +117,7 @@ export const usePathAutocomplete = (prefix: string, entityType: EntityKind) => {
71
117
  return;
72
118
  }
73
119
 
74
- const enrichedPaths: PathInfo[] = [];
75
-
76
- // Process leaves first
77
- (pathData.leaves || []).forEach((leaf) => {
78
- const primaryType = leaf.ui_types[0] || 'string';
79
- const typeDefinition = definitions[primaryType];
80
-
81
- enrichedPaths.push({
82
- path: leaf.name,
83
- type: primaryType as 'string' | 'number' | 'datetime' | 'boolean',
84
- operators: typeDefinition?.operators || [],
85
- value_schema: typeDefinition?.value_schema || {},
86
- group: 'leaf',
87
- displayLabel: leaf.name,
88
- ui_types: leaf.ui_types,
89
- availablePaths: leaf.paths || [],
90
- pathCount: leaf.paths ? leaf.paths?.length : 0,
91
- });
92
- });
93
-
94
- (pathData.components || []).forEach((component) => {
95
- const primaryType = component.ui_types[0] || 'string';
96
- const typeDefinition = definitions[primaryType];
97
-
98
- enrichedPaths.push({
99
- path: component.name,
100
- type: 'component',
101
- operators: typeDefinition?.operators || [],
102
- value_schema: typeDefinition?.value_schema || {},
103
- group: 'component',
104
- displayLabel: component.name,
105
- ui_types: component.ui_types,
106
- availablePaths: component.paths || [],
107
- pathCount: component.paths ? component.paths?.length : 0,
108
- });
109
- });
110
-
111
- setPaths(enrichedPaths);
120
+ setPaths(mapPathAutocompleteResponseToPathInfos(pathData, definitions));
112
121
  }, [pathData, definitions, debouncedPrefix?.length]);
113
122
 
114
123
  const errorMessage =
@@ -118,3 +127,51 @@ export const usePathAutocomplete = (prefix: string, entityType: EntityKind) => {
118
127
 
119
128
  return { paths, loading: isLoading, error: errorMessage };
120
129
  };
130
+
131
+ /**
132
+ * Resolves PathInfo for exact field paths that were never picked through the field
133
+ * selector, e.g. the fields of a query restored from the URL. Returns a map that gains
134
+ * an entry per field once its lookup settles: the matching PathInfo, or null when the
135
+ * backend does not know the path. Each field is looked up at most once.
136
+ */
137
+ export const useFieldsPathInfo = (fields: string[], entityType: EntityKind) => {
138
+ const [fieldsPathInfo, setFieldsPathInfo] = useState<Map<string, PathInfo | null>>(new Map());
139
+ const requestedFieldsRef = useRef<Set<string>>(new Set());
140
+ const [fetchPaths] = useLazySearchPathsQuery();
141
+ const { data: definitions, isError: definitionsFailed } = useSearchDefinitionsQuery();
142
+
143
+ // Wait for the definitions request to settle so resolved fields get the backend's
144
+ // operator lists instead of the fallback ones.
145
+ const settledDefinitions = definitions ?? (definitionsFailed ? FALLBACK_DEFINITIONS : undefined);
146
+
147
+ useEffect(() => {
148
+ // Changing entity type invalidates previous lookups.
149
+ requestedFieldsRef.current = new Set();
150
+ setFieldsPathInfo(new Map());
151
+ }, [entityType]);
152
+
153
+ useEffect(() => {
154
+ if (!settledDefinitions) {
155
+ return;
156
+ }
157
+ const newFields = fields.filter((field) => field && !requestedFieldsRef.current.has(field));
158
+ newFields.forEach((field) => {
159
+ requestedFieldsRef.current.add(field);
160
+ fetchPaths({ q: field, entity_type: entityType }, true)
161
+ .unwrap()
162
+ .then((pathData) => {
163
+ const pathInfos = mapPathAutocompleteResponseToPathInfos(pathData, settledDefinitions);
164
+ const match =
165
+ pathInfos.find((pathInfo) => pathInfo.path === field)
166
+ ?? pathInfos.find((pathInfo) => pathInfo.availablePaths?.includes(field));
167
+ setFieldsPathInfo((previous) => new Map(previous).set(field, match ?? null));
168
+ })
169
+ .catch(() => {
170
+ // Allow a retry on a later render, e.g. after a transient network error.
171
+ requestedFieldsRef.current.delete(field);
172
+ });
173
+ });
174
+ }, [fields, entityType, fetchPaths, settledDefinitions]);
175
+
176
+ return fieldsPathInfo;
177
+ };
@@ -23,6 +23,7 @@ export const useStoredTableConfig = <T>(localeStorageKey: string) => {
23
23
  if (storedConfig) {
24
24
  tableConfig.hiddenColumns = storedConfig.hiddenColumns;
25
25
  tableConfig.selectedPageSize = storedConfig.selectedPageSize;
26
+ tableConfig.showMatchDetails = storedConfig.showMatchDetails;
26
27
  }
27
28
  return tableConfig;
28
29
  } catch {