@rancher/shell 3.0.10 → 3.0.12-rc.1

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 (154) hide show
  1. package/assets/styles/base/_mixins.scss +31 -0
  2. package/assets/styles/base/_variables.scss +2 -0
  3. package/assets/styles/themes/_modern.scss +6 -5
  4. package/assets/translations/en-us.yaml +12 -9
  5. package/assets/translations/zh-hans.yaml +0 -3
  6. package/chart/__tests__/rancher-backup-index.test.ts +248 -0
  7. package/chart/rancher-backup/index.vue +41 -2
  8. package/components/BrandImage.vue +6 -5
  9. package/components/ConsumptionGauge.vue +12 -4
  10. package/components/DynamicContent/DynamicContentIcon.vue +3 -2
  11. package/components/EmptyProductPage.vue +76 -0
  12. package/components/ExplorerProjectsNamespaces.vue +1 -4
  13. package/components/LazyImage.vue +2 -1
  14. package/components/Resource/Detail/Card/Scaler.vue +4 -4
  15. package/components/Resource/Detail/CopyToClipboard.vue +1 -2
  16. package/components/Resource/Detail/Metadata/KeyValueRow.vue +9 -3
  17. package/components/Resource/Detail/TitleBar/__tests__/__snapshots__/index.test.ts.snap +31 -0
  18. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +45 -1
  19. package/components/Resource/Detail/TitleBar/index.vue +1 -1
  20. package/components/Resource/Detail/ViewOptions/__tests__/__snapshots__/index.test.ts.snap +9 -0
  21. package/components/Resource/Detail/ViewOptions/__tests__/index.test.ts +62 -0
  22. package/components/Resource/Detail/ViewOptions/index.vue +2 -1
  23. package/components/ResourceList/Masthead.vue +25 -2
  24. package/components/SideNav.vue +13 -0
  25. package/components/Tabbed/index.vue +6 -0
  26. package/components/__tests__/ConsumptionGauge.test.ts +31 -0
  27. package/components/__tests__/PromptModal.test.ts +2 -0
  28. package/components/fleet/FleetClusters.vue +1 -0
  29. package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
  30. package/components/form/NodeScheduling.vue +17 -3
  31. package/components/form/PrivateRegistry.vue +69 -0
  32. package/components/form/ProjectMemberEditor.vue +0 -10
  33. package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
  34. package/components/formatter/WorkloadHealthScale.vue +3 -1
  35. package/components/nav/Group.vue +26 -3
  36. package/components/nav/Header.vue +32 -7
  37. package/components/nav/TopLevelMenu.helper.ts +7 -79
  38. package/components/nav/TopLevelMenu.vue +15 -1
  39. package/components/nav/__tests__/TopLevelMenu.helper.test.ts +2 -53
  40. package/config/pagination-table-headers.js +8 -1
  41. package/config/private-label.js +2 -1
  42. package/config/product/apps.js +3 -1
  43. package/config/product/auth.js +1 -0
  44. package/config/product/backup.js +1 -0
  45. package/config/product/compliance.js +1 -1
  46. package/config/product/explorer.js +25 -6
  47. package/config/product/fleet.js +1 -0
  48. package/config/product/gatekeeper.js +1 -0
  49. package/config/product/istio.js +1 -0
  50. package/config/product/logging.js +1 -0
  51. package/config/product/longhorn.js +2 -1
  52. package/config/product/manager.js +1 -0
  53. package/config/product/monitoring.js +1 -0
  54. package/config/product/navlinks.js +1 -0
  55. package/config/product/neuvector.js +2 -1
  56. package/config/product/settings.js +1 -0
  57. package/config/product/uiplugins.js +1 -0
  58. package/core/__tests__/extension-manager-impl.test.js +187 -2
  59. package/core/__tests__/plugin-products-helpers.test.ts +454 -0
  60. package/core/__tests__/plugin-products.test.ts +3219 -0
  61. package/core/extension-manager-impl.js +34 -3
  62. package/core/plugin-helpers.ts +31 -0
  63. package/core/plugin-products-base.ts +375 -0
  64. package/core/plugin-products-extending.ts +44 -0
  65. package/core/plugin-products-helpers.ts +262 -0
  66. package/core/plugin-products-top-level.ts +66 -0
  67. package/core/plugin-products-type-guards.ts +33 -0
  68. package/core/plugin-products.ts +50 -0
  69. package/core/plugin-types.ts +222 -0
  70. package/core/plugin.ts +45 -10
  71. package/core/productDebugger.js +48 -0
  72. package/core/types.ts +95 -11
  73. package/detail/__tests__/__snapshots__/fleet.cattle.io.bundle.test.ts.snap +52 -0
  74. package/detail/__tests__/fleet.cattle.io.bundle.test.ts +171 -0
  75. package/detail/__tests__/node.test.ts +83 -0
  76. package/detail/fleet.cattle.io.bundle.vue +21 -34
  77. package/detail/management.cattle.io.oidcclient.vue +2 -1
  78. package/detail/node.vue +1 -0
  79. package/dialog/ExtensionCatalogInstallDialog.vue +1 -1
  80. package/dialog/InstallExtensionDialog.vue +6 -27
  81. package/dialog/UninstallExistingExtensionDialog.vue +141 -0
  82. package/dialog/UninstallExtensionDialog.vue +4 -26
  83. package/dialog/__tests__/UninstallExistingExtensionDialog.test.ts +114 -0
  84. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -0
  85. package/edit/catalog.cattle.io.clusterrepo.vue +17 -3
  86. package/edit/cloudcredential.vue +2 -1
  87. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -6
  88. package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
  89. package/edit/provisioning.cattle.io.cluster/index.vue +5 -4
  90. package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
  91. package/edit/provisioning.cattle.io.cluster/shared.ts +4 -2
  92. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
  93. package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
  94. package/edit/secret/generic.vue +1 -0
  95. package/edit/secret/index.vue +2 -1
  96. package/edit/service.vue +2 -14
  97. package/list/management.cattle.io.feature.vue +7 -1
  98. package/list/provisioning.cattle.io.cluster.vue +0 -50
  99. package/list/workload.vue +11 -4
  100. package/mixins/brand.js +2 -1
  101. package/mixins/resource-fetch.js +12 -3
  102. package/models/catalog.cattle.io.clusterrepo.js +9 -0
  103. package/models/cluster.x-k8s.io.machinedeployment.js +8 -3
  104. package/models/management.cattle.io.authconfig.js +2 -1
  105. package/models/management.cattle.io.cluster.js +4 -3
  106. package/models/monitoring.coreos.com.receiver.js +11 -6
  107. package/models/pod.js +18 -0
  108. package/models/provisioning.cattle.io.cluster.js +2 -2
  109. package/models/workload.js +20 -2
  110. package/package.json +5 -6
  111. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
  112. package/pages/c/_cluster/apps/charts/index.vue +3 -8
  113. package/pages/c/_cluster/apps/charts/install.vue +8 -9
  114. package/pages/c/_cluster/istio/index.vue +4 -2
  115. package/pages/c/_cluster/longhorn/index.vue +2 -1
  116. package/pages/c/_cluster/monitoring/index.vue +2 -2
  117. package/pages/c/_cluster/neuvector/index.vue +2 -1
  118. package/pages/c/_cluster/settings/brand.vue +4 -4
  119. package/pages/c/_cluster/settings/performance.vue +0 -5
  120. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +2 -1
  121. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +231 -13
  122. package/pages/c/_cluster/uiplugins/index.vue +145 -38
  123. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -0
  124. package/plugins/dashboard-store/actions.js +3 -2
  125. package/plugins/dashboard-store/resource-class.js +62 -6
  126. package/plugins/plugin.js +16 -0
  127. package/plugins/steve/steve-pagination-utils.ts +8 -2
  128. package/plugins/steve/subscribe.js +29 -4
  129. package/rancher-components/RcButton/RcButton.vue +3 -3
  130. package/rancher-components/RcButtonSplit/RcButtonSplit.test.ts +253 -0
  131. package/rancher-components/RcButtonSplit/RcButtonSplit.vue +158 -0
  132. package/rancher-components/RcButtonSplit/index.ts +1 -0
  133. package/scripts/test-plugins-build.sh +4 -4
  134. package/scripts/typegen.sh +13 -1
  135. package/store/__tests__/type-map.test.ts +84 -24
  136. package/store/type-map.js +42 -3
  137. package/tsconfig.paths.json +1 -0
  138. package/types/resources/pod.ts +18 -0
  139. package/types/shell/index.d.ts +8506 -2908
  140. package/types/store/dashboard-store.types.ts +5 -0
  141. package/types/store/pagination.types.ts +6 -0
  142. package/utils/__tests__/require-asset.test.ts +98 -0
  143. package/utils/async.ts +1 -5
  144. package/utils/axios.js +1 -4
  145. package/utils/brand.ts +3 -1
  146. package/utils/dynamic-importer.js +3 -2
  147. package/utils/favicon.js +4 -3
  148. package/utils/pagination-utils.ts +1 -1
  149. package/utils/require-asset.ts +95 -0
  150. package/utils/uiplugins.ts +12 -16
  151. package/utils/validators/__tests__/private-registry.test.ts +76 -0
  152. package/utils/validators/private-registry.ts +28 -0
  153. package/vue.config.js +4 -3
  154. package/components/HarvesterServiceAddOnConfig.vue +0 -207
