Merge pull request #47 from torchiaf/portings-harvester-version

Remove Harvester manager and disable legacy versions
This commit is contained in:
Francesco Torchia 2024-12-04 15:19:06 +01:00 committed by GitHub
commit e33cc457e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 122 additions and 791 deletions

View File

@ -1,86 +0,0 @@
import { HCI, MANAGEMENT, CAPI } from '@shell/config/types';
import { HARVESTER, MULTI_CLUSTER } from '@shell/store/features';
import { allHash } from '@shell/utils/promise';
import { BLANK_CLUSTER } from '@shell/store/store-types.js';
export const PRODUCT_NAME = 'harvester-manager';
export const NAME = 'harvesterManager';
const harvesterClustersLocation = {
name: 'c-cluster-product-resource',
params: {
cluster: BLANK_CLUSTER,
product: NAME,
resource: HCI.CLUSTER
}
};
export function init($plugin, store) {
const {
product,
basicType,
spoofedType,
configureType
} = $plugin.DSL(store, NAME);
product({
ifHaveType: CAPI.RANCHER_CLUSTER,
ifFeature: [MULTI_CLUSTER, HARVESTER],
inStore: 'management',
icon: 'harvester',
removable: false,
showClusterSwitcher: false,
weight: 100,
to: harvesterClustersLocation,
category: 'hci',
});
configureType(HCI.CLUSTER, { showListMasthead: false });
basicType([HCI.CLUSTER]);
spoofedType({
labelKey: 'harvesterManager.cluster.label',
name: HCI.CLUSTER,
type: HCI.CLUSTER,
namespaced: false,
weight: -1,
route: {
name: 'c-cluster-product-resource',
params: {
product: NAME,
resource: HCI.CLUSTER,
}
},
exact: false,
schemas: [
{
id: HCI.CLUSTER,
type: 'schema',
collectionMethods: [],
resourceFields: {},
attributes: { namespaced: true },
},
],
group: 'Root',
getInstances: async() => {
const hash = {
rancherClusters: store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER }),
clusters: store.dispatch('management/findAll', { type: MANAGEMENT.CLUSTER }),
};
if (store.getters['management/schemaFor'](MANAGEMENT.NODE)) {
hash.nodes = store.dispatch('management/findAll', { type: MANAGEMENT.NODE });
}
const res = await allHash(hash);
return res.rancherClusters.map((c) => {
return {
...c,
type: HCI.CLUSTER,
};
});
},
});
}

View File

