@rancher/shell 3.0.12-rc.2 → 3.0.12-rc.3

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 (272) hide show
  1. package/apis/impl/apis.ts +6 -0
  2. package/apis/index.ts +26 -0
  3. package/apis/intf/resources-api/cluster-api.ts +18 -0
  4. package/apis/intf/resources-api/mgmt-api.ts +15 -0
  5. package/apis/intf/resources-api/resource-base.ts +107 -0
  6. package/apis/intf/resources-api/resource-constants.ts +147 -0
  7. package/apis/intf/resources-api/resources-api.ts +143 -0
  8. package/apis/intf/resources.ts +49 -0
  9. package/apis/intf/{modal.ts → shell-api/modal.ts} +21 -26
  10. package/apis/intf/shell-api/proxy.ts +216 -0
  11. package/apis/intf/{slide-in.ts → shell-api/slide-in.ts} +4 -3
  12. package/apis/intf/{system.ts → shell-api/system.ts} +4 -1
  13. package/apis/intf/shell.ts +12 -6
  14. package/apis/resources/__tests__/resources-api-class.test.ts +550 -0
  15. package/apis/resources/index.ts +22 -0
  16. package/apis/resources/resources-api-class.ts +187 -0
  17. package/apis/shell/__tests__/proxy.test.ts +369 -0
  18. package/apis/shell/index.ts +8 -1
  19. package/apis/shell/modal.ts +4 -1
  20. package/apis/shell/notifications.ts +9 -6
  21. package/apis/shell/proxy.ts +256 -0
  22. package/apis/shell/slide-in.ts +4 -1
  23. package/apis/vue-shim.d.ts +2 -1
  24. package/assets/data/aws-regions.json +4 -0
  25. package/assets/fonts/lato/LatoLatin-Black.woff +0 -0
  26. package/assets/fonts/lato/LatoLatin-Black.woff2 +0 -0
  27. package/assets/fonts/lato/LatoLatin-BlackItalic.woff +0 -0
  28. package/assets/fonts/lato/LatoLatin-BlackItalic.woff2 +0 -0
  29. package/assets/fonts/lato/LatoLatin-Bold.woff +0 -0
  30. package/assets/fonts/lato/LatoLatin-Bold.woff2 +0 -0
  31. package/assets/fonts/lato/LatoLatin-BoldItalic.woff +0 -0
  32. package/assets/fonts/lato/LatoLatin-BoldItalic.woff2 +0 -0
  33. package/assets/fonts/lato/LatoLatin-Heavy.woff +0 -0
  34. package/assets/fonts/lato/LatoLatin-Heavy.woff2 +0 -0
  35. package/assets/fonts/lato/LatoLatin-HeavyItalic.woff +0 -0
  36. package/assets/fonts/lato/LatoLatin-HeavyItalic.woff2 +0 -0
  37. package/assets/fonts/lato/LatoLatin-Italic.woff +0 -0
  38. package/assets/fonts/lato/LatoLatin-Italic.woff2 +0 -0
  39. package/assets/fonts/lato/LatoLatin-Light.woff +0 -0
  40. package/assets/fonts/lato/LatoLatin-Light.woff2 +0 -0
  41. package/assets/fonts/lato/LatoLatin-LightItalic.woff +0 -0
  42. package/assets/fonts/lato/LatoLatin-LightItalic.woff2 +0 -0
  43. package/assets/fonts/lato/LatoLatin-Medium.woff +0 -0
  44. package/assets/fonts/lato/LatoLatin-Medium.woff2 +0 -0
  45. package/assets/fonts/lato/LatoLatin-MediumItalic.woff +0 -0
  46. package/assets/fonts/lato/LatoLatin-MediumItalic.woff2 +0 -0
  47. package/assets/fonts/lato/LatoLatin-Regular.woff +0 -0
  48. package/assets/fonts/lato/LatoLatin-Regular.woff2 +0 -0
  49. package/assets/fonts/lato/LatoLatin-Semibold.woff +0 -0
  50. package/assets/fonts/lato/LatoLatin-Semibold.woff2 +0 -0
  51. package/assets/fonts/lato/LatoLatin-SemiboldItalic.woff +0 -0
  52. package/assets/fonts/lato/LatoLatin-SemiboldItalic.woff2 +0 -0
  53. package/assets/styles/base/_variables.scss +2 -0
  54. package/assets/styles/fonts/_fontstack.scss +132 -8
  55. package/assets/translations/en-us.yaml +22 -5
  56. package/chart/monitoring/index.vue +10 -1
  57. package/components/ActionDropdownShell.vue +2 -1
  58. package/components/CruResourceFooter.vue +9 -5
  59. package/components/ExplorerProjectsNamespaces.vue +1 -1
  60. package/components/InstallHelmCharts.vue +2 -2
  61. package/components/LandingPagePreference.vue +14 -5
  62. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +15 -1
  63. package/components/Resource/Detail/Metadata/index.vue +6 -0
  64. package/components/Resource/Detail/ResourcePopover/index.vue +12 -1
  65. package/components/Resource/Detail/SpacedRow.vue +3 -1
  66. package/components/Resource/Detail/TitleBar/index.vue +10 -11
  67. package/components/ResourceList/Masthead.vue +12 -8
  68. package/components/SelectIconGrid.vue +0 -10
  69. package/components/SingleClusterInfo.vue +1 -0
  70. package/components/SortableTable/__tests__/sorting.test.ts +126 -0
  71. package/components/SortableTable/index.vue +6 -9
  72. package/components/SortableTable/selection.js +23 -5
  73. package/components/SortableTable/sorting.js +6 -3
  74. package/components/Wizard.vue +14 -13
  75. package/components/fleet/FleetBundles.vue +100 -12
  76. package/components/fleet/FleetClusterTargets/index.vue +37 -15
  77. package/components/fleet/__tests__/FleetClusterTargets.test.ts +149 -115
  78. package/components/fleet/__tests__/FleetClusters.test.ts +12 -12
  79. package/components/form/LabeledSelect.vue +20 -3
  80. package/components/form/NameNsDescription.vue +11 -0
  81. package/components/form/Security.vue +6 -2
  82. package/components/form/WorkloadPorts.vue +2 -7
  83. package/components/form/__tests__/Security.test.ts +76 -0
  84. package/components/formatter/Autoscaler.vue +4 -4
  85. package/components/formatter/ClusterKubeVersion.vue +27 -0
  86. package/components/formatter/ClusterLink.vue +1 -7
  87. package/components/formatter/ClusterProvider.vue +6 -10
  88. package/components/formatter/FleetSummaryGraph.vue +0 -3
  89. package/components/formatter/MachineSummaryGraph.vue +1 -1
  90. package/components/formatter/PodsUsage.vue +2 -2
  91. package/components/formatter/__tests__/Autoscaler.test.ts +19 -22
  92. package/components/formatter/__tests__/FleetSummaryGraph.test.ts +216 -0
  93. package/components/formatter/__tests__/PodsUsage.test.ts +6 -10
  94. package/components/nav/NamespaceFilter.vue +2 -2
  95. package/components/nav/TopLevelMenu.helper.ts +15 -3
  96. package/components/nav/TopLevelMenu.vue +16 -5
  97. package/components/nav/__tests__/TopLevelMenu.test.ts +145 -21
  98. package/components/templates/home.vue +18 -0
  99. package/components/templates/plain.vue +18 -0
  100. package/components/templates/standalone.vue +17 -0
  101. package/composables/useFormValidation.ts +93 -0
  102. package/composables/useVeeValidateField.test.ts +159 -0
  103. package/composables/useVeeValidateField.ts +67 -0
  104. package/config/pagination-table-headers.js +18 -1
  105. package/config/product/manager.js +82 -21
  106. package/config/router/routes.js +6 -0
  107. package/config/table-headers.js +20 -1
  108. package/config/types.js +2 -1
  109. package/core/__tests__/plugin-products.test.ts +904 -20
  110. package/core/plugin-products-base.ts +107 -7
  111. package/core/plugin-products.ts +4 -0
  112. package/core/plugin-types.ts +111 -1
  113. package/core/plugin.ts +15 -7
  114. package/core/productDebugger.js +9 -4
  115. package/core/types-provisioning.ts +43 -30
  116. package/core/types.ts +57 -20
  117. package/detail/__tests__/pod.test.ts +41 -0
  118. package/detail/harvesterhci.io.management.cluster.vue +6 -2
  119. package/detail/pod.vue +1 -1
  120. package/detail/provisioning.cattle.io.cluster.vue +4 -10
  121. package/edit/auth/__tests__/azuread.test.ts +217 -34
  122. package/edit/auth/azuread.vue +122 -14
  123. package/edit/auth/oidc.vue +2 -2
  124. package/edit/networking.k8s.io.ingress/DefaultBackend.vue +13 -4
  125. package/edit/networking.k8s.io.ingress/RulePath.vue +8 -4
  126. package/edit/networking.k8s.io.ingress/index.vue +75 -20
  127. package/edit/provisioning.cattle.io.cluster/__tests__/MachinePool.test.ts +104 -0
  128. package/edit/provisioning.cattle.io.cluster/index.vue +11 -7
  129. package/edit/provisioning.cattle.io.cluster/rke2.vue +8 -4
  130. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
  131. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +37 -4
  132. package/edit/provisioning.cattle.io.cluster/tabs/registries/__tests__/RegistryConfigs.test.ts +132 -7
  133. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +2 -1
  134. package/edit/secret/__tests__/ssh.test.ts +5 -6
  135. package/edit/secret/basic.vue +31 -0
  136. package/edit/secret/index.vue +68 -17
  137. package/edit/secret/registry.vue +38 -0
  138. package/edit/secret/ssh.vue +29 -0
  139. package/edit/secret/tls.vue +30 -0
  140. package/edit/service.vue +4 -4
  141. package/edit/workload/Upgrading.vue +3 -3
  142. package/edit/workload/__tests__/Upgrading.test.ts +6 -9
  143. package/edit/workload/mixins/workload.js +2 -1
  144. package/list/fleet.cattle.io.bundle.vue +7 -104
  145. package/list/fleet.cattle.io.clusterregistrationtoken.vue +20 -0
  146. package/list/provisioning.cattle.io.cluster.vue +262 -180
  147. package/list/utils/management.cattle.io.cluster.utils.ts +128 -0
  148. package/mixins/__tests__/chart.test.ts +112 -0
  149. package/mixins/brand.js +2 -1
  150. package/mixins/chart.js +12 -8
  151. package/mixins/resource-fetch-api-pagination.js +41 -5
  152. package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +67 -67
  153. package/models/__tests__/management.cattle.io.cluster.test.ts +1 -1
  154. package/models/__tests__/management.cattle.io.node.ts +6 -5
  155. package/models/__tests__/management.cattle.io.nodepool.ts +5 -4
  156. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +32 -11
  157. package/models/base-cluster.x-k8s.io.js +26 -0
  158. package/models/cluster.js +1 -1
  159. package/models/cluster.x-k8s.io.machine.js +4 -22
  160. package/models/cluster.x-k8s.io.machinedeployment.js +2 -20
  161. package/models/cluster.x-k8s.io.machineset.js +2 -20
  162. package/models/compliance.cattle.io.clusterscan.js +130 -2
  163. package/models/ext.cattle.io.kubeconfig.ts +4 -7
  164. package/models/fleet-application.js +3 -1
  165. package/models/management.cattle.io.cluster.js +417 -40
  166. package/models/management.cattle.io.node.js +6 -4
  167. package/models/management.cattle.io.nodepool.js +1 -1
  168. package/models/networking.k8s.io.ingress.js +12 -4
  169. package/models/provisioning.cattle.io.cluster.js +47 -330
  170. package/models/rke.cattle.io.etcdsnapshot.js +1 -2
  171. package/package.json +11 -29
  172. package/pages/__tests__/readme.test.ts +49 -0
  173. package/pages/auth/setup.vue +2 -3
  174. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +76 -0
  175. package/pages/c/_cluster/apps/charts/chart.vue +60 -8
  176. package/pages/c/_cluster/apps/charts/install.vue +10 -7
  177. package/pages/c/_cluster/explorer/__tests__/index.test.ts +23 -25
  178. package/pages/c/_cluster/explorer/index.vue +5 -49
  179. package/pages/c/_cluster/istio/__tests__/istio.index.test.ts +194 -0
  180. package/pages/c/_cluster/istio/index.vue +21 -6
  181. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -0
  182. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +719 -2
  183. package/pages/c/_cluster/uiplugins/index.vue +203 -197
  184. package/pages/diagnostic.vue +13 -17
  185. package/pages/fail-whale.vue +18 -0
  186. package/pages/home.vue +77 -260
  187. package/pages/readme.vue +88 -0
  188. package/plugins/dashboard-store/__tests__/resource-class.test.ts +88 -0
  189. package/plugins/dashboard-store/actions.js +40 -18
  190. package/plugins/dashboard-store/resource-class.js +5 -2
  191. package/plugins/steve/__tests__/subscribe.spec.ts +6 -3
  192. package/plugins/steve/steve-pagination-utils.ts +11 -3
  193. package/plugins/steve/subscribe.js +35 -5
  194. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +10 -4
  195. package/rancher-components/Form/LabeledInput/LabeledInput.vue +7 -52
  196. package/rancher-components/RcButton/RcButton.test.ts +37 -1
  197. package/rancher-components/RcButton/RcButton.vue +38 -8
  198. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +10 -8
  199. package/store/__tests__/catalog.test.ts +115 -1
  200. package/store/__tests__/type-map.test.ts +556 -1
  201. package/store/action-menu.js +8 -3
  202. package/store/auth.js +1 -1
  203. package/store/aws.js +27 -16
  204. package/store/catalog.js +27 -3
  205. package/store/digitalocean.js +20 -38
  206. package/store/index.js +2 -0
  207. package/store/linode.js +25 -40
  208. package/store/pnap.js +1 -0
  209. package/store/type-map.js +111 -29
  210. package/tsconfig.paths.json +8 -8
  211. package/types/kube/kube-api.ts +14 -1
  212. package/types/rancher/steve.api.ts +12 -12
  213. package/types/resources/settings.d.ts +2 -1
  214. package/types/shell/index.d.ts +102 -2
  215. package/types/store/dashboard-store.types.ts +108 -11
  216. package/types/store/pagination.types.ts +6 -3
  217. package/utils/__tests__/alertmanagerconfig.test.ts +117 -0
  218. package/utils/__tests__/async.test.ts +87 -0
  219. package/utils/__tests__/aws.test.ts +140 -0
  220. package/utils/__tests__/banners.test.ts +176 -0
  221. package/utils/__tests__/chart.test.ts +64 -1
  222. package/utils/__tests__/color.test.ts +226 -0
  223. package/utils/__tests__/duration.test.ts +140 -0
  224. package/utils/__tests__/fleet.test.ts +340 -0
  225. package/utils/__tests__/ingress.test.ts +553 -0
  226. package/utils/__tests__/kube.test.ts +68 -0
  227. package/utils/__tests__/namespace-filter.test.ts +109 -0
  228. package/utils/__tests__/pagination-utils.test.ts +361 -0
  229. package/utils/__tests__/parse-externalid.test.ts +137 -0
  230. package/utils/__tests__/perf-setting.utils.test.ts +98 -0
  231. package/utils/__tests__/poller-sequential.test.ts +177 -0
  232. package/utils/__tests__/poller.test.ts +170 -0
  233. package/utils/__tests__/promise.test.ts +346 -0
  234. package/utils/__tests__/settings.test.ts +140 -0
  235. package/utils/__tests__/sort-utils.test.ts +301 -0
  236. package/utils/__tests__/string-utils.test.ts +798 -0
  237. package/utils/__tests__/string.test.ts +23 -1
  238. package/utils/__tests__/style.test.ts +154 -0
  239. package/utils/__tests__/svg-filter.test.ts +184 -0
  240. package/utils/__tests__/units.test.ts +417 -0
  241. package/utils/__tests__/versions.test.ts +128 -0
  242. package/utils/__tests__/xccdf.test.ts +391 -0
  243. package/utils/chart.js +36 -0
  244. package/utils/fleet.ts +13 -3
  245. package/utils/gatekeeper/__tests__/util.test.ts +174 -0
  246. package/utils/gc/__tests__/gc-interval.test.ts +119 -0
  247. package/utils/gc/__tests__/gc-root-store.test.ts +225 -0
  248. package/utils/gc/__tests__/gc-route-changed.test.ts +96 -0
  249. package/utils/gc/__tests__/gc.test.ts +487 -0
  250. package/utils/ingress.ts +9 -1
  251. package/utils/pagination-utils.ts +2 -1
  252. package/utils/string.js +25 -2
  253. package/utils/uiplugins.ts +5 -5
  254. package/utils/validators/__tests__/cluster-name.test.ts +110 -0
  255. package/utils/validators/__tests__/cron-schedule.test.ts +79 -0
  256. package/utils/validators/__tests__/index.test.ts +481 -0
  257. package/utils/validators/__tests__/kubernetes-name.test.ts +163 -0
  258. package/utils/validators/__tests__/misc-validators.test.ts +246 -0
  259. package/utils/validators/__tests__/pod-affinity.test.ts +382 -0
  260. package/utils/validators/__tests__/prometheusrule.test.ts +211 -0
  261. package/utils/validators/__tests__/role-template.test.ts +149 -0
  262. package/utils/validators/__tests__/service.test.ts +283 -0
  263. package/utils/validators/__tests__/setting.test.js +32 -0
  264. package/utils/validators/formRules/__tests__/index.test.ts +50 -0
  265. package/utils/validators/formRules/index.ts +5 -5
  266. package/utils/validators/machine-pool.ts +1 -1
  267. package/utils/validators/setting.js +18 -3
  268. package/utils/xccdf.ts +418 -0
  269. package/assets/fonts/lato/lato-v17-latin-700.woff +0 -0
  270. package/assets/fonts/lato/lato-v17-latin-700.woff2 +0 -0
  271. package/assets/fonts/lato/lato-v17-latin-regular.woff +0 -0
  272. package/assets/fonts/lato/lato-v17-latin-regular.woff2 +0 -0