@@ -0,0 +1,69 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from 'vue';
3
+ import Banner from '@components/Banner/Banner.vue';
4
+ import { Checkbox } from '@components/Form/Checkbox';
5
+ import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
6
+
7
+ const props = defineProps<{
8
+ value?: string | null;
9
+ enabled?: boolean;
10
+ mode?: string;
11
+ rules?: Function[];
12
+ checkboxTestId?: string;
13
+ inputTestId?: string;
14
+ }>();
15
+
16
+ const emit = defineEmits<{
17
+ 'update:value': [val: string | null];
18
+ 'update:enabled': [val: boolean];
19
+ }>();
20
+
21
+ const showInput = ref(!!props.value);
22
+
23
+ watch(() => props.enabled, (neu) => {
24
+ if (typeof neu === 'boolean' && neu !== showInput.value) {
25
+ showInput.value = neu;
26
+ }
27
+ });
28
+
29
+ watch(showInput, (neu, old) => {
30
+ if (neu !== props.enabled) {
31
+ emit('update:enabled', neu);
32
+ }
33
+ if (!neu && old && props.value) {
34
+ emit('update:value', null);
35
+ }
36
+ });
37
+
38
+ watch(() => props.value, (neu) => {
39
+ if (!!neu && !showInput.value) {
40
+ showInput.value = true;
41
+ }
42
+ });
43
+ </script>
44
+
45
+ <template>
46
+ <Banner
47
+ color="info"
48
+ class="mt-0"
49
+ label-key="cluster.privateRegistry.importedDescription"
50
+ />
51
+ <Checkbox
52
+ v-model:value="showInput"
53
+ class="mb-20"
54
+ :mode="mode"
55
+ :label="t('cluster.privateRegistry.label')"
56
+ :data-testid="checkboxTestId"
57
+ />
58
+ <LabeledInput
59
+ v-if="showInput"
60
+ :value="value as string"
61
+ :mode="mode"
62
+ :rules="rules"
63
+ :required="true"
64
+ label-key="catalog.chart.registry.custom.inputLabel"
65
+ :data-testid="inputTestId"
66
+ :placeholder="t('catalog.chart.registry.custom.placeholder')"
67
+ @update:value="(val) => emit('update:value', val)"
68
+ />
69
+ </template>
@@ -61,11 +61,6 @@ export default {
61
61
  label: this.t('projectMembers.projectPermissions.ingressManage'),
62
62
  value: false,
63
63
  },
