@rancher/shell 3.0.11 → 3.0.12-rc.2

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 (219) hide show
  1. package/assets/images/providers/entraid-black.svg +4 -0
  2. package/assets/images/providers/entraid.svg +9 -0
  3. package/assets/images/vendor/entraid.svg +9 -0
  4. package/assets/styles/app.scss +0 -1
  5. package/assets/styles/base/_mixins.scss +31 -0
  6. package/assets/styles/base/_variables.scss +2 -0
  7. package/assets/styles/themes/_modern.scss +6 -5
  8. package/assets/translations/en-us.yaml +24 -21
  9. package/assets/translations/zh-hans.yaml +4 -11
  10. package/chart/__tests__/S3.test.ts +10 -3
  11. package/components/CountBox.vue +20 -0
  12. package/components/CreateDriver.vue +0 -12
  13. package/components/DetailText.vue +12 -3
  14. package/components/EmptyProductPage.vue +76 -0
  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/SelectIconGrid.vue +5 -0
  25. package/components/SideNav.vue +13 -0
  26. package/components/__tests__/CountBox.test.ts +72 -0
  27. package/components/__tests__/DetailText.test.ts +113 -0
  28. package/components/__tests__/PromptModal.test.ts +2 -0
  29. package/components/fleet/FleetClusterTargets/index.vue +18 -1
  30. package/components/fleet/FleetClusters.vue +1 -0
  31. package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
  32. package/components/form/InputWithSelect.vue +18 -10
  33. package/components/form/KeyValue.vue +17 -1
  34. package/components/form/LabeledSelect.vue +82 -24
  35. package/components/form/NodeScheduling.vue +17 -3
  36. package/components/form/PrivateRegistry.vue +69 -0
  37. package/components/form/Select.vue +73 -56
  38. package/components/form/ServiceNameSelect.vue +13 -11
  39. package/components/form/__tests__/KeyValue.test.ts +66 -0
  40. package/components/form/__tests__/NodeScheduling.test.ts +9 -0
  41. package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
  42. package/components/form/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
  43. package/components/formatter/WorkloadHealthScale.vue +3 -1
  44. package/components/nav/Group.vue +33 -9
  45. package/components/nav/Header.vue +56 -10
  46. package/components/nav/NotificationCenter/Notification.vue +4 -1
  47. package/components/nav/NotificationCenter/NotificationHeader.vue +20 -8
  48. package/components/nav/NotificationCenter/__tests__/NotificationHeader.test.ts +80 -0
  49. package/components/nav/TopLevelMenu.vue +15 -1
  50. package/components/nav/Type.vue +8 -7
  51. package/components/nav/WindowManager/index.vue +2 -1
  52. package/components/nav/WorkspaceSwitcher.vue +13 -0
  53. package/components/nav/__tests__/Group.test.ts +67 -0
  54. package/components/nav/__tests__/Header.test.ts +235 -0
  55. package/components/nav/__tests__/Type.test.ts +20 -3
  56. package/components/templates/default.vue +34 -4
  57. package/components/templates/home.vue +12 -25
  58. package/components/templates/plain.vue +13 -26
  59. package/composables/useLabeledFormElement.ts +10 -2
  60. package/composables/useLabeledSelect.ts +60 -0
  61. package/composables/useUserRetentionValidation.ts +1 -49
  62. package/config/cookies.js +0 -1
  63. package/config/labels-annotations.js +1 -0
  64. package/config/pagination-table-headers.js +8 -1
  65. package/config/product/apps.js +2 -1
  66. package/config/product/auth.js +1 -0
  67. package/config/product/backup.js +1 -0
  68. package/config/product/compliance.js +1 -1
  69. package/config/product/explorer.js +25 -6
  70. package/config/product/fleet.js +1 -0
  71. package/config/product/gatekeeper.js +1 -0
  72. package/config/product/istio.js +1 -0
  73. package/config/product/logging.js +1 -0
  74. package/config/product/longhorn.js +2 -1
  75. package/config/product/manager.js +1 -0
  76. package/config/product/monitoring.js +1 -0
  77. package/config/product/navlinks.js +1 -0
  78. package/config/product/neuvector.js +2 -1
  79. package/config/product/settings.js +1 -0
  80. package/config/product/uiplugins.js +1 -0
  81. package/config/query-params.js +1 -0
  82. package/config/router/routes.js +0 -8
  83. package/core/__tests__/plugin-products-helpers.test.ts +454 -0
  84. package/core/__tests__/plugin-products.test.ts +3810 -0
  85. package/core/extension-manager-impl.js +30 -1
  86. package/core/plugin-products-base.ts +392 -0
  87. package/core/plugin-products-extending.ts +44 -0
  88. package/core/plugin-products-helpers.ts +263 -0
  89. package/core/plugin-products-top-level.ts +66 -0
  90. package/core/plugin-products-type-guards.ts +33 -0
  91. package/core/plugin-products.ts +50 -0
  92. package/core/plugin-types.ts +237 -0
  93. package/core/plugin.ts +45 -10
  94. package/core/productDebugger.js +48 -0
  95. package/core/types.ts +97 -11
  96. package/detail/__tests__/__snapshots__/fleet.cattle.io.bundle.test.ts.snap +52 -0
  97. package/detail/__tests__/fleet.cattle.io.bundle.test.ts +171 -0
  98. package/detail/__tests__/management.cattle.io.fleetworkspace.test.ts +128 -0
  99. package/detail/fleet.cattle.io.bundle.vue +21 -34
  100. package/detail/management.cattle.io.fleetworkspace.vue +49 -0
  101. package/dialog/ExtensionCatalogInstallDialog.vue +1 -1
  102. package/dialog/InstallExtensionDialog.vue +6 -27
  103. package/dialog/UninstallExistingExtensionDialog.vue +141 -0
  104. package/dialog/UninstallExtensionDialog.vue +4 -26
  105. package/dialog/__tests__/UninstallExistingExtensionDialog.test.ts +114 -0
  106. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -0
  107. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +9 -0
  108. package/edit/__tests__/kontainerDriver.test.ts +0 -13
  109. package/edit/__tests__/nodeDriver.test.ts +5 -11
  110. package/edit/__tests__/resources.cattle.io.restore.test.ts +9 -0
  111. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  112. package/edit/auth/__tests__/oidc.test.ts +54 -0
  113. package/edit/auth/azuread.vue +1 -1
  114. package/edit/auth/oidc.vue +8 -0
  115. package/edit/kontainerDriver.vue +1 -2
  116. package/edit/nodeDriver.vue +0 -2
  117. package/edit/provisioning.cattle.io.cluster/AgentEnv.vue +1 -0
  118. package/edit/provisioning.cattle.io.cluster/__tests__/AgentEnv.test.ts +25 -0
  119. package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
  120. package/edit/provisioning.cattle.io.cluster/index.vue +70 -99
  121. package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
  122. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
  123. package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
  124. package/initialize/App.vue +29 -2
  125. package/initialize/install-plugins.js +0 -2
  126. package/list/__tests__/management.cattle.io.feature.test.ts +105 -0
  127. package/list/catalog.cattle.io.app.vue +25 -5
  128. package/list/management.cattle.io.feature.vue +1 -1
  129. package/list/management.cattle.io.fleetworkspace.vue +8 -0
  130. package/list/provisioning.cattle.io.cluster.vue +0 -1
  131. package/list/workload.vue +11 -4
  132. package/machine-config/amazonec2.vue +1 -0
  133. package/mixins/chart.js +40 -9
  134. package/mixins/resource-fetch.js +12 -3
  135. package/models/__tests__/catalog.cattle.io.app.test.ts +15 -1
  136. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +84 -0
  137. package/models/__tests__/chart.test.ts +99 -6
  138. package/models/__tests__/management.cattle.io.feature.test.ts +131 -0
  139. package/models/__tests__/monitoring.coreos.com.alertmanagerconfig.test.ts +98 -0
  140. package/models/catalog.cattle.io.app.js +21 -17
  141. package/models/catalog.cattle.io.clusterrepo.js +39 -11
  142. package/models/chart.js +33 -19
  143. package/models/fleet-application.js +1 -1
  144. package/models/fleet.cattle.io.bundle.js +1 -1
  145. package/models/kontainerdriver.js +11 -0
  146. package/models/management.cattle.io.authconfig.js +5 -1
  147. package/models/management.cattle.io.cluster.js +0 -53
  148. package/models/management.cattle.io.feature.js +3 -3
  149. package/models/management.cattle.io.kontainerdriver.js +1 -26
  150. package/models/monitoring.coreos.com.alertmanagerconfig.js +31 -17
  151. package/models/nodedriver.js +7 -0
  152. package/models/pod.js +18 -0
  153. package/models/workload.js +20 -2
  154. package/package.json +13 -13
  155. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
  156. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +189 -0
  157. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +55 -0
  158. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +53 -0
  159. package/pages/c/_cluster/apps/charts/chart.vue +217 -33
  160. package/pages/c/_cluster/apps/charts/index.vue +2 -2
  161. package/pages/c/_cluster/apps/charts/install.vue +8 -3
  162. package/pages/c/_cluster/auth/user.retention/index.vue +55 -22
  163. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -7
  164. package/pages/c/_cluster/settings/brand.vue +4 -4
  165. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +39 -2
  166. package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +61 -0
  167. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +246 -23
  168. package/pages/c/_cluster/uiplugins/index.vue +166 -62
  169. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -0
  170. package/plugins/dashboard-store/actions.js +3 -2
  171. package/plugins/dashboard-store/resource-class.js +62 -6
  172. package/plugins/plugin.js +16 -0
  173. package/plugins/steve/steve-pagination-utils.ts +7 -0
  174. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +205 -1
  175. package/rancher-components/Form/LabeledInput/LabeledInput.vue +82 -4
  176. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +1 -1
  177. package/scripts/test-plugins-build.sh +5 -2
  178. package/scripts/typegen.sh +13 -1
  179. package/server/server-middleware.js +2 -2
  180. package/static/humans.txt +1 -0
  181. package/static/robots.txt +34 -0
  182. package/static/welcome-cow.svg +18 -0
  183. package/store/__tests__/catalog.test.ts +161 -11
  184. package/store/__tests__/type-map.test.ts +84 -24
  185. package/store/auth.js +0 -3
  186. package/store/catalog.js +60 -8
  187. package/store/type-map.js +42 -3
  188. package/tsconfig.paths.json +1 -0
  189. package/types/resources/pod.ts +18 -0
  190. package/types/shell/index.d.ts +8539 -2938
  191. package/types/store/dashboard-store.types.ts +5 -0
  192. package/types/store/pagination.types.ts +6 -0
  193. package/utils/__tests__/git.test.ts +270 -0
  194. package/utils/__tests__/inactivity.test.ts +316 -0
  195. package/utils/__tests__/object.test.ts +77 -0
  196. package/utils/__tests__/time.test.ts +14 -1
  197. package/utils/__tests__/url.test.ts +246 -0
  198. package/utils/axios.js +1 -4
  199. package/utils/dynamic-importer.js +3 -2
  200. package/utils/object.js +33 -2
  201. package/utils/pagination-utils.ts +1 -1
  202. package/utils/time.ts +5 -0
  203. package/utils/uiplugins.ts +12 -16
  204. package/utils/validators/__tests__/private-registry.test.ts +76 -0
  205. package/utils/validators/private-registry.ts +28 -0
  206. package/vue.config.js +0 -9
  207. package/assets/images/providers/azuread-black.svg +0 -22
  208. package/assets/images/providers/azuread.svg +0 -25
  209. package/assets/images/vendor/azuread.svg +0 -18
  210. package/assets/styles/fonts/_dots.scss +0 -18
  211. package/components/EmberPage.vue +0 -622
  212. package/components/EmberPageView.vue +0 -39
  213. package/components/form/labeled-select-utils/labeled-select-pagination.ts +0 -116
  214. package/mixins/labeled-form-element.ts +0 -225
  215. package/pages/c/_cluster/explorer/tools/pages/_page.vue +0 -28
  216. package/pages/c/_cluster/manager/pages/_page.vue +0 -22
  217. package/pages/c/_cluster/mcapps/pages/_page.vue +0 -22
  218. package/plugins/ember-cookie.js +0 -17
  219. package/utils/ember-page.js +0 -30
