@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.
Files changed (51) hide show
  1. package/.DS_Store +0 -0
  2. package/assets/translations/en-us.yaml +11 -3
  3. package/assets/translations/zh-hans.yaml +2 -3
  4. package/components/AlertTable.vue +8 -6
  5. package/components/CruResource.vue +7 -4
  6. package/components/EmberPage.vue +2 -2
  7. package/components/EtcdInfoBanner.vue +12 -2
  8. package/components/GlobalRoleBindings.vue +10 -0
  9. package/components/GrafanaDashboard.vue +8 -3
  10. package/components/Wizard.vue +17 -1
  11. package/components/__tests__/ProjectRow.test.ts +63 -0
  12. package/components/auth/RoleDetailEdit.vue +21 -1
  13. package/components/auth/__tests__/RoleDetailEdit.test.ts +41 -0
  14. package/components/form/ArrayList.vue +20 -11
  15. package/components/form/ResourceQuota/ProjectRow.vue +6 -2
  16. package/components/form/__tests__/ArrayList.test.ts +44 -0
  17. package/components/nav/Header.vue +5 -4
  18. package/components/nav/TopLevelMenu.vue +38 -15
  19. package/components/nav/__tests__/TopLevelMenu.test.ts +120 -0
  20. package/components/nav/__tests__/Type.test.ts +139 -0
  21. package/config/private-label.js +1 -1
  22. package/config/settings.ts +0 -2
  23. package/core/types.ts +11 -4
  24. package/edit/provisioning.cattle.io.cluster/Basics.vue +13 -0
  25. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +1 -1
  26. package/edit/provisioning.cattle.io.cluster/rke2.vue +18 -2
  27. package/edit/workload/mixins/workload.js +14 -4
  28. package/models/fleet.cattle.io.cluster.js +11 -1
  29. package/models/management.cattle.io.globalrole.js +1 -1
  30. package/models/management.cattle.io.roletemplate.js +1 -1
  31. package/package.json +1 -1
  32. package/pages/c/_cluster/auth/roles/index.vue +11 -1
  33. package/pages/c/_cluster/explorer/index.vue +7 -2
  34. package/pages/c/_cluster/monitoring/index.vue +26 -39
  35. package/pages/support/index.vue +1 -8
  36. package/promptRemove/management.cattle.io.project.vue +6 -9
  37. package/rancher-components/components/Form/Radio/RadioGroup.test.ts +30 -0
  38. package/rancher-components/components/Form/Radio/RadioGroup.vue +4 -0
  39. package/scripts/extension/helm/charts/ui-plugin-server/Chart.yaml +2 -2
  40. package/store/features.js +1 -0
  41. package/types/shell/index.d.ts +4 -1
  42. package/utils/__tests__/object.test.ts +67 -1
  43. package/utils/__tests__/version.test.ts +13 -23
  44. package/utils/cluster.js +1 -1
  45. package/utils/grafana.js +1 -2
  46. package/utils/monitoring.js +25 -1
  47. package/utils/object.js +4 -3
  48. package/utils/sort.js +1 -1
  49. package/utils/validators/formRules/index.ts +1 -1
  50. package/utils/validators/role-template.js +1 -1
  51. 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.find((s) => s.id === SETTING.BANNERS);
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.map((x) => {
108
- const pCluster = pClusters?.find((c) => c.mgmt.id === x.id);
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.map((p) => {
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="{'marginBottom':
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();
@@ -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/docs/rancher/v2.7/en';
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';
@@ -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 ${ sanitizeValue(this.internalAddress) }`);
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
- return;
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
- await allHash({
150
- rancherClusters: this.$store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER }),
151
- harvesterConfigs: this.$store.dispatch('management/findAll', { type: HCI.HARVESTER_CONFIG }),
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
- return !this.isRke2 && !this.isLocal;
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "0.3.25",
3
+ "version": "0.3.27",
4
4
  "description": "Rancher Dashboard Shell",
5
5
  "repository": "https://github.com/rancherlabs/dashboard",
6
6
  "license": "Apache-2.0",