@@ -2,7 +2,8 @@ import { IExtension } from '@shell/core/types';
2
2
  import {
3
3
  ProductChild, ProductMetadata,
4
4
  ConfigureTypeConfiguration, VirtualTypeConfiguration,
5
- ProductChildCustomPage, VueRouteComponent, OverviewPageRoutingMetadata
5
+ ProductChildCustomPage, VueRouteComponent,
6
+ OverviewPageRoutingMetadata
6
7
  } from '@shell/core/plugin-types';
7
8
  import EmptyProductPage from '@shell/components/EmptyProductPage.vue';
8
9
  import pluginProductsHelpers from '@shell/core/plugin-products-helpers';
@@ -11,7 +12,7 @@ import {
11
12
  isProductChildWithComponent,
12
13
  isProductChildWithType,
13
14
  hasNameProperty,
14
- hasTypeProperty
15
+ hasTypeProperty,
15
16
  } from '@shell/core/plugin-products-type-guards';
16
17
 
17
18
  /**
@@ -27,6 +28,16 @@ export abstract class BasePluginProduct {
27
28
 
28
29
  protected registeredPageNames: Set<string> = new Set();
29
30
 
31
+ // Maps user-friendly group name → internal resolved name (e.g. 'monitoring' → 'myapp-monitoring')
32
+ // Populated during processGroupRecursively, consumed by moveToGroup resolution in processProductLevelDSLOptions
33
+ protected groupNameMap: Map<string, string> = new Map();
34
+
35
+ // Maps user-facing page identifier → internal basicType key
36
+ // Resource pages: type → type (identity, e.g. 'pod' → 'pod')
37
+ // Custom pages: name → prefixed name (e.g. 'myPage' → 'product1-myPage')
38
+ // Populated during configurePageItem, consumed by moveToGroup resolution in processProductLevelDSLOptions
39
+ protected pageIdMap: Map<string, string> = new Map();
40
+
30
41
  protected DSLMethods: any;
31
42
 
32
43
  protected config: ProductChild[];
@@ -40,11 +51,16 @@ export abstract class BasePluginProduct {
40
51
  */
