@rancher/shell 3.0.8-rc.2 → 3.0.8-rc.3
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/assets/brand/suse/dark/rancher-logo.svg +64 -1
- package/assets/brand/suse/rancher-logo.svg +1 -1
- package/assets/styles/global/_cards.scss +0 -3
- package/assets/styles/themes/_modern.scss +9 -1
- package/assets/styles/themes/_suse.scss +81 -24
- package/assets/translations/en-us.yaml +68 -3
- package/components/AutoscalerCard.vue +113 -0
- package/components/AutoscalerTab.vue +94 -0
- package/components/ClusterIconMenu.vue +1 -1
- package/components/ClusterProviderIcon.vue +1 -1
- package/components/IconOrSvg.vue +2 -2
- package/components/PopoverCard.vue +192 -0
- package/components/Resource/Detail/FetchLoader/composables.ts +18 -4
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +1 -1
- package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +4 -0
- package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +2 -19
- package/components/Resource/Detail/ResourcePopover/__tests__/ResourcePopoverCard.test.ts +0 -29
- package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +132 -150
- package/components/Resource/Detail/ResourcePopover/index.vue +54 -159
- package/components/ResourceDetail/Masthead/latest.vue +29 -0
- package/components/ResourceList/Masthead.vue +1 -1
- package/components/__tests__/AutoscalerCard.test.ts +154 -0
- package/components/__tests__/AutoscalerTab.test.ts +125 -0
- package/components/__tests__/PopoverCard.test.ts +204 -0
- package/components/formatter/Autoscaler.vue +97 -0
- package/components/formatter/InternalExternalIP.vue +195 -24
- package/components/formatter/__tests__/Autoscaler.test.ts +156 -0
- package/components/formatter/__tests__/InternalExternalIP.test.ts +133 -0
- package/components/nav/Group.vue +12 -3
- package/components/nav/TopLevelMenu.vue +2 -2
- package/composables/useInterval.ts +15 -0
- package/config/labels-annotations.js +8 -1
- package/config/product/manager.js +20 -9
- package/config/router/routes.js +4 -0
- package/config/settings.ts +2 -1
- package/config/table-headers.js +8 -0
- package/config/types.js +2 -0
- package/core/types-provisioning.ts +3 -0
- package/detail/provisioning.cattle.io.cluster.vue +12 -1
- package/directives/ui-context.ts +8 -2
- package/edit/auth/github.vue +5 -0
- package/edit/cloudcredential.vue +1 -1
- package/edit/fleet.cattle.io.gitrepo.vue +0 -10
- package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +32 -5
- package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.test.ts +35 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +132 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +18 -12
- package/edit/provisioning.cattle.io.cluster/rke2.vue +39 -8
- package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +107 -5
- package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +90 -3
- package/initialize/install-plugins.js +3 -1
- package/list/provisioning.cattle.io.cluster.vue +15 -2
- package/machine-config/amazonec2.vue +36 -135
- package/machine-config/components/EC2Networking.vue +474 -0
- package/machine-config/components/__tests__/EC2Networking.test.ts +94 -0
- package/machine-config/components/__tests__/utils/vpcSubnetMockData.js +294 -0
- package/machine-config/digitalocean.vue +11 -0
- package/models/cluster/node.js +13 -6
- package/models/cluster.x-k8s.io.machine.js +10 -20
- package/models/cluster.x-k8s.io.machinedeployment.js +5 -1
- package/models/management.cattle.io.kontainerdriver.js +1 -0
- package/models/provisioning.cattle.io.cluster.js +223 -2
- package/package.json +1 -1
- package/pages/c/_cluster/apps/charts/install.vue +1 -1
- package/pages/c/_cluster/manager/hostedprovider/index.vue +209 -0
- package/plugins/dynamic-content.js +13 -0
- package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +8 -0
- package/store/features.js +1 -0
- package/store/notifications.ts +32 -1
- package/store/plugins.js +7 -3
- package/store/prefs.js +1 -0
- package/types/notifications/index.ts +24 -3
- package/types/shell/index.d.ts +26 -1
- package/utils/__tests__/object.test.ts +19 -0
- package/utils/autoscaler-utils.ts +7 -0
- package/utils/dynamic-content/__tests__/announcement.test.ts +498 -0
- package/utils/dynamic-content/announcement.ts +112 -0
- package/utils/dynamic-content/example.json +40 -0
- package/utils/dynamic-content/index.ts +6 -2
- package/utils/dynamic-content/new-release.ts +1 -1
- package/utils/dynamic-content/notification-handler.ts +48 -0
- package/utils/dynamic-content/types.d.ts +33 -1
- package/utils/object.js +20 -2
- package/utils/scroll.js +7 -0
- package/utils/settings.ts +15 -0
- package/utils/validators/machine-pool.ts +13 -3
|
@@ -29,7 +29,7 @@ export type NotificationAction = {
|
|
|
29
29
|
* Defines the User Preference linked to a notification
|
|
30
30
|
*/
|
|
31
31
|
export type NotificationPreference = {
|
|
32
|
-
key: string; // User preference key to use when setting the preference when the notification is marked as read
|
|
32
|
+
key: string; // User preference key to use when setting the preference when the notification is marked as read/unread
|
|
33
33
|
value: string; // User preference value to use when setting the preference when the notification is marked as read
|
|
34
34
|
unsetValue?: string; // User preference value to use when setting the preference when the notification is marked as unread - defaults to empty string
|
|
35
35
|
};
|
|
@@ -47,6 +47,11 @@ export type EncryptedNotification = {
|
|
|
47
47
|
primaryAction?: NotificationAction;
|
|
48
48
|
// Secondary to be shown in the notification (optional)
|
|
49
49
|
secondaryAction?: NotificationAction;
|
|
50
|
+
// User Preference tied to the notification (optional) (the preference will be updated when the notification is marked read)
|
|
51
|
+
preference?: NotificationPreference;
|
|
52
|
+
// Handler to be associated with this notification that can invoke additional behaviour when the notification changes
|
|
53
|
+
// This is the name of the handler (the handlers are added as extensions). Notifications are persisted in the store, so can't use functions.
|
|
54
|
+
handlerName?: string;
|
|
50
55
|
};
|
|
51
56
|
|
|
52
57
|
/**
|
|
@@ -57,8 +62,6 @@ export type Notification = {
|
|
|
57
62
|
id: string;
|
|
58
63
|
// Progress (0-100) for notifications of type `Task` (optional)
|
|
59
64
|
progress?: number;
|
|
60
|
-
// User Preference tied to the notification (optional) (the preference will be updated when the notification is marked read)
|
|
61
|
-
preference?: NotificationPreference;
|
|
62
65
|
} & EncryptedNotification;
|
|
63
66
|
|
|
64
67
|
/**
|
|
@@ -72,3 +75,21 @@ export type StoredNotification = {
|
|
|
72
75
|
created: Date;
|
|
73
76
|
read: Boolean;
|
|
74
77
|
} & Notification;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Name to use when registering a custom notification handler
|
|
81
|
+
*/
|
|
82
|
+
export const NotificationHandlerExtensionName = 'notification-handler';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Interface for notification handler
|
|
86
|
+
*/
|
|
87
|
+
export interface NotificationHandler {
|
|
88
|
+
/**
|
|
89
|
+
* Called when a notification with this handler has its read status is updated (read or unread)
|
|
90
|
+
*
|
|
91
|
+
* @param notification Notification that was marked read or unread
|
|
92
|
+
* @param read Indicates whether the notification was updated to be read or unread
|
|
93
|
+
*/
|
|
94
|
+
onReadUpdated(notification: Notification, read: boolean): void;
|
|
95
|
+
}
|
package/types/shell/index.d.ts
CHANGED
|
@@ -70,6 +70,9 @@ export namespace CAPI {
|
|
|
70
70
|
let SECRET_AUTH: string;
|
|
71
71
|
let SECRET_WILL_DELETE: string;
|
|
72
72
|
let UI_CUSTOM_PROVIDER: string;
|
|
73
|
+
let AUTOSCALER_CLUSTER_PAUSE: string;
|
|
74
|
+
let AUTOSCALER_MACHINE_POOL_MIN_SIZE: string;
|
|
75
|
+
let AUTOSCALER_MACHINE_POOL_MAX_SIZE: string;
|
|
73
76
|
}
|
|
74
77
|
export namespace CATALOG {
|
|
75
78
|
let CERTIFIED: string;
|
|
@@ -2000,6 +2003,18 @@ export namespace PROJECT {
|
|
|
2000
2003
|
let labelKey_124: string;
|
|
2001
2004
|
export { labelKey_124 as labelKey };
|
|
2002
2005
|
}
|
|
2006
|
+
export namespace AUTOSCALER_ENABLED {
|
|
2007
|
+
let name_128: string;
|
|
2008
|
+
export { name_128 as name };
|
|
2009
|
+
let labelKey_125: string;
|
|
2010
|
+
export { labelKey_125 as labelKey };
|
|
2011
|
+
let value_127: string;
|
|
2012
|
+
export { value_127 as value };
|
|
2013
|
+
let sort_116: string[];
|
|
2014
|
+
export { sort_116 as sort };
|
|
2015
|
+
let formatter_71: string;
|
|
2016
|
+
export { formatter_71 as formatter };
|
|
2017
|
+
}
|
|
2003
2018
|
}
|
|
2004
2019
|
|
|
2005
2020
|
// @shell/config/types
|
|
@@ -2322,6 +2337,8 @@ export const ZERO_TIME: "0001-01-01T00:00:00Z";
|
|
|
2322
2337
|
export const DEFAULT_GRAFANA_STORAGE_SIZE: "10Gi";
|
|
2323
2338
|
export const DEPRECATED: "Deprecated";
|
|
2324
2339
|
export const EXPERIMENTAL: "Experimental";
|
|
2340
|
+
export const AUTOSCALER_CONFIG_MAP_ID: "kube-system/cluster-autoscaler-status";
|
|
2341
|
+
export const HOSTED_PROVIDER: "hostedprovider";
|
|
2325
2342
|
}
|
|
2326
2343
|
|
|
2327
2344
|
// @shell/config/version
|
|
@@ -3676,6 +3693,7 @@ export const UIEXTENSION: any;
|
|
|
3676
3693
|
export const PROVISIONING_PRE_BOOTSTRAP: any;
|
|
3677
3694
|
export const SCHEDULING_CUSTOMIZATION: any;
|
|
3678
3695
|
export const SCC: any;
|
|
3696
|
+
export const AUTOSCALER: any;
|
|
3679
3697
|
export namespace getters {
|
|
3680
3698
|
function get(state: any, getters: any, rootState: any, rootGetters: any): (name: any) => any;
|
|
3681
3699
|
}
|
|
@@ -3762,6 +3780,7 @@ export const SCALE_POOL_PROMPT: any;
|
|
|
3762
3780
|
export const READ_NEW_RELEASE: any;
|
|
3763
3781
|
export const READ_SUPPORT_NOTICE: any;
|
|
3764
3782
|
export const READ_UPCOMING_SUPPORT_NOTICE: any;
|
|
3783
|
+
export const READ_ANNOUNCEMENTS: any;
|
|
3765
3784
|
export function state(): {
|
|
3766
3785
|
cookiesLoaded: boolean;
|
|
3767
3786
|
data: {};
|
|
@@ -4495,7 +4514,7 @@ export function isEmpty(obj: any): boolean;
|
|
|
4495
4514
|
export function isSimpleKeyValue(obj: any): boolean;
|
|
4496
4515
|
export function cleanUp(obj: any): any;
|
|
4497
4516
|
export function definedKeys(obj: any): any;
|
|
4498
|
-
export function diff(from: any, to: any): any;
|
|
4517
|
+
export function diff(from: any, to: any, preventNull?: boolean): any;
|
|
4499
4518
|
export function changeset(from: any, to: any, parentPath?: any[]): {};
|
|
4500
4519
|
export function changesetConflicts(a: any, b: any): any[];
|
|
4501
4520
|
export function applyChangeset(obj: any, changeset: any): any;
|
|
@@ -4735,6 +4754,12 @@ export function routeRequiresAuthentication(to: any): boolean;
|
|
|
4735
4754
|
export function routeRequiresInstallRedirect(to: any): boolean;
|
|
4736
4755
|
}
|
|
4737
4756
|
|
|
4757
|
+
// @shell/utils/scroll
|
|
4758
|
+
|
|
4759
|
+
declare module '@shell/utils/scroll' {
|
|
4760
|
+
export function scrollToBottom(): void;
|
|
4761
|
+
}
|
|
4762
|
+
|
|
4738
4763
|
// @shell/utils/select
|
|
4739
4764
|
|
|
4740
4765
|
declare module '@shell/utils/select' {
|
|
@@ -182,6 +182,25 @@ describe('fx: diff', () => {
|
|
|
182
182
|
|
|
183
183
|
expect(result).toStrictEqual(expected);
|
|
184
184
|
});
|
|
185
|
+
it('should return an object with property "baz" different than "null" if using the flag "preventNull" as true', () => {
|
|
186
|
+
const from = {
|
|
187
|
+
foo: 'bar',
|
|
188
|
+
baz: 'bang',
|
|
189
|
+
};
|
|
190
|
+
const to = {
|
|
191
|
+
foo: 'bar',
|
|
192
|
+
bang: 'baz'
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const result = diff(from, to, true);
|
|
196
|
+
const expected = {
|
|
197
|
+
// the property "baz" having value !== null covers test case for https://github.com/rancher/dashboard/issues/15710
|
|
198
|
+
baz: 'bang',
|
|
199
|
+
bang: 'baz'
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
expect(result).toStrictEqual(expected);
|
|
203
|
+
});
|
|
185
204
|
it('should return an object and dot characters in object should still be respected', () => {
|
|
186
205
|
const from = {};
|
|
187
206
|
const to = { foo: { 'bar.baz': 'bang' } };
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import { processAnnouncements, ANNOUNCEMENT_PREFIX } from '../announcement';
|
|
2
|
+
import { NotificationLevel, Notification } from '@shell/types/notifications';
|
|
3
|
+
import { READ_ANNOUNCEMENTS } from '@shell/store/prefs';
|
|
4
|
+
import { DynamicContentAnnouncementHandlerName } from '../notification-handler';
|
|
5
|
+
import { Context, VersionInfo, Announcement } from '../types';
|
|
6
|
+
import semver from 'semver';
|
|
7
|
+
|
|
8
|
+
jest.mock('semver', () => ({
|
|
9
|
+
...jest.requireActual('semver'),
|
|
10
|
+
satisfies: jest.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe('processAnnouncements', () => {
|
|
14
|
+
let mockDispatch: jest.Mock;
|
|
15
|
+
let mockGetters: any;
|
|
16
|
+
let mockLogger: any;
|
|
17
|
+
let mockContext: Context;
|
|
18
|
+
|
|
19
|
+
const VERSION_270 = { version: semver.coerce('v2.7.0') as semver.SemVer, isPrime: false };
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
mockDispatch = jest.fn();
|
|
23
|
+
mockGetters = {
|
|
24
|
+
'notifications/item': jest.fn(),
|
|
25
|
+
'prefs/get': jest.fn(),
|
|
26
|
+
};
|
|
27
|
+
mockLogger = {
|
|
28
|
+
error: jest.fn(),
|
|
29
|
+
info: jest.fn(),
|
|
30
|
+
};
|
|
31
|
+
mockContext = {
|
|
32
|
+
dispatch: mockDispatch,
|
|
33
|
+
getters: mockGetters,
|
|
34
|
+
logger: mockLogger,
|
|
35
|
+
isAdmin: false, // Default to non-admin
|
|
36
|
+
} as unknown as Context;
|
|
37
|
+
|
|
38
|
+
// Reset all mocks before each test
|
|
39
|
+
jest.clearAllMocks();
|
|
40
|
+
|
|
41
|
+
// Default mock for semver.satisfies to return true
|
|
42
|
+
(semver.satisfies as jest.Mock).mockReturnValue(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// --- Early Exit Conditions ---
|
|
46
|
+
it('should return early if no announcements are provided', async() => {
|
|
47
|
+
await processAnnouncements(mockContext, undefined, VERSION_270);
|
|
48
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
49
|
+
expect(mockLogger.info).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return early if announcements array is empty', async() => {
|
|
53
|
+
await processAnnouncements(mockContext, [], VERSION_270);
|
|
54
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
55
|
+
expect(mockLogger.info).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return early if versionInfo is undefined', async() => {
|
|
59
|
+
await processAnnouncements(mockContext, [{
|
|
60
|
+
id: '1',
|
|
61
|
+
target: 'notification/announcement',
|
|
62
|
+
title: 'Test',
|
|
63
|
+
message: 'Msg'
|
|
64
|
+
}], undefined as any);
|
|
65
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
66
|
+
expect(mockLogger.info).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should return early if versionInfo.version is undefined', async() => {
|
|
70
|
+
await processAnnouncements(mockContext, [{
|
|
71
|
+
id: '1',
|
|
72
|
+
target: 'notification/announcement',
|
|
73
|
+
title: 'Test',
|
|
74
|
+
message: 'Msg'
|
|
75
|
+
}], { version: undefined as any, isPrime: false });
|
|
76
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
77
|
+
expect(mockLogger.info).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// --- Version and Audience Filtering ---
|
|
81
|
+
it('should not process announcement if version does not satisfy requirement', async() => {
|
|
82
|
+
(semver.satisfies as jest.Mock).mockReturnValue(false);
|
|
83
|
+
const announcements: Announcement[] = [
|
|
84
|
+
{
|
|
85
|
+
id: '1',
|
|
86
|
+
target: 'notification/announcement',
|
|
87
|
+
title: 'Test',
|
|
88
|
+
message: 'Msg',
|
|
89
|
+
version: 'v2.8.0'
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
await processAnnouncements(mockContext, announcements, VERSION_270);
|
|
94
|
+
|
|
95
|
+
expect(semver.satisfies).toHaveBeenCalledWith(VERSION_270.version, announcements[0].version);
|
|
96
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
97
|
+
expect(mockLogger.info).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should not process admin-only announcement if user is not admin', async() => {
|
|
101
|
+
mockContext.isAdmin = false;
|
|
102
|
+
const announcements: Announcement[] = [
|
|
103
|
+
{
|
|
104
|
+
id: '1',
|
|
105
|
+
target: 'notification/announcement',
|
|
106
|
+
title: 'Test',
|
|
107
|
+
message: 'Msg',
|
|
108
|
+
audience: 'admin'
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
await processAnnouncements(mockContext, announcements, VERSION_270);
|
|
113
|
+
|
|
114
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
115
|
+
expect(mockLogger.info).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should process admin-only announcement if user is admin', async() => {
|
|
119
|
+
mockContext.isAdmin = true;
|
|
120
|
+
const announcements: Announcement[] = [
|
|
121
|
+
{
|
|
122
|
+
id: '1',
|
|
123
|
+
target: 'notification/announcement',
|
|
124
|
+
title: 'Test',
|
|
125
|
+
message: 'Msg',
|
|
126
|
+
audience: 'admin'
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
const versionInfo: VersionInfo = { version: VERSION_270.version };
|
|
130
|
+
|
|
131
|
+
await processAnnouncements(mockContext, announcements, versionInfo);
|
|
132
|
+
|
|
133
|
+
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
|
134
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/add', expect.any(Object));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should process all-audience announcement regardless of admin status', async() => {
|
|
138
|
+
mockContext.isAdmin = false; // Test with non-admin
|
|
139
|
+
const announcements: Announcement[] = [
|
|
140
|
+
{
|
|
141
|
+
id: '1',
|
|
142
|
+
target: 'notification/announcement',
|
|
143
|
+
title: 'Test',
|
|
144
|
+
message: 'Msg',
|
|
145
|
+
audience: 'all'
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
await processAnnouncements(mockContext, announcements, VERSION_270);
|
|
150
|
+
|
|
151
|
+
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
|
152
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/add', expect.any(Object));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// --- Target Type Handling ---
|
|
156
|
+
it('should log error for unsupported announcement target type', async() => {
|
|
157
|
+
const announcements: Announcement[] = [
|
|
158
|
+
{
|
|
159
|
+
id: '1',
|
|
160
|
+
target: 'unsupported/type',
|
|
161
|
+
title: 'Test',
|
|
162
|
+
message: 'Msg'
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
await processAnnouncements(mockContext, announcements, VERSION_270);
|
|
167
|
+
|
|
168
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Announcement type unsupported/type is not supported');
|
|
169
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should log error for unsupported notification sub-type', async() => {
|
|
173
|
+
const announcements: Announcement[] = [
|
|
174
|
+
{
|
|
175
|
+
id: '1',
|
|
176
|
+
target: 'notification/unsupported',
|
|
177
|
+
title: 'Test',
|
|
178
|
+
message: 'Msg'
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
await processAnnouncements(mockContext, announcements, VERSION_270);
|
|
183
|
+
|
|
184
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Announcement notification type unsupported is not supported');
|
|
185
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// --- Notification Creation Logic ---
|
|
189
|
+
it('should log error and not add notification if announcement has no ID', async() => {
|
|
190
|
+
const announcements: Announcement[] = [
|
|
191
|
+
{
|
|
192
|
+
target: 'notification/announcement',
|
|
193
|
+
title: 'Test',
|
|
194
|
+
message: 'Msg'
|
|
195
|
+
} as Announcement, // Missing ID
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
await processAnnouncements(mockContext, announcements, VERSION_270);
|
|
199
|
+
|
|
200
|
+
expect(mockLogger.error).toHaveBeenCalledWith('No ID For announcement - not going to add a notification for the announcement');
|
|
201
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should not add notification if one with the same ID already exists', async() => {
|
|
205
|
+
const announcementId = 'existing-announcement';
|
|
206
|
+
|
|
207
|
+
mockGetters['notifications/item'].mockReturnValue({ id: `${ ANNOUNCEMENT_PREFIX }${ announcementId }` });
|
|
208
|
+
const announcements: Announcement[] = [
|
|
209
|
+
{
|
|
210
|
+
id: announcementId,
|
|
211
|
+
target: 'notification/announcement',
|
|
212
|
+
title: 'Test',
|
|
213
|
+
message: 'Msg'
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
await processAnnouncements(mockContext, announcements, VERSION_270);
|
|
218
|
+
|
|
219
|
+
expect(mockGetters['notifications/item']).toHaveBeenCalledWith(`${ ANNOUNCEMENT_PREFIX }${ announcementId }`);
|
|
220
|
+
expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('Not adding announcement with ID'));
|
|
221
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should not add notification if announcement ID is in READ_ANNOUNCEMENTS preference', async() => {
|
|
225
|
+
const announcementId = 'read-announcement';
|
|
226
|
+
|
|
227
|
+
mockGetters['notifications/item'].mockReturnValue(undefined); // No existing notification
|
|
228
|
+
mockGetters['prefs/get'].mockImplementation((key: string) => {
|
|
229
|
+
if (key === READ_ANNOUNCEMENTS) {
|
|
230
|
+
return `some-other-id,${ announcementId },another-one`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return '';
|
|
234
|
+
});
|
|
235
|
+
const announcements: Announcement[] = [
|
|
236
|
+
{
|
|
237
|
+
id: announcementId,
|
|
238
|
+
target: 'notification/announcement',
|
|
239
|
+
title: 'Test',
|
|
240
|
+
message: 'Msg'
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
await processAnnouncements(mockContext, announcements, VERSION_270);
|
|
245
|
+
|
|
246
|
+
expect(mockGetters['prefs/get']).toHaveBeenCalledWith(READ_ANNOUNCEMENTS);
|
|
247
|
+
expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('Not adding announcement with ID'));
|
|
248
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should add a new announcement notification with default level (announcement)', async() => {
|
|
252
|
+
const announcementId = 'new-announcement';
|
|
253
|
+
|
|
254
|
+
mockGetters['notifications/item'].mockReturnValue(undefined);
|
|
255
|
+
mockGetters['prefs/get'].mockReturnValue('');
|
|
256
|
+
const announcements: Announcement[] = [
|
|
257
|
+
{
|
|
258
|
+
id: announcementId,
|
|
259
|
+
target: 'notification/announcement',
|
|
260
|
+
title: 'New Announcement',
|
|
261
|
+
message: 'This is a new message.'
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
await processAnnouncements(mockContext, announcements, VERSION_270);
|
|
266
|
+
|
|
267
|
+
const expectedNotification: Notification = {
|
|
268
|
+
id: `${ ANNOUNCEMENT_PREFIX }${ announcementId }`,
|
|
269
|
+
level: NotificationLevel.Announcement,
|
|
270
|
+
title: 'New Announcement',
|
|
271
|
+
message: 'This is a new message.',
|
|
272
|
+
handlerName: DynamicContentAnnouncementHandlerName,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
|
276
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/add', expectedNotification);
|
|
277
|
+
expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining(`Adding announcement with ID ${ ANNOUNCEMENT_PREFIX }${ announcementId }`));
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should add a new info level notification', async() => {
|
|
281
|
+
const announcementId = 'new-info';
|
|
282
|
+
|
|
283
|
+
mockGetters['notifications/item'].mockReturnValue(undefined);
|
|
284
|
+
mockGetters['prefs/get'].mockReturnValue('');
|
|
285
|
+
const announcements: Announcement[] = [
|
|
286
|
+
{
|
|
287
|
+
id: announcementId,
|
|
288
|
+
target: 'notification/info',
|
|
289
|
+
title: 'Info Title',
|
|
290
|
+
message: 'Info Message'
|
|
291
|
+
},
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
await processAnnouncements(mockContext, announcements, VERSION_270);
|
|
295
|
+
|
|
296
|
+
const expectedNotification: Notification = {
|
|
297
|
+
id: `${ ANNOUNCEMENT_PREFIX }${ announcementId }`,
|
|
298
|
+
level: NotificationLevel.Info,
|
|
299
|
+
title: 'Info Title',
|
|
300
|
+
message: 'Info Message',
|
|
301
|
+
handlerName: DynamicContentAnnouncementHandlerName,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
|
305
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/add', expectedNotification);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should add a new warning level notification', async() => {
|
|
309
|
+
const announcementId = 'new-warning';
|
|
310
|
+
|
|
311
|
+
mockGetters['notifications/item'].mockReturnValue(undefined);
|
|
312
|
+
mockGetters['prefs/get'].mockReturnValue('');
|
|
313
|
+
const announcements: Announcement[] = [
|
|
314
|
+
{
|
|
315
|
+
id: announcementId,
|
|
316
|
+
target: 'notification/warning',
|
|
317
|
+
title: 'Warning Title',
|
|
318
|
+
message: 'Warning Message'
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
await processAnnouncements(mockContext, announcements, VERSION_270);
|
|
323
|
+
|
|
324
|
+
const expectedNotification: Notification = {
|
|
325
|
+
id: `${ ANNOUNCEMENT_PREFIX }${ announcementId }`,
|
|
326
|
+
level: NotificationLevel.Warning,
|
|
327
|
+
title: 'Warning Title',
|
|
328
|
+
message: 'Warning Message',
|
|
329
|
+
handlerName: DynamicContentAnnouncementHandlerName,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
|
333
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/add', expectedNotification);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// --- Call To Action (CTA) Handling ---
|
|
337
|
+
it('should add notification with primary CTA', async() => {
|
|
338
|
+
const announcementId = 'cta-primary';
|
|
339
|
+
|
|
340
|
+
mockGetters['notifications/item'].mockReturnValue(undefined);
|
|
341
|
+
mockGetters['prefs/get'].mockReturnValue('');
|
|
342
|
+
|
|
343
|
+
const announcements: Announcement[] = [
|
|
344
|
+
{
|
|
345
|
+
id: announcementId,
|
|
346
|
+
target: 'notification/announcement',
|
|
347
|
+
title: 'CTA Primary',
|
|
348
|
+
message: 'Message',
|
|
349
|
+
cta: { primary: { action: 'Click Me', link: '/some/path' } },
|
|
350
|
+
},
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
await processAnnouncements(mockContext, announcements, VERSION_270);
|
|
354
|
+
|
|
355
|
+
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
|
356
|
+
const notification = mockDispatch.mock.calls[0][1];
|
|
357
|
+
|
|
358
|
+
expect(notification.primaryAction).toStrictEqual({
|
|
359
|
+
label: 'Click Me',
|
|
360
|
+
target: '/some/path'
|
|
361
|
+
});
|
|
362
|
+
expect(notification.secondaryAction).toBeUndefined();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should add notification with secondary CTA', async() => {
|
|
366
|
+
const announcementId = 'cta-secondary';
|
|
367
|
+
|
|
368
|
+
mockGetters['notifications/item'].mockReturnValue(undefined);
|
|
369
|
+
mockGetters['prefs/get'].mockReturnValue('');
|
|
370
|
+
const announcements: Announcement[] = [
|
|
371
|
+
{
|
|
372
|
+
id: announcementId,
|
|
373
|
+
target: 'notification/announcement',
|
|
374
|
+
title: 'CTA Secondary',
|
|
375
|
+
message: 'Message',
|
|
376
|
+
cta: { secondary: { action: 'More Info', link: 'http://example.com' } },
|
|
377
|
+
},
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
await processAnnouncements(mockContext, announcements, VERSION_270);
|
|
381
|
+
|
|
382
|
+
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
|
383
|
+
const notification = mockDispatch.mock.calls[0][1];
|
|
384
|
+
|
|
385
|
+
expect(notification.secondaryAction).toStrictEqual({ label: 'More Info', target: 'http://example.com' });
|
|
386
|
+
expect(notification.primaryAction).toBeUndefined();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should add notification with both primary and secondary CTAs', async() => {
|
|
390
|
+
const announcementId = 'cta-both';
|
|
391
|
+
|
|
392
|
+
mockGetters['notifications/item'].mockReturnValue(undefined);
|
|
393
|
+
mockGetters['prefs/get'].mockReturnValue('');
|
|
394
|
+
const announcements: Announcement[] = [
|
|
395
|
+
{
|
|
396
|
+
id: announcementId,
|
|
397
|
+
target: 'notification/announcement',
|
|
398
|
+
title: 'CTA Both',
|
|
399
|
+
message: 'Message',
|
|
400
|
+
cta: {
|
|
401
|
+
primary: {
|
|
402
|
+
action: 'Primary',
|
|
403
|
+
link: '/primary'
|
|
404
|
+
},
|
|
405
|
+
secondary: {
|
|
406
|
+
action: 'Secondary',
|
|
407
|
+
link: '/secondary'
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
await processAnnouncements(mockContext, announcements, VERSION_270);
|
|
414
|
+
|
|
415
|
+
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
|
416
|
+
const notification = mockDispatch.mock.calls[0][1];
|
|
417
|
+
|
|
418
|
+
expect(notification.primaryAction).toStrictEqual({ label: 'Primary', target: '/primary' });
|
|
419
|
+
expect(notification.secondaryAction).toStrictEqual({ label: 'Secondary', target: '/secondary' });
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// --- Multiple Announcements ---
|
|
423
|
+
it('should process multiple announcements correctly, skipping invalid ones', async() => {
|
|
424
|
+
mockContext.isAdmin = true; // Ensure admin-only can be processed
|
|
425
|
+
mockGetters['notifications/item'].mockImplementation((id: string) => {
|
|
426
|
+
if (id === `${ ANNOUNCEMENT_PREFIX }existing-id`) {
|
|
427
|
+
return { id };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return undefined;
|
|
431
|
+
});
|
|
432
|
+
mockGetters['prefs/get'].mockImplementation((key: string) => {
|
|
433
|
+
if (key === READ_ANNOUNCEMENTS) {
|
|
434
|
+
return 'read-id';
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return '';
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const announcements: Announcement[] = [
|
|
441
|
+
{
|
|
442
|
+
id: 'valid-1',
|
|
443
|
+
target: 'notification/info',
|
|
444
|
+
title: 'Valid 1',
|
|
445
|
+
message: 'Msg 1',
|
|
446
|
+
audience: 'all'
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: 'existing-id',
|
|
450
|
+
target: 'notification/announcement',
|
|
451
|
+
title: 'Existing',
|
|
452
|
+
message: 'Msg Existing'
|
|
453
|
+
}, // Should be skipped
|
|
454
|
+
{
|
|
455
|
+
id: 'read-id',
|
|
456
|
+
target: 'notification/warning',
|
|
457
|
+
title: 'Read',
|
|
458
|
+
message: 'Msg Read'
|
|
459
|
+
}, // Should be skipped
|
|
460
|
+
{
|
|
461
|
+
id: 'valid-2',
|
|
462
|
+
target: 'notification/announcement',
|
|
463
|
+
title: 'Valid 2',
|
|
464
|
+
message: 'Msg 2',
|
|
465
|
+
audience: 'admin'
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
id: 'invalid-target',
|
|
469
|
+
target: 'unsupported/type',
|
|
470
|
+
title: 'Invalid',
|
|
471
|
+
message: 'Msg Invalid'
|
|
472
|
+
}, // Should log error
|
|
473
|
+
{
|
|
474
|
+
id: 'valid-3',
|
|
475
|
+
target: 'notification/info',
|
|
476
|
+
title: 'Valid 3',
|
|
477
|
+
message: 'Msg 3',
|
|
478
|
+
version: 'v1.0.0'
|
|
479
|
+
}, // semver.satisfies is mocked to true by default
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
await processAnnouncements(mockContext, announcements, VERSION_270);
|
|
483
|
+
|
|
484
|
+
// Expect 3 notifications to be added (valid-1, valid-2, valid-3)
|
|
485
|
+
expect(mockDispatch).toHaveBeenCalledTimes(3);
|
|
486
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/add', expect.objectContaining({ id: `${ ANNOUNCEMENT_PREFIX }valid-1` }));
|
|
487
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/add', expect.objectContaining({ id: `${ ANNOUNCEMENT_PREFIX }valid-2` }));
|
|
488
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/add', expect.objectContaining({ id: `${ ANNOUNCEMENT_PREFIX }valid-3` }));
|
|
489
|
+
|
|
490
|
+
// Expect errors for invalid target
|
|
491
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Announcement type unsupported/type is not supported');
|
|
492
|
+
|
|
493
|
+
// Expect info logs for skipped announcements
|
|
494
|
+
expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('Not adding announcement with ID '));
|
|
495
|
+
expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining(`${ ANNOUNCEMENT_PREFIX }existing-id`));
|
|
496
|
+
expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining(`${ ANNOUNCEMENT_PREFIX }read-id`));
|
|
497
|
+
});
|
|
498
|
+
});
|