@rancher/shell 3.0.11 → 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 (98) 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 +5 -4
  5. package/assets/translations/zh-hans.yaml +0 -3
  6. package/components/EmptyProductPage.vue +76 -0
  7. package/components/Resource/Detail/CopyToClipboard.vue +1 -2
  8. package/components/Resource/Detail/Metadata/KeyValueRow.vue +9 -3
  9. package/components/Resource/Detail/TitleBar/__tests__/__snapshots__/index.test.ts.snap +31 -0
  10. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +45 -1
  11. package/components/Resource/Detail/TitleBar/index.vue +1 -1
  12. package/components/Resource/Detail/ViewOptions/__tests__/__snapshots__/index.test.ts.snap +9 -0
  13. package/components/Resource/Detail/ViewOptions/__tests__/index.test.ts +62 -0
  14. package/components/Resource/Detail/ViewOptions/index.vue +2 -1
  15. package/components/ResourceList/Masthead.vue +25 -2
  16. package/components/SideNav.vue +13 -0
  17. package/components/__tests__/PromptModal.test.ts +2 -0
  18. package/components/fleet/FleetClusters.vue +1 -0
  19. package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
  20. package/components/form/NodeScheduling.vue +17 -3
  21. package/components/form/PrivateRegistry.vue +69 -0
  22. package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
  23. package/components/formatter/WorkloadHealthScale.vue +3 -1
  24. package/components/nav/Group.vue +26 -3
  25. package/components/nav/Header.vue +32 -7
  26. package/components/nav/TopLevelMenu.vue +15 -1
  27. package/config/pagination-table-headers.js +8 -1
  28. package/config/product/apps.js +2 -1
  29. package/config/product/auth.js +1 -0
  30. package/config/product/backup.js +1 -0
  31. package/config/product/compliance.js +1 -1
  32. package/config/product/explorer.js +25 -6
  33. package/config/product/fleet.js +1 -0
  34. package/config/product/gatekeeper.js +1 -0
  35. package/config/product/istio.js +1 -0
  36. package/config/product/logging.js +1 -0
  37. package/config/product/longhorn.js +2 -1
  38. package/config/product/manager.js +1 -0
  39. package/config/product/monitoring.js +1 -0
  40. package/config/product/navlinks.js +1 -0
  41. package/config/product/neuvector.js +2 -1
  42. package/config/product/settings.js +1 -0
  43. package/config/product/uiplugins.js +1 -0
  44. package/core/__tests__/plugin-products-helpers.test.ts +454 -0
  45. package/core/__tests__/plugin-products.test.ts +3219 -0
  46. package/core/extension-manager-impl.js +30 -1
  47. package/core/plugin-products-base.ts +375 -0
  48. package/core/plugin-products-extending.ts +44 -0
  49. package/core/plugin-products-helpers.ts +262 -0
  50. package/core/plugin-products-top-level.ts +66 -0
  51. package/core/plugin-products-type-guards.ts +33 -0
  52. package/core/plugin-products.ts +50 -0
  53. package/core/plugin-types.ts +222 -0
  54. package/core/plugin.ts +45 -10
  55. package/core/productDebugger.js +48 -0
  56. package/core/types.ts +95 -11
  57. package/detail/__tests__/__snapshots__/fleet.cattle.io.bundle.test.ts.snap +52 -0
  58. package/detail/__tests__/fleet.cattle.io.bundle.test.ts +171 -0
  59. package/detail/fleet.cattle.io.bundle.vue +21 -34
  60. package/dialog/ExtensionCatalogInstallDialog.vue +1 -1
  61. package/dialog/InstallExtensionDialog.vue +6 -27
  62. package/dialog/UninstallExistingExtensionDialog.vue +141 -0
  63. package/dialog/UninstallExtensionDialog.vue +4 -26
  64. package/dialog/__tests__/UninstallExistingExtensionDialog.test.ts +114 -0
  65. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -0
  66. package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
  67. package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
  68. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
  69. package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
  70. package/list/provisioning.cattle.io.cluster.vue +0 -1
  71. package/list/workload.vue +11 -4
  72. package/mixins/resource-fetch.js +12 -3
  73. package/models/pod.js +18 -0
  74. package/models/workload.js +20 -2
  75. package/package.json +1 -2
  76. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
  77. package/pages/c/_cluster/settings/brand.vue +4 -4
  78. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +231 -13
  79. package/pages/c/_cluster/uiplugins/index.vue +143 -37
  80. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -0
  81. package/plugins/dashboard-store/actions.js +3 -2
  82. package/plugins/dashboard-store/resource-class.js +62 -6
  83. package/plugins/plugin.js +16 -0
  84. package/plugins/steve/steve-pagination-utils.ts +7 -0
  85. package/scripts/typegen.sh +13 -1
  86. package/store/__tests__/type-map.test.ts +84 -24
  87. package/store/type-map.js +42 -3
  88. package/tsconfig.paths.json +1 -0
  89. package/types/resources/pod.ts +18 -0
  90. package/types/shell/index.d.ts +8506 -2909
  91. package/types/store/dashboard-store.types.ts +5 -0
  92. package/types/store/pagination.types.ts +6 -0
  93. package/utils/axios.js +1 -4
  94. package/utils/dynamic-importer.js +3 -2
  95. package/utils/pagination-utils.ts +1 -1
  96. package/utils/uiplugins.ts +12 -16
  97. package/utils/validators/__tests__/private-registry.test.ts +76 -0
  98. package/utils/validators/private-registry.ts +28 -0
