@orchestrator-ui/orchestrator-ui-components 0.10.0 → 0.11.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 +4 -4
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +11 -10
- package/CHANGELOG.md +12 -0
- package/dist/index.d.ts +49 -9
- package/dist/index.js +4397 -4140
- package/package.json +1 -1
- package/src/components/WfoBadges/WfoFailedTasksBadge/WfoFailedTasksBadge.tsx +1 -3
- package/src/components/WfoBadges/WfoHeaderBadge/WfoHeaderBadge.tsx +1 -1
- package/src/components/WfoBadges/WfoSubscriptionSyncStatusBadge/WfoSubscriptionSyncStatusBadge.stories.tsx +27 -0
- package/src/components/WfoBadges/WfoSubscriptionSyncStatusBadge/WfoSubscriptionSyncStatusBadge.tsx +45 -0
- package/src/components/WfoBadges/WfoSubscriptionSyncStatusBadge/index.ts +1 -0
- package/src/components/WfoBadges/WfoWebsocketStatusBadge/WfoWebsocketStatusBadge.stories.tsx +13 -0
- package/src/components/WfoBadges/WfoWebsocketStatusBadge/WfoWebsocketStatusBadge.tsx +41 -0
- package/src/components/WfoBadges/WfoWebsocketStatusBadge/index.ts +1 -0
- package/src/components/WfoForms/formFields/CustomerField.tsx +4 -1
- package/src/components/WfoPageTemplate/WfoPageHeader/WfoPageHeader.tsx +47 -2
- package/src/components/WfoPageTemplate/WfoPageTemplate/WfoPageTemplate.tsx +4 -1
- package/src/components/WfoSubscription/WfoSubscription.tsx +25 -2
- package/src/contexts/OrchestratorConfigContext.tsx +1 -0
- package/src/hooks/useOrchestratorTheme.ts +3 -1
- package/src/hooks/useShowToastMessage.ts +4 -26
- package/src/icons/WfoSideMenu.stories.tsx +1 -1
- package/src/messages/en-GB.json +8 -2
- package/src/messages/nl-NL.json +8 -2
- package/src/rtk/api.ts +7 -1
- package/src/rtk/endpoints/index.ts +1 -0
- package/src/rtk/endpoints/settings.ts +3 -3
- package/src/rtk/endpoints/streamMessages.ts +128 -0
- package/src/theme/defaultOrchestratorTheme.ts +28 -1
- package/src/types/types.ts +6 -0
- package/src/utils/getToastMessage.spec.ts +28 -0
- package/src/utils/getToastMessage.ts +35 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { debounce } from 'lodash';
|
|
2
|
+
|
|
3
|
+
import { addToastMessage } from '@/rtk/slices/toastMessages';
|
|
4
|
+
import type { RootState } from '@/rtk/store';
|
|
5
|
+
import { ToastTypes } from '@/types';
|
|
6
|
+
import { getToastMessage } from '@/utils/getToastMessage';
|
|
7
|
+
|
|
8
|
+
import { CacheTags, orchestratorApi } from '../api';
|
|
9
|
+
|
|
10
|
+
const getWebSocket = (url: string) => {
|
|
11
|
+
// TODO: Implement authentication taking this into account: https://stackoverflow.com/questions/4361173/http-headers-in-websockets-client-api/77060459#77060459
|
|
12
|
+
// https://github.com/workfloworchestrator/orchestrator-core/issues/502 - https://github.com/workfloworchestrator/orchestrator-ui-library/issues/823
|
|
13
|
+
return new WebSocket(url);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const PING_INTERVAL_MS = 5000;
|
|
17
|
+
const DEBOUNCE_CLOSE_INTERVAL_MS = 10000;
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* Websocket handling as recommended by RTK QUery see: https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#streaming-data-with-no-initial-request
|
|
21
|
+
* The websocket is opened after the cacheDataLoaded promise is resolved, and closed after the cacheEntryRemoved promise is resolved maintaining
|
|
22
|
+
* the connection in between
|
|
23
|
+
* - It sends a ping message every PING_INTERVAL ms to keep the connection alive
|
|
24
|
+
* - It debounces the close event to avoid closing the connection every time a 'pong' message is received
|
|
25
|
+
* - It closes the connection if any websocket error or close event is received
|
|
26
|
+
* - It invalidates the cache entry with the tag received in the message event
|
|
27
|
+
*/
|
|
28
|
+
const streamMessagesApi = orchestratorApi.injectEndpoints({
|
|
29
|
+
endpoints: (build) => ({
|
|
30
|
+
streamMessages: build.query<boolean, void>({
|
|
31
|
+
queryFn: () => {
|
|
32
|
+
return { data: false };
|
|
33
|
+
},
|
|
34
|
+
async onCacheEntryAdded(
|
|
35
|
+
_,
|
|
36
|
+
{
|
|
37
|
+
cacheDataLoaded,
|
|
38
|
+
cacheEntryRemoved,
|
|
39
|
+
dispatch,
|
|
40
|
+
getState,
|
|
41
|
+
updateCachedData,
|
|
42
|
+
},
|
|
43
|
+
) {
|
|
44
|
+
const cleanUp = () => {
|
|
45
|
+
const message = getToastMessage(
|
|
46
|
+
ToastTypes.ERROR,
|
|
47
|
+
'Connection to the server was lost. Please refresh the page to reconnect.',
|
|
48
|
+
'WebSocket closed',
|
|
49
|
+
);
|
|
50
|
+
dispatch(addToastMessage(message));
|
|
51
|
+
clearInterval(pingInterval);
|
|
52
|
+
updateCachedData(() => false);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
await cacheDataLoaded;
|
|
56
|
+
|
|
57
|
+
const state = getState() as RootState;
|
|
58
|
+
const { orchestratorWebsocketUrl } = state.orchestratorConfig;
|
|
59
|
+
|
|
60
|
+
// Starts the websocket
|
|
61
|
+
const webSocket = getWebSocket(orchestratorWebsocketUrl);
|
|
62
|
+
|
|
63
|
+
// Lets the WfoWebsocketStatusBadge know the websocket is connected
|
|
64
|
+
webSocket.onopen = () => {
|
|
65
|
+
updateCachedData(() => true);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Send a ping message every to the websocket server to keep the connection alive
|
|
69
|
+
const pingInterval = setInterval(() => {
|
|
70
|
+
webSocket.send('ping');
|
|
71
|
+
}, PING_INTERVAL_MS);
|
|
72
|
+
|
|
73
|
+
const debounceCloseWebSocket = debounce(() => {
|
|
74
|
+
webSocket.close();
|
|
75
|
+
}, DEBOUNCE_CLOSE_INTERVAL_MS);
|
|
76
|
+
// Start the debounced function to close the websocket when no 'pong' message is received after DEBOUNCE_CLOSE_INTERVAL
|
|
77
|
+
debounceCloseWebSocket();
|
|
78
|
+
|
|
79
|
+
webSocket.addEventListener(
|
|
80
|
+
'message',
|
|
81
|
+
(message: MessageEvent<string>) => {
|
|
82
|
+
const tagOrPong = message.data.trim();
|
|
83
|
+
|
|
84
|
+
if (tagOrPong === 'pong') {
|
|
85
|
+
// Reset the debounced every time a 'pong' message is received
|
|
86
|
+
debounceCloseWebSocket();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const tagToInvalidate = tagOrPong as CacheTags;
|
|
91
|
+
const validCacheTags = Object.values(CacheTags);
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
tagToInvalidate &&
|
|
95
|
+
validCacheTags.includes(tagToInvalidate)
|
|
96
|
+
) {
|
|
97
|
+
const cacheInvalidationAction =
|
|
98
|
+
orchestratorApi.util.invalidateTags([
|
|
99
|
+
tagToInvalidate,
|
|
100
|
+
]);
|
|
101
|
+
dispatch(cacheInvalidationAction);
|
|
102
|
+
} else {
|
|
103
|
+
console.error(
|
|
104
|
+
`Trying to invalidate a cache entry with an unknown tag: ${tagToInvalidate}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
webSocket.onerror = (event) => {
|
|
111
|
+
console.error('WebSocket error', event);
|
|
112
|
+
cleanUp();
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
webSocket.onclose = () => {
|
|
116
|
+
console.error('WebSocket closed');
|
|
117
|
+
cleanUp();
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
await cacheEntryRemoved;
|
|
121
|
+
|
|
122
|
+
webSocket.close();
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export const { useStreamMessagesQuery } = streamMessagesApi;
|
|
@@ -23,7 +23,34 @@ export const defaultOrchestratorTheme: EuiThemeModifications = {
|
|
|
23
23
|
xxxxl: '64px',
|
|
24
24
|
},
|
|
25
25
|
colors: {
|
|
26
|
-
DARK: {
|
|
26
|
+
DARK: {
|
|
27
|
+
primary: '#265391',
|
|
28
|
+
accent: '#F68FBE',
|
|
29
|
+
success: '#7DDED8',
|
|
30
|
+
warning: '#F3D371',
|
|
31
|
+
danger: '#F86B63',
|
|
32
|
+
primaryText: '#3EA6fA',
|
|
33
|
+
accentText: '#F68FBE',
|
|
34
|
+
successText: '#7DDEd8',
|
|
35
|
+
warningText: '#F3D371',
|
|
36
|
+
dangerText: '#F86B63',
|
|
37
|
+
emptyShade: '#1D1E24',
|
|
38
|
+
lightestShade: '#25262E',
|
|
39
|
+
lightShade: '#343741',
|
|
40
|
+
mediumShade: '#535966',
|
|
41
|
+
darkShade: '#98A2B3',
|
|
42
|
+
darkestShade: '#D4DAE5',
|
|
43
|
+
fullShade: '#FFFFFF',
|
|
44
|
+
text: '#DFE5EF',
|
|
45
|
+
title: '#DFE5EF',
|
|
46
|
+
subduedText: '#81858f',
|
|
47
|
+
link: '#3EA6fA',
|
|
48
|
+
body: '#141519',
|
|
49
|
+
highlight: '#262830',
|
|
50
|
+
disabled: '#515761',
|
|
51
|
+
disabledText: '#515761',
|
|
52
|
+
shadow: '#000000',
|
|
53
|
+
},
|
|
27
54
|
LIGHT: {
|
|
28
55
|
primary: '#0077C8',
|
|
29
56
|
accent: '#E67300',
|
package/src/types/types.ts
CHANGED
|
@@ -462,6 +462,7 @@ export enum Environment {
|
|
|
462
462
|
|
|
463
463
|
export type OrchestratorConfig = {
|
|
464
464
|
environmentName: Environment | string;
|
|
465
|
+
orchestratorWebsocketUrl: string;
|
|
465
466
|
orchestratorApiBaseUrl: string;
|
|
466
467
|
graphqlEndpointCore: string;
|
|
467
468
|
engineStatusEndpoint: string;
|
|
@@ -471,3 +472,8 @@ export type OrchestratorConfig = {
|
|
|
471
472
|
subscriptionProcessesEndpoint: string;
|
|
472
473
|
authActive: boolean;
|
|
473
474
|
};
|
|
475
|
+
|
|
476
|
+
export enum ColorModes {
|
|
477
|
+
LIGHT = 'LIGHT',
|
|
478
|
+
DARK = 'DARK',
|
|
479
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ToastTypes } from '../types';
|
|
2
|
+
import { getToastMessage } from './getToastMessage';
|
|
3
|
+
|
|
4
|
+
describe('getToastMessage()', () => {
|
|
5
|
+
it('returns message object with title, text and passed in type', () => {
|
|
6
|
+
const result = getToastMessage(ToastTypes.ERROR, 'text', 'title');
|
|
7
|
+
expect(result.title).toBe('title');
|
|
8
|
+
expect(result.text).toBe('text');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('returns message object with random integer id', () => {
|
|
12
|
+
const result = getToastMessage(ToastTypes.ERROR, 'text', 'title');
|
|
13
|
+
const result2 = getToastMessage(ToastTypes.ERROR, 'text', 'title');
|
|
14
|
+
expect(result.id).not.toBe(result2.id);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns message object with ToastTypes.Error to color danger and iconType warning', () => {
|
|
18
|
+
const result = getToastMessage(ToastTypes.ERROR, 'text', 'title');
|
|
19
|
+
expect(result.color).toBe('danger');
|
|
20
|
+
expect(result.iconType).toBe('warning');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns message object with ToastTypes.Error to color success and iconType check', () => {
|
|
24
|
+
const result = getToastMessage(ToastTypes.SUCCESS, 'text', 'title');
|
|
25
|
+
expect(result.color).toBe('success');
|
|
26
|
+
expect(result.iconType).toBe('check');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ToastTypes } from '../types';
|
|
2
|
+
import type { Toast } from '../types';
|
|
3
|
+
|
|
4
|
+
export const getToastMessage = (
|
|
5
|
+
type: ToastTypes,
|
|
6
|
+
text: string, // We use string here instead of Toast['text'] because we want to prevent passing in react component because they trigger an "unsynchronizable values in payload detected" error',
|
|
7
|
+
title: string, // same as above for string instead of Toast['title']
|
|
8
|
+
) => {
|
|
9
|
+
const getTypeProps = (type: ToastTypes): Partial<Toast> => {
|
|
10
|
+
switch (type) {
|
|
11
|
+
case ToastTypes.ERROR:
|
|
12
|
+
return {
|
|
13
|
+
color: 'danger',
|
|
14
|
+
iconType: 'warning',
|
|
15
|
+
};
|
|
16
|
+
case ToastTypes.SUCCESS:
|
|
17
|
+
return {
|
|
18
|
+
color: 'success',
|
|
19
|
+
iconType: 'check',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const id = Math.floor(Math.random() * 10000000).toString();
|
|
25
|
+
const typeProps = getTypeProps(type);
|
|
26
|
+
|
|
27
|
+
const toast: Toast = {
|
|
28
|
+
id,
|
|
29
|
+
text,
|
|
30
|
+
title,
|
|
31
|
+
...typeProps,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return toast;
|
|
35
|
+
};
|