@orchestrator-ui/orchestrator-ui-components 8.4.5 → 8.5.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 (69) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/.turbo/turbo-test.log +7 -7
  4. package/CHANGELOG.md +11 -0
  5. package/dist/index.d.ts +1890 -562
  6. package/dist/index.js +3814 -2103
  7. package/dist/index.js.map +1 -1
  8. package/package.json +2 -1
  9. package/src/components/WfoPydanticForm/WfoPydanticForm.tsx +23 -1
  10. package/src/components/WfoSearchBar/WfoSearchField.tsx +0 -1
  11. package/src/components/WfoSearchPage/WfoFilterGroup/WfoFilterGroup.tsx +5 -5
  12. package/src/components/WfoSearchPage/WfoSearch/WfoSearch.tsx +12 -5
  13. package/src/components/WfoSearchPage/constants.ts +0 -4
  14. package/src/components/WfoSearchPage/utils.ts +75 -20
  15. package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActionExpandableMenuItem.tsx +36 -18
  16. package/src/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActionsMenuItem.tsx +38 -2
  17. package/src/components/WfoSubscription/utils/index.ts +3 -0
  18. package/src/components/WfoSubscription/utils/utils.spec.ts +39 -4
  19. package/src/components/WfoSubscription/utils/utils.ts +10 -4
  20. package/src/components/WfoTable/WfoStructuredSearchTable/WfoAddGroupAction.tsx +19 -0
  21. package/src/components/WfoTable/WfoStructuredSearchTable/WfoAddRuleAction.tsx +23 -0
  22. package/src/components/WfoTable/WfoStructuredSearchTable/WfoCombinatorSelector.tsx +30 -0
  23. package/src/components/WfoTable/WfoStructuredSearchTable/WfoExpandingSearchRow.tsx +38 -0
  24. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFieldSelector.tsx +84 -0
  25. package/src/components/WfoTable/WfoStructuredSearchTable/WfoFilterBuilder.tsx +189 -0
  26. package/src/components/WfoTable/WfoStructuredSearchTable/WfoOperatorSelector.tsx +33 -0
  27. package/src/components/WfoTable/WfoStructuredSearchTable/WfoRangeEditor.tsx +64 -0
  28. package/src/components/WfoTable/WfoStructuredSearchTable/WfoRemoveGroupAction.tsx +21 -0
  29. package/src/components/WfoTable/WfoStructuredSearchTable/WfoRemoveRuleAction.tsx +20 -0
  30. package/src/components/WfoTable/WfoStructuredSearchTable/WfoRule.tsx +16 -0
  31. package/src/components/WfoTable/WfoStructuredSearchTable/WfoRuleGroup.tsx +73 -0
  32. package/src/components/WfoTable/WfoStructuredSearchTable/WfoStructuredSearchTable.tsx +323 -0
  33. package/src/components/WfoTable/WfoStructuredSearchTable/WfoValueEditor.tsx +159 -0
  34. package/src/components/WfoTable/WfoStructuredSearchTable/index.ts +2 -0
  35. package/src/components/WfoTable/WfoStructuredSearchTable/styles.ts +146 -0
  36. package/src/components/WfoTable/WfoTable/WfoDragHandler.tsx +4 -2
  37. package/src/components/WfoTable/index.ts +2 -6
  38. package/src/components/WfoTitleWithWebsocketBadge/WfoTitleWithWebsocketBadge.tsx +14 -6
  39. package/src/components/WfoWorkflowSteps/WfoStep/WfoStep.tsx +1 -0
  40. package/src/components/WfoWorkflowSteps/WfoStepList/WfoStepList.tsx +3 -0
  41. package/src/components/WfoWorkflowSteps/WfoWorkflowStepList/WfoWorkflowStepList.tsx +32 -12
  42. package/src/components/WfoWorkflowUserGuide/WfoPageWithUserGuide.tsx +38 -0
  43. package/src/components/WfoWorkflowUserGuide/WfoWorkflowGuideExpandablePanel.tsx +151 -0
  44. package/src/components/WfoWorkflowUserGuide/index.ts +2 -0
  45. package/src/components/WfoWorkflowUserGuide/styles.ts +87 -0
  46. package/src/components/index.ts +1 -0
  47. package/src/configuration/version.ts +1 -1
  48. package/src/contexts/OrchestratorConfigContext.tsx +1 -0
  49. package/src/hooks/index.ts +1 -0
  50. package/src/hooks/usePathAutoComplete.ts +3 -4
  51. package/src/hooks/useSearch.ts +20 -15
  52. package/src/hooks/useSearchPagination.ts +11 -8
  53. package/src/hooks/useUrlParams.ts +3 -5
  54. package/src/icons/WfoTrashFilled.tsx +13 -0
  55. package/src/icons/WfoXMarkSmall.tsx +2 -2
  56. package/src/icons/index.ts +1 -0
  57. package/src/messages/en-GB.json +94 -8
  58. package/src/messages/nl-NL.json +23 -5
  59. package/src/pages/WfoSearchPocPage.tsx +402 -0
  60. package/src/pages/index.ts +1 -0
  61. package/src/pages/processes/WfoProcessDetail.tsx +10 -10
  62. package/src/pages/processes/WfoProcessDetailPage.tsx +1 -0
  63. package/src/pages/processes/WfoStartProcessPage.tsx +15 -19
  64. package/src/rtk/api.ts +1 -0
  65. package/src/rtk/endpoints/index.ts +1 -0
  66. package/src/rtk/endpoints/search.ts +45 -10
  67. package/src/rtk/endpoints/workflowGuides.ts +25 -0
  68. package/src/types/search.ts +21 -5
  69. package/src/types/types.ts +10 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestrator-ui/orchestrator-ui-components",
