@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.
- package/assets/images/providers/entraid-black.svg +4 -0
- package/assets/images/providers/entraid.svg +9 -0
- package/assets/images/vendor/entraid.svg +9 -0
- package/assets/styles/app.scss +0 -1
- 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 +24 -21
- package/assets/translations/zh-hans.yaml +4 -11
- package/chart/__tests__/S3.test.ts +10 -3
- package/components/CountBox.vue +20 -0
- package/components/CreateDriver.vue +0 -12
- package/components/DetailText.vue +12 -3
- package/components/EmptyProductPage.vue +76 -0
- 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/SelectIconGrid.vue +5 -0
- package/components/SideNav.vue +13 -0
- package/components/__tests__/CountBox.test.ts +72 -0
- package/components/__tests__/DetailText.test.ts +113 -0
- package/components/__tests__/PromptModal.test.ts +2 -0
- package/components/fleet/FleetClusterTargets/index.vue +18 -1
- package/components/fleet/FleetClusters.vue +1 -0
- package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
- package/components/form/InputWithSelect.vue +18 -10
- package/components/form/KeyValue.vue +17 -1
- package/components/form/LabeledSelect.vue +82 -24
- package/components/form/NodeScheduling.vue +17 -3
- package/components/form/PrivateRegistry.vue +69 -0
- package/components/form/Select.vue +73 -56
- package/components/form/ServiceNameSelect.vue +13 -11
- package/components/form/__tests__/KeyValue.test.ts +66 -0
- package/components/form/__tests__/NodeScheduling.test.ts +9 -0
- package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
- package/components/form/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
- package/components/formatter/WorkloadHealthScale.vue +3 -1
- package/components/nav/Group.vue +33 -9
- package/components/nav/Header.vue +56 -10
- package/components/nav/NotificationCenter/Notification.vue +4 -1
- package/components/nav/NotificationCenter/NotificationHeader.vue +20 -8
- package/components/nav/NotificationCenter/__tests__/NotificationHeader.test.ts +80 -0
- package/components/nav/TopLevelMenu.vue +15 -1
- package/components/nav/Type.vue +8 -7
- package/components/nav/WindowManager/index.vue +2 -1
- package/components/nav/WorkspaceSwitcher.vue +13 -0
- package/components/nav/__tests__/Group.test.ts +67 -0
- package/components/nav/__tests__/Header.test.ts +235 -0
- package/components/nav/__tests__/Type.test.ts +20 -3
- package/components/templates/default.vue +34 -4
- package/components/templates/home.vue +12 -25
- package/components/templates/plain.vue +13 -26
- package/composables/useLabeledFormElement.ts +10 -2
- package/composables/useLabeledSelect.ts +60 -0
- package/composables/useUserRetentionValidation.ts +1 -49
- package/config/cookies.js +0 -1
- package/config/labels-annotations.js +1 -0
- package/config/pagination-table-headers.js +8 -1
- package/config/product/apps.js +2 -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/config/query-params.js +1 -0
- package/config/router/routes.js +0 -8
- package/core/__tests__/plugin-products-helpers.test.ts +454 -0
- package/core/__tests__/plugin-products.test.ts +3810 -0
- package/core/extension-manager-impl.js +30 -1
- package/core/plugin-products-base.ts +392 -0
- package/core/plugin-products-extending.ts +44 -0
- package/core/plugin-products-helpers.ts +263 -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 +237 -0
- package/core/plugin.ts +45 -10
- package/core/productDebugger.js +48 -0
- package/core/types.ts +97 -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__/management.cattle.io.fleetworkspace.test.ts +128 -0
- package/detail/fleet.cattle.io.bundle.vue +21 -34
- package/detail/management.cattle.io.fleetworkspace.vue +49 -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/__tests__/fleet.cattle.io.helmop.test.ts +9 -0
- package/edit/__tests__/kontainerDriver.test.ts +0 -13
- package/edit/__tests__/nodeDriver.test.ts +5 -11
- package/edit/__tests__/resources.cattle.io.restore.test.ts +9 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
- package/edit/auth/__tests__/oidc.test.ts +54 -0
- package/edit/auth/azuread.vue +1 -1
- package/edit/auth/oidc.vue +8 -0
- package/edit/kontainerDriver.vue +1 -2
- package/edit/nodeDriver.vue +0 -2
- package/edit/provisioning.cattle.io.cluster/AgentEnv.vue +1 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/AgentEnv.test.ts +25 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +70 -99
- package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
- package/initialize/App.vue +29 -2
- package/initialize/install-plugins.js +0 -2
- package/list/__tests__/management.cattle.io.feature.test.ts +105 -0
- package/list/catalog.cattle.io.app.vue +25 -5
- package/list/management.cattle.io.feature.vue +1 -1
- package/list/management.cattle.io.fleetworkspace.vue +8 -0
- package/list/provisioning.cattle.io.cluster.vue +0 -1
- package/list/workload.vue +11 -4
- package/machine-config/amazonec2.vue +1 -0
- package/mixins/chart.js +40 -9
- package/mixins/resource-fetch.js +12 -3
- package/models/__tests__/catalog.cattle.io.app.test.ts +15 -1
- package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +84 -0
- package/models/__tests__/chart.test.ts +99 -6
- package/models/__tests__/management.cattle.io.feature.test.ts +131 -0
- package/models/__tests__/monitoring.coreos.com.alertmanagerconfig.test.ts +98 -0
- package/models/catalog.cattle.io.app.js +21 -17
- package/models/catalog.cattle.io.clusterrepo.js +39 -11
- package/models/chart.js +33 -19
- package/models/fleet-application.js +1 -1
- package/models/fleet.cattle.io.bundle.js +1 -1
- package/models/kontainerdriver.js +11 -0
- package/models/management.cattle.io.authconfig.js +5 -1
- package/models/management.cattle.io.cluster.js +0 -53
- package/models/management.cattle.io.feature.js +3 -3
- package/models/management.cattle.io.kontainerdriver.js +1 -26
- package/models/monitoring.coreos.com.alertmanagerconfig.js +31 -17
- package/models/nodedriver.js +7 -0
- package/models/pod.js +18 -0
- package/models/workload.js +20 -2
- package/package.json +13 -13
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
- package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +189 -0
- package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +55 -0
- package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +53 -0
- package/pages/c/_cluster/apps/charts/chart.vue +217 -33
- package/pages/c/_cluster/apps/charts/index.vue +2 -2
- package/pages/c/_cluster/apps/charts/install.vue +8 -3
- package/pages/c/_cluster/auth/user.retention/index.vue +55 -22
- package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -7
- package/pages/c/_cluster/settings/brand.vue +4 -4
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +39 -2
- package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +61 -0
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +246 -23
- package/pages/c/_cluster/uiplugins/index.vue +166 -62
- 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 +7 -0
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +205 -1
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +82 -4
- package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +1 -1
- package/scripts/test-plugins-build.sh +5 -2
- package/scripts/typegen.sh +13 -1
- package/server/server-middleware.js +2 -2
- package/static/humans.txt +1 -0
- package/static/robots.txt +34 -0
- package/static/welcome-cow.svg +18 -0
- package/store/__tests__/catalog.test.ts +161 -11
- package/store/__tests__/type-map.test.ts +84 -24
- package/store/auth.js +0 -3
- package/store/catalog.js +60 -8
- 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 +8539 -2938
- package/types/store/dashboard-store.types.ts +5 -0
- package/types/store/pagination.types.ts +6 -0
- package/utils/__tests__/git.test.ts +270 -0
- package/utils/__tests__/inactivity.test.ts +316 -0
- package/utils/__tests__/object.test.ts +77 -0
- package/utils/__tests__/time.test.ts +14 -1
- package/utils/__tests__/url.test.ts +246 -0
- package/utils/axios.js +1 -4
- package/utils/dynamic-importer.js +3 -2
- package/utils/object.js +33 -2
- package/utils/pagination-utils.ts +1 -1
- package/utils/time.ts +5 -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 +0 -9
- package/assets/images/providers/azuread-black.svg +0 -22
- package/assets/images/providers/azuread.svg +0 -25
- package/assets/images/vendor/azuread.svg +0 -18
- package/assets/styles/fonts/_dots.scss +0 -18
- package/components/EmberPage.vue +0 -622
- package/components/EmberPageView.vue +0 -39
- package/components/form/labeled-select-utils/labeled-select-pagination.ts +0 -116
- package/mixins/labeled-form-element.ts +0 -225
- package/pages/c/_cluster/explorer/tools/pages/_page.vue +0 -28
- package/pages/c/_cluster/manager/pages/_page.vue +0 -22
- package/pages/c/_cluster/mcapps/pages/_page.vue +0 -22
- package/plugins/ember-cookie.js +0 -17
- 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
|
-
|
|
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
|
+
}
|