64
- {
65
- key: 'projectcatalogs-manage',
66
- label: this.t('projectMembers.projectPermissions.projectcatalogsManage'),
67
- value: false,
68
- },
69
64
  {
70
65
  key: 'projectroletemplatebindings-manage',
71
66
  label: this.t('projectMembers.projectPermissions.projectroletemplatebindingsManage'),
@@ -111,11 +106,6 @@ export default {
111
106
  label: this.t('projectMembers.projectPermissions.monitoringUiView'),
112
107
  value: false,
113
108
  },
114
- {
115
- key: 'projectcatalogs-view',
116
- label: this.t('projectMembers.projectPermissions.projectcatalogsView'),
117
- value: false,
118
- },
119
109
  {
120
110
  key: 'projectroletemplatebindings-view',
121
111
  label: this.t('projectMembers.projectPermissions.projectroletemplatebindingsView'),
@@ -0,0 +1,133 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import PrivateRegistry from '@shell/components/form/PrivateRegistry.vue';
3
+ import { Checkbox } from '@components/Form/Checkbox';
4
+ import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
5
+
6
+ const defaultMocks = {
7
+ $store: {
8
+ getters: {
9
+ 'i18n/t': (text: string) => text,
10
+ t: (text: string) => text,
11
+ }
12
+ }
13
+ };
14
+
15
+ const mountPrivateRegistry = (props = {}) => {
16
+ return shallowMount(PrivateRegistry, {
17
+ props: {
18
+ mode: 'edit',
19
+ ...props
20
+ },
21
+ global: { mocks: defaultMocks }
22
+ });
23
+ };
24
+
25
+ describe('privateRegistry', () => {
26
+ it('should render the info banner', () => {
27
+ const wrapper = mountPrivateRegistry();
28
+ const banner = wrapper.find('[color="info"]');
29
+
30
+ expect(banner.exists()).toBe(true);
31
+ });
32
+
33
+ it('should render the enable checkbox', () => {
34
+ const wrapper = mountPrivateRegistry();
35
+ const checkbox = wrapper.findComponent(Checkbox);
36
+
37
+ expect(checkbox.exists()).toBe(true);
38
+ });
39
+
40
+ it('should not show the URL input when no value is provided', () => {
41
+ const wrapper = mountPrivateRegistry();
42
+
43
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(false);
44
+ });
45
+
46
+ it('should show the URL input when a value is provided', () => {
47
+ const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
48
+
49
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(true);
50
+ });
51
+
52
+ it('should show the URL input when checkbox is checked', async() => {
53
+ const wrapper = mountPrivateRegistry();
54
+
55
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(false);
56
+
57
+ const checkbox = wrapper.findComponent(Checkbox);
58
+
59
+ await checkbox.vm.$emit('update:value', true);
60
+ await wrapper.vm.$nextTick();
61
+
62
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(true);
63
+ });
64
+
65
+ it('should emit update:value with null when checkbox is unchecked', async() => {
66
+ const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
67
+
68
+ const checkbox = wrapper.findComponent(Checkbox);
69
+
70
+ await checkbox.vm.$emit('update:value', false);
71
+ await wrapper.vm.$nextTick();
72
+
73
+ expect(wrapper.emitted('update:value')).toHaveLength(1);
74
+ expect(wrapper.emitted('update:value')![0]).toStrictEqual([null]);
75
+ });
76
+
77
+ it('should emit update:value when the URL input changes', async() => {
78
+ const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
79
+ const input = wrapper.findComponent(LabeledInput);
80
+
81
+ await input.vm.$emit('update:value', 'new-registry.example.com');
82
+
83
+ expect(wrapper.emitted('update:value')).toHaveLength(1);
84
+ expect(wrapper.emitted('update:value')![0]).toStrictEqual(['new-registry.example.com']);
85
+ });
86
+
87
+ it('should auto-enable the checkbox when value changes from null to a string', async() => {
88
+ const wrapper = mountPrivateRegistry();
89
+
90
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(false);
91
+
92
+ await wrapper.setProps({ value: 'registry.example.com' });
93
+
94
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(true);
95
+ });
96
+
97
+ it('should pass rules to the URL input', () => {
98
+ const mockRule = jest.fn();
99
+ const wrapper = mountPrivateRegistry({
100
+ value: 'registry.example.com',
101
+ rules: [mockRule]
102
+ });
103
+ const input = wrapper.findComponent(LabeledInput);
104
+
105
+ expect(input.attributes('rules')).toBeDefined();
106
+ });
107
+
108
+ it('should apply custom data-testid to checkbox when provided', () => {
109
+ const wrapper = mountPrivateRegistry({ checkboxTestId: 'my-checkbox' });
110
+ const checkbox = wrapper.findComponent(Checkbox);
111
+
112
+ expect(checkbox.attributes('data-testid')).toBe('my-checkbox');
113
+ });
114
+
115
+ it('should apply custom data-testid to input when provided', () => {
116
+ const wrapper = mountPrivateRegistry({
117
+ value: 'registry.example.com',
118
+ inputTestId: 'my-input'
119
+ });
120
+ const input = wrapper.findComponent(LabeledInput);
121
+
122
+ expect(input.attributes('data-testid')).toBe('my-input');
123
+ });
124
+
125
+ it('should not set data-testid when not provided', () => {
126
+ const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
127
+ const checkbox = wrapper.findComponent(Checkbox);
128
+ const input = wrapper.findComponent(LabeledInput);
129
+
130
+ expect(checkbox.attributes('data-testid')).toBeUndefined();
131
+ expect(input.attributes('data-testid')).toBeUndefined();
132
+ });
133
+ });
@@ -67,7 +67,9 @@ export default {
67
67
  color: `bg-${ value.color }`,
68
68
  value: value.count || 0,
69
69
  label: ucFirst(name)
70
- })).filter((x) => x.value > 0);
70
+ }))
71
+ .filter((x) => x.value > 0)
72
+ .sort((a, b) => a.label.localeCompare(b.label));
71
73
 