41
52
  abstract get isNewProduct(): boolean;
42
53
 
54
+ get productName(): string {
55
+ return this.name;
56
+ }
57
+
43
58
  /**
44
59
  * Helper to throw errors during product registration
45
60
  */
46
- protected surfaceError(message: string): void {
47
- throw new Error(`Extensions - product "${ this.name }" registration error ::: ${ message }`);
61
+ protected surfaceError(message: string, e?: any): never {
62
+ console.error(`Extensions - product "${ this.name }" registration error ::: ${ message }`); // eslint-disable-line no-console
63
+ throw new Error(`Extensions - product "${ this.name }" registration error ::: ${ message }`, { cause: e });
48
64
  }
49
65
 
50
66
  /**
@@ -106,6 +122,12 @@ export abstract class BasePluginProduct {
106
122
  this.processGroupRecursively(item, this.name);
107
123
  }
108
124
  });
125
+
126
+ // Process product-level DSL options after all groups are registered
127
+ // so that the groupNameMap is fully populated for moveToGroup resolution
128
+ if (this.product) {
129
+ this.processProductLevelDSLOptions();
130
+ }
109
131
  }
110
132
 
111
133
  /**
@@ -124,6 +146,10 @@ export abstract class BasePluginProduct {
124
146
  const itemGroup = item;
125
147
  const groupName = parentGroupName ? `${ productName }-${ parentGroupName }-${ itemGroup.name }` : `${ productName }-${ itemGroup.name }`;
126
148
 
149
+ // Map the user's friendly group name to the resolved internal name
150
+ // so that moveToGroup can translate friendly names automatically
151
+ this.groupNameMap.set(itemGroup.name, groupName);
152
+
127
153
  if (!Array.isArray(itemGroup.children)) {
128
154
  this.surfaceError('Children defined for group are not in an array format');
129
155
 
@@ -181,7 +207,7 @@ export abstract class BasePluginProduct {
181
207
  }
182
208
 
183
209
  /**
184
- * Handles product registration via DSL
210
+ * Handles product registration via DSL (we also define entry route for the product here based on the config of the product - ordering)
185
211
  */
186
212
  protected handleProductRegistration(): void {
187
213
  const { basicType, product } = this.DSLMethods;
@@ -196,7 +222,7 @@ export abstract class BasePluginProduct {
196
222
 
197
223
  if (isProductChildGroup(firstConfig)) {
198
224
  // First config item is a group
199
- if (firstConfig.children.length) {
225
+ if (firstConfig.children.length > 0) {
200
226
  const entryChild = firstConfig.children[0];
201
227
 
202
228
  if (!firstConfig.component) {
@@ -241,11 +267,67 @@ export abstract class BasePluginProduct {
241
267
  });
242
268
  }
243
269
 
270
+ /**
271
+ * Process product-level DSL options: renameGroups, ignoreGroups, moveToGroup.
272
+ * Called after all config items and groups are registered so that the groupNameMap is fully populated.
273
+ */
274
+ protected processProductLevelDSLOptions(): void {
275
+ const {
276
+ mapGroup, ignoreGroup, moveType, basicType
277
+ } = this.DSLMethods;
278
+
279
+ if (this.product?.renameGroups?.length) {
280
+ this.product.renameGroups.forEach((mapping) => {
281
+ mapGroup(mapping.groupSelector, mapping.newName);
282
+ });
283
+ }
284
+
285
+ if (this.product?.ignoreGroups?.length) {
286
+ this.product.ignoreGroups.forEach((ignore) => {
287
+ if (ignore.condition) {
288
+ ignoreGroup(ignore.groupSelector, ignore.condition);
289
+ } else {
290
+ ignoreGroup(ignore.groupSelector);
291
+ }
292
+ });
293
+ }
294
+
295
+ if (this.product?.moveToGroup?.length) {
296
+ this.product.moveToGroup.forEach((move) => {
297
+ const resolvedGroup = this.groupNameMap.get(move.groupName);
298
+
299
+ if (!resolvedGroup) {
300
+ this.surfaceError(`moveToGroup target group "${ move.groupName }" not found. Available groups: ${ Array.from(this.groupNameMap.keys()).join(', ') }`);
301
+ }
302
+
303
+ const resolvedPageId = this.pageIdMap.get(move.entryId);
304
+
305
+ if (!resolvedPageId) {
306
+ this.surfaceError(`moveToGroup entryId "${ move.entryId }" not found. Available pages: ${ Array.from(this.pageIdMap.keys()).join(', ') }`);
307
+ }
308
+
309
+ // Re-register via basicType to move the page in the nav tree (basic view mode)
310
+ basicType([resolvedPageId], resolvedGroup);
311
+
312
+ // Also register via moveType for non-basic view modes (e.g. "in use" mode).
313
+ // moveType uses regex matching against schema IDs, so it only works for resource types.
314
+ const isResourceType = resolvedPageId === move.entryId;
315
+
316
+ if (isResourceType) {
317
+ moveType(move.entryId, resolvedGroup, move.weight);
318
+ }
319
+ });
320
+ }
321
+ }
322
+
244
323
  /**
245
324
  * Configure virtualType (custom page) or configureType (resource page) for a page item
246
325
  */
247
326
  protected configurePageItem(parentName: string, item: ProductChild, groupNaming?: string): void {
248
- const { configureType, virtualType, weightType } = this.DSLMethods;
327
+ const {
328
+ configureType, virtualType, weightType,
329
+ mapType, ignoreType, hideBulkActions, headers
330
+ } = this.DSLMethods;
249
331
 
250
332
  // Page with a "component" specified maps to a virtualType
251
333
  if (isProductChildWithComponent(item) || (isProductChildGroup(item) && item.component)) {
@@ -259,6 +341,7 @@ export abstract class BasePluginProduct {
259
341
  }
260
342
 
261
343
  this.registeredPageNames.add(finalName);
344
+ this.pageIdMap.set(item.name, finalName);
262
345
 
263
346
  const virtualTypeConfig: VirtualTypeConfiguration = {
264
347
  label: item.label,
@@ -290,6 +373,7 @@ export abstract class BasePluginProduct {
290
373
  }
291
374
 
292
375
  this.registeredPageNames.add(typeValue);
376
+ this.pageIdMap.set(typeValue, typeValue);
293
377
 
294
378
  const route = pluginProductsHelpers.generateConfigureTypeRoute(parentName, item, { extendProduct: !this.isNewProduct });
295
379
 
@@ -301,6 +385,22 @@ export abstract class BasePluginProduct {
301
385
  customRoute: route
302
386
  };
303
387
 
388
+ if (item.headers || item.sspHeaders) {
389
+ headers(item.type, item.headers, item.sspHeaders);
390
+ }
391
+
392
+ if (item.overrideListResourceName) {
393
+ mapType(item.type, item.overrideListResourceName);
394
+ }
395
+
396
+ if (item.hideFromNav) {
397
+ ignoreType(item.type);
398
+ }
399
+
400
+ if (item.hideBulkActions) {
401
+ hideBulkActions(item.type, true);
402
+ }
403
+
304
404
  configureType(typeValue, { ...configureTypeConfig, ...(item.config || {}) });
305
405
 
306
406
  if (item.weight) {
@@ -47,4 +47,8 @@ export class PluginProduct {
47
47
  get newProduct(): boolean {
48
48
  return this.instance.isNewProduct;
49
49
  }
50
+
51
+ get productName(): string {
52
+ return this.instance.productName;
53
+ }
50
54
  }
@@ -4,7 +4,7 @@ import { NAME as EXPLORER_PROD_NAME } from '@shell/config/product/explorer.js';
4
4
  import { NAME as CLUSTER_MAN_PROD_NAME } from '@shell/config/product/manager.js';
5
5
  import { NAME as SETTINGS_PROD_NAME } from '@shell/config/product/settings.js';
6
6
  import { NAME as AUTH_PROD_NAME } from '@shell/config/product/auth.js';
7
- import { ProductOptions } from '@shell/core/types';
7
+ import { ProductOptions, HeaderOptions, PaginationHeaderOptions } from '@shell/core/types';
8
8
 
9
9
  type Async<T> = () => Promise<T>;
10
10
 
@@ -185,6 +185,16 @@ export type ProductChildResourcePage = {
185
185
  config?: ConfigureTypeConfiguration;
186
186
  /** Ordering weight for this page among its siblings */
187
187
  weight?: number;
188
+ /** Use this to override the resource name used in the list view for this type */
189
+ overrideListResourceName?: string;
190
+ /** Whether to hide this resource from the side-menu entirely */
191
+ hideFromNav?: boolean;
192
+ /** Whether to hide bulk actions for this resource */
193
+ hideBulkActions?: boolean;
194
+ /** Table headers for this resource type (client-side pagination) */
195
+ headers?: HeaderOptions[];
196
+ /** Table headers for this resource type (server-side pagination) */
197
+ sspHeaders?: PaginationHeaderOptions[];
188
198
  };
189
199
 
190
200
  /**
@@ -218,6 +228,106 @@ export type ProductMetadata = Omit<ProductOptions, 'name' | 'label' | 'labelKey'
218
228
  * Product name (unique identifier)
219
229
  */
220
230
  name: string;
231
+ /**
232
+ * @internal
233
+ * Use `renameGroups` on the product metadata to remap group display names in the side-menu. Each entry matches a group's internal ID (via string or regex) and replaces its display label with a new name. This only changes how the group is labelled in the UI — it does not move resources between groups.
234
+ *
235
+ * The `groupSelector` is evaluated against group internal IDs. It can be an exact string or a `RegExp` pattern. The `newName` value is the new display name.
236
+ *
237
+ * const product: ProductMetadata = {
238
+ * name: 'my-app',
239
+ * label: 'My App',
240
+ * renameGroups: [
241
+ * // Rename a group with an ugly internal ID to a friendlier display name
242
+ * { groupSelector: 'cert-manager.io', newName: 'Certificates' },
243
+ * // Use a regex to rename all groups matching a pattern
244
+ * { groupSelector: /^networking\./, newName: 'Networking' },
245
+ * ],
246
+ * };
247
+ */
248
+ renameGroups?: {
249
+ /** String or regex to match against group internal IDs */
250
+ groupSelector: RegExp | string;
251
+ /** Display name to use for matching groups */
252
+ newName: string;
253
+ }[];
254
+ /**
255
+ * @internal
256
+ *
257
+ * Use `moveToGroup` on the product metadata to move pages (resource types or custom pages) into specific side-menu groups. This is useful when a page should appear inside a group but isn't defined as a child of that group in the config.
258
+ * Each entry identifies a page by its `entryId` — the resource `type` string or the custom page `name` — and moves it into the specified group. Use the group's `name` as you defined it in your config.
259
+ *
260
+ * const monitoringGroup: ProductChildGroup = {
261
+ * name: 'monitoring',
262
+ * label: 'Monitoring',
263
+ * children: [
264
+ * { name: 'alerts', label: 'Alerts', component: () => import('./pages/Alerts.vue') },
265
+ * ],
266
+ * };
267
+
268
+ * const dashboardPage: ProductChildCustomPage = {
269
+ * name: 'dashboard', label: 'Dashboard', component: () => import('./pages/Dashboard.vue'),
270
+ * };
271
+
272
+ * const product: ProductMetadata = {
273
+ * name: 'my-app',
274
+ * label: 'My App',
275
+ * moveToGroup: [
276
+ * // Move the 'pod' resource type into the 'monitoring' group
277
+ * { entryId: 'pod', groupName: 'monitoring' },
278
+ * // Move a custom page into the 'monitoring' group
279
+ * { entryId: 'dashboard', groupName: 'monitoring' },
280
+ * ],
281
+ * };
282
+ *
283
+ * extension.addProduct(product, [monitoringGroup, { type: 'pod' }, dashboardPage]);
284
+ *
285
+ * Note: The `entryId` must match a page declared in the same product config — either a resource page's `type` or a custom page's `name`. The target `groupName` must be a `ProductChildGroup` defined in the same config. If either is not found, an error is thrown at registration time listing the available options. Only exact string identifiers are supported (no regex).
286
+ *
287
+ * The optional `weight` parameter controls precedence when multiple `moveToGroup` rules target the same page (default: `5`). Higher weight takes precedence.
288
+ */
289
+ moveToGroup?: {
290
+ /** Page identifier — the resource `type` string or the custom page `name` */
291
+ entryId: string;
292
+ /** Target group name as defined in your group config (`name` property) */
293
+ groupName: string;
294
+ /** Ordering weight for the mapping (default: 5). Higher weight takes precedence when multiple rules match */
295
+ weight?: number;
296
+ }[];
297
+ /**
298
+ * @internal
299
+ *
300
+ * maps to DSL ignoreGroup
301
+ *
302
+ * Use `ignoreGroups` on the product metadata to hide specific side-menu groups. Each entry specifies a `groupSelector` to match group names — either an exact string or a regex pattern.
303
+ *
304
+ * The `condition` callback is optional. When provided, it receives the store getters and returns `true` to hide the group (conditional hide). When omitted, the group is always hidden (unconditional hide).
305
+ *
306
+ * Example usage:
307
+ * const product: ProductMetadata = {
308
+ * name: 'my-app',
309
+ * label: 'My App',
310
+ * ignoreGroups: [
311
+ * // Always hide the "internal" group (unconditional — no condition)
312
+ * { groupSelector: 'internal' },
313
+ * // Hide all groups matching a regex pattern (unconditional)
314
+ * { groupSelector: /^deprecated/ },
315
+ * // Conditionally hide based on a feature flag
316
+ * {
317
+ * groupSelector: 'experimental',
318
+ * condition: (getters) => !getters['features/isEnabled']('experimental-feature'),
319
+ * },
320
+ * ],
321
+ * };
322
+ *
323
+ *
324
+ * In this example, the "internal" group is always hidden, any group with a name starting with "deprecated-" is hidden, and the "experimental" group is hidden unless the "experimental-feature" flag is enabled in the store.
325
+ */
326
+ ignoreGroups?: {
327
+ /** String or regex to match against group names */
328
+ groupSelector: string | RegExp;
329
+ /** Optional conditional function that accepts the root Dashboard Vuex store getters and returns true if the group should be ignored */
330
+ condition?: (getters: any) => boolean }[];
221
331
  } & (
222
332
  /** Human-readable label for the product
223
333
  * Either label or labelKey are required */
package/core/plugin.ts CHANGED
@@ -135,21 +135,29 @@ export class Plugin implements IPlugin {
135
135
  }
136
136
 
137
137
  addProduct(product: ProductFunction | ProductMetadata | ProductSinglePage | string, config?: ProductChild[]): void {
138
+ let pluginProduct: PluginProduct;
139
+
138
140
  if (typeof product === 'string') {
139
- this.productConfigs.push(PluginProduct.fromName(this, product));
141
+ pluginProduct = PluginProduct.fromName(this, product);
140
142
  } else if (product?.name) {
141
143
  if (!config) {
142
- const p = product as ProductSinglePage;
143
-
144
- this.productConfigs.push(new PluginProduct(this, p, []));
144
+ pluginProduct = new PluginProduct(this, product as ProductSinglePage, []);
145
145
  } else {
146
- const p = product as ProductMetadata;
147
-
148
- this.productConfigs.push(new PluginProduct(this, p, config));
146
+ pluginProduct = new PluginProduct(this, product as ProductMetadata, config);
149
147
  }
150
148
  } else {
151
149
  this.products.push(product as ProductFunction);
150
+
151
+ return;
152
152
  }
153
+
154
+ const existingProduct = this.productConfigs.find((p) => p.newProduct && p.productName === pluginProduct.productName);
155
+
156
+ if (existingProduct) {
157
+ throw new Error(`Extensions - product "${ pluginProduct.productName }" registration error ::: addProduct can only be called once per product. Use extendProduct to add pages to an existing product.`);
158
+ }
159
+
160
+ this.productConfigs.push(pluginProduct);
153
161
  }
154
162
 
155
163
  extendProduct(product: StandardProductName | string, config: ProductChild[] | ProductChild): void {
@@ -21,8 +21,13 @@ export function DSLRegistrationsPerProduct(store, prodName) {
21
21
  }
22
22
 
23
23
  // prod configureType
24
- if (dataType === 'typeOptions' && typeMapData[dataType].filter((item) => item.customRoute && item.customRoute.name.includes(prodName))) {
25
- parsedData['configureType'] = typeMapData[dataType].filter((item) => item.customRoute && item.customRoute.name.includes(prodName));
24
+ if (dataType === 'typeOptions') {
25
+ const allTypeOptions = Object.values(typeMapData[dataType]).flat();
26
+ const filtered = allTypeOptions.filter((item) => item.customRoute && item.customRoute.name.includes(prodName));
27
+
28
+ if (filtered.length > 0) {
29
+ parsedData['configureType'] = filtered;
30
+ }
26
31
  }
27
32
 
28
33
  // other types which map with prodName
@@ -36,7 +41,7 @@ export function DSLRegistrationsPerProduct(store, prodName) {
36
41
  }
37
42
  });
38
43
 
39
- console.error('*** PRODUCT DATA DEBUGGER **** DSLRegistrationsPerProduct', parsedData); // eslint-disable-line no-console
44
+ console.error(`*** PRODUCT DATA DEBUGGER ${ prodName } **** DSLRegistrationsPerProduct`, parsedData); // eslint-disable-line no-console
40
45
  }
41
46
 
42
47
  export function registeredRoutes(store, prodName) {
@@ -44,5 +49,5 @@ export function registeredRoutes(store, prodName) {
44
49
 
45
50
  const parsedData = routes.filter((route) => route.path.includes(prodName));
46
51
 
47
- console.error('*** PRODUCT DATA DEBUGGER **** registeredRoutes', parsedData); // eslint-disable-line no-console
52
+ console.error(`*** PRODUCT DATA DEBUGGER ${ prodName } **** registeredRoutes`, parsedData); // eslint-disable-line no-console
48
53
  }
@@ -86,6 +86,48 @@ export interface ClusterProvisionerContext {
86
86
  isView: boolean
87
87
  }
88
88
 
89
+ /**
90
+ * Existing tabs to show or hide in the cluster's detail view
91
+ */
92
+ export interface ClusterProvisionerDetailTabs {
93
+ /**
94
+ * CAPI machine pool tab
95
+ */
96
+ machines: boolean,
97
+ /**
98
+ * Mgmt node pool tab
99
+ */
100
+ nodes?: boolean,
101
+ /**
102
+ * RKE2 provisioning logs
103
+ */
104
+ logs: boolean,
105
+ /**
106
+ * RKE2 registration commands
107
+ */
108
+ registration: boolean,
109
+ /**
110
+ * RKE2 snapshots
111
+ */
112
+ snapshots: boolean,
113
+ /**
114
+ * Kube resources related to the instance of provisioning.cattle.io.cluster
115
+ */
116
+ related: boolean,
117
+ /**
118
+ * Kube events associated with the instance of provisioning.cattle.io.cluster
119
+ */
120
+ events: boolean,
121
+ /**
122
+ * Kube conditions of the provisioning.cattle.io.cluster instance
123
+ */
124
+ conditions: boolean,
125
+ /**
126
+ * RKE2 autoscaler
127
+ */
128
+ autoscaler?: boolean,
129
+ }
130
+
89
131
  /**
90
132
  * Interface that a custom Cluster Provisioner should implement
91
133
  *
@@ -180,36 +222,7 @@ export interface IClusterProvisioner {
180
222
  *
181
223
  * `plugin.addTab(TabLocation.RESOURCE_DETAIL... ` can be used to add additional tabs to the same view
182
224
  */
183
- detailTabs: {
184
- /**
185
- * RKE2 machine pool tabs
186
- */
187
- machines: boolean,
188
- /**
189
- * RKE2 provisioning logs
190
- */
191
- logs: boolean,
192
- /**
193
- * RKE2 registration commands
194
- */
195
- registration: boolean,
196
- /**
197
- * RKE2 snapshots
198
- */
199
- snapshots: boolean,
200
- /**
201
- * Kube resources related to the instance of provisioning.cattle.io.cluster
202
- */
203
- related: boolean,
204
- /**
205
- * Kube events associated with the instance of provisioning.cattle.io.cluster
206
- */
207
- events: boolean,
208
- /**
209
- * Kube conditions of the provisioning.cattle.io.cluster instance
210
- */
211
- conditions: boolean
212
- };
225
+ detailTabs: ClusterProvisionerDetailTabs;
213
226
 
214
227
  /* --------------------------------------------------------------------------------------
215
228
  * Getters / Functions for Managing Machine Configs
package/core/types.ts CHANGED
@@ -4,8 +4,9 @@ import type { ExtensionManager } from '@shell/types/extension-manager';
4
4
  import { PaginationSettingsStores } from '@shell/types/resources/settings';
5
5
  import type {
6
6
  ProductMetadata, ProductSinglePage,
7
- StandardProductName, RouteRecordRawWithParams, ProductChildGroup,
8
- ProductChildPage, ProductChild
7
+ StandardProductName, RouteRecordRawWithParams, ProductChild,
8
+ ProductChildGroup,
9
+ ProductChildPage
9
10
  } from './plugin-types';
10
11
 
11
12
  // Cluster Provisioning types
@@ -552,7 +553,7 @@ export interface DSLReturnType {
552
553
  * @param headers {@link HeaderOptions[]}
553
554
  * @returns {@link void}
554
555
  */
555
- headers: (type: string, headers: HeaderOptions[]) => void;
556
+ headers: (type: string, headers?: HeaderOptions[], paginationHeaders?: PaginationHeaderOptions[]) => void;
556
557
 
557
558
  /**
558
559
  * Create and register a new product
@@ -562,12 +563,30 @@ export interface DSLReturnType {
562
563
  product: (options: ProductOptions) => void;
563
564
 
564
565
  /**
565
- * Create and label a group. The group will show up in navigation
566
- * @param groupNane Name of the group
567
- * @param label Label in navigation
566
+ /**
567
+ * Remap group display names in the side-menu navigation.
568
+ *
569
+ * Each entry matches a group's internal ID (via string or regex) and replaces its display label
570
+ * with a new name. This only changes how the group is labelled in the UI — it does not move
571
+ * resources between groups.
572
+ *
573
+ * @param match String, string for a regex or a regex object to match against group names
574
+ * @param replace Replacement string or function for the display name
575
+ * @param weight Priority for applying this mapping (higher numbers applied first, default 5)
576
+ * @param continueOnMatch If true, continue matching other rules after this one matches
577
+ * @returns {@link void}
578
+ */
579
+ mapGroup: (match: string | RegExp, replace: string | Function, weight?: number, continueOnMatch?: boolean) => void;
580
+
581
+ /**
582
+ * Remap a type ID to a display name
583
+ * @param match String, string for a regex or a regex object to match against type IDs
584
+ * @param replace Replacement string or function for the display name
585
+ * @param weight Priority for applying this mapping (higher numbers applied first, default 5)
586
+ * @param continueOnMatch If true, continue matching other rules after this one matches
568
587
  * @returns {@link void}
569
588
  */
570
- mapGroup: (groupName: string, label: string) => void;
589
+ mapType: (match: string | RegExp, replace: string | Function, weight?: number, continueOnMatch?: boolean) => void;
571
590
 
572
591
  /**
573
592
  * Create and configure a myriad of options for a type
@@ -595,18 +614,36 @@ export interface DSLReturnType {
595
614
  weightType: (input: string, weight: number, forBasic: boolean) => void;
596
615
 
597
616
  /**
598
- * Leaving these here for completeness but I don't think these should be advertised as useable to plugin creators.
617
+ * Never show the specified type in the navigation
618
+ * @param regexOrString String, string for a regex or a regex object to match against type names
619
+ * @returns {@link void}
620
+ */
621
+ ignoreType: (regexOrString: string | RegExp) => void;
622
+
623
+ /**
624
+ * Never show the specified group or any types in it
625
+ * @param regexOrString String, string for a regex or a regex object to match against group names
626
+ * @param fn Conditional function that accepts getters and returns true if the group should be ignored
627
+ * @returns {@link void}
599
628
  */
600
- // componentForType: (type: string, replacementType: string)
601
- // groupBy: (type: string, field: string)
602
- // hideBulkActions: (type: string, field)
603
- // ignoreGroup: (regexOrString)
604
- // ignoreType: (regexOrString)
605
- //
606
- // mapType: (match, replace)
607
- // moveType: (match, group)
608
- // setGroupDefaultType: (input, defaultType)
609
- // spoofedType: (obj)
629
+ ignoreGroup: (regexOrString: string | RegExp, fn?: (getters: any) => boolean) => void;
630
+
631
+ /**
632
+ * Move a resource type into a different navigation group
633
+ * @param match String or regex to match against resource type names
634
+ * @param group Target group name to move the matched types into
635
+ * @param weight Ordering weight for the mapping (default: 5)
636
+ * @returns {@link void}
637
+ */
638
+ moveType: (match: string | RegExp, group: string, weight?: number) => void;
639
+
640
+ /**
641
+ * Control visibility of bulk actions (e.g. delete) in the list view toolbar for a specific resource type
642
+ * @param type The resource type to configure
643
+ * @param hide Whether to hide bulk actions. Set to `true` to hide them
644
+ * @returns {@link void}
645
+ */
646
+ hideBulkActions: (type: string, hide: boolean) => void;
610
647
 
611
648
  labelGroup: (group: string, label: string | undefined, labelKey?: string) => void;
612
649
 
@@ -663,8 +700,8 @@ export interface IExtension {
663
700
  * @param name
664
701
  * @param config
665
702
  */
666
- addProduct(product: ProductMetadata, config: ProductChildGroup[]): void;
667
703
  addProduct(product: ProductMetadata, config: ProductChildPage[]): void;
704
+ addProduct(product: ProductMetadata, config: ProductChildGroup[]): void;
668
705
  addProduct(product: ProductMetadata, config: ProductChild[]): void;
669
706
 
670
707
  /**
@@ -694,8 +731,8 @@ export interface IExtension {
694
731
  * @param product Product to be extended
695
732
  * @param config Product extension configuration
696
733
  */
697
- extendProduct(product: StandardProductName | string, config: ProductChildGroup[]): void;
698
734
  extendProduct(product: StandardProductName | string, config: ProductChildPage[]): void;
735
+ extendProduct(product: StandardProductName | string, config: ProductChildGroup[]): void;
699
736
  extendProduct(product: StandardProductName | string, config: ProductChild[]): void;
700
737
 
701
738
  /**
@@ -0,0 +1,41 @@
1
+ import podDetail from '@shell/detail/pod.vue';
2
+
3
+ const { containers } = podDetail.computed;
4
+
5
+ describe('view: pod', () => {
6
+ it('should not use 2x icon sizing in init container column', () => {
7
+ const initContainer = { name: 'init', image: 'init:latest' };
8
+ const appContainer = { name: 'app', image: 'app:latest' };
9
+
10
+ const rows = containers.call({
11
+ allContainers: [appContainer, initContainer],
12
+ allStatuses: [{
13
+ name: 'app',
14
+ ready: true,
15
+ state: {},
16
+ lastState: {}
17
+ }, {
18
+ name: 'init',
19
+ ready: false,
20
+ state: {},
21
+ lastState: {}
22
+ }],
23
+ dateTimeFormat: () => '',
24
+ t: () => '',
25
+ value: {
26
+ containerActions: [],
27
+ containerIsInit: (container: { name: string }) => container.name === 'init',
28
+ containerStateColor: () => 'text-success',
29
+ containerStateDisplay: () => 'running',
30
+ openLogs: jest.fn(),
31
+ openShell: jest.fn(),
32
+ },
33
+ });
34
+
35
+ const initRow = rows.find((row: { name: string }) => row.name === 'init');
36
+ const appRow = rows.find((row: { name: string }) => row.name === 'app');
37
+
38
+ expect(initRow.initIcon).toBe('icon-checkmark text-success ml-5');
39
+ expect(appRow.initIcon).toBe('icon-minus text-muted ml-5');
40
+ });
41
+ });