@rancher/shell 3.0.7 → 3.0.8-rc.2

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 (123) hide show
  1. package/assets/images/vendor/githubapp.svg +13 -0
  2. package/assets/styles/base/_typography.scss +1 -1
  3. package/assets/styles/global/_layout.scss +21 -35
  4. package/assets/styles/themes/_modern.scss +5 -5
  5. package/assets/translations/en-us.yaml +102 -17
  6. package/assets/translations/zh-hans.yaml +0 -4
  7. package/components/EmberPage.vue +1 -1
  8. package/components/Inactivity.vue +222 -106
  9. package/components/InstallHelmCharts.vue +2 -2
  10. package/components/Resource/Detail/CopyToClipboard.vue +1 -1
  11. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +0 -2
  12. package/components/Resource/Detail/TitleBar/index.vue +10 -6
  13. package/components/ResourceDetail/index.vue +4 -1
  14. package/components/SortableTable/index.vue +18 -2
  15. package/components/{nav/WindowManager → Window}/ContainerLogs.vue +1 -1
  16. package/components/{nav/WindowManager → Window}/ContainerLogsActions.vue +1 -0
  17. package/components/{nav/WindowManager → Window}/__tests__/ContainerLogs.test.ts +1 -1
  18. package/components/{nav/WindowManager → Window}/__tests__/ContainerShell.test.ts +2 -2
  19. package/components/fleet/FleetConfigMapSelector.vue +117 -0
  20. package/components/fleet/FleetSecretSelector.vue +127 -0
  21. package/components/fleet/__tests__/FleetConfigMapSelector.test.ts +125 -0
  22. package/components/fleet/__tests__/FleetSecretSelector.test.ts +82 -0
  23. package/components/form/FileImageSelector.vue +13 -4
  24. package/components/form/FileSelector.vue +11 -2
  25. package/components/form/ResourceLabeledSelect.vue +1 -0
  26. package/components/form/__tests__/ResourceLabeledSelect.test.ts +90 -0
  27. package/components/nav/Header.vue +34 -13
  28. package/components/{DraggableZone.vue → nav/WindowManager/PinArea.vue} +47 -80
  29. package/components/nav/WindowManager/composables/useComponentsMount.ts +70 -0
  30. package/components/nav/WindowManager/composables/useDimensionsHandler.ts +105 -0
  31. package/components/nav/WindowManager/composables/useDragHandler.ts +99 -0
  32. package/components/nav/WindowManager/composables/usePanelHandler.ts +72 -0
  33. package/components/nav/WindowManager/composables/usePanelsHandler.ts +14 -0
  34. package/components/nav/WindowManager/composables/useResizeHandler.ts +167 -0
  35. package/components/nav/WindowManager/composables/useTabsHandler.ts +51 -0
  36. package/components/nav/WindowManager/constants.ts +23 -0
  37. package/components/nav/WindowManager/index.vue +61 -575
  38. package/components/nav/WindowManager/panels/HorizontalPanel.vue +265 -0
  39. package/components/nav/WindowManager/panels/TabBodyContainer.vue +39 -0
  40. package/components/nav/WindowManager/panels/VerticalPanel.vue +308 -0
  41. package/components/templates/default.vue +4 -40
  42. package/components/templates/home.vue +31 -5
  43. package/config/product/auth.js +1 -0
  44. package/config/query-params.js +1 -0
  45. package/config/settings.ts +8 -1
  46. package/config/store.js +4 -2
  47. package/config/types.js +2 -0
  48. package/detail/pod.vue +1 -0
  49. package/dialog/AddonConfigConfirmationDialog.vue +45 -1
  50. package/directives/ui-context.ts +97 -0
  51. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +52 -11
  52. package/edit/auth/AuthProviderWarningBanners.vue +14 -1
  53. package/edit/auth/github-app-steps.vue +97 -0
  54. package/edit/auth/github-steps.vue +75 -0
  55. package/edit/auth/github.vue +94 -65
  56. package/edit/fleet.cattle.io.helmop.vue +51 -2
  57. package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +15 -5
  58. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +11 -9
  59. package/edit/provisioning.cattle.io.cluster/rke2.vue +56 -9
  60. package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +28 -2
  61. package/initialize/install-directives.js +2 -0
  62. package/list/projectsecret.vue +1 -1
  63. package/machine-config/azure.vue +1 -1
  64. package/mixins/chart.js +1 -1
  65. package/models/__tests__/chart.test.ts +17 -9
  66. package/models/__tests__/compliance.cattle.io.clusterscanprofile.spec.js +30 -0
  67. package/models/catalog.cattle.io.app.js +1 -1
  68. package/models/chart.js +3 -1
  69. package/models/compliance.cattle.io.clusterscanprofile.js +1 -1
  70. package/models/management.cattle.io.authconfig.js +1 -0
  71. package/package.json +2 -2
  72. package/pages/auth/login.vue +5 -2
  73. package/pages/auth/verify.vue +1 -1
  74. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +3 -2
  75. package/pages/c/_cluster/apps/charts/chart.vue +2 -2
  76. package/pages/c/_cluster/explorer/EventsTable.vue +89 -3
  77. package/pages/c/_cluster/explorer/tools/index.vue +3 -3
  78. package/pages/c/_cluster/settings/performance.vue +12 -25
  79. package/pages/home.vue +313 -12
  80. package/plugins/axios.js +2 -1
  81. package/plugins/dashboard-store/actions.js +1 -1
  82. package/plugins/dashboard-store/resource-class.js +17 -2
  83. package/plugins/steve/steve-pagination-utils.ts +2 -2
  84. package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +5 -1
  85. package/scripts/extension/publish +1 -1
  86. package/store/auth.js +8 -3
  87. package/store/aws.js +8 -6
  88. package/store/features.js +1 -0
  89. package/store/index.js +9 -3
  90. package/store/prefs.js +6 -0
  91. package/store/ui-context.ts +86 -0
  92. package/store/wm.ts +244 -0
  93. package/types/kube/kube-api.ts +2 -1
  94. package/types/rancher/index.d.ts +1 -0
  95. package/types/resources/settings.d.ts +29 -7
  96. package/types/shell/index.d.ts +59 -0
  97. package/types/window-manager.ts +22 -0
  98. package/utils/__tests__/cluster.test.ts +379 -1
  99. package/utils/cluster.js +157 -3
  100. package/utils/dynamic-content/__tests__/config.test.ts +187 -0
  101. package/utils/dynamic-content/__tests__/index.test.ts +390 -0
  102. package/utils/dynamic-content/__tests__/info.test.ts +263 -0
  103. package/utils/dynamic-content/__tests__/new-release.test.ts +216 -0
  104. package/utils/dynamic-content/__tests__/support-notice.test.ts +262 -0
  105. package/utils/dynamic-content/__tests__/util.test.ts +235 -0
  106. package/utils/dynamic-content/config.ts +55 -0
  107. package/utils/dynamic-content/index.ts +273 -0
  108. package/utils/dynamic-content/info.ts +219 -0
  109. package/utils/dynamic-content/new-release.ts +126 -0
  110. package/utils/dynamic-content/support-notice.ts +169 -0
  111. package/utils/dynamic-content/types.d.ts +101 -0
  112. package/utils/dynamic-content/util.ts +122 -0
  113. package/utils/dynamic-importer.js +2 -2
  114. package/utils/inactivity.ts +104 -0
  115. package/utils/pagination-utils.ts +19 -4
  116. package/utils/release-notes.ts +1 -1
  117. package/assets/images/icons/document.svg +0 -3
  118. package/store/wm.js +0 -95
  119. /package/components/{nav/WindowManager → Window}/ChartReadme.vue +0 -0
  120. /package/components/{nav/WindowManager → Window}/ContainerShell.vue +0 -0
  121. /package/components/{nav/WindowManager → Window}/KubectlShell.vue +0 -0
  122. /package/components/{nav/WindowManager → Window}/MachineSsh.vue +0 -0
  123. /package/components/{nav/WindowManager → Window}/Window.vue +0 -0
