@rancher/shell 0.3.25 → 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 +11 -3
- package/assets/translations/zh-hans.yaml +2 -3
- package/components/AlertTable.vue +8 -6
- package/components/CruResource.vue +7 -4
- 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/__tests__/ProjectRow.test.ts +63 -0
- package/components/auth/RoleDetailEdit.vue +21 -1
- package/components/auth/__tests__/RoleDetailEdit.test.ts +41 -0
- package/components/form/ArrayList.vue +20 -11
- package/components/form/ResourceQuota/ProjectRow.vue +6 -2
- 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/provisioning.cattle.io.cluster/rke2.vue +18 -2
- package/edit/workload/mixins/workload.js +14 -4
- package/models/fleet.cattle.io.cluster.js +11 -1
- package/models/management.cattle.io.globalrole.js +1 -1
- package/models/management.cattle.io.roletemplate.js +1 -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/scripts/extension/helm/charts/ui-plugin-server/Chart.yaml +2 -2
- 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
|
@@ -50,7 +50,6 @@ export default {
|
|
|
50
50
|
computed: {
|
|
51
51
|
...mapGetters(['clusterId']),
|
|
52
52
|
...mapGetters(['clusterReady', 'isRancher', 'currentCluster', 'currentProduct', 'isRancherInHarvester']),
|
|
53
|
-
...mapGetters('type-map', ['activeProducts']),
|
|
54
53
|
...mapGetters({ features: 'features/get' }),
|
|
55
54
|
|
|
56
55
|
value: {
|
|
@@ -59,9 +58,16 @@ export default {
|
|
|
59
58
|
},
|
|
60
59
|
},
|
|
61
60
|
|
|
61
|
+
sideMenuStyle() {
|
|
62
|
+
return {
|
|
63
|
+
marginBottom: this.globalBannerSettings?.footerFont,
|
|
64
|
+
marginTop: this.globalBannerSettings?.headerFont
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
|
|
62
68
|
globalBannerSettings() {
|
|
63
69
|
const settings = this.$store.getters['management/all'](MANAGEMENT.SETTING);
|
|
64
|
-
const bannerSettings = settings
|
|
70
|
+
const bannerSettings = settings?.find((s) => s.id === SETTING.BANNERS);
|
|
65
71
|
|
|
66
72
|
if (bannerSettings) {
|
|
67
73
|
const parsed = JSON.parse(bannerSettings.value);
|
|
@@ -104,8 +110,8 @@ export default {
|
|
|
104
110
|
kubeClusters = kubeClusters.filter((c) => !!available[c]);
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
return kubeClusters
|
|
108
|
-
const pCluster = pClusters?.find((c) => c.mgmt
|
|
113
|
+
return kubeClusters?.map((x) => {
|
|
114
|
+
const pCluster = pClusters?.find((c) => c.mgmt?.id === x.id);
|
|
109
115
|
|
|
110
116
|
return {
|
|
111
117
|
id: x.id,
|
|
@@ -120,14 +126,12 @@ export default {
|
|
|
120
126
|
pin: () => x.pin(),
|
|
121
127
|
unpin: () => x.unpin()
|
|
122
128
|
};
|
|
123
|
-
});
|
|
129
|
+
}) || [];
|
|
124
130
|
},
|
|
125
131
|
|
|
126
132
|
clustersFiltered() {
|
|
127
133
|
const search = (this.clusterFilter || '').toLowerCase();
|
|
128
|
-
|
|
129
|
-
const out = search ? this.clusters.filter((item) => item.label.toLowerCase().includes(search)) : this.clusters;
|
|
130
|
-
|
|
134
|
+
const out = search ? this.clusters.filter((item) => item.label?.toLowerCase().includes(search)) : this.clusters;
|
|
131
135
|
const sorted = sortBy(out, ['ready:desc', 'label']);
|
|
132
136
|
|
|
133
137
|
if (search) {
|
|
@@ -203,7 +207,7 @@ export default {
|
|
|
203
207
|
const cluster = this.clusterId || this.$store.getters['defaultClusterId'];
|
|
204
208
|
|
|
205
209
|
// TODO plugin routes
|
|
206
|
-
const entries = this.activeProducts
|
|
210
|
+
const entries = this.$store.getters['type-map/activeProducts']?.map((p) => {
|
|
207
211
|
// Try product-specific index first
|
|
208
212
|
const to = p.to || {
|
|
209
213
|
name: `c-cluster-${ p.name }`,
|
|
@@ -314,22 +318,22 @@ export default {
|
|
|
314
318
|
</script>
|
|
315
319
|
<template>
|
|
316
320
|
<div>
|
|
321
|
+
<!-- Overlay -->
|
|
317
322
|
<div
|
|
318
323
|
v-if="shown"
|
|
319
324
|
class="side-menu-glass"
|
|
320
325
|
@click="hide()"
|
|
321
326
|
/>
|
|
322
327
|
<transition name="fade">
|
|
328
|
+
<!-- Side menu -->
|
|
323
329
|
<div
|
|
324
330
|
data-testid="side-menu"
|
|
325
331
|
class="side-menu"
|
|
326
332
|
:class="{'menu-open': shown, 'menu-close':!shown}"
|
|
327
|
-
:style="
|
|
328
|
-
globalBannerSettings?.footerFont,
|
|
329
|
-
'marginTop':
|
|
330
|
-
globalBannerSettings?.headerFont}"
|
|
333
|
+
:style="sideMenuStyle"
|
|
331
334
|
tabindex="-1"
|
|
332
335
|
>
|
|
336
|
+
<!-- Logo and name -->
|
|
333
337
|
<div class="title">
|
|
334
338
|
<div
|
|
335
339
|
data-testid="top-level-menu"
|
|
@@ -351,8 +355,11 @@ export default {
|
|
|
351
355
|
<BrandImage file-name="rancher-logo.svg" />
|
|
352
356
|
</div>
|
|
353
357
|
</div>
|
|
358
|
+
|
|
359
|
+
<!-- Menu body -->
|
|
354
360
|
<div class="body">
|
|
355
361
|
<div>
|
|
362
|
+
<!-- Home button -->
|
|
356
363
|
<nuxt-link
|
|
357
364
|
class="option cluster selector home"
|
|
358
365
|
:to="{ name: 'home' }"
|
|
@@ -371,6 +378,8 @@ export default {
|
|
|
371
378
|
{{ t('nav.home') }}
|
|
372
379
|
</div>
|
|
373
380
|
</nuxt-link>
|
|
381
|
+
|
|
382
|
+
<!-- Search bar -->
|
|
374
383
|
<div
|
|
375
384
|
v-if="showClusterSearch"
|
|
376
385
|
class="clusters-search"
|
|
@@ -400,6 +409,8 @@ export default {
|
|
|
400
409
|
</div>
|
|
401
410
|
</div>
|
|
402
411
|
</div>
|
|
412
|
+
|
|
413
|
+
<!-- Harvester extras -->
|
|
403
414
|
<template v-if="hciApps.length">
|
|
404
415
|
<div class="category" />
|
|
405
416
|
<div>
|
|
@@ -416,7 +427,6 @@ export default {
|
|
|
416
427
|
</div>
|
|
417
428
|
</a>
|
|
418
429
|
</div>
|
|
419
|
-
|
|
420
430
|
<div
|
|
421
431
|
v-for="a in hciApps"
|
|
422
432
|
:key="a.label"
|
|
@@ -435,12 +445,14 @@ export default {
|
|
|
435
445
|
</div>
|
|
436
446
|
</template>
|
|
437
447
|
|
|
448
|
+
<!-- Cluster menu -->
|
|
438
449
|
<template v-if="clusters && !!clusters.length">
|
|
439
450
|
<div
|
|
440
451
|
ref="clusterList"
|
|
441
452
|
class="clusters"
|
|
442
453
|
:style="pinnedClustersHeight"
|
|
443
454
|
>
|
|
455
|
+
<!-- Pinned Clusters -->
|
|
444
456
|
<div
|
|
445
457
|
v-if="showPinClusters && pinFiltered.length"
|
|
446
458
|
class="clustersPinned"
|
|
@@ -490,10 +502,13 @@ export default {
|
|
|
490
502
|
<hr>
|
|
491
503
|
</div>
|
|
492
504
|
</div>
|
|
505
|
+
|
|
506
|
+
<!-- Clusters Search result -->
|
|
493
507
|
<div class="clustersList">
|
|
494
508
|
<div
|
|
495
|
-
v-for="c in clustersFiltered"
|
|
509
|
+
v-for="(c, index) in clustersFiltered"
|
|
496
510
|
:key="c.id"
|
|
511
|
+
:data-testid="`top-level-menu-cluster-${index}`"
|
|
497
512
|
@click="hide()"
|
|
498
513
|
>
|
|
499
514
|
<nuxt-link
|
|
@@ -532,14 +547,18 @@ export default {
|
|
|
532
547
|
</span>
|
|
533
548
|
</div>
|
|
534
549
|
</div>
|
|
550
|
+
|
|
551
|
+
<!-- No clusters message -->
|
|
535
552
|
<div
|
|
536
553
|
v-if="(clustersFiltered.length === 0 || pinFiltered.length === 0) && searchActive"
|
|
554
|
+
data-testid="top-level-menu-no-results"
|
|
537
555
|
class="none-matching"
|
|
538
556
|
>
|
|
539
557
|
{{ t('nav.search.noResults') }}
|
|
540
558
|
</div>
|
|
541
559
|
</div>
|
|
542
560
|
|
|
561
|
+
<!-- See all clusters -->
|
|
543
562
|
<nuxt-link
|
|
544
563
|
v-if="clusters.length > maxClustersToShow"
|
|
545
564
|
class="clusters-all"
|
|
@@ -611,6 +630,8 @@ export default {
|
|
|
611
630
|
</nuxt-link>
|
|
612
631
|
</div>
|
|
613
632
|
</template>
|
|
633
|
+
|
|
634
|
+
<!-- App menu -->
|
|
614
635
|
<template v-if="configurationApps.length">
|
|
615
636
|
<div
|
|
616
637
|
class="category-title"
|
|
@@ -640,6 +661,8 @@ export default {
|
|
|
640
661
|
</template>
|
|
641
662
|
</div>
|
|
642
663
|
</div>
|
|
664
|
+
|
|
665
|
+
<!-- Footer -->
|
|
643
666
|
<div
|
|
644
667
|
class="footer"
|
|
645
668
|
>
|
|
@@ -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 ) {
|
|
@@ -864,7 +864,7 @@ export default {
|
|
|
864
864
|
},
|
|
865
865
|
|
|
866
866
|
created() {
|
|
867
|
-
this.registerBeforeHook(this.saveMachinePools, 'save-machine-pools');
|
|
867
|
+
this.registerBeforeHook(this.saveMachinePools, 'save-machine-pools', 1);
|
|
868
868
|
this.registerBeforeHook(this.setRegistryConfig, 'set-registry-config');
|
|
869
869
|
this.registerAfterHook(this.cleanupMachinePools, 'cleanup-machine-pools');
|
|
870
870
|
this.registerAfterHook(this.saveRoleBindings, 'save-role-bindings');
|
|
@@ -1235,7 +1235,23 @@ export default {
|
|
|
1235
1235
|
|
|
1236
1236
|
async saveMachinePools(hookContext) {
|
|
1237
1237
|
if (hookContext === CONTEXT_HOOK_EDIT_YAML) {
|
|
1238
|
-
|
|
1238
|
+
await new Promise((resolve, reject) => {
|
|
1239
|
+
this.$store.dispatch('cluster/promptModal', {
|
|
1240
|
+
component: 'GenericPrompt',
|
|
1241
|
+
componentProps: {
|
|
1242
|
+
title: this.t('cluster.rke2.modal.editYamlMachinePool.title'),
|
|
1243
|
+
body: this.t('cluster.rke2.modal.editYamlMachinePool.body'),
|
|
1244
|
+
applyMode: 'editAndContinue',
|
|
1245
|
+
confirm: (confirmed) => {
|
|
1246
|
+
if (confirmed) {
|
|
1247
|
+
resolve();
|
|
1248
|
+
} else {
|
|
1249
|
+
reject(new Error('User Cancelled'));
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
},
|
|
1253
|
+
});
|
|
1254
|
+
});
|
|
1239
1255
|
}
|
|
1240
1256
|
|
|
1241
1257
|
const finalPools = [];
|
|
@@ -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() {
|
|
@@ -144,7 +144,7 @@ export default class GlobalRole extends SteveDescriptionModel {
|
|
|
144
144
|
const norman = await this.norman;
|
|
145
145
|
|
|
146
146
|
for (const rule of norman.rules) {
|
|
147
|
-
if (rule.nonResourceURLs.length) {
|
|
147
|
+
if (rule.nonResourceURLs && rule.nonResourceURLs.length) {
|
|
148
148
|
delete rule.resources;
|
|
149
149
|
delete rule.apiGroups;
|
|
150
150
|
} else {
|
|
@@ -192,7 +192,7 @@ export default class RoleTemplate extends SteveDescriptionModel {
|
|
|
192
192
|
const norman = await this.norman;
|
|
193
193
|
|
|
194
194
|
for (const rule of norman.rules) {
|
|
195
|
-
if (rule.nonResourceURLs.length) {
|
|
195
|
+
if (rule.nonResourceURLs && rule.nonResourceURLs.length) {
|
|
196
196
|
delete rule.resources;
|
|
197
197
|
delete rule.apiGroups;
|
|
198
198
|
} else {
|