@rancher/shell 0.3.26 → 0.3.27
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/.DS_Store +0 -0
- package/assets/translations/en-us.yaml +4 -3
- package/assets/translations/zh-hans.yaml +2 -3
- package/components/AlertTable.vue +8 -6
- package/components/EmberPage.vue +2 -2
- package/components/EtcdInfoBanner.vue +12 -2
- package/components/GlobalRoleBindings.vue +10 -0
- package/components/GrafanaDashboard.vue +8 -3
- package/components/Wizard.vue +17 -1
- package/components/auth/RoleDetailEdit.vue +17 -1
- package/components/form/ArrayList.vue +20 -11
- package/components/form/__tests__/ArrayList.test.ts +44 -0
- package/components/nav/Header.vue +5 -4
- package/components/nav/TopLevelMenu.vue +38 -15
- package/components/nav/__tests__/TopLevelMenu.test.ts +120 -0
- package/components/nav/__tests__/Type.test.ts +139 -0
- package/config/private-label.js +1 -1
- package/config/settings.ts +0 -2
- package/core/types.ts +11 -4
- package/edit/provisioning.cattle.io.cluster/Basics.vue +13 -0
- package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +1 -1
- package/edit/workload/mixins/workload.js +14 -4
- package/models/fleet.cattle.io.cluster.js +11 -1
- package/package.json +1 -1
- package/pages/c/_cluster/auth/roles/index.vue +11 -1
- package/pages/c/_cluster/explorer/index.vue +7 -2
- package/pages/c/_cluster/monitoring/index.vue +26 -39
- package/pages/support/index.vue +1 -8
- package/promptRemove/management.cattle.io.project.vue +6 -9
- package/rancher-components/components/Form/Radio/RadioGroup.test.ts +30 -0
- package/rancher-components/components/Form/Radio/RadioGroup.vue +4 -0
- package/store/features.js +1 -0
- package/types/shell/index.d.ts +4 -1
- package/utils/__tests__/object.test.ts +67 -1
- package/utils/__tests__/version.test.ts +13 -23
- package/utils/cluster.js +1 -1
- package/utils/grafana.js +1 -2
- package/utils/monitoring.js +25 -1
- package/utils/object.js +4 -3
- package/utils/sort.js +1 -1
- package/utils/validators/formRules/index.ts +1 -1
- package/utils/validators/role-template.js +1 -1
- package/utils/version.js +0 -13
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { mount, Wrapper } from '@vue/test-utils';
|
|
2
|
+
import TopLevelMenu from '@shell/components/nav/TopLevelMenu';
|
|
3
|
+
|
|
4
|
+
// DISCLAIMER: This should not be added here, although we have several store requests which are irrelevant
|
|
5
|
+
const defaultStore = {
|
|
6
|
+
'management/byId': jest.fn(),
|
|
7
|
+
'management/schemaFor': jest.fn(),
|
|
8
|
+
'i18n/t': jest.fn(),
|
|
9
|
+
'features/get': jest.fn(),
|
|
10
|
+
'prefs/theme': jest.fn(),
|
|
11
|
+
defaultClusterId: jest.fn(),
|
|
12
|
+
clusterId: jest.fn(),
|
|
13
|
+
'type-map/activeProducts': [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('topLevelMenu', () => {
|
|
17
|
+
it('should display clusters', () => {
|
|
18
|
+
const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
|
|
19
|
+
mocks: {
|
|
20
|
+
$store: {
|
|
21
|
+
getters: {
|
|
22
|
+
'management/all': () => [{ name: 'whatever' }],
|
|
23
|
+
...defaultStore
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
stubs: ['BrandImage', 'nuxt-link']
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const cluster = wrapper.find('[data-testid="top-level-menu-cluster-0"]');
|
|
31
|
+
|
|
32
|
+
expect(cluster.exists()).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('searching a term', () => {
|
|
36
|
+
describe('should displays a no results message if have clusters but', () => {
|
|
37
|
+
it('given no matching clusters', () => {
|
|
38
|
+
const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
|
|
39
|
+
data: () => ({ clusterFilter: 'whatever' }),
|
|
40
|
+
mocks: {
|
|
41
|
+
$store: {
|
|
42
|
+
getters: {
|
|
43
|
+
'management/all': () => [{ nameDisplay: 'something else' }],
|
|
44
|
+
...defaultStore
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
stubs: ['BrandImage', 'nuxt-link']
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const noResults = wrapper.find('[data-testid="top-level-menu-no-results"]');
|
|
52
|
+
|
|
53
|
+
expect(noResults.exists()).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('given no matched pinned clusters', () => {
|
|
57
|
+
const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
|
|
58
|
+
data: () => ({ clusterFilter: 'whatever' }),
|
|
59
|
+
mocks: {
|
|
60
|
+
$store: {
|
|
61
|
+
getters: {
|
|
62
|
+
'management/all': () => [{ nameDisplay: 'something else', pinned: true }],
|
|
63
|
+
...defaultStore
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
stubs: ['BrandImage', 'nuxt-link']
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const noResults = wrapper.find('[data-testid="top-level-menu-no-results"]');
|
|
71
|
+
|
|
72
|
+
expect(noResults.exists()).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('should not displays a no results message', () => {
|
|
77
|
+
it('given matching clusters', () => {
|
|
78
|
+
const search = 'you found me';
|
|
79
|
+
const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
|
|
80
|
+
data: () => ({ clusterFilter: search }),
|
|
81
|
+
mocks: {
|
|
82
|
+
$store: {
|
|
83
|
+
getters: {
|
|
84
|
+
'management/all': () => [{ nameDisplay: search }],
|
|
85
|
+
...defaultStore
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
stubs: ['BrandImage', 'nuxt-link']
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const noResults = wrapper.find('[data-testid="top-level-menu-no-results"]');
|
|
93
|
+
|
|
94
|
+
expect(wrapper.vm.clustersFiltered).toHaveLength(1);
|
|
95
|
+
expect(noResults.exists()).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('given clusters with status pinned', () => {
|
|
99
|
+
const search = 'you found me';
|
|
100
|
+
const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
|
|
101
|
+
data: () => ({ clusterFilter: search }),
|
|
102
|
+
mocks: {
|
|
103
|
+
$store: {
|
|
104
|
+
getters: {
|
|
105
|
+
'management/all': () => [{ nameDisplay: search, pinned: true }],
|
|
106
|
+
...defaultStore
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
stubs: ['BrandImage', 'nuxt-link']
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const noResults = wrapper.find('[data-testid="top-level-menu-no-results"]');
|
|
114
|
+
|
|
115
|
+
expect(wrapper.vm.pinFiltered).toHaveLength(1);
|
|
116
|
+
expect(noResults.exists()).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { mount, RouterLinkStub } from '@vue/test-utils';
|
|
2
|
+
import Type from '@shell/components/nav/Type.vue';
|
|
3
|
+
|
|
4
|
+
// Mandatory to mock vue-router in this test
|
|
5
|
+
jest.mock('vue-router');
|
|
6
|
+
|
|
7
|
+
// Configuration text
|
|
8
|
+
const className = 'nuxt-link-active';
|
|
9
|
+
|
|
10
|
+
describe('component: Type', () => {
|
|
11
|
+
describe('should not use highlight class', () => {
|
|
12
|
+
it('given no hash', () => {
|
|
13
|
+
const wrapper = mount(Type, {
|
|
14
|
+
propsData: { type: { route: 'something else' } },
|
|
15
|
+
stubs: { nLink: RouterLinkStub },
|
|
16
|
+
mocks: {
|
|
17
|
+
$route: { path: 'whatever' },
|
|
18
|
+
$router: { resolve: () => ({ route: { path: 'whatever' } }) },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const highlight = wrapper.find(`.${ className }`);
|
|
23
|
+
|
|
24
|
+
expect(highlight.exists()).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('given no path', () => {
|
|
28
|
+
const wrapper = mount(Type, {
|
|
29
|
+
propsData: { type: { route: 'something else' } },
|
|
30
|
+
stubs: { nLink: RouterLinkStub },
|
|
31
|
+
mocks: {
|
|
32
|
+
$route: { hash: 'whatever' },
|
|
33
|
+
$router: { resolve: () => ({ route: { path: 'whatever' } }) },
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const highlight = wrapper.find(`.${ className }`);
|
|
38
|
+
|
|
39
|
+
expect(highlight.exists()).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('given no matching values', () => {
|
|
43
|
+
const wrapper = mount(Type, {
|
|
44
|
+
propsData: { type: {} },
|
|
45
|
+
stubs: { nLink: RouterLinkStub },
|
|
46
|
+
mocks: {
|
|
47
|
+
$route: {
|
|
48
|
+
hash: 'hash',
|
|
49
|
+
path: 'path',
|
|
50
|
+
},
|
|
51
|
+
$router: { resolve: () => ({ route: { path: 'whatever' } }) },
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const highlight = wrapper.find(`.${ className }`);
|
|
56
|
+
|
|
57
|
+
expect(highlight.exists()).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('given navigation path is bigger than current page route path', () => {
|
|
61
|
+
const wrapper = mount(Type, {
|
|
62
|
+
propsData: { type: { route: 'not empty' } },
|
|
63
|
+
stubs: { nLink: RouterLinkStub },
|
|
64
|
+
mocks: {
|
|
65
|
+
$route: {
|
|
66
|
+
hash: 'not empty',
|
|
67
|
+
path: 'whatever',
|
|
68
|
+
},
|
|
69
|
+
$router: { resolve: () => ({ route: { path: 'many/parts' } }) },
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const highlight = wrapper.find(`.${ className }`);
|
|
74
|
+
|
|
75
|
+
expect(highlight.exists()).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it.each([
|
|
79
|
+
// URL with fragments like anchors
|
|
80
|
+
[
|
|
81
|
+
'/c/c-m-hzqf4tqt/explorer/members#project-membership',
|
|
82
|
+
'/c/c-m-hzqf4tqt/explorer/members'
|
|
83
|
+
],
|
|
84
|
+
// Similar paths
|
|
85
|
+
[
|
|
86
|
+
'/c/c-m-hzqf4tqt/fleet/fleet.cattle.io.bundlenamespacemapping',
|
|
87
|
+
'/c/c-m-hzqf4tqt/fleet/fleet.cattle.io.bundle'
|
|
88
|
+
],
|
|
89
|
+
// paths with same parts, e.g. parents
|
|
90
|
+
[
|
|
91
|
+
'/c/c-m-hzqf4tqt/fleet',
|
|
92
|
+
'/c/c-m-hzqf4tqt/fleet/management.cattle.io.fleetworkspace'
|
|
93
|
+
],
|
|
94
|
+
])('given different current path %p and menu path %p', (currentPath, menuPath) => {
|
|
95
|
+
const wrapper = mount(Type, {
|
|
96
|
+
propsData: { type: { route: 'not empty' } },
|
|
97
|
+
stubs: { nLink: RouterLinkStub },
|
|
98
|
+
mocks: {
|
|
99
|
+
$route: {
|
|
100
|
+
hash: 'not empty',
|
|
101
|
+
path: currentPath,
|
|
102
|
+
},
|
|
103
|
+
$router: { resolve: () => ({ route: { path: menuPath } }) },
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const highlight = wrapper.find(`.${ className }`);
|
|
108
|
+
|
|
109
|
+
expect(highlight.exists()).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('should use highlight class', () => {
|
|
114
|
+
it.each([
|
|
115
|
+
[
|
|
116
|
+
'same',
|
|
117
|
+
'same'
|
|
118
|
+
],
|
|
119
|
+
])('given same current path %p and menu path %p (on first load)', (currentPath, menuPath) => {
|
|
120
|
+
const wrapper = mount(Type, {
|
|
121
|
+
propsData: { type: { route: 'not empty' } },
|
|
122
|
+
stubs: { nLink: RouterLinkStub },
|
|
123
|
+
mocks: {
|
|
124
|
+
$route: {
|
|
125
|
+
hash: 'not empty',
|
|
126
|
+
path: currentPath,
|
|
127
|
+
},
|
|
128
|
+
$router: { resolve: () => ({ route: { path: menuPath } }) },
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const highlight = wrapper.find(`.${ className }`);
|
|
133
|
+
|
|
134
|
+
expect(highlight.exists()).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
jest.restoreAllMocks();
|
package/config/private-label.js
CHANGED
|
@@ -3,7 +3,7 @@ import { SETTING } from './settings';
|
|
|
3
3
|
export const ANY = 0;
|
|
4
4
|
export const STANDARD = 1;
|
|
5
5
|
export const CUSTOM = 2;
|
|
6
|
-
export const DOCS_BASE = 'https://rancher.com/
|
|
6
|
+
export const DOCS_BASE = 'https://ranchermanager.docs.rancher.com/v2.8';
|
|
7
7
|
|
|
8
8
|
const STANDARD_VENDOR = 'Rancher';
|
|
9
9
|
const STANDARD_PRODUCT = 'Explorer';
|
package/config/settings.ts
CHANGED
|
@@ -42,7 +42,6 @@ export const SETTING = {
|
|
|
42
42
|
HIDE_LOCAL_CLUSTER: 'hide-local-cluster',
|
|
43
43
|
AUTH_TOKEN_MAX_TTL_MINUTES: 'auth-token-max-ttl-minutes',
|
|
44
44
|
KUBECONFIG_GENERATE_TOKEN: 'kubeconfig-generate-token',
|
|
45
|
-
KUBECONFIG_TOKEN_TTL_MINUTES: 'kubeconfig-token-ttl-minutes',
|
|
46
45
|
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
|
|
47
46
|
ENGINE_URL: 'engine-install-url',
|
|
48
47
|
ENGINE_ISO_URL: 'engine-iso-url',
|
|
@@ -127,7 +126,6 @@ export const ALLOWED_SETTINGS: GlobalSetting = {
|
|
|
127
126
|
[SETTING.AUTH_USER_SESSION_TTL_MINUTES]: {},
|
|
128
127
|
[SETTING.AUTH_TOKEN_MAX_TTL_MINUTES]: {},
|
|
129
128
|
[SETTING.KUBECONFIG_GENERATE_TOKEN]: { kind: 'boolean' },
|
|
130
|
-
[SETTING.KUBECONFIG_TOKEN_TTL_MINUTES]: {},
|
|
131
129
|
[SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'integer' },
|
|
132
130
|
[SETTING.AUTH_USER_INFO_RESYNC_CRON]: {},
|
|
133
131
|
[SETTING.SERVER_URL]: { kind: 'url', canReset: true },
|
package/core/types.ts
CHANGED
|
@@ -323,6 +323,11 @@ export interface ConfigureTypeOptions {
|
|
|
323
323
|
*/
|
|
324
324
|
isRemovable?: boolean;
|
|
325
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Resources of this type can be edited
|
|
328
|
+
*/
|
|
329
|
+
isEditable?: boolean;
|
|
330
|
+
|
|
326
331
|
/**
|
|
327
332
|
* This type should be grouped by namespaces when displayed in a table
|
|
328
333
|
*/
|
|
@@ -343,16 +348,18 @@ export interface ConfigureTypeOptions {
|
|
|
343
348
|
*/
|
|
344
349
|
showState?: boolean;
|
|
345
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Define where this type/page should navigate to (menu entry routing)
|
|
353
|
+
*/
|
|
354
|
+
customRoute?: Object;
|
|
355
|
+
|
|
346
356
|
/**
|
|
347
357
|
* Leaving these here for completeness but I don't think these should be advertised as useable to plugin creators.
|
|
348
358
|
*/
|
|
349
359
|
// alias
|
|
350
|
-
// customRoute
|
|
351
|
-
// customRoute
|
|
352
360
|
// depaginate
|
|
353
361
|
// graphConfig
|
|
354
362
|
// hasGraph
|
|
355
|
-
// isEditable
|
|
356
363
|
// limit
|
|
357
364
|
// listGroups
|
|
358
365
|
// localOnly
|
|
@@ -379,7 +386,7 @@ export interface ConfigureVirtualTypeOptions extends ConfigureTypeOptions {
|
|
|
379
386
|
/**
|
|
380
387
|
* The route that this type should correspond to {@link PluginRouteConfig} {@link RouteConfig}
|
|
381
388
|
*/
|
|
382
|
-
route: PluginRouteConfig | RouteConfig;
|
|
389
|
+
route: PluginRouteConfig | RouteConfig | Object;
|
|
383
390
|
}
|
|
384
391
|
|
|
385
392
|
export interface DSLReturnType {
|
|
@@ -377,6 +377,13 @@ export default {
|
|
|
377
377
|
const canNotEdit = this.clusterIsAlreadyCreated && !this.unsupportedCloudProvider;
|
|
378
378
|
|
|
379
379
|
return canNotEdit;
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Display warning about additional configuration needed for cloud provider Amazon if kube >= 1.27
|
|
384
|
+
*/
|
|
385
|
+
showCloudProviderAmazonAdditionalConfigWarning() {
|
|
386
|
+
return !!semver.gte(this.value.spec.kubernetesVersion, 'v1.27.0') && this.agentConfig['cloud-provider-name'] === 'aws';
|
|
380
387
|
}
|
|
381
388
|
},
|
|
382
389
|
|
|
@@ -413,6 +420,12 @@ export default {
|
|
|
413
420
|
v-clean-html="t('cluster.harvester.warning.cloudProvider.incompatible', null, true)"
|
|
414
421
|
/>
|
|
415
422
|
</Banner>
|
|
423
|
+
<Banner
|
|
424
|
+
v-if="showCloudProviderAmazonAdditionalConfigWarning"
|
|
425
|
+
color="warning"
|
|
426
|
+
>
|
|
427
|
+
<span v-clean-html="t('cluster.banner.cloudProviderAddConfig', {}, true)" />
|
|
428
|
+
</Banner>
|
|
416
429
|
<div class="row mb-10">
|
|
417
430
|
<div class="col span-6">
|
|
418
431
|
<LabeledSelect
|
|
@@ -55,7 +55,7 @@ export default {
|
|
|
55
55
|
this.controlPlane && out.push('--controlplane');
|
|
56
56
|
this.worker && out.push('--worker');
|
|
57
57
|
this.address && out.push(`--address ${ sanitizeIP(this.address) }`);
|
|
58
|
-
this.internalAddress && out.push(`--internal-address ${
|
|
58
|
+
this.internalAddress && out.push(`--internal-address ${ sanitizeIP(this.internalAddress) }`);
|
|
59
59
|
this.nodeName && out.push(`--node-name ${ sanitizeValue(this.nodeName) }`);
|
|
60
60
|
|
|
61
61
|
for ( const key in this.labels ) {
|
|
@@ -146,10 +146,20 @@ export default {
|
|
|
146
146
|
|
|
147
147
|
async fetch() {
|
|
148
148
|
// TODO Should remove these lines
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
149
|
+
// ? The results aren't stored, so don't know why we fetch?
|
|
150
|
+
|
|
151
|
+
// User might not have access to these resources - so check before trying to fetch
|
|
152
|
+
const fetches = {};
|
|
153
|
+
|
|
154
|
+
if (this.$store.getters[`management/canList`](CAPI.RANCHER_CLUSTER)) {
|
|
155
|
+
fetches.rancherClusters = this.$store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (this.$store.getters[`management/canList`](HCI.HARVESTER_CONFIG)) {
|
|
159
|
+
fetches.harvesterConfigs = this.$store.dispatch('management/findAll', { type: HCI.HARVESTER_CONFIG });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await allHash(fetches);
|
|
153
163
|
|
|
154
164
|
// don't block UI for these resources
|
|
155
165
|
this.resourceManagerFetchSecondaryResources(this.secondaryResourceData);
|
|
@@ -5,6 +5,7 @@ import SteveModel from '@shell/plugins/steve/steve-class';
|
|
|
5
5
|
import { escapeHtml } from '@shell/utils/string';
|
|
6
6
|
import { insertAt } from '@shell/utils/array';
|
|
7
7
|
import jsyaml from 'js-yaml';
|
|
8
|
+
import { FLEET_WORKSPACE_BACK } from '@shell/store/features';
|
|
8
9
|
|
|
9
10
|
export default class FleetCluster extends SteveModel {
|
|
10
11
|
get _availableActions() {
|
|
@@ -80,7 +81,16 @@ export default class FleetCluster extends SteveModel {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
get canChangeWorkspace() {
|
|
83
|
-
|
|
84
|
+
// https://github.com/rancher/dashboard/issues/7745
|
|
85
|
+
if (this.isLocal) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// https://github.com/rancher/dashboard/issues/9730
|
|
89
|
+
if (this.isRke2) {
|
|
90
|
+
return this.$rootGetters['features/get'](FLEET_WORKSPACE_BACK);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return true;
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
get isLocal() {
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import { mapGetters } from 'vuex';
|
|
2
3
|
import Tab from '@shell/components/Tabbed/Tab';
|
|
3
4
|
import Tabbed from '@shell/components/Tabbed';
|
|
4
5
|
import { MANAGEMENT } from '@shell/config/types';
|
|
@@ -7,6 +8,7 @@ import Loading from '@shell/components/Loading';
|
|
|
7
8
|
import { SUBTYPE_MAPPING, CREATE_VERBS } from '@shell/models/management.cattle.io.roletemplate';
|
|
8
9
|
import { NAME } from '@shell/config/product/auth';
|
|
9
10
|
import { BLANK_CLUSTER } from '@shell/store/store-types.js';
|
|
11
|
+
import { Banner } from '@components/Banner';
|
|
10
12
|
|
|
11
13
|
const GLOBAL = SUBTYPE_MAPPING.GLOBAL.key;
|
|
12
14
|
const CLUSTER = SUBTYPE_MAPPING.CLUSTER.key;
|
|
@@ -31,7 +33,7 @@ export default {
|
|
|
31
33
|
name: 'Roles',
|
|
32
34
|
|
|
33
35
|
components: {
|
|
34
|
-
Tab, Tabbed, ResourceTable, Loading
|
|
36
|
+
Tab, Tabbed, ResourceTable, Loading, Banner
|
|
35
37
|
},
|
|
36
38
|
|
|
37
39
|
async asyncData({ store }) {
|
|
@@ -100,6 +102,8 @@ export default {
|
|
|
100
102
|
},
|
|
101
103
|
|
|
102
104
|
computed: {
|
|
105
|
+
...mapGetters(['releaseNotesUrl']),
|
|
106
|
+
|
|
103
107
|
globalResources() {
|
|
104
108
|
return this.globalRoles;
|
|
105
109
|
},
|
|
@@ -181,6 +185,12 @@ export default {
|
|
|
181
185
|
:weight="tabs[GLOBAL].weight"
|
|
182
186
|
:label-key="tabs[GLOBAL].labelKey"
|
|
183
187
|
>
|
|
188
|
+
<Banner
|
|
189
|
+
color="warning"
|
|
190
|
+
class="mb-20"
|
|
191
|
+
>
|
|
192
|
+
<span v-clean-html="t('rbac.globalRoles.role.restricted-admin.deprecation', { releaseNotesUrl }, true)" />
|
|
193
|
+
</Banner>
|
|
184
194
|
<ResourceTable
|
|
185
195
|
:schema="tabs[GLOBAL].schema"
|
|
186
196
|
:rows="globalResources"
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from '@shell/config/table-headers';
|
|
29
29
|
|
|
30
30
|
import { mapPref, PSP_DEPRECATION_BANNER } from '@shell/store/prefs';
|
|
31
|
-
import { haveV1Monitoring, monitoringStatus } from '@shell/utils/monitoring';
|
|
31
|
+
import { haveV1Monitoring, monitoringStatus, canViewGrafanaLink } from '@shell/utils/monitoring';
|
|
32
32
|
import Tabbed from '@shell/components/Tabbed';
|
|
33
33
|
import Tab from '@shell/components/Tabbed/Tab';
|
|
34
34
|
import { allDashboardsExist } from '@shell/utils/grafana';
|
|
@@ -103,6 +103,10 @@ export default {
|
|
|
103
103
|
`Determine etcd metrics`
|
|
104
104
|
);
|
|
105
105
|
|
|
106
|
+
// It's not enough to check that the grafana links are working for the current user; embedded cluster-level dashboards should only be shown if the user can view the grafana endpoint
|
|
107
|
+
// https://github.com/rancher/dashboard/issues/9792
|
|
108
|
+
setPromiseResult(canViewGrafanaLink(this.$store), this, 'canViewMetrics', 'Determine Grafana Permission');
|
|
109
|
+
|
|
106
110
|
if (this.currentCluster.isLocal && this.$store.getters['management/schemaFor'](MANAGEMENT.NODE)) {
|
|
107
111
|
this.$store.dispatch('management/findAll', { type: MANAGEMENT.NODE });
|
|
108
112
|
}
|
|
@@ -125,6 +129,7 @@ export default {
|
|
|
125
129
|
showClusterMetrics: false,
|
|
126
130
|
showK8sMetrics: false,
|
|
127
131
|
showEtcdMetrics: false,
|
|
132
|
+
canViewMetrics: false,
|
|
128
133
|
CLUSTER_METRICS_DETAIL_URL,
|
|
129
134
|
CLUSTER_METRICS_SUMMARY_URL,
|
|
130
135
|
K8S_METRICS_DETAIL_URL,
|
|
@@ -346,7 +351,7 @@ export default {
|
|
|
346
351
|
},
|
|
347
352
|
|
|
348
353
|
hasMetricsTabs() {
|
|
349
|
-
return this.showClusterMetrics || this.showK8sMetrics || this.showEtcdMetrics;
|
|
354
|
+
return this.canViewMetrics && ( this.showClusterMetrics || this.showK8sMetrics || this.showEtcdMetrics);
|
|
350
355
|
},
|
|
351
356
|
|
|
352
357
|
hasBadge() {
|
|
@@ -4,16 +4,14 @@ import isEmpty from 'lodash/isEmpty';
|
|
|
4
4
|
import InstallRedirect from '@shell/utils/install-redirect';
|
|
5
5
|
import AlertTable from '@shell/components/AlertTable';
|
|
6
6
|
import { NAME, CHART_NAME } from '@shell/config/product/monitoring';
|
|
7
|
-
import { CATALOG,
|
|
7
|
+
import { CATALOG, MONITORING } from '@shell/config/types';
|
|
8
8
|
import { allHash } from '@shell/utils/promise';
|
|
9
9
|
import { findBy } from '@shell/utils/array';
|
|
10
10
|
import { getClusterPrefix } from '@shell/utils/grafana';
|
|
11
11
|
import { Banner } from '@components/Banner';
|
|
12
12
|
import LazyImage from '@shell/components/LazyImage';
|
|
13
13
|
import SimpleBox from '@shell/components/SimpleBox';
|
|
14
|
-
import { haveV1MonitoringWorkloads } from '@shell/utils/monitoring';
|
|
15
|
-
|
|
16
|
-
const CATTLE_MONITORING_NAMESPACE = 'cattle-monitoring-system';
|
|
14
|
+
import { haveV1MonitoringWorkloads, canViewAlertManagerLink, canViewGrafanaLink, canViewPrometheusLink } from '@shell/utils/monitoring';
|
|
17
15
|
|
|
18
16
|
export default {
|
|
19
17
|
components: {
|
|
@@ -96,52 +94,41 @@ export default {
|
|
|
96
94
|
const { $store, externalLinks } = this;
|
|
97
95
|
|
|
98
96
|
this.v1Installed = await haveV1MonitoringWorkloads($store);
|
|
99
|
-
const hash =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
const hash = {};
|
|
98
|
+
|
|
99
|
+
if ($store.getters['cluster/canList'](CATALOG.APP)) {
|
|
100
|
+
hash.apps = $store.dispatch('cluster/findAll', { type: CATALOG.APP });
|
|
101
|
+
}
|
|
102
|
+
const res = await allHash(hash);
|
|
103
|
+
|
|
104
|
+
const canViewAlertManager = await canViewAlertManagerLink(this.$store);
|
|
105
|
+
const canViewGrafana = await canViewGrafanaLink(this.$store);
|
|
106
|
+
const canViewPrometheus = await canViewPrometheusLink(this.$store);
|
|
103
107
|
|
|
104
|
-
if (
|
|
108
|
+
if (canViewAlertManager) {
|
|
105
109
|
const amMatch = findBy(externalLinks, 'group', 'alertmanager');
|
|
106
|
-
const grafanaMatch = findBy(externalLinks, 'group', 'grafana');
|
|
107
|
-
const promeMatch = externalLinks.filter(
|
|
108
|
-
(el) => el.group === 'prometheus'
|
|
109
|
-
);
|
|
110
110
|
|
|
111
|
+
amMatch.enabled = true;
|
|
112
|
+
}
|
|
113
|
+
if (canViewGrafana) {
|
|
114
|
+
const grafanaMatch = findBy(externalLinks, 'group', 'grafana');
|
|
111
115
|
// Generate Grafana link
|
|
112
116
|
const currentCluster = this.$store.getters['currentCluster'];
|
|
113
|
-
const rancherMonitoring = !isEmpty(
|
|
117
|
+
const rancherMonitoring = !isEmpty(res.apps) ? findBy(res.apps, 'id', 'cattle-monitoring-system/rancher-monitoring') : '';
|
|
114
118
|
const clusterPrefix = getClusterPrefix(rancherMonitoring?.currentVersion || '', currentCluster.id);
|
|
115
119
|
|
|
116
120
|
grafanaMatch.link = `${ clusterPrefix }/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/`;
|
|
121
|
+
grafanaMatch.enabled = true;
|
|
122
|
+
}
|
|
117
123
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
'
|
|
121
|
-
`${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-alertmanager`
|
|
122
|
-
);
|
|
123
|
-
const grafana = findBy(
|
|
124
|
-
hash.endpoints,
|
|
125
|
-
'id',
|
|
126
|
-
`${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-grafana`
|
|
127
|
-
);
|
|
128
|
-
const prometheus = findBy(
|
|
129
|
-
hash.endpoints,
|
|
130
|
-
'id',
|
|
131
|
-
`${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-prometheus`
|
|
124
|
+
if (canViewPrometheus) {
|
|
125
|
+
const promeMatch = externalLinks.filter(
|
|
126
|
+
(el) => el.group === 'prometheus'
|
|
132
127
|
);
|
|
133
128
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
if (!isEmpty(grafana) && !isEmpty(grafana.subsets)) {
|
|
138
|
-
grafanaMatch.enabled = true;
|
|
139
|
-
}
|
|
140
|
-
if (!isEmpty(prometheus) && !isEmpty(prometheus.subsets)) {
|
|
141
|
-
promeMatch.forEach((match) => {
|
|
142
|
-
match.enabled = true;
|
|
143
|
-
});
|
|
144
|
-
}
|
|
129
|
+
promeMatch.forEach((match) => {
|
|
130
|
+
match.enabled = true;
|
|
131
|
+
});
|
|
145
132
|
}
|
|
146
133
|
},
|
|
147
134
|
},
|
package/pages/support/index.vue
CHANGED
|
@@ -8,7 +8,6 @@ import { SETTING } from '@shell/config/settings';
|
|
|
8
8
|
import { addParam } from '@shell/utils/url';
|
|
9
9
|
import { isRancherPrime } from '@shell/config/version';
|
|
10
10
|
import { hasCspAdapter } from 'mixins/brand';
|
|
11
|
-
import { generateSupportLink } from '@shell/utils/version';
|
|
12
11
|
|
|
13
12
|
export default {
|
|
14
13
|
layout: 'home',
|
|
@@ -112,12 +111,6 @@ export default {
|
|
|
112
111
|
|
|
113
112
|
sccLink() {
|
|
114
113
|
return this.hasAWSSupport ? addParam('https://scc.suse.com', 'from_marketplace', '1') : 'https://scc.suse.com';
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
supportLink() {
|
|
118
|
-
const version = this.settings?.find((s) => s.id === SETTING.VERSION_RANCHER)?.value;
|
|
119
|
-
|
|
120
|
-
return generateSupportLink(version);
|
|
121
114
|
}
|
|
122
115
|
},
|
|
123
116
|
|
|
@@ -139,7 +132,7 @@ export default {
|
|
|
139
132
|
<div class="support-link">
|
|
140
133
|
<a
|
|
141
134
|
class="support-link"
|
|
142
|
-
|
|
135
|
+
href="https://www.rancher.com/support"
|
|
143
136
|
target="_blank"
|
|
144
137
|
rel="noopener noreferrer nofollow"
|
|
145
138
|
>{{ t('support.community.learnMore') }}</a>
|