@rancher/shell 3.0.9-rc.3 → 3.0.9-rc.5

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 (128) hide show
  1. package/assets/brand/suse/metadata.json +2 -1
  2. package/assets/translations/en-us.yaml +105 -5
  3. package/components/ActionMenuShell.vue +1 -1
  4. package/components/Inactivity.vue +2 -2
  5. package/components/Resource/Detail/Card/ExtrasCard.vue +49 -15
  6. package/components/Resource/Detail/Card/__tests__/ExtrasCard.test.ts +111 -0
  7. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +0 -17
  8. package/components/Resource/Detail/Masthead/index.vue +11 -4
  9. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +3 -1
  10. package/components/Resource/Detail/Metadata/index.vue +1 -1
  11. package/components/Resource/Detail/ResourceRow.vue +1 -1
  12. package/components/ResourceDetail/Masthead/latest.vue +12 -2
  13. package/components/ResourceList/index.vue +9 -0
  14. package/components/ResourceTable.vue +38 -4
  15. package/components/Tabbed/Tab.vue +4 -0
  16. package/components/Tabbed/index.vue +4 -1
  17. package/components/__tests__/ProjectRow.test.ts +60 -0
  18. package/components/form/ChangePassword.vue +41 -35
  19. package/components/form/ResourceQuota/Project.vue +42 -1
  20. package/components/form/ResourceQuota/ProjectRow.vue +71 -4
  21. package/components/form/ResourceQuota/__tests__/Project.test.ts +63 -0
  22. package/components/form/SelectOrCreateAuthSecret.vue +6 -1
  23. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +35 -0
  24. package/components/formatter/KubeconfigClusters.vue +74 -0
  25. package/components/formatter/MachineSummaryGraph.vue +10 -2
  26. package/components/formatter/__tests__/KubeconfigClusters.test.ts +125 -0
  27. package/components/nav/TopLevelMenu.helper.ts +50 -2
  28. package/components/nav/TopLevelMenu.vue +14 -0
  29. package/components/nav/Type.vue +5 -0
  30. package/components/nav/__tests__/TopLevelMenu.test.ts +3 -3
  31. package/components/nav/__tests__/Type.test.ts +6 -4
  32. package/config/product/explorer.js +4 -3
  33. package/config/product/manager.js +47 -3
  34. package/config/router/navigation-guards/authentication.js +8 -9
  35. package/config/router/routes.js +4 -1
  36. package/config/types.js +10 -2
  37. package/detail/auditlog.cattle.io.auditpolicy.vue +19 -0
  38. package/detail/management.cattle.io.user.vue +1 -2
  39. package/detail/node.vue +0 -1
  40. package/detail/provisioning.cattle.io.cluster.vue +2 -1
  41. package/dialog/ChangePasswordDialog.vue +8 -0
  42. package/dialog/GenericPrompt.vue +20 -3
  43. package/dialog/ScaleMachineDownDialog.vue +65 -15
  44. package/dialog/SearchDialog.vue +10 -2
  45. package/dialog/__tests__/ScaleMachineDownDialog.test.ts +184 -0
  46. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +89 -0
  47. package/edit/__tests__/management.cattle.io.project.test.js +56 -1
  48. package/edit/auditlog.cattle.io.auditpolicy/AdditionalRedactions.vue +114 -0
  49. package/edit/auditlog.cattle.io.auditpolicy/Filters.vue +119 -0
  50. package/edit/auditlog.cattle.io.auditpolicy/General.vue +180 -0
  51. package/edit/auditlog.cattle.io.auditpolicy/__tests__/AdditionalRedactions.test.ts +327 -0
  52. package/edit/auditlog.cattle.io.auditpolicy/__tests__/Filters.test.ts +449 -0
  53. package/edit/auditlog.cattle.io.auditpolicy/__tests__/General.test.ts +472 -0
  54. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/AdditionalRedactions.test.ts.snap +27 -0
  55. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/Filters.test.ts.snap +39 -0
  56. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +174 -0
  57. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +29 -0
  58. package/edit/auditlog.cattle.io.auditpolicy/__tests__/index.test.ts +215 -0
  59. package/edit/auditlog.cattle.io.auditpolicy/index.vue +104 -0
  60. package/edit/auditlog.cattle.io.auditpolicy/types.ts +28 -0
  61. package/edit/fleet.cattle.io.gitrepo.vue +16 -1
  62. package/edit/management.cattle.io.project.vue +8 -2
  63. package/edit/management.cattle.io.user.vue +29 -34
  64. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +178 -0
  65. package/edit/provisioning.cattle.io.cluster/rke2.vue +22 -2
  66. package/edit/provisioning.cattle.io.cluster/shared.ts +4 -0
  67. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
  68. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +57 -2
  69. package/edit/provisioning.cattle.io.cluster/tabs/etcd/__tests__/S3Config.test.ts +109 -0
  70. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +1 -0
  71. package/list/auditlog.cattle.io.auditpolicy.vue +63 -0
  72. package/list/ext.cattle.io.kubeconfig.vue +118 -0
  73. package/list/group.principal.vue +11 -15
  74. package/list/management.cattle.io.user.vue +11 -21
  75. package/machine-config/azure.vue +14 -0
  76. package/mixins/__tests__/chart.test.ts +147 -0
  77. package/mixins/browser-tab-visibility.js +5 -4
  78. package/mixins/chart.js +10 -8
  79. package/mixins/fetch.client.js +6 -0
  80. package/models/__tests__/auditlog.cattle.io.auditpolicy.test.ts +117 -0
  81. package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +364 -0
  82. package/models/__tests__/secret.test.ts +55 -0
  83. package/models/__tests__/workload.test.ts +49 -6
  84. package/models/auditlog.cattle.io.auditpolicy.js +46 -0
  85. package/models/cluster.x-k8s.io.machine.js +1 -1
  86. package/models/cluster.x-k8s.io.machinedeployment.js +5 -5
  87. package/models/event.js +5 -0
  88. package/models/ext.cattle.io.groupmembershiprefreshrequest.js +15 -0
  89. package/models/ext.cattle.io.kubeconfig.ts +97 -0
  90. package/models/ext.cattle.io.passwordchangerequest.js +15 -0
  91. package/models/ext.cattle.io.selfuser.js +15 -0
  92. package/models/fleet-application.js +17 -7
  93. package/models/management.cattle.io.user.js +28 -31
  94. package/models/schema.js +18 -0
  95. package/models/secret.js +28 -25
  96. package/models/steve-schema.ts +39 -2
  97. package/models/workload.js +3 -2
  98. package/package.json +2 -2
  99. package/pages/about.vue +3 -2
  100. package/pages/account/index.vue +23 -16
  101. package/pages/auth/login.vue +15 -8
  102. package/pages/auth/setup.vue +52 -15
  103. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +38 -14
  104. package/pages/c/_cluster/apps/charts/index.vue +1 -0
  105. package/pages/home.vue +9 -3
  106. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -3
  107. package/plugins/dashboard-store/actions.js +7 -0
  108. package/plugins/dashboard-store/getters.js +23 -1
  109. package/plugins/dashboard-store/index.js +3 -2
  110. package/plugins/dashboard-store/mutations.js +4 -0
  111. package/plugins/dashboard-store/resource-class.js +12 -5
  112. package/plugins/steve/__tests__/steve-class.test.ts +167 -0
  113. package/plugins/steve/schema.d.ts +5 -0
  114. package/plugins/steve/steve-class.js +19 -0
  115. package/plugins/steve/steve-pagination-utils.ts +2 -1
  116. package/rancher-components/RcItemCard/RcItemCard.test.ts +4 -2
  117. package/rancher-components/RcItemCard/RcItemCard.vue +27 -10
  118. package/store/auth.js +57 -19
  119. package/store/notifications.ts +1 -1
  120. package/store/type-map.js +12 -1
  121. package/types/shell/index.d.ts +24 -15
  122. package/types/store/dashboard-store.types.ts +7 -0
  123. package/utils/__tests__/chart.test.ts +96 -0
  124. package/utils/__tests__/version.test.ts +1 -19
  125. package/utils/chart.js +64 -0
  126. package/utils/pagination-wrapper.ts +11 -3
  127. package/utils/version.js +5 -17
  128. package/vue.config.js +26 -13