@@ -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,375 @@
1
+ import { IExtension } from '@shell/core/types';
2
+ import {
3
+ ProductChild, ProductMetadata,
4
+ ConfigureTypeConfiguration, VirtualTypeConfiguration,
5
+ ProductChildCustomPage
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 DSLMethods: any;
29
+
30
+ protected config: ProductChild[];
31
+
32
+ constructor(config: ProductChild[]) {
33
+ this.config = config;
34
+ }
35
+
36
+ /**
37
+ * Indicates whether this is a new product or extending an existing one
38
+ */
39
+ abstract get isNewProduct(): boolean;
40
+
41
+ /**
42
+ * Helper to throw errors during product registration
43
+ */
44
+ protected surfaceError(message: string): void {
45
+ throw new Error(`Extensions - product "${ this.name }" registration error ::: ${ message }`);
46
+ }
47
+
48
+ /**
49
+ * Validates and reorders config children by weight
50
+ */
51
+ protected processConfigChildren(): void {
52
+ if (this.config?.length > 0) {
53
+ // consider weights of children to determine default route
54
+ const reorderedChildren = pluginProductsHelpers.gatherChildrenOrdering(this.config);
55
+
56
+ if (reorderedChildren.length === 0) {
57
+ this.surfaceError('No children found for product with config');
58
+ }
59
+
60
+ const firstChild = reorderedChildren[0];
61
+
62
+ if (!hasNameProperty(firstChild) && !hasTypeProperty(firstChild)) {
63
+ this.surfaceError('Invalid child item for product default route - missing name or type');
64
+ }
65
+
66
+ // update config with reordered children
67
+ this.config = reorderedChildren;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * This is where we register the product and its children via the DSL
73
+ */
74
+ apply(plugin: IExtension, store: any): void {
75
+ // store the DSL methods for easier access
76
+ this.DSLMethods = plugin.DSL(store, this.name);
77
+
78
+ const { basicType } = this.DSLMethods;
79
+
80
+ // execute the product registration
81
+ // this.product is NOT set when extending existing standard products
82
+ // this is deliberate as we don't need to re-register existing products
83
+ // we just leverage the DSL to add routes and configure types/virtual types with the correct product context
84
+ if (this.product) {
85
+ this.handleProductRegistration();
86
+ }
87
+
88
+ // Now deal with each config item
89
+ this.config.forEach((item) => {
90
+ // needs to be "true" so that group base pages are registered correctly (when group parent has component)
91
+ const names = this.getIDsForGroupsOrBasicTypes(this.name, this.config, true);
92
+
93
+ basicType(names);
94
+ this.configurePageItem(this.name, item);
95
+
96
+ if (isProductChildGroup(item)) {
97
+ this.processGroupRecursively(item, this.name);
98
+ }
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Recursively processes a group and all its nested children/groups
104
+ */
105
+ protected processGroupRecursively(item: ProductChild, productName: string, parentGroupName?: string, parentHierarchicalPath?: string): void {
106
+ const {
107
+ basicType, labelGroup, setGroupDefaultType, weightGroup
108
+ } = this.DSLMethods;
109
+
110
+ // Type guard to ensure we're working with a group
111
+ if (!isProductChildGroup(item)) {
112
+ return;
113
+ }
114
+
115
+ const itemGroup = item;
116
+ const groupName = parentGroupName ? `${ productName }-${ parentGroupName }-${ itemGroup.name }` : `${ productName }-${ itemGroup.name }`;
117
+
118
+ if (!Array.isArray(itemGroup.children)) {
119
+ this.surfaceError('Children defined for group are not in an array format');
120
+
121
+ return;
122
+ }
123
+
124
+ const navNames = this.getIDsForGroupsOrBasicTypes(groupName, itemGroup.children);
125
+
126
+ // Build the full hierarchical path with :: separators for the store's _ensureGroup function
127
+ // For example: "explorer-root::explorer-root-group1" tells the store to nest group1 inside root
128
+ const hierarchicalPath = parentHierarchicalPath ? `${ parentHierarchicalPath }::${ groupName }` : groupName;
129
+
130
+ // For root-level groups (no parent), add the group itself to establish its identity.
131
+ // For nested groups, skip this - they're already registered in their parent's basicType.
132
+ // Adding nested groups here would overwrite their parent registration and make them
133
+ // appear at the wrong level in the hierarchy.
134
+ if (!parentGroupName) {
135
+ navNames.push(groupName);
136
+ }
137
+
138
+ // Register the group's children (and possibly itself if root) under this group's hierarchical path
139
+ // This uses :: separators so the store knows to create nested group structure
140
+ basicType(navNames, hierarchicalPath);
141
+
142
+ // register virtualTypes/configureTypes for each child item
143
+ itemGroup.children.forEach((subItem: ProductChild) => {
144
+ const currentGroupName = parentGroupName ? `${ parentGroupName }-${ itemGroup.name }` : itemGroup.name;
145
+
146
+ this.configurePageItem(productName, subItem, currentGroupName);
147
+
148
+ // Recursively process nested groups, passing the full hierarchical path
149
+ if (isProductChildGroup(subItem)) {
150
+ this.processGroupRecursively(subItem, productName, currentGroupName, hierarchicalPath);
151
+ }
152
+ });
153
+
154
+ // set the group indication based on whether the group has its own component page
155
+ if (!itemGroup.component) {
156
+ // Group without component - route to first child
157
+ setGroupDefaultType(groupName, navNames[0]);
158
+ } else {
159
+ // Group with component - route to the group's own page
160
+ setGroupDefaultType(groupName, groupName);
161
+ }
162
+
163
+ // group weight
164
+ if (itemGroup.weight) {
165
+ weightGroup(groupName, itemGroup.weight, true);
166
+ }
167
+
168
+ // group label
169
+ if (itemGroup.label || itemGroup.labelKey) {
170
+ labelGroup(groupName, itemGroup.label, itemGroup.labelKey);
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Handles product registration via DSL
176
+ */
177
+ protected handleProductRegistration(): void {
178
+ const { basicType, product } = this.DSLMethods;
179
+
180
+ let defaultRoute;
181
+ const names = this.getIDsForGroupsOrBasicTypes(this.name, this.config);
182
+ const defaultResource = names[0] || '';
183
+
184
+ // this is the default "to" route for a product with config (at least 1 item on config) ordered by weight
185
+ if (defaultResource) {
186
+ const firstConfig = this.config[0];
187
+
188
+ if (isProductChildGroup(firstConfig)) {
189
+ // First config item is a group
190
+ if (firstConfig.children.length) {
191
+ const entryChild = firstConfig.children[0];
192
+
193
+ if (!firstConfig.component) {
194
+ // Group without component - route to first child
195
+ if (isProductChildWithType(entryChild)) {
196
+ defaultRoute = pluginProductsHelpers.generateConfigureTypeRoute(this.name, entryChild, { omitPath: true, extendProduct: !this.isNewProduct });
197
+ } else if (isProductChildWithComponent(entryChild)) {
198
+ defaultRoute = pluginProductsHelpers.generateVirtualTypeRoute(this.name, entryChild, { omitPath: true, extendProduct: !this.isNewProduct });
199
+ }
200
+ } else {
201
+ // Group with component - route to the group page itself
202
+ defaultRoute = pluginProductsHelpers.generateVirtualTypeRoute(this.name, undefined, {
203
+ omitPath: true, component: firstConfig.component, extendProduct: !this.isNewProduct
204
+ });
205
+ }
206
+ } else if (firstConfig.component) {
207
+ // Group with component but no children - route to the group page itself
208
+ defaultRoute = pluginProductsHelpers.generateVirtualTypeRoute(this.name, undefined, {
209
+ omitPath: true, component: firstConfig.component, extendProduct: !this.isNewProduct
210
+ });
211
+ }
212
+ } else if (isProductChildWithType(firstConfig)) {
213
+ // Simple configureType page (resource page)
214
+ defaultRoute = pluginProductsHelpers.generateConfigureTypeRoute(this.name, firstConfig, { omitPath: true, extendProduct: !this.isNewProduct });
215
+ } else if (isProductChildWithComponent(firstConfig)) {
216
+ // Simple virtual type page (custom page)
217
+ defaultRoute = pluginProductsHelpers.generateVirtualTypeRoute(this.name, firstConfig, { omitPath: true, extendProduct: !this.isNewProduct });
218
+ }
219
+ } else if (this.isNewProduct) {
220
+ // this is the "to" route for a simple page product (no config items)
221
+ defaultRoute = pluginProductsHelpers.generateTopLevelExtensionSimpleBaseRoute(this.name, { omitPath: true });
222
+ basicType(names);
223
+ }
224
+
225
+ // register the product via DSL
226
+ product({
227
+ showClusterSwitcher: false,
228
+ extendable: false,
229
+ ...this.product,
230
+ category: 'global',
231
+ to: defaultRoute,
232
+ icon: this.product?.icon || 'extension',
233
+ version: 2,
234
+ inStore: 'management',
235
+ name: this.name,
236
+ });
237
+ }
238
+
239
+ /**
240
+ * Configure virtualType (custom page) or configureType (resource page) for a page item
241
+ */
242
+ protected configurePageItem(parentName: string, item: ProductChild, groupNaming?: string): void {
243
+ const { configureType, virtualType, weightType } = this.DSLMethods;
244
+
245
+ // Page with a "component" specified maps to a virtualType
246
+ if (isProductChildWithComponent(item) || (isProductChildGroup(item) && item.component)) {
247
+ // Extract properties we need from the narrowed item
248
+ const name = `${ parentName }-${ item.name }`;
249
+ const finalName = groupNaming ? `${ parentName }-${ groupNaming }-${ item.name }` : name;
250
+
251
+ const virtualTypeConfig: VirtualTypeConfiguration = {
252
+ label: item.label,
253
+ labelKey: item.labelKey,
254
+ namespaced: false,
255
+ name: finalName,
256
+ weight: item.weight, // ordering is done here and not via "weightType"
257
+ };
258
+
259
+ // if the item with COMPONENT has children then it's a GROUP virtualType, so set "exact" and "overview" to "true"
260
+ // so that when navigating to the group page, it shows the custom page for the group
261
+ if (isProductChildGroup(item)) {
262
+ virtualTypeConfig.exact = true;
263
+ virtualTypeConfig.overview = true;
264
+ virtualTypeConfig.route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, undefined, { extendProduct: !this.isNewProduct });
265
+ } else {
266
+ virtualTypeConfig.route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, item, { extendProduct: !this.isNewProduct });
267
+ }
268
+
269
+ virtualType({ ...virtualTypeConfig, ...(isProductChildWithComponent(item) ? item.config || {} : {}) });
270
+ } else if (isProductChildWithType(item)) {
271
+ // Page with a "type" specified maps to a configureType
272
+ const typeValue = item.type;
273
+ const route = pluginProductsHelpers.generateConfigureTypeRoute(parentName, item, { extendProduct: !this.isNewProduct });
274
+
275
+ const configureTypeConfig: ConfigureTypeConfiguration = {
276
+ isCreatable: true,
277
+ isEditable: true,
278
+ isRemovable: true,
279
+ canYaml: true,
280
+ customRoute: route
281
+ };
282
+
283
+ configureType(typeValue, { ...configureTypeConfig, ...(item.config || {}) });
284
+
285
+ if (item.weight) {
286
+ weightType(typeValue, item.weight, true);
287
+ }
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Add routes in Vue-router for any items that need them
293
+ */
294
+ protected addRoutes(plugin: IExtension, parentName: string, item: ProductChild[]): void {
295
+ item.forEach((child) => {
296
+ // if the child has children, then it's a group
297
+ if (isProductChildGroup(child)) {
298
+ // Validate group doesn't have type property
299
+ if (hasTypeProperty(child)) {
300
+ this.surfaceError('Group items cannot have a "type" property - only custom pages can have groups.');
301
+ }
302
+
303
+ if (child.component && !this.isNewProduct) {
304
+ this.surfaceError('When extending an existing product, group parent items cannot have a component because of route matching conflicts.');
305
+ }
306
+
307
+ let route;
308
+
309
+ if (!child.component) {
310
+ // Create minimal page object for route generation
311
+ const pageForRoute: ProductChildCustomPage = {
312
+ name: child.name,
313
+ label: child.label || child.labelKey || child.name,
314
+ component: EmptyProductPage
315
+ };
316
+
317
+ route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, pageForRoute, { extendProduct: !this.isNewProduct });
318
+ } else {
319
+ route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, undefined, { component: child.component, extendProduct: !this.isNewProduct });
320
+ }
321
+
322
+ // add the route for the group page/parent
323
+ plugin.addRoute(route);
324
+
325
+ // add children routes
326
+ this.addRoutes(plugin, `${ parentName }`, child.children);
327
+ } else if (isProductChildWithComponent(child)) {
328
+ // virtualType page
329
+ if (hasTypeProperty(child)) {
330
+ this.surfaceError('Custom pages cannot have a "type" property - only resource pages can use "type".');
331
+ }
332
+
333
+ const route = pluginProductsHelpers.generateVirtualTypeRoute(parentName, child, { component: child.component, extendProduct: !this.isNewProduct });
334
+
335
+ plugin.addRoute(route);
336
+ } else if (isProductChildWithType(child)) {
337
+ // Validate type-based children don't have component property
338
+ // The type guard ensures child has 'type', so we just need to check component doesn't exist
339
+ // Since ProductChildPage with type has component?: never, this is a runtime validation
340
+
341
+ // configureType page (resource)
342
+ if (!this.addedResourceRoutes) {
343
+ this.addedResourceRoutes = true;
344
+
345
+ const resourceRoutes = pluginProductsHelpers.generateResourceRoutes(parentName, child, { extendProduct: !this.isNewProduct });
346
+
347
+ resourceRoutes.forEach((resRoute) => {
348
+ plugin.addRoute(resRoute);
349
+ });
350
+ }
351
+ }
352
+ });
353
+ }
354
+
355
+ /**
356
+ * Get IDs for groups/basicTypes
357
+ */
358
+ protected getIDsForGroupsOrBasicTypes(parent: string, data: ProductChild[], excludeGrouping = false): string[] {
359
+ return data.map((item) => {
360
+ if (excludeGrouping && isProductChildGroup(item)) {
361
+ return null;
362
+ }
363
+
364
+ if (typeof item === 'string') {
365
+ return item;
366
+ } else if (hasNameProperty(item)) {
367
+ return `${ parent }-${ item.name }`;
368
+ } else if (hasTypeProperty(item)) {
369
+ return item.type;
370
+ }
371
+
372
+ return '';
373
+ }).filter((name): name is string => typeof name === 'string' && name.length > 0);
374
+ }
375
+ }
@@ -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
+ }