@ -1,166 +0,0 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import CruResource from '@shell/components/CruResource';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import Tab from '@shell/components/Tabbed/Tab';
import Tabbed from '@shell/components/Tabbed';
import { HCI, SCHEMA, CAPI, VIRTUAL_HARVESTER_PROVIDER } from '@shell/config/types';
import ClusterMembershipEditor from '@shell/components/form/Members/ClusterMembershipEditor';
import { Banner } from '@components/Banner';
import Labels from '@shell/edit/provisioning.cattle.io.cluster/Labels';
import AgentEnv from '@shell/edit/provisioning.cattle.io.cluster/AgentEnv';
import { set, get, clone } from '@shell/utils/object';
import { CAPI as CAPI_LABEL } from '@shell/config/labels-annotations';
import { createYaml } from '@shell/utils/create-yaml';
const REAL_TYPE = CAPI.RANCHER_CLUSTER;
export default {
emits: ['input'],
components: {
Banner,
ClusterMembershipEditor,
NameNsDescription,
CruResource,
Tab,
Tabbed,
Labels,
AgentEnv
},
mixins: [CreateEditView],
inheritAttrs: false,
props: {
mode: {
type: String,
required: true,
},
value: {
type: Object,
required: true,
},
},
data() {
return { membershipUpdate: {} };
},
computed: {
generateYaml() {
return () => {
const resource = this.value;
const inStore = this.$store.getters['currentStore'](resource);
const schemas = this.$store.getters[`${ inStore }/all`](SCHEMA);
const clonedResource = clone(resource);
delete clonedResource.isSpoofed;
const out = createYaml(schemas, REAL_TYPE, clonedResource);
return out;
};
},
},
methods: {
done() {
return this.$router.replace({
name: 'c-cluster-product-resource-namespace-id',
params: {
resource: HCI.CLUSTER,
namespace: this.value.metadata.namespace,
id: this.value.metadata.name,
},
});
},
async saveOverride() {
set(this.value, 'metadata.labels', {
...(get(this.value, 'metadata.labels') || {}),
[CAPI_LABEL.PROVIDER]: VIRTUAL_HARVESTER_PROVIDER,
});
set(this.value, 'type', REAL_TYPE);
await this.save(...arguments);
this.value.waitForMgmt().then(() => {
if (this.membershipUpdate.save) {
this.membershipUpdate.save(this.value.mgmt.id);
}
});
},
onMembershipUpdate(update) {
this['membershipUpdate'] = update;
},
},
};
</script>
<template>
<CruResource
:mode="mode"
:resource="value"
:errors="errors"
:validation-passed="true"
:done-route="doneRoute"
:generate-yaml="generateYaml"
@finish="saveOverride"
@error="e=>errors = e"
>
<Banner
v-if="isCreate"
color="warning"
>
{{ t('harvesterManager.cluster.supportMessage') }}
</Banner>
<div class="mt-20">
<NameNsDescription
v-if="!isView"
:value="value"
:mode="mode"
:namespaced="false"
name-label="cluster.name.label"
name-placeholder="cluster.name.placeholder"
description-label="cluster.description.label"
description-placeholder="cluster.description.placeholder"
@update:value="$emit('input', $event)"
/>
</div>
<Tabbed :side-tabs="true">
<Tab
name="memberRoles"
label-key="cluster.tabs.memberRoles"
:weight="3"
>
<Banner
v-if="isEdit"
color="info"
>
{{ t('cluster.memberRoles.removeMessage') }}
</Banner>
<ClusterMembershipEditor
:mode="mode"
:parent-id="value.mgmt ? value.mgmt.id : null"
@membership-update="onMembershipUpdate"
/>
</Tab>
<AgentEnv
:value="value"
:mode="mode"
@update:value="$emit('input', $event)"
/>
<Labels
:value="value"
:mode="mode"
@update:value="$emit('input', $event)"
/>
</Tabbed>
</CruResource>
</template>

View File

