@rancher/shell 0.3.23 → 0.3.25

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 (149) hide show
  1. package/assets/styles/base/_variables.scss +1 -0
  2. package/assets/styles/themes/_dark.scss +1 -0
  3. package/assets/styles/themes/_light.scss +6 -5
  4. package/assets/translations/en-us.yaml +44 -17
  5. package/assets/translations/zh-hans.yaml +2 -2
  6. package/components/ClusterIconMenu.vue +143 -0
  7. package/components/CruResource.vue +7 -1
  8. package/components/ExplorerProjectsNamespaces.vue +11 -1
  9. package/components/FixedBanner.vue +17 -1
  10. package/components/Loading.vue +1 -1
  11. package/components/Markdown.vue +1 -1
  12. package/components/Questions/__tests__/Yaml.test.ts +3 -2
  13. package/components/SideNav.vue +1 -1
  14. package/components/SortableTable/index.vue +3 -2
  15. package/components/auth/RoleDetailEdit.vue +15 -2
  16. package/components/auth/login/saml.vue +12 -1
  17. package/components/form/LabeledSelect.vue +12 -5
  18. package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
  19. package/components/form/Members/MembershipEditor.vue +6 -1
  20. package/components/form/SelectOrCreateAuthSecret.vue +7 -0
  21. package/components/form/__tests__/KeyValue.test.ts +6 -3
  22. package/components/form/__tests__/LabeledSelect.test.ts +18 -0
  23. package/components/formatter/PodsUsage.vue +11 -36
  24. package/components/formatter/PrincipalGroupBindings.vue +8 -5
  25. package/components/formatter/__tests__/PodsUsage.test.ts +36 -19
  26. package/components/nav/Group.vue +62 -34
  27. package/components/nav/Header.vue +13 -6
  28. package/components/nav/Pinned.vue +47 -0
  29. package/components/nav/TopLevelMenu.vue +673 -325
  30. package/components/nav/Type.vue +88 -8
  31. package/config/home-links.js +1 -1
  32. package/config/product/istio.js +15 -5
  33. package/config/router.js +3 -9
  34. package/config/table-headers.js +5 -6
  35. package/config/uiplugins.js +1 -0
  36. package/core/plugin-helpers.js +3 -0
  37. package/core/types.ts +6 -1
  38. package/creators/app/files/.vscode/settings.json +0 -1
  39. package/creators/pkg/init +2 -2
  40. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +118 -0
  41. package/detail/autoscaling.horizontalpodautoscaler/index.vue +4 -4
  42. package/detail/provisioning.cattle.io.cluster.vue +7 -5
  43. package/edit/__tests__/management.cattle.io.clusterroletemplatebinding.test.ts +58 -0
  44. package/edit/__tests__/namespace.test.ts +5 -3
  45. package/edit/fleet.cattle.io.gitrepo.vue +43 -15
  46. package/edit/logging.banzaicloud.io.output/index.vue +7 -0
  47. package/edit/management.cattle.io.clusterroletemplatebinding.vue +3 -11
  48. package/edit/namespace.vue +8 -4
  49. package/edit/provisioning.cattle.io.cluster/Basics.vue +662 -0
  50. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +9 -8
  51. package/edit/provisioning.cattle.io.cluster/DrainOptions.vue +13 -8
  52. package/edit/provisioning.cattle.io.cluster/MachinePool.vue +11 -2
  53. package/edit/provisioning.cattle.io.cluster/MemberRoles.vue +40 -0
  54. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.tests.ts +237 -0
  55. package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.tests.ts +71 -23
  56. package/edit/provisioning.cattle.io.cluster/__tests__/DrainOptions.test.ts +52 -0
  57. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +65 -142
  58. package/edit/provisioning.cattle.io.cluster/rke2.vue +253 -582
  59. package/edit/workload/storage/ContainerMountPaths.vue +7 -5
  60. package/edit/workload/storage/__tests__/Storage.test.ts +2 -2
  61. package/edit/workload/storage/persistentVolumeClaim/__tests__/persistentvolumeclaim.test.ts +36 -0
  62. package/edit/workload/storage/persistentVolumeClaim/persistentvolumeclaim.vue +15 -7
  63. package/initialize/App.js +2 -0
  64. package/initialize/client.js +63 -51
  65. package/initialize/index.js +7 -5
  66. package/layouts/default.vue +10 -2
  67. package/layouts/home.vue +6 -2
  68. package/layouts/plain.vue +9 -2
  69. package/list/fleet.cattle.io.cluster.vue +2 -2
  70. package/list/management.cattle.io.feature.vue +1 -1
  71. package/machine-config/amazonec2.vue +1 -0
  72. package/machine-config/vmwarevsphere.vue +48 -7
  73. package/mixins/brand.js +0 -8
  74. package/mixins/child-hook.js +2 -2
  75. package/mixins/create-edit-view/impl.js +3 -3
  76. package/mixins/fetch.client.js +3 -3
  77. package/models/__tests__/management.cattle.io.node.ts +96 -0
  78. package/models/__tests__/node.ts +74 -0
  79. package/models/cluster/node.js +6 -5
  80. package/models/cluster.x-k8s.io.machinedeployment.js +2 -2
  81. package/models/management.cattle.io.cluster.js +22 -1
  82. package/models/management.cattle.io.clusterroletemplatebinding.js +3 -3
  83. package/models/management.cattle.io.globalrole.js +17 -2
  84. package/models/management.cattle.io.node.js +6 -4
  85. package/models/management.cattle.io.projectroletemplatebinding.js +3 -3
  86. package/models/management.cattle.io.roletemplate.js +17 -2
  87. package/package.json +2 -6
  88. package/pages/__tests__/prefs.test.ts +1 -1
  89. package/pages/about.vue +2 -0
  90. package/pages/auth/setup.vue +5 -4
  91. package/pages/c/_cluster/explorer/ConfigBadge.vue +1 -0
  92. package/pages/c/_cluster/monitoring/index.vue +8 -3
  93. package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +9 -66
  94. package/pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue +182 -0
  95. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +15 -32
  96. package/pages/c/_cluster/uiplugins/UninstallDialog.vue +8 -46
  97. package/pages/c/_cluster/uiplugins/index.vue +64 -64
  98. package/pages/diagnostic.vue +0 -39
  99. package/pages/home.vue +1 -1
  100. package/pages/prefs.vue +3 -13
  101. package/plugins/dashboard-store/normalize.js +4 -4
  102. package/plugins/dashboard-store/resource-class.js +1 -1
  103. package/plugins/int-number.js +5 -2
  104. package/plugins/positive-int-number.js +19 -0
  105. package/plugins/steve/__tests__/getters.spec.ts +15 -0
  106. package/plugins/steve/getters.js +22 -10
  107. package/public/index.html +4 -2
  108. package/rancher-components/BadgeState/BadgeState.vue +5 -1
  109. package/rancher-components/Banner/Banner.test.ts +51 -1
  110. package/rancher-components/Banner/Banner.vue +134 -53
  111. package/rancher-components/Card/Card.test.ts +37 -0
  112. package/rancher-components/Card/Card.vue +24 -7
  113. package/rancher-components/Form/Checkbox/Checkbox.test.ts +20 -29
  114. package/rancher-components/Form/Checkbox/Checkbox.vue +45 -20
  115. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +2 -8
  116. package/rancher-components/Form/LabeledInput/LabeledInput.vue +22 -10
  117. package/rancher-components/Form/Radio/RadioButton.test.ts +31 -0
  118. package/rancher-components/Form/Radio/RadioButton.vue +30 -13
  119. package/rancher-components/Form/Radio/RadioGroup.vue +26 -7
  120. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +7 -6
  121. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +25 -38
  122. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +23 -11
  123. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +19 -5
  124. package/rancher-components/StringList/StringList.test.ts +453 -49
  125. package/rancher-components/StringList/StringList.vue +92 -58
  126. package/scripts/extension/parse-tag-name +0 -0
  127. package/store/index.js +4 -0
  128. package/store/prefs.js +4 -4
  129. package/store/type-map.js +2 -16
  130. package/types/shell/index.d.ts +26 -14
  131. package/utils/__tests__/cluster.test.ts +55 -0
  132. package/utils/__tests__/object.test.ts +21 -2
  133. package/utils/__tests__/sort.test.ts +61 -0
  134. package/utils/cluster.js +47 -1
  135. package/utils/object.js +12 -5
  136. package/utils/string.js +12 -0
  137. package/utils/validators/formRules/__tests__/index.test.ts +13 -1
  138. package/utils/validators/formRules/index.ts +4 -0
  139. package/utils/validators/role-template.js +9 -1
  140. package/utils/version.js +1 -1
  141. package/vue.config.js +1 -4
  142. package/yarn-error.log +200 -0
  143. package/content/docs/en-us/getting-started.md +0 -224
  144. package/content/docs/en-us/whats-new.md +0 -29
  145. package/content/docs/zh-hans/getting-started.md +0 -224
  146. package/content/docs/zh-hans/whats-new.md +0 -28
  147. package/pages/docs/_doc.vue +0 -345
  148. package/pages/docs/toc.js +0 -27
  149. package/plugins/console.js +0 -34