@@ -1,4 +1,5 @@
1
- import { abbreviateClusterName } from '@shell/utils/cluster';
1
+ import { abbreviateClusterName, _addonConfigPreserveFilter, addonConfigPreserve } from '@shell/utils/cluster';
2
+ import { diff } from '@shell/utils/object';
2
3
 
3
4
  describe('fx: abbreviateClusterName', () => {
4
5
  it.each([
@@ -55,3 +56,380 @@ describe('fx: abbreviateClusterName', () => {
55
56
  expect(result).toStrictEqual(expected);
56
57
  });
57
58
  });
59
+
60
+ describe('fx: _addonConfigPreserveFilter', () => {
61
+ it('should return an empty object if there are no differences between default values', () => {
62
+ const oldDefaults = { replicaCount: 1 };
63
+ const newDefaults = { replicaCount: 1 };
64
+ const userVals = { replicaCount: 3 };
65
+ const diffs = diff(oldDefaults, newDefaults);
66
+ const expected = {};
67
+
68
+ expect(_addonConfigPreserveFilter(diffs, userVals)).toStrictEqual(expected);
69
+ });
70
+
71
+ it('should return an empty object if no user values are provided', () => {
72
+ const oldDefaults = { replicaCount: 1 };
73
+ const newDefaults = { replicaCount: 2 };
74
+ const userVals = {};
75
+ const diffs = diff(oldDefaults, newDefaults);
76
+ const expected = {};
77
+
78
+ expect(_addonConfigPreserveFilter(diffs, userVals)).toStrictEqual(expected);
79
+ });
80
+
81
+ it('should filter out diffs for fields not customized by the user', () => {
82
+ const oldDefaults = {
83
+ replicaCount: 1,
84
+ persistence: false
85
+ };
86
+ const newDefaults = {
87
+ replicaCount: 2,
88
+ persistence: true
89
+ };
90
+ const userVals = { replicaCount: 3 };
91
+ const diffs = diff(oldDefaults, newDefaults);
92
+ const expected = { replicaCount: 2 };
93
+
94
+ expect(_addonConfigPreserveFilter(diffs, userVals)).toStrictEqual(expected);
95
+ });
96
+
97
+ it('should include diffs for fields customized by the user', () => {
98
+ const oldDefaults = { replicaCount: 1 };
99
+ const newDefaults = { replicaCount: 2 };
100
+ const userVals = { replicaCount: 3 };
101
+ const diffs = diff(oldDefaults, newDefaults);
102
+ const expected = { replicaCount: 2 };
103
+
104
+ expect(_addonConfigPreserveFilter(diffs, userVals)).toStrictEqual(expected);
105
+ });
106
+
107
+ it('should handle nested objects: include diff if user customized nested property', () => {
108
+ const oldDefaults = { service: { port: 80 } };
109
+ const newDefaults = { service: { port: 8080 } };
110
+ const userVals = { service: { port: 9000 } };
111
+ const diffs = diff(oldDefaults, newDefaults);
112
+ const expected = { service: { port: 8080 } };
113
+
114
+ expect(_addonConfigPreserveFilter(diffs, userVals)).toStrictEqual(expected);
115
+ });
116
+
117
+ it('should handle nested objects: exclude diff if user did not customize nested property', () => {
118
+ const oldDefaults = { service: { port: 80, type: 'ClusterIP' } };
119
+ const newDefaults = { service: { port: 8080, type: 'ClusterIP' } };
120
+ const userVals = { service: { type: 'NodePort' } };
121
+ const diffs = diff(oldDefaults, newDefaults);
122
+ const expected = {};
123
+
124
+ expect(_addonConfigPreserveFilter(diffs, userVals)).toStrictEqual(expected);
125
+ });
126
+
127
+ it('should handle nested objects: include diff if user customized a removed nested object', () => {
128
+ const oldDefaults = { service: { port: 80 } };
129
+ const newDefaults = {};
130
+ const userVals = { service: { port: 9000 } };
131
+ const diffs = diff(oldDefaults, newDefaults);
132
+ const expected = { service: { port: null } };
133
+
134
+ expect(_addonConfigPreserveFilter(diffs, userVals)).toStrictEqual(expected);
135
+ });
136
+
137
+ it('should handle nested objects: exclude diff if a new property is added to default nested object and user did not customize it', () => {
138
+ const oldDefaults = { service: { port: 80 } };
139
+ const newDefaults = { service: { port: 80, type: 'ClusterIP' } };
140
+ const userVals = { service: { port: 80 } };
141
+ const diffs = diff(oldDefaults, newDefaults);
142
+ const expected = {};
143
+
144
+ expect(_addonConfigPreserveFilter(diffs, userVals)).toStrictEqual(expected);
145
+ });
146
+
147
+ it('should handle arrays of primitives: include diff if user customized array and default array changed', () => {
148
+ const oldDefaults = { ingress: { hosts: ['host1.com', 'host2.com'] } };
149
+ const newDefaults = { ingress: { hosts: ['host1.com', 'host3.com'] } };
150
+ const userVals = { ingress: { hosts: ['user.host.com'] } };
151
+ const diffs = diff(oldDefaults, newDefaults);
152
+ const expected = { ingress: { hosts: ['host1.com', 'host3.com'] } };
153
+
154
+ expect(_addonConfigPreserveFilter(diffs, userVals)).toStrictEqual(expected);
155
+ });
156
+
157
+ it('should handle arrays of primitives: exclude diff if user did not customize array', () => {
158
+ const oldDefaults = { ingress: { hosts: ['host1.com', 'host2.com'] } };
159
+ const newDefaults = { ingress: { hosts: ['host1.com', 'host3.com'] } };
160
+ const userVals = { ingress: { enabled: true } };
161
+ const diffs = diff(oldDefaults, newDefaults);
162
+ const expected = {};
163
+
164
+ expect(_addonConfigPreserveFilter(diffs, userVals)).toStrictEqual(expected);
165
+ });
166
+
167
+ it('should handle arrays of objects: include diff if user customized array and default array changed', () => {
168
+ const oldDefaults = { service: { ports: [{ name: 'http', port: 80 }] } };
169
+ const newDefaults = { service: { ports: [{ name: 'http', port: 80 }, { name: 'https', port: 443 }] } };
170
+ const userVals = { service: { ports: [{ name: 'http', port: 8080 }] } };
171
+ const diffs = diff(oldDefaults, newDefaults);
172
+ const expected = { service: { ports: [{ name: 'http', port: 80 }, { name: 'https', port: 443 }] } };
173
+
174
+ expect(_addonConfigPreserveFilter(diffs, userVals)).toStrictEqual(expected);
175
+ });
176
+
177
+ it('should handle arrays of objects: exclude diff if user did not customize array', () => {
178
+ const oldDefaults = { service: { ports: [{ name: 'http', port: 80 }] } };
179
+ const newDefaults = { service: { ports: [{ name: 'http', port: 80 }, { name: 'https', port: 443 }] } };
180
+ const userVals = { service: { type: 'ClusterIP' } };
181
+ const diffs = diff(oldDefaults, newDefaults);
182
+ const expected = {};
183
+
184
+ expect(_addonConfigPreserveFilter(diffs, userVals)).toStrictEqual(expected);
185
+ });
186
+
187
+ it('should handle properties added/removed: include diff if user customized a removed property', () => {
188
+ const oldDefaults = { oldProperty: 'defaultValue' };
189
+ const newDefaults = {};
190
+ const userVals = { oldProperty: 'customValue' };
191
+ const diffs = diff(oldDefaults, newDefaults);
192
+ const expected = { oldProperty: null };
193
+
194
+ expect(_addonConfigPreserveFilter(diffs, userVals)).toStrictEqual(expected);
195
+ });
196
+
197
+ it('should handle properties added/removed: exclude diff if user did not customize an added property', () => {
198
+ const oldDefaults = {};
199
+ const newDefaults = { newProperty: 'defaultValue' };
200
+ const userVals = { otherProp: 'value' };
201
+ const diffs = diff(oldDefaults, newDefaults);
202
+ const expected = {};
203
+
204
+ expect(_addonConfigPreserveFilter(diffs, userVals)).toStrictEqual(expected);
205
+ });
206
+
207
+ it('should handle complex nested structures with multiple changes', () => {
208
+ const oldDefaults = {
209
+ replicaCount: 1,
210
+ image: {
211
+ repository: 'nginx',
212
+ tag: 'stable'
213
+ },
214
+ service: {
215
+ type: 'ClusterIP',
216
+ port: 80
217
+ },
218
+ ingress: {
219
+ enabled: false,
220
+ hosts: ['chart-example.local']
221
+ }
222
+ };
223
+ const newDefaults = {
224
+ replicaCount: 2,
225
+ image: {
226
+ repository: 'nginx',
227
+ tag: 'mainline'
228
+ },
229
+ service: {
230
+ type: 'ClusterIP',
231
+ port: 80
232
+ },
233
+ ingress: {
234
+ enabled: true,
235
+ hosts: ['chart-example.local', 'new.chart-example.local'],
236
+ tls: true
237
+ }
238
+ };
239
+ const userVals = {
240
+ replicaCount: 3,
241
+ ingress: { hosts: ['my.custom.host'] }
242
+ };
243
+ const diffs = diff(oldDefaults, newDefaults);
244
+ const expected = {
245
+ replicaCount: 2,
246
+ ingress: { hosts: ['chart-example.local', 'new.chart-example.local'] }
247
+ };
248
+
249
+ expect(_addonConfigPreserveFilter(diffs, userVals)).toStrictEqual(expected);
250
+ });
251
+ });
252
+
253
+ describe('fx: addonConfigPreserve', () => {
254
+ const ADDON_NAME = 'rke2-my-addon';
255
+ const mockOldVersionCharts = {
256
+ [ADDON_NAME]: {
257
+ repo: 'repo',
258
+ version: '1.0.0'
259
+ }
260
+ };
261
+
262
+ const mockNewVersionCharts = {
263
+ [ADDON_NAME]: {
264
+ repo: 'repo',
265
+ version: '1.1.0'
266
+ }
267
+ };
268
+
269
+ const mockOldVersionInfo = {
270
+ values: {
271
+ replicas: 1,
272
+ service: { type: 'ClusterIP' }
273
+ }
274
+ };
275
+
276
+ const mockNewVersionInfo = {
277
+ values: {
278
+ replicas: 2, // changed
279
+ service: { type: 'ClusterIP' },
280
+ persistence: true // new
281
+ }
282
+ };
283
+
284
+ let mockStore: any;
285
+ let addonConfigDiffs: any;
286
+ let userChartValues: any;
287
+ let context: any;
288
+
289
+ beforeEach(() => {
290
+ mockStore = {
291
+ dispatch: jest.fn((action, payload) => {
292
+ if (action === 'catalog/getVersionInfo') {
293
+ if (payload.versionName === '1.0.0') {
294
+ return Promise.resolve(mockOldVersionInfo);
295
+ }
296
+ if (payload.versionName === '1.1.0') {
297
+ return Promise.resolve(mockNewVersionInfo);
298
+ }
299
+ }
300
+
301
+ return Promise.resolve({});
302
+ })
303
+ };
304
+ addonConfigDiffs = {};
305
+ userChartValues = {};
306
+ context = {
307
+ addonConfigDiffs,
308
+ addonNames: [ADDON_NAME],
309
+ $store: mockStore,
310
+ userChartValues,
311
+ };
312
+ });
313
+
314
+ it('should identify no relevant changes if user has no custom values', async() => {
315
+ // No user overrides means no relevant differences.
316
+ context.userChartValues = {};
317
+ await addonConfigPreserve(context, mockOldVersionCharts, mockNewVersionCharts);
318
+
319
+ expect(context.addonConfigDiffs[ADDON_NAME]).toStrictEqual({});
320
+ expect(context.userChartValues[`${ ADDON_NAME }-1.1.0`]).toBeUndefined();
321
+ });
322
+
323
+ it('should identify no relevant changes if user custom values are for different fields', async() => {
324
+ // User overrides for non-differing fields should not be considered relevant.
325
+ context.userChartValues = { [`${ ADDON_NAME }-1.0.0`]: { service: { type: 'NodePort' } } };
326
+ await addonConfigPreserve(context, mockOldVersionCharts, mockNewVersionCharts);
327
+
328
+ expect(context.addonConfigDiffs[ADDON_NAME]).toStrictEqual({});
329
+ // Since diffs are empty, user values should be preserved
330
+ expect(context.userChartValues[`${ ADDON_NAME }-1.1.0`]).toStrictEqual({ service: { type: 'NodePort' } });
331
+ });
332
+
333
+ it('should identify relevant changes if user has customized a changed field', async() => {
334
+ // A user override on a changed default should be flagged as a relevant difference.
335
+ context.userChartValues = { [`${ ADDON_NAME }-1.0.0`]: { replicas: 3 } };
336
+ await addonConfigPreserve(context, mockOldVersionCharts, mockNewVersionCharts);
337
+
338
+ expect(context.addonConfigDiffs[ADDON_NAME]).toStrictEqual({ replicas: 2 });
339
+ // Since diffs are NOT empty, user values should NOT be preserved
340
+ expect(context.userChartValues[`${ ADDON_NAME }-1.1.0`]).toBeUndefined();
341
+ });
342
+
343
+ it('should not identify relevant changes for new fields not customized by user', async() => {
344
+ // New fields in the addon default values are not relevant if not customized by the user.
345
+ context.userChartValues = { [`${ ADDON_NAME }-1.0.0`]: {} };
346
+ await addonConfigPreserve(context, mockOldVersionCharts, mockNewVersionCharts);
347
+
348
+ // The only diff is `replicas`, which user didn't touch. `persistence` is new but also not touched.
349
+ // So final diffs should be empty.
350
+ expect(context.addonConfigDiffs[ADDON_NAME]).toStrictEqual({});
351
+ expect(context.userChartValues[`${ ADDON_NAME }-1.1.0`]).toStrictEqual({});
352
+ });
353
+
354
+ it('should preserve user values if there are no relevant differences', async() => {
355
+ // User values should be carried over to the new version if no relevant diffs are found.
356
+ context.userChartValues = { [`${ ADDON_NAME }-1.0.0`]: { service: { type: 'NodePort' } } };
357
+ await addonConfigPreserve(context, mockOldVersionCharts, mockNewVersionCharts);
358
+
359
+ expect(context.addonConfigDiffs[ADDON_NAME]).toStrictEqual({});
360
+ expect(context.userChartValues[`${ ADDON_NAME }-1.1.0`]).toStrictEqual({ service: { type: 'NodePort' } });
361
+ });
362
+
363
+ it('should not preserve user values if there are relevant differences', async() => {
364
+ // User values should not be carried over if relevant diffs are found.
365
+ context.userChartValues = { [`${ ADDON_NAME }-1.0.0`]: { replicas: 3 } };
366
+ await addonConfigPreserve(context, mockOldVersionCharts, mockNewVersionCharts);
367
+
368
+ expect(context.addonConfigDiffs[ADDON_NAME]).toStrictEqual({ replicas: 2 });
369
+ expect(context.userChartValues[`${ ADDON_NAME }-1.1.0`]).toBeUndefined();
370
+ });
371
+
372
+ it('should handle catalog API errors gracefully', async() => {
373
+ // Errors from fetching chart info should be caught and handled.
374
+ const error = new Error('catalog fetch failed');
375
+
376
+ jest.spyOn(context.$store, 'dispatch').mockImplementation().mockRejectedValue(error);
377
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
378
+
379
+ await addonConfigPreserve(context, mockOldVersionCharts, mockNewVersionCharts);
380
+
381
+ expect(context.addonConfigDiffs[ADDON_NAME]).toBeUndefined();
382
+ expect(errorSpy).toHaveBeenCalledWith(`Failed to get chart version info for diff for chart ${ ADDON_NAME }`, error);
383
+ errorSpy.mockRestore();
384
+ });
385
+
386
+ it('should do nothing if oldCharts is not provided', async() => {
387
+ await addonConfigPreserve(context, {}, mockNewVersionCharts);
388
+ expect(context.addonConfigDiffs).toStrictEqual({});
389
+ expect(mockStore.dispatch).not.toHaveBeenCalled();
390
+ });
391
+
392
+ it('should do nothing if newCharts is not provided', async() => {
393
+ await addonConfigPreserve(context, mockOldVersionCharts, {});
394
+ expect(context.addonConfigDiffs).toStrictEqual({});
395
+ expect(mockStore.dispatch).not.toHaveBeenCalled();
396
+ });
397
+
398
+ it('should do nothing if addon version is the same', async() => {
399
+ const mockNewVersionSame = {
400
+ [ADDON_NAME]: {
401
+ repo: 'repo',
402
+ version: '1.0.0'
403
+ }
404
+ };
405
+
406
+ await addonConfigPreserve(context, mockOldVersionCharts, mockNewVersionSame);
407
+ expect(context.addonConfigDiffs[ADDON_NAME]).toBeUndefined();
408
+ expect(mockStore.dispatch).not.toHaveBeenCalled();
409
+ });
410
+
411
+ it('should handle multiple addons', async() => {
412
+ const ADDON2_NAME = 'rke2-my-addon2';
413
+ const ADDON3_NAME = 'rke2-my-addon3'; // same version
414
+
415
+ const oldCharts = {
416
+ ...mockOldVersionCharts,
417
+ [ADDON2_NAME]: { repo: 'repo', version: '1.0.0' },
418
+ [ADDON3_NAME]: { repo: 'repo', version: '1.0.0' },
419
+ };
420
+ const newCharts = {
421
+ ...mockNewVersionCharts,
422
+ [ADDON2_NAME]: { repo: 'repo', version: '1.1.0' }, // changed version, but no user values
423
+ [ADDON3_NAME]: { repo: 'repo', version: '1.0.0' }, // same version
424
+ };
425
+
426
+ context.addonNames = [ADDON_NAME, ADDON2_NAME, ADDON3_NAME];
427
+ context.userChartValues = { [`${ ADDON_NAME }-1.0.0`]: { replicas: 3 } };
428
+
429
+ await addonConfigPreserve(context, oldCharts, newCharts);
430
+
431
+ expect(context.addonConfigDiffs[ADDON_NAME]).toStrictEqual({ replicas: 2 });
432
+ expect(context.addonConfigDiffs[ADDON2_NAME]).toStrictEqual({});
433
+ expect(context.addonConfigDiffs[ADDON3_NAME]).toBeUndefined();
434
+ });
435
+ });
package/utils/cluster.js CHANGED
@@ -8,8 +8,8 @@ import { compare, sortable } from '@shell/utils/version';
8
8
  import { sortBy } from '@shell/utils/sort';
9
9
  import { HARVESTER_CONTAINER, SCHEDULING_CUSTOMIZATION } from '@shell/store/features';
10
10
  import { _CREATE, _EDIT } from '@shell/config/query-params';
11
- import isEmpty from 'lodash/isEmpty';
12
- import { set } from '@shell/utils/object';
11
+ import isEmptyLodash from 'lodash/isEmpty';
12
+ import { set, diff, isEmpty, clone } from '@shell/utils/object';
13
13
 
14
14
  /**
15
15
  * Combination of paginationFilterHiddenLocalCluster and paginationFilterOnlyKubernetesClusters
@@ -323,7 +323,7 @@ export async function initSchedulingCustomization(value, features, store, mode)
323
323
  errors.push(e);
324
324
  }
325
325
 
326
- if (schedulingCustomizationFeatureEnabled && mode === _CREATE && isEmpty(value?.clusterAgentDeploymentCustomization?.schedulingCustomization)) {
326
+ if (schedulingCustomizationFeatureEnabled && mode === _CREATE && isEmptyLodash(value?.clusterAgentDeploymentCustomization?.schedulingCustomization)) {
327
327
  set(value, 'clusterAgentDeploymentCustomization.schedulingCustomization', { priorityClass: clusterAgentDefaultPC, podDisruptionBudget: clusterAgentDefaultPDB });
328
328
  }
329
329
 
@@ -335,3 +335,157 @@ export async function initSchedulingCustomization(value, features, store, mode)
335
335
  clusterAgentDefaultPC, clusterAgentDefaultPDB, schedulingCustomizationFeatureEnabled, schedulingCustomizationOriginallyEnabled, errors
336
336
  };
337
337
  }
338
+
339
+ /**
340
+ * Recursively filters a `diffs` object to only include differences that are relevant to the user.
341
+ * A difference is considered relevant if the user has provided a custom value for that specific field.
342
+ *
343
+ * @param {object} diffs - The object representing the differences between two chart versions' default values.
344
+ * @param {object} userVals - The object containing the user's custom values.
345
+ * @returns {object} A new object containing only the relevant differences.
346
+ */
347
+ export function _addonConfigPreserveFilter(diffs, userVals) {
348
+ const filtered = {};
349
+
350
+ for (const key of Object.keys(diffs)) {
351
+ const diffVal = diffs[key];
352
+ const userVal = userVals?.[key];
353
+
354
+ const isDiffObject = typeof diffVal === 'object' && diffVal !== null && !Array.isArray(diffVal);
355
+ const isUserObject = typeof userVal === 'object' && userVal !== null && !Array.isArray(userVal);
356
+
357
+ // If both the diff and user value are objects, we need to recurse into them.
358
+ if (isDiffObject && isUserObject) {
359
+ const nestedFiltered = _addonConfigPreserveFilter(diffVal, userVal);
360
+
361
+ if (!isEmpty(nestedFiltered)) {
362
+ filtered[key] = nestedFiltered;
363
+ }
364
+ } else if (userVal !== undefined) {
365
+ // If the user has provided a value for this key, the difference is relevant.
366
+ filtered[key] = diffVal;
367
+ }
368
+ }
369
+
370
+ return filtered;
371
+ }
372
+
373
+ /**
374
+ * Processes a single add-on version change. It fetches the old and new chart information,
375
+ * calculates the differences in default values, and filters them based on user's customizations.
376
+ * If there are no significant differences, it preserves the user's custom values for the new version.
377
+ *
378
+ * @param {object} store The Vuex store.
379
+ * @param {object} userChartValues The user's customized chart values.
380
+ * @param {string} chartName The name of the chart to process.
381
+ * @param {object} oldAddon The addon information from the previous Kubernetes version.
382
+ * @param {object} newAddon The addon information from the new Kubernetes version.
383
+ * @returns {object|null} An object containing the diff and a preserve flag, or null on error.
384
+ */
385
+ async function _addonConfigPreserveProcess(store, userChartValues, chartName, oldAddon, newAddon) {
386
+ if (chartName.includes('none')) {
387
+ return null;
388
+ }
389
+
390
+ try {
391
+ const [oldVersionInfo, newVersionInfo] = await Promise.all([
392
+ store.dispatch('catalog/getVersionInfo', {
393
+ repoType: 'cluster',
394
+ repoName: oldAddon.repo,
395
+ chartName,
396
+ versionName: oldAddon.version,
397
+ }),
398
+ store.dispatch('catalog/getVersionInfo', {
399
+ repoType: 'cluster',
400
+ repoName: newAddon.repo,
401
+ chartName,
402
+ versionName: newAddon.version,
403
+ })
404
+ ]);
405
+
406
+ const oldDefaults = oldVersionInfo.values;
407
+ const newDefaults = newVersionInfo.values;
408
+ const defaultsDifferences = diff(oldDefaults, newDefaults);
409
+
410
+ const userOldValues = userChartValues[`${ chartName }-${ oldAddon.version }`];
411
+
412
+ // We only care about differences in values that the user has actually customized.
413
+ // If the user hasn't touched a value, a change in its default should not be considered a breaking change.
414
+ const defaultsAndUserDifferences = userOldValues ? _addonConfigPreserveFilter(defaultsDifferences, userOldValues) : {};
415
+
416
+ return {
417
+ diff: defaultsAndUserDifferences,
418
+ preserve: isEmpty(defaultsAndUserDifferences)
419
+ };
420
+ } catch (e) {
421
+ console.error(`Failed to get chart version info for diff for chart ${ chartName }`, e); // eslint-disable-line no-console
422
+
423
+ return null;
424
+ }
425
+ }
426
+
427
+ /**
428
+ * @typedef {object} AddonPreserveContext
429
+ * @property {object} addonConfigDiffs - An object that stores the diffs.
430
+ * @property {string[]} addonNames - An array of addon names to check.
431
+ * @property {object} $store - The Vuex store.
432
+ * @property {object} userChartValues - The user's customized chart values.
433
+ *
434
+ * When the Kubernetes version is changed, this method is called to handle the add-on configurations
435
+ * for all enabled addons. It checks if an addon's version has changed and, if so, determines if the
436
+ * user's custom configurations should be preserved for the new version.
437
+ *
438
+ * The goal is to avoid showing a confirmation dialog for changes in default values that the user has not customized.
439
+ *
440
+ * @param {AddonPreserveContext} context The context object from the component.
441
+ * @param {object} oldCharts The charts object from the K8s release object being changed from.
442
+ * @param {object} newCharts The charts object from the K8s release object being changed to.
443
+ */
444
+ export async function addonConfigPreserve(context, oldCharts, newCharts) {
445
+ const {
446
+ addonConfigDiffs,
447
+ addonNames,
448
+ $store,
449
+ userChartValues
450
+ } = context;
451
+
452
+ if (!oldCharts || !newCharts) {
453
+ return;
454
+ }
455
+
456
+ // Clear the diffs object for the new run
457
+ for (const key in addonConfigDiffs) {
458
+ delete addonConfigDiffs[key];
459
+ }
460
+
461
+ // Iterate through the addons that are enabled for the cluster.
462
+ for (const chartName of addonNames) {
463
+ const oldAddon = oldCharts[chartName];
464
+ const newAddon = newCharts[chartName];
465
+
466
+ // If the addon didn't exist in the old K8s version, there's nothing to compare.
467
+ if (!oldAddon) {
468
+ continue;
469
+ }
470
+
471
+ // Check if the add-on version has changed.
472
+ if (newAddon && newAddon.version !== oldAddon.version) {
473
+ const result = await _addonConfigPreserveProcess($store, userChartValues, chartName, oldAddon, newAddon);
474
+
475
+ if (result) {
476
+ addonConfigDiffs[chartName] = result.diff;
477
+
478
+ if (result.preserve) {
479
+ const oldKey = `${ chartName }-${ oldAddon.version }`;
480
+ const newKey = `${ chartName }-${ newAddon.version }`;
481
+
482
+ // If custom values exist for the old version, and none exist for the new version,
483
+ // copy the values to the new key to preserve them.
484
+ if (userChartValues[oldKey] && !userChartValues[newKey]) {
485
+ userChartValues[newKey] = clone(userChartValues[oldKey]);
486
+ }
487
+ }
488
+ }
489
+ }
490
+ }
491
+ }