3
- "version": "8.4.5",
3
+ "version": "8.5.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Library of UI Components used to display the workflow orchestrator frontend",
6
6
  "author": {
@@ -52,6 +52,7 @@
52
52
  "pydantic-forms": "^3.0.1",
53
53
  "react-diff-view": "^3.2.0",
54
54
  "react-draggable": "^4.4.6",
55
+ "react-querybuilder": "^8.18.0",
55
56
  "react-redux": "^9.3.0",
56
57
  "react-select": "^5.8.0",
57
58
  "scroll-into-view": "^1.16.2",
@@ -1,16 +1,19 @@
1
1
  import React, { useCallback, useMemo } from 'react';
2
2
 
3
3
  import _ from 'lodash';
4
+ import { useTranslations } from 'next-intl';
4
5
  import { useRouter } from 'next/router';
5
6
  import type { PydanticFormApiProvider } from 'pydantic-forms';
6
7
  import { PydanticForm } from 'pydantic-forms';
7
8
 
8
9
  import { PATH_TASKS, PATH_WORKFLOWS } from '@/components';
9
10
  import { Footer } from '@/components/WfoPydanticForm/Footer';
11
+ import { useShowToastMessage } from '@/hooks';
10
12
  import { useGetPydanticFormsConfig } from '@/hooks/useGetPydanticFormsConfig';
11
13
  import { StartWorkflowPayload } from '@/pages/processes/WfoStartProcessPage';
12
14
  import { HttpStatus, isFetchBaseQueryError, isRecord } from '@/rtk';
13
15
  import { useStartProcessMutation } from '@/rtk/endpoints/forms';
16
+ import { ToastTypes } from '@/types';
14
17
 
15
18
  interface WfoPydanticFormProps {
16
19
  processName: string;
@@ -29,6 +32,8 @@ export const WfoPydanticForm = ({ processName, startProcessPayload, isTask }: Wf
29
32
 
30
33
  const [startProcess] = useStartProcessMutation();
31
34
  const router = useRouter();
35
+ const t = useTranslations('pydanticForms.userInputForm');
36
+ const { showToastMessage } = useShowToastMessage();
32
37
 
33
38
  const onSuccess = useCallback(
34
39
  (_fieldValues: object, req: object) => {
@@ -64,6 +69,23 @@ export const WfoPydanticForm = ({ processName, startProcessPayload, isTask }: Wf
64
69
  ...error.data,
65
70
  status: error.status,
66
71
  });
72
+ } else if (error.status === HttpStatus.PreconditionFailed) {
73
+ const detail = typeof error.data.detail === 'string' ? error.data.detail : '';
74
+ showToastMessage(
75
+ ToastTypes.ERROR,
76
+ detail || t('preconditionFailedFallback'),
77
+ t('preconditionFailedTitle'),
78
+ );
79
+ router.replace(isTask ? PATH_TASKS : PATH_WORKFLOWS);
80
+ resolve({
81
+ validation_errors: [
82
+ {
83
+ loc: ['__root__'],
84
+ msg: detail || t('preconditionFailedFallback'),
85
+ type: 'precondition_failed',
86
+ },
87
+ ],
88
+ });
67
89
  }
68
90
  } else if (data) {
69
91
  resolve({
@@ -86,7 +108,7 @@ export const WfoPydanticForm = ({ processName, startProcessPayload, isTask }: Wf
86
108
  };
87
109
 
88
110
  return pydanticFormProvider;
89
- }, [startProcess, startProcessPayload]);
111
+ }, [startProcess, startProcessPayload, t, showToastMessage, router, isTask]);
90
112
 
91
113
  const config = useGetPydanticFormsConfig(getPydanticFormProvider, (props) => <Footer {...props} isTask={isTask} />);
92
114
 
@@ -39,7 +39,6 @@ export const WfoSearchField = ({ queryString, queryIsValid = true, onUpdateQuery
39
39
  placeholder={`${t('search')}...`}
40
40
  onChange={(event) => setCurrentQuery(event.target.value)}
41
41
  onSearch={handleSearch.current}
42
- onBlur={(event) => handleSearch.current(event.target.value)}
43
42
  isInvalid={!queryIsValid}
44
43
  fullWidth
45
44
  />
@@ -17,14 +17,14 @@ import {
17
17
  import { WfoToolTip } from '@/components';
18
18
  import { isCondition } from '@/components/WfoSearchPage/utils';
19
19
  import { useOrchestratorTheme } from '@/hooks';
20
- import { Condition, EntityKind, Group } from '@/types';
20
+ import type { Condition, EntityKind, Filter } from '@/types';
21
21
 
22
22
  import { ConditionRow } from '../WfoConditionRow';
23
23
 
24
24
  interface FilterGroupProps {
25
- group: Group;
25
+ group: Filter;
26
26
  entityType: EntityKind;
27
- onChange: (group: Group) => void;
27
+ onChange: (group: Filter) => void;
28
28
  onRemove?: () => void;
29
29
  depth?: number;
30
30
  isRoot?: boolean;
@@ -58,7 +58,7 @@ export const FilterGroup: FC<FilterGroupProps> = ({
58
58
  const addGroup = () => {
59
59
  if (!canAddGroup) return;
60
60
 
61
- const newGroup: Group = {
61
+ const newGroup: Filter = {
62
62
  op: 'AND',
63
63
  children: [],
64
64
  };
@@ -68,7 +68,7 @@ export const FilterGroup: FC<FilterGroupProps> = ({
68
68
  });
69
69
  };
70
70
 
71
- const updateChild = (index: number, child: Group | Condition) => {
71
+ const updateChild = (index: number, child: Filter | Condition) => {
72
72
  const newChildren = [...group.children];
73
73
  newChildren[index] = child;
74
74
  onChange({
@@ -29,7 +29,7 @@ import { useDebounce } from '@/hooks/useDebounce';
29
29
  import { useSearch } from '@/hooks/useSearch';
30
30
  import { useSearchPagination } from '@/hooks/useSearchPagination';
31
31
  import { useUrlParams } from '@/hooks/useUrlParams';
32
- import { EntityKind, Group, RetrieverType } from '@/types';
32
+ import { EntityKind, Filter, RetrieverType } from '@/types';
33
33
 
34
34
  import { DEFAULT_DEBOUNCE_DELAY, DEFAULT_PAGE_SIZE, LAYOUT_RATIOS, SMALL_RESULT_THRESHOLD } from '../constants';
35
35
 
@@ -53,7 +53,7 @@ export const WfoSearch = () => {
53
53
 
54
54
  const pageSize = DEFAULT_PAGE_SIZE;
55
55
 
56
- const [filterGroup, setFilterGroup] = useState<Group>({
56
+ const [filterGroup, setFilterGroup] = useState<Filter>({
57
57
  op: 'AND',
58
58
  children: [],
59
59
  });
@@ -105,6 +105,11 @@ export const WfoSearch = () => {
105
105
  });
106
106
  setResults({
107
107
  data: [],
108
+ cursor: {
109
+ total_items: 0,
110
+ start_cursor: 0,
111
+ end_cursor: 0,
112
+ },
108
113
  page_info: {
109
114
  has_next_page: false,
110
115
  next_page_cursor: null,
@@ -189,7 +194,7 @@ export const WfoSearch = () => {
189
194
 
190
195
  <EuiFieldSearch
191
196
  placeholder={t('searchPlaceholder', {
192
- entityType: currentTab?.label.toLowerCase(),
197
+ entityType: currentTab?.label.toLowerCase() || '',
193
198
  })}
194
199
  value={searchValue || ''}
195
200
  onChange={(event) => {
@@ -272,7 +277,7 @@ export const WfoSearch = () => {
272
277
  <EuiCallOut title={t('noResults')} color="primary" iconType="search" size="m">
273
278
  <p>
274
279
  {t('noResultsMessage', {
275
- entityType: currentTab?.label.toLowerCase(),
280
+ entityType: currentTab?.label.toLowerCase() || '',
276
281
  })}
277
282
  </p>
278
283
  <EuiSpacer size="s" />
@@ -304,7 +309,9 @@ export const WfoSearch = () => {
304
309
  <EuiFlexItem grow={false}>
305
310
  <WfoSearchPaginationInfo
306
311
  has_next_page={results.page_info.has_next_page}
307
- next_page_cursor={results.page_info.next_page_cursor}
312
+ next_page_cursor={
313
+ results.page_info.next_page_cursor ? parseInt(results.page_info.next_page_cursor, 0) : null
314
+ }
308
315
  onNextPage={handleNextPage}
309
316
  onPrevPage={handlePrevPage}
310
317
  isLoading={isLoadingMore}
@@ -6,7 +6,3 @@ export const LAYOUT_RATIOS = {
6
6
  RESULTS_GROW: 2,
7
7
  DETAIL_GROW: 3,
8
8
  } as const;
9
-
10
- export const VALID_ENTITY_TYPES = ['SUBSCRIPTION', 'PRODUCT', 'WORKFLOW', 'PROCESS'] as const;
11
-
12
- export const DEFAULT_ENTITY_TAB = 'SUBSCRIPTION';
@@ -1,4 +1,18 @@
1
- import { Condition, EntityKind, Group, RetrieverType, SearchResult } from '@/types';
1
+ import type { RuleGroupType } from 'react-querybuilder';
2
+ import { formatQuery } from 'react-querybuilder/formatQuery';
3
+
4
+ import { WfoSubscriptionListTab } from '@/components';
5
+ import { SearchPaginationPayload } from '@/rtk';
6
+ import {
7
+ Condition,
8
+ EntityKind,
9
+ Filter,
10
+ OperatorDisplay,
11
+ PathInfo,
12
+ RetrieverType,
13
+ SearchResult,
14
+ SubscriptionStatus,
15
+ } from '@/types';
2
16
 
3
17
  export function isSubscriptionSearchResult(item: SearchResult): boolean {
4
18
  return item.entity_type === 'SUBSCRIPTION';
@@ -16,7 +30,7 @@ export function isWorkflowSearchResult(item: SearchResult): boolean {
16
30
  return item.entity_type === 'WORKFLOW';
17
31
  }
18
32
 
19
- export const isCondition = (item: Group | Condition): item is Condition => {
33
+ export const isCondition = (item: Filter | Condition): item is Condition => {
20
34
  return 'path' in item && 'condition' in item;
21
35
  };
22
36
 
@@ -37,10 +51,10 @@ export const getDetailUrl = (result: SearchResult, baseUrl: string): string => {
37
51
  };
38
52
 
39
53
  export const ENTITY_TABS = [
40
- { id: 'SUBSCRIPTION' as const, label: 'Subscriptions' },
41
- { id: 'PRODUCT' as const, label: 'Products' },
42
- { id: 'WORKFLOW' as const, label: 'Workflows' },
43
- { id: 'PROCESS' as const, label: 'Processes' },
54
+ { id: EntityKind.SUBSCRIPTION, label: 'Subscriptions' },
55
+ { id: EntityKind.PRODUCT, label: 'Products' },
56
+ { id: EntityKind.WORKFLOW, label: 'Workflows' },
57
+ { id: EntityKind.PROCESS, label: 'Processes' },
44
58
  ];
45
59
 
46
60
  interface ThemeColors {
@@ -68,16 +82,6 @@ export const getTypeColor = (type: string, theme: Theme): string => {
68
82
  return colorKey ? theme.colors[colorKey] : theme.colors.textSubdued;
69
83
  };
70
84
 
71
- interface PathInfo {
72
- type?: string;
73
- [key: string]: unknown;
74
- }
75
-
76
- interface OperatorDisplay {
77
- symbol: string;
78
- description: string;
79
- }
80
-
81
85
  const OPERATOR_MAP: Record<string, OperatorDisplay> = {
82
86
  eq: { symbol: '=', description: 'equals' },
83
87
  neq: { symbol: '≠', description: 'not equals' },
@@ -88,6 +92,7 @@ const OPERATOR_MAP: Record<string, OperatorDisplay> = {
88
92
  between: { symbol: '⟷', description: 'between (range)' },
89
93
  has_component: { symbol: '✓', description: 'has component' },
90
94
  not_has_component: { symbol: '✗', description: 'does not have component' },
95
+ like: { symbol: '∋', description: 'contains' },
91
96
  };
92
97
 
93
98
  const BOOLEAN_OPERATOR_MAP: Record<string, OperatorDisplay> = {
@@ -117,7 +122,7 @@ export const getButtonFill = (op: string, pathInfo: PathInfo | null, condition:
117
122
  return condition.condition.op === op;
118
123
  };
119
124
 
120
- export const isFilterValid = (group: Group): boolean => {
125
+ export const isFilterValid = (group: Filter): boolean => {
121
126
  return group.children.every((child) => {
122
127
  if (isCondition(child)) {
123
128
  return child.path && child.condition.op && child.condition.value !== undefined;
@@ -133,18 +138,68 @@ interface SearchQuery {
133
138
  export const buildSearchParams = (
134
139
  debouncedQuery: SearchQuery | string,
135
140
  selectedEntityTab: EntityKind,
136
- filterGroup: Group,
141
+ filterGroup: Filter,
137
142
  pageSize: number,
143
+ cursor: number,
138
144
  retriever?: Exclude<RetrieverType, 'auto'>,
139
- ) => {
145
+ ): SearchPaginationPayload => {
140
146
  const queryText = typeof debouncedQuery === 'string' ? debouncedQuery : debouncedQuery?.text?.trim() || '';
141
147
 
142
148
  return {
143
- action: 'select' as const,
144
149
  entity_type: selectedEntityTab,
145
150
  query: queryText || '',
146
151
  filters: filterGroup?.children.length > 0 ? filterGroup : undefined,
147
152
  limit: pageSize,
148
153
  retriever,
154
+ cursor,
155
+ response_columns: [],
149
156
  };
150
157
  };
158
+
159
+ const parseRuleGroupToFilters = (ruleGroup?: RuleGroupType) => {
160
+ const elasticQuery =
161
+ ruleGroup ? formatQuery(ruleGroup, { format: 'elasticsearch', fallbackExpression: '' }) : undefined;
162
+ return elasticQuery as unknown as Filter;
163
+ };
164
+
165
+ const getSubscriptionStatusesFromTab = (tab: WfoSubscriptionListTab) => {
166
+ switch (tab) {
167
+ case WfoSubscriptionListTab.ACTIVE:
168
+ return [SubscriptionStatus.ACTIVE];
169
+ case WfoSubscriptionListTab.TERMINATED:
170
+ return [SubscriptionStatus.TERMINATED];
171
+ case WfoSubscriptionListTab.TRANSIENT:
172
+ return [SubscriptionStatus.INITIAL, SubscriptionStatus.PROVISIONING, SubscriptionStatus.MIGRATING];
173
+ case WfoSubscriptionListTab.ALL:
174
+ return [
175
+ SubscriptionStatus.ACTIVE,
176
+ SubscriptionStatus.TERMINATED,
177
+ SubscriptionStatus.INITIAL,
178
+ SubscriptionStatus.MIGRATING,
179
+ SubscriptionStatus.PROVISIONING,
180
+ ];
181
+ default:
182
+ return [SubscriptionStatus.ACTIVE];
183
+ }
184
+ };
185
+
186
+ const buildSubscriptionStatusFilter = (tab: WfoSubscriptionListTab) => {
187
+ return {
188
+ combinator: 'or',
189
+ rules: getSubscriptionStatusesFromTab(tab).map((status) => ({
190
+ field: 'subscription.status',
191
+ operator: '=',
192
+ value: status,
193
+ })),
194
+ };
195
+ };
196
+
197
+ export const combineSearchFilters = (ruleGroup: RuleGroupType | false | undefined, tab: WfoSubscriptionListTab) => {
198
+ const userRuleGroup = ruleGroup === false ? undefined : ruleGroup;
199
+ const ruleGroups = [buildSubscriptionStatusFilter(tab), userRuleGroup].filter(Boolean) as RuleGroupType[];
200
+
201
+ const combinedRuleGroup: RuleGroupType =
202
+ ruleGroups.length === 1 ? ruleGroups[0] : { combinator: 'and', rules: ruleGroups };
203
+
204
+ return parseRuleGroupToFilters(combinedRuleGroup);
205
+ };
@@ -7,13 +7,13 @@ import { EuiButtonIcon, EuiText } from '@elastic/eui';
7
7
 
8
8
  import { PATH_SUBSCRIPTIONS } from '@/components';
9
9
  import { useWithOrchestratorTheme } from '@/hooks';
10
- import { SubscriptionAction } from '@/types';
10
+ import { SubscriptionAction, SubscriptionRelation } from '@/types';
11
11
 
12
12
  import { getSubscriptionActionStyles } from './styles';
13
13
 
14
14
  export type WfoSubscriptionActionExpandableMenuItemProps = {
15
15
  subscriptionAction: SubscriptionAction;
16
- onClickLockedRelation: (relation: string) => void;
16
+ onClickLockedRelation: (relation: SubscriptionRelation) => void;
17
17
  children: React.ReactNode;
18
18
  };
19
19
 
@@ -28,16 +28,19 @@ export const WfoSubscriptionActionExpandableMenuItem: FC<WfoSubscriptionActionEx
28
28
  useWithOrchestratorTheme(getSubscriptionActionStyles);
29
29
  const [isExpanded, setIsExpanded] = useState(false);
30
30
 
31
- const { locked_relations: lockedRelations } = subscriptionAction;
31
+ // TODO: remove lockedRelationsUuids fallback when orchestrator-core 6.0.0 is released and only use the _detail variant
32
+ const lockedRelationsDetail = subscriptionAction.locked_relations_detail;
33
+ const lockedRelationsUuids = subscriptionAction.locked_relations;
34
+ const hasLockedRelations = (lockedRelationsDetail?.length ?? 0) > 0 || (lockedRelationsUuids?.length ?? 0) > 0;
32
35
 
33
36
  return (
34
37
  <div>
35
38
  <div
36
- css={[expandableMenuItemStyle, lockedRelations && clickableStyle]}
39
+ css={[expandableMenuItemStyle, hasLockedRelations && clickableStyle]}
37
40
  onClick={() => setIsExpanded(!isExpanded)}
38
41
  >
39
42
  <div>{children}</div>
40
- {lockedRelations && (
43
+ {hasLockedRelations && (
41
44
  <EuiButtonIcon
42
45
  css={expandButtonStyle}
43
46
  iconType={isExpanded ? 'arrowDown' : 'arrowRight'}
@@ -46,21 +49,36 @@ export const WfoSubscriptionActionExpandableMenuItem: FC<WfoSubscriptionActionEx
46
49
  />
47
50
  )}
48
51
  </div>
49
- {lockedRelations && isExpanded && (
52
+ {hasLockedRelations && isExpanded && (
50
53
  <div css={expandedContentStyle}>
51
54
  <EuiText size="xs">{t('lockedBySubscriptions')}</EuiText>
52
- {lockedRelations.map((relation) => (
53
- <EuiText key={relation} size="xs">
54
- <Link
55
- css={linkStyle}
56
- href={`${PATH_SUBSCRIPTIONS}/${relation}`}
57
- target="_blank"
58
- onClick={() => onClickLockedRelation(relation)}
59
- >
60
- {relation}
61
- </Link>
62
- </EuiText>
63
- ))}
55
+ <ul css={{ margin: 0, paddingLeft: 16, listStyleType: 'disc' }}>
56
+ {lockedRelationsDetail ?
57
+ lockedRelationsDetail.map((relation) => (
58
+ <li key={relation.subscription_id}>
59
+ <EuiText size="xs">
60
+ <Link
61
+ css={linkStyle}
62
+ href={`${PATH_SUBSCRIPTIONS}/${relation.subscription_id}`}
63
+ target="_blank"
64
+ onClick={() => onClickLockedRelation(relation)}
65
+ >
66
+ {relation.subscription_description}
67
+ </Link>
68
+ </EuiText>
69
+ </li>
70
+ ))
71
+ : lockedRelationsUuids?.map((id) => (
72
+ <li key={id}>
73
+ <EuiText size="xs">
74
+ <Link css={linkStyle} href={`${PATH_SUBSCRIPTIONS}/${id}`} target="_blank">
75
+ {id}
76
+ </Link>
77
+ </EuiText>
78
+ </li>
79
+ ))
80
+ }
81
+ </ul>
64
82
  </div>
65
83
  )}
66
84
  </div>
@@ -4,7 +4,7 @@ import { useTranslations } from 'next-intl';
4
4
 
5
5
  import { EuiContextMenuItem, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
6
6
 
7
- import { flattenArrayProps } from '@/components';
7
+ import { flattenSubscriptionActionProps } from '@/components';
8
8
  import { WfoSubscriptionActionExpandableMenuItem } from '@/components/WfoSubscription/WfoSubscriptionActions/WfoSubscriptionActionExpandableMenuItem';
9
9
  import { getSubscriptionActionStyles } from '@/components/WfoSubscription/WfoSubscriptionActions/styles';
10
10
  import { useCheckEngineStatus, useOrchestratorTheme, useWithOrchestratorTheme } from '@/hooks';
@@ -56,9 +56,45 @@ export const WfoSubscriptionActionsMenuItem: FC<MenuItemProps> = ({
56
56
  );
57
57
  };
58
58
 
59
+ // TODO: remove UUID-only fallback when orchestrator-core 6.0.0 is released and only use the _detail variants
60
+ const getRelationsList = () => {
61
+ const detailRelations = [
62
+ ...(subscriptionAction.locked_relations_detail ?? []),
63
+ ...(subscriptionAction.unterminated_in_use_by_subscriptions_detail ?? []),
64
+ ];
65
+ if (detailRelations.length > 0) {
66
+ return (
67
+ <ul css={{ margin: 0, paddingLeft: 16, listStyleType: 'disc' }}>
68
+ {detailRelations.map((r) => (
69
+ <li key={r.subscription_id}>{r.subscription_description || r.subscription_id}</li>
70
+ ))}
71
+ </ul>
72
+ );
73
+ }
74
+ const uuidRelations = [
75
+ ...(subscriptionAction.locked_relations ?? []),
76
+ ...(subscriptionAction.unterminated_in_use_by_subscriptions ?? []),
77
+ ];
78
+ if (uuidRelations.length === 0) return null;
79
+ return (
80
+ <ul css={{ margin: 0, paddingLeft: 16, listStyleType: 'disc' }}>
81
+ {uuidRelations.map((id) => (
82
+ <li key={id}>{id}</li>
83
+ ))}
84
+ </ul>
85
+ );
86
+ };
87
+
59
88
  const tooltipIt = (actionItem: React.ReactNode) => {
60
89
  if (!subscriptionActionReason) return actionItem;
61
- const tooltipContent = t(subscriptionActionReason, flattenArrayProps(subscriptionAction));
90
+ const relationsList = getRelationsList();
91
+ const tooltipContent =
92
+ relationsList ?
93
+ <span css={{ whiteSpace: 'pre-line' }}>
94
+ {t(subscriptionActionReason, flattenSubscriptionActionProps(subscriptionAction))}
95
+ {relationsList}
96
+ </span>
97
+ : t(subscriptionActionReason, flattenSubscriptionActionProps(subscriptionAction));
62
98
 
63
99
  return (
64
100
  <div css={tooltipMenuItemStyle}>
@@ -1 +1,4 @@
1
1
  export * from './utils';
2
+
3
+ /** @deprecated Use flattenSubscriptionActionProps instead */
4
+ export { flattenSubscriptionActionProps as flattenArrayProps } from './utils';
@@ -9,7 +9,7 @@ import {
9
9
  WorkflowTarget,
10
10
  } from '../../../types';
11
11
  import {
12
- flattenArrayProps,
12
+ flattenSubscriptionActionProps,
13
13
  getFieldFromProductBlockInstanceValues,
14
14
  getLastUncompletedProcess,
15
15
  getLatestTaskDate,
@@ -61,7 +61,7 @@ describe('getProductBlockTitle()', () => {
61
61
  });
62
62
  });
63
63
 
64
- describe('flattenArrayProps', () => {
64
+ describe('flattenSubscriptionActionProps', () => {
65
65
  it('should flatten an object with array values into a comma-separated string', () => {
66
66
  const action: SubscriptionAction = {
67
67
  name: 'action name',
@@ -69,7 +69,7 @@ describe('flattenArrayProps', () => {
69
69
  usable_when: ['Status1', 'Status2', 'Status3'],
70
70
  };
71
71
 
72
- const result = flattenArrayProps(action);
72
+ const result = flattenSubscriptionActionProps(action);
73
73
 
74
74
  expect(result).toEqual({
75
75
  name: 'action name',
@@ -78,12 +78,47 @@ describe('flattenArrayProps', () => {
78
78
  });
79
79
  });
80
80
 
81
+ it('should flatten locked_relations_detail objects using subscription_description', () => {
82
+ const action: SubscriptionAction = {
83
+ name: 'action name',
84
+ description: 'action description',
85
+ locked_relations_detail: [
86
+ { subscription_id: 'uuid-1', subscription_description: 'Sub A' },
87
+ { subscription_id: 'uuid-2', subscription_description: 'Sub B' },
88
+ ],
89
+ };
90
+
91
+ const result = flattenSubscriptionActionProps(action);
92
+
93
+ expect(result).toEqual({
94
+ name: 'action name',
95
+ description: 'action description',
96
+ locked_relations_detail: 'Sub A, Sub B',
97
+ });
98
+ });
99
+
100
+ it('should flatten locked_relations UUIDs', () => {
101
+ const action: SubscriptionAction = {
102
+ name: 'action name',
103
+ description: 'action description',
104
+ locked_relations: ['uuid-1', 'uuid-2'],
105
+ };
106
+
107
+ const result = flattenSubscriptionActionProps(action);
108
+
109
+ expect(result).toEqual({
110
+ name: 'action name',
111
+ description: 'action description',
112
+ locked_relations: 'uuid-1, uuid-2',
113
+ });
114
+ });
115
+
81
116
  it('should handle an object with non-array values', () => {
82
117
  const action: SubscriptionAction = {
83
118
  name: 'action name',
84
119
  description: 'action description',
85
120
  };
86
- const result = flattenArrayProps(action);
121
+ const result = flattenSubscriptionActionProps(action);
87
122
  expect(result).toEqual({
88
123
  name: 'action name',
89
124
  description: 'action description',
@@ -10,6 +10,7 @@ import {
10
10
  SortOrder,
11
11
  SubscriptionAction,
12
12
  SubscriptionDetailProcess,
13
+ SubscriptionRelation,
13
14
  WorkflowTarget,
14
15
  } from '@/types';
15
16
 
@@ -40,16 +41,21 @@ export const getProductBlockTitle = (instanceValues: FieldValue[]): string | num
40
41
  return title;
41
42
  };
42
43
 
43
- export const flattenArrayProps = (action: SubscriptionAction): TranslationValues => {
44
+ const toLabel = (item: string | SubscriptionRelation): string =>
45
+ typeof item === 'object' ? (item.subscription_description ?? item.subscription_id) : item;
46
+
47
+ export const flattenSubscriptionActionProps = (action?: SubscriptionAction | null): TranslationValues => {
48
+ if (!action) return {};
49
+
44
50
  const flatObject: TranslationValues = {};
45
51
  for (const [key, value] of Object.entries(action)) {
46
52
  if (Array.isArray(value)) {
47
- flatObject[key] = value.join(', ');
48
- } else {
53
+ flatObject[key] = value.map(toLabel).join(', ');
54
+ } else if (typeof value !== 'object' || value === null) {
49
55
  flatObject[key] = value;
50
56
  }
51
57
  }
52
- return action ? flatObject : {};
58
+ return flatObject;
53
59
  };
54
60
 
55
61
  export const getWorkflowTargetColor = (workflowTarget: WorkflowTarget, theme: EuiThemeComputed) => {
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import type { ActionProps } from 'react-querybuilder';
3
+
4
+ import { useTranslations } from 'next-intl';
5
+
6
+ import { EuiFlexItem } from '@elastic/eui';
7
+
8
+ import { getWfoStructuredSearchTableStyles } from '@/components/WfoTable/WfoStructuredSearchTable/styles';
9
+ import { useWithOrchestratorTheme } from '@/hooks';
10
+
11
+ export const WfoAddGroupAction = ({ disabled, handleOnClick }: ActionProps) => {
12
+ const t = useTranslations('search.page');
13
+ const { addGroupStyles, addRulePlusStyles } = useWithOrchestratorTheme(getWfoStructuredSearchTableStyles);
14
+ return disabled ? null : (
15
+ <EuiFlexItem css={addGroupStyles} onClick={() => handleOnClick()}>
16
+ <span css={addRulePlusStyles}>+</span>&nbsp;{t('addGroup')}
17
+ </EuiFlexItem>
18
+ );
19
+ };
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import type { ActionProps } from 'react-querybuilder';
3
+
4
+ import { useTranslations } from 'next-intl';
5
+
6
+ import { EuiFlexItem } from '@elastic/eui';
7
+
8
+ import { useWithOrchestratorTheme } from '@/hooks';
9
+
10
+ import { getWfoStructuredSearchTableStyles } from './styles';
11
+
12
+ export const WfoAddRuleAction = (props: ActionProps) => {
13
+ const { addRulePlusStyles, addRuleContainerStyles } = useWithOrchestratorTheme(getWfoStructuredSearchTableStyles);
14
+
15
+ const t = useTranslations('search.page');
16
+
17
+ return (
18
+ <EuiFlexItem grow={false} onClick={() => props.handleOnClick()} css={addRuleContainerStyles}>
19
+ <span css={addRulePlusStyles}>+</span>
20
+ {t('addRule')}
21
+ </EuiFlexItem>
22
+ );
23
+ };