@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,125 @@
|
|
|
1
|
+
import { mount, RouterLinkStub } from '@vue/test-utils';
|
|
2
|
+
import KubeconfigClusters from '@shell/components/formatter/KubeconfigClusters.vue';
|
|
3
|
+
|
|
4
|
+
describe('component: KubeconfigClusters', () => {
|
|
5
|
+
const MAX_DISPLAY = 25;
|
|
6
|
+
|
|
7
|
+
const createCluster = (label: string, hasLocation = true) => ({
|
|
8
|
+
label,
|
|
9
|
+
location: hasLocation ? { name: 'cluster-detail', params: { cluster: label } } : null
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const createClusters = (count: number, hasLocation = true) => {
|
|
13
|
+
return Array.from({ length: count }, (_, i) => createCluster(`cluster-${ i + 1 }`, hasLocation));
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const defaultMocks = { t: (key: string, args: Record<string, unknown>) => `+ ${ args.remainingCount } more` };
|
|
17
|
+
|
|
18
|
+
const mountComponent = (clusters: unknown[] = [], mocks = defaultMocks) => {
|
|
19
|
+
return mount(KubeconfigClusters, {
|
|
20
|
+
props: { row: { id: 'test-row', sortedReferencedClusters: clusters } },
|
|
21
|
+
global: {
|
|
22
|
+
mocks,
|
|
23
|
+
stubs: { 'router-link': RouterLinkStub }
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe('displaying clusters', () => {
|
|
29
|
+
it('should display a dash when there are no clusters', () => {
|
|
30
|
+
const wrapper = mountComponent([]);
|
|
31
|
+
const emptySpan = wrapper.find('.text-muted');
|
|
32
|
+
|
|
33
|
+
expect(emptySpan.text()).toBe('—');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should display cluster labels with router-links when clusters have locations', () => {
|
|
37
|
+
const clusters = [createCluster('local'), createCluster('downstream')];
|
|
38
|
+
const wrapper = mountComponent(clusters);
|
|
39
|
+
const links = wrapper.findAllComponents(RouterLinkStub);
|
|
40
|
+
|
|
41
|
+
expect(links).toHaveLength(2);
|
|
42
|
+
expect(links[0].text()).toBe('local');
|
|
43
|
+
expect(links[1].text()).toBe('downstream');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should display cluster labels as text-muted spans when clusters have no location', () => {
|
|
47
|
+
const clusters = [createCluster('deleted-cluster', false)];
|
|
48
|
+
const wrapper = mountComponent(clusters);
|
|
49
|
+
const mutedSpan = wrapper.find('.text-muted');
|
|
50
|
+
|
|
51
|
+
expect(mutedSpan.text()).toBe('deleted-cluster');
|
|
52
|
+
expect(wrapper.findComponent(RouterLinkStub).exists()).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should separate clusters with commas', () => {
|
|
56
|
+
const clusters = [createCluster('cluster-1'), createCluster('cluster-2')];
|
|
57
|
+
const wrapper = mountComponent(clusters);
|
|
58
|
+
|
|
59
|
+
expect(wrapper.text()).toContain(',');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('max display limit', () => {
|
|
64
|
+
it('should display all clusters when count is at or below the limit', () => {
|
|
65
|
+
const clusters = createClusters(MAX_DISPLAY);
|
|
66
|
+
const wrapper = mountComponent(clusters);
|
|
67
|
+
const links = wrapper.findAllComponents(RouterLinkStub);
|
|
68
|
+
|
|
69
|
+
expect(links).toHaveLength(MAX_DISPLAY);
|
|
70
|
+
expect(wrapper.text()).not.toContain('more');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should limit displayed clusters to MAX_DISPLAY', () => {
|
|
74
|
+
const clusters = createClusters(MAX_DISPLAY + 10);
|
|
75
|
+
const wrapper = mountComponent(clusters);
|
|
76
|
+
const links = wrapper.findAllComponents(RouterLinkStub);
|
|
77
|
+
|
|
78
|
+
expect(links).toHaveLength(MAX_DISPLAY);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should show remaining count when clusters exceed the limit', () => {
|
|
82
|
+
const totalClusters = MAX_DISPLAY + 5;
|
|
83
|
+
const clusters = createClusters(totalClusters);
|
|
84
|
+
const wrapper = mountComponent(clusters);
|
|
85
|
+
|
|
86
|
+
expect(wrapper.text()).toContain('+ 5 more');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should show correct remaining count for large cluster lists', () => {
|
|
90
|
+
const totalClusters = MAX_DISPLAY + 100;
|
|
91
|
+
const clusters = createClusters(totalClusters);
|
|
92
|
+
const wrapper = mountComponent(clusters);
|
|
93
|
+
|
|
94
|
+
expect(wrapper.text()).toContain('+ 100 more');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('computed properties', () => {
|
|
99
|
+
it('should return empty array for allClusters when row has no sortedReferencedClusters', () => {
|
|
100
|
+
const wrapper = mount(KubeconfigClusters, {
|
|
101
|
+
props: { row: { id: 'test-row' } },
|
|
102
|
+
global: {
|
|
103
|
+
mocks: defaultMocks,
|
|
104
|
+
stubs: { 'router-link': RouterLinkStub }
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(wrapper.vm.allClusters).toStrictEqual([]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should calculate remainingCount as 0 when clusters are at or below limit', () => {
|
|
112
|
+
const clusters = createClusters(MAX_DISPLAY);
|
|
113
|
+
const wrapper = mountComponent(clusters);
|
|
114
|
+
|
|
115
|
+
expect(wrapper.vm.remainingCount).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should calculate correct remainingCount when clusters exceed limit', () => {
|
|
119
|
+
const clusters = createClusters(MAX_DISPLAY + 15);
|
|
120
|
+
const wrapper = mountComponent(clusters);
|
|
121
|
+
|
|
122
|
+
expect(wrapper.vm.remainingCount).toBe(15);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { CAPI, MANAGEMENT } from '@shell/config/types';
|
|
1
|
+
import { CAPI, MANAGEMENT, SAVED_COUNTS } from '@shell/config/types';
|
|
2
2
|
import { STORE } from '@shell/store/store-types';
|
|
3
|
+
import { ActionFindPageArgs } from '@shell/types/store/dashboard-store.types';
|
|
3
4
|
import { PaginationParam, PaginationParamFilter, PaginationSort } from '@shell/types/store/pagination.types';
|
|
4
5
|
import { VuexStore } from '@shell/types/store/vuex';
|
|
5
6
|
import { filterHiddenLocalCluster, filterOnlyKubernetesClusters, paginationFilterClusters } from '@shell/utils/cluster';
|
|
@@ -100,6 +101,8 @@ export interface TopLevelMenuHelper {
|
|
|
100
101
|
* Cleanup on destroy of TopLevelMenu
|
|
101
102
|
*/
|
|
102
103
|
destroy: () => Promise<void>;
|
|
104
|
+
|
|
105
|
+
updateCount: (count: number) => Promise<void>;
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
export abstract class BaseTopLevelMenuHelper {
|
|
@@ -172,6 +175,8 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
172
175
|
private clustersOthersWrapper: PaginationWrapper<any>;
|
|
173
176
|
private provClusterWrapper: PaginationWrapper<any>;
|
|
174
177
|
|
|
178
|
+
private clusterCount = 0;
|
|
179
|
+
|
|
175
180
|
constructor({ $store }: {
|
|
176
181
|
$store: VuexStore,
|
|
177
182
|
}) {
|
|
@@ -216,7 +221,7 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
216
221
|
context: 'side-bar',
|
|
217
222
|
}
|
|
218
223
|
},
|
|
219
|
-
formatResponse: { classify: true }
|
|
224
|
+
formatResponse: { classify: true },
|
|
220
225
|
});
|
|
221
226
|
// Fetch all prov clusters for the mgmt clusters we have
|
|
222
227
|
this.provClusterWrapper = new PaginationWrapper({
|
|
@@ -400,6 +405,47 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
|
|
|
400
405
|
.then((r) => r.data);
|
|
401
406
|
}
|
|
402
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Update the cluster count used when showing lists of home page + resource menu cluster count
|
|
410
|
+
*
|
|
411
|
+
* This is a convenient place to make the request
|
|
412
|
+
*/
|
|
413
|
+
public async updateCount(count: number) {
|
|
414
|
+
if (count === this.clusterCount) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
this.clusterCount = count;
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
const commonClusterFilters = paginationFilterClusters({ getters: this.$store.getters });
|
|
422
|
+
|
|
423
|
+
if (commonClusterFilters.length === 0) {
|
|
424
|
+
// We're not filtering out harvester clusters or local cluster, so no need to tweak the saved count for clusters
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const args:ActionFindPageArgs = {
|
|
429
|
+
pagination: {
|
|
430
|
+
filters: commonClusterFilters,
|
|
431
|
+
page: 1,
|
|
432
|
+
pageSize: 1,
|
|
433
|
+
sort: [],
|
|
434
|
+
projectsOrNamespaces: [],
|
|
435
|
+
},
|
|
436
|
+
transient: true,
|
|
437
|
+
saveCountAs: SAVED_COUNTS.K8S_CLUSTERS
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
await this.$store.dispatch('management/findPage', {
|
|
441
|
+
type: MANAGEMENT.CLUSTER,
|
|
442
|
+
opt: args
|
|
443
|
+
});
|
|
444
|
+
} catch (err) {
|
|
445
|
+
console.warn('Unable to set saved count for clusters', err); // eslint-disable-line no-console
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
403
449
|
/**
|
|
404
450
|
* Find all provisioning clusters associated with the displayed mgmt clusters
|
|
405
451
|
*/
|
|
@@ -595,6 +641,8 @@ export class TopLevelMenuHelperLegacy extends BaseTopLevelMenuHelper implements
|
|
|
595
641
|
|
|
596
642
|
return sorted;
|
|
597
643
|
}
|
|
644
|
+
|
|
645
|
+
public async updateCount(count: number) {}
|
|
598
646
|
}
|
|
599
647
|
|
|
600
648
|
/**
|
|
@@ -270,6 +270,12 @@ export default {
|
|
|
270
270
|
const value = hideLocalSetting.value || hideLocalSetting.default || 'false';
|
|
271
271
|
|
|
272
272
|
return value === 'true';
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
clusterCountsFromCounts() {
|
|
276
|
+
const counts = this.$store.getters[`management/all`](COUNT)?.[0]?.counts || {};
|
|
277
|
+
|
|
278
|
+
return counts[CAPI.RANCHER_CLUSTER]?.summary.count;
|
|
273
279
|
}
|
|
274
280
|
},
|
|
275
281
|
|
|
@@ -331,7 +337,15 @@ export default {
|
|
|
331
337
|
|
|
332
338
|
hideLocalCluster() {
|
|
333
339
|
this.updateClusters(this.pinnedIds, 'slow');
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
clusterCountsFromCounts: {
|
|
343
|
+
async handler(neu, old) {
|
|
344
|
+
await this.helper.updateCount(neu);
|
|
345
|
+
},
|
|
346
|
+
immediate: true,
|
|
334
347
|
}
|
|
348
|
+
|
|
335
349
|
},
|
|
336
350
|
|
|
337
351
|
mounted() {
|
package/components/nav/Type.vue
CHANGED
|
@@ -58,6 +58,11 @@ export default {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
const inStore = this.$store.getters['currentStore'](this.type.name);
|
|
61
|
+
const typeOptions = this.$store.getters[`type-map/optionsFor`](this.type.name);
|
|
62
|
+
|
|
63
|
+
if (typeOptions?.custom?.countGetter && typeof typeOptions.custom?.countGetter === 'function') {
|
|
64
|
+
return typeOptions.custom.countGetter(this.$store.getters);
|
|
65
|
+
}
|
|
61
66
|
|
|
62
67
|
return this.$store.getters[`${ inStore }/count`]({ name: this.type.name });
|
|
63
68
|
},
|
|
@@ -595,7 +595,7 @@ describe('topLevelMenu', () => {
|
|
|
595
595
|
jest.spyOn(sideNavService, 'init').mockImplementation(() => {});
|
|
596
596
|
const updateSpy = jest.fn();
|
|
597
597
|
const mockHelper = {
|
|
598
|
-
update: updateSpy, clustersPinned: [], clustersOthers: []
|
|
598
|
+
update: updateSpy, clustersPinned: [], clustersOthers: [], updateCount: () => {}
|
|
599
599
|
};
|
|
600
600
|
|
|
601
601
|
jest.spyOn(sideNavService, 'helper', 'get').mockReturnValue(mockHelper as any);
|
|
@@ -624,7 +624,7 @@ describe('topLevelMenu', () => {
|
|
|
624
624
|
jest.spyOn(sideNavService, 'init').mockImplementation(() => {});
|
|
625
625
|
const updateSpy = jest.fn();
|
|
626
626
|
const mockHelper = {
|
|
627
|
-
update: updateSpy, clustersPinned: [], clustersOthers: []
|
|
627
|
+
update: updateSpy, clustersPinned: [], clustersOthers: [], updateCount: () => {}
|
|
628
628
|
};
|
|
629
629
|
|
|
630
630
|
jest.spyOn(sideNavService, 'helper', 'get').mockReturnValue(mockHelper as any);
|
|
@@ -653,7 +653,7 @@ describe('topLevelMenu', () => {
|
|
|
653
653
|
jest.spyOn(sideNavService, 'init').mockImplementation(() => {});
|
|
654
654
|
const updateSpy = jest.fn();
|
|
655
655
|
const mockHelper = {
|
|
656
|
-
update: updateSpy, clustersPinned: [], clustersOthers: []
|
|
656
|
+
update: updateSpy, clustersPinned: [], clustersOthers: [], updateCount: () => {}
|
|
657
657
|
};
|
|
658
658
|
|
|
659
659
|
jest.spyOn(sideNavService, 'helper', 'get').mockReturnValue(mockHelper as any);
|
|
@@ -21,8 +21,9 @@ describe('component: Type', () => {
|
|
|
21
21
|
const defaultCount = 1;
|
|
22
22
|
const storeMock = {
|
|
23
23
|
getters: {
|
|
24
|
-
currentStore:
|
|
25
|
-
'cluster/count':
|
|
24
|
+
currentStore: () => 'cluster',
|
|
25
|
+
'cluster/count': () => defaultCount,
|
|
26
|
+
'type-map/optionsFor': () => {},
|
|
26
27
|
}
|
|
27
28
|
};
|
|
28
29
|
const routerMock = {
|
|
@@ -449,8 +450,9 @@ describe('component: Type', () => {
|
|
|
449
450
|
|
|
450
451
|
$store: {
|
|
451
452
|
getters: {
|
|
452
|
-
currentStore:
|
|
453
|
-
'cluster/count':
|
|
453
|
+
currentStore: () => 'cluster',
|
|
454
|
+
'cluster/count': () => null,
|
|
455
|
+
'type-map/optionsFor': () => {},
|
|
454
456
|
}
|
|
455
457
|
},
|
|
456
458
|
$router: routerMock,
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
CONFIG_MAP,
|
|
4
4
|
EVENT,
|
|
5
5
|
NODE, SECRET, INGRESS,
|
|
6
|
-
WORKLOAD, WORKLOAD_TYPES, SERVICE, HPA, NETWORK_POLICY, PV, PVC, STORAGE_CLASS, POD, POD_DISRUPTION_BUDGET, LIMIT_RANGE, RESOURCE_QUOTA,
|
|
6
|
+
WORKLOAD, WORKLOAD_TYPES, SERVICE, HPA, NETWORK_POLICY, PV, PVC, STORAGE_CLASS, POD, POD_DISRUPTION_BUDGET, LIMIT_RANGE, RESOURCE_QUOTA, AUDIT_POLICY,
|
|
7
7
|
MANAGEMENT,
|
|
8
8
|
NAMESPACE,
|
|
9
9
|
NORMAN,
|
|
@@ -83,6 +83,7 @@ export function init(store) {
|
|
|
83
83
|
NETWORK_POLICY,
|
|
84
84
|
POD_DISRUPTION_BUDGET,
|
|
85
85
|
RESOURCE_QUOTA,
|
|
86
|
+
AUDIT_POLICY,
|
|
86
87
|
], 'policy');
|
|
87
88
|
|
|
88
89
|
basicType([
|
|
@@ -118,6 +119,7 @@ export function init(store) {
|
|
|
118
119
|
weightGroup('storage', 95, true);
|
|
119
120
|
weightGroup('policy', 94, true);
|
|
120
121
|
weightType(POD, -1, true);
|
|
122
|
+
weightType(AUDIT_POLICY, -1, true);
|
|
121
123
|
|
|
122
124
|
// here is where we define the usage of the WORKLOAD custom list view for
|
|
123
125
|
// all the workload types (ex: deployments, cron jobs, daemonsets, etc)
|
|
@@ -152,6 +154,7 @@ export function init(store) {
|
|
|
152
154
|
mapGroup('autoscaling', 'Autoscaling');
|
|
153
155
|
mapGroup('policy', 'Policy');
|
|
154
156
|
mapGroup('networking.k8s.io', 'Networking');
|
|
157
|
+
mapGroup('auditlog.cattle.io', 'Policy');
|
|
155
158
|
mapGroup(/^(.+\.)?api(server)?.*\.k8s\.io$/, 'API');
|
|
156
159
|
mapGroup('rbac.authorization.k8s.io', 'RBAC');
|
|
157
160
|
mapGroup('admissionregistration.k8s.io', 'admission');
|
|
@@ -181,7 +184,6 @@ export function init(store) {
|
|
|
181
184
|
|
|
182
185
|
const dePaginateBindings = configureConditionalDepaginate({ maxResourceCount: 5000 });
|
|
183
186
|
const dePaginateNormanBindings = configureConditionalDepaginate({ maxResourceCount: 5000, isNorman: true });
|
|
184
|
-
const dePaginateNormanUsers = configureConditionalDepaginate({ maxResourceCount: 5000, isNorman: true });
|
|
185
187
|
|
|
186
188
|
configureType(NODE, { isCreatable: false, isEditable: true });
|
|
187
189
|
configureType(WORKLOAD_TYPES.JOB, { isEditable: false, match: WORKLOAD_TYPES.JOB });
|
|
@@ -190,7 +192,6 @@ export function init(store) {
|
|
|
190
192
|
configureType(MANAGEMENT.PROJECT, { displayName: store.getters['i18n/t']('namespace.project.label') });
|
|
191
193
|
configureType(NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING, { depaginate: dePaginateNormanBindings });
|
|
192
194
|
configureType(NORMAN.PROJECT_ROLE_TEMPLATE_BINDING, { depaginate: dePaginateNormanBindings });
|
|
193
|
-
configureType(NORMAN.USER, { depaginate: dePaginateNormanUsers });
|
|
194
195
|
configureType(SNAPSHOT, { depaginate: true });
|
|
195
196
|
|
|
196
197
|
configureType(SECRET, { showListMasthead: false });
|
|
@@ -2,16 +2,21 @@ import { AGE, NAME as NAME_COL, STATE } from '@shell/config/table-headers';
|
|
|
2
2
|
import {
|
|
3
3
|
CAPI,
|
|
4
4
|
CATALOG,
|
|
5
|
+
EXT,
|
|
6
|
+
COUNT,
|
|
5
7
|
NORMAN,
|
|
6
8
|
HCI,
|
|
7
9
|
MANAGEMENT,
|
|
8
10
|
SNAPSHOT,
|
|
9
11
|
VIRTUAL_TYPES,
|
|
10
|
-
HOSTED_PROVIDER
|
|
12
|
+
HOSTED_PROVIDER,
|
|
13
|
+
SAVED_COUNTS
|
|
11
14
|
} from '@shell/config/types';
|
|
12
15
|
import { MULTI_CLUSTER } from '@shell/store/features';
|
|
13
16
|
import { DSL } from '@shell/store/type-map';
|
|
14
17
|
import { BLANK_CLUSTER } from '@shell/store/store-types.js';
|
|
18
|
+
import { markRaw } from 'vue';
|
|
19
|
+
|
|
15
20
|
export const NAME = 'manager';
|
|
16
21
|
|
|
17
22
|
export function init(store) {
|
|
@@ -125,14 +130,17 @@ export function init(store) {
|
|
|
125
130
|
weightType(CAPI.MACHINE_DEPLOYMENT, 4, true);
|
|
126
131
|
weightType(CAPI.MACHINE_SET, 3, true);
|
|
127
132
|
weightType(CAPI.MACHINE, 2, true);
|
|
128
|
-
|
|
133
|
+
configureType(EXT.KUBECONFIG, { canYaml: false });
|
|
134
|
+
weightType(EXT.KUBECONFIG, 1, true);
|
|
135
|
+
weightType(CATALOG.CLUSTER_REPO, 0, true);
|
|
129
136
|
weightType(MANAGEMENT.PSA, 5, true);
|
|
130
|
-
weightType(VIRTUAL_TYPES.JWT_AUTHENTICATION,
|
|
137
|
+
weightType(VIRTUAL_TYPES.JWT_AUTHENTICATION, -1, true);
|
|
131
138
|
|
|
132
139
|
basicType([
|
|
133
140
|
CAPI.MACHINE_DEPLOYMENT,
|
|
134
141
|
CAPI.MACHINE_SET,
|
|
135
142
|
CAPI.MACHINE,
|
|
143
|
+
EXT.KUBECONFIG,
|
|
136
144
|
CATALOG.CLUSTER_REPO,
|
|
137
145
|
MANAGEMENT.PSA,
|
|
138
146
|
VIRTUAL_TYPES.JWT_AUTHENTICATION
|
|
@@ -192,4 +200,40 @@ export function init(store) {
|
|
|
192
200
|
MACHINE_SUMMARY,
|
|
193
201
|
AGE
|
|
194
202
|
]);
|
|
203
|
+
|
|
204
|
+
headers(EXT.KUBECONFIG, [
|
|
205
|
+
STATE,
|
|
206
|
+
{
|
|
207
|
+
name: 'clusters',
|
|
208
|
+
labelKey: 'tableHeaders.clusters',
|
|
209
|
+
value: 'spec.clusters',
|
|
210
|
+
sort: ['referencedClustersSortable'],
|
|
211
|
+
search: ['referencedClustersSortable'],
|
|
212
|
+
formatter: 'KubeconfigClusters',
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'ttl',
|
|
216
|
+
labelKey: 'tableHeaders.ttl',
|
|
217
|
+
value: 'expiresAt',
|
|
218
|
+
formatter: 'LiveDate',
|
|
219
|
+
formatterOpts: { isCountdown: true },
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
...AGE,
|
|
223
|
+
defaultSort: true,
|
|
224
|
+
},
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
// Configure custom count getter for cluster count (so we don't include Harvester clusters)
|
|
228
|
+
configureType(CAPI.RANCHER_CLUSTER, {
|
|
229
|
+
custom: {
|
|
230
|
+
countGetter: markRaw((getters) => {
|
|
231
|
+
const savedClusterCount = getters['management/getSavedCount'](SAVED_COUNTS.K8S_CLUSTERS);
|
|
232
|
+
const counts = getters[`management/all`](COUNT)?.[0]?.counts || {};
|
|
233
|
+
const clusterCount = counts[CAPI.RANCHER_CLUSTER]?.summary.count;
|
|
234
|
+
|
|
235
|
+
return savedClusterCount || clusterCount;
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
});
|
|
195
239
|
}
|
|
@@ -28,16 +28,16 @@ export function install(router, context) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Generate an object that includes both the
|
|
32
|
-
* @param {*}
|
|
31
|
+
* Generate an object that includes both the user and the me data
|
|
32
|
+
* @param {*} user User information
|
|
33
33
|
* @param {*} me Me user data
|
|
34
34
|
* @returns User info to be passed to `isLoggedIn`
|
|
35
35
|
*/
|
|
36
|
-
function getUserObject(
|
|
36
|
+
function getUserObject(user, me) {
|
|
37
37
|
return {
|
|
38
38
|
id: me.id,
|
|
39
39
|
me,
|
|
40
|
-
|
|
40
|
+
user,
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -59,10 +59,9 @@ export async function authenticate(to, from, next, { store }) {
|
|
|
59
59
|
if ( store.getters['auth/enabled'] !== false && !store.getters['auth/loggedIn'] ) {
|
|
60
60
|
// `await` so we have one successfully request whilst possibly logged in (ensures fromHeader is populated from `x-api-cattle-auth`)
|
|
61
61
|
await store.dispatch('auth/getUser');
|
|
62
|
+
const user = store.getters['auth/user'] || {};
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (v3User?.mustChangePassword) {
|
|
64
|
+
if (user?.mustChangePassword) {
|
|
66
65
|
return next({ name: 'auth-setup' });
|
|
67
66
|
}
|
|
68
67
|
|
|
@@ -75,7 +74,7 @@ export async function authenticate(to, from, next, { store }) {
|
|
|
75
74
|
} else if ( fromHeader === 'true' ) {
|
|
76
75
|
const me = await findMe(store);
|
|
77
76
|
|
|
78
|
-
await isLoggedIn(store, getUserObject(
|
|
77
|
+
await isLoggedIn(store, getUserObject(user, me));
|
|
79
78
|
handleOidcRedirectToCallbackUrl();
|
|
80
79
|
} else if ( fromHeader === 'false' ) {
|
|
81
80
|
notLoggedIn(store, next, to);
|
|
@@ -86,7 +85,7 @@ export async function authenticate(to, from, next, { store }) {
|
|
|
86
85
|
try {
|
|
87
86
|
const me = await findMe(store);
|
|
88
87
|
|
|
89
|
-
await isLoggedIn(store, getUserObject(
|
|
88
|
+
await isLoggedIn(store, getUserObject(user, me));
|
|
90
89
|
handleOidcRedirectToCallbackUrl();
|
|
91
90
|
} catch (e) {
|
|
92
91
|
const status = e?._status;
|
package/config/router/routes.js
CHANGED
|
@@ -514,7 +514,10 @@ export default [
|
|
|
514
514
|
name: 'c-cluster-product-resource-id',
|
|
515
515
|
meta: { asyncSetup: true }
|
|
516
516
|
}, {
|
|
517
|
-
path
|
|
517
|
+
// Used this regex syntax in order to strict match the 'projectsecret' path segment
|
|
518
|
+
// while simultaneously capturing it as the 'resource' parameter.
|
|
519
|
+
// This is required because the Side Navigation relies on route.params.resource to determine which menu item to highlight.
|
|
520
|
+
path: `/c/:cluster/:product/:resource(${ VIRTUAL_TYPES.PROJECT_SECRETS })/:namespace/:id`,
|
|
518
521
|
component: () => interopDefault(import(`@shell/pages/c/_cluster/explorer/${ VIRTUAL_TYPES.PROJECT_SECRETS }.vue`)),
|
|
519
522
|
name: `c-cluster-product-${ VIRTUAL_TYPES.PROJECT_SECRETS }-namespace-id`,
|
|
520
523
|
}, {
|
package/config/types.js
CHANGED
|
@@ -61,6 +61,7 @@ export const POD_DISRUPTION_BUDGET = 'policy.poddisruptionbudget';
|
|
|
61
61
|
export const PV = 'persistentvolume';
|
|
62
62
|
export const PVC = 'persistentvolumeclaim';
|
|
63
63
|
export const RESOURCE_QUOTA = 'resourcequota';
|
|
64
|
+
export const AUDIT_POLICY = 'auditlog.cattle.io.auditpolicy';
|
|
64
65
|
export const SCHEMA = 'schema';
|
|
65
66
|
export const SERVICE = 'service';
|
|
66
67
|
export const SECRET = 'secret';
|
|
@@ -263,8 +264,11 @@ export const BRAND = {
|
|
|
263
264
|
};
|
|
264
265
|
|
|
265
266
|
export const EXT = {
|
|
266
|
-
USER_ACTIVITY:
|
|
267
|
-
|
|
267
|
+
USER_ACTIVITY: 'ext.cattle.io.useractivity',
|
|
268
|
+
SELFUSER: 'ext.cattle.io.selfuser',
|
|
269
|
+
GROUP_MEMBERSHIP_REFRESH_REQUESTS: 'ext.cattle.io.groupmembershiprefreshrequest',
|
|
270
|
+
PASSWORD_CHANGE_REQUESTS: 'ext.cattle.io.passwordchangerequest',
|
|
271
|
+
KUBECONFIG: 'ext.cattle.io.kubeconfig',
|
|
268
272
|
};
|
|
269
273
|
|
|
270
274
|
export const CAPI = {
|
|
@@ -409,3 +413,7 @@ export const DEPRECATED = 'Deprecated';
|
|
|
409
413
|
export const EXPERIMENTAL = 'Experimental';
|
|
410
414
|
export const AUTOSCALER_CONFIG_MAP_ID = 'kube-system/cluster-autoscaler-status';
|
|
411
415
|
export const HOSTED_PROVIDER = 'hostedprovider';
|
|
416
|
+
|
|
417
|
+
// Named saved counts
|
|
418
|
+
|
|
419
|
+
export const SAVED_COUNTS = { K8S_CLUSTERS: 'k8sClusters' };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Shared AuditPolicy interface for all audit log policy components
|
|
3
|
+
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
4
|
+
import ResourceTabs from '@shell/components/form/ResourceTabs/index.vue';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
name: 'DetailAuditPolicy',
|
|
8
|
+
components: { ResourceTabs },
|
|
9
|
+
mixins: [CreateEditView],
|
|
10
|
+
};
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<Loading v-if="$fetchState.pending" />
|
|
15
|
+
<ResourceTabs
|
|
16
|
+
:mode="mode"
|
|
17
|
+
:value="value"
|
|
18
|
+
/>
|
|
19
|
+
</template>
|
|
@@ -3,7 +3,7 @@ import CreateEditView from '@shell/mixins/create-edit-view';
|
|
|
3
3
|
import Tab from '@shell/components/Tabbed/Tab';
|
|
4
4
|
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
|
5
5
|
import SortableTable from '@shell/components/SortableTable';
|
|
6
|
-
import { MANAGEMENT
|
|
6
|
+
import { MANAGEMENT } from '@shell/config/types';
|
|
7
7
|
import Loading from '@shell/components/Loading';
|
|
8
8
|
import { NAME } from '@shell/config/table-headers';
|
|
9
9
|
|
|
@@ -31,7 +31,6 @@ export default {
|
|
|
31
31
|
if (this.canSeeRoleTemplates) {
|
|
32
32
|
// Upfront fetch, avoid async computes
|
|
33
33
|
await Promise.all([
|
|
34
|
-
await this.$store.dispatch('rancher/find', { type: NORMAN.USER, id: this.value.id }),
|
|
35
34
|
await this.$store.dispatch('management/findAll', { type: MANAGEMENT.ROLE_TEMPLATE }),
|
|
36
35
|
await this.$store.dispatch('management/findAll', { type: MANAGEMENT.CLUSTER_ROLE_TEMPLATE_BINDING }),
|
|
37
36
|
await this.$store.dispatch('management/findAll', { type: MANAGEMENT.PROJECT_ROLE_TEMPLATE_BINDING })
|
package/detail/node.vue
CHANGED
|
@@ -364,7 +364,8 @@ export default {
|
|
|
364
364
|
const machineFullName = machineNameFn(this.value.name, mp.name);
|
|
365
365
|
|
|
366
366
|
const machines = this.value.machines.filter((machine) => {
|
|
367
|
-
|
|
367
|
+
// elemental machines use an older version of CAPI CRDs that use apiVersion instead of apiGroup
|
|
368
|
+
const isElementalCluster = (machine.spec?.infrastructureRef?.apiGroup || machine.spec?.infrastructureRef?.apiVersion).startsWith('elemental.cattle.io');
|
|
368
369
|
const machinePoolInfName = machine.spec?.infrastructureRef?.name;
|
|
369
370
|
|
|
370
371
|
if (isElementalCluster) {
|
|
@@ -9,6 +9,13 @@ export default {
|
|
|
9
9
|
components: {
|
|
10
10
|
Card, AsyncButton, ChangePassword
|
|
11
11
|
},
|
|
12
|
+
props: {
|
|
13
|
+
user: {
|
|
14
|
+
type: Object,
|
|
15
|
+
default: () => null,
|
|
16
|
+
required: true
|
|
17
|
+
}
|
|
18
|
+
},
|
|
12
19
|
data() {
|
|
13
20
|
return { valid: false, password: '' };
|
|
14
21
|
},
|
|
@@ -45,6 +52,7 @@ export default {
|
|
|
45
52
|
<form @submit.prevent>
|
|
46
53
|
<ChangePassword
|
|
47
54
|
ref="changePassword"
|
|
55
|
+
:user="user"
|
|
48
56
|
@valid="valid = $event"
|
|
49
57
|
/>
|
|
50
58
|
</form>
|
package/dialog/GenericPrompt.vue
CHANGED
|
@@ -38,9 +38,24 @@ export default {
|
|
|
38
38
|
type: Function,
|
|
39
39
|
default: () => { }
|
|
40
40
|
},
|
|
41
|
+
actionColor: {
|
|
42
|
+
type: String,
|
|
43
|
+
default: 'role-primary',
|
|
44
|
+
},
|
|
45
|
+
errors: {
|
|
46
|
+
type: Array,
|
|
47
|
+
default: () => []
|
|
48
|
+
}
|
|
41
49
|
},
|
|
50
|
+
|
|
42
51
|
data() {
|
|
43
|
-
return {
|
|
52
|
+
return { applyErrors: [] };
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
computed: {
|
|
56
|
+
allErrors() {
|
|
57
|
+
return [...this.errors, ...this.applyErrors];
|
|
58
|
+
}
|
|
44
59
|
},
|
|
45
60
|
|
|
46
61
|
methods: {
|
|
@@ -51,13 +66,14 @@ export default {
|
|
|
51
66
|
},
|
|
52
67
|
|
|
53
68
|
async apply(buttonDone) {
|
|
69
|
+
this.applyErrors = [];
|
|
54
70
|
try {
|
|
55
71
|
await this.applyAction(buttonDone);
|
|
56
72
|
this.confirm(true);
|
|
57
73
|
this.$emit('close');
|
|
58
74
|
} catch (err) {
|
|
59
75
|
console.error(err); // eslint-disable-line
|
|
60
|
-
this.
|
|
76
|
+
this.applyErrors = exceptionToErrorsArray(err);
|
|
61
77
|
buttonDone(false);
|
|
62
78
|
}
|
|
63
79
|
}
|
|
@@ -92,7 +108,7 @@ export default {
|
|
|
92
108
|
<template #actions>
|
|
93
109
|
<div class="bottom">
|
|
94
110
|
<Banner
|
|
95
|
-
v-for="(err, i) in
|
|
111
|
+
v-for="(err, i) in allErrors"
|
|
96
112
|
:key="i"
|
|
97
113
|
color="error"
|
|
98
114
|
:label="err"
|
|
@@ -107,6 +123,7 @@ export default {
|
|
|
107
123
|
|
|
108
124
|
<AsyncButton
|
|
109
125
|
:mode="applyMode"
|
|
126
|
+
:action-color="actionColor"
|
|
110
127
|
@click="apply"
|
|
111
128
|
/>
|
|
112
129
|
</div>
|