@orchestrator-ui/orchestrator-ui-components 8.9.3 → 9.0.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.
- package/.turbo/turbo-build.log +8 -8
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +17 -50
- package/CHANGELOG.md +19 -0
- package/dist/index.d.ts +2081 -2517
- package/dist/index.js +1247 -1579
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/WfoError/WfoError.tsx +2 -2
- package/src/components/WfoInlineNoteEdit/WfoSubscriptionDetailNoteEdit.tsx +4 -8
- package/src/components/WfoInlineNoteEdit/WfoSubscriptionNoteEdit.tsx +6 -7
- package/src/components/WfoPageTemplate/paths.ts +0 -1
- package/src/components/WfoSubscriptionsList/index.ts +1 -2
- package/src/components/WfoSubscriptionsList/subscriptionListItem.ts +14 -0
- package/src/components/WfoSubscriptionsList/subscriptionListTabs.ts +1 -20
- package/src/components/WfoSummary/WfoLatestActiveSubscriptionsSummaryCard.tsx +17 -13
- package/src/components/WfoSummary/WfoLatestOutOfSyncSubscriptionSummaryCard.tsx +28 -15
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoFieldSelector.spec.tsx +0 -16
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoFieldSelector.tsx +4 -10
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoFilterBuilder.tsx +9 -6
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoRestoreLoop.spec.tsx +2 -2
- package/src/components/WfoTable/WfoStructuredSearchTable/WfoStructuredSearchTable.tsx +3 -5
- package/src/components/WfoTable/WfoStructuredSearchTable/useSearchWithDebouncedCallback.spec.tsx +56 -3
- package/src/components/WfoTable/WfoStructuredSearchTable/utils.spec.ts +60 -1
- package/src/components/WfoTable/WfoStructuredSearchTable/utils.ts +27 -3
- package/src/configuration/version.ts +1 -1
- package/src/messages/en-GB.json +1 -1
- package/src/messages/nl-NL.json +1 -1
- package/src/pages/index.ts +0 -1
- package/src/pages/startPage/index.ts +1 -0
- package/src/pages/startPage/mappers.ts +15 -8
- package/src/pages/startPage/queryVariables.ts +0 -29
- package/src/pages/startPage/searchPayloads.ts +22 -0
- package/src/pages/subscriptions/WfoSubscriptionsListPage.tsx +511 -39
- package/src/rtk/endpoints/index.ts +0 -1
- package/src/rtk/endpoints/search.ts +3 -0
- package/src/rtk/endpoints/subscriptionListMutation.spec.ts +80 -0
- package/src/rtk/endpoints/subscriptionListMutation.ts +71 -52
- package/src/rtk/utils.spec.ts +30 -1
- package/src/rtk/utils.ts +2 -1
- package/src/types/search.ts +0 -1
- package/src/types/types.ts +0 -2
- package/src/utils/getDefaultTableConfig.ts +1 -6
- package/src/utils/getQueryParams.ts +1 -0
- package/src/components/WfoSubscriptionsList/WfoSubscriptionsList.tsx +0 -243
- package/src/components/WfoSubscriptionsList/subscriptionResultMappers.ts +0 -49
- package/src/pages/WfoSearchPocPage.tsx +0 -575
- package/src/rtk/endpoints/subscriptionListSummary.ts +0 -66
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@ export const WfoError = () => {
|
|
|
17
17
|
const { theme } = useOrchestratorTheme();
|
|
18
18
|
return (
|
|
19
19
|
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s">
|
|
20
|
-
<WfoXCircleFill color={theme.colors.danger} />
|
|
20
|
+
<WfoXCircleFill color={theme.colors.danger} width={32} height={32} />
|
|
21
21
|
<h1>{t('errorMessage')}</h1>
|
|
22
22
|
</EuiFlexGroup>
|
|
23
23
|
);
|
|
@@ -30,7 +30,7 @@ export const WfoErrorWithMessage = ({ error }: WfoErrorWithMessageProps) => {
|
|
|
30
30
|
return (
|
|
31
31
|
<>
|
|
32
32
|
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s">
|
|
33
|
-
<WfoXCircleFill color={theme.colors.danger} />
|
|
33
|
+
<WfoXCircleFill color={theme.colors.danger} width={32} height={32} />
|
|
34
34
|
<EuiText color={theme.colors.textDanger}>
|
|
35
35
|
{t('errorMessage')} ({message})
|
|
36
36
|
</EuiText>
|
|
@@ -3,7 +3,7 @@ import React from 'react';
|
|
|
3
3
|
|
|
4
4
|
import { useGetSubscriptionDetailQuery } from '@/rtk';
|
|
5
5
|
import { useStartProcessMutation } from '@/rtk/endpoints/forms';
|
|
6
|
-
import {
|
|
6
|
+
import { useUpdateSubscriptionNoteOptimisticMutation } from '@/rtk/endpoints/subscriptionListMutation';
|
|
7
7
|
import { SubscriptionDetail } from '@/types';
|
|
8
8
|
import { INVISIBLE_CHARACTER } from '@/utils';
|
|
9
9
|
|
|
@@ -18,13 +18,13 @@ export const WfoSubscriptionDetailNoteEdit: FC<WfoSubscriptionDetailNoteEditProp
|
|
|
18
18
|
subscriptionId,
|
|
19
19
|
onlyShowOnHover = false,
|
|
20
20
|
}) => {
|
|
21
|
-
const { data
|
|
21
|
+
const { data } = useGetSubscriptionDetailQuery({
|
|
22
22
|
subscriptionId,
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
const selectedItem = data?.subscription ?? { note: '' };
|
|
26
26
|
const [startProcess] = useStartProcessMutation();
|
|
27
|
-
const [
|
|
27
|
+
const [updateSubscriptionNoteOptimistic] = useUpdateSubscriptionNoteOptimisticMutation();
|
|
28
28
|
|
|
29
29
|
const triggerNoteModifyWorkflow = (note: string) => {
|
|
30
30
|
const noteModifyPayload = [{ subscription_id: subscriptionId }, { note: note }];
|
|
@@ -33,11 +33,7 @@ export const WfoSubscriptionDetailNoteEdit: FC<WfoSubscriptionDetailNoteEditProp
|
|
|
33
33
|
userInputs: noteModifyPayload,
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
queryName: endpointName ?? '',
|
|
38
|
-
subscriptionId: subscriptionId,
|
|
39
|
-
note: note,
|
|
40
|
-
});
|
|
36
|
+
updateSubscriptionNoteOptimistic({ subscriptionId, note });
|
|
41
37
|
};
|
|
42
38
|
|
|
43
39
|
return (
|
|
@@ -8,7 +8,7 @@ import { INVISIBLE_CHARACTER } from '@/utils';
|
|
|
8
8
|
|
|
9
9
|
interface WfoSubscriptionNoteEditProps {
|
|
10
10
|
onlyShowOnHover?: boolean;
|
|
11
|
-
queryVariables:
|
|
11
|
+
queryVariables: object;
|
|
12
12
|
endpointName: string | undefined;
|
|
13
13
|
subscriptionId: string;
|
|
14
14
|
note: string | null;
|
|
@@ -22,7 +22,7 @@ export const WfoSubscriptionNoteEdit: FC<WfoSubscriptionNoteEditProps> = ({
|
|
|
22
22
|
note,
|
|
23
23
|
}) => {
|
|
24
24
|
const [startProcess] = useStartProcessMutation();
|
|
25
|
-
const [
|
|
25
|
+
const [updateSubscriptionNoteOptimistic] = useUpdateSubscriptionNoteOptimisticMutation();
|
|
26
26
|
|
|
27
27
|
const triggerNoteModifyWorkflow = (note: string) => {
|
|
28
28
|
const noteModifyPayload = [{ subscription_id: subscriptionId }, { note: note === INVISIBLE_CHARACTER ? '' : note }];
|
|
@@ -31,11 +31,10 @@ export const WfoSubscriptionNoteEdit: FC<WfoSubscriptionNoteEditProps> = ({
|
|
|
31
31
|
userInputs: noteModifyPayload,
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
note: note,
|
|
34
|
+
updateSubscriptionNoteOptimistic({
|
|
35
|
+
subscriptionId,
|
|
36
|
+
note,
|
|
37
|
+
listQuery: { queryName: endpointName ?? '', queryVariables },
|
|
39
38
|
});
|
|
40
39
|
};
|
|
41
40
|
|
|
@@ -3,7 +3,6 @@ export const PATH_START_NEW_WORKFLOW = '/workflows/new';
|
|
|
3
3
|
export const PATH_START_NEW_TASK = '/tasks/new';
|
|
4
4
|
export const PATH_WORKFLOWS = '/workflows';
|
|
5
5
|
export const PATH_SUBSCRIPTIONS = '/subscriptions';
|
|
6
|
-
export const PATH_SUBSCRIPTIONS_BETA = '/beta-subscriptions';
|
|
7
6
|
export const PATH_METADATA = '/metadata';
|
|
8
7
|
export const PATH_METADATA_PRODUCTS = '/metadata/products';
|
|
9
8
|
export const PATH_METADATA_PRODUCT_BLOCKS = '/metadata/productblocks';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Subscription } from '@/types';
|
|
2
|
+
|
|
3
|
+
export type SubscriptionListItem = Pick<
|
|
4
|
+
Subscription,
|
|
5
|
+
'subscriptionId' | 'description' | 'status' | 'insync' | 'note'
|
|
6
|
+
> & {
|
|
7
|
+
startDate: Date | null;
|
|
8
|
+
endDate: Date | null;
|
|
9
|
+
productName: string;
|
|
10
|
+
tag: string | null;
|
|
11
|
+
customerFullname: string;
|
|
12
|
+
customerShortcode: string;
|
|
13
|
+
metadata: object | null;
|
|
14
|
+
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { WfoFilterTab } from '../../components';
|
|
2
|
-
import { SubscriptionListItem } from './subscriptionResultMappers';
|
|
3
2
|
|
|
4
3
|
export enum WfoSubscriptionListTab {
|
|
5
4
|
ACTIVE = 'ACTIVE',
|
|
@@ -8,36 +7,18 @@ export enum WfoSubscriptionListTab {
|
|
|
8
7
|
ALL = 'ALL',
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
export const subscriptionListTabs: WfoFilterTab<WfoSubscriptionListTab
|
|
10
|
+
export const subscriptionListTabs: WfoFilterTab<WfoSubscriptionListTab>[] = [
|
|
12
11
|
{
|
|
13
12
|
id: WfoSubscriptionListTab.ACTIVE,
|
|
14
13
|
translationKey: 'active',
|
|
15
|
-
alwaysOnFilters: [
|
|
16
|
-
{
|
|
17
|
-
field: 'status',
|
|
18
|
-
value: 'active',
|
|
19
|
-
},
|
|
20
|
-
],
|
|
21
14
|
},
|
|
22
15
|
{
|
|
23
16
|
id: WfoSubscriptionListTab.TERMINATED,
|
|
24
17
|
translationKey: 'terminated',
|
|
25
|
-
alwaysOnFilters: [
|
|
26
|
-
{
|
|
27
|
-
field: 'status',
|
|
28
|
-
value: 'terminated',
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
18
|
},
|
|
32
19
|
{
|
|
33
20
|
id: WfoSubscriptionListTab.TRANSIENT,
|
|
34
21
|
translationKey: 'transient',
|
|
35
|
-
alwaysOnFilters: [
|
|
36
|
-
{
|
|
37
|
-
field: 'status',
|
|
38
|
-
value: 'initial-provisioning-migrating',
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
22
|
},
|
|
42
23
|
{
|
|
43
24
|
id: WfoSubscriptionListTab.ALL,
|
|
@@ -3,30 +3,34 @@ import React from 'react';
|
|
|
3
3
|
import { useTranslations } from 'next-intl';
|
|
4
4
|
|
|
5
5
|
import { PATH_SUBSCRIPTIONS, SummaryCardStatus, WfoSummaryCard } from '@/components';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { mapSubscriptionSearchResultToSummaryCardListItem } from '@/pages/startPage/mappers';
|
|
7
|
+
import { getSubscriptionSummarySearchPayload } from '@/pages/startPage/searchPayloads';
|
|
8
|
+
import { useSearchQuery } from '@/rtk';
|
|
9
|
+
import { Filter } from '@/types';
|
|
9
10
|
import { optionalArrayMapper } from '@/utils';
|
|
10
11
|
|
|
12
|
+
// Elasticsearch equivalent of the list page's default Active tab filter
|
|
13
|
+
// (`subscription.status == "active"`), so the count on the card matches the list.
|
|
14
|
+
const activeSubscriptionsFilter = {
|
|
15
|
+
bool: {
|
|
16
|
+
must: [{ term: { 'subscription.status': 'active' } }],
|
|
17
|
+
},
|
|
18
|
+
} as unknown as Filter;
|
|
19
|
+
|
|
20
|
+
const activeSubscriptionsSearchPayload = getSubscriptionSummarySearchPayload(activeSubscriptionsFilter);
|
|
21
|
+
|
|
11
22
|
export const WfoLatestActiveSubscriptionsSummaryCard = () => {
|
|
12
23
|
const t = useTranslations('startPage.activeSubscriptions');
|
|
13
24
|
|
|
14
|
-
const {
|
|
15
|
-
data: subscriptionsSummaryResult,
|
|
16
|
-
isFetching,
|
|
17
|
-
isLoading,
|
|
18
|
-
} = useGetSubscriptionSummaryListQuery(subscriptionsListSummaryQueryVariables);
|
|
25
|
+
const { data, isFetching, isLoading } = useSearchQuery(activeSubscriptionsSearchPayload);
|
|
19
26
|
|
|
20
27
|
return (
|
|
21
28
|
<WfoSummaryCard
|
|
22
29
|
headerTitle={t('headerTitle')}
|
|
23
|
-
headerValue={
|
|
30
|
+
headerValue={data?.cursor?.total_items ?? '-'}
|
|
24
31
|
headerStatus={SummaryCardStatus.Neutral}
|
|
25
32
|
listTitle={t('listTitle')}
|
|
26
|
-
listItems={optionalArrayMapper(
|
|
27
|
-
subscriptionsSummaryResult?.subscriptions,
|
|
28
|
-
mapSubscriptionSummaryToSummaryCardListItem,
|
|
29
|
-
)}
|
|
33
|
+
listItems={optionalArrayMapper(data?.data, mapSubscriptionSearchResultToSummaryCardListItem)}
|
|
30
34
|
button={{ name: t('buttonText'), url: PATH_SUBSCRIPTIONS }}
|
|
31
35
|
isLoading={isLoading}
|
|
32
36
|
isFetching={isFetching}
|
|
@@ -3,37 +3,50 @@ import React from 'react';
|
|
|
3
3
|
import { useTranslations } from 'next-intl';
|
|
4
4
|
|
|
5
5
|
import { PATH_SUBSCRIPTIONS, SummaryCardStatus, WfoSubscriptionListTab, WfoSummaryCard } from '@/components';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { mapSubscriptionSearchResultToSummaryCardListItem } from '@/pages/startPage/mappers';
|
|
7
|
+
import { getSubscriptionSummarySearchPayload } from '@/pages/startPage/searchPayloads';
|
|
8
|
+
import { useSearchQuery } from '@/rtk';
|
|
9
|
+
import { Filter } from '@/types';
|
|
9
10
|
import { optionalArrayMapper } from '@/utils';
|
|
10
11
|
import { WfoQueryParams, getUrlWithQueryParams } from '@/utils/getQueryParams';
|
|
11
12
|
|
|
13
|
+
const OUT_OF_SYNC_CEL =
|
|
14
|
+
'subscription.insync == false && (subscription.status == "provisioning" || subscription.status == "active")';
|
|
15
|
+
|
|
16
|
+
// Elasticsearch equivalent of OUT_OF_SYNC_CEL. The list page produces the same clauses from the
|
|
17
|
+
// CEL in the button link, so the count on the card matches the list.
|
|
18
|
+
const outOfSyncSubscriptionsFilter = {
|
|
19
|
+
bool: {
|
|
20
|
+
must: [
|
|
21
|
+
{ term: { 'subscription.insync': false } },
|
|
22
|
+
{
|
|
23
|
+
bool: {
|
|
24
|
+
should: [{ term: { 'subscription.status': 'active' } }, { term: { 'subscription.status': 'provisioning' } }],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
} as unknown as Filter;
|
|
30
|
+
|
|
31
|
+
const outOfSyncSubscriptionsSearchPayload = getSubscriptionSummarySearchPayload(outOfSyncSubscriptionsFilter);
|
|
32
|
+
|
|
12
33
|
export const WfoLatestOutOfSyncSubscriptionSummaryCard = () => {
|
|
13
34
|
const t = useTranslations('startPage.outOfSyncSubscriptions');
|
|
14
35
|
|
|
15
|
-
const {
|
|
16
|
-
data: outOfSyncSubscriptionsSummaryResult,
|
|
17
|
-
isFetching,
|
|
18
|
-
isLoading,
|
|
19
|
-
} = useGetSubscriptionSummaryListQuery(outOfSyncSubscriptionsListSummaryQueryVariables);
|
|
36
|
+
const { data, isFetching, isLoading } = useSearchQuery(outOfSyncSubscriptionsSearchPayload);
|
|
20
37
|
|
|
21
38
|
const queryParams = {
|
|
22
39
|
[WfoQueryParams.ACTIVE_TAB]: WfoSubscriptionListTab.ALL,
|
|
23
|
-
[WfoQueryParams.
|
|
24
|
-
[WfoQueryParams.QUERY_STRING]: 'status:(provisioning|active) insync:false',
|
|
40
|
+
[WfoQueryParams.FILTER_STRING]: OUT_OF_SYNC_CEL,
|
|
25
41
|
};
|
|
26
42
|
|
|
27
43
|
return (
|
|
28
44
|
<WfoSummaryCard
|
|
29
45
|
headerTitle={t('headerTitle')}
|
|
30
|
-
headerValue={
|
|
46
|
+
headerValue={data?.cursor?.total_items ?? '-'}
|
|
31
47
|
headerStatus={SummaryCardStatus.Error}
|
|
32
48
|
listTitle={t('listTitle')}
|
|
33
|
-
listItems={optionalArrayMapper(
|
|
34
|
-
outOfSyncSubscriptionsSummaryResult?.subscriptions,
|
|
35
|
-
mapSubscriptionSummaryToSummaryCardListItem,
|
|
36
|
-
)}
|
|
49
|
+
listItems={optionalArrayMapper(data?.data, mapSubscriptionSearchResultToSummaryCardListItem)}
|
|
37
50
|
button={{
|
|
38
51
|
name: t('buttonText'),
|
|
39
52
|
url: getUrlWithQueryParams(PATH_SUBSCRIPTIONS, queryParams),
|
|
@@ -38,11 +38,6 @@ const SAPS_PATH_INFO = {
|
|
|
38
38
|
availablePaths: ['saps.port'],
|
|
39
39
|
} as unknown as PathInfo;
|
|
40
40
|
|
|
41
|
-
const PREFILLED_FIELD_OPTIONS = new Map([
|
|
42
|
-
['subscription.insync', ['eq', 'neq']],
|
|
43
|
-
['subscription.status', ['eq', 'neq', 'like']],
|
|
44
|
-
]);
|
|
45
|
-
|
|
46
41
|
// The mocked backend only knows saps, returned for any prefix of it.
|
|
47
42
|
const mockAutocomplete = (loading = false) =>
|
|
48
43
|
jest.mocked(usePathAutocomplete).mockImplementation((prefix) => ({
|
|
@@ -56,7 +51,6 @@ const renderFieldSelector = (field: string = defaultPlaceholderFieldName) => {
|
|
|
56
51
|
const onFieldSelected = jest.fn();
|
|
57
52
|
const context: WfoQueryBuilderContext = {
|
|
58
53
|
onFieldSelected,
|
|
59
|
-
prefilledFieldOptions: PREFILLED_FIELD_OPTIONS,
|
|
60
54
|
fieldPathInfoMap: new Map(),
|
|
61
55
|
useAdvancedNestedSearch: true,
|
|
62
56
|
};
|
|
@@ -154,16 +148,6 @@ describe('WfoFieldSelector', () => {
|
|
|
154
148
|
expect(listedOptions()).toEqual(['startTypingToLoadOptions']);
|
|
155
149
|
});
|
|
156
150
|
|
|
157
|
-
it('lists matching prefilled fields alongside the backend paths', () => {
|
|
158
|
-
renderFieldSelector();
|
|
159
|
-
|
|
160
|
-
typeSearchTerm('ins');
|
|
161
|
-
expect(listedOptions()).toEqual(['subscription.insync']);
|
|
162
|
-
|
|
163
|
-
typeSearchTerm('s');
|
|
164
|
-
expect(listedOptions()).toEqual(['subscription.insync', 'subscription.status', 'saps', 'saps.port']);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
151
|
it('does not offer the raw typed text as an option and reports no match once loaded', () => {
|
|
168
152
|
renderFieldSelector();
|
|
169
153
|
|
|
@@ -37,7 +37,7 @@ interface WfoFieldSelectorProps extends Omit<FieldSelectorProps, 'context'> {
|
|
|
37
37
|
|
|
38
38
|
export const WfoFieldSelector: FC<WfoFieldSelectorProps> = ({ handleOnChange, disabled, rule, context }) => {
|
|
39
39
|
const { field } = rule;
|
|
40
|
-
const { useAdvancedNestedSearch,
|
|
40
|
+
const { useAdvancedNestedSearch, onFieldSelected } = context;
|
|
41
41
|
const [autoFocus] = useState(field === defaultPlaceholderFieldName);
|
|
42
42
|
const selectedField = field === defaultPlaceholderFieldName ? '' : field;
|
|
43
43
|
const [searchTerm, setSearchTerm] = useState('');
|
|
@@ -66,19 +66,13 @@ export const WfoFieldSelector: FC<WfoFieldSelectorProps> = ({ handleOnChange, di
|
|
|
66
66
|
error: errorMessage,
|
|
67
67
|
} = usePathAutocomplete(autocompletePrefix, EntityKind.SUBSCRIPTION);
|
|
68
68
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const prefilledFields = Array.from(prefilledFieldOptions.keys()).filter(matchesPrefix);
|
|
72
|
-
const autocompleteOptions = getOptionsFromPathInfo(paths).filter(
|
|
73
|
-
(option) => !prefilledFields.includes(option.value ?? ''),
|
|
74
|
-
);
|
|
69
|
+
const autocompleteOptions = getOptionsFromPathInfo(paths);
|
|
75
70
|
|
|
76
71
|
const startTypingHintOption: EuiComboBoxOptionOption<string> = {
|
|
77
72
|
label: t('startTypingToLoadOptions'),
|
|
78
73
|
disabled: true,
|
|
79
74
|
};
|
|
80
|
-
const options: EuiComboBoxOptionOption<string>[] =
|
|
81
|
-
autocompletePrefix ? [...prefilledFields.map(getOption), ...autocompleteOptions] : [startTypingHintOption];
|
|
75
|
+
const options: EuiComboBoxOptionOption<string>[] = autocompletePrefix ? autocompleteOptions : [startTypingHintOption];
|
|
82
76
|
|
|
83
77
|
const renderHintOption = (option: EuiComboBoxOptionOption<string>) => (
|
|
84
78
|
<EuiText size="xs" color="default">
|
|
@@ -103,7 +97,7 @@ export const WfoFieldSelector: FC<WfoFieldSelectorProps> = ({ handleOnChange, di
|
|
|
103
97
|
const matchingPath =
|
|
104
98
|
paths.find((path) => path.path === selectedValue)
|
|
105
99
|
?? paths.find((path) => path.availablePaths?.includes(selectedValue));
|
|
106
|
-
const operators = matchingPath?.operators ??
|
|
100
|
+
const operators = matchingPath?.operators ?? [];
|
|
107
101
|
|
|
108
102
|
onFieldSelected(selectedValue, operators, matchingPath);
|
|
109
103
|
};
|
|
@@ -11,10 +11,11 @@ import { useTranslations } from 'next-intl';
|
|
|
11
11
|
|
|
12
12
|
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
|
13
13
|
|
|
14
|
-
import { SearchParams, WfoAutoExpandableTextArea, WfoTextAnchor } from '@/components';
|
|
14
|
+
import { SearchParams, WfoAutoExpandableTextArea, WfoErrorWithMessage, WfoTextAnchor } from '@/components';
|
|
15
15
|
import { WfoApplyFilterButton } from '@/components/WfoTable/WfoStructuredSearchTable/WfoApplyFilterButton';
|
|
16
16
|
import { WfoCombinatorSelector } from '@/components/WfoTable/WfoStructuredSearchTable/WfoCombinatorSelector';
|
|
17
17
|
import { useFieldsPathInfo, useWithOrchestratorTheme } from '@/hooks';
|
|
18
|
+
import type { WfoGraphqlError } from '@/rtk';
|
|
18
19
|
import type { FieldToOperatorMap, PathInfo, WfoQueryBuilderContext } from '@/types';
|
|
19
20
|
import { EntityKind } from '@/types';
|
|
20
21
|
|
|
@@ -28,6 +29,7 @@ import { WfoValueEditor } from './WfoValueEditor';
|
|
|
28
29
|
import { getWfoStructuredSearchTableStyles } from './styles';
|
|
29
30
|
import {
|
|
30
31
|
collectRuleFields,
|
|
32
|
+
hasNestedRuleWithEmptyValue,
|
|
31
33
|
onAddGroupHandler,
|
|
32
34
|
operatorsToRQBOperatorOptionsMapper,
|
|
33
35
|
useSearchWithDebouncedCallback,
|
|
@@ -41,8 +43,8 @@ interface WfoFilterBuilderProps {
|
|
|
41
43
|
onUpdateQueryBuilder: (ruleGroup: RuleGroupType | false) => void;
|
|
42
44
|
handleSearch: (searchParams?: SearchParams) => void;
|
|
43
45
|
onToggleFilterBuilder: (isVisible: boolean) => void;
|
|
44
|
-
prefilledFieldOptions: FieldToOperatorMap;
|
|
45
46
|
useAdvancedNestedSearch?: boolean;
|
|
47
|
+
error?: WfoGraphqlError[];
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
const initialRuleGroup: RuleGroupType = {
|
|
@@ -58,13 +60,13 @@ export const WfoFilterBuilder = ({
|
|
|
58
60
|
queryBuilderRuleGroup = initialRuleGroup,
|
|
59
61
|
onUpdateQueryBuilder,
|
|
60
62
|
handleSearch,
|
|
61
|
-
prefilledFieldOptions,
|
|
62
63
|
onToggleFilterBuilder,
|
|
63
64
|
useAdvancedNestedSearch = true,
|
|
65
|
+
error,
|
|
64
66
|
}: WfoFilterBuilderProps) => {
|
|
65
67
|
const t = useTranslations('common');
|
|
66
68
|
const { queryBuilderContainerStyles } = useWithOrchestratorTheme(getWfoStructuredSearchTableStyles);
|
|
67
|
-
const [fieldToOperatorMap, setFieldToOperatorMap] = useState<FieldToOperatorMap>(
|
|
69
|
+
const [fieldToOperatorMap, setFieldToOperatorMap] = useState<FieldToOperatorMap>(new Map());
|
|
68
70
|
const [fieldPathInfoMap, setFieldPathInfoMap] = useState<Map<string, PathInfo>>(new Map());
|
|
69
71
|
|
|
70
72
|
// Enter in a value editor commits its value on blur, and that state update has not
|
|
@@ -77,6 +79,7 @@ export const WfoFilterBuilder = ({
|
|
|
77
79
|
const { handleSubmitSearchOnClick, pendingSearchRun, handleSubmitSearchOnEnter } = useSearchWithDebouncedCallback({
|
|
78
80
|
filterString,
|
|
79
81
|
isValidFilterString,
|
|
82
|
+
hasEmptyRuleValue: hasNestedRuleWithEmptyValue(queryBuilderRuleGroup),
|
|
80
83
|
searchCallback: () => handleSearch({ ruleGroup: latestRuleGroupRef.current }),
|
|
81
84
|
});
|
|
82
85
|
|
|
@@ -106,7 +109,6 @@ export const WfoFilterBuilder = ({
|
|
|
106
109
|
|
|
107
110
|
const queryBuilderContext: WfoQueryBuilderContext = {
|
|
108
111
|
onFieldSelected: handleFieldSelected,
|
|
109
|
-
prefilledFieldOptions,
|
|
110
112
|
fieldPathInfoMap,
|
|
111
113
|
useAdvancedNestedSearch,
|
|
112
114
|
};
|
|
@@ -173,7 +175,7 @@ export const WfoFilterBuilder = ({
|
|
|
173
175
|
/>
|
|
174
176
|
</EuiFlexItem>
|
|
175
177
|
|
|
176
|
-
<EuiFlexGroup direction={'rowReverse'} alignItems={'center'}>
|
|
178
|
+
<EuiFlexGroup direction={'rowReverse'} alignItems={'center'} gutterSize={'l'}>
|
|
177
179
|
<WfoApplyFilterButton
|
|
178
180
|
isDisabled={!isValidFilterString}
|
|
179
181
|
pendingSearchRun={pendingSearchRun}
|
|
@@ -189,6 +191,7 @@ export const WfoFilterBuilder = ({
|
|
|
189
191
|
onToggleFilterBuilder(false);
|
|
190
192
|
}}
|
|
191
193
|
/>
|
|
194
|
+
<EuiFlexItem>{error && <WfoErrorWithMessage error={error} />}</EuiFlexItem>
|
|
192
195
|
</EuiFlexGroup>
|
|
193
196
|
</EuiFlexGroup>
|
|
194
197
|
</EuiFlexGroup>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Regression test for the URL-restore render loop: a controlled QueryBuilder wired like
|
|
3
|
-
*
|
|
3
|
+
* WfoSubscriptionsListPage + WfoFilterBuilder, restoring `lldp == true` from CEL while the path
|
|
4
4
|
* info of `lldp` resolves asynchronously (boolean ui type + operator list).
|
|
5
5
|
*
|
|
6
6
|
* The rule group is deliberately created with parseCEL directly — without the rule ids
|
|
@@ -80,7 +80,7 @@ const Harness = ({ initialCel = 'lldp == true' }: { initialCel?: string }) => {
|
|
|
80
80
|
onQueryChange={(ruleGroup: RuleGroupType) => {
|
|
81
81
|
queryChangeLog.push(JSON.stringify(ruleGroup));
|
|
82
82
|
if (queryChangeLog.length > 20) return;
|
|
83
|
-
// Mirrors
|
|
83
|
+
// Mirrors WfoSubscriptionsListPage.onUpdateQueryBuilder
|
|
84
84
|
setQuery({ ...ruleGroup });
|
|
85
85
|
setFilterString(formatQuery({ ...ruleGroup }, { format: 'cel', fallbackExpression: '' }));
|
|
86
86
|
}}
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
import { useOrchestratorTheme, useWithOrchestratorTheme } from '@/hooks';
|
|
31
31
|
import { WfoArrowsExpand } from '@/icons';
|
|
32
32
|
import { WfoGraphqlError } from '@/rtk';
|
|
33
|
-
import {
|
|
33
|
+
import { RetrieverType } from '@/types';
|
|
34
34
|
import { getDefaultTableConfig } from '@/utils';
|
|
35
35
|
|
|
36
36
|
import { ColumnType, WfoTable, WfoTableProps } from '../WfoTable';
|
|
@@ -97,7 +97,6 @@ export type WfoStructuredSearchTableProps<T extends object> = Omit<
|
|
|
97
97
|
setPageSize: (updatedPageSize: number) => void;
|
|
98
98
|
totalItems: number | false;
|
|
99
99
|
hasNextPage: boolean;
|
|
100
|
-
prefilledFieldOptions: FieldToOperatorMap;
|
|
101
100
|
};
|
|
102
101
|
|
|
103
102
|
export const WfoStructuredSearchTable = <T extends object>({
|
|
@@ -131,7 +130,6 @@ export const WfoStructuredSearchTable = <T extends object>({
|
|
|
131
130
|
hasNextPage,
|
|
132
131
|
data,
|
|
133
132
|
isLoading,
|
|
134
|
-
prefilledFieldOptions,
|
|
135
133
|
...tableProps
|
|
136
134
|
}: WfoStructuredSearchTableProps<T>) => {
|
|
137
135
|
const { theme } = useOrchestratorTheme();
|
|
@@ -283,13 +281,13 @@ export const WfoStructuredSearchTable = <T extends object>({
|
|
|
283
281
|
onUpdateQueryBuilder={onUpdateQueryBuilder}
|
|
284
282
|
handleSearch={handleSearch}
|
|
285
283
|
onToggleFilterBuilder={setIsFilterBuilderVisible}
|
|
286
|
-
prefilledFieldOptions={prefilledFieldOptions}
|
|
287
284
|
useAdvancedNestedSearch={advancedNestedSearch}
|
|
285
|
+
error={error}
|
|
288
286
|
/>
|
|
289
287
|
</>
|
|
290
288
|
)}
|
|
291
289
|
|
|
292
|
-
{error && <WfoErrorWithMessage error={error} />}
|
|
290
|
+
{error && !isFilterBuilderVisible && <WfoErrorWithMessage error={error} />}
|
|
293
291
|
|
|
294
292
|
<EuiSpacer size="m" />
|
|
295
293
|
|
package/src/components/WfoTable/WfoStructuredSearchTable/useSearchWithDebouncedCallback.spec.tsx
CHANGED
|
@@ -5,11 +5,13 @@ import { act, renderHook } from '@testing-library/react';
|
|
|
5
5
|
|
|
6
6
|
import { FILTER_CHANGE_DEBOUNCE_DELAY, useSearchWithDebouncedCallback } from './utils';
|
|
7
7
|
|
|
8
|
+
type SearchHookProps = { filterString: string; hasEmptyRuleValue?: boolean };
|
|
9
|
+
|
|
8
10
|
const renderSearchHook = (searchCallback: () => void, initialFilterString = 'subscription.status == "active"') =>
|
|
9
11
|
renderHook(
|
|
10
|
-
({ filterString
|
|
11
|
-
useSearchWithDebouncedCallback({ filterString, isValidFilterString: true, searchCallback }),
|
|
12
|
-
{ initialProps: { filterString: initialFilterString } },
|
|
12
|
+
({ filterString, hasEmptyRuleValue = false }: SearchHookProps) =>
|
|
13
|
+
useSearchWithDebouncedCallback({ filterString, isValidFilterString: true, hasEmptyRuleValue, searchCallback }),
|
|
14
|
+
{ initialProps: { filterString: initialFilterString } as SearchHookProps },
|
|
13
15
|
);
|
|
14
16
|
|
|
15
17
|
describe('useSearchWithDebouncedCallback', () => {
|
|
@@ -91,4 +93,55 @@ describe('useSearchWithDebouncedCallback', () => {
|
|
|
91
93
|
});
|
|
92
94
|
expect(searchCallback).toHaveBeenCalledTimes(1);
|
|
93
95
|
});
|
|
96
|
+
|
|
97
|
+
it('does not schedule a search while a value editor is empty', () => {
|
|
98
|
+
const searchCallback = jest.fn();
|
|
99
|
+
const { result, rerender } = renderSearchHook(searchCallback);
|
|
100
|
+
|
|
101
|
+
// Clearing the value editor empties the value out of the filter string.
|
|
102
|
+
rerender({ filterString: 'subscription.status == ""', hasEmptyRuleValue: true });
|
|
103
|
+
expect(result.current.pendingSearchRun).toBeUndefined();
|
|
104
|
+
|
|
105
|
+
act(() => {
|
|
106
|
+
jest.advanceTimersByTime(FILTER_CHANGE_DEBOUNCE_DELAY);
|
|
107
|
+
});
|
|
108
|
+
expect(searchCallback).not.toHaveBeenCalled();
|
|
109
|
+
|
|
110
|
+
// Typing a value again resumes the automatic search.
|
|
111
|
+
rerender({ filterString: 'subscription.status == "a"', hasEmptyRuleValue: false });
|
|
112
|
+
expect(result.current.pendingSearchRun).toBeDefined();
|
|
113
|
+
|
|
114
|
+
act(() => {
|
|
115
|
+
jest.advanceTimersByTime(FILTER_CHANGE_DEBOUNCE_DELAY);
|
|
116
|
+
});
|
|
117
|
+
expect(searchCallback).toHaveBeenCalledTimes(1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('drops a scheduled search when the value editor is cleared before it runs', () => {
|
|
121
|
+
const searchCallback = jest.fn();
|
|
122
|
+
const { result, rerender } = renderSearchHook(searchCallback);
|
|
123
|
+
|
|
124
|
+
rerender({ filterString: 'subscription.status == "activ"' });
|
|
125
|
+
expect(result.current.pendingSearchRun).toBeDefined();
|
|
126
|
+
|
|
127
|
+
rerender({ filterString: 'subscription.status == ""', hasEmptyRuleValue: true });
|
|
128
|
+
expect(result.current.pendingSearchRun).toBeUndefined();
|
|
129
|
+
|
|
130
|
+
act(() => {
|
|
131
|
+
jest.advanceTimersByTime(FILTER_CHANGE_DEBOUNCE_DELAY);
|
|
132
|
+
});
|
|
133
|
+
expect(searchCallback).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('still searches on click while a value editor is empty', () => {
|
|
137
|
+
const searchCallback = jest.fn();
|
|
138
|
+
const { result, rerender } = renderSearchHook(searchCallback);
|
|
139
|
+
|
|
140
|
+
rerender({ filterString: 'subscription.status == ""', hasEmptyRuleValue: true });
|
|
141
|
+
|
|
142
|
+
act(() => {
|
|
143
|
+
result.current.handleSubmitSearchOnClick();
|
|
144
|
+
});
|
|
145
|
+
expect(searchCallback).toHaveBeenCalledTimes(1);
|
|
146
|
+
});
|
|
94
147
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RuleGroupType, RuleType } from 'react-querybuilder';
|
|
2
2
|
import { formatQuery } from 'react-querybuilder';
|
|
3
3
|
|
|
4
|
-
import { collectRuleFields, parseCelToRuleGroup } from './utils';
|
|
4
|
+
import { collectRuleFields, hasNestedRuleWithEmptyValue, parseCelToRuleGroup } from './utils';
|
|
5
5
|
|
|
6
6
|
describe('parseCelToRuleGroup', () => {
|
|
7
7
|
it('assigns ids to the parsed group and rules so rule identity stays stable', () => {
|
|
@@ -54,3 +54,62 @@ describe('collectRuleFields', () => {
|
|
|
54
54
|
expect(collectRuleFields({ combinator: 'and', rules: [] })).toEqual([]);
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
|
+
|
|
58
|
+
describe('hasRuleWithEmptyValue', () => {
|
|
59
|
+
const groupWith = (rules: RuleGroupType['rules']): RuleGroupType => ({ combinator: 'and', rules });
|
|
60
|
+
|
|
61
|
+
it('reports an empty text value, including whitespace only', () => {
|
|
62
|
+
expect(hasNestedRuleWithEmptyValue(groupWith([{ field: 'subscription.status', operator: '=', value: '' }]))).toBe(
|
|
63
|
+
true,
|
|
64
|
+
);
|
|
65
|
+
expect(hasNestedRuleWithEmptyValue(groupWith([{ field: 'subscription.status', operator: '=', value: ' ' }]))).toBe(
|
|
66
|
+
true,
|
|
67
|
+
);
|
|
68
|
+
expect(
|
|
69
|
+
hasNestedRuleWithEmptyValue(groupWith([{ field: 'subscription.status', operator: '=', value: 'active' }])),
|
|
70
|
+
).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('treats a missing value as empty and a committed boolean or number as filled', () => {
|
|
74
|
+
expect(hasNestedRuleWithEmptyValue(groupWith([{ field: 'lldp', operator: '=', value: undefined }]))).toBe(true);
|
|
75
|
+
expect(hasNestedRuleWithEmptyValue(groupWith([{ field: 'lldp', operator: '=', value: false }]))).toBe(false);
|
|
76
|
+
expect(hasNestedRuleWithEmptyValue(groupWith([{ field: 'port.speed', operator: '>', value: 0 }]))).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('reports a range with only one side filled in', () => {
|
|
80
|
+
expect(hasNestedRuleWithEmptyValue(groupWith([{ field: 'port.speed', operator: 'between', value: '1000,' }]))).toBe(
|
|
81
|
+
true,
|
|
82
|
+
);
|
|
83
|
+
expect(hasNestedRuleWithEmptyValue(groupWith([{ field: 'port.speed', operator: 'between', value: '1000' }]))).toBe(
|
|
84
|
+
true,
|
|
85
|
+
);
|
|
86
|
+
expect(
|
|
87
|
+
hasNestedRuleWithEmptyValue(groupWith([{ field: 'port.speed', operator: 'between', value: '1000,2000' }])),
|
|
88
|
+
).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('never reports operators that take no value, whatever their leftover value is', () => {
|
|
92
|
+
expect(hasNestedRuleWithEmptyValue(groupWith([{ field: 'port', operator: 'notNull', value: null }]))).toBe(false);
|
|
93
|
+
expect(hasNestedRuleWithEmptyValue(groupWith([{ field: 'port', operator: 'null', value: null }]))).toBe(false);
|
|
94
|
+
expect(hasNestedRuleWithEmptyValue(groupWith([{ field: 'port', operator: 'notNull', value: '' }]))).toBe(false);
|
|
95
|
+
expect(hasNestedRuleWithEmptyValue(groupWith([{ field: 'port', operator: 'null', value: 'abc' }]))).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('does not hold back a search for a filter restored from CEL that uses has component', () => {
|
|
99
|
+
expect(hasNestedRuleWithEmptyValue(parseCelToRuleGroup('subscription.port != null'))).toBe(false);
|
|
100
|
+
expect(hasNestedRuleWithEmptyValue(parseCelToRuleGroup('subscription.port == null'))).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('looks inside nested groups', () => {
|
|
104
|
+
const ruleGroup = groupWith([
|
|
105
|
+
{ field: 'subscription.status', operator: '=', value: 'active' },
|
|
106
|
+
groupWith([{ field: 'subscription.note', operator: 'contains', value: '' }]),
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
expect(hasNestedRuleWithEmptyValue(ruleGroup)).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns false without a rule group', () => {
|
|
113
|
+
expect(hasNestedRuleWithEmptyValue(undefined)).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|