@orchestrator-ui/orchestrator-ui-components 1.2.0 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestrator-ui/orchestrator-ui-components",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "test": "jest",
@@ -0,0 +1,99 @@
1
+ import React, { useContext } from 'react';
2
+
3
+ import { useTranslations } from 'next-intl';
4
+ import Link from 'next/link';
5
+
6
+ import { EuiButton } from '@elastic/eui';
7
+
8
+ import { ConfirmationDialogContext } from '@/contexts';
9
+ import { useOrchestratorTheme, useShowToastMessage } from '@/hooks';
10
+ import { useSetSubscriptionInSyncMutation } from '@/rtk/endpoints';
11
+ import { SubscriptionDetail, ToastTypes } from '@/types';
12
+ import { formatDate } from '@/utils';
13
+
14
+ import { WfoInsyncIcon } from '../WfoInsyncIcon/WfoInsyncIcon';
15
+ import { PATH_TASKS, PATH_WORKFLOWS } from '../WfoPageTemplate';
16
+ import { getLastUncompletedProcess, getLatestTaskDate } from './utils';
17
+
18
+ interface WfoInSyncFieldProps {
19
+ subscriptionDetail: SubscriptionDetail;
20
+ }
21
+
22
+ export const WfoInSyncField = ({ subscriptionDetail }: WfoInSyncFieldProps) => {
23
+ const t = useTranslations('subscriptions.detail');
24
+ const { theme } = useOrchestratorTheme();
25
+ const inSync = subscriptionDetail.insync;
26
+ const lastTaskRunDate = getLatestTaskDate(
27
+ subscriptionDetail.processes.page,
28
+ );
29
+ const lastUncompletedProcess = getLastUncompletedProcess(
30
+ subscriptionDetail.processes.page,
31
+ );
32
+ const [setSubscriptionInSync] = useSetSubscriptionInSyncMutation();
33
+ const { showToastMessage } = useShowToastMessage();
34
+ const { showConfirmDialog } = useContext(ConfirmationDialogContext);
35
+
36
+ const setInSyncAction = () => {
37
+ setSubscriptionInSync(subscriptionDetail.subscriptionId)
38
+ .unwrap()
39
+ .then(() => {
40
+ // Optimistic update for now
41
+ showToastMessage(
42
+ ToastTypes.SUCCESS,
43
+ t('setInSyncSuccess.text'),
44
+ t('setInSyncSuccess.title'),
45
+ );
46
+ subscriptionDetail.insync = true;
47
+ })
48
+ .catch((error) => {
49
+ showToastMessage(
50
+ ToastTypes.ERROR,
51
+ error?.data?.detail
52
+ ? error.data.detail
53
+ : t('setInSyncFailed.text').toString,
54
+ t('setInSyncFailed.title'),
55
+ );
56
+ console.error('Failed to set subscription in sync.', error);
57
+ });
58
+ };
59
+
60
+ const getProcessLink = () => {
61
+ const processUrl = `${lastUncompletedProcess?.isTask ? PATH_TASKS : PATH_WORKFLOWS}/${lastUncompletedProcess?.processId}`;
62
+
63
+ const confirmSetInSync = () => {
64
+ showConfirmDialog({
65
+ question: t('setInSyncQuestion'),
66
+ confirmAction: () => {
67
+ setInSyncAction();
68
+ },
69
+ });
70
+ };
71
+
72
+ return (
73
+ <>
74
+ <Link
75
+ href={processUrl}
76
+ css={{
77
+ paddingLeft: theme.base / 2,
78
+ paddingRight: theme.base,
79
+ }}
80
+ >
81
+ {t('see')} {lastUncompletedProcess?.processId}
82
+ </Link>
83
+ <EuiButton color="danger" size="s" onClick={confirmSetInSync}>
84
+ {t('setInSync')}
85
+ </EuiButton>
86
+ </>
87
+ );
88
+ };
89
+
90
+ return (
91
+ <>
92
+ <div css={{ paddingRight: theme.base / 4, display: 'flex' }}>
93
+ <WfoInsyncIcon inSync={inSync} />
94
+ </div>
95
+ {inSync && lastTaskRunDate && `(${formatDate(lastTaskRunDate)})`}
96
+ {!inSync && lastUncompletedProcess && getProcessLink()}
97
+ </>
98
+ );
99
+ };
@@ -133,16 +133,16 @@ export const WfoProcessesTimeline = ({
133
133
  <EuiCommentList aria-label="Processes">
134
134
  {subscriptionDetailProcesses && (
135
135
  <EuiCommentList aria-label="Processes">
136
- {subscriptionDetailProcesses.map(
137
- (subscriptionDetailProcess, index) => (
136
+ {subscriptionDetailProcesses
137
+ .filter((process) => !process.isTask)
138
+ .map((subscriptionDetailProcess, index) => (
138
139
  <WfoRenderSubscriptionProcess
139
140
  key={index}
140
141
  subscriptionDetailProcess={
141
142
  subscriptionDetailProcess
142
143
  }
143
144
  />
144
- ),
145
- )}
145
+ ))}
146
146
  </EuiCommentList>
147
147
  )}
148
148
  </EuiCommentList>
@@ -11,9 +11,9 @@ import {
11
11
  WfoProductStatusBadge,
12
12
  WfoSubscriptionStatusBadge,
13
13
  } from '../WfoBadges';
14
- import { WfoInsyncIcon } from '../WfoInsyncIcon/WfoInsyncIcon';
15
14
  import { WfoKeyValueTableDataType } from '../WfoKeyValueTable/WfoKeyValueTable';
16
15
  import { SubscriptionKeyValueBlock } from './SubscriptionKeyValueBlock';
16
+ import { WfoInSyncField } from './WfoInSyncField';
17
17
 
18
18
  interface WfoSubscriptionGeneralProps {
19
19
  subscriptionDetail: SubscriptionDetail;
@@ -57,7 +57,9 @@ export const WfoSubscriptionGeneral = ({
57
57
  },
58
58
  {
59
59
  key: t('insync'),
60
- value: <WfoInsyncIcon inSync={subscriptionDetail.insync} />,
60
+ value: (
61
+ <WfoInSyncField subscriptionDetail={subscriptionDetail} />
62
+ ),
61
63
  },
62
64
  {
63
65
  key: t('customer'),
@@ -6,3 +6,4 @@ export * from './SubscriptionKeyValueBlock';
6
6
  export * from './WfoSubscriptionDetailTree';
7
7
  export * from './WfoSubscriptionGeneral';
8
8
  export * from './WfoSubscription';
9
+ export * from './WfoInSyncField';
@@ -1,10 +1,17 @@
1
1
  import { EuiThemeComputed } from '@elastic/eui';
2
2
 
3
3
  import { SubscriptionAction } from '../../../hooks';
4
- import { FieldValue, WorkflowTarget } from '../../../types';
4
+ import {
5
+ FieldValue,
6
+ ProcessStatus,
7
+ SubscriptionDetailProcess,
8
+ WorkflowTarget,
9
+ } from '../../../types';
5
10
  import {
6
11
  flattenArrayProps,
7
12
  getFieldFromProductBlockInstanceValues,
13
+ getLastUncompletedProcess,
14
+ getLatestTaskDate,
8
15
  getProductBlockTitle,
9
16
  getWorkflowTargetColor,
10
17
  getWorkflowTargetIconContent,
@@ -190,3 +197,166 @@ describe('getWorkflowTargetIconContent', () => {
190
197
  ).toBe('M');
191
198
  });
192
199
  });
200
+
201
+ const testProcess: SubscriptionDetailProcess = {
202
+ workflowName: 'testWorkflow 1',
203
+ lastStatus: ProcessStatus.COMPLETED,
204
+ workflowTarget: WorkflowTarget.MODIFY,
205
+ createdBy: 'testUser 1',
206
+ processId: 'testProcessId 1',
207
+ startedAt: '2021-01-01T00:00:00Z',
208
+ isTask: false,
209
+ };
210
+
211
+ describe('getLastUncompletedProcess', () => {
212
+ it('Returns empty string with empty process array', () => {
213
+ const processes: SubscriptionDetailProcess[] = [];
214
+
215
+ const getLastUncompletedProcessResult =
216
+ getLastUncompletedProcess(processes);
217
+
218
+ expect(getLastUncompletedProcessResult).toBe(undefined);
219
+ });
220
+
221
+ it('Return undefined string when there is only completed process', () => {
222
+ const completedProcesses = [
223
+ {
224
+ ...testProcess,
225
+ lastStatus: ProcessStatus.COMPLETED,
226
+ startedAt: '2021-01-01T00:00:00Z',
227
+ },
228
+ {
229
+ ...testProcess,
230
+ lastStatus: ProcessStatus.COMPLETED,
231
+ startedAt: '2021-02-01T00:00:00Z',
232
+ },
233
+ {
234
+ ...testProcess,
235
+ lastStatus: ProcessStatus.COMPLETED,
236
+ startedAt: '2021-03-01T00:00:00Z',
237
+ },
238
+ ];
239
+
240
+ const lastUncompletedProcess =
241
+ getLastUncompletedProcess(completedProcesses);
242
+
243
+ expect(lastUncompletedProcess).toBe(undefined);
244
+ });
245
+ it('Returns uncompleted process from list of processes', () => {
246
+ const failedProcess = {
247
+ ...testProcess,
248
+ lastStatus: ProcessStatus.FAILED,
249
+ processId: 'FAILED_PROCESS_ID',
250
+ startedAt: '2021-02-01T00:00:00Z',
251
+ };
252
+
253
+ const failedProcesses = [
254
+ {
255
+ ...testProcess,
256
+ lastStatus: ProcessStatus.COMPLETED,
257
+ startedAt: '2021-01-01T00:00:00Z',
258
+ },
259
+ failedProcess,
260
+ {
261
+ ...testProcess,
262
+ lastStatus: ProcessStatus.COMPLETED,
263
+ startedAt: '2021-03-01T00:00:00Z',
264
+ },
265
+ ];
266
+
267
+ const lastUncompletedProcess =
268
+ getLastUncompletedProcess(failedProcesses);
269
+
270
+ expect(lastUncompletedProcess?.processId).toBe('FAILED_PROCESS_ID');
271
+ });
272
+
273
+ it('Returns last failed process if there are more uncompleted processes', () => {
274
+ const failedProcess = {
275
+ ...testProcess,
276
+ lastStatus: ProcessStatus.FAILED,
277
+ processId: 'FAILED_PROCESS_1',
278
+ startedAt: '2021-02-01T00:00:00Z',
279
+ };
280
+
281
+ const failedProcess2 = {
282
+ ...testProcess,
283
+ lastStatus: ProcessStatus.SUSPENDED,
284
+ processId: 'FAILED_PROCESS_ID_2',
285
+ startedAt: '2021-04-01T00:00:00Z',
286
+ };
287
+
288
+ const failedProcesses = [
289
+ {
290
+ ...testProcess,
291
+ lastStatus: ProcessStatus.COMPLETED,
292
+ startedAt: '2021-01-01T00:00:00Z',
293
+ },
294
+ failedProcess,
295
+ {
296
+ ...testProcess,
297
+ lastStatus: ProcessStatus.COMPLETED,
298
+ startedAt: '2021-03-01T00:00:00Z',
299
+ },
300
+ failedProcess2,
301
+ ];
302
+
303
+ const lastUncompletedProcess =
304
+ getLastUncompletedProcess(failedProcesses);
305
+
306
+ expect(lastUncompletedProcess?.processId).toBe('FAILED_PROCESS_ID_2');
307
+ });
308
+ });
309
+
310
+ describe('getLatestTaskDate', () => {
311
+ it('Returns empty string on empty array', () => {
312
+ const tasks: SubscriptionDetailProcess[] = [];
313
+
314
+ const lastTask = getLatestTaskDate(tasks);
315
+
316
+ expect(lastTask).toBe('');
317
+ });
318
+
319
+ it('Returns empty string if there are no tasks among the processes', () => {
320
+ const workflowsOnly = [
321
+ { ...testProcess, isTask: false },
322
+ { ...testProcess, isTask: false },
323
+ ];
324
+
325
+ const lastTask = getLatestTaskDate(workflowsOnly);
326
+
327
+ expect(lastTask).toBe('');
328
+ });
329
+
330
+ it('Returns date of tasks among the processes', () => {
331
+ const workflowsAndTask = [
332
+ { ...testProcess, isTask: false },
333
+ { ...testProcess, isTask: true, startedAt: '2021-01-01T00:00:00Z' },
334
+ { ...testProcess, isTask: false },
335
+ ];
336
+
337
+ const lastTaskDate = getLatestTaskDate(workflowsAndTask);
338
+
339
+ expect(lastTaskDate).toBe('2021-01-01T00:00:00Z');
340
+ });
341
+
342
+ it('Returns date of last task among the processes if there are more tasks', () => {
343
+ const workflowsAndTask = [
344
+ {
345
+ ...testProcess,
346
+ isTask: false,
347
+ startedAt: '2021-01-01T00:00:00Z',
348
+ },
349
+ { ...testProcess, isTask: true, startedAt: '2021-02-01T00:00:00Z' },
350
+ {
351
+ ...testProcess,
352
+ isTask: false,
353
+ startedAt: '2021-03-01T00:00:00Z',
354
+ },
355
+ { ...testProcess, isTask: true, startedAt: '2021-04-01T00:00:00Z' },
356
+ ];
357
+
358
+ const lastTaskDate = getLatestTaskDate(workflowsAndTask);
359
+
360
+ expect(lastTaskDate).toBe('2021-04-01T00:00:00Z');
361
+ });
362
+ });
@@ -2,9 +2,14 @@ import { TranslationValues } from 'next-intl';
2
2
 
3
3
  import { EuiThemeComputed } from '@elastic/eui';
4
4
 
5
- import { SubscriptionAction } from '../../../hooks';
6
- import type { FieldValue } from '../../../types';
7
- import { WorkflowTarget } from '../../../types';
5
+ import { SubscriptionAction } from '@/hooks';
6
+
7
+ import {
8
+ FieldValue,
9
+ ProcessStatus,
10
+ SubscriptionDetailProcess,
11
+ WorkflowTarget,
12
+ } from '../../../types';
8
13
 
9
14
  const MAX_LABEL_LENGTH = 45;
10
15
 
@@ -90,3 +95,39 @@ export const getWorkflowTargetIconContent = (
90
95
  return 'M';
91
96
  }
92
97
  };
98
+
99
+ export const getLastUncompletedProcess = (
100
+ processes: SubscriptionDetailProcess[],
101
+ ): SubscriptionDetailProcess | undefined => {
102
+ if (processes.length === 0) {
103
+ return;
104
+ }
105
+
106
+ const uncompletedProcesses = processes
107
+ .filter((process) => process.lastStatus !== ProcessStatus.COMPLETED)
108
+ .sort((a, b) => {
109
+ const dateA = new Date(a.startedAt);
110
+ const dateB = new Date(b.startedAt);
111
+ return dateB.getTime() - dateA.getTime();
112
+ });
113
+
114
+ return uncompletedProcesses.length > 0
115
+ ? uncompletedProcesses[0]
116
+ : undefined;
117
+ };
118
+
119
+ export const getLatestTaskDate = (processes: SubscriptionDetailProcess[]) => {
120
+ if (processes.length === 0) {
121
+ return '';
122
+ }
123
+
124
+ const tasks = processes
125
+ .filter((process) => process.isTask)
126
+ .sort((a, b) => {
127
+ const dateA = new Date(a.startedAt);
128
+ const dateB = new Date(b.startedAt);
129
+ return dateB.getTime() - dateA.getTime();
130
+ });
131
+
132
+ return tasks.length > 0 ? tasks[0].startedAt : '';
133
+ };
@@ -46,10 +46,7 @@ export const GET_SUBSCRIPTION_DETAIL_GRAPHQL_QUERY: TypedDocumentNode<
46
46
  subscriptionInstanceId
47
47
  inUseByRelations
48
48
  }
49
- processes(
50
- sortBy: { field: "startedAt", order: ASC }
51
- filterBy: { field: "isTask", value: "false" }
52
- ) {
49
+ processes(sortBy: { field: "startedAt", order: ASC }) {
53
50
  page {
54
51
  processId
55
52
  lastStatus
@@ -314,7 +314,18 @@
314
314
  "startedBy": "Started by"
315
315
  },
316
316
  "showAll": "Show all",
317
- "hideAll": "Hide all"
317
+ "hideAll": "Hide all",
318
+ "see": "See",
319
+ "setInSync": "Set in Sync",
320
+ "setInSyncQuestion": "Are you sure you want to do this? You're about to force a subscription in sync. When it's clear why the subscription is out of sync this could enable you to start or finish a change on this subscription. When you're in doubt, please consult the network automators first as running workflows on subscriptions that are not in sync can potentially do great harm to the network.",
321
+ "setInSyncFailed": {
322
+ "title": "Set in sync failed",
323
+ "text": "The subscription could not be set in sync. Please try again later."
324
+ },
325
+ "setInSyncSuccess": {
326
+ "title": "Subscription set in sync",
327
+ "text": "The subscription was successfully set in sync."
328
+ }
318
329
  }
319
330
  },
320
331
  "tasks": {
@@ -313,7 +313,18 @@
313
313
  "startedBy": "Started by"
314
314
  },
315
315
  "showAll": "Toon alles",
316
- "hideAll": "Verberg alles"
316
+ "hideAll": "Verberg alles",
317
+ "see": "Bekijk",
318
+ "setInSync": "Set in Sync",
319
+ "setInSyncQuestion": "Weet je zeker dat je de subscription in-sync wilt zetten? Je gaat een subscription geforceerd in-sync zetten. Alleen als je zeker weet wat de reden is van out-of-sync kun je de actie uitvoeren, zodat je daarna wijzigingen kunt doorvoeren op deze subscription. Bij twijfel - check eerst bij de network automators of het in sync zetten van de subscription mogelijk schadelijke gevolgen kan hebben op het netwerk.",
320
+ "setInSyncFailed": {
321
+ "title": "In sync zetten mislukt",
322
+ "text": "De subscription kon niet inSync worden gezet. Probeer het opnieuw."
323
+ },
324
+ "setInSyncSuccess": {
325
+ "title": "Subscription inSync",
326
+ "text": "De subscription is in sync gezet."
327
+ }
317
328
  }
318
329
  },
319
330
  "tasks": {
@@ -0,0 +1,22 @@
1
+ import { Subscription } from '@/types';
2
+
3
+ import { BaseQueryTypes, orchestratorApi } from '../api';
4
+
5
+ const inSyncApi = orchestratorApi.injectEndpoints({
6
+ endpoints: (build) => ({
7
+ setSubscriptionInSync: build.mutation<
8
+ void,
9
+ Subscription['subscriptionId']
10
+ >({
11
+ query: (subscriptionId) => ({
12
+ url: `subscriptions/${subscriptionId}/set_in_sync`,
13
+ method: 'PUT',
14
+ }),
15
+ extraOptions: {
16
+ baseQueryType: BaseQueryTypes.fetch,
17
+ },
18
+ }),
19
+ }),
20
+ });
21
+
22
+ export const { useSetSubscriptionInSyncMutation } = inSyncApi;
@@ -2,3 +2,4 @@ export * from './customers';
2
2
  export * from './processList';
3
3
  export * from './settings';
4
4
  export * from './streamMessages';
5
+ export * from './inSync';