@ -20,10 +20,6 @@ export default function (plugin: IPlugin) {
// Built-in icon
plugin.metadata.icon = require('./icon.svg');
if (isDev && !isSingleVirtualCluster) {
plugin.addProduct(require('./config/harvester-manager'));
}
plugin.addProduct(require('./config/harvester-cluster'));

View File

@ -1,34 +1,3 @@
harvesterManager:
manage: Manage
tableHeaders:
kubernetesVersion: Kubernetes Version
harvesterVersion: Harvester Version
cluster:
label: Harvester Clusters
none: There are no Harvester Clusters
learnMore: Learn more about Harvester from the <a target="_blank" href="https://harvesterhci.io/" rel="noopener noreferrer nofollow">Harvester Web Site</a> or read the the <a target="_blank" href="https://docs.harvesterhci.io/" rel="noopener noreferrer nofollow">Harvester Docs</a>
description: Harvester is a modern Hyperconverged infrastructure (HCI) solution built for bare metal servers using enterprise-grade open source technologies including Kubernetes, Kubevirt and Longhorn.
supportMessage: Harvester ui extension only supports Harvester cluster version greater or equal to 1.3.0
plugins:
loadError: Error loading harvester plugin
rke:
templateError: Incorrect template format
affinity:
thisPodNamespace: This VM's namespace
matchExpressions:
inNamespaces: "Workloads in these namespaces"
vmAffinityTitle: VM Scheduling
namespaces:
placeholder: e.g. default,system,base
label: Namespaces
addLabel: Add Workload Selector
topologyKey:
placeholder: 'topology.kubernetes.io/zone'
vGpu:
title: VGPUs
label: VGPU type
placeholder: 'Please select a VGPU'
generic:
tip: Tip
resourceExternalLinkTips: 'External Link'
@ -39,6 +8,11 @@ generic:
basic: Basic
loading: Loading...
unsupported:
serverVersion: 'Current version: <code>{serverVersion}</code>'
description: Harvester ui only supports Harvester cluster version greater or equal to 1.3.0.
learnMore: Learn more about Harvester from the <a target="_blank" href="https://harvesterhci.io/" rel="noopener noreferrer nofollow">Harvester Web Site</a> or read the <a target="_blank" href="https://docs.harvesterhci.io/" rel="noopener noreferrer nofollow">Harvester Docs</a>
nav:
group:
networks: Networks

View File

@ -1,279 +0,0 @@
<script>
import BrandImage from '@shell/components/BrandImage';
import TypeDescription from '@shell/components/TypeDescription';
import ResourceTable from '@shell/components/ResourceTable';
import Masthead from '@shell/components/ResourceList/Masthead';
import Loading from '@shell/components/Loading';
import { HARVESTER_NAME as VIRTUAL } from '@shell/config/features';
import { CAPI, HCI, MANAGEMENT } from '@shell/config/types';
import { isHarvesterCluster } from '@shell/utils/cluster';
import { allHash } from '@shell/utils/promise';
import { STATE, NAME as NAME_COL, AGE, VERSION } from '@shell/config/table-headers';
import { MACHINE_POOLS } from '../config/table-headers';
export default {
components: {
BrandImage,
ResourceTable,
Masthead,
TypeDescription,
Loading
},
props: {
schema: {
type: Object,
required: true,
},
useQueryParamsForSimpleFiltering: {
type: Boolean,
default: false
}
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await allHash({
hciClusters: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER }),
mgmtClusters: this.$store.dispatch(`${ inStore }/findAll`, { type: MANAGEMENT.CLUSTER })
});
this.hciClusters = hash.hciClusters;
this.mgmtClusters = hash.mgmtClusters;
},
data() {
const resource = CAPI.RANCHER_CLUSTER;
return {
navigating: false,
VIRTUAL,
hciDashboard: HCI.DASHBOARD,
resource,
hResource: HCI.CLUSTER,
realSchema: this.$store.getters['management/schemaFor'](CAPI.RANCHER_CLUSTER),
hciClusters: [],
mgmtClusters: []
};
},
computed: {
headers() {
return [
STATE,
NAME_COL,
{
name: 'harvesterVersion',
sort: 'harvesterVersion',
labelKey: 'harvesterManager.tableHeaders.harvesterVersion',
value: 'harvesterVersion',
getValue: (row) => row.harvesterVersion
},
{
...VERSION,
labelKey: 'harvesterManager.tableHeaders.kubernetesVersion',
value: 'kubernetesVersion',
getValue: (row) => row.kubernetesVersion
},
MACHINE_POOLS,
AGE,
{
name: 'harvester',
label: ' ',
align: 'right',
width: 65,
},
];
},
importLocation() {
return {
name: 'c-cluster-product-resource-create',
params: {
product: this.$store.getters['currentProduct'].name,
resource: this.schema.id,
},
};
},
canCreateCluster() {
const schema = this.$store.getters['management/schemaFor'](CAPI.RANCHER_CLUSTER);
return !!schema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
},
rows() {
return this.hciClusters
.filter((c) => {
const cluster = this.mgmtClusters.find((cluster) => cluster?.metadata?.name === c?.status?.clusterName);
return isHarvesterCluster(cluster);
})
.map((row) => {
if (row.isReady) {
row.setSupportedHarvesterVersion();
}
return row;
});
},
typeDisplay() {
return this.t(`typeLabel."${ HCI.CLUSTER }"`, { count: this.rows?.length || 0 });
},
},
methods: {
async goToCluster(row) {
const timeout = setTimeout(() => {
// Don't show loading indicator for quickly fetched plugins
this.navigating = row.id;
}, 1000);
try {
await row.goToCluster();
clearTimeout(timeout);
this.navigating = false;
} catch {
// The error handling is carried out within goToCluster, but just in case something happens before the promise chain can catch it...
clearTimeout(timeout);
this.navigating = false;
}
}
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<Masthead
:schema="realSchema"
:resource="resource"
:is-creatable="false"
:type-display="typeDisplay"
>
<template #typeDescription>
<TypeDescription :resource="hResource" />
</template>
<template
v-if="canCreateCluster"
#extraActions
>
<router-link
:to="importLocation"
class="btn role-primary"
>
{{ t('cluster.importAction') }}
</router-link>
</template>
</Masthead>
<ResourceTable
v-if="rows && rows.length"
:schema="schema"
:headers="headers"
:rows="rows"
:is-creatable="true"
:namespaced="false"
:use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
>
<template #col:name="{row}">
<td>
<span class="cluster-link">
<a
v-if="row.isReady && row.isSupportedHarvester"
class="link"
:disabled="navigating ? true : null"
@click="goToCluster(row)"
>{{ row.nameDisplay }}</a>
<span v-else>
{{ row.nameDisplay }}
</span>
<i
class="icon icon-spinner icon-spin ml-5"
:class="{'navigating': navigating === row.id}"
/>
</span>
</td>
</template>
<template #cell:harvester="{row}">
<router-link
class="btn btn-sm role-primary"
:to="row.detailLocation"
>
{{ t('harvesterManager.manage') }}
</router-link>
</template>
</ResourceTable>
<div v-else>
<div class="no-clusters">
{{ t('harvesterManager.cluster.none') }}
</div>
<hr class="info-section">
<div class="logo">
<BrandImage
file-name="harvester.png"
height="64"
/>
</div>
<div class="tagline">
<div>{{ t('harvesterManager.cluster.description') }}</div>
</div>
<div class="tagline sub-tagline">
<div v-clean-html="t('harvesterManager.cluster.learnMore', {}, true)" />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.cluster-link {
display: flex;
align-items: center;
.icon {
// Use visibility to avoid the columns re-adjusting when the icon is shown
visibility: hidden;
&.navigating {
visibility: visible;
}
}
}
.no-clusters {
text-align: center;
}
.info-section {
margin-top: 60px;
}
.logo {
display: flex;
justify-content: center;
margin: 60px 0 40px 0;
}
.tagline {
display: flex;
justify-content: center;
margin-top: 30px;
> div {
font-size: 16px;
line-height: 22px;
max-width: 80%;
text-align: center;
}
}
.link {
cursor: pointer;
}
</style>

View File

@ -1,223 +0,0 @@
import ProvCluster from '@shell/models/provisioning.cattle.io.cluster';
import { DEFAULT_WORKSPACE, HCI, MANAGEMENT } from '@shell/config/types';
import { HARVESTER_NAME, HARVESTER_NAME as VIRTUAL } from '@shell/config/features';
import { SETTING } from '@shell/config/settings';
import { colorForState, stateDisplay, STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
export default class HciCluster extends ProvCluster {
get isSupportedHarvester() {
return this._isSupportedHarvester === undefined ? true : this._isSupportedHarvester;
}
get harvesterVersion() {
return this._harvesterVersion;
}
get stateObj() {
if (!this.isSupportedHarvester) {
return { error: true, message: this.t('harvesterManager.cluster.supportMessage') };
}
return this._stateObj;
}
applyDefaults() {
if ( !this.spec ) {
this['spec'] = { agentEnvVars: [] };
this['metadata'] = { namespace: DEFAULT_WORKSPACE };
}
}
get isReady() {
// If the Connected condition exists, use that (2.6+)
if ( this.hasCondition('Connected') ) {
return this.isCondition('Connected');
}
// Otherwise use Ready (older)
return this.isCondition('Ready');
}
get canEdit() {
return false;
}
get stateColor() {
if (!this.isSupportedHarvester) {
return colorForState(STATES_ENUM.DENIED);
}
return colorForState(this.state);
}
get stateDisplay() {
if (!this.isSupportedHarvester) {
return stateDisplay(STATES_ENUM.DENIED);
}
return stateDisplay(this.state);
}
/**
* Fetch and cache the response for /ui-info
*
* Storing this in a cache means any changes to `ui-info` require a dashboard refresh... but it cuts out a http request every time we
* go to a cluster
*
* @param {string} clusterId
*/
async _getUiInfo(clusterId) {
if (!this._uiInfo) {
try {
const infoUrl = `/k8s/clusters/${ clusterId }/v1/harvester/ui-info`;
this._uiInfo = await this.$dispatch('request', { url: infoUrl });
} catch (e) {
console.info(`Failed to fetch harvester ui-info from ${ this.nameDisplay }, this may be an older cluster that cannot provide one`); // eslint-disable-line no-console
}
}
return this._uiInfo;
}
/**
* Determine the harvester plugin's package name and url for legacy clusters that don't provide the package (i.e. it's coming from
* outside the cluster)
*/
_legacyClusterPkgDetails() {
let uiOfflinePreferred = this.$rootGetters['management/byId'](MANAGEMENT.SETTING, SETTING.UI_OFFLINE_PREFERRED)?.value;
// options: ['dynamic', 'true', 'false']
if (uiOfflinePreferred === 'dynamic') {
// We shouldn't need to worry about the version of the dashboard when embedded in harvester (aka in isSingleProduct)
const version = this.$rootGetters['management/byId'](MANAGEMENT.SETTING, SETTING.VERSION_RANCHER)?.value;
if (version.endsWith('-head')) {
uiOfflinePreferred = 'false';
} else {
uiOfflinePreferred = 'true';
}
}
// This is the version that's embedded in the dashboard
const pkgName = `${ HARVESTER_NAME }-1.0.3`;
if (uiOfflinePreferred === 'true') {
// Embedded (aka give me the embedded plugin that was in the last rancher release)
const embeddedPath = `${ pkgName }/${ pkgName }.umd.min.js`;
return {
pkgUrl: process.env.dev ? `${ process.env.api }/dashboard/${ embeddedPath }` : embeddedPath,
pkgName
};
}
if (uiOfflinePreferred === 'false') {
// Remote (aka give me the latest version of the embedded plugin that might not have been released yet)
const uiDashboardHarvesterRemotePlugin = this.$rootGetters['management/byId'](MANAGEMENT.SETTING, SETTING.UI_DASHBOARD_HARVESTER_LEGACY_PLUGIN)?.value;
const parts = uiDashboardHarvesterRemotePlugin?.replace('.umd.min.js', '').split('/');
const pkgNameFromUrl = parts?.length > 1 ? parts[parts.length - 1] : null;
if (!pkgNameFromUrl) {
throw new Error(`Unable to determine harvester plugin name from '${ uiDashboardHarvesterRemotePlugin }'`);
}
return {
pkgUrl: uiDashboardHarvesterRemotePlugin,
pkgName: pkgNameFromUrl
};
}
throw new Error(`Unsupported value for ${ SETTING.UI_OFFLINE_PREFERRED }: 'uiOfflinePreferred'`);
}
/**
* Determine the harvester plugin's package name and url for clusters that provide the plugin
*/
_supportedClusterPkgDetails(uiInfo, clusterId) {
let pkgName = `${ HARVESTER_NAME }-${ uiInfo['ui-plugin-bundled-version'] }`;
const fileName = `${ pkgName }.umd.min.js`;
let pkgUrl;
if (uiInfo['ui-source'] === 'bundled' ) { // offline bundled
pkgUrl = `/k8s/clusters/${ clusterId }/v1/harvester/plugin-assets/${ fileName }`;
} else if (uiInfo['ui-source'] === 'external') {
if (uiInfo['ui-plugin-index']) {
pkgUrl = uiInfo['ui-plugin-index'];
// When using an external address, the pkgName should also be get from the url
const names = pkgUrl.split('/');
const jsName = names[names.length - 1];
pkgName = jsName?.split('.umd.min.js')[0];
} else {
throw new Error('Harvester cluster requested the plugin at `ui-plugin-index` is used, however did not provide a value for it');
}
}
return {
pkgUrl,
pkgName
};
}
_overridePkgDetails() {
// Support loading the pkg from a locally, or other, address
// This helps testing of the harvester plugin when packaged up, instead of directly imported
const harvesterPkgUrl = process.env.harvesterPkgUrl;
if (!harvesterPkgUrl) {
return;
}
const parts = harvesterPkgUrl.replace('.umd.min.js', '').split('/');
const pkgNameFromUrl = parts.length > 1 ? parts[parts.length - 1] : null;
if (pkgNameFromUrl) {
return {
pkgUrl: harvesterPkgUrl,
pkgName: pkgNameFromUrl
};
}
}
async _pkgDetails() {
const overridePkgDetails = this._overridePkgDetails();
if (overridePkgDetails) {
return overridePkgDetails;
}
const clusterId = this.mgmt.id;
const uiInfo = await this._getUiInfo(clusterId);
return uiInfo ? this._supportedClusterPkgDetails(uiInfo, clusterId) : this._legacyClusterPkgDetails();
}
async goToCluster() {
this.currentRouter().push({
name: `${ VIRTUAL }-c-cluster-resource`,
params: {
cluster: this.status.clusterName,
product: VIRTUAL,
resource: HCI.DASHBOARD // Go directly to dashboard to avoid blip of components on screen
}
});
}
async setSupportedHarvesterVersion() {
if (this._isSupportedHarvester !== undefined) {
return;
}
const url = `/k8s/clusters/${ this.status.clusterName }/v1`;
try {
const setting = await this.$dispatch('request', { url: `${ url }/${ HCI.SETTING }s/server-version` });
this._harvesterVersion = setting?.value;
this._isSupportedHarvester = this.$rootGetters['harvester-common/getFeatureEnabled']('supportHarvesterClusterVersion', setting?.value);
} catch (error) {
console.error('unable to get harvester version from settings/server-version', error); // eslint-disable-line no-console
}
}
}

View File

@ -7,8 +7,8 @@
"annotations": {
"catalog.cattle.io/display-name": "Harvester",
"catalog.cattle.io/kube-version": ">= 1.16.0-0",
"catalog.cattle.io/rancher-version": ">= 2.10.0-0",
"catalog.cattle.io/ui-extensions-version": ">= 3.0.0-0"
"catalog.cattle.io/rancher-version": ">= 2.10.1-0",
"catalog.cattle.io/ui-extensions-version": ">= 3.0.0 < 4.0.0"
}
},
"icon": "https://raw.githubusercontent.com/harvester/harvester-ui-extension/main/pkg/harvester/icon.svg",

View File

@ -0,0 +1,85 @@
<script>
import BrandImage from '@shell/components/BrandImage';
export default {
components: { BrandImage },
computed: {
serverVersion() {
const version = this.$store.getters['harvester-common/getServerVersion']() || '';
return version.replace('v', '');
}
}
};
</script>
<template>
<div class="dashboard-root">
<div class="dashboard-content">
<main class="main-layout">
<div class="logo">
<BrandImage
file-name="harvester.png"
height="64"
/>
</div>
<div class="tagline">
<div class="description">
{{ t('unsupported.description') }}
</div>
</div>
<div class="tagline">
<div v-clean-html="t('unsupported.serverVersion', { serverVersion }, true)" />
</div>
<div class="tagline">
<div v-clean-html="t('unsupported.learnMore', {}, true)" />
</div>
</main>
</div>
</div>
</template>
<style lang="scss" scoped>
.dashboard-root {
display: flex;
flex-direction: column;
height: 100vh;
}
.dashboard-content {
display: grid;
flex-grow:1;
grid-template-areas:
"main";
grid-template-columns: auto;
grid-template-rows: auto;
}
.logo {
display: flex;
justify-content: center;
margin: 200px 0 40px 0;
}
.tagline {
display: flex;
justify-content: center;
margin-top: 30px;
> div {
font-size: 16px;
line-height: 22px;
max-width: 80%;
text-align: center;
}
}
.description {
font-size: 24px !important;
font-weight: 400;
}
</style>

View File

@ -15,8 +15,25 @@ import HarvesterAirgapUpdgrade from '../pages/c/_cluster/airgapupgrade/index.vue
import HarvesterMembers from '../pages/c/_cluster/members/index.vue';
import ProjectNamespaces from '../pages/c/_cluster/projectsnamespaces.vue';
import HarvesterAlertmanagerReceiver from '../pages/c/_cluster/alertmanagerconfig/_alertmanagerconfigid/receiver.vue';
import HarvesterUnsupported from '../pages/c/_cluster/unsupported/index.vue';
const routes = [
{
route: {
name: `${ PRODUCT_NAME }-c-cluster-unsupported-standalone`,
path: `/:product`,
component: HarvesterUnsupported
},
parent: 'blank'
},
{
route: {
name: `${ PRODUCT_NAME }-c-cluster-unsupported`,
path: `/:product`,
component: HarvesterUnsupported
},
parent: 'plain'
},
{
name: `${ PRODUCT_NAME }-c-cluster-support`,
path: `/:product/c/:cluster/support`,

View File

@ -101,6 +101,19 @@ export default {
const res: any = await allHash(hash);
const isHarvesterVersionSupported = rootGetters['harvester-common/getFeatureEnabled']('supportHarvesterClusterVersion');
if (!isHarvesterVersionSupported) {
const product = rootGetters['productId'];
this.$router?.push({
name: `${ product }-c-cluster-unsupported${ id === 'local' ? '-standalone' : '' }`,
params: { product }
});
return;
}
await dispatch('cleanNamespaces', null, { root: true });
commit('updateNamespaces', {