@rancher/shell 0.3.24 → 0.3.26

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 (115) hide show
  1. package/assets/styles/themes/_light.scss +1 -1
  2. package/assets/translations/en-us.yaml +36 -7
  3. package/assets/translations/zh-hans.yaml +1 -1
  4. package/components/ClusterIconMenu.vue +143 -0
  5. package/components/CruResource.vue +10 -1
  6. package/components/ExplorerProjectsNamespaces.vue +11 -1
  7. package/components/FixedBanner.vue +17 -1
  8. package/components/Markdown.vue +1 -1
  9. package/components/Questions/__tests__/Yaml.test.ts +3 -2
  10. package/components/SortableTable/index.vue +3 -2
  11. package/components/__tests__/ProjectRow.test.ts +63 -0
  12. package/components/auth/RoleDetailEdit.vue +19 -2
  13. package/components/auth/__tests__/RoleDetailEdit.test.ts +41 -0
  14. package/components/auth/login/saml.vue +12 -1
  15. package/components/form/LabeledSelect.vue +12 -5
  16. package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
  17. package/components/form/Members/MembershipEditor.vue +6 -1
  18. package/components/form/ResourceQuota/ProjectRow.vue +6 -2
  19. package/components/form/__tests__/KeyValue.test.ts +6 -3
  20. package/components/form/__tests__/LabeledSelect.test.ts +18 -0
  21. package/components/formatter/PodsUsage.vue +11 -36
  22. package/components/formatter/PrincipalGroupBindings.vue +8 -5
  23. package/components/formatter/__tests__/PodsUsage.test.ts +36 -19
  24. package/components/nav/Group.vue +25 -27
  25. package/components/nav/Header.vue +12 -5
  26. package/components/nav/Pinned.vue +47 -0
  27. package/components/nav/TopLevelMenu.vue +233 -60
  28. package/components/nav/Type.vue +57 -3
  29. package/config/home-links.js +1 -1
  30. package/config/product/istio.js +15 -5
  31. package/config/router.js +3 -9
  32. package/config/table-headers.js +5 -6
  33. package/config/uiplugins.js +1 -0
  34. package/core/plugin-helpers.js +3 -0
  35. package/core/types.ts +6 -1
  36. package/creators/app/files/.vscode/settings.json +0 -1
  37. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +118 -0
  38. package/detail/autoscaling.horizontalpodautoscaler/index.vue +4 -4
  39. package/detail/provisioning.cattle.io.cluster.vue +7 -5
  40. package/edit/__tests__/management.cattle.io.clusterroletemplatebinding.test.ts +58 -0
  41. package/edit/__tests__/namespace.test.ts +5 -3
  42. package/edit/management.cattle.io.clusterroletemplatebinding.vue +3 -11
  43. package/edit/namespace.vue +8 -4
  44. package/edit/provisioning.cattle.io.cluster/Basics.vue +662 -0
  45. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +6 -0
  46. package/edit/provisioning.cattle.io.cluster/DrainOptions.vue +13 -8
  47. package/edit/provisioning.cattle.io.cluster/MachinePool.vue +11 -2
  48. package/edit/provisioning.cattle.io.cluster/MemberRoles.vue +40 -0
  49. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.tests.ts +237 -0
  50. package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.tests.ts +71 -23
  51. package/edit/provisioning.cattle.io.cluster/__tests__/DrainOptions.test.ts +52 -0
  52. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +65 -142
  53. package/edit/provisioning.cattle.io.cluster/rke2.vue +211 -599
  54. package/edit/workload/storage/__tests__/Storage.test.ts +2 -2
  55. package/edit/workload/storage/persistentVolumeClaim/__tests__/persistentvolumeclaim.test.ts +36 -0
  56. package/edit/workload/storage/persistentVolumeClaim/persistentvolumeclaim.vue +15 -7
  57. package/initialize/index.js +5 -5
  58. package/layouts/default.vue +6 -6
  59. package/layouts/home.vue +6 -2
  60. package/layouts/plain.vue +9 -2
  61. package/list/fleet.cattle.io.cluster.vue +2 -2
  62. package/list/management.cattle.io.feature.vue +1 -1
  63. package/machine-config/vmwarevsphere.vue +48 -7
  64. package/mixins/brand.js +0 -8
  65. package/mixins/child-hook.js +2 -2
  66. package/mixins/create-edit-view/impl.js +3 -3
  67. package/models/__tests__/management.cattle.io.node.ts +96 -0
  68. package/models/__tests__/node.ts +74 -0
  69. package/models/cluster/node.js +6 -5
  70. package/models/cluster.x-k8s.io.machinedeployment.js +2 -2
  71. package/models/management.cattle.io.cluster.js +22 -1
  72. package/models/management.cattle.io.clusterroletemplatebinding.js +3 -3
  73. package/models/management.cattle.io.globalrole.js +17 -2
  74. package/models/management.cattle.io.node.js +6 -4
  75. package/models/management.cattle.io.projectroletemplatebinding.js +3 -3
  76. package/models/management.cattle.io.roletemplate.js +17 -2
  77. package/package.json +2 -6
  78. package/pages/about.vue +2 -0
  79. package/pages/auth/setup.vue +5 -4
  80. package/pages/c/_cluster/monitoring/index.vue +8 -3
  81. package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +9 -66
  82. package/pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue +182 -0
  83. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +15 -32
  84. package/pages/c/_cluster/uiplugins/UninstallDialog.vue +8 -46
  85. package/pages/c/_cluster/uiplugins/index.vue +64 -64
  86. package/pages/diagnostic.vue +0 -39
  87. package/pages/home.vue +1 -1
  88. package/plugins/dashboard-store/normalize.js +4 -4
  89. package/plugins/int-number.js +5 -2
  90. package/plugins/positive-int-number.js +19 -0
  91. package/plugins/steve/__tests__/getters.spec.ts +15 -0
  92. package/plugins/steve/getters.js +22 -10
  93. package/rancher-components/Form/LabeledInput/LabeledInput.vue +0 -8
  94. package/rancher-components/Form/Radio/RadioButton.test.ts +3 -7
  95. package/scripts/extension/helm/charts/ui-plugin-server/Chart.yaml +2 -2
  96. package/store/index.js +4 -0
  97. package/store/prefs.js +1 -0
  98. package/types/shell/index.d.ts +13 -4
  99. package/utils/__tests__/cluster.test.ts +55 -0
  100. package/utils/__tests__/object.test.ts +21 -2
  101. package/utils/cluster.js +47 -1
  102. package/utils/object.js +12 -5
  103. package/utils/validators/formRules/__tests__/index.test.ts +13 -1
  104. package/utils/validators/formRules/index.ts +4 -0
  105. package/utils/validators/role-template.js +9 -1
  106. package/utils/version.js +1 -1
  107. package/yarn-error.log +16 -16
  108. package/components/ClusterProviderIconMenu.vue +0 -161
  109. package/content/docs/en-us/getting-started.md +0 -224
  110. package/content/docs/en-us/whats-new.md +0 -29
  111. package/content/docs/zh-hans/getting-started.md +0 -224
  112. package/content/docs/zh-hans/whats-new.md +0 -28
  113. package/pages/docs/_doc.vue +0 -345
  114. package/pages/docs/toc.js +0 -27
  115. package/plugins/console.js +0 -34