@@ -5,7 +5,18 @@ export default {
5
5
 
6
6
  methods: {
7
7
  async login() {
8
- const res = await this.$store.dispatch('auth/login', { provider: this.name, body: { finalRedirectUrl: window.location.origin } });
8
+ const { requestId, publicKey, responseType } = this.$route.query;
9
+
10
+ const res = await this.$store.dispatch('auth/login', {
11
+ provider: this.name,
12
+ body: {
13
+ finalRedirectUrl: window.location.origin,
14
+ requestId,
15
+ publicKey,
16
+ responseType
17
+ }
18
+ });
19
+
9
20
  const { idpRedirectUrl } = res;
10
21
 
11
22
  window.location.href = idpRedirectUrl;
@@ -149,11 +149,10 @@ export default {
149
149
  return;
150
150
  }
151
151
 
152
- // Force to update the option label if prop has been changed
153
- const isOutdated = !this.options.find((opt) => option[this.optionLabel] === opt[this.optionLabel]);
154
-
155
- if (isOutdated && this.options) {
156
- const newOption = this.options.find((opt) => isEqual(this.reduce(option), this.reduce(opt)));
152
+ // This check is only needed if its possible for an option's label to change without the option's value changing - we can skip this if options are just strings or numbers
153
+ // HOWEVER even if strings are passed to v-select the 'option' in the slot is normalized to {label: <option>} so we have to check the options prop here instead of the 'option' itself
154
+ if (typeof this.options[0] === 'object') {
155
+ const newOption = this.getUpdatedOption(option);
157
156
 
158
157
  if (newOption) {
159
158
  const label = get(newOption, this.optionLabel);
@@ -178,6 +177,14 @@ export default {
178
177
  }
179
178
  },
180
179
 
180
+ // If the option's label changed in parent but value did not, the label wont be automatically updated here
181
+ // Ensure that the label being shown is still present in the options prop and find the new one if not
182
+ getUpdatedOption(option) {
183
+ const isOutdated = this.options && !this.options.find((opt) => option[this.optionLabel] === opt[this.optionLabel]);
184
+
185
+ return isOutdated ? this.options.find((opt) => isEqual(this.reduce(option), this.reduce(opt))) : undefined;
186
+ },
187
+
181
188
  positionDropdown(dropdownList, component, { width }) {
182
189
  calculatePosition(dropdownList, component, width, this.placement);
183
190
  },
@@ -198,7 +198,7 @@ export default {
198
198
  async principalProperty() {
199
199
  const principal = await this.principal;
200
200
 
201
- return principal.principalType === 'group' ? 'groupPrincipalId' : 'userPrincipalId';
201
+ return principal?.principalType === 'group' ? 'groupPrincipalId' : 'userPrincipalId';
202
202
  },
203
203
 
204
204
  onAdd(principalId) {
@@ -56,8 +56,13 @@ export default {
56
56
  },
57
57
 
58
58
  async fetch() {
59
+ const roleBindingRequestParams = { type: this.type, opt: { force: true } };
60
+
61
+ if (this.type === NORMAN.PROJECT_ROLE_TEMPLATE_BINDING && this.parentId) {
62
+ Object.assign(roleBindingRequestParams, { opt: { filter: { projectId: this.parentId.split('/').join(':') } } });
63
+ }
59
64
  const userHydration = [
60
- this.schema ? this.$store.dispatch(`rancher/findAll`, { type: this.type, opt: { force: true } }) : [],
65
+ this.schema ? this.$store.dispatch(`rancher/findAll`, roleBindingRequestParams) : [],
61
66
  this.$store.dispatch('rancher/findAll', { type: NORMAN.PRINCIPAL }),
62
67
  this.$store.dispatch(`management/findAll`, { type: MANAGEMENT.ROLE_TEMPLATE }),
63
68
  this.$store.dispatch(`management/findAll`, { type: MANAGEMENT.USER })
@@ -477,6 +477,7 @@ export default {
477
477
  <div :class="firstCol">
478
478
  <LabeledSelect
479
479
  v-model="selected"
480
+ data-testid="auth-secret-select"
480
481
  :mode="mode"
481
482
  :label-key="labelKey"
482
483
  :loading="$fetchState.pending"
@@ -488,6 +489,7 @@ export default {
488
489
  <div :class="moreCols">
489
490
  <LabeledInput
490
491
  v-model="publicKey"
492
+ data-testid="auth-secret-ssh-public-key"
491
493
  :mode="mode"
492
494
  type="multiline"
493
495
  label-key="selectOrCreateAuthSecret.ssh.publicKey"
@@ -496,6 +498,7 @@ export default {
496
498
  <div :class="moreCols">
497
499
  <LabeledInput
498
500
  v-model="privateKey"
501
+ data-testid="auth-secret-ssh-private-key"
499
502
  :mode="mode"
500
503
  type="multiline"
501
504
  label-key="selectOrCreateAuthSecret.ssh.privateKey"
@@ -506,6 +509,7 @@ export default {
506
509
  <div :class="moreCols">
507
510
  <LabeledInput
508
511
  v-model="publicKey"
512
+ data-testid="auth-secret-basic-public-key"
509
513
  :mode="mode"
510
514
  label-key="selectOrCreateAuthSecret.basic.username"
511
515
  />
@@ -513,6 +517,7 @@ export default {
513
517
  <div :class="moreCols">
514
518
  <LabeledInput
515
519
  v-model="privateKey"
520
+ data-testid="auth-secret-basic-private-key"
516
521
  :mode="mode"
517
522
  type="password"
518
523
  label-key="selectOrCreateAuthSecret.basic.password"
@@ -523,6 +528,7 @@ export default {
523
528
  <div :class="moreCols">
524
529
  <LabeledInput
525
530
  v-model="publicKey"
531
+ data-testid="auth-secret-s3-public-key"
526
532
  :mode="mode"
527
533
  label-key="selectOrCreateAuthSecret.s3.accessKey"
528
534
  />
@@ -530,6 +536,7 @@ export default {
530
536
  <div :class="moreCols">
531
537
  <LabeledInput
532
538
  v-model="privateKey"
539
+ data-testid="auth-secret-s3-private-key"
533
540
  :mode="mode"
534
541
  type="password"
535
542
  label-key="selectOrCreateAuthSecret.s3.secretKey"
@@ -8,7 +8,8 @@ describe('component: KeyValue', () => {
8
8
  const wrapper = mount(KeyValue, {
9
9
  propsData: { value: { value } },
10
10
  mocks: { $store: { getters: { 'i18n/t': jest.fn() } } },
11
- directives: { t }
11
+ directives: { t },
12
+ stubs: { CodeMirror: true }
12
13
  });
13
14
 
14
15
  const inputValue = wrapper.find('textarea').element as HTMLInputElement;
@@ -24,7 +25,8 @@ describe('component: KeyValue', () => {
24
25
  valueMarkdownMultiline: true,
25
26
  },
26
27
  mocks: { $store: { getters: { 'i18n/t': jest.fn() } } },
27
- directives: { t }
28
+ directives: { t },
29
+ stubs: { CodeMirror: true }
28
30
  });
29
31
 
30
32
  const inputFieldTextArea = wrapper.find('textarea').element;
@@ -41,7 +43,8 @@ describe('component: KeyValue', () => {
41
43
  valueMarkdownMultiline: false,
42
44
  },
43
45
  mocks: { $store: { getters: { 'i18n/t': jest.fn() } } },
44
- directives: { t }
46
+ directives: { t },
47
+ stubs: { CodeMirror: true }
45
48
  });
46
49
 
47
50
  const inputFieldTextArea = wrapper.find('[data-testid="text-area-auto-grow"]');
@@ -133,6 +133,24 @@ describe('component: LabeledSelect', () => {
133
133
  // Component is from a library and class is not going to be changed
134
134
  expect(wrapper.find('.vs__selected').text()).toBe(translation);
135
135
  });
136
+
137
+ it.each([
138
+ [['a'], 'b', 0],
139
+ [[{ value: 'a', label: 'A' }], { value: 'a', label: 'B' }, 4]
140
+ ])('should only check for a new label if options are objects', async(options: any[], newOption, checkUpdateCalled: number) => {
141
+ const spyUpdatedOption = jest.spyOn(LabeledSelect.methods, 'getUpdatedOption');
142
+
143
+ const wrapper = mount(LabeledSelect, {
144
+ propsData: {
145
+ value: 'a',
146
+ options
147
+ }
148
+ });
149
+
150
+ await wrapper.setProps({ options: [newOption] });
151
+
152
+ expect(spyUpdatedOption).toHaveBeenCalledTimes(checkUpdateCalled);
153
+ });
136
154
  });
137
155
  });
138
156
  });
@@ -1,5 +1,4 @@
1
1
  <script>
2
- import { POD } from '@shell/config/types';
3
2
  export default {
4
3
  name: 'PodsUsage',
5
4
  props: {
@@ -8,47 +7,23 @@ export default {
8
7
  required: true
9
8
  },
10
9
  },
11
- data() {
12
- return {
13
- loading: true,
14
- podsUsage: null
15
- };
16
- },
17
- methods: {
18
- async startDelayedLoading() {
19
- const id = this.row?.mgmt?.id;
20
-
21
- if (this.row?.isReady && id) {
22
- const req = await this.$store.dispatch('management/request', { url: `/k8s/clusters/${ id }/v1/counts` });
10
+ computed: {
11
+ podsUsage() {
12
+ const usedPods = this.row?.mgmt?.status?.requested?.pods;
13
+ const totalPods = this.row?.mgmt?.status?.allocatable?.pods;
23
14
 
24
- this.loading = false;
25
- const usedPods = req.data?.[0]?.counts[POD]?.summary?.count || 0;
26
- const totalPods = this.row?.mgmt?.status?.allocatable?.pods;
27
-
28
- if (totalPods) {
29
- this.podsUsage = `${ usedPods }/${ totalPods }`;
30
- } else {
31
- this.podsUsage = '—';
32
- }
33
- } else {
34
- this.loading = false;
35
- this.podsUsage = '—';
15
+ if (!this.row?.isReady || !totalPods) {
16
+ return '—';
36
17
  }
18
+
19
+ return `${ usedPods || 0 }/${ totalPods }`;
37
20
  }
38
- },
21
+ }
39
22
  };
40
23
  </script>
41
24
 
42
25
  <template>
43
- <i
44
- v-if="loading"
45
- class="icon icon-spinner icon-spin"
46
- />
47
- <p v-else>
48
- {{ podsUsage }}
26
+ <p>
27
+ <span>{{ podsUsage }}</span>
49
28
  </p>
50
29
  </template>
51
-
52
- <style lang="scss" scoped>
53
-
54
- </style>
@@ -11,12 +11,15 @@ export default {
11
11
  computed: {
12
12
 
13
13
  boundRoles() {
14
- const principal = this.$store.getters['rancher/byId'](NORMAN.PRINCIPAL, this.value);
14
+ // need to use getter to fetch all NORMAN.PRINCIPAL, otherwise `rancher/byId` is not reactive...
15
+ const principals = this.$store.getters['rancher/all'](NORMAN.PRINCIPAL);
15
16
  const globalRoleBindings = this.$store.getters['management/all'](MANAGEMENT.GLOBAL_ROLE_BINDING);
16
17
 
18
+ const principal = principals.find((x) => x.id === this.value);
19
+
17
20
  return globalRoleBindings
18
21
  // Bindings for this group
19
- .filter((globalRoleBinding) => globalRoleBinding.groupPrincipalName === principal.id)
22
+ .filter((globalRoleBinding) => globalRoleBinding.groupPrincipalName === principal?.id)
20
23
  // Display name of role associated with binding
21
24
  .map((binding) => {
22
25
  const role = this.$store.getters['management/byId'](MANAGEMENT.GLOBAL_ROLE, binding.globalRoleName);
@@ -35,9 +38,9 @@ export default {
35
38
  <template>
36
39
  <div class="pgb">
37
40
  <template v-for="(role, i) in boundRoles">
38
- <nuxt-link
39
- :key="role.id"
40
- :to="role.detailLocation"
41
+ <nuxt-link
42
+ :key="role.id"
43
+ :to="role.detailLocation"
41
44
  >
42
45
  {{ role.label }}
43
46
  </nuxt-link>
@@ -2,37 +2,54 @@ import { mount } from '@vue/test-utils';
2
2
  import PodsUsage from '@shell/components/formatter/PodsUsage.vue';
3
3
 
4
4
  describe('component: PodsUsage', () => {
5
- it('should not display values if data is not ready', () => {
5
+ it('should display podsUsage value', () => {
6
6
  const wrapper = mount(PodsUsage, {
7
- propsData: { row: {} },
8
- mocks: { $store: { dispatch: { 'management/request': jest.fn() } } }
7
+ propsData: {
8
+ row: {
9
+ isReady: true,
10
+ mgmt: {
11
+ status: {
12
+ requested: { pods: 10 },
13
+ allocatable: { pods: 20 }
14
+ }
15
+ }
16
+ }
17
+ },
18
+ mocks: { $store: { dispatch: { 'management/request': jest.fn() } } }
9
19
  });
10
20
 
11
- const element = wrapper.find('p').element;
21
+ const { element } = wrapper.find('p');
12
22
 
13
- expect(element).toBeUndefined();
23
+ expect(element.textContent).toBeDefined();
24
+ expect(element.textContent).toBe('10/20');
14
25
  });
15
-
16
- it('should display spinning icon', () => {
26
+ it('should display dash when there are no totalPods', () => {
17
27
  const wrapper = mount(PodsUsage, {
18
- propsData: { row: {} },
19
- mocks: { $store: { dispatch: { 'management/request': jest.fn() } } }
28
+ propsData: {
29
+ row: {
30
+ isReady: true,
31
+ mgmt: {
32
+ status: {
33
+ requested: { pods: 10 },
34
+ allocatable: { pods: 0 }
35
+ }
36
+ }
37
+ }
38
+ },
39
+ mocks: { $store: { dispatch: { 'management/request': jest.fn() } } }
20
40
  });
21
41
 
22
- const element = wrapper.find('i').element;
42
+ const { element } = wrapper.find('p');
23
43
 
24
- expect(element).toBeDefined();
44
+ expect(element.textContent).toBeDefined();
45
+ expect(element.textContent).toBe('—');
25
46
  });
47
+ it('should display a dash when there is no management cluster ti query for status', () => {
48
+ const wrapper = mount(PodsUsage, { propsData: { row: { isReady: true } } });
26
49
 
27
- it('should display podsUsage value', () => {
28
- const wrapper = mount(PodsUsage, {
29
- propsData: { row: { isReady: true } },
30
- data: () => ({ loading: false }),
31
- mocks: { $store: { dispatch: { 'management/request': jest.fn() } } }
32
- });
33
-
34
- const element = wrapper.find('p').element;
50
+ const { element } = wrapper.find('p');
35
51
 
36
52
  expect(element.textContent).toBeDefined();
53
+ expect(element.textContent).toBe('—');
37
54
  });
38
55
  });
@@ -49,6 +49,10 @@ export default {
49
49
  },
50
50
 
51
51
  computed: {
52
+ isGroupActive() {
53
+ return this.isOverview || (this.hasActiveRoute() && this.isExpanded && this.showHeader);
54
+ },
55
+
52
56
  hasChildren() {
53
57
  return this.group.children?.length > 0;
54
58
  },
@@ -69,7 +73,7 @@ export default {
69
73
  if (overviewRoute && grp.overview) {
70
74
  const route = this.$router.resolve(overviewRoute || {});
71
75
 
72
- return this.$route.fullPath === route?.route?.fullPath;
76
+ return this.$route.fullPath.split('#')[0] === route?.route?.fullPath;
73
77
  }
74
78
  }
75
79
 
@@ -96,8 +100,14 @@ export default {
96
100
  // Don't auto-select first group entry if we're already expanded and contain the currently-selected nav item
97
101
  if (this.hasActiveRoute() && this.isExpanded) {
98
102
  return;
99
- }
103
+ } else {
104
+ // Remove all active class if click on group header and not active route
105
+ const headerEl = document.querySelectorAll('.header');
100
106
 
107
+ headerEl.forEach((el) => {
108
+ el.classList.remove('active');
109
+ });
110
+ }
101
111
  this.expandGroup();
102
112
 
103
113
  const items = this.group[this.childrenKey];
@@ -132,6 +142,11 @@ export default {
132
142
 
133
143
  // User clicked on the expander icon, so toggle the expansion so the user can see inside the group
134
144
  peek($event) {
145
+ // Add active class to the current header if click on chevron icon
146
+ $event.target.parentElement.classList.remove('active');
147
+ if (this.hasActiveRoute() && this.isExpanded) {
148
+ $event.target.parentElement.classList.add('active');
149
+ }
135
150
  this.isExpanded = !this.isExpanded;
136
151
  $event.stopPropagation();
137
152
  },
@@ -188,7 +203,7 @@ export default {
188
203
  <template>
189
204
  <div
190
205
  class="accordion"
191
- :class="{[`depth-${depth}`]: true, 'expanded': isExpanded, 'has-children': hasChildren}"
206
+ :class="{[`depth-${depth}`]: true, 'expanded': isExpanded, 'has-children': hasChildren, 'group-highlight': isGroupActive}"
192
207
  >
193
208
  <div
194
209
  v-if="showHeader"
@@ -212,7 +227,7 @@ export default {
212
227
  <i
213
228
  v-if="!onlyHasOverview && canCollapse"
214
229
  class="icon toggle"
215
- :class="{'icon-chevron-down': !isExpanded, 'icon-chevron-up': isExpanded}"
230
+ :class="{'icon-chevron-right': !isExpanded, 'icon-chevron-down': isExpanded}"
216
231
  @click="peek($event, true)"
217
232
  />
218
233
  </div>
@@ -267,8 +282,9 @@ export default {
267
282
  position: relative;
268
283
  cursor: pointer;
269
284
  color: var(--body-text);
285
+ height: 33px;
270
286
 
271
- > H6 {
287
+ H6 {
272
288
  color: var(--body-text);
273
289
  user-select: none;
274
290
  text-transform: none;
@@ -277,39 +293,37 @@ export default {
277
293
 
278
294
  > A {
279
295
  display: block;
280
- padding-left: 10px;
296
+ padding-left: 16px;
281
297
  &:hover{
282
- text-decoration: none;
283
- }
298
+ text-decoration: none;
299
+ }
284
300
  &:focus{
285
301
  outline:none;
286
302
  }
287
303
  > H6 {
288
- font-size: 14px;
289
304
  text-transform: none;
290
305
  }
291
306
  }
292
-
293
- &.active {
294
- background-color: var(--nav-active);
295
- }
296
- }
297
-
298
- .body {
299
- margin-left: 10px;
300
307
  }
301
308
 
302
309
  .accordion {
303
310
  .header {
304
- &:hover:not(.noHover) {
305
- background-color: var(--nav-hover);
306
- }
311
+ &.active {
312
+ color: var(--primary-hover-text);
313
+ background-color: var(--primary-hover-bg);
314
+
315
+ h6 {
316
+ font-weight: bold;
317
+ color: var(--primary-hover-text);
318
+ }
307
319
 
308
- > I {
309
320
  &:hover {
310
- background-color: var(--nav-expander-hover);
321
+ background-color: var(--primary-hover-bg);
311
322
  }
312
323
  }
324
+ &:hover:not(.active) {
325
+ background-color: var(--nav-hover);
326
+ }
313
327
  }
314
328
  }
315
329
 
@@ -323,16 +337,15 @@ export default {
323
337
  }
324
338
 
325
339
  > H6 {
326
- font-size: 14px;
327
340
  text-transform: none;
328
- padding-left: 10px;
341
+ padding-left: 16px;
329
342
  }
330
343
 
331
344
  > I {
332
345
  position: absolute;
333
346
  right: 0;
334
347
  top: 0;
335
- padding: 10px 7px 9px 7px;
348
+ padding: 10px 10px 9px 7px;
336
349
  user-select: none;
337
350
  }
338
351
  }
@@ -340,24 +353,27 @@ export default {
340
353
  > .body {
341
354
  margin-left: 0;
342
355
  }
356
+
357
+ &.group-highlight {
358
+ background: var(--nav-active);
359
+ }
343
360
  }
344
361
 
345
362
  &.depth-1 {
346
363
  > .header {
364
+ padding-left: 20px;
347
365
  > H6 {
348
- font-size: 13px;
349
- line-height: 16px;
366
+ line-height: 18px;
350
367
  padding: 8px 0 7px 5px !important;
351
368
  }
352
369
  > I {
353
- padding: 9px 7px 8px 7px !important;
370
+ padding: 10px 7px 9px 7px !important;
354
371
  }
355
372
  }
356
373
  }
357
374
 
358
375
  &:not(.depth-0) {
359
376
  > .header {
360
- padding-left: 10px;
361
377
  > H6 {
362
378
  // Child groups that aren't linked themselves
363
379
  display: inline-block;
@@ -374,16 +390,18 @@ export default {
374
390
  }
375
391
  }
376
392
 
377
- .body ::v-deep > .child.nuxt-link-active,
378
- .header ::v-deep > .child.nuxt-link-exact-active {
393
+ .body ::v-deep > .child.nuxt-link-active,
394
+ .header ::v-deep > .child.nuxt-link-exact-active {
379
395
  padding: 0;
380
396
 
381
397
  A, A I {
382
- color: var(--body-text);
398
+ color: var(--primary-hover-text);
383
399
  }
384
400
 
385
401
  A {
386
- background-color: var(--nav-active);
402
+ color: var(--primary-hover-text);
403
+ background-color: var(--primary-hover-bg);
404
+ font-weight: bold;
387
405
  }
388
406
  }
389
407
 
@@ -391,11 +409,21 @@ export default {
391
409
  A {
392
410
  border-left: solid 5px transparent;
393
411
  line-height: 16px;
394
- font-size: 13px;
412
+ font-size: 14px;
413
+ padding-left: 24px;
414
+ display: flex;
415
+ justify-content: space-between;
395
416
  }
396
417
 
397
418
  A:focus {
398
419
  outline: none;
399
420
  }
421
+
422
+ &.root {
423
+ background: transparent;
424
+ A {
425
+ padding-left: 14px;
426
+ }
427
+ }
400
428
  }
401
429
  </style>
@@ -60,8 +60,8 @@ export default {
60
60
  },
61
61
 
62
62
  computed: {
63
- ...mapGetters(['clusterReady', 'isExplorer', 'isMultiCluster', 'isRancher', 'currentCluster',
64
- 'currentProduct', 'backToRancherLink', 'backToRancherGlobalLink', 'pageActions', 'isSingleProduct', 'isRancherInHarvester']),
63
+ ...mapGetters(['clusterReady', 'isExplorer', 'isRancher', 'currentCluster',
64
+ 'currentProduct', 'backToRancherLink', 'backToRancherGlobalLink', 'pageActions', 'isSingleProduct', 'isRancherInHarvester', 'showTopLevelMenu']),
65
65
  ...mapGetters('type-map', ['activeProducts']),
66
66
 
67
67
  appName() {
@@ -314,7 +314,7 @@ export default {
314
314
  const enabled = action.enabled ? action.enabled.apply(this, [opts]) : true;
315
315
 
316
316
  if (fn && enabled) {
317
- fn.apply(this, [opts, []]);
317
+ fn.apply(this, [opts, [], { $route: this.$route }]);
318
318
  }
319
319
  },
320
320
 
@@ -338,7 +338,7 @@ export default {
338
338
  data-testid="header"
339
339
  >
340
340
  <div>
341
- <TopLevelMenu v-if="isRancherInHarvester || isMultiCluster || !isSingleProduct" />
341
+ <TopLevelMenu v-if="showTopLevelMenu" />
342
342
  </div>
343
343
  <div
344
344
  class="menu-spacer"
@@ -706,6 +706,10 @@ export default {
706
706
  </template>
707
707
 
708
708
  <style lang="scss" scoped>
709
+ $side-menu-logo-margin-left: 5px;
710
+ // It would be nice to grab this from `Group.vue`, but there's margin, padding and border, which is overkill to var
711
+ $side-menu-group-padding-left: 16px;
712
+
709
713
  HEADER {
710
714
  display: flex;
711
715
  z-index: z-index('mainHeader');
@@ -715,11 +719,14 @@ export default {
715
719
  }
716
720
 
717
721
  > .menu-spacer {
718
- flex: 0 0 calc(var(--header-height) + 10px);
722
+ flex: 0 0 15px;
719
723
 
720
724
  &.isSingleProduct {
721
725
  display: flex;
722
726
  justify-content: center;
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;
723
730
  }
724
731
  }
725
732
 
@@ -808,7 +815,7 @@ export default {
808
815
  display: flex;
809
816
  margin-right: 8px;
810
817
  height: 55px;
811
- margin-left: 5px;
818
+ margin-left: $side-menu-logo-margin-left;
812
819
  max-width: 200px;
813
820
  padding: 12px 0;
814
821
  }