@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.
- package/assets/brand/suse/metadata.json +2 -1
- package/assets/translations/en-us.yaml +105 -5
- package/components/ActionMenuShell.vue +1 -1
- package/components/Inactivity.vue +2 -2
- package/components/Resource/Detail/Card/ExtrasCard.vue +49 -15
- package/components/Resource/Detail/Card/__tests__/ExtrasCard.test.ts +111 -0
- package/components/Resource/Detail/Masthead/__tests__/index.test.ts +0 -17
- package/components/Resource/Detail/Masthead/index.vue +11 -4
- package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +3 -1
- package/components/Resource/Detail/Metadata/index.vue +1 -1
- package/components/Resource/Detail/ResourceRow.vue +1 -1
- package/components/ResourceDetail/Masthead/latest.vue +12 -2
- package/components/ResourceList/index.vue +9 -0
- package/components/ResourceTable.vue +38 -4
- package/components/Tabbed/Tab.vue +4 -0
- package/components/Tabbed/index.vue +4 -1
- package/components/__tests__/ProjectRow.test.ts +60 -0
- package/components/form/ChangePassword.vue +41 -35
- package/components/form/ResourceQuota/Project.vue +42 -1
- package/components/form/ResourceQuota/ProjectRow.vue +71 -4
- package/components/form/ResourceQuota/__tests__/Project.test.ts +63 -0
- package/components/form/SelectOrCreateAuthSecret.vue +6 -1
- package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +35 -0
- package/components/formatter/KubeconfigClusters.vue +74 -0
- package/components/formatter/MachineSummaryGraph.vue +10 -2
- package/components/formatter/__tests__/KubeconfigClusters.test.ts +125 -0
- package/components/nav/TopLevelMenu.helper.ts +50 -2
- package/components/nav/TopLevelMenu.vue +14 -0
- package/components/nav/Type.vue +5 -0
- package/components/nav/__tests__/TopLevelMenu.test.ts +3 -3
- package/components/nav/__tests__/Type.test.ts +6 -4
- package/config/product/explorer.js +4 -3
- package/config/product/manager.js +47 -3
- package/config/router/navigation-guards/authentication.js +8 -9
- package/config/router/routes.js +4 -1
- package/config/types.js +10 -2
- package/detail/auditlog.cattle.io.auditpolicy.vue +19 -0
- package/detail/management.cattle.io.user.vue +1 -2
- package/detail/node.vue +0 -1
- package/detail/provisioning.cattle.io.cluster.vue +2 -1
- package/dialog/ChangePasswordDialog.vue +8 -0
- package/dialog/GenericPrompt.vue +20 -3
- package/dialog/ScaleMachineDownDialog.vue +65 -15
- package/dialog/SearchDialog.vue +10 -2
- package/dialog/__tests__/ScaleMachineDownDialog.test.ts +184 -0
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +89 -0
- package/edit/__tests__/management.cattle.io.project.test.js +56 -1
- package/edit/auditlog.cattle.io.auditpolicy/AdditionalRedactions.vue +114 -0
- package/edit/auditlog.cattle.io.auditpolicy/Filters.vue +119 -0
- package/edit/auditlog.cattle.io.auditpolicy/General.vue +180 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/AdditionalRedactions.test.ts +327 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/Filters.test.ts +449 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/General.test.ts +472 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/AdditionalRedactions.test.ts.snap +27 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/Filters.test.ts.snap +39 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +174 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +29 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/index.test.ts +215 -0
- package/edit/auditlog.cattle.io.auditpolicy/index.vue +104 -0
- package/edit/auditlog.cattle.io.auditpolicy/types.ts +28 -0
- package/edit/fleet.cattle.io.gitrepo.vue +16 -1
- package/edit/management.cattle.io.project.vue +8 -2
- package/edit/management.cattle.io.user.vue +29 -34
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +178 -0
- package/edit/provisioning.cattle.io.cluster/rke2.vue +22 -2
- package/edit/provisioning.cattle.io.cluster/shared.ts +4 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +57 -2
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/__tests__/S3Config.test.ts +109 -0
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +1 -0
- package/list/auditlog.cattle.io.auditpolicy.vue +63 -0
- package/list/ext.cattle.io.kubeconfig.vue +118 -0
- package/list/group.principal.vue +11 -15
- package/list/management.cattle.io.user.vue +11 -21
- package/machine-config/azure.vue +14 -0
- package/mixins/__tests__/chart.test.ts +147 -0
- package/mixins/browser-tab-visibility.js +5 -4
- package/mixins/chart.js +10 -8
- package/mixins/fetch.client.js +6 -0
- package/models/__tests__/auditlog.cattle.io.auditpolicy.test.ts +117 -0
- package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +364 -0
- package/models/__tests__/secret.test.ts +55 -0
- package/models/__tests__/workload.test.ts +49 -6
- package/models/auditlog.cattle.io.auditpolicy.js +46 -0
- package/models/cluster.x-k8s.io.machine.js +1 -1
- package/models/cluster.x-k8s.io.machinedeployment.js +5 -5
- package/models/event.js +5 -0
- package/models/ext.cattle.io.groupmembershiprefreshrequest.js +15 -0
- package/models/ext.cattle.io.kubeconfig.ts +97 -0
- package/models/ext.cattle.io.passwordchangerequest.js +15 -0
- package/models/ext.cattle.io.selfuser.js +15 -0
- package/models/fleet-application.js +17 -7
- package/models/management.cattle.io.user.js +28 -31
- package/models/schema.js +18 -0
- package/models/secret.js +28 -25
- package/models/steve-schema.ts +39 -2
- package/models/workload.js +3 -2
- package/package.json +2 -2
- package/pages/about.vue +3 -2
- package/pages/account/index.vue +23 -16
- package/pages/auth/login.vue +15 -8
- package/pages/auth/setup.vue +52 -15
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +38 -14
- package/pages/c/_cluster/apps/charts/index.vue +1 -0
- package/pages/home.vue +9 -3
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -3
- package/plugins/dashboard-store/actions.js +7 -0
- package/plugins/dashboard-store/getters.js +23 -1
- package/plugins/dashboard-store/index.js +3 -2
- package/plugins/dashboard-store/mutations.js +4 -0
- package/plugins/dashboard-store/resource-class.js +12 -5
- package/plugins/steve/__tests__/steve-class.test.ts +167 -0
- package/plugins/steve/schema.d.ts +5 -0
- package/plugins/steve/steve-class.js +19 -0
- package/plugins/steve/steve-pagination-utils.ts +2 -1
- package/rancher-components/RcItemCard/RcItemCard.test.ts +4 -2
- package/rancher-components/RcItemCard/RcItemCard.vue +27 -10
- package/store/auth.js +57 -19
- package/store/notifications.ts +1 -1
- package/store/type-map.js +12 -1
- package/types/shell/index.d.ts +24 -15
- package/types/store/dashboard-store.types.ts +7 -0
- package/utils/__tests__/chart.test.ts +96 -0
- package/utils/__tests__/version.test.ts +1 -19
- package/utils/chart.js +64 -0
- package/utils/pagination-wrapper.ts +11 -3
- package/utils/version.js +5 -17
- 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
|
|
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 = `${
|
|
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.
|
|
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 = `${
|
|
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?.
|
|
95
|
+
return Math.max(0, (this.status?.replicas || 0) - (this.status?.upToDateReplicas || 0));
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
get ready() {
|
|
99
|
-
return
|
|
99
|
+
return this.status?.availableReplicas || 0;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
get unavailable() {
|
|
103
|
-
return this.status?.
|
|
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
|
+
}
|