@@ -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 })
@@ -2,6 +2,7 @@
2
2
  import Select from '@shell/components/form/Select';
3
3
  import UnitInput from '@shell/components/form/UnitInput';
4
4
  import { ROW_COMPUTED } from './shared';
5
+ import Vue from 'vue';
5
6
 
6
7
  export default {
7
8
  components: { Select, UnitInput },
@@ -57,10 +58,10 @@ export default {
57
58
 
58
59
  updateQuotaLimit(prop, type, val) {
59
60
  if (!this.value.spec[prop]) {
60
- this.value.spec[prop] = { limit: { } };
61
+ Vue.set(this.value.spec, prop, { limit: { } });
61
62
  }
62
63
 
63
- this.value.spec[prop].limit[type] = val;
64
+ Vue.set(this.value.spec[prop].limit, type, val);
64
65
  }
65
66
  },
66
67
  };
@@ -75,6 +76,7 @@ export default {
75
76
  :mode="mode"
76
77
  :value="type"
77
78
  :options="types"
79
+ data-testid="projectrow-type-input"
78
80
  @input="updateType($event)"
79
81
  />
80
82
  <UnitInput
@@ -86,6 +88,7 @@ export default {
86
88
  :input-exponent="typeOption.inputExponent"
87
89
  :base-unit="typeOption.baseUnit"
88
90
  :output-modifier="true"
91
+ data-testid="projectrow-project-quota-input"
89
92
  @input="updateQuotaLimit('resourceQuota', type, $event)"
90
93
  />
91
94
  <UnitInput
@@ -96,6 +99,7 @@ export default {
96
99
  :input-exponent="typeOption.inputExponent"
97
100
  :base-unit="typeOption.baseUnit"
98
101
  :output-modifier="true"
102
+ data-testid="projectrow-namespace-quota-input"
99
103
  @input="updateQuotaLimit('namespaceDefaultResourceQuota', type, $event)"
100
104
  />
101
105
  </div>
@@ -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
  },
@@ -140,7 +144,7 @@ export default {
140
144
  peek($event) {
141
145
  // Add active class to the current header if click on chevron icon
142
146
  $event.target.parentElement.classList.remove('active');
143
- if (this.hasActiveRoute()) {
147
+ if (this.hasActiveRoute() && this.isExpanded) {
144
148
  $event.target.parentElement.classList.add('active');
145
149
  }
146
150
  this.isExpanded = !this.isExpanded;
@@ -199,7 +203,7 @@ export default {
199
203
  <template>
200
204
  <div
201
205
  class="accordion"
202
- :class="{[`depth-${depth}`]: true, 'expanded': isExpanded, 'has-children': hasChildren}"
206
+ :class="{[`depth-${depth}`]: true, 'expanded': isExpanded, 'has-children': hasChildren, 'group-highlight': isGroupActive}"
203
207
  >
204
208
  <div
205
209
  v-if="showHeader"
@@ -284,15 +288,15 @@ export default {
284
288
  color: var(--body-text);
285
289
  user-select: none;
286
290
  text-transform: none;
287
- font-size: 16px;
291
+ font-size: 14px;
288
292
  }
289
293
 
290
294
  > A {
291
295
  display: block;
292
296
  padding-left: 16px;
293
297
  &:hover{
294
- text-decoration: none;
295
- }
298
+ text-decoration: none;
299
+ }
296
300
  &:focus{
297
301
  outline:none;
298
302
  }
@@ -300,22 +304,26 @@ export default {
300
304
  text-transform: none;
301
305
  }
302
306
  }
303
- &.active {
304
- background-color: var(--nav-active);
305
- }
306
307
  }
307
308
 
308
309
  .accordion {
309
310
  .header {
310
- &:hover:not(.noHover) {
311
- background-color: var(--nav-hover);
312
- }
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
+ }
313
319
 
314
- > I {
315
320
  &:hover {
316
- background-color: var(--nav-expander-hover);
321
+ background-color: var(--primary-hover-bg);
317
322
  }
318
323
  }
324
+ &:hover:not(.active) {
325
+ background-color: var(--nav-hover);
326
+ }
319
327
  }
320
328
  }
321
329
 
@@ -340,15 +348,15 @@ export default {
340
348
  padding: 10px 10px 9px 7px;
341
349
  user-select: none;
342
350
  }
343
-
344
- &:has(> a.nuxt-link-active) {
345
- background: var(--nav-active);
346
- }
347
351
  }
348
352
 
349
353
  > .body {
350
354
  margin-left: 0;
351
355
  }
356
+
357
+ &.group-highlight {
358
+ background: var(--nav-active);
359
+ }
352
360
  }
353
361
 
354
362
  &.depth-1 {
@@ -380,15 +388,6 @@ export default {
380
388
  }
381
389
  }
382
390
  }
