@rancher/shell 3.0.10 → 3.0.12-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/styles/base/_mixins.scss +31 -0
- package/assets/styles/base/_variables.scss +2 -0
- package/assets/styles/themes/_modern.scss +6 -5
- package/assets/translations/en-us.yaml +12 -9
- package/assets/translations/zh-hans.yaml +0 -3
- package/chart/__tests__/rancher-backup-index.test.ts +248 -0
- package/chart/rancher-backup/index.vue +41 -2
- package/components/BrandImage.vue +6 -5
- package/components/ConsumptionGauge.vue +12 -4
- package/components/DynamicContent/DynamicContentIcon.vue +3 -2
- package/components/EmptyProductPage.vue +76 -0
- package/components/ExplorerProjectsNamespaces.vue +1 -4
- package/components/LazyImage.vue +2 -1
- package/components/Resource/Detail/Card/Scaler.vue +4 -4
- package/components/Resource/Detail/CopyToClipboard.vue +1 -2
- package/components/Resource/Detail/Metadata/KeyValueRow.vue +9 -3
- package/components/Resource/Detail/TitleBar/__tests__/__snapshots__/index.test.ts.snap +31 -0
- package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +45 -1
- package/components/Resource/Detail/TitleBar/index.vue +1 -1
- package/components/Resource/Detail/ViewOptions/__tests__/__snapshots__/index.test.ts.snap +9 -0
- package/components/Resource/Detail/ViewOptions/__tests__/index.test.ts +62 -0
- package/components/Resource/Detail/ViewOptions/index.vue +2 -1
- package/components/ResourceList/Masthead.vue +25 -2
- package/components/SideNav.vue +13 -0
- package/components/Tabbed/index.vue +6 -0
- package/components/__tests__/ConsumptionGauge.test.ts +31 -0
- package/components/__tests__/PromptModal.test.ts +2 -0
- package/components/fleet/FleetClusters.vue +1 -0
- package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
- package/components/form/NodeScheduling.vue +17 -3
- package/components/form/PrivateRegistry.vue +69 -0
- package/components/form/ProjectMemberEditor.vue +0 -10
- package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
- package/components/formatter/WorkloadHealthScale.vue +3 -1
- package/components/nav/Group.vue +26 -3
- package/components/nav/Header.vue +32 -7
- package/components/nav/TopLevelMenu.helper.ts +7 -79
- package/components/nav/TopLevelMenu.vue +15 -1
- package/components/nav/__tests__/TopLevelMenu.helper.test.ts +2 -53
- package/config/pagination-table-headers.js +8 -1
- package/config/private-label.js +2 -1
- package/config/product/apps.js +3 -1
- package/config/product/auth.js +1 -0
- package/config/product/backup.js +1 -0
- package/config/product/compliance.js +1 -1
- package/config/product/explorer.js +25 -6
- package/config/product/fleet.js +1 -0
- package/config/product/gatekeeper.js +1 -0
- package/config/product/istio.js +1 -0
- package/config/product/logging.js +1 -0
- package/config/product/longhorn.js +2 -1
- package/config/product/manager.js +1 -0
- package/config/product/monitoring.js +1 -0
- package/config/product/navlinks.js +1 -0
- package/config/product/neuvector.js +2 -1
- package/config/product/settings.js +1 -0
- package/config/product/uiplugins.js +1 -0
- package/core/__tests__/extension-manager-impl.test.js +187 -2
- package/core/__tests__/plugin-products-helpers.test.ts +454 -0
- package/core/__tests__/plugin-products.test.ts +3219 -0
- package/core/extension-manager-impl.js +34 -3
- package/core/plugin-helpers.ts +31 -0
- package/core/plugin-products-base.ts +375 -0
- package/core/plugin-products-extending.ts +44 -0
- package/core/plugin-products-helpers.ts +262 -0
- package/core/plugin-products-top-level.ts +66 -0
- package/core/plugin-products-type-guards.ts +33 -0
- package/core/plugin-products.ts +50 -0
- package/core/plugin-types.ts +222 -0
- package/core/plugin.ts +45 -10
- package/core/productDebugger.js +48 -0
- package/core/types.ts +95 -11
- package/detail/__tests__/__snapshots__/fleet.cattle.io.bundle.test.ts.snap +52 -0
- package/detail/__tests__/fleet.cattle.io.bundle.test.ts +171 -0
- package/detail/__tests__/node.test.ts +83 -0
- package/detail/fleet.cattle.io.bundle.vue +21 -34
- package/detail/management.cattle.io.oidcclient.vue +2 -1
- package/detail/node.vue +1 -0
- package/dialog/ExtensionCatalogInstallDialog.vue +1 -1
- package/dialog/InstallExtensionDialog.vue +6 -27
- package/dialog/UninstallExistingExtensionDialog.vue +141 -0
- package/dialog/UninstallExtensionDialog.vue +4 -26
- package/dialog/__tests__/UninstallExistingExtensionDialog.test.ts +114 -0
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -0
- package/edit/catalog.cattle.io.clusterrepo.vue +17 -3
- package/edit/cloudcredential.vue +2 -1
- package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -6
- package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +5 -4
- package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
- package/edit/provisioning.cattle.io.cluster/shared.ts +4 -2
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
- package/edit/secret/generic.vue +1 -0
- package/edit/secret/index.vue +2 -1
- package/edit/service.vue +2 -14
- package/list/management.cattle.io.feature.vue +7 -1
- package/list/provisioning.cattle.io.cluster.vue +0 -50
- package/list/workload.vue +11 -4
- package/mixins/brand.js +2 -1
- package/mixins/resource-fetch.js +12 -3
- package/models/catalog.cattle.io.clusterrepo.js +9 -0
- package/models/cluster.x-k8s.io.machinedeployment.js +8 -3
- package/models/management.cattle.io.authconfig.js +2 -1
- package/models/management.cattle.io.cluster.js +4 -3
- package/models/monitoring.coreos.com.receiver.js +11 -6
- package/models/pod.js +18 -0
- package/models/provisioning.cattle.io.cluster.js +2 -2
- package/models/workload.js +20 -2
- package/package.json +5 -6
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
- package/pages/c/_cluster/apps/charts/index.vue +3 -8
- package/pages/c/_cluster/apps/charts/install.vue +8 -9
- package/pages/c/_cluster/istio/index.vue +4 -2
- package/pages/c/_cluster/longhorn/index.vue +2 -1
- package/pages/c/_cluster/monitoring/index.vue +2 -2
- package/pages/c/_cluster/neuvector/index.vue +2 -1
- package/pages/c/_cluster/settings/brand.vue +4 -4
- package/pages/c/_cluster/settings/performance.vue +0 -5
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +2 -1
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +231 -13
- package/pages/c/_cluster/uiplugins/index.vue +145 -38
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -0
- package/plugins/dashboard-store/actions.js +3 -2
- package/plugins/dashboard-store/resource-class.js +62 -6
- package/plugins/plugin.js +16 -0
- package/plugins/steve/steve-pagination-utils.ts +8 -2
- package/plugins/steve/subscribe.js +29 -4
- package/rancher-components/RcButton/RcButton.vue +3 -3
- package/rancher-components/RcButtonSplit/RcButtonSplit.test.ts +253 -0
- package/rancher-components/RcButtonSplit/RcButtonSplit.vue +158 -0
- package/rancher-components/RcButtonSplit/index.ts +1 -0
- package/scripts/test-plugins-build.sh +4 -4
- package/scripts/typegen.sh +13 -1
- package/store/__tests__/type-map.test.ts +84 -24
- package/store/type-map.js +42 -3
- package/tsconfig.paths.json +1 -0
- package/types/resources/pod.ts +18 -0
- package/types/shell/index.d.ts +8506 -2908
- package/types/store/dashboard-store.types.ts +5 -0
- package/types/store/pagination.types.ts +6 -0
- package/utils/__tests__/require-asset.test.ts +98 -0
- package/utils/async.ts +1 -5
- package/utils/axios.js +1 -4
- package/utils/brand.ts +3 -1
- package/utils/dynamic-importer.js +3 -2
- package/utils/favicon.js +4 -3
- package/utils/pagination-utils.ts +1 -1
- package/utils/require-asset.ts +95 -0
- package/utils/uiplugins.ts +12 -16
- package/utils/validators/__tests__/private-registry.test.ts +76 -0
- package/utils/validators/private-registry.ts +28 -0
- package/vue.config.js +4 -3
- package/components/HarvesterServiceAddOnConfig.vue +0 -207
|
@@ -124,7 +124,9 @@ export const createExtensionManager = (context) => {
|
|
|
124
124
|
} catch (e) {
|
|
125
125
|
delete plugins[id];
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
console.error(`Could not initialize plugin ${ id }`, e); // eslint-disable-line no-console
|
|
128
|
+
|
|
129
|
+
return reject(new Error(`Could not initialize plugin ${ id } - ${ e?.message }`));
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
// Load all of the types etc from the plugin
|
|
@@ -405,6 +407,11 @@ export const createExtensionManager = (context) => {
|
|
|
405
407
|
}
|
|
406
408
|
},
|
|
407
409
|
|
|
410
|
+
// Internal use only
|
|
411
|
+
_add(id, plugin) {
|
|
412
|
+
plugins[id] = plugin;
|
|
413
|
+
},
|
|
414
|
+
|
|
408
415
|
// For debugging
|
|
409
416
|
getAll() {
|
|
410
417
|
return dynamic;
|
|
@@ -426,7 +433,7 @@ export const createExtensionManager = (context) => {
|
|
|
426
433
|
* Return the UI configuration for the given type and location
|
|
427
434
|
*/
|
|
428
435
|
getUIConfig(type, uiArea) {
|
|
429
|
-
return uiConfig[type][uiArea] || [];
|
|
436
|
+
return uiConfig[type]?.[uiArea] || [];
|
|
430
437
|
},
|
|
431
438
|
|
|
432
439
|
/**
|
|
@@ -498,7 +505,11 @@ export const createExtensionManager = (context) => {
|
|
|
498
505
|
loadPlugins = Object.values(plugins);
|
|
499
506
|
}
|
|
500
507
|
|
|
501
|
-
|
|
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) => {
|
|
502
513
|
if (plugin.products) {
|
|
503
514
|
plugin.products.forEach(async(p) => {
|
|
504
515
|
const impl = await p;
|
|
@@ -508,6 +519,26 @@ export const createExtensionManager = (context) => {
|
|
|
508
519
|
}
|
|
509
520
|
});
|
|
510
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
|
+
}
|
|
511
542
|
});
|
|
512
543
|
},
|
|
513
544
|
};
|
package/core/plugin-helpers.ts
CHANGED
|
@@ -137,6 +137,30 @@ function checkExtensionRouteBinding($route: any, locationConfig: any, context: a
|
|
|
137
137
|
return res;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
// Track which ExtensionPoint keys are missing from the extension manager's uiConfig.
|
|
141
|
+
// This handles forwards-compatibility when extensions ship a newer shell that defines
|
|
142
|
+
// ExtensionPoint values the running dashboard doesn't know about (e.g. 'Table' on 2.13).
|
|
143
|
+
const _uiConfigPatched: { [point: string]: boolean } = {};
|
|
144
|
+
|
|
145
|
+
function ensureUIConfigCompat(extensionManager: any) {
|
|
146
|
+
const uiConfig = extensionManager.getAllUIConfig?.();
|
|
147
|
+
|
|
148
|
+
if (uiConfig) {
|
|
149
|
+
const missingPoints: { [point: string]: boolean } = {};
|
|
150
|
+
|
|
151
|
+
Object.values(ExtensionPoint).forEach((ep) => {
|
|
152
|
+
if (!uiConfig[ep] && !_uiConfigPatched[ep]) {
|
|
153
|
+
missingPoints[ep] = true;
|
|
154
|
+
_uiConfigPatched[ep] = true;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (Object.keys(missingPoints).length) {
|
|
159
|
+
console.warn(`[plugin-helpers] These ExtensionPoints aren't available for usage in this Rancher version: ${ Object.keys(missingPoints).join(', ') }`); // eslint-disable-line no-console
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
140
164
|
export function getApplicableExtensionEnhancements<T>(
|
|
141
165
|
pluginCtx: ComponentOptionsMixin,
|
|
142
166
|
actionType: ExtensionPoint,
|
|
@@ -148,6 +172,13 @@ export function getApplicableExtensionEnhancements<T>(
|
|
|
148
172
|
|
|
149
173
|
// gate it so that we prevent errors on older versions of dashboard
|
|
150
174
|
if (pluginCtx.$extension?.getUIConfig) {
|
|
175
|
+
ensureUIConfigCompat(pluginCtx.$extension);
|
|
176
|
+
|
|
177
|
+
// Exit early if actionType doesn't exist in the extension manager's uiConfig
|
|
178
|
+
if (_uiConfigPatched[actionType]) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
|
|
151
182
|
const actions = pluginCtx.$extension.getUIConfig(actionType, uiArea);
|
|
152
183
|
|
|
153
184
|
actions.forEach((action: any, i: number) => {
|
|
@@ -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
|
+
}
|