@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.
Files changed (146) hide show
  1. package/assets/images/pl/dark/rancher-logo.svg +131 -44
  2. package/assets/images/pl/rancher-logo.svg +120 -44
  3. package/assets/images/vendor/githubapp.svg +13 -0
  4. package/assets/styles/base/_basic.scss +2 -2
  5. package/assets/styles/base/_color-classic.scss +51 -0
  6. package/assets/styles/base/_color.scss +3 -3
  7. package/assets/styles/base/_mixins.scss +1 -1
  8. package/assets/styles/base/_typography.scss +1 -1
  9. package/assets/styles/base/_variables-classic.scss +47 -0
  10. package/assets/styles/global/_button.scss +49 -17
  11. package/assets/styles/global/_form.scss +1 -1
  12. package/assets/styles/themes/_dark.scss +4 -0
  13. package/assets/styles/themes/_light.scss +3 -69
  14. package/assets/styles/themes/_modern.scss +194 -50
  15. package/assets/styles/vendor/vue-select.scss +1 -2
  16. package/assets/translations/en-us.yaml +124 -32
  17. package/assets/translations/zh-hans.yaml +0 -4
  18. package/components/ClusterIconMenu.vue +1 -1
  19. package/components/ClusterProviderIcon.vue +1 -1
  20. package/components/CodeMirror.vue +1 -1
  21. package/components/IconOrSvg.vue +40 -29
  22. package/components/Inactivity.vue +222 -106
  23. package/components/InstallHelmCharts.vue +2 -2
  24. package/components/ResourceDetail/index.vue +2 -1
  25. package/components/SortableTable/index.vue +17 -2
  26. package/components/SortableTable/sorting.js +3 -1
  27. package/components/Tabbed/index.vue +5 -5
  28. package/components/fleet/FleetConfigMapSelector.vue +117 -0
  29. package/components/fleet/FleetSecretSelector.vue +127 -0
  30. package/components/fleet/__tests__/FleetConfigMapSelector.test.ts +125 -0
  31. package/components/fleet/__tests__/FleetSecretSelector.test.ts +82 -0
  32. package/components/form/FileImageSelector.vue +13 -4
  33. package/components/form/FileSelector.vue +11 -2
  34. package/components/form/ResourceLabeledSelect.vue +1 -0
  35. package/components/form/ResourceTabs/index.vue +37 -18
  36. package/components/form/SecretSelector.vue +6 -2
  37. package/components/form/__tests__/ResourceLabeledSelect.test.ts +90 -0
  38. package/components/nav/Group.vue +29 -9
  39. package/components/nav/Header.vue +7 -8
  40. package/components/nav/NamespaceFilter.vue +1 -1
  41. package/components/nav/TopLevelMenu.helper.ts +47 -20
  42. package/components/nav/TopLevelMenu.vue +44 -14
  43. package/components/nav/Type.vue +0 -5
  44. package/components/nav/__tests__/TopLevelMenu.test.ts +2 -0
  45. package/config/pagination-table-headers.js +10 -2
  46. package/config/product/auth.js +1 -0
  47. package/config/product/explorer.js +4 -3
  48. package/config/query-params.js +1 -0
  49. package/config/settings.ts +8 -1
  50. package/config/table-headers.js +9 -0
  51. package/config/types.js +2 -0
  52. package/core/plugin.ts +18 -6
  53. package/core/types.ts +8 -0
  54. package/detail/provisioning.cattle.io.cluster.vue +1 -0
  55. package/dialog/AddonConfigConfirmationDialog.vue +45 -1
  56. package/dialog/InstallExtensionDialog.vue +71 -45
  57. package/dialog/UninstallExtensionDialog.vue +2 -1
  58. package/dialog/__tests__/InstallExtensionDialog.test.ts +111 -0
  59. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +52 -11
  60. package/edit/auth/AuthProviderWarningBanners.vue +14 -1
  61. package/edit/auth/github-app-steps.vue +97 -0
  62. package/edit/auth/github-steps.vue +75 -0
  63. package/edit/auth/github.vue +94 -65
  64. package/edit/auth/oidc.vue +86 -16
  65. package/edit/fleet.cattle.io.helmop.vue +51 -2
  66. package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +15 -5
  67. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +11 -9
  68. package/edit/provisioning.cattle.io.cluster/rke2.vue +56 -9
  69. package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +28 -2
  70. package/list/projectsecret.vue +1 -1
  71. package/machine-config/azure.vue +1 -1
  72. package/mixins/__tests__/chart.test.ts +1 -1
  73. package/mixins/chart.js +2 -2
  74. package/models/__tests__/chart.test.ts +17 -9
  75. package/models/__tests__/compliance.cattle.io.clusterscanprofile.spec.js +30 -0
  76. package/models/catalog.cattle.io.app.js +1 -1
  77. package/models/chart.js +3 -1
  78. package/models/compliance.cattle.io.clusterscanprofile.js +1 -1
  79. package/models/event.js +7 -0
  80. package/models/management.cattle.io.authconfig.js +1 -0
  81. package/models/provisioning.cattle.io.cluster.js +9 -0
  82. package/package.json +2 -2
  83. package/pages/auth/login.vue +5 -2
  84. package/pages/auth/verify.vue +1 -1
  85. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +3 -2
  86. package/pages/c/_cluster/apps/charts/chart.vue +2 -2
  87. package/pages/c/_cluster/explorer/EventsTable.vue +92 -9
  88. package/pages/c/_cluster/explorer/tools/index.vue +3 -3
  89. package/pages/c/_cluster/settings/performance.vue +13 -26
  90. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +159 -62
  91. package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +102 -0
  92. package/pages/c/_cluster/uiplugins/__tests__/{index.spec.ts → index.test.ts} +121 -55
  93. package/pages/c/_cluster/uiplugins/index.vue +110 -94
  94. package/pages/home.vue +313 -12
  95. package/plugins/__tests__/subscribe.events.test.ts +194 -0
  96. package/plugins/axios.js +2 -1
  97. package/plugins/dashboard-store/actions.js +4 -1
  98. package/plugins/dashboard-store/getters.js +1 -1
  99. package/plugins/dashboard-store/resource-class.js +20 -5
  100. package/plugins/steve/__tests__/subscribe.spec.ts +27 -24
  101. package/plugins/steve/index.js +18 -10
  102. package/plugins/steve/mutations.js +2 -2
  103. package/plugins/steve/resourceWatcher.js +2 -2
  104. package/plugins/steve/steve-pagination-utils.ts +12 -9
  105. package/plugins/steve/subscribe.js +113 -85
  106. package/plugins/subscribe-events.ts +211 -0
  107. package/rancher-components/BadgeState/BadgeState.vue +8 -6
  108. package/rancher-components/Banner/Banner.vue +2 -1
  109. package/rancher-components/Form/Checkbox/Checkbox.vue +3 -3
  110. package/rancher-components/Form/Radio/RadioButton.vue +3 -3
  111. package/scripts/extension/publish +1 -1
  112. package/store/auth.js +8 -3
  113. package/store/aws.js +8 -6
  114. package/store/features.js +1 -0
  115. package/store/index.js +21 -25
  116. package/store/prefs.js +6 -0
  117. package/types/extension-manager.ts +8 -1
  118. package/types/kube/kube-api.ts +2 -1
  119. package/types/rancher/index.d.ts +1 -0
  120. package/types/resources/settings.d.ts +52 -23
  121. package/types/shell/index.d.ts +412 -336
  122. package/types/store/subscribe-events.types.ts +70 -0
  123. package/types/store/subscribe.types.ts +6 -22
  124. package/utils/__tests__/cluster.test.ts +379 -1
  125. package/utils/cluster.js +157 -3
  126. package/utils/dynamic-content/__tests__/config.test.ts +187 -0
  127. package/utils/dynamic-content/__tests__/index.test.ts +390 -0
  128. package/utils/dynamic-content/__tests__/info.test.ts +263 -0
  129. package/utils/dynamic-content/__tests__/new-release.test.ts +216 -0
  130. package/utils/dynamic-content/__tests__/support-notice.test.ts +262 -0
  131. package/utils/dynamic-content/__tests__/util.test.ts +235 -0
  132. package/utils/dynamic-content/config.ts +55 -0
  133. package/utils/dynamic-content/index.ts +273 -0
  134. package/utils/dynamic-content/info.ts +219 -0
  135. package/utils/dynamic-content/new-release.ts +126 -0
  136. package/utils/dynamic-content/support-notice.ts +169 -0
  137. package/utils/dynamic-content/types.d.ts +101 -0
  138. package/utils/dynamic-content/util.ts +122 -0
  139. package/utils/inactivity.ts +104 -0
  140. package/utils/pagination-utils.ts +105 -31
  141. package/utils/pagination-wrapper.ts +6 -8
  142. package/utils/release-notes.ts +1 -1
  143. package/utils/sort.js +5 -0
  144. package/utils/unit-tests/pagination-utils.spec.ts +283 -0
  145. package/utils/validators/formRules/__tests__/index.test.ts +7 -0
  146. 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
+ }