383
-
384
- &.expanded:has(> .active),
385
- &.expanded:has(> ul li.nuxt-link-active) {
386
- background: var(--nav-active);
387
- }
388
-
389
- &.expanded:has(> ul li.root) {
390
- background: transparent;
391
- }
392
391
  }
393
392
 
394
393
  .body ::v-deep > .child.nuxt-link-active,
@@ -427,5 +426,4 @@ export default {
427
426
  }
428
427
  }
429
428
  }
430
-
431
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');
@@ -720,6 +724,9 @@ export default {
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
  }
@@ -0,0 +1,47 @@
1
+ <script>
2
+ // Allow the user to pin a cluster by clicking it.
3
+ export default {
4
+ props: {
5
+ cluster: {
6
+ type: Object,
7
+ required: true,
8
+ }
9
+ },
10
+
11
+ computed: {
12
+ pinned() {
13
+ return this.cluster.pinned;
14
+ }
15
+ },
16
+
17
+ methods: {
18
+ toggle() {
19
+ if ( this.pinned ) {
20
+ this.cluster.unpin();
21
+ } else {
22
+ this.cluster.pin();
23
+ }
24
+ }
25
+ }
26
+ };
27
+ </script>
28
+
29
+ <template>
30
+ <i
31
+ :tabindex="0"
32
+ :aria-checked="!!pinned"
33
+ class="pin icon"
34
+ :class="{'icon-pin-outlined': !pinned, 'icon-pin': pinned}"
35
+ aria-role="button"
36
+ @click.stop.prevent="toggle"
37
+ @keydown.enter.prevent="toggle"
38
+ @keydown.space.prevent="toggle"
39
+ />
40
+ </template>
41
+
42
+ <style lang="scss" scoped>
43
+ .icon {
44
+ font-size: 14px;
45
+ transform: scaleX(-1);
46
+ }
47
+ </style>