@@ -0,0 +1,364 @@
1
+ import Kubeconfig from '@shell/models/ext.cattle.io.kubeconfig';
2
+ import { CAPI, MANAGEMENT } from '@shell/config/types';
3
+
4
+ // SteveModel is JS, so we need to type the constructor
5
+ const KubeconfigModel = Kubeconfig as unknown as new (data: object) => Kubeconfig;
6
+
7
+ describe('class Kubeconfig', () => {
8
+ const mockT = jest.fn((key: string, opts?: { name: string }) => {
9
+ if (key === '"ext.cattle.io.kubeconfig".deleted') {
10
+ return `${ opts?.name } (deleted)`;
11
+ }
12
+
13
+ return key;
14
+ });
15
+
16
+ const createKubeconfig = (data: object, rootGetters: object = {}) => {
17
+ const kubeconfig = new KubeconfigModel(data);
18
+
19
+ // Mock $rootGetters before any getters are accessed
20
+ // Cast to any since $rootGetters is inherited from JS SteveModel
21
+ jest.spyOn(kubeconfig as any, '$rootGetters', 'get').mockReturnValue({
22
+ 'i18n/t': mockT,
23
+ 'management/all': () => [],
24
+ ...rootGetters
25
+ });
26
+
27
+ return kubeconfig;
28
+ };
29
+
30
+ beforeEach(() => {
31
+ jest.clearAllMocks();
32
+ });
33
+
34
+ describe('expiresAt', () => {
35
+ it('should return null when ttl is not set', () => {
36
+ const kubeconfig = createKubeconfig({
37
+ metadata: { creationTimestamp: '2024-01-01T00:00:00Z' },
38
+ spec: {}
39
+ });
40
+
41
+ expect(kubeconfig.expiresAt).toBeNull();
42
+ });
43
+
44
+ it('should return null when creationTimestamp is not set', () => {
45
+ const kubeconfig = createKubeconfig({
46
+ metadata: {},
47
+ spec: { ttl: 3600 }
48
+ });
49
+
50
+ expect(kubeconfig.expiresAt).toBeNull();
51
+ });
52
+
53
+ it('should calculate expiry correctly', () => {
54
+ const kubeconfig = createKubeconfig({
55
+ metadata: { creationTimestamp: '2024-01-01T00:00:00Z' },
56
+ spec: { ttl: 3600 } // 1 hour in seconds
57
+ });
58
+
59
+ expect(kubeconfig.expiresAt).toBe('2024-01-01T01:00:00.000Z');
60
+ });
61
+
62
+ it('should handle large ttl values', () => {
63
+ const kubeconfig = createKubeconfig({
64
+ metadata: { creationTimestamp: '2024-01-01T00:00:00Z' },
65
+ spec: { ttl: 86400 } // 24 hours in seconds
66
+ });
67
+
68
+ expect(kubeconfig.expiresAt).toBe('2024-01-02T00:00:00.000Z');
69
+ });
70
+ });
71
+
72
+ describe('referencedClusters', () => {
73
+ const mockProvCluster = {
74
+ mgmt: { id: 'c-m-abc123' },
75
+ status: { clusterName: 'c-m-abc123' },
76
+ nameDisplay: 'my-cluster',
77
+ detailLocation: { name: 'c-cluster-product-resource-id', params: { cluster: 'my-cluster' } }
78
+ };
79
+
80
+ const mockMgmtCluster = {
81
+ id: 'c-m-def456',
82
+ nameDisplay: 'mgmt-cluster',
83
+ detailLocation: { name: 'c-cluster-product-resource-id', params: { cluster: 'mgmt-cluster' } }
84
+ };
85
+
86
+ it('should return empty array when no clusters are specified', () => {
87
+ const kubeconfig = createKubeconfig({
88
+ metadata: {},
89
+ spec: {}
90
+ });
91
+
92
+ expect(kubeconfig.referencedClusters).toStrictEqual([]);
93
+ });
94
+
95
+ it('should map provisioning cluster by mgmt id', () => {
96
+ const kubeconfig = createKubeconfig(
97
+ {
98
+ metadata: {},
99
+ spec: { clusters: ['c-m-abc123'] }
100
+ },
101
+ {
102
+ 'management/all': (type: string) => {
103
+ if (type === CAPI.RANCHER_CLUSTER) {
104
+ return [mockProvCluster];
105
+ }
106
+
107
+ return [];
108
+ }
109
+ }
110
+ );
111
+
112
+ expect(kubeconfig.referencedClusters).toStrictEqual([
113
+ {
114
+ label: 'my-cluster',
115
+ location: mockProvCluster.detailLocation
116
+ }
117
+ ]);
118
+ });
119
+
120
+ it('should map management cluster when no provisioning cluster found', () => {
121
+ const kubeconfig = createKubeconfig(
122
+ {
123
+ metadata: {},
124
+ spec: { clusters: ['c-m-def456'] }
125
+ },
126
+ {
127
+ 'management/all': (type: string) => {
128
+ if (type === MANAGEMENT.CLUSTER) {
129
+ return [mockMgmtCluster];
130
+ }
131
+
132
+ return [];
133
+ }
134
+ }
135
+ );
136
+
137
+ expect(kubeconfig.referencedClusters).toStrictEqual([
138
+ {
139
+ label: 'mgmt-cluster',
140
+ location: mockMgmtCluster.detailLocation
141
+ }
142
+ ]);
143
+ });
144
+
145
+ it('should return deleted label when cluster not found', () => {
146
+ const kubeconfig = createKubeconfig({
147
+ metadata: {},
148
+ spec: { clusters: ['c-m-deleted'] }
149
+ });
150
+
151
+ expect(kubeconfig.referencedClusters).toStrictEqual([
152
+ {
153
+ label: 'c-m-deleted (deleted)',
154
+ location: null
155
+ }
156
+ ]);
157
+ expect(mockT).toHaveBeenCalledWith('"ext.cattle.io.kubeconfig".deleted', { name: 'c-m-deleted' });
158
+ });
159
+
160
+ it('should prefer provisioning cluster over management cluster', () => {
161
+ const mgmtClusterSameId = {
162
+ id: 'c-m-abc123',
163
+ nameDisplay: 'mgmt-version',
164
+ detailLocation: { name: 'mgmt-location' }
165
+ };
166
+
167
+ const kubeconfig = createKubeconfig(
168
+ {
169
+ metadata: {},
170
+ spec: { clusters: ['c-m-abc123'] }
171
+ },
172
+ {
173
+ 'management/all': (type: string) => {
174
+ if (type === CAPI.RANCHER_CLUSTER) {
175
+ return [mockProvCluster];
176
+ }
177
+ if (type === MANAGEMENT.CLUSTER) {
178
+ return [mgmtClusterSameId];
179
+ }
180
+
181
+ return [];
182
+ }
183
+ }
184
+ );
185
+
186
+ expect(kubeconfig.referencedClusters).toStrictEqual([
187
+ {
188
+ label: 'my-cluster',
189
+ location: mockProvCluster.detailLocation
190
+ }
191
+ ]);
192
+ });
193
+ });
194
+
195
+ describe('sortedReferencedClusters', () => {
196
+ it('should sort existing clusters before deleted clusters', () => {
197
+ const existingCluster = {
198
+ mgmt: { id: 'c-m-exists' },
199
+ nameDisplay: 'existing-cluster',
200
+ detailLocation: { name: 'location' }
201
+ };
202
+
203
+ const kubeconfig = createKubeconfig(
204
+ {
205
+ metadata: {},
206
+ spec: { clusters: ['deleted-1', 'c-m-exists', 'deleted-2'] }
207
+ },
208
+ {
209
+ 'management/all': (type: string) => {
210
+ if (type === CAPI.RANCHER_CLUSTER) {
211
+ return [existingCluster];
212
+ }
213
+
214
+ return [];
215
+ }
216
+ }
217
+ );
218
+
219
+ const sorted = kubeconfig.sortedReferencedClusters;
220
+
221
+ expect(sorted[0].label).toBe('existing-cluster');
222
+ expect(sorted[0].location).not.toBeNull();
223
+ expect(sorted[1].location).toBeNull();
224
+ expect(sorted[2].location).toBeNull();
225
+ });
226
+
227
+ it('should sort existing clusters alphabetically', () => {
228
+ const clusters = [
229
+ {
230
+ mgmt: { id: 'c-m-zebra' }, nameDisplay: 'zebra', detailLocation: { name: 'z' }
231
+ },
232
+ {
233
+ mgmt: { id: 'c-m-alpha' }, nameDisplay: 'alpha', detailLocation: { name: 'a' }
234
+ },
235
+ {
236
+ mgmt: { id: 'c-m-beta' }, nameDisplay: 'beta', detailLocation: { name: 'b' }
237
+ }
238
+ ];
239
+
240
+ const kubeconfig = createKubeconfig(
241
+ {
242
+ metadata: {},
243
+ spec: { clusters: ['c-m-zebra', 'c-m-alpha', 'c-m-beta'] }
244
+ },
245
+ {
246
+ 'management/all': (type: string) => {
247
+ if (type === CAPI.RANCHER_CLUSTER) {
248
+ return clusters;
249
+ }
250
+
251
+ return [];
252
+ }
253
+ }
254
+ );
255
+
256
+ const sorted = kubeconfig.sortedReferencedClusters;
257
+
258
+ expect(sorted.map((c) => c.label)).toStrictEqual(['alpha', 'beta', 'zebra']);
259
+ });
260
+
261
+ it('should sort numerically when names contain numbers', () => {
262
+ const clusters = [
263
+ {
264
+ mgmt: { id: 'c-m-2' }, nameDisplay: 'cluster2', detailLocation: { name: 'c2' }
265
+ },
266
+ {
267
+ mgmt: { id: 'c-m-10' }, nameDisplay: 'cluster10', detailLocation: { name: 'c10' }
268
+ },
269
+ {
270
+ mgmt: { id: 'c-m-1' }, nameDisplay: 'cluster1', detailLocation: { name: 'c1' }
271
+ }
272
+ ];
273
+
274
+ const kubeconfig = createKubeconfig(
275
+ {
276
+ metadata: {},
277
+ spec: { clusters: ['c-m-2', 'c-m-10', 'c-m-1'] }
278
+ },
279
+ {
280
+ 'management/all': (type: string) => {
281
+ if (type === CAPI.RANCHER_CLUSTER) {
282
+ return clusters;
283
+ }
284
+
285
+ return [];
286
+ }
287
+ }
288
+ );
289
+
290
+ const sorted = kubeconfig.sortedReferencedClusters;
291
+
292
+ expect(sorted.map((c) => c.label)).toStrictEqual(['cluster1', 'cluster2', 'cluster10']);
293
+ });
294
+ });
295
+
296
+ describe('referencedClustersSortable', () => {
297
+ it('should return comma-separated lowercase labels', () => {
298
+ const clusters = [
299
+ {
300
+ mgmt: { id: 'c-m-1' }, nameDisplay: 'Alpha', detailLocation: { name: 'a' }
301
+ },
302
+ {
303
+ mgmt: { id: 'c-m-2' }, nameDisplay: 'Beta', detailLocation: { name: 'b' }
304
+ }
305
+ ];
306
+
307
+ const kubeconfig = createKubeconfig(
308
+ {
309
+ metadata: {},
310
+ spec: { clusters: ['c-m-1', 'c-m-2'] }
311
+ },
312
+ {
313
+ 'management/all': (type: string) => {
314
+ if (type === CAPI.RANCHER_CLUSTER) {
315
+ return clusters;
316
+ }
317
+
318
+ return [];
319
+ }
320
+ }
321
+ );
322
+
323
+ expect(kubeconfig.referencedClustersSortable).toBe('alpha,beta');
324
+ });
325
+
326
+ it('should return empty string when no clusters', () => {
327
+ const kubeconfig = createKubeconfig({
328
+ metadata: {},
329
+ spec: {}
330
+ });
331
+
332
+ expect(kubeconfig.referencedClustersSortable).toBe('');
333
+ });
334
+ });
335
+
336
+ describe('_availableActions', () => {
337
+ it('should filter out goToEdit, goToEditYaml, cloneYaml, and download actions', () => {
338
+ const kubeconfig = createKubeconfig({
339
+ metadata: {},
340
+ spec: {}
341
+ });
342
+
343
+ const mockActions = [
344
+ { action: 'goToClone' },
345
+ { action: 'divider' },
346
+ { action: 'goToEdit' },
347
+ { action: 'goToEditYaml' },
348
+ { action: 'cloneYaml' },
349
+ { action: 'download' },
350
+ { action: 'promptRemove' }
351
+ ];
352
+
353
+ jest.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(kubeconfig)), '_availableActions', 'get')
354
+ .mockReturnValue(mockActions);
355
+
356
+ const actions = kubeconfig._availableActions;
357
+
358
+ expect(actions).toStrictEqual([
359
+ { action: 'goToClone' },
360
+ { action: 'promptRemove' }
361
+ ]);
362
+ });
363
+ });
364
+ });
@@ -1,7 +1,62 @@
1
1
  import Secret from '@shell/models/secret';