72
74
  return 5;
73
75
  },
@@ -269,8 +269,9 @@ export default {
269
269
  @keyup.space="groupSelected()"
270
270
  >
271
271
  <slot name="header">
272
+ <!-- Group overview with link -->
272
273
  <router-link
273
- v-if="hasOverview"
274
+ v-if="hasOverview && hasChildren"
274
275
  :to="headerRoute"
275
276
  :exact="group.children[0].exact"
276
277
  :tabindex="-1"
@@ -279,15 +280,32 @@ export default {
279
280
  <span v-clean-html="group.labelDisplay || group.label" />
280
281
  </h6>
281
282
  </router-link>
283
+ <!-- Non-linked group header -->
282
284
  <h6
283
- v-else
285
+ v-else-if="hasChildren"
284
286
  >
285
287
  <span v-clean-html="group.labelDisplay || group.label" />
286
288
  </h6>
289
+ <!-- Simple child (nav item) -->
290
+ <ul
291
+ v-else
292
+ class="list-unstyled body root-depth"
293
+ v-bind="$attrs"
294
+ >
295
+ <Type
296
+
297
+ :key="id+'_' + group.name + '_type'"
298
+ :is-root="depth == 0 && !showHeader"
299
+ :type="group"
300
+ :depth="depth"
301
+ :highlight-route="highlightRoute"
302
+ @selected="selectType($event)"
303
+ />
304
+ </ul>
287
305
  </slot>
288
306
  </div>
289
307
  <i
290
- v-if="!onlyHasOverview && canCollapse"
308
+ v-if="!onlyHasOverview && canCollapse && hasChildren"
291
309
  class="icon toggle toggle-accordion"
292
310
  :class="{'icon-chevron-right': !isExpanded, 'icon-chevron-down': isExpanded}"
293
311
  role="button"
@@ -377,6 +395,7 @@ export default {
377
395
  display: block;
378
396
  box-sizing:border-box;
379
397
  height: 100%;
398
+
380
399
  &:hover{
381
400
  text-decoration: none;
382
401
  }
@@ -484,6 +503,10 @@ export default {
484
503
  }
485
504
  }
486
505
  }
506
+
507
+ .root-depth :deep() > .child.nav-type a {
508
+ padding-left: 14px;
509
+ }
487
510
  }
