@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.
Files changed (43) hide show
  1. package/.DS_Store +0 -0
  2. package/assets/translations/en-us.yaml +4 -3
  3. package/assets/translations/zh-hans.yaml +2 -3
  4. package/components/AlertTable.vue +8 -6
  5. package/components/EmberPage.vue +2 -2
  6. package/components/EtcdInfoBanner.vue +12 -2
  7. package/components/GlobalRoleBindings.vue +10 -0
  8. package/components/GrafanaDashboard.vue +8 -3
  9. package/components/Wizard.vue +17 -1
  10. package/components/auth/RoleDetailEdit.vue +17 -1
  11. package/components/form/ArrayList.vue +20 -11
  12. package/components/form/__tests__/ArrayList.test.ts +44 -0
  13. package/components/nav/Header.vue +5 -4
  14. package/components/nav/TopLevelMenu.vue +38 -15
  15. package/components/nav/__tests__/TopLevelMenu.test.ts +120 -0
  16. package/components/nav/__tests__/Type.test.ts +139 -0
  17. package/config/private-label.js +1 -1
  18. package/config/settings.ts +0 -2
  19. package/core/types.ts +11 -4
  20. package/edit/provisioning.cattle.io.cluster/Basics.vue +13 -0
  21. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +1 -1
  22. package/edit/workload/mixins/workload.js +14 -4
  23. package/models/fleet.cattle.io.cluster.js +11 -1
  24. package/package.json +1 -1
  25. package/pages/c/_cluster/auth/roles/index.vue +11 -1
  26. package/pages/c/_cluster/explorer/index.vue +7 -2
  27. package/pages/c/_cluster/monitoring/index.vue +26 -39
  28. package/pages/support/index.vue +1 -8
  29. package/promptRemove/management.cattle.io.project.vue +6 -9
  30. package/rancher-components/components/Form/Radio/RadioGroup.test.ts +30 -0
  31. package/rancher-components/components/Form/Radio/RadioGroup.vue +4 -0
  32. package/store/features.js +1 -0
  33. package/types/shell/index.d.ts +4 -1
  34. package/utils/__tests__/object.test.ts +67 -1
  35. package/utils/__tests__/version.test.ts +13 -23
  36. package/utils/cluster.js +1 -1
  37. package/utils/grafana.js +1 -2
  38. package/utils/monitoring.js +25 -1
  39. package/utils/object.js +4 -3
  40. package/utils/sort.js +1 -1
  41. package/utils/validators/formRules/index.ts +1 -1
  42. package/utils/validators/role-template.js +1 -1
  43. package/utils/version.js +0 -13
package/.DS_Store ADDED
Binary file
@@ -1677,6 +1677,7 @@ cluster:
1677
1677
  haveArgInfo: Configuration information is not available for the selected Kubernetes version. The options available on this screen will be limited; you may want to use the YAML editor.
1678
1678
  deprecatedPsp: Pod Security Policies are deprecated as of Kubernetes v1.21, and have been removed in Kubernetes v1.25.
1679
1679
  removedPsp: Pod Security Policies have been removed in Kubernetes v1.25. Use Pod Security Admission instead.
1680
+ cloudProviderAddConfig: 'On Kubernetes 1.27 or greater, the Amazon Cloud Provider requires additional configuration. See <a href="https://ranchermanager.docs.rancher.com/how-to-guides/new-user-guides/kubernetes-clusters-in-rancher-setup/set-up-cloud-providers/amazon" target="_blank" rel="noopener noreferrer nofollow">the documentation</a> for more information.'
1680
1681
  machinePoolError: |-