2
2
  import { SECRET_TYPES as TYPES } from '@shell/config/secret';
3
+ import { VIRTUAL_TYPES } from '@shell/config/types';
4
+ import { UI_PROJECT_SECRET } from '@shell/config/labels-annotations';
3
5
 
4
6
  describe('class Secret', () => {
7
+ describe('detailLocation', () => {
8
+ it('should return correct route for project scoped secret', () => {
9
+ const secret = new Secret({
10
+ metadata: {
11
+ namespace: 'c-cluster-p-project',
12
+ labels: { [UI_PROJECT_SECRET]: 'p-project' }
13
+ },
14
+ id: 'c-cluster-p-project/my-secret'
15
+ });
16
+
17
+ // Mock $rootGetters
18
+ Object.defineProperty(secret, '$rootGetters', {
19
+ value: {
20
+ productId: 'explorer',
21
+ clusterId: 'c-cluster',
22
+ isRancher: true
23
+ }
24
+ });
25
+
26
+ const location = secret.detailLocation;
27
+
28
+ expect(location.name).toBe(`c-cluster-product-${ VIRTUAL_TYPES.PROJECT_SECRETS }-namespace-id`);
29
+ expect(location.params.resource).toBe(VIRTUAL_TYPES.PROJECT_SECRETS);
30
+ expect(location.params.product).toBe('explorer');
31
+ expect(location.params.cluster).toBe('c-cluster');
32
+ expect(location.params.namespace).toBe('c-cluster-p-project');
33
+ expect(location.params.id).toBe('my-secret');
34
+ });
35
+
36
+ it('should return default detailLocation for non-project scoped secret', () => {
37
+ const secret = new Secret({
38
+ metadata: { namespace: 'default' },
39
+ id: 'default/my-secret'
40
+ });
41
+
42
+ // Mock $rootGetters
43
+ Object.defineProperty(secret, '$rootGetters', {
44
+ value: {
45
+ productId: 'explorer',
46
+ clusterId: 'c-cluster',
47
+ isRancher: true
48
+ }
49
+ });
50
+
51
+ const expectedLocation = { name: 'some-route' };
52
+
53
+ // Mock _detailLocation (the parent class implementation or default behavior)
54
+ Object.defineProperty(secret, '_detailLocation', { value: expectedLocation });
55
+
56
+ expect(secret.detailLocation).toBe(expectedLocation);
57
+ });
58
+ });
59
+
5
60
  describe('cleanForDownload', () => {
6
61
  it('should contains the type attribute if cleanForDownload', async() => {
7
62
  const secret = new Secret({});
@@ -253,6 +253,8 @@ describe('class: Workload', () => {
253
253
  });
254
254
 
255
255
  describe('getter: podsCard', () => {
256
+ const mockPod = { metadata: { name: 'pod-1', namespace: 'default' } };
257
+
256
258
  it('should return card for Deployment type', () => {
257
259
  const workload = new Workload({
258
260
  type: WORKLOAD_TYPES.DEPLOYMENT,
@@ -264,7 +266,7 @@ describe('class: Workload', () => {
264
266
  rootGetters: { 'i18n/t': (key: string) => key },
265
267
  });
266
268
 
267
- Object.defineProperty(workload, 'pods', { get: () => [] });
269
+ Object.defineProperty(workload, 'pods', { get: () => [mockPod] });
268
270
  Object.defineProperty(workload, 'canUpdate', { get: () => true });
269
271
 
270
272
  const card = workload.podsCard;
@@ -285,7 +287,7 @@ describe('class: Workload', () => {
285
287
  rootGetters: { 'i18n/t': (key: string) => key },
286
288
  });
287
289
 
288
- Object.defineProperty(workload, 'pods', { get: () => [] });
290
+ Object.defineProperty(workload, 'pods', { get: () => [mockPod] });
289
291
  Object.defineProperty(workload, 'canUpdate', { get: () => true });
290
292
 
291
293
  const card = workload.podsCard;
@@ -310,7 +312,7 @@ describe('class: Workload', () => {
310
312
  expect(card).toBeNull();
311
313
  });
312
314
 
313
- it('should hide scaling when canUpdate is false', () => {
315
+ it('should return null when pods array is empty', () => {
314
316
  const workload = new Workload({
315
317
  type: WORKLOAD_TYPES.DEPLOYMENT,
316
318
  metadata: { name: 'test', namespace: 'default' },
@@ -322,6 +324,24 @@ describe('class: Workload', () => {
322
324
  });
323
325
 
324
326
  Object.defineProperty(workload, 'pods', { get: () => [] });
327
+
328
+ const card = workload.podsCard;
329
+
330
+ expect(card).toBeNull();
331
+ });
332
+
333
+ it('should hide scaling when canUpdate is false', () => {
334
+ const workload = new Workload({
335
+ type: WORKLOAD_TYPES.DEPLOYMENT,
336
+ metadata: { name: 'test', namespace: 'default' },
337
+ spec: {}
338
+ }, {
339
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
340
+ dispatch: jest.fn(),
341
+ rootGetters: { 'i18n/t': (key: string) => key },
342
+ });
343
+
344
+ Object.defineProperty(workload, 'pods', { get: () => [mockPod] });
325
345
  Object.defineProperty(workload, 'canUpdate', { get: () => false });
326
346
 
327
347
  const card = workload.podsCard;
@@ -331,6 +351,8 @@ describe('class: Workload', () => {
331
351
  });
332
352
 
333
353
  describe('getter: jobsCard', () => {
354
+ const mockJob = { metadata: { name: 'job-1', namespace: 'default' } };
355
+
334
356
  it('should return card for CronJob type', () => {
335
357
  const workload = new Workload({
336
358
  type: WORKLOAD_TYPES.CRON_JOB,
@@ -342,7 +364,7 @@ describe('class: Workload', () => {
342
364
  rootGetters: { 'i18n/t': (key: string) => key },
343
365
  });
344
366
 
345
- Object.defineProperty(workload, 'jobs', { get: () => [] });
367
+ Object.defineProperty(workload, 'jobs', { get: () => [mockJob] });
346
368
 
347
369
  const card = workload.jobsCard;
348
370
 
@@ -366,9 +388,30 @@ describe('class: Workload', () => {
366
388
 
367
389
  expect(card).toBeNull();
368
390
  });
391
+
392
+ it('should return null when jobs array is empty', () => {
393
+ const workload = new Workload({
394
+ type: WORKLOAD_TYPES.CRON_JOB,
395
+ metadata: { name: 'test', namespace: 'default' },
396
+ spec: {}
397
+ }, {
398
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
399
+ dispatch: jest.fn(),
400
+ rootGetters: { 'i18n/t': (key: string) => key },
401
+ });
402
+
403
+ Object.defineProperty(workload, 'jobs', { get: () => [] });
404
+
405
+ const card = workload.jobsCard;
406
+
407
+ expect(card).toBeNull();
408
+ });
369
409
  });
370
410
 
371
411
  describe('getter: cards', () => {
412
+ const mockPod = { metadata: { name: 'pod-1', namespace: 'default' } };
413
+ const mockJob = { metadata: { name: 'job-1', namespace: 'default' } };
414
+
372
415
  it('should include podsCard for Deployment', () => {
373
416
  const workload = new Workload({
374
417
  type: WORKLOAD_TYPES.DEPLOYMENT,
@@ -384,7 +427,7 @@ describe('class: Workload', () => {
384
427
  },
385
428
  });
386
429
 
387
- Object.defineProperty(workload, 'pods', { get: () => [] });
430
+ Object.defineProperty(workload, 'pods', { get: () => [mockPod] });
388
431
  Object.defineProperty(workload, 'canUpdate', { get: () => true });
389
432
 
390
433
  const cards = workload.cards;
@@ -411,7 +454,7 @@ describe('class: Workload', () => {
411
454
  },
412
455
  });
413
456
 
414
- Object.defineProperty(workload, 'jobs', { get: () => [] });
457
+ Object.defineProperty(workload, 'jobs', { get: () => [mockJob] });
415
458
 
416
459
  const cards = workload.cards;
417
460
  const nonNullCards = cards.filter((c: any) => c !== null);
@@ -0,0 +1,46 @@
1
+ import { insertAt } from '@shell/utils/array';
2
+ import SteveModel from '@shell/plugins/steve/steve-class';
3
+
4
+ export default class AuditPolicy extends SteveModel {
5
+ get _availableActions() {
6
+ const out = super._availableActions;
7
+
8
+ insertAt(out, 0, {
9
+ action: 'enable',
10
+ label: this.t('action.enable'),
11
+ icon: 'icon icon-play',
12
+ enabled: (this.canEdit || this.canEditYaml) && !this.spec.enabled,
13
+ bulkable: true,
14
+ weight: 2,
15
+ });
16
+ insertAt(out, 0, {
17
+ action: 'disable',
18
+ label: this.t('action.disable'),
19
+ icon: 'icon icon-pause',
20
+ enabled: (this.canEdit || this.canEditYaml) && this.spec.enabled,
21
+ bulkable: true,
22
+ weight: 1,
23
+ });
24
+
25
+ return out;
26
+ }
27
+
28
+ enable() {
29
+ this.enableOrDisable('enable');
30
+ }
31
+
32
+ disable() {
33
+ this.enableOrDisable('disable');
34
+ }
35
+
36
+ async enableOrDisable(flag) {
37
+ const clone = await this.$dispatch('rancher/clone', { resource: this }, { root: true });
38
+
39
+ clone.spec.enabled = flag === 'enable';
40
+ await clone.save().catch((err) => {
41
+ this.$dispatch('growl/fromError', {
42
+ title: this.t('auditPolicy.error.enableOrDisable', { flag, id: this.id }), err, timeout: 5000
43
+ }, { root: true });
44
+ });
45
+ }
46
+ }
@@ -138,7 +138,7 @@ export default class CapiMachine extends SteveModel {
138
138
 
139
139
  async machineRef() {
140
140
  const ref = this.spec.infrastructureRef;
141
- const id = `${ ref.namespace }/${ ref.name }`;
141
+ const id = `${ this.metadata.namespace }/${ ref.name }`;
142
142
  const kind = `rke-machine.cattle.io.${ ref.kind.toLowerCase() }`;
143
143
 
144
144
  return await this.$dispatch('find', { type: kind, id });
@@ -41,12 +41,12 @@ export default class CapiMachineDeployment extends SteveModel {
41
41
  }
42
42
 
43
43
  get templateType() {
44
- return this.spec.template.spec.infrastructureRef.kind ? `rke-machine.cattle.io.${ this.spec.template.spec.infrastructureRef.kind.toLowerCase() }` : null;
44
+ return this.infrastructureRefKind ? `rke-machine.cattle.io.${ this.infrastructureRefKind.toLowerCase() }` : null;
45
45
  }
46
46
 
47
47
  get template() {
48
48
  const ref = this.spec.template.spec.infrastructureRef;
49
- const id = `${ ref.namespace }/${ ref.name }`;
49
+ const id = `${ this.metadata.namespace }/${ ref.name }`;
50
50
  const template = this.$rootGetters['management/byId'](this.templateType, id);
51
51
 
52
52
  return template;
@@ -92,15 +92,15 @@ export default class CapiMachineDeployment extends SteveModel {
92
92
  }
93
93
 
94
94
  get outdated() {
95
- return Math.max(0, (this.status?.replicas || 0) - (this.status?.updatedReplicas || 0));
95
+ return Math.max(0, (this.status?.replicas || 0) - (this.status?.upToDateReplicas || 0));
96
96
  }
97
97
 
98
98
  get ready() {
99
- return Math.max(0, (this.status?.replicas || 0) - (this.status?.unavailableReplicas || 0));
99
+ return this.status?.availableReplicas || 0;
100
100
  }
101
101
 
102
102
  get unavailable() {
103
- return this.status?.unavailableReplicas || 0;
103
+ return Math.max(0, (this.status?.replicas || 0) - (this.status?.availableReplicas || 0));
104
104
  }
105
105
 
106
106
  get isControlPlane() {
package/models/event.js CHANGED
@@ -38,4 +38,9 @@ export default class K8sEvent extends SteveModel {
38
38
 
39
39
  return schema && rowValueGetter ? rowValueGetter(schema, 'Last Seen')(this) : null;
40
40
  }
41
+
42
+ // Because we're using eventType which is a non-standard state we don't have a reliable way to provide a state color anymore and have therefore disabled the color.
43
+ get insightsColor() {
44
+ return 'disabled';
45
+ }
41
46
  }
@@ -0,0 +1,15 @@
1
+ import SteveModel from '@shell/plugins/steve/steve-class';
2
+
3
+ export default class GroupMembershipRefreshRequest extends SteveModel {
4
+ get canRefreshMemberships() {
5
+ return this.schema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
6
+ }
7
+
8
+ cleanForSave(data) {
9
+ const val = super.cleanForSave(data);
10
+
11
+ delete val.type;
12
+
13
+ return val;
14
+ }
15
+ }