@rancher/shell 3.0.6 → 3.0.8-rc.1
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/images/pl/dark/rancher-logo.svg +131 -44
- package/assets/images/pl/rancher-logo.svg +120 -44
- package/assets/images/vendor/githubapp.svg +13 -0
- package/assets/styles/base/_basic.scss +2 -2
- package/assets/styles/base/_color-classic.scss +51 -0
- package/assets/styles/base/_color.scss +3 -3
- package/assets/styles/base/_mixins.scss +1 -1
- package/assets/styles/base/_typography.scss +1 -1
- package/assets/styles/base/_variables-classic.scss +47 -0
- package/assets/styles/global/_button.scss +49 -17
- package/assets/styles/global/_form.scss +1 -1
- package/assets/styles/themes/_dark.scss +4 -0
- package/assets/styles/themes/_light.scss +3 -69
- package/assets/styles/themes/_modern.scss +194 -50
- package/assets/styles/vendor/vue-select.scss +1 -2
- package/assets/translations/en-us.yaml +124 -32
- package/assets/translations/zh-hans.yaml +0 -4
- package/components/ClusterIconMenu.vue +1 -1
- package/components/ClusterProviderIcon.vue +1 -1
- package/components/CodeMirror.vue +1 -1
- package/components/IconOrSvg.vue +40 -29
- package/components/Inactivity.vue +222 -106
- package/components/InstallHelmCharts.vue +2 -2
- package/components/ResourceDetail/index.vue +2 -1
- package/components/SortableTable/index.vue +17 -2
- package/components/SortableTable/sorting.js +3 -1
- package/components/Tabbed/index.vue +5 -5
- package/components/fleet/FleetConfigMapSelector.vue +117 -0
- package/components/fleet/FleetSecretSelector.vue +127 -0
- package/components/fleet/__tests__/FleetConfigMapSelector.test.ts +125 -0
- package/components/fleet/__tests__/FleetSecretSelector.test.ts +82 -0
- package/components/form/FileImageSelector.vue +13 -4
- package/components/form/FileSelector.vue +11 -2
- package/components/form/ResourceLabeledSelect.vue +1 -0
- package/components/form/ResourceTabs/index.vue +37 -18
- package/components/form/SecretSelector.vue +6 -2
- package/components/form/__tests__/ResourceLabeledSelect.test.ts +90 -0
- package/components/nav/Group.vue +29 -9
- package/components/nav/Header.vue +7 -8
- package/components/nav/NamespaceFilter.vue +1 -1
- package/components/nav/TopLevelMenu.helper.ts +47 -20
- package/components/nav/TopLevelMenu.vue +44 -14
- package/components/nav/Type.vue +0 -5
- package/components/nav/__tests__/TopLevelMenu.test.ts +2 -0
- package/config/pagination-table-headers.js +10 -2
- package/config/product/auth.js +1 -0
- package/config/product/explorer.js +4 -3
- package/config/query-params.js +1 -0
- package/config/settings.ts +8 -1
- package/config/table-headers.js +9 -0
- package/config/types.js +2 -0
- package/core/plugin.ts +18 -6
- package/core/types.ts +8 -0
- package/detail/provisioning.cattle.io.cluster.vue +1 -0
- package/dialog/AddonConfigConfirmationDialog.vue +45 -1
- package/dialog/InstallExtensionDialog.vue +71 -45
- package/dialog/UninstallExtensionDialog.vue +2 -1
- package/dialog/__tests__/InstallExtensionDialog.test.ts +111 -0
- package/edit/__tests__/fleet.cattle.io.helmop.test.ts +52 -11
- package/edit/auth/AuthProviderWarningBanners.vue +14 -1
- package/edit/auth/github-app-steps.vue +97 -0
- package/edit/auth/github-steps.vue +75 -0
- package/edit/auth/github.vue +94 -65
- package/edit/auth/oidc.vue +86 -16
- package/edit/fleet.cattle.io.helmop.vue +51 -2
- package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +15 -5
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +11 -9
- package/edit/provisioning.cattle.io.cluster/rke2.vue +56 -9
- package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +28 -2
- package/list/projectsecret.vue +1 -1
- package/machine-config/azure.vue +1 -1
- package/mixins/__tests__/chart.test.ts +1 -1
- package/mixins/chart.js +2 -2
- package/models/__tests__/chart.test.ts +17 -9
- package/models/__tests__/compliance.cattle.io.clusterscanprofile.spec.js +30 -0
- package/models/catalog.cattle.io.app.js +1 -1
- package/models/chart.js +3 -1
- package/models/compliance.cattle.io.clusterscanprofile.js +1 -1
- package/models/event.js +7 -0
- package/models/management.cattle.io.authconfig.js +1 -0
- package/models/provisioning.cattle.io.cluster.js +9 -0
- package/package.json +2 -2
- package/pages/auth/login.vue +5 -2
- package/pages/auth/verify.vue +1 -1
- package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +3 -2
- package/pages/c/_cluster/apps/charts/chart.vue +2 -2
- package/pages/c/_cluster/explorer/EventsTable.vue +92 -9
- package/pages/c/_cluster/explorer/tools/index.vue +3 -3
- package/pages/c/_cluster/settings/performance.vue +13 -26
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +159 -62
- package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +102 -0
- package/pages/c/_cluster/uiplugins/__tests__/{index.spec.ts → index.test.ts} +121 -55
- package/pages/c/_cluster/uiplugins/index.vue +110 -94
- package/pages/home.vue +313 -12
- package/plugins/__tests__/subscribe.events.test.ts +194 -0
- package/plugins/axios.js +2 -1
- package/plugins/dashboard-store/actions.js +4 -1
- package/plugins/dashboard-store/getters.js +1 -1
- package/plugins/dashboard-store/resource-class.js +20 -5
- package/plugins/steve/__tests__/subscribe.spec.ts +27 -24
- package/plugins/steve/index.js +18 -10
- package/plugins/steve/mutations.js +2 -2
- package/plugins/steve/resourceWatcher.js +2 -2
- package/plugins/steve/steve-pagination-utils.ts +12 -9
- package/plugins/steve/subscribe.js +113 -85
- package/plugins/subscribe-events.ts +211 -0
- package/rancher-components/BadgeState/BadgeState.vue +8 -6
- package/rancher-components/Banner/Banner.vue +2 -1
- package/rancher-components/Form/Checkbox/Checkbox.vue +3 -3
- package/rancher-components/Form/Radio/RadioButton.vue +3 -3
- package/scripts/extension/publish +1 -1
- package/store/auth.js +8 -3
- package/store/aws.js +8 -6
- package/store/features.js +1 -0
- package/store/index.js +21 -25
- package/store/prefs.js +6 -0
- package/types/extension-manager.ts +8 -1
- package/types/kube/kube-api.ts +2 -1
- package/types/rancher/index.d.ts +1 -0
- package/types/resources/settings.d.ts +52 -23
- package/types/shell/index.d.ts +412 -336
- package/types/store/subscribe-events.types.ts +70 -0
- package/types/store/subscribe.types.ts +6 -22
- package/utils/__tests__/cluster.test.ts +379 -1
- package/utils/cluster.js +157 -3
- package/utils/dynamic-content/__tests__/config.test.ts +187 -0
- package/utils/dynamic-content/__tests__/index.test.ts +390 -0
- package/utils/dynamic-content/__tests__/info.test.ts +263 -0
- package/utils/dynamic-content/__tests__/new-release.test.ts +216 -0
- package/utils/dynamic-content/__tests__/support-notice.test.ts +262 -0
- package/utils/dynamic-content/__tests__/util.test.ts +235 -0
- package/utils/dynamic-content/config.ts +55 -0
- package/utils/dynamic-content/index.ts +273 -0
- package/utils/dynamic-content/info.ts +219 -0
- package/utils/dynamic-content/new-release.ts +126 -0
- package/utils/dynamic-content/support-notice.ts +169 -0
- package/utils/dynamic-content/types.d.ts +101 -0
- package/utils/dynamic-content/util.ts +122 -0
- package/utils/inactivity.ts +104 -0
- package/utils/pagination-utils.ts +105 -31
- package/utils/pagination-wrapper.ts +6 -8
- package/utils/release-notes.ts +1 -1
- package/utils/sort.js +5 -0
- package/utils/unit-tests/pagination-utils.spec.ts +283 -0
- package/utils/validators/formRules/__tests__/index.test.ts +7 -0
- package/utils/validators/formRules/index.ts +2 -2
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { processSupportNotices } from '../support-notice';
|
|
2
|
+
import { NotificationLevel } from '@shell/types/notifications';
|
|
3
|
+
import { READ_SUPPORT_NOTICE, READ_UPCOMING_SUPPORT_NOTICE } from '@shell/store/prefs';
|
|
4
|
+
import { Context, VersionInfo, SupportInfo } from '../types';
|
|
5
|
+
import * as util from '../util'; // To mock removeMatchingNotifications
|
|
6
|
+
import semver from 'semver';
|
|
7
|
+
import day from 'dayjs';
|
|
8
|
+
|
|
9
|
+
describe('processSupportNotices', () => {
|
|
10
|
+
let mockContext: Context;
|
|
11
|
+
let mockDispatch: jest.Mock;
|
|
12
|
+
let mockGetters: any;
|
|
13
|
+
let mockLogger: any;
|
|
14
|
+
let mockRemoveMatchingNotifications: jest.SpyInstance;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockDispatch = jest.fn();
|
|
18
|
+
mockGetters = {
|
|
19
|
+
'prefs/get': jest.fn(),
|
|
20
|
+
'i18n/t': jest.fn((key: string, params?: any) => {
|
|
21
|
+
const { version, days } = params || {};
|
|
22
|
+
|
|
23
|
+
if (key === 'dynamicContent.eol.title') return `EOL Title for ${ version }`;
|
|
24
|
+
if (key === 'dynamicContent.eol.message') return `EOL Message for ${ version }`;
|
|
25
|
+
if (key === 'dynamicContent.eom.title') return `EOM Title for ${ version }`;
|
|
26
|
+
if (key === 'dynamicContent.eom.message') return `EOM Message for ${ version }`;
|
|
27
|
+
if (key === 'dynamicContent.upcomingEol.title') return `Upcoming EOL Title for ${ version } in ${ days } days`;
|
|
28
|
+
if (key === 'dynamicContent.upcomingEol.message') return `Upcoming EOL Message for ${ version } in ${ days } days`;
|
|
29
|
+
if (key === 'dynamicContent.upcomingEom.title') return `Upcoming EOM Title for ${ version } in ${ days } days`;
|
|
30
|
+
if (key === 'dynamicContent.upcomingEom.message') return `Upcoming EOM Message for ${ version } in ${ days } days`;
|
|
31
|
+
|
|
32
|
+
return key;
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
mockLogger = {
|
|
36
|
+
info: jest.fn(),
|
|
37
|
+
debug: jest.fn(),
|
|
38
|
+
error: jest.fn(),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
mockContext = {
|
|
42
|
+
dispatch: mockDispatch,
|
|
43
|
+
getters: mockGetters,
|
|
44
|
+
axios: {},
|
|
45
|
+
logger: mockLogger,
|
|
46
|
+
isAdmin: true,
|
|
47
|
+
config: {
|
|
48
|
+
enabled: true,
|
|
49
|
+
debug: false,
|
|
50
|
+
log: false,
|
|
51
|
+
endpoint: '',
|
|
52
|
+
prime: false,
|
|
53
|
+
distribution: 'community',
|
|
54
|
+
},
|
|
55
|
+
settings: {
|
|
56
|
+
releaseNotesUrl: '',
|
|
57
|
+
suseExtensions: [],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Mock the utility function. Default: notification does not exist, so add it.
|
|
62
|
+
mockRemoveMatchingNotifications = jest.spyOn(util, 'removeMatchingNotifications')
|
|
63
|
+
.mockResolvedValue(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
jest.restoreAllMocks();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return early if statusInfo is null/undefined', async() => {
|
|
71
|
+
const versionInfo: VersionInfo = { version: semver.coerce('2.12.0')!, isPrime: false };
|
|
72
|
+
|
|
73
|
+
await processSupportNotices(mockContext, null as any, versionInfo);
|
|
74
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return early if versionInfo is null/undefined or version is missing', async() => {
|
|
78
|
+
const statusInfo: SupportInfo = { status: { eol: '<= 2.11', eom: '<= 2.12' }, upcoming: {} as any };
|
|
79
|
+
|
|
80
|
+
await processSupportNotices(mockContext, statusInfo, null as any);
|
|
81
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
82
|
+
await processSupportNotices(mockContext, statusInfo, { version: null, isPrime: false });
|
|
83
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should not add notification if no support status matches', async() => {
|
|
87
|
+
const versionInfo: VersionInfo = { version: semver.coerce('2.13.0')!, isPrime: false };
|
|
88
|
+
const statusInfo: SupportInfo = {
|
|
89
|
+
status: { eol: '<= 2.11.x', eom: '<= 2.12.x' },
|
|
90
|
+
upcoming: {
|
|
91
|
+
eom: { version: '= 2.13.x', date: day().add(40, 'day').toDate() },
|
|
92
|
+
eol: { version: '= 2.13.x', date: day().add(40, 'day').toDate() },
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
await processSupportNotices(mockContext, statusInfo, versionInfo);
|
|
97
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
98
|
+
expect(mockLogger.info).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should add EOL notification if version is EOL', async() => {
|
|
102
|
+
const versionInfo: VersionInfo = { version: semver.coerce('2.11.5')!, isPrime: false };
|
|
103
|
+
const statusInfo: SupportInfo = {
|
|
104
|
+
status: { eol: '<= 2.11.x', eom: '<= 2.12.x' },
|
|
105
|
+
upcoming: {} as any
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
await processSupportNotices(mockContext, statusInfo, versionInfo);
|
|
109
|
+
|
|
110
|
+
expect(mockLogger.info).toHaveBeenCalledWith('This version (2.11.5) is End of Life');
|
|
111
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/add', expect.objectContaining({
|
|
112
|
+
id: 'support-notice-eol-2.11',
|
|
113
|
+
level: NotificationLevel.Warning,
|
|
114
|
+
title: 'EOL Title for 2.11',
|
|
115
|
+
}));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should add EOM notification if version is EOM but not EOL', async() => {
|
|
119
|
+
const versionInfo: VersionInfo = { version: semver.coerce('2.12.3')!, isPrime: false };
|
|
120
|
+
const statusInfo: SupportInfo = {
|
|
121
|
+
status: { eol: '<= 2.11.x', eom: '<= 2.12.x' },
|
|
122
|
+
upcoming: {} as any
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
await processSupportNotices(mockContext, statusInfo, versionInfo);
|
|
126
|
+
|
|
127
|
+
expect(mockLogger.info).toHaveBeenCalledWith('This version (2.12.3) is End of Maintenance');
|
|
128
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/add', expect.objectContaining({
|
|
129
|
+
id: 'support-notice-eom-2.12',
|
|
130
|
+
level: NotificationLevel.Warning,
|
|
131
|
+
title: 'EOM Title for 2.12',
|
|
132
|
+
}));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should add upcoming EOL notification', async() => {
|
|
136
|
+
const versionInfo: VersionInfo = { version: semver.coerce('2.12.0')!, isPrime: false };
|
|
137
|
+
const statusInfo: SupportInfo = {
|
|
138
|
+
status: { eol: '<= 2.11.x', eom: '<= 2.11.x' },
|
|
139
|
+
upcoming: {
|
|
140
|
+
eol: { version: '= 2.12.x', date: day().add(15, 'day').toDate() },
|
|
141
|
+
eom: {} as any,
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
await processSupportNotices(mockContext, statusInfo, versionInfo);
|
|
146
|
+
|
|
147
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/add', expect.objectContaining({
|
|
148
|
+
id: 'upcoming-support-notice-eol-2.12',
|
|
149
|
+
level: NotificationLevel.Warning,
|
|
150
|
+
title: 'Upcoming EOL Title for 2.12 in 15 days',
|
|
151
|
+
}));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should add upcoming EOM notification', async() => {
|
|
155
|
+
const versionInfo: VersionInfo = { version: semver.coerce('2.13.0')!, isPrime: false };
|
|
156
|
+
const statusInfo: SupportInfo = {
|
|
157
|
+
status: { eol: '<= 2.12.x', eom: '<= 2.12.x' },
|
|
158
|
+
upcoming: {
|
|
159
|
+
eom: {
|
|
160
|
+
version: '= 2.13.x', date: day().add(20, 'day').toDate(), noticeDays: 25
|
|
161
|
+
},
|
|
162
|
+
eol: {} as any,
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
await processSupportNotices(mockContext, statusInfo, versionInfo);
|
|
167
|
+
|
|
168
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/add', expect.objectContaining({
|
|
169
|
+
id: 'upcoming-support-notice-eom-2.13',
|
|
170
|
+
level: NotificationLevel.Warning,
|
|
171
|
+
title: 'Upcoming EOM Title for 2.13 in 20 days',
|
|
172
|
+
}));
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should not add notification if removeMatchingNotifications indicates it exists', async() => {
|
|
176
|
+
mockRemoveMatchingNotifications.mockResolvedValue(true); // Simulate notification already exists
|
|
177
|
+
const versionInfo: VersionInfo = { version: semver.coerce('2.11.5')!, isPrime: false };
|
|
178
|
+
const statusInfo: SupportInfo = {
|
|
179
|
+
status: { eol: '<= 2.11.x', eom: '<= 2.12.x' },
|
|
180
|
+
upcoming: {} as any
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
await processSupportNotices(mockContext, statusInfo, versionInfo);
|
|
184
|
+
|
|
185
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('user preferences', () => {
|
|
189
|
+
it('should not add EOL notification if it was already read', async() => {
|
|
190
|
+
mockGetters['prefs/get'].mockImplementation((key: string) => {
|
|
191
|
+
if (key === READ_SUPPORT_NOTICE) return 'eol-2.11';
|
|
192
|
+
|
|
193
|
+
return '';
|
|
194
|
+
});
|
|
195
|
+
const versionInfo: VersionInfo = { version: semver.coerce('2.11.5')!, isPrime: false };
|
|
196
|
+
const statusInfo: SupportInfo = {
|
|
197
|
+
status: { eol: '<= 2.11.x', eom: '<= 2.12.x' },
|
|
198
|
+
upcoming: {} as any
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
await processSupportNotices(mockContext, statusInfo, versionInfo);
|
|
202
|
+
|
|
203
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should not add EOM notification if it was already read', async() => {
|
|
207
|
+
mockGetters['prefs/get'].mockImplementation((key: string) => {
|
|
208
|
+
if (key === READ_SUPPORT_NOTICE) return 'eom-2.12';
|
|
209
|
+
|
|
210
|
+
return '';
|
|
211
|
+
});
|
|
212
|
+
const versionInfo: VersionInfo = { version: semver.coerce('2.12.3')!, isPrime: false };
|
|
213
|
+
const statusInfo: SupportInfo = {
|
|
214
|
+
status: { eol: '<= 2.11.x', eom: '<= 2.12.x' },
|
|
215
|
+
upcoming: {} as any
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
await processSupportNotices(mockContext, statusInfo, versionInfo);
|
|
219
|
+
|
|
220
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should not add upcoming EOL notification if it was already read', async() => {
|
|
224
|
+
mockGetters['prefs/get'].mockImplementation((key: string) => {
|
|
225
|
+
if (key === READ_UPCOMING_SUPPORT_NOTICE) return 'eol-2.12';
|
|
226
|
+
|
|
227
|
+
return '';
|
|
228
|
+
});
|
|
229
|
+
const versionInfo: VersionInfo = { version: semver.coerce('2.12.0')!, isPrime: false };
|
|
230
|
+
const statusInfo: SupportInfo = {
|
|
231
|
+
status: { eol: '<= 2.11.x', eom: '<= 2.11.x' },
|
|
232
|
+
upcoming: { eol: { version: '= 2.12.x', date: day().add(145, 'day').toDate() }, eom: {} as any }
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
await processSupportNotices(mockContext, statusInfo, versionInfo);
|
|
236
|
+
|
|
237
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should not add upcoming EOM notification if it was already read', async() => {
|
|
241
|
+
mockGetters['prefs/get'].mockImplementation((key: string) => {
|
|
242
|
+
if (key === READ_UPCOMING_SUPPORT_NOTICE) return 'eom-2.13';
|
|
243
|
+
|
|
244
|
+
return '';
|
|
245
|
+
});
|
|
246
|
+
const versionInfo: VersionInfo = { version: semver.coerce('2.13.0')!, isPrime: false };
|
|
247
|
+
const statusInfo: SupportInfo = {
|
|
248
|
+
status: { eol: '<= 2.12.x', eom: '<= 2.12.x' },
|
|
249
|
+
upcoming: {
|
|
250
|
+
eom: {
|
|
251
|
+
version: '= 2.13.x', date: day().add(20, 'day').toDate(), noticeDays: 25
|
|
252
|
+
},
|
|
253
|
+
eol: {} as any
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
await processSupportNotices(mockContext, statusInfo, versionInfo);
|
|
258
|
+
|
|
259
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { removeMatchingNotifications, createLogger, LOCAL_STORAGE_CONTENT_DEBUG_LOG } from '../util';
|
|
2
|
+
import { Context, Configuration } from '../types';
|
|
3
|
+
|
|
4
|
+
describe('util.ts', () => {
|
|
5
|
+
describe('removeMatchingNotifications', () => {
|
|
6
|
+
let mockContext: Context;
|
|
7
|
+
let mockDispatch: jest.Mock;
|
|
8
|
+
let mockGetters: any;
|
|
9
|
+
let mockLogger: any;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockDispatch = jest.fn();
|
|
13
|
+
mockGetters = { 'notifications/all': [] };
|
|
14
|
+
mockLogger = { debug: jest.fn() };
|
|
15
|
+
mockContext = {
|
|
16
|
+
dispatch: mockDispatch,
|
|
17
|
+
getters: mockGetters,
|
|
18
|
+
logger: mockLogger,
|
|
19
|
+
// The following properties are not used by this function but are required by the type
|
|
20
|
+
axios: {},
|
|
21
|
+
isAdmin: true,
|
|
22
|
+
config: {} as any,
|
|
23
|
+
settings: {} as any,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return false and not remove anything if no notifications exist', async() => {
|
|
28
|
+
const found = await removeMatchingNotifications(mockContext, 'prefix-', 'current');
|
|
29
|
+
|
|
30
|
+
expect(found).toBe(false);
|
|
31
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return false and not remove anything if no notifications match the prefix', async() => {
|
|
35
|
+
mockGetters['notifications/all'] = [
|
|
36
|
+
{ id: 'other-1' },
|
|
37
|
+
{ id: 'other-2' },
|
|
38
|
+
];
|
|
39
|
+
const found = await removeMatchingNotifications(mockContext, 'prefix-', 'current');
|
|
40
|
+
|
|
41
|
+
expect(found).toBe(false);
|
|
42
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return true and not remove anything if the current notification is the only one matching', async() => {
|
|
46
|
+
mockGetters['notifications/all'] = [
|
|
47
|
+
{ id: 'prefix-current' },
|
|
48
|
+
{ id: 'other-1' },
|
|
49
|
+
];
|
|
50
|
+
const found = await removeMatchingNotifications(mockContext, 'prefix-', 'current');
|
|
51
|
+
|
|
52
|
+
expect(found).toBe(true);
|
|
53
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return false and remove a notification that matches the prefix but not the currentId', async() => {
|
|
57
|
+
mockGetters['notifications/all'] = [
|
|
58
|
+
{ id: 'prefix-old' },
|
|
59
|
+
{ id: 'other-1' },
|
|
60
|
+
];
|
|
61
|
+
const found = await removeMatchingNotifications(mockContext, 'prefix-', 'current');
|
|
62
|
+
|
|
63
|
+
expect(found).toBe(false);
|
|
64
|
+
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
|
65
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/remove', 'prefix-old');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return true and remove old notifications when the current one also exists', async() => {
|
|
69
|
+
mockGetters['notifications/all'] = [
|
|
70
|
+
{ id: 'prefix-old-1' },
|
|
71
|
+
{ id: 'prefix-current' },
|
|
72
|
+
{ id: 'prefix-old-2' },
|
|
73
|
+
{ id: 'other-1' },
|
|
74
|
+
];
|
|
75
|
+
const found = await removeMatchingNotifications(mockContext, 'prefix-', 'current');
|
|
76
|
+
|
|
77
|
+
expect(found).toBe(true);
|
|
78
|
+
expect(mockDispatch).toHaveBeenCalledTimes(2);
|
|
79
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/remove', 'prefix-old-1');
|
|
80
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/remove', 'prefix-old-2');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('createLogger / log', () => {
|
|
85
|
+
let mockLocalStorage: { [key: string]: string };
|
|
86
|
+
let consoleErrorSpy: jest.SpyInstance;
|
|
87
|
+
let consoleInfoSpy: jest.SpyInstance;
|
|
88
|
+
let consoleDebugSpy: jest.SpyInstance;
|
|
89
|
+
let dispatchEventSpy: jest.SpyInstance;
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
// Mock localStorage
|
|
93
|
+
mockLocalStorage = {};
|
|
94
|
+
jest.spyOn(Storage.prototype, 'getItem').mockImplementation((key) => mockLocalStorage[key] || null);
|
|
95
|
+
jest.spyOn(Storage.prototype, 'setItem').mockImplementation((key, value) => {
|
|
96
|
+
mockLocalStorage[key] = value;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Mock console
|
|
100
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
101
|
+
consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => {});
|
|
102
|
+
consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
|
|
103
|
+
|
|
104
|
+
// Mock dispatchEvent
|
|
105
|
+
dispatchEventSpy = jest.spyOn(window, 'dispatchEvent').mockImplementation(() => true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
jest.restoreAllMocks();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should always log errors to console, but only to localStorage if config.log is true', () => {
|
|
113
|
+
const config: Configuration = {
|
|
114
|
+
enabled: true, debug: false, log: false, endpoint: '', prime: false, distribution: 'community'
|
|
115
|
+
};
|
|
116
|
+
const logger = createLogger(config);
|
|
117
|
+
|
|
118
|
+
logger.error('test error');
|
|
119
|
+
|
|
120
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('test error');
|
|
121
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeUndefined();
|
|
122
|
+
|
|
123
|
+
// Test with arg
|
|
124
|
+
logger.error('test error', 'with arg');
|
|
125
|
+
|
|
126
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('test error', 'with arg');
|
|
127
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeUndefined();
|
|
128
|
+
|
|
129
|
+
config.log = true;
|
|
130
|
+
|
|
131
|
+
logger.error('test error with log');
|
|
132
|
+
|
|
133
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('test error with log');
|
|
134
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeDefined();
|
|
135
|
+
expect(JSON.parse(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG])[0].message).toBe('test error with log');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should log info to console and localStorage only if config.log is true', () => {
|
|
139
|
+
const config: Configuration = {
|
|
140
|
+
enabled: true, debug: false, log: false, endpoint: '', prime: false, distribution: 'community'
|
|
141
|
+
};
|
|
142
|
+
const logger = createLogger(config);
|
|
143
|
+
|
|
144
|
+
logger.info('test info');
|
|
145
|
+
|
|
146
|
+
expect(consoleInfoSpy).not.toHaveBeenCalled();
|
|
147
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeUndefined();
|
|
148
|
+
|
|
149
|
+
config.log = true;
|
|
150
|
+
logger.info('test info with log');
|
|
151
|
+
|
|
152
|
+
expect(consoleInfoSpy).toHaveBeenCalledWith('test info with log');
|
|
153
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeDefined();
|
|
154
|
+
expect(JSON.parse(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG])[0].message).toBe('test info with log');
|
|
155
|
+
|
|
156
|
+
// Test with arg
|
|
157
|
+
logger.info('test info', 'with arg');
|
|
158
|
+
|
|
159
|
+
expect(consoleInfoSpy).toHaveBeenCalledWith('test info', 'with arg');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should log debug to console only if config.debug is true, and localStorage if config.log is true', () => {
|
|
163
|
+
const config: Configuration = {
|
|
164
|
+
enabled: true, debug: false, log: false, endpoint: '', prime: false, distribution: 'community'
|
|
165
|
+
};
|
|
166
|
+
const logger = createLogger(config);
|
|
167
|
+
|
|
168
|
+
logger.debug('test debug');
|
|
169
|
+
expect(consoleDebugSpy).not.toHaveBeenCalled();
|
|
170
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeUndefined();
|
|
171
|
+
|
|
172
|
+
config.log = true;
|
|
173
|
+
logger.debug('test debug with log');
|
|
174
|
+
expect(consoleDebugSpy).not.toHaveBeenCalled();
|
|
175
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeDefined();
|
|
176
|
+
expect(JSON.parse(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG])[0].message).toBe('test debug with log');
|
|
177
|
+
|
|
178
|
+
config.debug = true;
|
|
179
|
+
logger.debug('test debug with debug and log');
|
|
180
|
+
expect(consoleDebugSpy).toHaveBeenCalledWith('test debug with debug and log');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should dispatch a custom event when logging to localStorage', () => {
|
|
184
|
+
const config: Configuration = {
|
|
185
|
+
enabled: true, debug: false, log: true, endpoint: '', prime: false, distribution: 'community'
|
|
186
|
+
};
|
|
187
|
+
const logger = createLogger(config);
|
|
188
|
+
|
|
189
|
+
logger.info('test event');
|
|
190
|
+
|
|
191
|
+
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
|
|
192
|
+
const event = dispatchEventSpy.mock.calls[0][0] as CustomEvent;
|
|
193
|
+
|
|
194
|
+
expect(event.type).toBe('dynamicContentLog');
|
|
195
|
+
expect(event.detail.message).toBe('test event');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should limit the number of log messages in localStorage', () => {
|
|
199
|
+
const config: Configuration = {
|
|
200
|
+
enabled: true, debug: false, log: true, endpoint: '', prime: false, distribution: 'community'
|
|
201
|
+
};
|
|
202
|
+
const logger = createLogger(config);
|
|
203
|
+
|
|
204
|
+
// MAX_LOG_MESSAGES is 50
|
|
205
|
+
for (let i = 0; i < 60; i++) {
|
|
206
|
+
logger.info(`message ${ i }`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const logs = JSON.parse(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]);
|
|
210
|
+
|
|
211
|
+
expect(logs).toHaveLength(50);
|
|
212
|
+
expect(logs[0].message).toBe('message 59'); // Most recent
|
|
213
|
+
expect(logs[49].message).toBe('message 10'); // Oldest
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should not throw if localStorage is corrupted', () => {
|
|
217
|
+
const config: Configuration = {
|
|
218
|
+
enabled: true, debug: false, log: true, endpoint: '', prime: false, distribution: 'community'
|
|
219
|
+
};
|
|
220
|
+
const logger = createLogger(config);
|
|
221
|
+
|
|
222
|
+
mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG] = 'this is not valid json';
|
|
223
|
+
|
|
224
|
+
expect(() => {
|
|
225
|
+
logger.info('test message');
|
|
226
|
+
}).not.toThrow();
|
|
227
|
+
|
|
228
|
+
// It should have overwritten the bad data
|
|
229
|
+
const logs = JSON.parse(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]);
|
|
230
|
+
|
|
231
|
+
expect(logs).toHaveLength(1);
|
|
232
|
+
expect(logs[0].message).toBe('test message');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { SETTING } from '@shell/config/settings';
|
|
2
|
+
import { isRancherPrime } from '@shell/config/version';
|
|
3
|
+
import { Configuration, Distribution } from './types';
|
|
4
|
+
import { MANAGEMENT } from '@shell/config/types';
|
|
5
|
+
|
|
6
|
+
// Default endpoint ($dist is either 'community' or 'prime')
|
|
7
|
+
const DEFAULT_ENDPOINT = 'https://updates.rancher.io/rancher/$dist/updates';
|
|
8
|
+
|
|
9
|
+
// We only support retrieving content from secure endpoints
|
|
10
|
+
const HTTPS_PREFIX = 'https://';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get configuration data based on the distribution and Rancher settings
|
|
14
|
+
*
|
|
15
|
+
* @param getters Store getters to access the store
|
|
16
|
+
* @returns Dynamic Content configuration
|
|
17
|
+
*/
|
|
18
|
+
export function getConfig(getters: any): Configuration {
|
|
19
|
+
const prime = isRancherPrime();
|
|
20
|
+
const distribution: Distribution = prime ? 'prime' : 'community';
|
|
21
|
+
|
|
22
|
+
// Default configuration
|
|
23
|
+
const config: Configuration = {
|
|
24
|
+
enabled: true,
|
|
25
|
+
debug: false,
|
|
26
|
+
log: false,
|
|
27
|
+
endpoint: DEFAULT_ENDPOINT,
|
|
28
|
+
prime,
|
|
29
|
+
distribution,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Update 'enabled' and 'endpoint' from Rancher settings, if applicable
|
|
33
|
+
try {
|
|
34
|
+
const enabledSetting = getters['management/byId'](MANAGEMENT.SETTING, SETTING.DYNAMIC_CONTENT_ENABLED);
|
|
35
|
+
|
|
36
|
+
if (enabledSetting?.value) {
|
|
37
|
+
// Any value other than 'false' means enabled (can't disable on Prime)
|
|
38
|
+
config.enabled = config.prime ? enabledSetting.value !== 'false' : true;
|
|
39
|
+
config.debug = enabledSetting.value === 'debug';
|
|
40
|
+
config.log = enabledSetting.value === 'log' || config.debug;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Can only override the url when Prime
|
|
44
|
+
const urlSetting = getters['management/byId'](MANAGEMENT.SETTING, SETTING.DYNAMIC_CONTENT_ENDPOINT);
|
|
45
|
+
|
|
46
|
+
// Are we Prime, do we have a URL and does the URL start with the https prefix? If so, use it
|
|
47
|
+
if (prime && urlSetting?.value && urlSetting.value.startsWith(HTTPS_PREFIX)) {
|
|
48
|
+
config.endpoint = urlSetting.value;
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.error('Error reading dynamic content settings', e); // eslint-disable-line no-console
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return config;
|
|
55
|
+
}
|