@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.
- package/.turbo/turbo-build.log +8 -8
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +17 -10
- package/CHANGELOG.md +28 -0
- package/dist/index.d.ts +506 -19
- package/dist/index.js +614 -399
- package/dist/index.js.map +1 -1
- package/jest.config.cjs +9 -8
- package/package.json +1 -1
- package/src/components/WfoBadges/WfoWorkflowTargetBadge/WfoWorkflowTargetBadge.tsx +2 -2
- package/src/components/WfoContentHeader/WfoContentHeader.tsx +1 -1
- package/src/components/WfoInlineNoteEdit/WfoProcessListNoteEdit.spec.tsx +35 -0
- package/src/components/WfoInlineNoteEdit/WfoProcessListNoteEdit.tsx +7 -9
- package/src/components/WfoProcessList/WfoProcessListDeltaPopover.tsx +1 -1
- package/src/components/WfoProcessList/WfoProcessesList.tsx +7 -5
- package/src/components/WfoSubscription/WfoProcessesTimeline.tsx +21 -7
- package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActions.tsx +25 -9
- package/src/components/WfoSubscription/WfoSubscriptionGeneralSections/WfoSubscriptionDetailSection.tsx +1 -1
- package/src/components/WfoSubscription/styles.ts +6 -0
- package/src/components/WfoSubscription/utils/utils.ts +2 -4
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoExpandingSearchRow.tsx +0 -1
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoFilterBuilder.tsx +30 -3
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoOperatorSelector.spec.tsx +143 -0
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoOperatorSelector.tsx +33 -11
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoRestoreLoop.spec.tsx +130 -0
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoStructuredSearchTable.tsx +40 -1
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoValueEditor.spec.tsx +60 -0
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoValueEditor.tsx +22 -4
- package/src/components/WfoTable/WfoStructuredSearchTable/styles.ts +8 -9
- package/src/components/WfoTable/WfoStructuredSearchTable/utils.spec.ts +56 -0
- package/src/components/WfoTable/WfoStructuredSearchTable/utils.ts +40 -1
- package/src/components/WfoTable/WfoTable/WfoTable.tsx +5 -1
- package/src/components/WfoTable/WfoTable/WfoTableDataRows.tsx +11 -2
- package/src/components/WfoTable/WfoTable/WfoTableExpandedRowReveal.spec.tsx +120 -0
- package/src/components/WfoTable/WfoTable/styles.ts +6 -0
- package/src/components/WfoTable/utils/tableConfigPersistence.ts +1 -0
- package/src/components/WfoTimeline/styles.ts +1 -1
- package/src/components/WfoWorkflowUserGuide/WfoPageWithUserGuide.tsx +8 -2
- package/src/components/WfoWorkflowUserGuide/WfoWorkflowGuideExpandablePanel.tsx +39 -30
- package/src/components/WfoWorkflowUserGuide/styles.ts +55 -7
- package/src/configuration/constants.ts +1 -1
- package/src/configuration/version.ts +1 -1
- package/src/hooks/usePathAutoComplete.spec.tsx +64 -0
- package/src/hooks/usePathAutoComplete.ts +98 -41
- package/src/hooks/useStoredTableConfig.ts +1 -0
- package/src/messages/en-GB.json +3 -1
- package/src/messages/nl-NL.json +4 -2
- package/src/pages/WfoSearchPocPage.tsx +19 -18
- package/src/pages/tasks/WfoTasksListPage.tsx +1 -0
- package/src/rtk/endpoints/search.ts +1 -0
- package/src/types/types.ts +11 -11
- 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:
|
|
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
|
|
32
|
+
<EuiFlexGroup
|
|
33
|
+
gutterSize="s"
|
|
34
|
+
direction={isBigScreen ? 'row' : 'column'}
|
|
35
|
+
alignItems={isBigScreen ? 'flexStart' : 'stretch'}
|
|
36
|
+
>
|
|
33
37
|
{!isBigScreen && workflowGuideExpandablePanel}
|
|
34
|
-
<EuiFlexItem grow={true}
|
|
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={[
|
|
68
|
+
css={[
|
|
69
|
+
toggleStripPanelStyle,
|
|
70
|
+
sticky && stickyPanelFillStyle,
|
|
71
|
+
noLeadingBorder && (isBigScreen ? noLeftBorderStyle : noBottomBorderStyle),
|
|
72
|
+
]}
|
|
61
73
|
>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
87
|
-
<
|
|
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
|
-
|
|
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
|
|
25
|
-
transition: '
|
|
57
|
+
border: `2px solid ${theme.colors.backgroundLightPrimary}`,
|
|
58
|
+
transition: 'border-color 100ms ease-in-out',
|
|
26
59
|
'&:hover': {
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 {
|