@rancher/shell 3.0.12-rc.1 → 3.0.12-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apis/impl/apis.ts +6 -0
- package/apis/index.ts +26 -0
- package/apis/intf/resources-api/cluster-api.ts +18 -0
- package/apis/intf/resources-api/mgmt-api.ts +15 -0
- package/apis/intf/resources-api/resource-base.ts +107 -0
- package/apis/intf/resources-api/resource-constants.ts +147 -0
- package/apis/intf/resources-api/resources-api.ts +143 -0
- package/apis/intf/resources.ts +49 -0
- package/apis/intf/{modal.ts → shell-api/modal.ts} +21 -26
- package/apis/intf/shell-api/proxy.ts +216 -0
- package/apis/intf/{slide-in.ts → shell-api/slide-in.ts} +4 -3
- package/apis/intf/{system.ts → shell-api/system.ts} +4 -1
- package/apis/intf/shell.ts +12 -6
- package/apis/resources/__tests__/resources-api-class.test.ts +550 -0
- package/apis/resources/index.ts +22 -0
- package/apis/resources/resources-api-class.ts +187 -0
- package/apis/shell/__tests__/proxy.test.ts +369 -0
- package/apis/shell/index.ts +8 -1
- package/apis/shell/modal.ts +4 -1
- package/apis/shell/notifications.ts +9 -6
- package/apis/shell/proxy.ts +256 -0
- package/apis/shell/slide-in.ts +4 -1
- package/apis/vue-shim.d.ts +2 -1
- package/assets/data/aws-regions.json +4 -0
- package/assets/fonts/lato/LatoLatin-Black.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Black.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-BlackItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-BlackItalic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Bold.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Bold.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-BoldItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-BoldItalic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Heavy.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Heavy.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-HeavyItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-HeavyItalic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Italic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Italic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Light.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Light.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-LightItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-LightItalic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Medium.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Medium.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-MediumItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-MediumItalic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Regular.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Regular.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Semibold.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Semibold.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-SemiboldItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-SemiboldItalic.woff2 +0 -0
- 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/_variables.scss +2 -0
- package/assets/styles/fonts/_fontstack.scss +132 -8
- package/assets/translations/en-us.yaml +41 -22
- package/assets/translations/zh-hans.yaml +4 -8
- package/chart/__tests__/S3.test.ts +10 -3
- package/chart/monitoring/index.vue +10 -1
- package/components/ActionDropdownShell.vue +2 -1
- package/components/CountBox.vue +20 -0
- package/components/CreateDriver.vue +0 -12
- package/components/CruResourceFooter.vue +9 -5
- package/components/DetailText.vue +12 -3
- package/components/ExplorerProjectsNamespaces.vue +1 -1
- package/components/InstallHelmCharts.vue +2 -2
- package/components/LandingPagePreference.vue +14 -5
- package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +15 -1
- package/components/Resource/Detail/Metadata/index.vue +6 -0
- package/components/Resource/Detail/ResourcePopover/index.vue +12 -1
- package/components/Resource/Detail/SpacedRow.vue +3 -1
- package/components/Resource/Detail/TitleBar/index.vue +10 -11
- package/components/ResourceList/Masthead.vue +12 -8
- package/components/SelectIconGrid.vue +5 -10
- package/components/SingleClusterInfo.vue +1 -0
- package/components/SortableTable/__tests__/sorting.test.ts +126 -0
- package/components/SortableTable/index.vue +6 -9
- package/components/SortableTable/selection.js +23 -5
- package/components/SortableTable/sorting.js +6 -3
- package/components/Wizard.vue +14 -13
- package/components/__tests__/CountBox.test.ts +72 -0
- package/components/__tests__/DetailText.test.ts +113 -0
- package/components/fleet/FleetBundles.vue +100 -12
- package/components/fleet/FleetClusterTargets/index.vue +54 -15
- package/components/fleet/__tests__/FleetClusterTargets.test.ts +149 -115
- package/components/fleet/__tests__/FleetClusters.test.ts +12 -12
- package/components/form/InputWithSelect.vue +18 -10
- package/components/form/KeyValue.vue +17 -1
- package/components/form/LabeledSelect.vue +101 -26
- package/components/form/NameNsDescription.vue +11 -0
- package/components/form/Security.vue +6 -2
- package/components/form/Select.vue +73 -56
- package/components/form/ServiceNameSelect.vue +13 -11
- package/components/form/WorkloadPorts.vue +2 -7
- package/components/form/__tests__/KeyValue.test.ts +66 -0
- package/components/form/__tests__/NodeScheduling.test.ts +9 -0
- package/components/form/__tests__/Security.test.ts +76 -0
- package/components/form/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
- package/components/formatter/Autoscaler.vue +4 -4
- package/components/formatter/ClusterKubeVersion.vue +27 -0
- package/components/formatter/ClusterLink.vue +1 -7
- package/components/formatter/ClusterProvider.vue +6 -10
- package/components/formatter/FleetSummaryGraph.vue +0 -3
- package/components/formatter/MachineSummaryGraph.vue +1 -1
- package/components/formatter/PodsUsage.vue +2 -2
- package/components/formatter/__tests__/Autoscaler.test.ts +19 -22
- package/components/formatter/__tests__/FleetSummaryGraph.test.ts +216 -0
- package/components/formatter/__tests__/PodsUsage.test.ts +6 -10
- package/components/nav/Group.vue +7 -6
- package/components/nav/Header.vue +24 -3
- package/components/nav/NamespaceFilter.vue +2 -2
- 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.helper.ts +15 -3
- package/components/nav/TopLevelMenu.vue +16 -5
- 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__/TopLevelMenu.test.ts +145 -21
- package/components/nav/__tests__/Type.test.ts +20 -3
- package/components/templates/default.vue +34 -4
- package/components/templates/home.vue +30 -25
- package/components/templates/plain.vue +31 -26
- package/components/templates/standalone.vue +17 -0
- package/composables/useFormValidation.ts +93 -0
- package/composables/useLabeledFormElement.ts +10 -2
- package/composables/useLabeledSelect.ts +60 -0
- package/composables/useUserRetentionValidation.ts +1 -49
- package/composables/useVeeValidateField.test.ts +159 -0
- package/composables/useVeeValidateField.ts +67 -0
- package/config/cookies.js +0 -1
- package/config/labels-annotations.js +1 -0
- package/config/pagination-table-headers.js +18 -1
- package/config/product/manager.js +82 -21
- package/config/query-params.js +1 -0
- package/config/router/routes.js +6 -8
- package/config/table-headers.js +20 -1
- package/config/types.js +2 -1
- package/core/__tests__/plugin-products.test.ts +1505 -30
- package/core/plugin-products-base.ts +137 -20
- package/core/plugin-products-helpers.ts +5 -4
- package/core/plugin-products.ts +4 -0
- package/core/plugin-types.ts +129 -4
- package/core/plugin.ts +15 -7
- package/core/productDebugger.js +9 -4
- package/core/types-provisioning.ts +43 -30
- package/core/types.ts +58 -19
- package/detail/__tests__/management.cattle.io.fleetworkspace.test.ts +128 -0
- package/detail/__tests__/pod.test.ts +41 -0
- package/detail/harvesterhci.io.management.cluster.vue +6 -2
- package/detail/management.cattle.io.fleetworkspace.vue +49 -0
- package/detail/pod.vue +1 -1
- package/detail/provisioning.cattle.io.cluster.vue +4 -10
- 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__/azuread.test.ts +217 -34
- package/edit/auth/__tests__/oidc.test.ts +54 -0
- package/edit/auth/azuread.vue +123 -15
- package/edit/auth/oidc.vue +10 -2
- package/edit/kontainerDriver.vue +1 -2
- package/edit/networking.k8s.io.ingress/DefaultBackend.vue +13 -4
- package/edit/networking.k8s.io.ingress/RulePath.vue +8 -4
- package/edit/networking.k8s.io.ingress/index.vue +75 -20
- 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__/MachinePool.test.ts +104 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +81 -106
- package/edit/provisioning.cattle.io.cluster/rke2.vue +8 -4
- package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
- package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +37 -4
- package/edit/provisioning.cattle.io.cluster/tabs/registries/__tests__/RegistryConfigs.test.ts +132 -7
- package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +2 -1
- package/edit/secret/__tests__/ssh.test.ts +5 -6
- package/edit/secret/basic.vue +31 -0
- package/edit/secret/index.vue +68 -17
- package/edit/secret/registry.vue +38 -0
- package/edit/secret/ssh.vue +29 -0
- package/edit/secret/tls.vue +30 -0
- package/edit/service.vue +4 -4
- package/edit/workload/Upgrading.vue +3 -3
- package/edit/workload/__tests__/Upgrading.test.ts +6 -9
- package/edit/workload/mixins/workload.js +2 -1
- 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/fleet.cattle.io.bundle.vue +7 -104
- package/list/fleet.cattle.io.clusterregistrationtoken.vue +20 -0
- 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 +262 -180
- package/list/utils/management.cattle.io.cluster.utils.ts +128 -0
- package/machine-config/amazonec2.vue +1 -0
- package/mixins/__tests__/chart.test.ts +112 -0
- package/mixins/brand.js +2 -1
- package/mixins/chart.js +50 -15
- package/mixins/resource-fetch-api-pagination.js +41 -5
- 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__/ext.cattle.io.kubeconfig.test.ts +67 -67
- package/models/__tests__/management.cattle.io.cluster.test.ts +1 -1
- package/models/__tests__/management.cattle.io.feature.test.ts +131 -0
- package/models/__tests__/management.cattle.io.node.ts +6 -5
- package/models/__tests__/management.cattle.io.nodepool.ts +5 -4
- package/models/__tests__/monitoring.coreos.com.alertmanagerconfig.test.ts +98 -0
- package/models/__tests__/provisioning.cattle.io.cluster.test.ts +32 -11
- package/models/base-cluster.x-k8s.io.js +26 -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/cluster.js +1 -1
- package/models/cluster.x-k8s.io.machine.js +4 -22
- package/models/cluster.x-k8s.io.machinedeployment.js +2 -20
- package/models/cluster.x-k8s.io.machineset.js +2 -20
- package/models/compliance.cattle.io.clusterscan.js +130 -2
- package/models/ext.cattle.io.kubeconfig.ts +4 -7
- package/models/fleet-application.js +4 -2
- 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 +402 -78
- package/models/management.cattle.io.feature.js +3 -3
- package/models/management.cattle.io.kontainerdriver.js +1 -26
- package/models/management.cattle.io.node.js +6 -4
- package/models/management.cattle.io.nodepool.js +1 -1
- package/models/monitoring.coreos.com.alertmanagerconfig.js +31 -17
- package/models/networking.k8s.io.ingress.js +12 -4
- package/models/nodedriver.js +7 -0
- package/models/provisioning.cattle.io.cluster.js +47 -330
- package/models/rke.cattle.io.etcdsnapshot.js +1 -2
- package/package.json +20 -37
- package/pages/__tests__/readme.test.ts +49 -0
- package/pages/auth/setup.vue +2 -3
- package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +265 -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 +275 -39
- package/pages/c/_cluster/apps/charts/index.vue +2 -2
- package/pages/c/_cluster/apps/charts/install.vue +18 -10
- package/pages/c/_cluster/auth/user.retention/index.vue +55 -22
- package/pages/c/_cluster/explorer/__tests__/index.test.ts +23 -25
- package/pages/c/_cluster/explorer/index.vue +5 -49
- package/pages/c/_cluster/istio/__tests__/istio.index.test.ts +194 -0
- package/pages/c/_cluster/istio/index.vue +21 -6
- package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -7
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +40 -2
- package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +61 -0
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +735 -13
- package/pages/c/_cluster/uiplugins/index.vue +226 -222
- package/pages/diagnostic.vue +13 -17
- package/pages/fail-whale.vue +18 -0
- package/pages/home.vue +77 -260
- package/pages/readme.vue +88 -0
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +88 -0
- package/plugins/dashboard-store/actions.js +40 -18
- package/plugins/dashboard-store/resource-class.js +5 -2
- package/plugins/steve/__tests__/subscribe.spec.ts +6 -3
- package/plugins/steve/steve-pagination-utils.ts +11 -3
- package/plugins/steve/subscribe.js +35 -5
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +211 -1
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +37 -4
- package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +1 -1
- package/rancher-components/RcButton/RcButton.test.ts +37 -1
- package/rancher-components/RcButton/RcButton.vue +38 -8
- package/rancher-components/RcDropdown/RcDropdownTrigger.vue +10 -8
- package/scripts/test-plugins-build.sh +5 -2
- 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 +276 -12
- package/store/__tests__/type-map.test.ts +556 -1
- package/store/action-menu.js +8 -3
- package/store/auth.js +1 -4
- package/store/aws.js +27 -16
- package/store/catalog.js +87 -11
- package/store/digitalocean.js +20 -38
- package/store/index.js +2 -0
- package/store/linode.js +25 -40
- package/store/pnap.js +1 -0
- package/store/type-map.js +111 -29
- package/tsconfig.paths.json +8 -8
- package/types/kube/kube-api.ts +14 -1
- package/types/rancher/steve.api.ts +12 -12
- package/types/resources/settings.d.ts +2 -1
- package/types/shell/index.d.ts +128 -24
- package/types/store/dashboard-store.types.ts +108 -11
- package/types/store/pagination.types.ts +6 -3
- package/utils/__tests__/alertmanagerconfig.test.ts +117 -0
- package/utils/__tests__/async.test.ts +87 -0
- package/utils/__tests__/aws.test.ts +140 -0
- package/utils/__tests__/banners.test.ts +176 -0
- package/utils/__tests__/chart.test.ts +64 -1
- package/utils/__tests__/color.test.ts +226 -0
- package/utils/__tests__/duration.test.ts +140 -0
- package/utils/__tests__/fleet.test.ts +340 -0
- package/utils/__tests__/git.test.ts +270 -0
- package/utils/__tests__/inactivity.test.ts +316 -0
- package/utils/__tests__/ingress.test.ts +553 -0
- package/utils/__tests__/kube.test.ts +68 -0
- package/utils/__tests__/namespace-filter.test.ts +109 -0
- package/utils/__tests__/object.test.ts +77 -0
- package/utils/__tests__/pagination-utils.test.ts +361 -0
- package/utils/__tests__/parse-externalid.test.ts +137 -0
- package/utils/__tests__/perf-setting.utils.test.ts +98 -0
- package/utils/__tests__/poller-sequential.test.ts +177 -0
- package/utils/__tests__/poller.test.ts +170 -0
- package/utils/__tests__/promise.test.ts +346 -0
- package/utils/__tests__/settings.test.ts +140 -0
- package/utils/__tests__/sort-utils.test.ts +301 -0
- package/utils/__tests__/string-utils.test.ts +798 -0
- package/utils/__tests__/string.test.ts +23 -1
- package/utils/__tests__/style.test.ts +154 -0
- package/utils/__tests__/svg-filter.test.ts +184 -0
- package/utils/__tests__/time.test.ts +14 -1
- package/utils/__tests__/units.test.ts +417 -0
- package/utils/__tests__/url.test.ts +246 -0
- package/utils/__tests__/versions.test.ts +128 -0
- package/utils/__tests__/xccdf.test.ts +391 -0
- package/utils/chart.js +36 -0
- package/utils/fleet.ts +13 -3
- package/utils/gatekeeper/__tests__/util.test.ts +174 -0
- package/utils/gc/__tests__/gc-interval.test.ts +119 -0
- package/utils/gc/__tests__/gc-root-store.test.ts +225 -0
- package/utils/gc/__tests__/gc-route-changed.test.ts +96 -0
- package/utils/gc/__tests__/gc.test.ts +487 -0
- package/utils/ingress.ts +9 -1
- package/utils/object.js +33 -2
- package/utils/pagination-utils.ts +2 -1
- package/utils/string.js +25 -2
- package/utils/time.ts +5 -0
- package/utils/uiplugins.ts +5 -5
- package/utils/validators/__tests__/cluster-name.test.ts +110 -0
- package/utils/validators/__tests__/cron-schedule.test.ts +79 -0
- package/utils/validators/__tests__/index.test.ts +481 -0
- package/utils/validators/__tests__/kubernetes-name.test.ts +163 -0
- package/utils/validators/__tests__/misc-validators.test.ts +246 -0
- package/utils/validators/__tests__/pod-affinity.test.ts +382 -0
- package/utils/validators/__tests__/prometheusrule.test.ts +211 -0
- package/utils/validators/__tests__/role-template.test.ts +149 -0
- package/utils/validators/__tests__/service.test.ts +283 -0
- package/utils/validators/__tests__/setting.test.js +32 -0
- package/utils/validators/formRules/__tests__/index.test.ts +50 -0
- package/utils/validators/formRules/index.ts +5 -5
- package/utils/validators/machine-pool.ts +1 -1
- package/utils/validators/setting.js +18 -3
- package/utils/xccdf.ts +418 -0
- package/vue.config.js +0 -9
- package/assets/fonts/lato/lato-v17-latin-700.woff +0 -0
- package/assets/fonts/lato/lato-v17-latin-700.woff2 +0 -0
- package/assets/fonts/lato/lato-v17-latin-regular.woff +0 -0
- package/assets/fonts/lato/lato-v17-latin-regular.woff2 +0 -0
- 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
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { PluginProduct } from '@shell/core/plugin-products';
|
|
2
|
+
import { Plugin } from '@shell/core/plugin';
|
|
2
3
|
import {
|
|
3
4
|
ProductMetadata, ProductSinglePage, ProductChildPage,
|
|
4
|
-
ProductChildGroup,
|
|
5
|
+
ProductChildGroup, ProductChildCustomPage, ProductChildResourcePage,
|
|
6
|
+
ProductChild, StandardProductNames
|
|
5
7
|
} from '@shell/core/plugin-types';
|
|
6
8
|
import { IExtension } from '@shell/core/types';
|
|
7
9
|
|
|
@@ -56,9 +58,10 @@ jest.mock('@shell/core/productDebugger', () => ({
|
|
|
56
58
|
// Create mock factories
|
|
57
59
|
function createMockPlugin(): IExtension {
|
|
58
60
|
return {
|
|
59
|
-
_registerTopLevelProduct:
|
|
60
|
-
addRoute:
|
|
61
|
-
|
|
61
|
+
_registerTopLevelProduct: jest.fn(),
|
|
62
|
+
addRoute: jest.fn(),
|
|
63
|
+
enableServerSidePagination: jest.fn(),
|
|
64
|
+
DSL: jest.fn((store, productName) => ({
|
|
62
65
|
basicType: jest.fn(),
|
|
63
66
|
labelGroup: jest.fn(),
|
|
64
67
|
setGroupDefaultType: jest.fn(),
|
|
@@ -67,12 +70,24 @@ function createMockPlugin(): IExtension {
|
|
|
67
70
|
configureType: jest.fn(),
|
|
68
71
|
weightType: jest.fn(),
|
|
69
72
|
product: jest.fn(),
|
|
73
|
+
headers: jest.fn(),
|
|
74
|
+
hideBulkActions: jest.fn(),
|
|
75
|
+
mapGroup: jest.fn(),
|
|
76
|
+
ignoreGroup: jest.fn(),
|
|
77
|
+
mapType: jest.fn(),
|
|
78
|
+
ignoreType: jest.fn(),
|
|
79
|
+
moveType: jest.fn(),
|
|
70
80
|
})),
|
|
71
81
|
} as any;
|
|
72
82
|
}
|
|
73
83
|
|
|
74
|
-
function createMockStore(extendableProducts: string[] = Object.values(StandardProductNames)): any {
|
|
75
|
-
return {
|
|
84
|
+
function createMockStore(extendableProducts: string[] = Object.values(StandardProductNames), managementSchemas: string[] = []): any {
|
|
85
|
+
return {
|
|
86
|
+
getters: {
|
|
87
|
+
'type-map/productByName': (productName: string) => (extendableProducts.includes(productName) ? { name: productName, extendable: true } : undefined),
|
|
88
|
+
'management/schemaFor': (type: string) => (managementSchemas.includes(type) ? { id: type } : undefined),
|
|
89
|
+
}
|
|
90
|
+
};
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
describe('pluginProduct', () => {
|
|
@@ -891,9 +906,9 @@ describe('pluginProduct', () => {
|
|
|
891
906
|
pluginProduct.apply(mockPlugin, mockStore);
|
|
892
907
|
|
|
893
908
|
// Verify default route points to the group's component page (not first child)
|
|
894
|
-
// When a group has a component,
|
|
909
|
+
// When a group has a component, the route includes the group's name for proper side-menu highlighting
|
|
895
910
|
expect(mockDSL.product).toHaveBeenCalledWith(
|
|
896
|
-
expect.objectContaining({ to: expect.objectContaining({ name: 'groupwithpage-
|
|
911
|
+
expect.objectContaining({ to: expect.objectContaining({ name: 'groupwithpage-settings' }) })
|
|
897
912
|
);
|
|
898
913
|
|
|
899
914
|
// Verify virtualType was still created for the group component
|
|
@@ -965,28 +980,6 @@ describe('pluginProduct', () => {
|
|
|
965
980
|
new PluginProduct(mockPlugin, productMetadata, badConfig);
|
|
966
981
|
}).toThrow('forEach');
|
|
967
982
|
});
|
|
968
|
-
|
|
969
|
-
it('should throw when extending standard product and group parent has component', () => {
|
|
970
|
-
const mockPlugin = createMockPlugin();
|
|
971
|
-
const config: ProductChildGroup[] = [
|
|
972
|
-
{
|
|
973
|
-
name: 'parent-group',
|
|
974
|
-
label: 'Parent Group',
|
|
975
|
-
component: { name: 'GroupComponent' },
|
|
976
|
-
children: [
|
|
977
|
-
{
|
|
978
|
-
name: 'child',
|
|
979
|
-
label: 'Child',
|
|
980
|
-
component: { name: 'ChildComponent' },
|
|
981
|
-
},
|
|
982
|
-
],
|
|
983
|
-
},
|
|
984
|
-
];
|
|
985
|
-
|
|
986
|
-
expect(() => {
|
|
987
|
-
new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, config);
|
|
988
|
-
}).toThrow('When extending an existing product, group parent items cannot have a component because of route matching conflicts.');
|
|
989
|
-
});
|
|
990
983
|
});
|
|
991
984
|
|
|
992
985
|
describe('state verification', () => {
|
|
@@ -3153,6 +3146,13 @@ describe('pluginProduct', () => {
|
|
|
3153
3146
|
virtualType: jest.fn(),
|
|
3154
3147
|
configureType: jest.fn(),
|
|
3155
3148
|
weightType: jest.fn(),
|
|
3149
|
+
headers: jest.fn(),
|
|
3150
|
+
hideBulkActions: jest.fn(),
|
|
3151
|
+
|
|
3152
|
+
mapGroup: jest.fn(),
|
|
3153
|
+
ignoreGroup: jest.fn(),
|
|
3154
|
+
mapType: jest.fn(),
|
|
3155
|
+
ignoreType: jest.fn(),
|
|
3156
3156
|
};
|
|
3157
3157
|
|
|
3158
3158
|
jest.spyOn(mockPlugin, 'DSL').mockReturnValue(mockDSL);
|
|
@@ -3181,6 +3181,13 @@ describe('pluginProduct', () => {
|
|
|
3181
3181
|
virtualType: jest.fn(),
|
|
3182
3182
|
configureType: jest.fn(),
|
|
3183
3183
|
weightType: jest.fn(),
|
|
3184
|
+
headers: jest.fn(),
|
|
3185
|
+
hideBulkActions: jest.fn(),
|
|
3186
|
+
|
|
3187
|
+
mapGroup: jest.fn(),
|
|
3188
|
+
ignoreGroup: jest.fn(),
|
|
3189
|
+
mapType: jest.fn(),
|
|
3190
|
+
ignoreType: jest.fn(),
|
|
3184
3191
|
};
|
|
3185
3192
|
|
|
3186
3193
|
jest.spyOn(mockPlugin, 'DSL').mockReturnValue(mockDSL);
|
|
@@ -3205,6 +3212,13 @@ describe('pluginProduct', () => {
|
|
|
3205
3212
|
virtualType: jest.fn(),
|
|
3206
3213
|
configureType: jest.fn(),
|
|
3207
3214
|
weightType: jest.fn(),
|
|
3215
|
+
headers: jest.fn(),
|
|
3216
|
+
hideBulkActions: jest.fn(),
|
|
3217
|
+
|
|
3218
|
+
mapGroup: jest.fn(),
|
|
3219
|
+
ignoreGroup: jest.fn(),
|
|
3220
|
+
mapType: jest.fn(),
|
|
3221
|
+
ignoreType: jest.fn(),
|
|
3208
3222
|
};
|
|
3209
3223
|
|
|
3210
3224
|
jest.spyOn(mockPlugin, 'DSL').mockReturnValue(mockDSL);
|
|
@@ -3216,4 +3230,1465 @@ describe('pluginProduct', () => {
|
|
|
3216
3230
|
expect(productCalls[0][0]).toStrictEqual(expect.objectContaining({ name: 'myproduct' }));
|
|
3217
3231
|
});
|
|
3218
3232
|
});
|
|
3233
|
+
|
|
3234
|
+
describe('documentation examples', () => {
|
|
3235
|
+
describe('quick start: string convenience method', () => {
|
|
3236
|
+
it('should create a new product from just a string name', () => {
|
|
3237
|
+
const mockPlugin = createMockPlugin();
|
|
3238
|
+
const pluginProduct = PluginProduct.fromName(mockPlugin, 'my-first-product');
|
|
3239
|
+
|
|
3240
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3241
|
+
expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
|
|
3242
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
|
|
3243
|
+
});
|
|
3244
|
+
});
|
|
3245
|
+
|
|
3246
|
+
describe('single page product', () => {
|
|
3247
|
+
it('should create a product with a single page component and no config', () => {
|
|
3248
|
+
const mockPlugin = createMockPlugin();
|
|
3249
|
+
const product: ProductSinglePage = {
|
|
3250
|
+
name: 'my-dashboard',
|
|
3251
|
+
label: 'My Dashboard',
|
|
3252
|
+
icon: 'globe',
|
|
3253
|
+
component: { name: 'DashboardPage' },
|
|
3254
|
+
};
|
|
3255
|
+
|
|
3256
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, []);
|
|
3257
|
+
|
|
3258
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3259
|
+
expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
|
|
3260
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
|
|
3261
|
+
});
|
|
3262
|
+
});
|
|
3263
|
+
|
|
3264
|
+
describe('product with custom pages', () => {
|
|
3265
|
+
it('should register routes for each custom page', () => {
|
|
3266
|
+
const mockPlugin = createMockPlugin();
|
|
3267
|
+
|
|
3268
|
+
const overviewPage: ProductChildCustomPage = {
|
|
3269
|
+
name: 'overview',
|
|
3270
|
+
label: 'Overview',
|
|
3271
|
+
component: { name: 'OverviewPage' },
|
|
3272
|
+
weight: 2,
|
|
3273
|
+
};
|
|
3274
|
+
|
|
3275
|
+
const settingsPage: ProductChildCustomPage = {
|
|
3276
|
+
name: 'settings',
|
|
3277
|
+
label: 'Settings',
|
|
3278
|
+
component: { name: 'SettingsPage' },
|
|
3279
|
+
weight: 1,
|
|
3280
|
+
};
|
|
3281
|
+
|
|
3282
|
+
const product: ProductMetadata = {
|
|
3283
|
+
name: 'my-app',
|
|
3284
|
+
label: 'My App',
|
|
3285
|
+
icon: 'gear',
|
|
3286
|
+
};
|
|
3287
|
+
|
|
3288
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [overviewPage, settingsPage]);
|
|
3289
|
+
|
|
3290
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3291
|
+
expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
|
|
3292
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(2);
|
|
3293
|
+
});
|
|
3294
|
+
});
|
|
3295
|
+
|
|
3296
|
+
describe('product with resource pages', () => {
|
|
3297
|
+
it('should register resource CRUD routes for resource page items', () => {
|
|
3298
|
+
const mockPlugin = createMockPlugin();
|
|
3299
|
+
|
|
3300
|
+
const clusterPage: ProductChildResourcePage = {
|
|
3301
|
+
type: 'provisioning.cattle.io.cluster',
|
|
3302
|
+
weight: 2,
|
|
3303
|
+
config: {
|
|
3304
|
+
displayName: 'Clusters',
|
|
3305
|
+
isCreatable: true,
|
|
3306
|
+
isEditable: true,
|
|
3307
|
+
isRemovable: true,
|
|
3308
|
+
canYaml: true,
|
|
3309
|
+
},
|
|
3310
|
+
};
|
|
3311
|
+
|
|
3312
|
+
const nodePage: ProductChildResourcePage = {
|
|
3313
|
+
type: 'management.cattle.io.node',
|
|
3314
|
+
weight: 1,
|
|
3315
|
+
};
|
|
3316
|
+
|
|
3317
|
+
const product: ProductMetadata = {
|
|
3318
|
+
name: 'my-resources',
|
|
3319
|
+
label: 'My Resources',
|
|
3320
|
+
};
|
|
3321
|
+
|
|
3322
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [clusterPage, nodePage]);
|
|
3323
|
+
|
|
3324
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3325
|
+
// Resource routes are only added once (shared CRUD routes) - mock generates list + detail = 2
|
|
3326
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(2);
|
|
3327
|
+
});
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3330
|
+
describe('product with groups', () => {
|
|
3331
|
+
it('should register routes for a product with groups and standalone pages', () => {
|
|
3332
|
+
const mockPlugin = createMockPlugin();
|
|
3333
|
+
|
|
3334
|
+
const alertsPage: ProductChildCustomPage = {
|
|
3335
|
+
name: 'alerts',
|
|
3336
|
+
label: 'Alerts',
|
|
3337
|
+
component: { name: 'AlertsPage' },
|
|
3338
|
+
};
|
|
3339
|
+
|
|
3340
|
+
const metricsPage: ProductChildCustomPage = {
|
|
3341
|
+
name: 'metrics',
|
|
3342
|
+
label: 'Metrics',
|
|
3343
|
+
component: { name: 'MetricsPage' },
|
|
3344
|
+
};
|
|
3345
|
+
|
|
3346
|
+
const monitoringGroup: ProductChildGroup = {
|
|
3347
|
+
name: 'monitoring',
|
|
3348
|
+
label: 'Monitoring',
|
|
3349
|
+
weight: 2,
|
|
3350
|
+
children: [alertsPage, metricsPage],
|
|
3351
|
+
};
|
|
3352
|
+
|
|
3353
|
+
const overviewPage: ProductChildCustomPage = {
|
|
3354
|
+
name: 'overview',
|
|
3355
|
+
label: 'Overview',
|
|
3356
|
+
component: { name: 'OverviewPage' },
|
|
3357
|
+
weight: 3,
|
|
3358
|
+
};
|
|
3359
|
+
|
|
3360
|
+
const product: ProductMetadata = {
|
|
3361
|
+
name: 'my-platform',
|
|
3362
|
+
label: 'My Platform',
|
|
3363
|
+
};
|
|
3364
|
+
|
|
3365
|
+
const config: ProductChild[] = [overviewPage, monitoringGroup];
|
|
3366
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, config);
|
|
3367
|
+
|
|
3368
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3369
|
+
expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
|
|
3370
|
+
// 1 standalone page + 1 group parent route + 2 group children routes = 4
|
|
3371
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(4);
|
|
3372
|
+
});
|
|
3373
|
+
});
|
|
3374
|
+
|
|
3375
|
+
describe('extending an existing product', () => {
|
|
3376
|
+
it('should extend explorer with a custom page', () => {
|
|
3377
|
+
const mockPlugin = createMockPlugin();
|
|
3378
|
+
|
|
3379
|
+
const customPage: ProductChildCustomPage = {
|
|
3380
|
+
name: 'my-custom-view',
|
|
3381
|
+
label: 'My Custom View',
|
|
3382
|
+
component: { name: 'MyCustomView' },
|
|
3383
|
+
};
|
|
3384
|
+
|
|
3385
|
+
const pluginProduct = new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, [customPage]);
|
|
3386
|
+
|
|
3387
|
+
expect(pluginProduct.newProduct).toBe(false);
|
|
3388
|
+
expect(mockPlugin._registerTopLevelProduct).not.toHaveBeenCalled();
|
|
3389
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
|
|
3390
|
+
});
|
|
3391
|
+
});
|
|
3392
|
+
|
|
3393
|
+
describe('mixed pages: custom + resource', () => {
|
|
3394
|
+
it('should register routes for both custom pages and resource pages together', () => {
|
|
3395
|
+
const mockPlugin = createMockPlugin();
|
|
3396
|
+
|
|
3397
|
+
const dashboardPage: ProductChildCustomPage = {
|
|
3398
|
+
name: 'dashboard',
|
|
3399
|
+
label: 'Dashboard',
|
|
3400
|
+
component: { name: 'Dashboard' },
|
|
3401
|
+
weight: 3,
|
|
3402
|
+
};
|
|
3403
|
+
|
|
3404
|
+
const clusterPage: ProductChildResourcePage = {
|
|
3405
|
+
type: 'provisioning.cattle.io.cluster',
|
|
3406
|
+
weight: 2,
|
|
3407
|
+
};
|
|
3408
|
+
|
|
3409
|
+
const settingsPage: ProductChildCustomPage = {
|
|
3410
|
+
name: 'settings',
|
|
3411
|
+
label: 'Settings',
|
|
3412
|
+
component: { name: 'Settings' },
|
|
3413
|
+
weight: 1,
|
|
3414
|
+
};
|
|
3415
|
+
|
|
3416
|
+
const product: ProductMetadata = {
|
|
3417
|
+
name: 'my-platform',
|
|
3418
|
+
label: 'My Platform',
|
|
3419
|
+
};
|
|
3420
|
+
|
|
3421
|
+
const config: ProductChild[] = [dashboardPage, clusterPage, settingsPage];
|
|
3422
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, config);
|
|
3423
|
+
|
|
3424
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3425
|
+
// 2 custom page routes + resource CRUD routes (list + detail = 2 from mock) = 4
|
|
3426
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(4);
|
|
3427
|
+
// Verify addRoute was called for both custom pages and resource routes
|
|
3428
|
+
const routeCalls = (mockPlugin.addRoute as jest.Mock).mock.calls;
|
|
3429
|
+
const hasCustomRoutes = routeCalls.some((call) => call[0]?.name?.includes('dashboard'));
|
|
3430
|
+
const hasResourceRoutes = routeCalls.some((call) => call[0]?.name?.includes('provisioning.cattle.io.cluster'));
|
|
3431
|
+
|
|
3432
|
+
expect(hasCustomRoutes).toBe(true);
|
|
3433
|
+
expect(hasResourceRoutes).toBe(true);
|
|
3434
|
+
});
|
|
3435
|
+
});
|
|
3436
|
+
|
|
3437
|
+
describe('group with its own page', () => {
|
|
3438
|
+
it('should register a route for the group page itself and its children', () => {
|
|
3439
|
+
const mockPlugin = createMockPlugin();
|
|
3440
|
+
|
|
3441
|
+
const alertsPage: ProductChildCustomPage = {
|
|
3442
|
+
name: 'alerts',
|
|
3443
|
+
label: 'Alerts',
|
|
3444
|
+
component: { name: 'AlertsPage' },
|
|
3445
|
+
};
|
|
3446
|
+
|
|
3447
|
+
const metricsPage: ProductChildCustomPage = {
|
|
3448
|
+
name: 'metrics',
|
|
3449
|
+
label: 'Metrics',
|
|
3450
|
+
component: { name: 'MetricsPage' },
|
|
3451
|
+
};
|
|
3452
|
+
|
|
3453
|
+
const monitoringGroup: ProductChildGroup = {
|
|
3454
|
+
name: 'monitoring',
|
|
3455
|
+
label: 'Monitoring',
|
|
3456
|
+
component: { name: 'MonitoringOverview' },
|
|
3457
|
+
children: [alertsPage, metricsPage],
|
|
3458
|
+
};
|
|
3459
|
+
|
|
3460
|
+
const product: ProductMetadata = {
|
|
3461
|
+
name: 'my-platform',
|
|
3462
|
+
label: 'My Platform',
|
|
3463
|
+
};
|
|
3464
|
+
|
|
3465
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [monitoringGroup]);
|
|
3466
|
+
|
|
3467
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3468
|
+
// 1 group parent route (with component) + 2 children routes = 3
|
|
3469
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(3);
|
|
3470
|
+
});
|
|
3471
|
+
|
|
3472
|
+
it('should generate a route with the group name for proper side-menu highlighting', () => {
|
|
3473
|
+
const mockPlugin = createMockPlugin();
|
|
3474
|
+
|
|
3475
|
+
const childPage: ProductChildCustomPage = {
|
|
3476
|
+
name: 'child',
|
|
3477
|
+
label: 'Child',
|
|
3478
|
+
component: { name: 'ChildComponent' },
|
|
3479
|
+
};
|
|
3480
|
+
|
|
3481
|
+
const monitoringGroup: ProductChildGroup = {
|
|
3482
|
+
name: 'monitoring',
|
|
3483
|
+
label: 'Monitoring',
|
|
3484
|
+
component: { name: 'MonitoringOverview' },
|
|
3485
|
+
children: [childPage],
|
|
3486
|
+
};
|
|
3487
|
+
|
|
3488
|
+
const product: ProductMetadata = {
|
|
3489
|
+
name: 'my-platform',
|
|
3490
|
+
label: 'My Platform',
|
|
3491
|
+
};
|
|
3492
|
+
|
|
3493
|
+
new PluginProduct(mockPlugin, product, [monitoringGroup]);
|
|
3494
|
+
|
|
3495
|
+
// The group's own route should include the group name in the route name
|
|
3496
|
+
// This ensures the side-menu can highlight the correct item
|
|
3497
|
+
const routeCalls = (mockPlugin.addRoute as jest.Mock).mock.calls;
|
|
3498
|
+
const groupRoute = routeCalls.find((call) => call[0]?.name?.includes('monitoring'));
|
|
3499
|
+
|
|
3500
|
+
expect(groupRoute).toBeDefined();
|
|
3501
|
+
expect(groupRoute[0].name).toStrictEqual(expect.stringContaining('monitoring'));
|
|
3502
|
+
});
|
|
3503
|
+
});
|
|
3504
|
+
|
|
3505
|
+
describe('extending Cluster Explorer', () => {
|
|
3506
|
+
it('should extend explorer with a standalone page', () => {
|
|
3507
|
+
const mockPlugin = createMockPlugin();
|
|
3508
|
+
|
|
3509
|
+
const customPage: ProductChildCustomPage = {
|
|
3510
|
+
name: 'cost-analysis',
|
|
3511
|
+
label: 'Cost Analysis',
|
|
3512
|
+
component: { name: 'CostAnalysis' },
|
|
3513
|
+
};
|
|
3514
|
+
|
|
3515
|
+
const pluginProduct = new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, [customPage]);
|
|
3516
|
+
|
|
3517
|
+
expect(pluginProduct.newProduct).toBe(false);
|
|
3518
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
|
|
3519
|
+
});
|
|
3520
|
+
|
|
3521
|
+
it('should extend explorer with a group containing multiple pages', () => {
|
|
3522
|
+
const mockPlugin = createMockPlugin();
|
|
3523
|
+
|
|
3524
|
+
const costPage: ProductChildCustomPage = {
|
|
3525
|
+
name: 'cost-analysis',
|
|
3526
|
+
label: 'Cost Analysis',
|
|
3527
|
+
component: { name: 'CostAnalysis' },
|
|
3528
|
+
};
|
|
3529
|
+
|
|
3530
|
+
const usagePage: ProductChildCustomPage = {
|
|
3531
|
+
name: 'usage-report',
|
|
3532
|
+
label: 'Usage Report',
|
|
3533
|
+
component: { name: 'UsageReport' },
|
|
3534
|
+
};
|
|
3535
|
+
|
|
3536
|
+
const insightsGroup: ProductChildGroup = {
|
|
3537
|
+
name: 'insights',
|
|
3538
|
+
label: 'Insights',
|
|
3539
|
+
children: [costPage, usagePage],
|
|
3540
|
+
};
|
|
3541
|
+
|
|
3542
|
+
const pluginProduct = new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, [insightsGroup]);
|
|
3543
|
+
|
|
3544
|
+
expect(pluginProduct.newProduct).toBe(false);
|
|
3545
|
+
// 1 group parent route + 2 child routes = 3
|
|
3546
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(3);
|
|
3547
|
+
});
|
|
3548
|
+
});
|
|
3549
|
+
|
|
3550
|
+
describe('translation keys instead of labels', () => {
|
|
3551
|
+
it('should accept labelKey instead of label for product and pages', () => {
|
|
3552
|
+
const mockPlugin = createMockPlugin();
|
|
3553
|
+
|
|
3554
|
+
const product: ProductMetadata = {
|
|
3555
|
+
name: 'my-app',
|
|
3556
|
+
labelKey: 'product.myApp.label',
|
|
3557
|
+
icon: 'gear',
|
|
3558
|
+
};
|
|
3559
|
+
|
|
3560
|
+
const overviewPage: ProductChildCustomPage = {
|
|
3561
|
+
name: 'overview',
|
|
3562
|
+
labelKey: 'product.myApp.overview',
|
|
3563
|
+
component: { name: 'OverviewPage' },
|
|
3564
|
+
};
|
|
3565
|
+
|
|
3566
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [overviewPage]);
|
|
3567
|
+
|
|
3568
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3569
|
+
expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
|
|
3570
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
|
|
3571
|
+
});
|
|
3572
|
+
|
|
3573
|
+
it('should register labelKey on virtualType during apply', () => {
|
|
3574
|
+
const mockPlugin = createMockPlugin();
|
|
3575
|
+
const mockStore = createMockStore();
|
|
3576
|
+
const virtualTypeCalls: any[] = [];
|
|
3577
|
+
const mockDSL = {
|
|
3578
|
+
product: jest.fn(),
|
|
3579
|
+
basicType: jest.fn(),
|
|
3580
|
+
labelGroup: jest.fn(),
|
|
3581
|
+
setGroupDefaultType: jest.fn(),
|
|
3582
|
+
weightGroup: jest.fn(),
|
|
3583
|
+
virtualType: jest.fn((...args: any[]) => virtualTypeCalls.push(args)),
|
|
3584
|
+
configureType: jest.fn(),
|
|
3585
|
+
weightType: jest.fn(),
|
|
3586
|
+
};
|
|
3587
|
+
|
|
3588
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3589
|
+
|
|
3590
|
+
const product: ProductMetadata = {
|
|
3591
|
+
name: 'my-app',
|
|
3592
|
+
labelKey: 'product.myApp.label',
|
|
3593
|
+
};
|
|
3594
|
+
|
|
3595
|
+
const overviewPage: ProductChildCustomPage = {
|
|
3596
|
+
name: 'overview',
|
|
3597
|
+
labelKey: 'product.myApp.overview',
|
|
3598
|
+
component: { name: 'OverviewPage' },
|
|
3599
|
+
};
|
|
3600
|
+
|
|
3601
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [overviewPage]);
|
|
3602
|
+
|
|
3603
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3604
|
+
|
|
3605
|
+
expect(virtualTypeCalls).toHaveLength(1);
|
|
3606
|
+
expect(virtualTypeCalls[0][0]).toStrictEqual(expect.objectContaining({ labelKey: 'product.myApp.overview' }));
|
|
3607
|
+
});
|
|
3608
|
+
});
|
|
3609
|
+
|
|
3610
|
+
describe('duplicate page name detection', () => {
|
|
3611
|
+
it('should throw when two custom pages have the same name in a new product', () => {
|
|
3612
|
+
const mockPlugin = createMockPlugin();
|
|
3613
|
+
const mockStore = createMockStore();
|
|
3614
|
+
const mockDSL = {
|
|
3615
|
+
product: jest.fn(),
|
|
3616
|
+
basicType: jest.fn(),
|
|
3617
|
+
labelGroup: jest.fn(),
|
|
3618
|
+
setGroupDefaultType: jest.fn(),
|
|
3619
|
+
weightGroup: jest.fn(),
|
|
3620
|
+
virtualType: jest.fn(),
|
|
3621
|
+
configureType: jest.fn(),
|
|
3622
|
+
weightType: jest.fn(),
|
|
3623
|
+
};
|
|
3624
|
+
|
|
3625
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3626
|
+
|
|
3627
|
+
const page1: ProductChildCustomPage = {
|
|
3628
|
+
name: 'overview',
|
|
3629
|
+
label: 'Overview',
|
|
3630
|
+
component: { name: 'Page1' },
|
|
3631
|
+
};
|
|
3632
|
+
|
|
3633
|
+
const page2: ProductChildCustomPage = {
|
|
3634
|
+
name: 'overview',
|
|
3635
|
+
label: 'Overview Duplicate',
|
|
3636
|
+
component: { name: 'Page2' },
|
|
3637
|
+
};
|
|
3638
|
+
|
|
3639
|
+
const product: ProductMetadata = {
|
|
3640
|
+
name: 'my-app',
|
|
3641
|
+
label: 'My App',
|
|
3642
|
+
};
|
|
3643
|
+
|
|
3644
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [page1, page2]);
|
|
3645
|
+
|
|
3646
|
+
expect(() => {
|
|
3647
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3648
|
+
}).toThrow('Duplicate page name "overview"');
|
|
3649
|
+
});
|
|
3650
|
+
|
|
3651
|
+
it('should not throw when pages with the same name are in different groups (different resolved names)', () => {
|
|
3652
|
+
const mockPlugin = createMockPlugin();
|
|
3653
|
+
const mockStore = createMockStore();
|
|
3654
|
+
const mockDSL = {
|
|
3655
|
+
product: jest.fn(),
|
|
3656
|
+
basicType: jest.fn(),
|
|
3657
|
+
labelGroup: jest.fn(),
|
|
3658
|
+
setGroupDefaultType: jest.fn(),
|
|
3659
|
+
weightGroup: jest.fn(),
|
|
3660
|
+
virtualType: jest.fn(),
|
|
3661
|
+
configureType: jest.fn(),
|
|
3662
|
+
weightType: jest.fn(),
|
|
3663
|
+
};
|
|
3664
|
+
|
|
3665
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3666
|
+
|
|
3667
|
+
// Same page name 'overview' but in different groups produces different resolved names
|
|
3668
|
+
const standalonePage: ProductChildCustomPage = {
|
|
3669
|
+
name: 'cost-analysis',
|
|
3670
|
+
label: 'Cost Analysis',
|
|
3671
|
+
component: { name: 'CostAnalysis1' },
|
|
3672
|
+
};
|
|
3673
|
+
|
|
3674
|
+
const groupChildPage: ProductChildCustomPage = {
|
|
3675
|
+
name: 'cost-analysis',
|
|
3676
|
+
label: 'Cost Analysis',
|
|
3677
|
+
component: { name: 'CostAnalysis2' },
|
|
3678
|
+
};
|
|
3679
|
+
|
|
3680
|
+
const group: ProductChildGroup = {
|
|
3681
|
+
name: 'insights',
|
|
3682
|
+
label: 'Insights',
|
|
3683
|
+
children: [groupChildPage],
|
|
3684
|
+
};
|
|
3685
|
+
|
|
3686
|
+
const product: ProductMetadata = {
|
|
3687
|
+
name: 'my-app',
|
|
3688
|
+
label: 'My App',
|
|
3689
|
+
};
|
|
3690
|
+
|
|
3691
|
+
const config: ProductChild[] = [standalonePage, group];
|
|
3692
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, config);
|
|
3693
|
+
|
|
3694
|
+
// Different groups produce different resolved names (myapp-cost-analysis vs myapp-insights-cost-analysis)
|
|
3695
|
+
expect(() => {
|
|
3696
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3697
|
+
}).not.toThrow();
|
|
3698
|
+
});
|
|
3699
|
+
|
|
3700
|
+
it('should throw when two resource pages have the same type', () => {
|
|
3701
|
+
const mockPlugin = createMockPlugin();
|
|
3702
|
+
const mockStore = createMockStore();
|
|
3703
|
+
const mockDSL = {
|
|
3704
|
+
product: jest.fn(),
|
|
3705
|
+
basicType: jest.fn(),
|
|
3706
|
+
labelGroup: jest.fn(),
|
|
3707
|
+
setGroupDefaultType: jest.fn(),
|
|
3708
|
+
weightGroup: jest.fn(),
|
|
3709
|
+
virtualType: jest.fn(),
|
|
3710
|
+
configureType: jest.fn(),
|
|
3711
|
+
weightType: jest.fn(),
|
|
3712
|
+
};
|
|
3713
|
+
|
|
3714
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3715
|
+
|
|
3716
|
+
const resource1: ProductChildResourcePage = { type: 'provisioning.cattle.io.cluster' };
|
|
3717
|
+
|
|
3718
|
+
const resource2: ProductChildResourcePage = { type: 'provisioning.cattle.io.cluster' };
|
|
3719
|
+
|
|
3720
|
+
const product: ProductMetadata = {
|
|
3721
|
+
name: 'my-app',
|
|
3722
|
+
label: 'My App',
|
|
3723
|
+
};
|
|
3724
|
+
|
|
3725
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [resource1, resource2]);
|
|
3726
|
+
|
|
3727
|
+
expect(() => {
|
|
3728
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3729
|
+
}).toThrow('Duplicate resource type "provisioning.cattle.io.cluster"');
|
|
3730
|
+
});
|
|
3731
|
+
|
|
3732
|
+
it('should not throw when pages have different names', () => {
|
|
3733
|
+
const mockPlugin = createMockPlugin();
|
|
3734
|
+
const mockStore = createMockStore();
|
|
3735
|
+
const mockDSL = {
|
|
3736
|
+
product: jest.fn(),
|
|
3737
|
+
basicType: jest.fn(),
|
|
3738
|
+
labelGroup: jest.fn(),
|
|
3739
|
+
setGroupDefaultType: jest.fn(),
|
|
3740
|
+
weightGroup: jest.fn(),
|
|
3741
|
+
virtualType: jest.fn(),
|
|
3742
|
+
configureType: jest.fn(),
|
|
3743
|
+
weightType: jest.fn(),
|
|
3744
|
+
};
|
|
3745
|
+
|
|
3746
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3747
|
+
|
|
3748
|
+
const page1: ProductChildCustomPage = {
|
|
3749
|
+
name: 'overview',
|
|
3750
|
+
label: 'Overview',
|
|
3751
|
+
component: { name: 'Page1' },
|
|
3752
|
+
};
|
|
3753
|
+
|
|
3754
|
+
const page2: ProductChildCustomPage = {
|
|
3755
|
+
name: 'settings',
|
|
3756
|
+
label: 'Settings',
|
|
3757
|
+
component: { name: 'Page2' },
|
|
3758
|
+
};
|
|
3759
|
+
|
|
3760
|
+
const product: ProductMetadata = {
|
|
3761
|
+
name: 'my-app',
|
|
3762
|
+
label: 'My App',
|
|
3763
|
+
};
|
|
3764
|
+
|
|
3765
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [page1, page2]);
|
|
3766
|
+
|
|
3767
|
+
expect(() => {
|
|
3768
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3769
|
+
}).not.toThrow();
|
|
3770
|
+
});
|
|
3771
|
+
});
|
|
3772
|
+
|
|
3773
|
+
describe('group with component route naming', () => {
|
|
3774
|
+
it('should include the group name in the virtualType route for side-menu highlighting', () => {
|
|
3775
|
+
const mockPlugin = createMockPlugin();
|
|
3776
|
+
const mockStore = createMockStore();
|
|
3777
|
+
const virtualTypeCalls: any[] = [];
|
|
3778
|
+
const mockDSL = {
|
|
3779
|
+
product: jest.fn(),
|
|
3780
|
+
basicType: jest.fn(),
|
|
3781
|
+
labelGroup: jest.fn(),
|
|
3782
|
+
setGroupDefaultType: jest.fn(),
|
|
3783
|
+
weightGroup: jest.fn(),
|
|
3784
|
+
virtualType: jest.fn((...args: any[]) => virtualTypeCalls.push(args)),
|
|
3785
|
+
configureType: jest.fn(),
|
|
3786
|
+
weightType: jest.fn(),
|
|
3787
|
+
};
|
|
3788
|
+
|
|
3789
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3790
|
+
|
|
3791
|
+
const childPage: ProductChildCustomPage = {
|
|
3792
|
+
name: 'alerts',
|
|
3793
|
+
label: 'Alerts',
|
|
3794
|
+
component: { name: 'AlertsPage' },
|
|
3795
|
+
};
|
|
3796
|
+
|
|
3797
|
+
const monitoringGroup: ProductChildGroup = {
|
|
3798
|
+
name: 'monitoring',
|
|
3799
|
+
label: 'Monitoring',
|
|
3800
|
+
component: { name: 'MonitoringOverview' },
|
|
3801
|
+
children: [childPage],
|
|
3802
|
+
};
|
|
3803
|
+
|
|
3804
|
+
const product: ProductMetadata = {
|
|
3805
|
+
name: 'my-app',
|
|
3806
|
+
label: 'My App',
|
|
3807
|
+
};
|
|
3808
|
+
|
|
3809
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [monitoringGroup]);
|
|
3810
|
+
|
|
3811
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3812
|
+
|
|
3813
|
+
// Find the virtualType call for the group (has exact + overview flags)
|
|
3814
|
+
const groupVirtualType = virtualTypeCalls.find((call) => call[0].exact === true && call[0].overview === true);
|
|
3815
|
+
|
|
3816
|
+
expect(groupVirtualType).toBeDefined();
|
|
3817
|
+
// The route name should contain the group name 'monitoring', not a generic 'group'
|
|
3818
|
+
expect(groupVirtualType[0].route.name).toStrictEqual(expect.stringContaining('monitoring'));
|
|
3819
|
+
// It should NOT be a generic route without the group name
|
|
3820
|
+
expect(groupVirtualType[0].route.name).not.toBe('myapp-c-cluster');
|
|
3821
|
+
});
|
|
3822
|
+
describe('resource page: headers support', () => {
|
|
3823
|
+
it('should call DSL headers method when resource page has headers configured', () => {
|
|
3824
|
+
const mockPlugin = createMockPlugin();
|
|
3825
|
+
const mockStore = createMockStore();
|
|
3826
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
3827
|
+
|
|
3828
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3829
|
+
|
|
3830
|
+
const productMetadata: ProductMetadata = {
|
|
3831
|
+
name: 'headers-test',
|
|
3832
|
+
label: 'Headers Test',
|
|
3833
|
+
};
|
|
3834
|
+
|
|
3835
|
+
const testHeaders = [
|
|
3836
|
+
{
|
|
3837
|
+
name: 'name', label: 'Name', value: 'metadata.name'
|
|
3838
|
+
},
|
|
3839
|
+
{
|
|
3840
|
+
name: 'status', labelKey: 'tableHeaders.status', value: 'status'
|
|
3841
|
+
},
|
|
3842
|
+
];
|
|
3843
|
+
|
|
3844
|
+
const config: ProductChildPage[] = [
|
|
3845
|
+
{ type: 'some.resource.type', headers: testHeaders },
|
|
3846
|
+
];
|
|
3847
|
+
|
|
3848
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
3849
|
+
|
|
3850
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3851
|
+
|
|
3852
|
+
expect(mockDSL.headers).toHaveBeenCalledWith('some.resource.type', testHeaders, undefined);
|
|
3853
|
+
});
|
|
3854
|
+
|
|
3855
|
+
it('should call DSL headers method with sspHeaders when configured', () => {
|
|
3856
|
+
const mockPlugin = createMockPlugin();
|
|
3857
|
+
const mockStore = createMockStore();
|
|
3858
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
3859
|
+
|
|
3860
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3861
|
+
|
|
3862
|
+
const productMetadata: ProductMetadata = {
|
|
3863
|
+
name: 'ssp-headers-test',
|
|
3864
|
+
label: 'SSP Headers Test',
|
|
3865
|
+
};
|
|
3866
|
+
|
|
3867
|
+
const testSspHeaders = [
|
|
3868
|
+
{
|
|
3869
|
+
name: 'name', label: 'Name', value: 'metadata.name'
|
|
3870
|
+
},
|
|
3871
|
+
{
|
|
3872
|
+
name: 'namespace', labelKey: 'tableHeaders.namespace', value: 'metadata.namespace'
|
|
3873
|
+
},
|
|
3874
|
+
];
|
|
3875
|
+
|
|
3876
|
+
const config: ProductChildPage[] = [
|
|
3877
|
+
{ type: 'some.resource.type', sspHeaders: testSspHeaders },
|
|
3878
|
+
];
|
|
3879
|
+
|
|
3880
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
3881
|
+
|
|
3882
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3883
|
+
|
|
3884
|
+
expect(mockDSL.headers).toHaveBeenCalledWith('some.resource.type', undefined, testSspHeaders);
|
|
3885
|
+
});
|
|
3886
|
+
|
|
3887
|
+
it('should call DSL headers method with both headers and sspHeaders when both are configured', () => {
|
|
3888
|
+
const mockPlugin = createMockPlugin();
|
|
3889
|
+
const mockStore = createMockStore();
|
|
3890
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
3891
|
+
|
|
3892
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3893
|
+
|
|
3894
|
+
const productMetadata: ProductMetadata = {
|
|
3895
|
+
name: 'both-headers-test',
|
|
3896
|
+
label: 'Both Headers Test',
|
|
3897
|
+
};
|
|
3898
|
+
|
|
3899
|
+
const testHeaders = [
|
|
3900
|
+
{
|
|
3901
|
+
name: 'name', label: 'Name', value: 'metadata.name'
|
|
3902
|
+
},
|
|
3903
|
+
];
|
|
3904
|
+
|
|
3905
|
+
const testSspHeaders = [
|
|
3906
|
+
{
|
|
3907
|
+
name: 'name', label: 'Name', value: 'metadata.name'
|
|
3908
|
+
},
|
|
3909
|
+
];
|
|
3910
|
+
|
|
3911
|
+
const config: ProductChildPage[] = [
|
|
3912
|
+
{
|
|
3913
|
+
type: 'some.resource.type',
|
|
3914
|
+
headers: testHeaders,
|
|
3915
|
+
sspHeaders: testSspHeaders,
|
|
3916
|
+
},
|
|
3917
|
+
];
|
|
3918
|
+
|
|
3919
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
3920
|
+
|
|
3921
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3922
|
+
|
|
3923
|
+
expect(mockDSL.headers).toHaveBeenCalledWith('some.resource.type', testHeaders, testSspHeaders);
|
|
3924
|
+
});
|
|
3925
|
+
|
|
3926
|
+
it('should not call DSL headers method when resource page has no headers or sspHeaders', () => {
|
|
3927
|
+
const mockPlugin = createMockPlugin();
|
|
3928
|
+
const mockStore = createMockStore();
|
|
3929
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
3930
|
+
|
|
3931
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3932
|
+
|
|
3933
|
+
const productMetadata: ProductMetadata = {
|
|
3934
|
+
name: 'no-headers-test',
|
|
3935
|
+
label: 'No Headers Test',
|
|
3936
|
+
};
|
|
3937
|
+
|
|
3938
|
+
const config: ProductChildPage[] = [{ type: 'some.resource.type' }];
|
|
3939
|
+
|
|
3940
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
3941
|
+
|
|
3942
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3943
|
+
|
|
3944
|
+
expect(mockDSL.headers).not.toHaveBeenCalled();
|
|
3945
|
+
});
|
|
3946
|
+
});
|
|
3947
|
+
|
|
3948
|
+
describe('resource page: overrideListResourceName (mapType) support', () => {
|
|
3949
|
+
it('should call DSL mapType when resource page has overrideListResourceName', () => {
|
|
3950
|
+
const mockPlugin = createMockPlugin();
|
|
3951
|
+
const mockStore = createMockStore();
|
|
3952
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
3953
|
+
|
|
3954
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3955
|
+
|
|
3956
|
+
const productMetadata: ProductMetadata = {
|
|
3957
|
+
name: 'maptype-test',
|
|
3958
|
+
label: 'MapType Test',
|
|
3959
|
+
};
|
|
3960
|
+
|
|
3961
|
+
const config: ProductChildPage[] = [
|
|
3962
|
+
{ type: 'apps.deployment', overrideListResourceName: 'Deployments' },
|
|
3963
|
+
];
|
|
3964
|
+
|
|
3965
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
3966
|
+
|
|
3967
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3968
|
+
|
|
3969
|
+
expect(mockDSL.mapType).toHaveBeenCalledWith('apps.deployment', 'Deployments');
|
|
3970
|
+
});
|
|
3971
|
+
|
|
3972
|
+
it('should not call mapType when overrideListResourceName is not set', () => {
|
|
3973
|
+
const mockPlugin = createMockPlugin();
|
|
3974
|
+
const mockStore = createMockStore();
|
|
3975
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
3976
|
+
|
|
3977
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3978
|
+
|
|
3979
|
+
const productMetadata: ProductMetadata = {
|
|
3980
|
+
name: 'no-maptype',
|
|
3981
|
+
label: 'No MapType',
|
|
3982
|
+
};
|
|
3983
|
+
|
|
3984
|
+
const config: ProductChildPage[] = [{ type: 'apps.deployment' }];
|
|
3985
|
+
|
|
3986
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
3987
|
+
|
|
3988
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3989
|
+
|
|
3990
|
+
expect(mockDSL.mapType).not.toHaveBeenCalled();
|
|
3991
|
+
});
|
|
3992
|
+
});
|
|
3993
|
+
|
|
3994
|
+
describe('resource page: hideFromNav (ignoreType) support', () => {
|
|
3995
|
+
it('should call DSL ignoreType when resource page has hideFromNav set', () => {
|
|
3996
|
+
const mockPlugin = createMockPlugin();
|
|
3997
|
+
const mockStore = createMockStore();
|
|
3998
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
3999
|
+
|
|
4000
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4001
|
+
|
|
4002
|
+
const productMetadata: ProductMetadata = {
|
|
4003
|
+
name: 'ignoretype-test',
|
|
4004
|
+
label: 'IgnoreType Test',
|
|
4005
|
+
};
|
|
4006
|
+
|
|
4007
|
+
const config: ProductChildPage[] = [
|
|
4008
|
+
{ type: 'secret.type', hideFromNav: true },
|
|
4009
|
+
];
|
|
4010
|
+
|
|
4011
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4012
|
+
|
|
4013
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4014
|
+
|
|
4015
|
+
expect(mockDSL.ignoreType).toHaveBeenCalledWith('secret.type');
|
|
4016
|
+
});
|
|
4017
|
+
|
|
4018
|
+
it('should not call ignoreType when hideFromNav is not set', () => {
|
|
4019
|
+
const mockPlugin = createMockPlugin();
|
|
4020
|
+
const mockStore = createMockStore();
|
|
4021
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4022
|
+
|
|
4023
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4024
|
+
|
|
4025
|
+
const productMetadata: ProductMetadata = {
|
|
4026
|
+
name: 'no-ignoretype',
|
|
4027
|
+
label: 'No IgnoreType',
|
|
4028
|
+
};
|
|
4029
|
+
|
|
4030
|
+
const config: ProductChildPage[] = [{ type: 'apps.deployment' }];
|
|
4031
|
+
|
|
4032
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4033
|
+
|
|
4034
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4035
|
+
|
|
4036
|
+
expect(mockDSL.ignoreType).not.toHaveBeenCalled();
|
|
4037
|
+
});
|
|
4038
|
+
});
|
|
4039
|
+
|
|
4040
|
+
describe('resource page: hideBulkActions support', () => {
|
|
4041
|
+
it('should call DSL hideBulkActions when resource page has hideBulkActions set', () => {
|
|
4042
|
+
const mockPlugin = createMockPlugin();
|
|
4043
|
+
const mockStore = createMockStore();
|
|
4044
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4045
|
+
|
|
4046
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4047
|
+
|
|
4048
|
+
const productMetadata: ProductMetadata = {
|
|
4049
|
+
name: 'bulk-actions-test',
|
|
4050
|
+
label: 'Bulk Actions Test',
|
|
4051
|
+
};
|
|
4052
|
+
|
|
4053
|
+
const config: ProductChildPage[] = [
|
|
4054
|
+
{ type: 'management.cattle.io.feature', hideBulkActions: true },
|
|
4055
|
+
];
|
|
4056
|
+
|
|
4057
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4058
|
+
|
|
4059
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4060
|
+
|
|
4061
|
+
expect(mockDSL.hideBulkActions).toHaveBeenCalledWith('management.cattle.io.feature', true);
|
|
4062
|
+
});
|
|
4063
|
+
});
|
|
4064
|
+
|
|
4065
|
+
describe('resource page: ordering - weight is set last', () => {
|
|
4066
|
+
it('should call weightType after configureType for resource pages', () => {
|
|
4067
|
+
const mockPlugin = createMockPlugin();
|
|
4068
|
+
const mockStore = createMockStore();
|
|
4069
|
+
const callOrder: string[] = [];
|
|
4070
|
+
const mockDSL = {
|
|
4071
|
+
product: jest.fn(),
|
|
4072
|
+
basicType: jest.fn(),
|
|
4073
|
+
labelGroup: jest.fn(),
|
|
4074
|
+
setGroupDefaultType: jest.fn(),
|
|
4075
|
+
weightGroup: jest.fn(),
|
|
4076
|
+
virtualType: jest.fn(),
|
|
4077
|
+
configureType: jest.fn(() => callOrder.push('configureType')),
|
|
4078
|
+
weightType: jest.fn(() => callOrder.push('weightType')),
|
|
4079
|
+
headers: jest.fn(() => callOrder.push('headers')),
|
|
4080
|
+
hideBulkActions: jest.fn(() => callOrder.push('hideBulkActions')),
|
|
4081
|
+
|
|
4082
|
+
mapGroup: jest.fn(),
|
|
4083
|
+
ignoreGroup: jest.fn(),
|
|
4084
|
+
mapType: jest.fn(() => callOrder.push('mapType')),
|
|
4085
|
+
ignoreType: jest.fn(() => callOrder.push('ignoreType')),
|
|
4086
|
+
};
|
|
4087
|
+
|
|
4088
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4089
|
+
|
|
4090
|
+
const productMetadata: ProductMetadata = {
|
|
4091
|
+
name: 'order-test',
|
|
4092
|
+
label: 'Order Test',
|
|
4093
|
+
};
|
|
4094
|
+
|
|
4095
|
+
const config: ProductChildPage[] = [
|
|
4096
|
+
{
|
|
4097
|
+
type: 'apps.deployment',
|
|
4098
|
+
weight: 10,
|
|
4099
|
+
headers: [{ name: 'col1', label: 'Col1' }],
|
|
4100
|
+
hideBulkActions: true,
|
|
4101
|
+
overrideListResourceName: 'Deployments',
|
|
4102
|
+
hideFromNav: true,
|
|
4103
|
+
},
|
|
4104
|
+
];
|
|
4105
|
+
|
|
4106
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4107
|
+
|
|
4108
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4109
|
+
|
|
4110
|
+
// weight must be the last thing set
|
|
4111
|
+
const weightIndex = callOrder.indexOf('weightType');
|
|
4112
|
+
const configureIndex = callOrder.indexOf('configureType');
|
|
4113
|
+
|
|
4114
|
+
expect(weightIndex).toBeGreaterThan(configureIndex);
|
|
4115
|
+
expect(callOrder[callOrder.length - 1]).toBe('weightType');
|
|
4116
|
+
});
|
|
4117
|
+
});
|
|
4118
|
+
|
|
4119
|
+
describe('product-level: renameGroups support', () => {
|
|
4120
|
+
it('should call DSL mapGroup for each renameGroups entry in product metadata', () => {
|
|
4121
|
+
const mockPlugin = createMockPlugin();
|
|
4122
|
+
const mockStore = createMockStore();
|
|
4123
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4124
|
+
|
|
4125
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4126
|
+
|
|
4127
|
+
const productMetadata: ProductMetadata = {
|
|
4128
|
+
name: 'maptogroup-test',
|
|
4129
|
+
label: 'MapToGroup Test',
|
|
4130
|
+
renameGroups: [
|
|
4131
|
+
{ groupSelector: /some\.regex/, newName: 'my-group' },
|
|
4132
|
+
{ groupSelector: 'exact.match', newName: 'other-group' },
|
|
4133
|
+
],
|
|
4134
|
+
};
|
|
4135
|
+
|
|
4136
|
+
const config: ProductChildPage[] = [
|
|
4137
|
+
{
|
|
4138
|
+
name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
|
|
4139
|
+
},
|
|
4140
|
+
];
|
|
4141
|
+
|
|
4142
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4143
|
+
|
|
4144
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4145
|
+
|
|
4146
|
+
expect(mockDSL.mapGroup).toHaveBeenCalledTimes(2);
|
|
4147
|
+
expect(mockDSL.mapGroup).toHaveBeenCalledWith(/some\.regex/, 'my-group');
|
|
4148
|
+
expect(mockDSL.mapGroup).toHaveBeenCalledWith('exact.match', 'other-group');
|
|
4149
|
+
});
|
|
4150
|
+
|
|
4151
|
+
it('should not call mapGroup when no renameGroups entries exist', () => {
|
|
4152
|
+
const mockPlugin = createMockPlugin();
|
|
4153
|
+
const mockStore = createMockStore();
|
|
4154
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4155
|
+
|
|
4156
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4157
|
+
|
|
4158
|
+
const productMetadata: ProductMetadata = {
|
|
4159
|
+
name: 'no-maptogroup',
|
|
4160
|
+
label: 'No MapToGroup',
|
|
4161
|
+
};
|
|
4162
|
+
|
|
4163
|
+
const config: ProductChildPage[] = [
|
|
4164
|
+
{
|
|
4165
|
+
name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
|
|
4166
|
+
},
|
|
4167
|
+
];
|
|
4168
|
+
|
|
4169
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4170
|
+
|
|
4171
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4172
|
+
|
|
4173
|
+
expect(mockDSL.mapGroup).not.toHaveBeenCalled();
|
|
4174
|
+
});
|
|
4175
|
+
});
|
|
4176
|
+
|
|
4177
|
+
describe('product-level: ignoreGroups support', () => {
|
|
4178
|
+
it('should call DSL ignoreGroup with callback when condition is provided', () => {
|
|
4179
|
+
const mockPlugin = createMockPlugin();
|
|
4180
|
+
const mockStore = createMockStore();
|
|
4181
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4182
|
+
|
|
4183
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4184
|
+
|
|
4185
|
+
const cbFn = jest.fn(() => true);
|
|
4186
|
+
|
|
4187
|
+
const productMetadata: ProductMetadata = {
|
|
4188
|
+
name: 'ignoregroups-test',
|
|
4189
|
+
label: 'IgnoreGroups Test',
|
|
4190
|
+
ignoreGroups: [
|
|
4191
|
+
{ groupSelector: 'hidden-group', condition: cbFn },
|
|
4192
|
+
],
|
|
4193
|
+
};
|
|
4194
|
+
|
|
4195
|
+
const config: ProductChildPage[] = [
|
|
4196
|
+
{
|
|
4197
|
+
name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
|
|
4198
|
+
},
|
|
4199
|
+
];
|
|
4200
|
+
|
|
4201
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4202
|
+
|
|
4203
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4204
|
+
|
|
4205
|
+
expect(mockDSL.ignoreGroup).toHaveBeenCalledTimes(1);
|
|
4206
|
+
expect(mockDSL.ignoreGroup).toHaveBeenCalledWith('hidden-group', cbFn);
|
|
4207
|
+
});
|
|
4208
|
+
|
|
4209
|
+
it('should call DSL ignoreGroup without callback when condition is not provided (unconditional hide)', () => {
|
|
4210
|
+
const mockPlugin = createMockPlugin();
|
|
4211
|
+
const mockStore = createMockStore();
|
|
4212
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4213
|
+
|
|
4214
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4215
|
+
|
|
4216
|
+
const productMetadata: ProductMetadata = {
|
|
4217
|
+
name: 'ignoregroups-unconditional',
|
|
4218
|
+
label: 'IgnoreGroups Unconditional',
|
|
4219
|
+
ignoreGroups: [
|
|
4220
|
+
{ groupSelector: 'always-hidden' },
|
|
4221
|
+
],
|
|
4222
|
+
};
|
|
4223
|
+
|
|
4224
|
+
const config: ProductChildPage[] = [
|
|
4225
|
+
{
|
|
4226
|
+
name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
|
|
4227
|
+
},
|
|
4228
|
+
];
|
|
4229
|
+
|
|
4230
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4231
|
+
|
|
4232
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4233
|
+
|
|
4234
|
+
expect(mockDSL.ignoreGroup).toHaveBeenCalledTimes(1);
|
|
4235
|
+
expect(mockDSL.ignoreGroup).toHaveBeenCalledWith('always-hidden');
|
|
4236
|
+
});
|
|
4237
|
+
|
|
4238
|
+
it('should support regex patterns in ignoreGroups', () => {
|
|
4239
|
+
const mockPlugin = createMockPlugin();
|
|
4240
|
+
const mockStore = createMockStore();
|
|
4241
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4242
|
+
|
|
4243
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4244
|
+
|
|
4245
|
+
const productMetadata: ProductMetadata = {
|
|
4246
|
+
name: 'ignoregroups-regex',
|
|
4247
|
+
label: 'IgnoreGroups Regex',
|
|
4248
|
+
ignoreGroups: [
|
|
4249
|
+
{ groupSelector: /^internal-.*/ },
|
|
4250
|
+
],
|
|
4251
|
+
};
|
|
4252
|
+
|
|
4253
|
+
const config: ProductChildPage[] = [
|
|
4254
|
+
{
|
|
4255
|
+
name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
|
|
4256
|
+
},
|
|
4257
|
+
];
|
|
4258
|
+
|
|
4259
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4260
|
+
|
|
4261
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4262
|
+
|
|
4263
|
+
expect(mockDSL.ignoreGroup).toHaveBeenCalledTimes(1);
|
|
4264
|
+
expect(mockDSL.ignoreGroup).toHaveBeenCalledWith(/^internal-.*/);
|
|
4265
|
+
});
|
|
4266
|
+
|
|
4267
|
+
it('should not call ignoreGroup when no ignoreGroups entries exist', () => {
|
|
4268
|
+
const mockPlugin = createMockPlugin();
|
|
4269
|
+
const mockStore = createMockStore();
|
|
4270
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4271
|
+
|
|
4272
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4273
|
+
|
|
4274
|
+
const productMetadata: ProductMetadata = {
|
|
4275
|
+
name: 'no-ignoregroups',
|
|
4276
|
+
label: 'No IgnoreGroups',
|
|
4277
|
+
};
|
|
4278
|
+
|
|
4279
|
+
const config: ProductChildPage[] = [
|
|
4280
|
+
{
|
|
4281
|
+
name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
|
|
4282
|
+
},
|
|
4283
|
+
];
|
|
4284
|
+
|
|
4285
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4286
|
+
|
|
4287
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4288
|
+
|
|
4289
|
+
expect(mockDSL.ignoreGroup).not.toHaveBeenCalled();
|
|
4290
|
+
});
|
|
4291
|
+
});
|
|
4292
|
+
|
|
4293
|
+
describe('product-level DSL options are not called when extending', () => {
|
|
4294
|
+
it('should not call mapGroup, ignoreGroup, or moveType when extending an existing product', () => {
|
|
4295
|
+
const mockPlugin = createMockPlugin();
|
|
4296
|
+
const mockStore = createMockStore();
|
|
4297
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4298
|
+
|
|
4299
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4300
|
+
|
|
4301
|
+
const config: ProductChildPage[] = [
|
|
4302
|
+
{
|
|
4303
|
+
name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
|
|
4304
|
+
},
|
|
4305
|
+
];
|
|
4306
|
+
|
|
4307
|
+
const pluginProduct = new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, config);
|
|
4308
|
+
|
|
4309
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4310
|
+
|
|
4311
|
+
expect(mockDSL.mapGroup).not.toHaveBeenCalled();
|
|
4312
|
+
expect(mockDSL.ignoreGroup).not.toHaveBeenCalled();
|
|
4313
|
+
expect(mockDSL.moveType).not.toHaveBeenCalled();
|
|
4314
|
+
});
|
|
4315
|
+
});
|
|
4316
|
+
|
|
4317
|
+
describe('product-level: moveToGroup support', () => {
|
|
4318
|
+
it('should call basicType and moveType to move a resource type into a group', () => {
|
|
4319
|
+
const mockPlugin = createMockPlugin();
|
|
4320
|
+
const mockStore = createMockStore();
|
|
4321
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4322
|
+
|
|
4323
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4324
|
+
|
|
4325
|
+
const monitoringGroup: ProductChildGroup = {
|
|
4326
|
+
name: 'monitoring',
|
|
4327
|
+
label: 'Monitoring',
|
|
4328
|
+
children: [
|
|
4329
|
+
{
|
|
4330
|
+
name: 'alerts', label: 'Alerts', component: { name: 'AlertsPage' }
|
|
4331
|
+
},
|
|
4332
|
+
],
|
|
4333
|
+
};
|
|
4334
|
+
|
|
4335
|
+
const productMetadata: ProductMetadata = {
|
|
4336
|
+
name: 'my-app',
|
|
4337
|
+
label: 'My App',
|
|
4338
|
+
moveToGroup: [
|
|
4339
|
+
{ entryId: 'pod', groupName: 'monitoring' },
|
|
4340
|
+
],
|
|
4341
|
+
};
|
|
4342
|
+
|
|
4343
|
+
const config: ProductChild[] = [
|
|
4344
|
+
monitoringGroup,
|
|
4345
|
+
{ type: 'pod' },
|
|
4346
|
+
];
|
|
4347
|
+
|
|
4348
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4349
|
+
|
|
4350
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4351
|
+
|
|
4352
|
+
// basicType re-registers the page under the resolved group for the nav tree
|
|
4353
|
+
expect(mockDSL.basicType).toHaveBeenCalledWith(['pod'], 'myapp-monitoring');
|
|
4354
|
+
// moveType also called for resource types (non-basic view modes)
|
|
4355
|
+
expect(mockDSL.moveType).toHaveBeenCalledTimes(1);
|
|
4356
|
+
expect(mockDSL.moveType).toHaveBeenCalledWith('pod', 'myapp-monitoring', undefined);
|
|
4357
|
+
});
|
|
4358
|
+
|
|
4359
|
+
it('should call basicType but NOT moveType when moving a custom page into a group', () => {
|
|
4360
|
+
const mockPlugin = createMockPlugin();
|
|
4361
|
+
const mockStore = createMockStore();
|
|
4362
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4363
|
+
|
|
4364
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4365
|
+
|
|
4366
|
+
const monitoringGroup: ProductChildGroup = {
|
|
4367
|
+
name: 'monitoring',
|
|
4368
|
+
label: 'Monitoring',
|
|
4369
|
+
children: [
|
|
4370
|
+
{
|
|
4371
|
+
name: 'alerts', label: 'Alerts', component: { name: 'AlertsPage' }
|
|
4372
|
+
},
|
|
4373
|
+
],
|
|
4374
|
+
};
|
|
4375
|
+
|
|
4376
|
+
const customPage: ProductChildCustomPage = {
|
|
4377
|
+
name: 'dashboard', label: 'Dashboard', component: { name: 'DashboardPage' }
|
|
4378
|
+
};
|
|
4379
|
+
|
|
4380
|
+
const productMetadata: ProductMetadata = {
|
|
4381
|
+
name: 'my-app',
|
|
4382
|
+
label: 'My App',
|
|
4383
|
+
moveToGroup: [
|
|
4384
|
+
{ entryId: 'dashboard', groupName: 'monitoring' },
|
|
4385
|
+
],
|
|
4386
|
+
};
|
|
4387
|
+
|
|
4388
|
+
const config: ProductChild[] = [monitoringGroup, customPage];
|
|
4389
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4390
|
+
|
|
4391
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4392
|
+
|
|
4393
|
+
// basicType re-registers the custom page under the resolved group
|
|
4394
|
+
expect(mockDSL.basicType).toHaveBeenCalledWith(['myapp-dashboard'], 'myapp-monitoring');
|
|
4395
|
+
// moveType is NOT called for custom pages (no schema to match against)
|
|
4396
|
+
expect(mockDSL.moveType).not.toHaveBeenCalled();
|
|
4397
|
+
});
|
|
4398
|
+
|
|
4399
|
+
it('should pass weight to DSL moveType when specified', () => {
|
|
4400
|
+
const mockPlugin = createMockPlugin();
|
|
4401
|
+
const mockStore = createMockStore();
|
|
4402
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4403
|
+
|
|
4404
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4405
|
+
|
|
4406
|
+
const myGroup: ProductChildGroup = {
|
|
4407
|
+
name: 'resources',
|
|
4408
|
+
label: 'Resources',
|
|
4409
|
+
children: [
|
|
4410
|
+
{
|
|
4411
|
+
name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
|
|
4412
|
+
},
|
|
4413
|
+
],
|
|
4414
|
+
};
|
|
4415
|
+
|
|
4416
|
+
const productMetadata: ProductMetadata = {
|
|
4417
|
+
name: 'my-app',
|
|
4418
|
+
label: 'My App',
|
|
4419
|
+
moveToGroup: [
|
|
4420
|
+
{
|
|
4421
|
+
entryId: 'apps.deployment', groupName: 'resources', weight: 10
|
|
4422
|
+
},
|
|
4423
|
+
],
|
|
4424
|
+
};
|
|
4425
|
+
|
|
4426
|
+
const config: ProductChild[] = [myGroup, { type: 'apps.deployment' }];
|
|
4427
|
+
|
|
4428
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4429
|
+
|
|
4430
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4431
|
+
|
|
4432
|
+
expect(mockDSL.basicType).toHaveBeenCalledWith(['apps.deployment'], 'myapp-resources');
|
|
4433
|
+
expect(mockDSL.moveType).toHaveBeenCalledWith('apps.deployment', 'myapp-resources', 10);
|
|
4434
|
+
});
|
|
4435
|
+
|
|
4436
|
+
it('should throw when moveToGroup references a groupName that does not exist in the config', () => {
|
|
4437
|
+
const mockPlugin = createMockPlugin();
|
|
4438
|
+
const mockStore = createMockStore();
|
|
4439
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4440
|
+
|
|
4441
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4442
|
+
|
|
4443
|
+
const productMetadata: ProductMetadata = {
|
|
4444
|
+
name: 'my-app',
|
|
4445
|
+
label: 'My App',
|
|
4446
|
+
moveToGroup: [
|
|
4447
|
+
{ entryId: 'pod', groupName: 'nonexistent-group' },
|
|
4448
|
+
],
|
|
4449
|
+
};
|
|
4450
|
+
|
|
4451
|
+
const config: ProductChildPage[] = [
|
|
4452
|
+
{ type: 'pod' },
|
|
4453
|
+
];
|
|
4454
|
+
|
|
4455
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4456
|
+
|
|
4457
|
+
expect(() => {
|
|
4458
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4459
|
+
}).toThrow('moveToGroup target group "nonexistent-group" not found');
|
|
4460
|
+
});
|
|
4461
|
+
|
|
4462
|
+
it('should throw when moveToGroup entryId does not match any registered page', () => {
|
|
4463
|
+
const mockPlugin = createMockPlugin();
|
|
4464
|
+
const mockStore = createMockStore();
|
|
4465
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4466
|
+
|
|
4467
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4468
|
+
|
|
4469
|
+
const myGroup: ProductChildGroup = {
|
|
4470
|
+
name: 'monitoring',
|
|
4471
|
+
label: 'Monitoring',
|
|
4472
|
+
children: [
|
|
4473
|
+
{
|
|
4474
|
+
name: 'alerts', label: 'Alerts', component: { name: 'AlertsPage' }
|
|
4475
|
+
},
|
|
4476
|
+
],
|
|
4477
|
+
};
|
|
4478
|
+
|
|
4479
|
+
const productMetadata: ProductMetadata = {
|
|
4480
|
+
name: 'my-app',
|
|
4481
|
+
label: 'My App',
|
|
4482
|
+
moveToGroup: [
|
|
4483
|
+
{ entryId: 'nonexistent-page', groupName: 'monitoring' },
|
|
4484
|
+
],
|
|
4485
|
+
};
|
|
4486
|
+
|
|
4487
|
+
const config: ProductChild[] = [myGroup];
|
|
4488
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4489
|
+
|
|
4490
|
+
expect(() => {
|
|
4491
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4492
|
+
}).toThrow('moveToGroup entryId "nonexistent-page" not found');
|
|
4493
|
+
});
|
|
4494
|
+
|
|
4495
|
+
it('should not call moveType when no moveToGroup entries exist', () => {
|
|
4496
|
+
const mockPlugin = createMockPlugin();
|
|
4497
|
+
const mockStore = createMockStore();
|
|
4498
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4499
|
+
|
|
4500
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4501
|
+
|
|
4502
|
+
const productMetadata: ProductMetadata = {
|
|
4503
|
+
name: 'no-move',
|
|
4504
|
+
label: 'No Move',
|
|
4505
|
+
};
|
|
4506
|
+
|
|
4507
|
+
const config: ProductChildPage[] = [
|
|
4508
|
+
{
|
|
4509
|
+
name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
|
|
4510
|
+
},
|
|
4511
|
+
];
|
|
4512
|
+
|
|
4513
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4514
|
+
|
|
4515
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4516
|
+
|
|
4517
|
+
expect(mockDSL.moveType).not.toHaveBeenCalled();
|
|
4518
|
+
});
|
|
4519
|
+
|
|
4520
|
+
it('should support multiple moveToGroup entries targeting different groups', () => {
|
|
4521
|
+
const mockPlugin = createMockPlugin();
|
|
4522
|
+
const mockStore = createMockStore();
|
|
4523
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4524
|
+
|
|
4525
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4526
|
+
|
|
4527
|
+
const networkingGroup: ProductChildGroup = {
|
|
4528
|
+
name: 'networking',
|
|
4529
|
+
label: 'Networking',
|
|
4530
|
+
children: [
|
|
4531
|
+
{
|
|
4532
|
+
name: 'net-overview', label: 'Overview', component: { name: 'NetOverview' }
|
|
4533
|
+
},
|
|
4534
|
+
],
|
|
4535
|
+
};
|
|
4536
|
+
|
|
4537
|
+
const storageGroup: ProductChildGroup = {
|
|
4538
|
+
name: 'storage',
|
|
4539
|
+
label: 'Storage',
|
|
4540
|
+
children: [
|
|
4541
|
+
{
|
|
4542
|
+
name: 'storage-overview', label: 'Overview', component: { name: 'StorageOverview' }
|
|
4543
|
+
},
|
|
4544
|
+
],
|
|
4545
|
+
};
|
|
4546
|
+
|
|
4547
|
+
const productMetadata: ProductMetadata = {
|
|
4548
|
+
name: 'my-app',
|
|
4549
|
+
label: 'My App',
|
|
4550
|
+
moveToGroup: [
|
|
4551
|
+
{ entryId: 'networking.ingress', groupName: 'networking' },
|
|
4552
|
+
{ entryId: 'storage.pvc', groupName: 'storage' },
|
|
4553
|
+
],
|
|
4554
|
+
};
|
|
4555
|
+
|
|
4556
|
+
const config: ProductChild[] = [
|
|
4557
|
+
networkingGroup,
|
|
4558
|
+
storageGroup,
|
|
4559
|
+
{ type: 'networking.ingress' },
|
|
4560
|
+
{ type: 'storage.pvc' },
|
|
4561
|
+
];
|
|
4562
|
+
|
|
4563
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
4564
|
+
|
|
4565
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4566
|
+
|
|
4567
|
+
expect(mockDSL.basicType).toHaveBeenCalledWith(['networking.ingress'], 'myapp-networking');
|
|
4568
|
+
expect(mockDSL.basicType).toHaveBeenCalledWith(['storage.pvc'], 'myapp-storage');
|
|
4569
|
+
expect(mockDSL.moveType).toHaveBeenCalledTimes(2);
|
|
4570
|
+
expect(mockDSL.moveType).toHaveBeenCalledWith('networking.ingress', 'myapp-networking', undefined);
|
|
4571
|
+
expect(mockDSL.moveType).toHaveBeenCalledWith('storage.pvc', 'myapp-storage', undefined);
|
|
4572
|
+
});
|
|
4573
|
+
});
|
|
4574
|
+
|
|
4575
|
+
describe('resource page DSL options work when extending a product', () => {
|
|
4576
|
+
it('should support headers, hideBulkActions, overrideListResourceName, hideFromNav on resource pages when extending', () => {
|
|
4577
|
+
const mockPlugin = createMockPlugin();
|
|
4578
|
+
const mockStore = createMockStore();
|
|
4579
|
+
const mockDSL = (mockPlugin.DSL as jest.Mock)();
|
|
4580
|
+
|
|
4581
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
4582
|
+
|
|
4583
|
+
const testHeaders = [{ name: 'col1', label: 'Column 1' }];
|
|
4584
|
+
|
|
4585
|
+
const config: ProductChildPage[] = [
|
|
4586
|
+
{
|
|
4587
|
+
type: 'custom.resource.type',
|
|
4588
|
+
headers: testHeaders,
|
|
4589
|
+
hideBulkActions: true,
|
|
4590
|
+
overrideListResourceName: 'Custom Name',
|
|
4591
|
+
hideFromNav: true,
|
|
4592
|
+
},
|
|
4593
|
+
];
|
|
4594
|
+
|
|
4595
|
+
const pluginProduct = new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, config);
|
|
4596
|
+
|
|
4597
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
4598
|
+
|
|
4599
|
+
expect(mockDSL.headers).toHaveBeenCalledWith('custom.resource.type', testHeaders, undefined);
|
|
4600
|
+
expect(mockDSL.hideBulkActions).toHaveBeenCalledWith('custom.resource.type', true);
|
|
4601
|
+
expect(mockDSL.mapType).toHaveBeenCalledWith('custom.resource.type', 'Custom Name');
|
|
4602
|
+
expect(mockDSL.ignoreType).toHaveBeenCalledWith('custom.resource.type');
|
|
4603
|
+
});
|
|
4604
|
+
});
|
|
4605
|
+
});
|
|
4606
|
+
});
|
|
4607
|
+
|
|
4608
|
+
describe('addProduct duplicate guard', () => {
|
|
4609
|
+
it('should throw when addProduct is called twice with the same product name (object form)', () => {
|
|
4610
|
+
const plugin = new Plugin('test-extension');
|
|
4611
|
+
|
|
4612
|
+
const product: ProductMetadata = {
|
|
4613
|
+
name: 'my-product',
|
|
4614
|
+
label: 'My Product',
|
|
4615
|
+
};
|
|
4616
|
+
|
|
4617
|
+
const config: ProductChildPage[] = [
|
|
4618
|
+
{
|
|
4619
|
+
name: 'page-a', label: 'Page A', component: { name: 'PageA' }
|
|
4620
|
+
},
|
|
4621
|
+
];
|
|
4622
|
+
|
|
4623
|
+
plugin.addProduct(product, config);
|
|
4624
|
+
|
|
4625
|
+
expect(() => {
|
|
4626
|
+
plugin.addProduct(product, [{
|
|
4627
|
+
name: 'page-b', label: 'Page B', component: { name: 'PageB' }
|
|
4628
|
+
}]);
|
|
4629
|
+
}).toThrow('addProduct can only be called once per product');
|
|
4630
|
+
});
|
|
4631
|
+
|
|
4632
|
+
it('should throw when addProduct is called twice with the same product name (string form)', () => {
|
|
4633
|
+
const plugin = new Plugin('test-extension');
|
|
4634
|
+
|
|
4635
|
+
plugin.addProduct('my-product');
|
|
4636
|
+
|
|
4637
|
+
expect(() => {
|
|
4638
|
+
plugin.addProduct('my-product');
|
|
4639
|
+
}).toThrow('addProduct can only be called once per product');
|
|
4640
|
+
});
|
|
4641
|
+
|
|
4642
|
+
it('should throw when addProduct is called twice mixing string and object form for the same name', () => {
|
|
4643
|
+
const plugin = new Plugin('test-extension');
|
|
4644
|
+
|
|
4645
|
+
plugin.addProduct('my-product');
|
|
4646
|
+
|
|
4647
|
+
expect(() => {
|
|
4648
|
+
plugin.addProduct({ name: 'my-product', label: 'My Product' }, []);
|
|
4649
|
+
}).toThrow('addProduct can only be called once per product');
|
|
4650
|
+
});
|
|
4651
|
+
|
|
4652
|
+
it('should allow addProduct for different product names', () => {
|
|
4653
|
+
const plugin = new Plugin('test-extension');
|
|
4654
|
+
|
|
4655
|
+
plugin.addProduct('product-a');
|
|
4656
|
+
|
|
4657
|
+
expect(() => {
|
|
4658
|
+
plugin.addProduct('product-b');
|
|
4659
|
+
}).not.toThrow();
|
|
4660
|
+
|
|
4661
|
+
expect(plugin.productConfigs).toHaveLength(2);
|
|
4662
|
+
});
|
|
4663
|
+
|
|
4664
|
+
it('should allow addProduct and extendProduct for the same name (extending is separate)', () => {
|
|
4665
|
+
const plugin = new Plugin('test-extension');
|
|
4666
|
+
|
|
4667
|
+
plugin.addProduct('my-product');
|
|
4668
|
+
|
|
4669
|
+
expect(() => {
|
|
4670
|
+
plugin.extendProduct('explorer', [{
|
|
4671
|
+
name: 'extra-page', label: 'Extra', component: { name: 'Extra' }
|
|
4672
|
+
}]);
|
|
4673
|
+
}).not.toThrow();
|
|
4674
|
+
|
|
4675
|
+
expect(plugin.productConfigs).toHaveLength(2);
|
|
4676
|
+
});
|
|
4677
|
+
|
|
4678
|
+
it('should throw when addProduct is called twice with single page product form', () => {
|
|
4679
|
+
const plugin = new Plugin('test-extension');
|
|
4680
|
+
|
|
4681
|
+
const singlePage: ProductSinglePage = {
|
|
4682
|
+
name: 'my-dashboard',
|
|
4683
|
+
label: 'My Dashboard',
|
|
4684
|
+
component: { name: 'DashboardPage' },
|
|
4685
|
+
};
|
|
4686
|
+
|
|
4687
|
+
plugin.addProduct(singlePage);
|
|
4688
|
+
|
|
4689
|
+
expect(() => {
|
|
4690
|
+
plugin.addProduct(singlePage);
|
|
4691
|
+
}).toThrow('addProduct can only be called once per product');
|
|
4692
|
+
});
|
|
4693
|
+
});
|
|
3219
4694
|
});
|