@@ -407,6 +407,11 @@ export const createExtensionManager = (context) => {
407
407
  }
408
408
  },
409
409
 
410
+ // Internal use only
411
+ _add(id, plugin) {
412
+ plugins[id] = plugin;
413
+ },
414
+
410
415
  // For debugging
411
416
  getAll() {
412
417
  return dynamic;
@@ -500,7 +505,11 @@ export const createExtensionManager = (context) => {
500
505
  loadPlugins = Object.values(plugins);
501
506
  }
502
507
 
503
- loadPlugins.forEach((plugin) => {
508
+ // Ensure builtin plugins are processed before external plugins so that
509
+ // core + builtin products are registered first and available for extending
510
+ const orderedPlugins = [...loadPlugins.filter((p) => p.builtin), ...loadPlugins.filter((p) => !p.builtin)];
511
+
512
+ orderedPlugins.forEach((plugin) => {
504
513
  if (plugin.products) {
505
514
  plugin.products.forEach(async(p) => {
506
515
  const impl = await p;
@@ -510,6 +519,26 @@ export const createExtensionManager = (context) => {
510
519
  }
511
520
  });
512
521
  }
522
+
523
+ // Load products and product extensions using the simpler API
524
+ if (plugin.productConfigs?.length) {
525
+ // Add new products first
526
+ plugin.productConfigs.filter((p) => p.newProduct).forEach((productConfig) => {
527
+ productConfig.apply(plugin, store, app.router, pluginRoutes);
528
+ });
529
+
530
+ // Extend existing products after new products are added
531
+ plugin.productConfigs.filter((p) => !p.newProduct).forEach((productConfig) => {
532
+ productConfig.apply(plugin, store, app.router, pluginRoutes);
533
+ });
534
+ }
535
+
536
+ // Apply all type configurations
537
+ if (plugin.resourceTypeConfigs?.length) {
538
+ plugin.resourceTypeConfigs.forEach((resourceTypeConfig) => {
539
+ resourceTypeConfig.apply(plugin, store, app.router, pluginRoutes);
540
+ });
541
+ }
513
542
  });
514
543
  },
515
544
  };
@@ -0,0 +1,392 @@
1
+ import { IExtension } from '@shell/core/types';
2
+ import {
3
+ ProductChild, ProductMetadata,
4
+ ConfigureTypeConfiguration, VirtualTypeConfiguration,
5
+ ProductChildCustomPage, VueRouteComponent, OverviewPageRoutingMetadata
6
+ } from '@shell/core/plugin-types';
7
+ import EmptyProductPage from '@shell/components/EmptyProductPage.vue';
8
+ import pluginProductsHelpers from '@shell/core/plugin-products-helpers';
9
+ import {
10
+ isProductChildGroup,
11
+ isProductChildWithComponent,
12
+ isProductChildWithType,
13
+ hasNameProperty,
14
+ hasTypeProperty
15
+ } from '@shell/core/plugin-products-type-guards';
16
+
17
+ /**
18
+ * Base class for product registration in extensions
19
+ * @internal
20
+ */
21
+ export abstract class BasePluginProduct {
22
+ protected name!: string;
23
+
24
+ protected product?: ProductMetadata;
25
+
26
+ protected addedResourceRoutes = false;
27
+
28
+ protected registeredPageNames: Set<string> = new Set();
29
+
30
+ protected DSLMethods: any;
31
+
32
+ protected config: ProductChild[];
33
+
34
+ constructor(config: ProductChild[]) {
35
+ this.config = config;
36
+ }
37
+
38
+ /**
39
+ * Indicates whether this is a new product or extending an existing one
40
+ */
41
+ abstract get isNewProduct(): boolean;
42
+
43
+ /**
44
+ * Helper to throw errors during product registration
45
+ */
46
+ protected surfaceError(message: string): void {
47
+ throw new Error(`Extensions - product "${ this.name }" registration error ::: ${ message }`);
48
+ }
49
+
50
+ /**
51
+ * Validates and reorders config children by weight
52
+ */
53
+ protected processConfigChildren(): void {
54
+ if (this.config?.length > 0) {
55
+ // consider weights of children to determine default route
56
+ const reorderedChildren = pluginProductsHelpers.gatherChildrenOrdering(this.config);
57
+
58
+ if (reorderedChildren.length === 0) {
59
+ this.surfaceError('No children found for product with config');
60
+ }
61
+
62
+ const firstChild = reorderedChildren[0];
63
+
64
+ if (!hasNameProperty(firstChild) && !hasTypeProperty(firstChild)) {
65
+ this.surfaceError('Invalid child item for product default route - missing name or type');
66
+ }
67
+
68
+ // update config with reordered children
69
+ this.config = reorderedChildren;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Generates data for group overview page routing
75
+ */
76
+ protected generateMetadataForGroupOverviewPageRouting(name: string, component: VueRouteComponent): OverviewPageRoutingMetadata {
77
+ return { name, component };
78
+ }
79
+
80
+ /**
81
+ * This is where we register the product and its children via the DSL
82
+ */
83
+ apply(plugin: IExtension, store: any): void {
84
+ // store the DSL methods for easier access
85
+ this.DSLMethods = plugin.DSL(store, this.name);
86
+
87
+ const { basicType } = this.DSLMethods;
88
+
89
+ // execute the product registration
90
+ // this.product is NOT set when extending existing standard products
91
+ // this is deliberate as we don't need to re-register existing products
92
+ // we just leverage the DSL to add routes and configure types/virtual types with the correct product context
93
+ if (this.product) {
94
+ this.handleProductRegistration();
95
+ }
96
+
97
+ // Now deal with each config item
98
+ this.config.forEach((item) => {
99
+ // needs to be "true" so that group base pages are registered correctly (when group parent has component)
100
+ const names = this.getIDsForGroupsOrBasicTypes(this.name, this.config, true);
101
+
102
+ basicType(names);
103
+ this.configurePageItem(this.name, item);
104
+
105
+ if (isProductChildGroup(item)) {
106
+ this.processGroupRecursively(item, this.name);
107
+ }
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Recursively processes a group and all its nested children/groups
113
+ */
114
+ protected processGroupRecursively(item: ProductChild, productName: string, parentGroupName?: string, parentHierarchicalPath?: string): void {
115
+ const {
116
+ basicType, labelGroup, setGroupDefaultType, weightGroup
117
+ } = this.DSLMethods;
118
+
119
+ // Type guard to ensure we're working with a group
120
+ if (!isProductChildGroup(item)) {
121
+ return;
122
+ }
123
+
124
+ const itemGroup = item;
125
+ const groupName = parentGroupName ? `${ productName }-${ parentGroupName }-${ itemGroup.name }` : `${ productName }-${ itemGroup.name }`;
126
+
127
+ if (!Array.isArray(itemGroup.children)) {
128
+ this.surfaceError('Children defined for group are not in an array format');
129
+
130
+ return;
131
+ }
132
+
133
+ const navNames = this.getIDsForGroupsOrBasicTypes(groupName, itemGroup.children);
134
+
135
+ // Build the full hierarchical path with :: separators for the store's _ensureGroup function
136
+ // For example: "explorer-root::explorer-root-group1" tells the store to nest group1 inside root
137
+ const hierarchicalPath = parentHierarchicalPath ? `${ parentHierarchicalPath }::${ groupName }` : groupName;
138
+
139
+ // For root-level groups (no parent), add the group itself to establish its identity.
140
+ // For nested groups, skip this - they're already registered in their parent's basicType.
141
+ // Adding nested groups here would overwrite their parent registration and make them
142
+ // appear at the wrong level in the hierarchy.
143
+ if (!parentGroupName) {
144
+ navNames.push(groupName);
145
+ }
146
+
147
+ // Register the group's children (and possibly itself if root) under this group's hierarchical path
148
+ // This uses :: separators so the store knows to create nested group structure
149
+ basicType(navNames, hierarchicalPath);
150
+
151
+ // register virtualTypes/configureTypes for each child item
152
+ itemGroup.children.forEach((subItem: ProductChild) => {
153
+ const currentGroupName = parentGroupName ? `${ parentGroupName }-${ itemGroup.name }` : itemGroup.name;
154
+
155
+ this.configurePageItem(productName, subItem, currentGroupName);
156
+
157
+ // Recursively process nested groups, passing the full hierarchical path
158
+ if (isProductChildGroup(subItem)) {
159
+ this.processGroupRecursively(subItem, productName, currentGroupName, hierarchicalPath);
160
+ }
161
+ });
162
+
163
+ // set the group indication based on whether the group has its own component page
164
+ if (!itemGroup.component) {
165
+ // Group without component - route to first child
166
+ setGroupDefaultType(groupName, navNames[0]);
167
+ } else {
168
+ // Group with component - route to the group's own page
169
+ setGroupDefaultType(groupName, groupName);
170
+ }
171
+
172
+ // group weight
173
+ if (itemGroup.weight) {
174
+ weightGroup(groupName, itemGroup.weight, true);
175
+ }
176
+
177
+ // group label
178
+ if (itemGroup.label || itemGroup.labelKey) {
179
+ labelGroup(groupName, itemGroup.label, itemGroup.labelKey);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Handles product registration via DSL
185
+ */
186
+ protected handleProductRegistration(): void {
187
+ const { basicType, product } = this.DSLMethods;
188
+
189
+ let defaultRoute;
190
+ const names = this.getIDsForGroupsOrBasicTypes(this.name, this.config);
191
+ const defaultResource = names[0] || '';
192
+
193
+ // this is the default "to" route for a product with config (at least 1 item on config) ordered by weight
194
+ if (defaultResource) {
195
+ const firstConfig = this.config[0];
196
+
197
+ if (isProductChildGroup(firstConfig)) {
198
+ // First config item is a group
199
+ if (firstConfig.children.length) {
200
+ const entryChild = firstConfig.children[0];
201
+
202
+ if (!firstConfig.component) {
203
+ // Group without component - route to first child
204
+ if (isProductChildWithType(entryChild)) {
205
+ defaultRoute = pluginProductsHelpers.generateConfigureTypeRoute(this.name, entryChild, { omitPath: true, extendProduct: !this.isNewProduct });
206
+ } else if (isProductChildWithComponent(entryChild)) {
207
+ defaultRoute = pluginProductsHelpers.generateVirtualTypeRoute(this.name, entryChild, { omitPath: true, extendProduct: !this.isNewProduct });
208
+ }
209
+ } else {
210
+ // Group with component - route to the group overview page (which will render the group's component and side-menu)
211
+ defaultRoute = pluginProductsHelpers.generateVirtualTypeRoute(this.name, this.generateMetadataForGroupOverviewPageRouting(firstConfig.name, firstConfig.component), { omitPath: true, extendProduct: !this.isNewProduct });
212
+ }
213
+ } else if (firstConfig.component) {
214
+ // Group with component but no children - route to the group page itself
215
+ defaultRoute = pluginProductsHelpers.generateVirtualTypeRoute(this.name, this.generateMetadataForGroupOverviewPageRouting(firstConfig.name, firstConfig.component), { omitPath: true, extendProduct: !this.isNewProduct });
216
+ }
217
+ } else if (isProductChildWithType(firstConfig)) {
218
+ // Simple configureType page (resource page)
219
+ defaultRoute = pluginProductsHelpers.generateConfigureTypeRoute(this.name, firstConfig, { omitPath: true, extendProduct: !this.isNewProduct });
220
+ } else if (isProductChildWithComponent(firstConfig)) {
221
+ // Simple virtual type page (custom page)
222
+ defaultRoute = pluginProductsHelpers.generateVirtualTypeRoute(this.name, firstConfig, { omitPath: true, extendProduct: !this.isNewProduct });
223
+ }
224
+ } else if (this.isNewProduct) {
225
+ // this is the "to" route for a simple page product (no config items)
226
+ defaultRoute = pluginProductsHelpers.generateTopLevelExtensionSimpleBaseRoute(this.name, { omitPath: true });
227
+ basicType(names);
228
+ }
229
+
230
+ // register the product via DSL
231
+ product({
232
+ showClusterSwitcher: false,
233
+ extendable: false,
234
+ ...this.product,
235
+ category: 'global',
236
+ to: defaultRoute,
237
+ icon: this.product?.icon || 'extension',
238
+ version: 2,
239
+ inStore: 'management',
240
+ name: this.name,
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Configure virtualType (custom page) or configureType (resource page) for a page item
246
+ */
247
+ protected configurePageItem(parentName: string, item: ProductChild, groupNaming?: string): void {
248
+ const { configureType, virtualType, weightType } = this.DSLMethods;
249
+
250
+ // Page with a "component" specified maps to a virtualType
251
+ if (isProductChildWithComponent(item) || (isProductChildGroup(item) && item.component)) {
252
+ // Extract properties we need from the narrowed item
253
+ const name = `${ parentName }-${ item.name }`;
254
+ const finalName = groupNaming ? `${ parentName }-${ groupNaming }-${ item.name }` : name;
255
+
256
+ // Check for duplicate page names within the same product
257
+ if (this.registeredPageNames.has(finalName)) {
258
+ this.surfaceError(`Duplicate page name "${ item.name }" - each page must have a unique name within a product`);
259
+ }
260
+
261
+ this.registeredPageNames.add(finalName);
262
+
263
+ const virtualTypeConfig: VirtualTypeConfiguration = {
264
+ label: item.label,
265
+ labelKey: item.labelKey,
266
+ namespaced: false,
267
+ name: finalName,
268
+ weight: item.weight, // ordering is done here and not via "weightType"
269
+ };
270
+
271
+ // if the item with COMPONENT has children then it's a GROUP virtualType, so set "exact" and "overview" to "true"
272
+ // so that when navigating to the group page, it shows the custom page for the group
273
+ if (isProductChildGroup(item)) {
274
+ virtualTypeConfig.exact = true;
275
+ virtualTypeConfig.overview = true;
276
+ // Pass group metadata as pageChild so the route gets a unique path segment (e.g. /product/c/:cluster/groupName)
277
+ virtualTypeConfig.route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, this.generateMetadataForGroupOverviewPageRouting(item.name, item.component as ProductChildCustomPage['component']), { extendProduct: !this.isNewProduct });
278
+ } else {
279
+ virtualTypeConfig.route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, item, { extendProduct: !this.isNewProduct });
280
+ }
281
+
282
+ virtualType({ ...virtualTypeConfig, ...(isProductChildWithComponent(item) ? item.config || {} : {}) });
283
+ } else if (isProductChildWithType(item)) {
284
+ // Page with a "type" specified maps to a configureType
285
+ const typeValue = item.type;
286
+
287
+ // Check for duplicate resource type within the same product
288
+ if (this.registeredPageNames.has(typeValue)) {
289
+ this.surfaceError(`Duplicate resource type "${ typeValue }" - each resource type must be unique within a product`);
290
+ }
291
+
292
+ this.registeredPageNames.add(typeValue);
293
+
294
+ const route = pluginProductsHelpers.generateConfigureTypeRoute(parentName, item, { extendProduct: !this.isNewProduct });
295
+
296
+ const configureTypeConfig: ConfigureTypeConfiguration = {
297
+ isCreatable: true,
298
+ isEditable: true,
299
+ isRemovable: true,
300
+ canYaml: true,
301
+ customRoute: route
302
+ };
303
+
304
+ configureType(typeValue, { ...configureTypeConfig, ...(item.config || {}) });
305
+
306
+ if (item.weight) {
307
+ weightType(typeValue, item.weight, true);
308
+ }
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Add routes in Vue-router for any items that need them
314
+ */
315
+ protected addRoutes(plugin: IExtension, parentName: string, item: ProductChild[]): void {
316
+ item.forEach((child) => {
317
+ // if the child has children, then it's a group
318
+ if (isProductChildGroup(child)) {
319
+ // Validate group doesn't have type property
320
+ if (hasTypeProperty(child)) {
321
+ this.surfaceError('Group items cannot have a "type" property - only custom pages can have groups.');
322
+ }
323
+
324
+ let route;
325
+
326
+ if (!child.component) {
327
+ // Create minimal page object for route generation
328
+ const pageForRoute: ProductChildCustomPage = {
329
+ name: child.name,
330
+ label: child.label || child.labelKey || child.name,
331
+ component: EmptyProductPage
332
+ };
333
+
334
+ route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, pageForRoute, { extendProduct: !this.isNewProduct });
335
+ } else {
336
+ route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, this.generateMetadataForGroupOverviewPageRouting(child.name, child.component), { component: child.component, extendProduct: !this.isNewProduct });
337
+ }
338
+
339
+ // add the route for the group page/parent
340
+ plugin.addRoute(route);
341
+
342
+ // add children routes
343
+ this.addRoutes(plugin, `${ parentName }`, child.children);
344
+ } else if (isProductChildWithComponent(child)) {
345
+ // virtualType page
346
+ if (hasTypeProperty(child)) {
347
+ this.surfaceError('Custom pages cannot have a "type" property - only resource pages can use "type".');
348
+ }
349
+
350
+ const route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, child, { component: child.component, extendProduct: !this.isNewProduct });
351
+
352
+ plugin.addRoute(route);
353
+ } else if (isProductChildWithType(child)) {
354
+ // Validate type-based children don't have component property
355
+ // The type guard ensures child has 'type', so we just need to check component doesn't exist
356
+ // Since ProductChildPage with type has component?: never, this is a runtime validation
357
+
358
+ // configureType page (resource)
359
+ if (!this.addedResourceRoutes) {
360
+ this.addedResourceRoutes = true;
361
+
362
+ const resourceRoutes = pluginProductsHelpers.generateResourceRoutes(parentName, child, { extendProduct: !this.isNewProduct });
363
+
364
+ resourceRoutes.forEach((resRoute) => {
365
+ plugin.addRoute(resRoute);
366
+ });
367
+ }
368
+ }
369
+ });
370
+ }
371
+
372
+ /**
373
+ * Get IDs for groups/basicTypes
374
+ */
375
+ protected getIDsForGroupsOrBasicTypes(parent: string, data: ProductChild[], excludeGrouping = false): string[] {
376
+ return data.map((item) => {
377
+ if (excludeGrouping && isProductChildGroup(item)) {
378
+ return null;
379
+ }
380
+
381
+ if (typeof item === 'string') {
382
+ return item;
383
+ } else if (hasNameProperty(item)) {
384
+ return `${ parent }-${ item.name }`;
385
+ } else if (hasTypeProperty(item)) {
386
+ return item.type;
387
+ }
388
+
389
+ return '';
390
+ }).filter((name): name is string => typeof name === 'string' && name.length > 0);
391
+ }
392
+ }
@@ -0,0 +1,44 @@
1
+ import { IExtension } from '@shell/core/types';
2
+ import { ProductChild, StandardProductName } from '@shell/core/plugin-types';
3
+ import EmptyProductPage from '@shell/components/EmptyProductPage.vue';
4
+ import { BasePluginProduct } from '@shell/core/plugin-products-base';
5
+
6
+ /**
7
+ * Represents extending an existing standard product
8
+ * @internal
9
+ */
10
+ export class ExtendingPluginProduct extends BasePluginProduct {
11
+ get isNewProduct(): boolean {
12
+ return false;
13
+ }
14
+
15
+ constructor(plugin: IExtension, productName: StandardProductName | string, config: ProductChild[]) {
16
+ super(config);
17
+
18
+ // existing standard product - no need to add routes
19
+ this.name = productName;
20
+
21
+ if (this.config?.length > 0) {
22
+ this.processConfigChildren();
23
+ } else {
24
+ // If no config is provided, add a default empty page
25
+ this.config = [{
26
+ name: 'main',
27
+ label: 'Main',
28
+ component: EmptyProductPage,
29
+ }];
30
+ }
31
+
32
+ this.addRoutes(plugin, this.name, this.config);
33
+ }
34
+
35
+ apply(plugin: IExtension, store: any): void {
36
+ const product = store.getters['type-map/productByName'](this.name);
37
+
38
+ if (!product?.extendable) {
39
+ this.surfaceError(`Product "${ this.name }" is not extendable. You can only extend core Dashboard products or builtin extensions.`);
40
+ }
41
+
42
+ super.apply(plugin, store);
43
+ }
44
+ }