1681
1682
  {count, plural,
1682
1683
  =1 { {pool_name}: The provided value for {fields} was not found in the list of expected values. This can happen with clusters provisioned outside of Rancher or when options for the provider have changed. }
@@ -2763,7 +2764,7 @@ landing:
2763
2764
  cpuUsed: CPU Used
2764
2765
  memoryUsed: Memory Used
2765
2766
  seeWhatsNew: Learn more about the improvements and new capabilities in this version.
2766
- whatsNewLink: "What's new in 2.7"
2767
+ whatsNewLink: "What's new in 2.8"
2767
2768
  learnMore: Learn More
2768
2769
  support: Support
2769
2770
  psps: PSPs
@@ -2833,7 +2834,7 @@ logging:
2833
2834
  dockerRootDirectory: Docker Root Directory
2834
2835
  systemdLogPath: systemd Log Path
2835
2836
  tooltip: 'Some kubernetes distributions log to <code>journald</code>. In order to collect these logs the <code>systemdLogPath</code> needs to be defined. While the <code>/run/log/journal</code> directory is used by default, some Linux distributions do not default to this path.'
2836
- url: '<a href="https://rancher.com/docs/rancher/v2.6/en/logging/helm-chart-options/" target="_blank" rel="noopener nofollow noreferrer">Learn more</a>'
2837
+ url: '<a href="https://ranchermanager.docs.rancher.com/v2.8/integrations-in-rancher/logging/logging-helm-chart-options" target="_blank" rel="noopener nofollow noreferrer">Learn more</a>'
2837
2838
  default: /run/log/journal
2838
2839
  elasticsearch:
2839
2840
  host: Host
@@ -4613,6 +4614,7 @@ rbac:
4613
4614
  restricted-admin:
4614
4615
  label: Restricted Administrator
4615
4616
  description: Restricted Admins have full control over all resources in all downstream clusters but no access to the local cluster.
4617
+ deprecation: 'Warning: The Restricted Administrator role has been deprecated as of Rancher 2.8.0 and will be removed in a future release - Check out the <a href="{releaseNotesUrl}" target="_blank" rel="noopener noreferrer nofollow">Release Notes</a>'
4616
4618
  user:
4617
4619
  label: Standard User
4618
4620
  description: Standard Users can create new clusters and manage clusters and projects they have been granted access to.
@@ -7079,7 +7081,6 @@ advancedSettings:
7079
7081
  'auth-user-session-ttl-minutes': 'Custom TTL (in minutes) on a user auth session.'
7080
7082
  'auth-token-max-ttl-minutes': 'Max TTL (in minutes) for all authentication tokens. When set to 0, the token never expires.'
7081
7083
  'kubeconfig-generate-token': 'Automatically generate tokens for users when a kubeconfig is requested.'
7082
- 'kubeconfig-token-ttl-minutes': 'TTL used for tokens generated via the CLI. Deprecated: This setting will be removed, and kubeconfig-default-token-ttl-minutes will be used for all kubeconfig tokens.'
7083
7084
  'kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on all kubeconfig tokens. When set to 0, the token never expires.'
7084
7085
  'rke-metadata-config': 'Configure RKE metadata refresh parameters.'
7085
7086
  'ui-banners': 'Classification banner is used to display a custom fixed banner in the header, footer, or both.'
@@ -2735,7 +2735,7 @@ landing:
2735
2735
  cpuUsed: 已用 CPU
2736
2736
  memoryUsed: 已用内存
2737
2737
  seeWhatsNew: 点击右侧链接,了解此版本的新功能和优化。
2738
- whatsNewLink: "2.7 的新功能"
2738
+ whatsNewLink: "2.8 的新功能"
2739
2739
  learnMore: 了解更多
2740
2740
  support: 支持
2741
2741
  psps: PSP
@@ -2805,7 +2805,7 @@ logging:
2805
2805
  dockerRootDirectory: Docker 根目录
2806
2806
  systemdLogPath: systemd 日志路径
2807
2807
  tooltip: '某些 Kubernetes 发行版在 <code>journald</code>中记录日志。你需要定义<code>systemdLogPath</code> 以收集日志。默认路径是<code>/run/log/journal</code>,但某些 Linux 发行版不默认使用该路径。'
2808
- url: '<a href="https://rancher.com/docs/rancher/v2.6/en/logging/helm-chart-options/" target="_blank" rel="noopener nofollow noreferrer">了解更多</a>'
2808
+ url: '<a href="https://ranchermanager.docs.rancher.com/v2.8/integrations-in-rancher/logging/logging-helm-chart-options" target="_blank" rel="noopener nofollow noreferrer">了解更多</a>'
2809
2809
  default: /run/log/journal
2810
2810
  elasticsearch:
2811
2811
  host: 主机
@@ -7051,7 +7051,6 @@ advancedSettings:
7051
7051
  'auth-user-session-ttl-minutes': '用户认证会话的自定义 TTL(单位:分钟)。'
7052
7052
  'auth-token-max-ttl-minutes': '所有身份认证 Token 的最大 TTL(单位:分钟)。如果设置为 0,则 Token 永不过期。'
7053
7053
  'kubeconfig-generate-token': '请求 kubeconfig 时自动为用户生成 Token。'
7054
- 'kubeconfig-token-ttl-minutes': '在 CLI 中生成的 Token TTL。已弃用:此设置将被删除,kubeconfig-default-token-ttl-minutes 将用于所有 kubeconfig Token。'
7055
7054
  'kubeconfig-default-token-ttl-minutes': '应用于所有 kubeconfig Token 的 TTL(单位:分钟)。如果设置为 0,则 Token 永不过期。'
7056
7055
  'rke-metadata-config': '配置 RKE 元数据刷新参数。'
7057
7056
  'ui-banners': '分类横幅用于在页眉、页脚或两者中显示自定义的固定横幅。'
@@ -82,14 +82,16 @@ export default {
82
82
  },
83
83
 
84
84
  async fetchDeps() {
85
- try {
86
- const am = await this.$store.dispatch('cluster/find', { type: ENDPOINTS, id: `${ this.monitoringNamespace }/${ this.alertServiceEndpoint }` });
85
+ if (this.$store.getters['cluster/canList'](ENDPOINTS)) {
86
+ try {
87
+ const am = await this.$store.dispatch('cluster/find', { type: ENDPOINTS, id: `${ this.monitoringNamespace }/${ this.alertServiceEndpoint }` });
87
88
 
88
- if (!isEmpty(am) && !isEmpty(am.subsets)) {
89
- this.alertManagerPoller.start();
90
- }
91
- } catch {
89
+ if (!isEmpty(am) && !isEmpty(am.subsets)) {
90
+ this.alertManagerPoller.start();
91
+ }
92
+ } catch {
92
93
 
94
+ }
93
95
  }
94
96
  },
95
97
  }
@@ -597,11 +597,11 @@ export default {
597
597
 
598
598
  .ember-iframe {
599
599
  border: 0;
600
- left: var(--nav-width);
600
+ left: calc(var(--nav-width) + $app-bar-collapsed-width);
601
601
  height: calc(100vh - var(--header-height));
602
602
  position: absolute;
603
603
  top: var(--header-height);
604
- width: calc(100vw - var(--nav-width));
604
+ width: calc(100vw - var(--nav-width) - $app-bar-collapsed-width);
605
605
  visibility: show;
606
606
  }
607
607
 
@@ -9,8 +9,18 @@ export default {
9
9
  components: { Banner, Loading },
10
10
  async fetch() {
11
11
  const inStore = this.$store.getters['currentProduct'].inStore;
12
- const res = await this.$store.dispatch(`${ inStore }/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' });
13
- const monitoringVersion = res?.currentVersion;
12
+ let monitoringVersion = '';
13
+
14
+ if (this.$store.getters[`${ inStore }/canList}`](CATALOG.APP)) {
15
+ try {
16
+ const res = await this.$store.dispatch(`${ inStore }/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' });
17
+
18
+ monitoringVersion = res?.currentVersion;
19
+ } catch (err) {
20
+
21
+ }
22
+ }
23
+
14
24
  const leader = await hasLeader(monitoringVersion, this.$store.dispatch, this.currentCluster.id);
15
25
 
16
26
  this.hasLeader = leader ? this.t('generic.yes') : this.t('generic.no');
@@ -94,6 +94,7 @@ export default {
94
94
  };
95
95
  },
96
96
  computed: {
97
+ ...mapGetters(['releaseNotesUrl']),
97
98
  ...mapGetters({ t: 'i18n/t' }),
98
99
 
99
100
  isCreate() {
@@ -347,6 +348,11 @@ export default {
347
348
  </div>
348
349
  </template>
349
350
  </Checkbox>
351
+ <p
352
+ v-if="role.id === 'restricted-admin'"
353
+ v-clean-html="t('rbac.globalRoles.role.restricted-admin.deprecation', { releaseNotesUrl }, true)"
354
+ class="deprecation-notice"
355
+ />
350
356
  </div>
351
357
  </div>
352
358
  </template>
@@ -364,6 +370,10 @@ export default {
364
370
  </style>
365
371
  <style lang='scss' scoped>
366
372
  $detailSize: 11px;
373
+
374
+ .deprecation-notice {
375
+ margin: 8px 0 8px 20px;
376
+ }
367
377
  .role-group {
368
378
  .type-title {
369
379
  display: flex;
@@ -40,13 +40,18 @@ export default {
40
40
  },
41
41
  async fetch() {
42
42
  const inStore = this.$store.getters['currentProduct'].inStore;
43
- const res = await this.$store.dispatch(`${ inStore }/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' });
44
43
 
45
- this.monitoringVersion = res?.currentVersion;
44
+ if (this.$store.getters[`${ inStore }/canList`](CATALOG.APP)) {
45
+ try {
46
+ const res = await this.$store.dispatch(`${ inStore }/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' });
47
+
48
+ this.monitoringVersion = res?.currentVersion;
49
+ } catch (err) {}
50
+ }
46
51
  },
47
52
  data() {
48
53
  return {
49
- loading: false, error: false, interval: null, errorTimer: null, monitoringVersion: null
54
+ loading: false, error: false, interval: null, errorTimer: null, monitoringVersion: ''
50
55
  };
51
56
  },
52
57
  computed: {
@@ -182,6 +182,12 @@ export default {
182
182
  this.activeStep = this.visibleSteps[this.initStepIndex];
183
183
  this.goToStep(this.activeStepIndex + 1);
184
184
  }
185
+ },
186
+ errors() {
187
+ // Ensurce we scroll the errors into view
188
+ this.$nextTick(() => {
189
+ this.$refs.wizard.scrollTop = this.$refs.wizard.scrollHeight;
190
+ });
185
191
  }
186
192
  },
187
193
 
@@ -253,7 +259,10 @@ export default {
253
259
  </script>
254
260
 
255
261
  <template>
256
- <div class="outer-container">
262
+ <div
263
+ ref="wizard"
264
+ class="outer-container"
265
+ >
257
266
  <Loading
258
267
  v-if="!stepsLoaded"
259
268
  mode="relative"
@@ -397,6 +406,7 @@ export default {
397
406
  color="error"
398
407
  :label="err"
399
408
  :closable="true"
409
+ class="footer-error"
400
410
  @close="errors.splice(idx, 1)"
401
411
  />
402
412
  </div>
@@ -647,6 +657,12 @@ $spacer: 10px;
647
657
  }
648
658
  }
649
659
 
660
+ // We have to account for the absolute position of the .controls-row
661
+ .footer-error {
662
+ margin-top: -40px;
663
+ margin-bottom: 70px;
664
+ }
665
+
650
666
  .controls-row {
651
667
 
652
668
  // Overrides outlet padding
@@ -1,4 +1,5 @@
1
1
  <script>
2
+ import { mapGetters } from 'vuex';
2
3
  import { MANAGEMENT, RBAC } from '@shell/config/types';
3
4
  import CruResource from '@shell/components/CruResource';
4
5
  import CreateEditView from '@shell/mixins/create-edit-view';
@@ -14,6 +15,7 @@ import { ucFirst } from '@shell/utils/string';
14
15
  import SortableTable from '@shell/components/SortableTable';
15
16
  import { _CLONE, _DETAIL } from '@shell/config/query-params';
16
17
  import { SCOPED_RESOURCES } from '@shell/config/roles';
18
+ import { Banner } from '@components/Banner';
17
19
 
18
20
  import { SUBTYPE_MAPPING, VERBS } from '@shell/models/management.cattle.io.roletemplate';
19
21
  import Loading from '@shell/components/Loading';
@@ -60,7 +62,8 @@ export default {
60
62
  Tabbed,
61
63
  SortableTable,
62
64
  Loading,
63
- Error
65
+ Error,
66
+ Banner
64
67
  },
65
68
 
66
69
  mixins: [CreateEditView, FormValidation],
@@ -162,6 +165,12 @@ export default {
162
165
  },
163
166
 
164
167
  computed: {
168
+ ...mapGetters(['releaseNotesUrl']),
169
+
170
+ showRestrictedAdminDeprecationBanner() {
171
+ return this.value.subtype === GLOBAL && this.value.id === 'restricted-admin';
172
+ },
173
+
165
174
  label() {
166
175
  return this.t(`rbac.roletemplate.subtypes.${ this.value.subtype }.label`);
167
176
  },
@@ -541,6 +550,13 @@ export default {
541
550
  @finish="save"
542
551
  @cancel="cancel"
543
552
  >
553
+ <Banner
554
+ v-if="showRestrictedAdminDeprecationBanner"
555
+ color="warning"
556
+ class="mb-20"
557
+ >
558
+ <span v-clean-html="t('rbac.globalRoles.role.restricted-admin.deprecation', { releaseNotesUrl }, true)" />
559
+ </Banner>
544
560
  <template v-if="isDetail">
545
561
  <SortableTable
546
562
  key-field="index"
@@ -164,6 +164,10 @@ export default {
164
164
  removeAt(this.rows, index);
165
165
  this.queueUpdate();
166
166
  },
167
+
168
+ /**
169
+ * Cleanup rows and emit input
170
+ */
167
171
  update() {
168
172
  if ( this.isView ) {
169
173
  return;
@@ -180,22 +184,24 @@ export default {
180
184
  }
181
185
  this.$emit('input', out);
182
186
  },
187
+
188
+ /**
189
+ * Handle paste event, e.g. split multiple lines in rows
190
+ */
183
191
  onPaste(index, event) {
184
- if (this.valueMultiline) {
185
- return;
186
- }
187
192
  event.preventDefault();
188
193
  const text = event.clipboardData.getData('text/plain');
189
- const split = text.split('\n').map((value) => ({ value }));
190
-
191
- if (split.length === 1) {
192
- // It's not multi-line, so don't treat it as such
193
- return;
194
- }
195
194
 
196
- event.preventDefault();
195
+ if (this.valueMultiline) {
196
+ // Allow to paste multiple lines
197
+ this.rows[index].value = text;
198
+ } else {
199
+ // Prevent to paste the value and emit text in multiple rows
200
+ const split = text.split('\n').map((value) => ({ value }));
197
201
 
198
- this.rows.splice(index, 1, ...split);
202
+ event.preventDefault();
203
+ this.rows.splice(index, 1, ...split);
204
+ }
199
205
 
200
206
  this.update();
201
207
  }
@@ -256,6 +262,7 @@ export default {
256
262
  v-if="valueMultiline"
257
263
  ref="value"
258
264
  v-model="row.value"
265
+ :data-testid="`textarea-${idx}`"
259
266
  :placeholder="valuePlaceholder"
260
267
  :mode="mode"
261
268
  :disabled="disabled"
@@ -266,6 +273,7 @@ export default {
266
273
  v-else-if="rules.length > 0"
267
274
  ref="value"
268
275
  v-model="row.value"
276
+ :data-testid="`labeled-input-${idx}`"
269
277
  :placeholder="valuePlaceholder"
270
278
  :disabled="isView || disabled"
271
279
  :rules="rules"
@@ -277,6 +285,7 @@ export default {
277
285
  v-else
278
286
  ref="value"
279
287
  v-model="row.value"
288
+ :data-testid="`input-${idx}`"
280
289
  :placeholder="valuePlaceholder"
281
290
  :disabled="isView || disabled"
282
291
  @paste="onPaste(idx, $event)"
@@ -1,6 +1,8 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import ArrayList from '@shell/components/form/ArrayList.vue';
3
3
  import { _EDIT, _VIEW } from '@shell/config/query-params';
4
+ import { ExtendedVue, Vue } from 'vue/types/vue';
5
+ import { DefaultProps } from 'vue/types/options';
4
6
 
5
7
  describe('the ArrayList', () => {
6
8
  it('is empty', () => {
@@ -71,4 +73,46 @@ describe('the ArrayList', () => {
71
73
 
72
74
  expect(arrayListButtons).toHaveLength(0);
73
75
  });
76
+
77
+ describe('onPaste', () => {
78
+ it('should emit value with updated row text', () => {
79
+ const text = 'test';
80
+ const expectation = [text];
81
+ const wrapper = mount(ArrayList as unknown as ExtendedVue<Vue, {}, {}, {}, DefaultProps>, { propsData: { value: [''] } });
82
+ const event = { preventDefault: jest.fn(), clipboardData: { getData: jest.fn().mockReturnValue(text) } } as any;
83
+
84
+ wrapper.vm.onPaste(0, event);
85
+
86
+ expect(wrapper.emitted().input?.[0][0]).toStrictEqual(expectation);
87
+ });
88
+
89
+ it('should emit value with multiple rows', () => {
90
+ const wrapper = mount(ArrayList as unknown as ExtendedVue<Vue, {}, {}, {}, DefaultProps>, { propsData: { value: [''] } });
91
+ const text = `multiline
92
+ rows`;
93
+ const expectation = ['multiline', 'rows'];
94
+ const event = { preventDefault: jest.fn(), clipboardData: { getData: jest.fn().mockReturnValue(text) } } as any;
95
+
96
+ wrapper.vm.onPaste(0, event);
97
+
98
+ expect(wrapper.emitted().input?.[0][0]).toStrictEqual(expectation);
99
+ });
100
+
101
+ it('should allow emit multiline pasted values if enabled', () => {
102
+ const wrapper = mount(ArrayList as unknown as ExtendedVue<Vue, {}, {}, {}, DefaultProps>, {
103
+ propsData: {
104
+ value: [''],
105
+ valueMultiline: true,
106
+ }
107
+ });
108
+ const text = `multiline
109
+ text`;
110
+ const expectation = [text];
111
+ const event = { preventDefault: jest.fn(), clipboardData: { getData: jest.fn().mockReturnValue(text) } } as any;
112
+
113
+ wrapper.vm.onPaste(0, event);
114
+
115
+ expect(wrapper.emitted().input?.[0][0]).toStrictEqual(expectation);
116
+ });
117
+ });
74
118
  });
@@ -706,7 +706,6 @@ export default {
706
706
  </template>
707
707
 
708
708
  <style lang="scss" scoped>
709
- $side-menu-logo-margin-left: 5px;
710
709
  // It would be nice to grab this from `Group.vue`, but there's margin, padding and border, which is overkill to var
711
710
  $side-menu-group-padding-left: 16px;
712
711
 
@@ -724,9 +723,11 @@ export default {
724
723
  &.isSingleProduct {
725
724
  display: flex;
726
725
  justify-content: center;
726
+
727
727
  // Align the icon with the side nav menu items ($side-menu-group-padding-left)
728
- // There's margin already in the icon component, so take that in to account ($side-menu-logo-margin-left)
729
- margin-left: $side-menu-group-padding-left - $side-menu-logo-margin-left;
728
+ .side-menu-logo {
729
+ margin-left: $side-menu-group-padding-left;
730
+ }
730
731
  }
731
732
  }
732
733
 
@@ -815,7 +816,7 @@ export default {
815
816
  display: flex;
816
817
  margin-right: 8px;
817
818
  height: 55px;
818
- margin-left: $side-menu-logo-margin-left;
819
+ margin-left: 5px;
819
820
  max-width: 200px;
820
821
  padding: 12px 0;
821
822
  }
@@ -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
  >