@orchestrator-ui/orchestrator-ui-components 8.9.2 → 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.
Files changed (63) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/.turbo/turbo-test.log +262 -19
  4. package/CHANGELOG.md +28 -0
  5. package/dist/index.d.ts +2082 -2505
  6. package/dist/index.js +2045 -2190
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/WfoError/WfoError.tsx +2 -2
  10. package/src/components/WfoInlineNoteEdit/WfoSubscriptionDetailNoteEdit.tsx +4 -8
  11. package/src/components/WfoInlineNoteEdit/WfoSubscriptionNoteEdit.tsx +6 -7
  12. package/src/components/WfoKeyValueTable/styles.ts +24 -0
  13. package/src/components/WfoPageTemplate/paths.ts +0 -1
  14. package/src/components/WfoPydanticForm/fields/WfoLabel.tsx +5 -1
  15. package/src/components/WfoPydanticForm/fields/styles.ts +12 -3
  16. package/src/components/WfoSettingsModal/WfoInformationModal.tsx +3 -2
  17. package/src/components/WfoSubscriptionsList/index.ts +1 -2
  18. package/src/components/WfoSubscriptionsList/subscriptionListItem.ts +14 -0
  19. package/src/components/WfoSubscriptionsList/subscriptionListTabs.ts +1 -20
  20. package/src/components/WfoSummary/WfoLatestActiveSubscriptionsSummaryCard.tsx +17 -13
  21. package/src/components/WfoSummary/WfoLatestOutOfSyncSubscriptionSummaryCard.tsx +28 -15
  22. package/src/components/WfoTable/WfoAdvancedTable/WfoAdvancedTable.tsx +5 -1
  23. package/src/components/WfoTable/WfoStructuredSearchTable/WfoApplyFilterButton.tsx +37 -0
  24. package/src/components/WfoTable/WfoStructuredSearchTable/WfoDebounceCountdown.tsx +58 -0
  25. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFieldSelector.spec.tsx +180 -0
  26. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFieldSelector.tsx +66 -51
  27. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFilterBuilder.tsx +33 -98
  28. package/src/components/WfoTable/WfoStructuredSearchTable/WfoRangeEditor.tsx +0 -1
  29. package/src/components/WfoTable/WfoStructuredSearchTable/WfoRestoreLoop.spec.tsx +2 -2
  30. package/src/components/WfoTable/WfoStructuredSearchTable/WfoStructuredSearchTable.tsx +5 -7
  31. package/src/components/WfoTable/WfoStructuredSearchTable/WfoValueEditor.tsx +9 -13
  32. package/src/components/WfoTable/WfoStructuredSearchTable/styles.ts +7 -0
  33. package/src/components/WfoTable/WfoStructuredSearchTable/useSearchWithDebouncedCallback.spec.tsx +147 -0
  34. package/src/components/WfoTable/WfoStructuredSearchTable/utils.spec.ts +60 -1
  35. package/src/components/WfoTable/WfoStructuredSearchTable/utils.ts +133 -1
  36. package/src/configuration/version.ts +1 -1
  37. package/src/hooks/index.ts +1 -0
  38. package/src/hooks/useDebouncedCallback.ts +48 -0
  39. package/src/hooks/usePathAutoComplete.spec.tsx +94 -9
  40. package/src/hooks/usePathAutoComplete.ts +10 -15
  41. package/src/hooks/useSearchPagination.ts +1 -1
  42. package/src/messages/en-GB.json +4 -2
  43. package/src/messages/nl-NL.json +4 -2
  44. package/src/pages/index.ts +0 -1
  45. package/src/pages/startPage/index.ts +1 -0
  46. package/src/pages/startPage/mappers.ts +15 -8
  47. package/src/pages/startPage/queryVariables.ts +0 -29
  48. package/src/pages/startPage/searchPayloads.ts +22 -0
  49. package/src/pages/subscriptions/WfoSubscriptionsListPage.tsx +511 -39
  50. package/src/rtk/endpoints/index.ts +0 -1
  51. package/src/rtk/endpoints/search.ts +3 -0
  52. package/src/rtk/endpoints/subscriptionListMutation.spec.ts +80 -0
  53. package/src/rtk/endpoints/subscriptionListMutation.ts +71 -52
  54. package/src/rtk/utils.spec.ts +30 -1
  55. package/src/rtk/utils.ts +2 -1
  56. package/src/types/search.ts +1 -3
  57. package/src/types/types.ts +0 -2
  58. package/src/utils/getDefaultTableConfig.ts +1 -6
  59. package/src/utils/getQueryParams.ts +1 -0
  60. package/src/components/WfoSubscriptionsList/WfoSubscriptionsList.tsx +0 -243
  61. package/src/components/WfoSubscriptionsList/subscriptionResultMappers.ts +0 -49
  62. package/src/pages/WfoSearchPocPage.tsx +0 -575
  63. package/src/rtk/endpoints/subscriptionListSummary.ts +0 -66
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestrator-ui/orchestrator-ui-components",
3
- "version": "8.9.2",
3
+ "version": "9.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Library of UI Components used to display the workflow orchestrator frontend",
6
6
  "author": {
@@ -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 { useUpdateSubscriptionDetailNoteOptimisticMutation } from '@/rtk/endpoints/subscriptionListMutation';
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, endpointName } = useGetSubscriptionDetailQuery({
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 [updateSub] = useUpdateSubscriptionDetailNoteOptimisticMutation();
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
- updateSub({
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: Record<string, unknown>;
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 [updateSub] = useUpdateSubscriptionNoteOptimisticMutation();
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
- updateSub({
35
- queryName: endpointName ?? '',
36
- subscriptionId: subscriptionId,
37
- graphQlQueryVariables: queryVariables,
38
- note: note,
34
+ updateSubscriptionNoteOptimistic({
35
+ subscriptionId,
36
+ note,
37
+ listQuery: { queryName: endpointName ?? '', queryVariables },
39
38
  });
40
39
  };
41
40
 
@@ -63,8 +63,32 @@ export const getStyles = ({ theme }: WfoThemeHelpers) => {
63
63
  color: theme.colors.textParagraph,
64
64
  display: 'flex',
65
65
  alignItems: 'center',
66
+ flexWrap: 'wrap',
67
+ gap: theme.size.s,
66
68
  overflowWrap: 'anywhere',
67
69
  whiteSpace: 'pre-wrap',
70
+
71
+ '& > *': {
72
+ maxWidth: '100%',
73
+ flexGrow: 1,
74
+ },
75
+
76
+ '& > .euiBadge + .euiBadge': {
77
+ marginInlineStart: 0,
78
+ },
79
+
80
+ '&:has(.euiInlineEdit), & > :has(.euiInlineEdit)': {
81
+ flexGrow: 1,
82
+ },
83
+
84
+ '.euiInlineEditText .euiButtonEmpty': {
85
+ blockSize: 'auto',
86
+ whiteSpace: 'normal',
87
+ textAlign: 'start',
88
+ },
89
+ '.euiInlineEditText .eui-textTruncate': {
90
+ whiteSpace: 'pre-wrap !important',
91
+ },
68
92
  });
69
93
 
70
94
  const clipboardIconStyle = css({
@@ -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';
@@ -2,6 +2,8 @@ import React from 'react';
2
2
 
3
3
  import { PydanticFormElement } from 'pydantic-forms';
4
4
 
5
+ import { euiFontSizeFromScale } from '@elastic/eui';
6
+
5
7
  import { useOrchestratorTheme } from '@/hooks';
6
8
 
7
9
  export const WfoLabel: PydanticFormElement = ({ pydanticFormField }) => {
@@ -11,7 +13,9 @@ export const WfoLabel: PydanticFormElement = ({ pydanticFormField }) => {
11
13
  <div data-testid={pydanticFormField.id}>
12
14
  <label
13
15
  css={{
14
- color: theme.colors.textParagraph,
16
+ fontSize: euiFontSizeFromScale('m', theme),
17
+ fontWeight: theme.font.weight.semiBold,
18
+ color: theme.colors.link,
15
19
  display: 'block',
16
20
  }}
17
21
  id={pydanticFormField.id}
@@ -1,3 +1,4 @@
1
+ import { euiFontSizeFromScale, euiLineHeightFromBaseline } from '@elastic/eui';
1
2
  import { css } from '@emotion/react';
2
3
 
3
4
  import type { WfoThemeHelpers } from '@/hooks';
@@ -114,15 +115,23 @@ export const getCommonFormFieldStyles = ({ theme }: WfoThemeHelpers) => {
114
115
  color: theme.colors.textParagraph,
115
116
  },
116
117
  '.euiFormLabel': {
117
- color: theme.colors.textParagraph,
118
+ fontSize: euiFontSizeFromScale('m', theme),
119
+ lineHeight: euiLineHeightFromBaseline('m', theme),
120
+ color: theme.colors.link,
118
121
  cursor: 'text',
119
- '&.euiFormLabel-isFocused': {
120
- color: theme.colors.textPrimary,
122
+ '&.euiFormLabel-isInvalid': {
123
+ color: theme.colors.textDanger,
121
124
  },
122
125
  },
123
126
  '.euiFormRow__labelWrapper': {
124
127
  display: 'flex',
125
128
  flexDirection: 'column',
129
+ '> .euiText': {
130
+ fontSize: euiFontSizeFromScale('s', theme),
131
+ lineHeight: euiLineHeightFromBaseline('s', theme),
132
+ fontWeight: theme.font.weight.regular,
133
+ color: theme.colors.textParagraph,
134
+ },
126
135
  },
127
136
  });
128
137
 
@@ -14,11 +14,12 @@ export type WfoInformationModalProps = {
14
14
  title: string;
15
15
  onClose: () => void;
16
16
  children: ReactNode;
17
+ maxWidth?: number;
17
18
  };
18
19
 
19
- export const WfoInformationModal: FC<WfoInformationModalProps> = ({ title, onClose, children }) => {
20
+ export const WfoInformationModal: FC<WfoInformationModalProps> = ({ title, onClose, children, maxWidth }) => {
20
21
  return (
21
- <EuiModal onClose={onClose}>
22
+ <EuiModal onClose={onClose} maxWidth={maxWidth}>
22
23
  <EuiModalHeader>
23
24
  <EuiModalHeaderTitle size="xs">{title}</EuiModalHeaderTitle>
24
25
  </EuiModalHeader>
@@ -1,3 +1,2 @@
1
- export * from './subscriptionResultMappers';
1
+ export * from './subscriptionListItem';
2
2
  export * from './subscriptionListTabs';
3
- export * from './WfoSubscriptionsList';
@@ -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, SubscriptionListItem>[] = [
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 { mapSubscriptionSummaryToSummaryCardListItem } from '@/pages/startPage/mappers';
7
- import { subscriptionsListSummaryQueryVariables } from '@/pages/startPage/queryVariables';
8
- import { useGetSubscriptionSummaryListQuery } from '@/rtk';
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={subscriptionsSummaryResult?.pageInfo.totalItems ?? '-'}
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 { mapSubscriptionSummaryToSummaryCardListItem } from '@/pages/startPage/mappers';
7
- import { outOfSyncSubscriptionsListSummaryQueryVariables } from '@/pages/startPage/queryVariables';
8
- import { useGetSubscriptionSummaryListQuery } from '@/rtk';
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.SORT_BY]: 'field-startDate_order-ASC',
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={outOfSyncSubscriptionsSummaryResult?.pageInfo.totalItems ?? '-'}
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),
@@ -190,7 +190,11 @@ export const WfoAdvancedTable = <T extends object>({
190
190
  )}
191
191
 
192
192
  {rowDetailData && (
193
- <WfoInformationModal title={detailModalTitle} onClose={() => setSelectedDataForDetailModal(undefined)}>
193
+ <WfoInformationModal
194
+ title={detailModalTitle}
195
+ onClose={() => setSelectedDataForDetailModal(undefined)}
196
+ maxWidth={theme.breakpoint.m}
197
+ >
194
198
  <WfoKeyValueTable keyValues={rowDetailData} showCopyToClipboardIcon />
195
199
  </WfoInformationModal>
196
200
  )}
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+
3
+ import { useTranslations } from 'next-intl';
4
+
5
+ import { EuiButton } from '@elastic/eui';
6
+
7
+ import { WfoDebounceCountdown } from '@/components/WfoTable/WfoStructuredSearchTable/WfoDebounceCountdown';
8
+ import { getWfoStructuredSearchTableStyles } from '@/components/WfoTable/WfoStructuredSearchTable/styles';
9
+ import { DebouncedPendingRun, useWithOrchestratorTheme } from '@/hooks';
10
+
11
+ interface ApplyFilterButtonProps {
12
+ isDisabled: boolean;
13
+ pendingSearchRun?: DebouncedPendingRun;
14
+ onClick: () => void;
15
+ }
16
+
17
+ export const WfoApplyFilterButton = ({ isDisabled, pendingSearchRun, onClick }: ApplyFilterButtonProps) => {
18
+ const t = useTranslations('common');
19
+ const { applyFilterContentStyles } = useWithOrchestratorTheme(getWfoStructuredSearchTableStyles);
20
+
21
+ return (
22
+ <EuiButton
23
+ onClick={onClick}
24
+ id={'button-apply-filter'}
25
+ data-test-id={'button-apply-filter'}
26
+ fill
27
+ type="submit"
28
+ aria-label={t('applyFilter')}
29
+ disabled={isDisabled}
30
+ >
31
+ <span css={applyFilterContentStyles}>
32
+ {pendingSearchRun && <WfoDebounceCountdown key={pendingSearchRun.id} durationMs={pendingSearchRun.delay} />}
33
+ {t('applyFilter')}
34
+ </span>
35
+ </EuiButton>
36
+ );
37
+ };
@@ -0,0 +1,58 @@
1
+ import React from 'react';
2
+
3
+ import { keyframes } from '@emotion/react';
4
+
5
+ const SIZE = 14;
6
+ const STROKE_WIDTH = 2;
7
+ const CENTER = SIZE / 2;
8
+ const RADIUS = (SIZE - STROKE_WIDTH) / 2;
9
+ const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
10
+
11
+ const sweep = keyframes({
12
+ from: { strokeDashoffset: CIRCUMFERENCE },
13
+ to: { strokeDashoffset: 0 },
14
+ });
15
+
16
+ interface WfoDebounceCountdownProps {
17
+ durationMs: number;
18
+ }
19
+
20
+ /**
21
+ * Ring that fills over `durationMs`, showing how long is left before the pending search runs.
22
+ * Render it with a key that changes per scheduled run, so the sweep restarts from empty every
23
+ * time an edit pushes the debounce back.
24
+ */
25
+ export const WfoDebounceCountdown = ({ durationMs }: WfoDebounceCountdownProps) => (
26
+ <svg width={SIZE} height={SIZE} viewBox={`0 0 ${SIZE} ${SIZE}`} aria-hidden="true" css={{ flex: 'none' }}>
27
+ <circle
28
+ cx={CENTER}
29
+ cy={CENTER}
30
+ r={RADIUS}
31
+ fill="none"
32
+ stroke="currentColor"
33
+ strokeWidth={STROKE_WIDTH}
34
+ opacity={0.3}
35
+ />
36
+ <circle
37
+ cx={CENTER}
38
+ cy={CENTER}
39
+ r={RADIUS}
40
+ fill="none"
41
+ stroke="currentColor"
42
+ strokeWidth={STROKE_WIDTH}
43
+ strokeLinecap="round"
44
+ strokeDasharray={CIRCUMFERENCE}
45
+ // Start the sweep at 12 o'clock instead of 3 o'clock.
46
+ transform={`rotate(-90 ${CENTER} ${CENTER})`}
47
+ css={{
48
+ strokeDashoffset: CIRCUMFERENCE,
49
+ animation: `${sweep} ${durationMs}ms linear forwards`,
50
+ // A ring that jumps straight to full is still an honest "a search is queued" marker.
51
+ '@media (prefers-reduced-motion: reduce)': {
52
+ animation: 'none',
53
+ strokeDashoffset: 0,
54
+ },
55
+ }}
56
+ />
57
+ </svg>
58
+ );