488
511
 
489
512
  &.depth-1 {
@@ -203,12 +203,6 @@ export default {
203
203
  return !!this.currentCluster?.actions?.apply;
204
204
  },
205
205
 
206
- prod() {
207
- const name = this.rootProduct.name;
208
-
209
- return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
210
- },
211
-
212
206
  showSearch() {
213
207
  return this.rootProduct?.inStore === 'cluster';
214
208
  },
@@ -239,6 +233,30 @@ export default {
239
233
  isHarvester() {
240
234
  return this.$store.getters['currentProduct'].inStore === HARVESTER;
241
235
  },
236
+
237
+ productLabel() {
238
+ const name = this.rootProduct.name;
239
+
240
+ // single products do their own thing, which is the previous default behavior as per next line
241
+ if (this.isSingleProduct) {
242
+ return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
243
+ } else {
244
+ if (this.rootProduct?.label) {
245
+ return this.rootProduct.label;
246
+ }
247
+ if (this.rootProduct?.labelKey) {
248
+ return this.$store.getters['i18n/t'](this.rootProduct.labelKey);
249
+ }
250
+
251
+ return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
252
+ }
253
+ },
254
+
255
+ // Determine if we are on a route that shows the logo instead of the product label
256
+ // This is to enforce the logo display on certain routes like home, about, prefs, account, etc
257
+ isLogoRoute() {
258
+ return !this.$route.name.includes('c-cluster');
259
+ }
242
260
  },
243
261
 
244
262
  watch: {
@@ -518,7 +536,7 @@ export default {
518
536
  :alt="t('branding.logos.label')"
519
537
  >
520
538
  <div class="product-name">
521
- {{ prod }}
539
+ {{ productLabel }}
522
540
  </div>
523
541
  </div>
524
542
  </div>
@@ -534,6 +552,13 @@ export default {
534
552
  {{ t(isSingleProduct.productNameKey) }}
535
553
  </div>
536
554
 
555
+ <div
556
+ v-else-if="productLabel && !isLogoRoute"
557
+ class="product-name"
558
+ >
559
+ {{ productLabel }}
560
+ </div>
561
+
537
562
  <div
538
563
  v-else
539
564
  class="side-menu-logo"
@@ -107,7 +107,6 @@ export interface TopLevelMenuHelper {
107
107
 
108
108
  export abstract class BaseTopLevelMenuHelper {
109
109
  protected $store: VuexStore;
110
- protected hasProvCluster: boolean;
111
110
 
112
111
  /**
113
112
  * Filter mgmt clusters by
@@ -143,11 +142,9 @@ export abstract class BaseTopLevelMenuHelper {
143
142
  $store: VuexStore,
144
143
  }) {
145
144
  this.$store = $store;
146
-
147
- this.hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
148
145
  }
149
146
 
150
- protected convertToCluster(mgmtCluster: MgmtCluster, provCluster: ProvCluster): TopLevelMenuCluster {
147
+ protected convertToCluster(mgmtCluster: MgmtCluster, provCluster?: ProvCluster): TopLevelMenuCluster {
151
148
  return {
152
149
  id: mgmtCluster.id,
153
150
  label: mgmtCluster.nameDisplay,
@@ -173,7 +170,6 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
173
170
 
174
171
  private clustersPinnedWrapper: PaginationWrapper<any>;
175
172
  private clustersOthersWrapper: PaginationWrapper<any>;
176
- private provClusterWrapper: PaginationWrapper<any>;
177
173
 
178
174
  private clusterCount = 0;
179
175
 
@@ -223,43 +219,10 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
223
219
  },
224
220
  formatResponse: { classify: true },
225
221
  });
226
- // Fetch all prov clusters for the mgmt clusters we have
227
- this.provClusterWrapper = new PaginationWrapper({
228
- $store,
229
- id: 'top-level-menu-prov-clusters',
230
- onChange: async({ forceWatch, revision }) => {
231
- if (!this.args) {
232
- return;
233
- }
234
- try {
235
- await this.update({
236
- ...this.args,
237
- forceWatch,
238
- provClusterRevision: revision,
239
- });
240
- } catch {
241
- // Failures should be logged lower down, not much we can do here except catch to prevent whole ui page warnings in dev mode
242
- }
243
- },
244
- enabledFor: {
245
- store: STORE.MANAGEMENT,
246
- resource: {
247
- id: CAPI.RANCHER_CLUSTER,
248
- context: 'side-bar',
249
- }
250
- },
251
- formatResponse: { classify: true }
252
- });
253
222
  }
254
223
 
255
224
  // ---------- requests ----------
256
225
  async update(args: UpdateArgs) {
257
- if (!this.hasProvCluster) {
258
- // We're filtering out mgmt clusters without prov clusters, so if the user can't see any prov clusters at all
259
- // exit early
260
- return;
261
- }
262
-
263
226
  this.args = args;
264
227
  const promises = {
265
228
  pinned: this.updatePinned(args),
@@ -271,22 +234,11 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
271
234
  notPinned: MgmtCluster[]
272
235
  } = await allHash(promises) as any;
273
236
 
274
- const provClusters = await this.updateProvCluster(res.notPinned, res.pinned, args);
275
- const provClustersByMgmtId = provClusters.reduce((res: { [mgmtId: string]: ProvCluster}, provCluster: ProvCluster) => {
276
- if (provCluster.mgmtClusterId) {
277
- res[provCluster.mgmtClusterId] = provCluster;
278
- }
279
-
280
- return res;
281
- }, {} as { [mgmtId: string]: ProvCluster});
282
-
283
237
  // Filter out mgmt clusters that don't have matching prov cluster and convert remaining to required format
284
238
  const _clustersNotPinned = res.notPinned
285
- .filter((mgmtCluster) => !!provClustersByMgmtId[mgmtCluster.id])
286
- .map((mgmtCluster) => this.convertToCluster(mgmtCluster, provClustersByMgmtId[mgmtCluster.id]));
239
+ .map((mgmtCluster) => this.convertToCluster(mgmtCluster));
287
240
  const _clustersPinned = res.pinned
288
- .filter((mgmtCluster) => !!provClustersByMgmtId[mgmtCluster.id])
289
- .map((mgmtCluster) => this.convertToCluster(mgmtCluster, provClustersByMgmtId[mgmtCluster.id]));
241
+ .map((mgmtCluster) => this.convertToCluster(mgmtCluster));
290
242
 
291
243
  this.clustersPinned.length = 0;
292
244
  this.clustersOthers.length = 0;
@@ -298,7 +250,6 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
298
250
  async destroy() {
299
251
  this.clustersPinnedWrapper.onDestroy();
300
252
  this.clustersOthersWrapper.onDestroy();
301
- this.provClusterWrapper.onDestroy();
302
253
  }
303
254
 
304
255
  /**
@@ -445,41 +396,21 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
445
396
  console.warn('Unable to set saved count for clusters', err); // eslint-disable-line no-console
446
397
  }
447
398
  }
448
-
449
- /**
450
- * Find all provisioning clusters associated with the displayed mgmt clusters
451
- */
452
- private async updateProvCluster(notPinned: MgmtCluster[], pinned: MgmtCluster[], args: UpdateArgs): Promise<ProvCluster[]> {
453
- return this.provClusterWrapper.request({
454
- forceWatch: args.forceWatch,
455
- pagination: {
456
- filters: [
457
- PaginationParamFilter.createMultipleFields(
458
- [...notPinned, ...pinned]
459
- .map((mgmtCluster) => ({
460
- field: 'status.clusterName', value: mgmtCluster.id, equals: true, exact: true
461
- }))
462
- )
463
- ],
464
- page: 1,
465
- sort: [],
466
- projectsOrNamespaces: []
467
- },
468
- revision: args.provClusterRevision
469
- })
470
- .then((r) => r.data);
471
- }
472
399
  }
473
400
 
474
401
  /**
475
402
  * Helper designed to supply non-paginated results for the top level menu cluster resources
476
403
  */
477
404
  export class TopLevelMenuHelperLegacy extends BaseTopLevelMenuHelper implements TopLevelMenuHelper {
405
+ protected hasProvCluster: boolean;
406
+
478
407
  constructor({ $store }: {
479
408
  $store: VuexStore,
480
409
  }) {
481
410
  super({ $store });
482
411
 
412
+ this.hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
413
+
483
414
  if (this.hasProvCluster) {
484
415
  $store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER });
485
416
  }
@@ -664,9 +595,6 @@ class TopLevelMenuHelperService {
664
595
  const canPagination = $store.getters[`management/paginationEnabled`]({
665
596
  id: MANAGEMENT.CLUSTER,
666
597
  context: 'side-bar',
667
- }) && $store.getters[`management/paginationEnabled`]({
668
- id: CAPI.RANCHER_CLUSTER,
669
- context: 'side-bar',
670
598
  });
671
599
 
672
600
  this._helper = canPagination ? new TopLevelMenuHelperPagination({ $store }) : new TopLevelMenuHelperLegacy({ $store });
@@ -186,8 +186,22 @@ export default {
186
186
  to.params.product = p.name;
187
187
  }
188
188
 
189
+ let label;
190
+
191
+ // Allow product to specify its label (old DSL product() did not have "label" or "labelKey")
192
+ // new extensions product registration supports both "label" and "labelKey" (with "labelKey" taking precedence if both are provided)
193
+ if (p.labelKey) {
194
+ label = this.$store.getters['i18n/t'](p.labelKey);
195
+ } else if (p.label) {
196
+ label = p.label;
197
+ }
198
+
199
+ if (!label) {
200
+ label = this.$store.getters['i18n/withFallback'](`product.${ p.name }`, null, ucFirst(p.name));
201
+ }
202
+
189
203
  return {
190
- label: this.$store.getters['i18n/withFallback'](`product."${ p.name }"`, null, ucFirst(p.name)),
204
+ label,
191
205
  icon: `icon-${ p.icon || 'copy' }`,
192
206
  svg: p.svg,
193
207
  value: p.name,
@@ -102,7 +102,7 @@ describe('topLevelMenu.helper', () => {
102
102
  it('should initialize PaginationWrappers', () => {
103
103
  mockStore.getters['management/schemaFor'].mockReturnValue(true);
104
104
  new TopLevelMenuHelperPagination({ $store: mockStore });
105
- expect(PaginationWrapper).toHaveBeenCalledTimes(3);
105
+ expect(PaginationWrapper).toHaveBeenCalledTimes(2);
106
106
  });
107
107
 
108
108
  it('should update clusters correctly', async() => {
@@ -113,19 +113,13 @@ describe('topLevelMenu.helper', () => {
113
113
  const mgmtOthers = [{
114
114
  id: 'c2', nameDisplay: 'Other', isReady: true, pinned: false, pin: jest.fn(), unpin: jest.fn()
115
115
  }];
116
- const provClusters = [
117
- { mgmtClusterId: 'c1' },
118
- { mgmtClusterId: 'c2' }
119
- ];
120
116
 
121
117
  const mockRequestPinned = jest.fn().mockResolvedValue({ data: mgmtPinned });
122
118
  const mockRequestOthers = jest.fn().mockResolvedValue({ data: mgmtOthers });
123
- const mockRequestProv = jest.fn().mockResolvedValue({ data: provClusters });
124
119
 
125
120
  (PaginationWrapper as unknown as jest.Mock)
126
121
  .mockImplementationOnce(() => ({ request: mockRequestPinned, onDestroy: jest.fn() }))
127
- .mockImplementationOnce(() => ({ request: mockRequestOthers, onDestroy: jest.fn() }))
128
- .mockImplementationOnce(() => ({ request: mockRequestProv, onDestroy: jest.fn() }));
122
+ .mockImplementationOnce(() => ({ request: mockRequestOthers, onDestroy: jest.fn() }));
129
123
 
130
124
  const helper = new TopLevelMenuHelperPagination({ $store: mockStore });
131
125
 
@@ -169,57 +163,12 @@ describe('topLevelMenu.helper', () => {
169
163
  },
170
164
  revision: undefined
171
165
  });
172
- expect(mockRequestProv).toHaveBeenCalledWith({
173
- forceWatch: undefined,
174
- pagination: {
175
- filters: [{
176
- equals: true,
177
- fields: [{
178
- equals: true, exact: true, field: 'status.clusterName', value: mgmtOthers[0].id
179
- }, {
180
- equals: true, exact: true, field: 'status.clusterName', value: mgmtPinned[0].id
181
- }],
182
- param: 'filter'
183
- }],
184
- page: 1,
185
- projectsOrNamespaces: [],
186
- sort: []
187
- },
188
- revision: undefined
189
- });
190
166
 
191
167
  expect(helper.clustersPinned).toHaveLength(1);
192
168
  expect(helper.clustersPinned[0].id).toBe('c1');
193
169
  expect(helper.clustersOthers).toHaveLength(1);
194
170
  expect(helper.clustersOthers[0].id).toBe('c2');
195
171
  });
196
-
197
- it('should filter out mgmt clusters without matching prov clusters', async() => {
198
- mockStore.getters['management/schemaFor'].mockReturnValue(true);
199
- const mgmtOthers = [{
200
- id: 'c2', nameDisplay: 'Other', isReady: true, pinned: false, pin: jest.fn(), unpin: jest.fn()
201
- }];
202
- // No prov cluster for c2
203
- const provClusters: any[] = [];
204
-
205
- const mockRequestPinned = jest.fn().mockResolvedValue({ data: [] });
206
- const mockRequestOthers = jest.fn().mockResolvedValue({ data: mgmtOthers });
207
- const mockRequestProv = jest.fn().mockResolvedValue({ data: provClusters });
208
-
209
- (PaginationWrapper as unknown as jest.Mock)
210
- .mockImplementationOnce(() => ({ request: mockRequestPinned, onDestroy: jest.fn() }))
211
- .mockImplementationOnce(() => ({ request: mockRequestOthers, onDestroy: jest.fn() }))
212
- .mockImplementationOnce(() => ({ request: mockRequestProv, onDestroy: jest.fn() }));
213
-
214
- const helper = new TopLevelMenuHelperPagination({ $store: mockStore });
215
-
216
- await helper.update({
217
- searchTerm: '',
218
- pinnedIds: [],
219
- });
220
-
221
- expect(helper.clustersOthers).toHaveLength(0);
222
- });
223
172
  });
224
173
 
225
174
  describe('class: TopLevelMenuHelperService', () => {
@@ -4,7 +4,8 @@ import {
4
4
  EVENT_LAST_SEEN_TIME,
5
5
  EVENT_TYPE,
6
6
  SECRET_ORIGIN,
7
- EVENT_FIRST_SEEN_TIME
7
+ EVENT_FIRST_SEEN_TIME,
8
+ WORKLOAD_HEALTH_SCALE
8
9
  } from '@shell/config/table-headers';
9
10
 
10
11
  // This file contains table headers
@@ -95,3 +96,9 @@ export const STEVE_SECRET_ORIGIN = {
95
96
  // So we sort by the 'UI_PROJECT_SECRET_COPY' annotation (management.cattle.io/project-scoped-secret-copy) which at least groups the copies.
96
97
  sort: `metadata.annotations[${ UI_PROJECT_SECRET_COPY }]:desc`,
97
98
  };
99
+
100
+ export const STEVE_WORKLOAD_HEALTH_SCALE = {
101
+ ...WORKLOAD_HEALTH_SCALE,
102
+ sort: false,
103
+ search: false,
104
+ };
@@ -1,5 +1,6 @@
1
1
  import { SETTING } from './settings';
2
2
  import { CURRENT_RANCHER_VERSION } from './version';
3
+ import { requireAsset } from '@shell/utils/require-asset';
3
4
 
4
5
  export const ANY = 0;
5
6
  export const STANDARD = 1;
@@ -78,7 +79,7 @@ export function setTitle() {
78
79
  const v = getVendor();
79
80
 
80
81
  if (v === 'Harvester') {
81
- const ico = require(`~shell/assets/images/pl/harvester.png`);
82
+ const ico = requireAsset(`~shell/assets/images/pl/harvester.png`);
82
83
 
83
84
  document.title = 'Harvester';
84
85
  const link = document.createElement('link');