refactor: forklift UI bug and enhancement (#989)

* fix: hide Virtul machine migration tabs if forklift addon is not enabled

Signed-off-by: Andy Lee <andy.lee@suse.com>

* fix: rename advanced to configurations

Signed-off-by: Andy Lee <andy.lee@suse.com>

* fix: fixed column width

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: review comment

Signed-off-by: Andy Lee <andy.lee@suse.com>

* revert: power on background color

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
Andy Lee 2026-07-14 12:04:00 +08:00 committed by GitHub
parent 7d09cb7142
commit 41cf44d231
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 71 additions and 25 deletions

View File

@ -402,6 +402,7 @@ defineExpose({ testConnection, clickTestButton });
<div v-if="!createOnly && !editMode">
<LabeledSelect
v-model:value="selectedProvider"
required
:label="t('harvester.addons.vmMigration.configureProvider.providerSelect')"
:options="providerOptions"
:reduce="(opt) => opt.value"

View File

@ -107,18 +107,21 @@ const headers = [
labelKey: 'harvester.addons.vmMigration.selectVms.columns.os',
value: 'os',
sort: ['os'],
width: 180,
},
{
name: 'resources',
labelKey: 'harvester.addons.vmMigration.selectVms.columns.resources',
value: 'resources',
sort: false,
width: 140,
},
{
name: 'powerState',
labelKey: 'harvester.addons.vmMigration.selectVms.columns.powerState',
value: 'powerState',
sort: ['powerState'],
width: 130,
},
{
name: 'network',
@ -126,6 +129,7 @@ const headers = [
value: 'network',
sort: ['network'],
subLabel: t('harvester.addons.vmMigration.generic.identifier'),
width: 200,
},
{
name: 'datastore',
@ -133,6 +137,7 @@ const headers = [
value: 'datastore',
sort: ['datastore'],
subLabel: t('harvester.addons.vmMigration.generic.identifier'),
width: 200,
},
];

View File

@ -1378,7 +1378,7 @@ export function init($plugin, store) {
virtualType({
name: HCI.FORKLIFT_NETWORK_MAP,
labelKey: 'harvester.addons.vmMigration.labels.networkMap',
group: 'vmMigration::Advanced',
group: 'vmMigration::Configurations',
namespaced: true,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
@ -1405,7 +1405,7 @@ export function init($plugin, store) {
virtualType({
name: HCI.FORKLIFT_STORAGE_MAP,
labelKey: 'harvester.addons.vmMigration.labels.storageMap',
group: 'vmMigration::Advanced',
group: 'vmMigration::Configurations',
namespaced: true,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
@ -1433,7 +1433,7 @@ export function init($plugin, store) {
virtualType({
name: HCI.FORKLIFT_PLAN,
labelKey: 'harvester.addons.vmMigration.labels.plan',
group: 'vmMigration::Advanced',
group: 'vmMigration::Configurations',
namespaced: true,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
@ -1459,7 +1459,7 @@ export function init($plugin, store) {
virtualType({
name: HCI.FORKLIFT_MIGRATION,
labelKey: 'harvester.addons.vmMigration.labels.migration',
group: 'vmMigration::Advanced',
group: 'vmMigration::Configurations',
namespaced: true,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
@ -1479,11 +1479,16 @@ export function init($plugin, store) {
}
});
// Register the dashboard entry directly — it's a virtual type with no schema,
// so it cannot go through registerAddonSideNav (which filters by schema).
basicType(['forklift-create'], 'vmMigration');
// Enable SideNav based on Forklift Addon Status
// The dashboard entry ('forklift-create') is a schema-less virtual type, so it
// is registered with requireSchema:false to still be gated by the addon status.
registerAddonSideNav(store, PRODUCT_NAME, {
addonName: ADD_ONS.FORKLIFT_OPERATOR,
resourceType: HCI.ADD_ONS,
navGroup: 'vmMigration',
requireSchema: false,
types: ['forklift-create']
});
registerAddonSideNav(store, PRODUCT_NAME, {
addonName: ADD_ONS.FORKLIFT_OPERATOR,
resourceType: HCI.ADD_ONS,
@ -1495,7 +1500,7 @@ export function init($plugin, store) {
registerAddonSideNav(store, PRODUCT_NAME, {
addonName: ADD_ONS.FORKLIFT_OPERATOR,
resourceType: HCI.ADD_ONS,
navGroup: 'vmMigration::Advanced',
navGroup: 'vmMigration::Configurations',
types: [
HCI.FORKLIFT_NETWORK_MAP,
HCI.FORKLIFT_STORAGE_MAP,

View File

@ -36,7 +36,8 @@ nav:
Logging: Logging
'Monitoring and Logging': Monitoring and Logging
vmimport: Virtual Machine Imports
vmMigration: VM Migration
vmMigration: Virtual Machine Migration
Configurations: Configurations
resourceTable:
groupBy:
@ -378,7 +379,7 @@ harvester:
vmMigrationMapSourceProvider: Source Provider
vmMigrationMapDestProvider: Destination Provider
vmMigrationPlanTargetNs: Target Namespace
vmMigrationPlanVmCount: VMs
vmMigrationPlanVmCount: VM Count
vmMigrationMigrationPlan: Plan
tab:
volume: Volumes
@ -1855,7 +1856,7 @@ harvester:
connectionFailed: Connection failed
providerNotReady: Provider not ready
labels:
dashboard: Migrations
dashboard: Migration Overview
provider: Providers
networkMap: Network Maps
storageMap: Storage Maps
@ -1937,8 +1938,8 @@ harvester:
network: Network
datastore: Datastore
dashboard:
title: VMware to Harvester Migration
description: Migrate virtual machines from VMware vSphere to Harvester using cold migration for data consistency. Create a migration plan to select VMs, map networks and storage, and prepare a migration to Harvester.
title: Virtual Machine Migration Overview
description: "Migrate virtual machines from VMware vSphere to Harvester using cold migration for data consistency. Create a migration plan to select VMs, map networks and storage, and prepare a migration to Harvester."
createPlan: Create Migration
tableTitle: Migrations
columns:

View File

@ -14,6 +14,17 @@ export default class ForkliftPlan extends HarvesterResource {
};
}
/**
* The base HarvesterResource sets `doneOverride` to `listLocation`, which would
* force every single-item delete to redirect to the VM Migration dashboard.
* Return undefined so the delete flow stays on the current page (list/dashboard)
* and only falls back to the shell's default navigation when a now-invalid
* detail page is being viewed.
*/
get doneOverride() {
return undefined;
}
get planFailed() {
const conditions = this.status?.conditions || [];
@ -247,7 +258,7 @@ export default class ForkliftPlan extends HarvesterResource {
/**
* Deleting a Plan cascades via ownerReferences set at creation time.
* Kubernetes GC will automatically delete: Migration, NetworkMap, StorageMap, Provider ( Secret).
* Kubernetes GC will automatically delete: Migration, NetworkMap, StorageMap.
* Use foreground propagation to ensure children are deleted before the parent.
*/
remove(opt = {}) {

View File

@ -11,6 +11,8 @@ import { SCHEMA } from '@shell/config/types';
import { useI18n } from '@shell/composables/useI18n';
import { HCI } from '../../../../types';
import { PRODUCT_NAME } from '../../../../config/harvester';
import { ADD_ONS } from '../../../../config/harvester-map';
import { currentRouter } from '../../../../utils/router';
import { FORKLIFT_PLAN_VM_COUNT } from '../../../../config/table-headers';
import { STATE, NAME as NAME_COL, AGE } from '@shell/config/table-headers';
@ -158,6 +160,29 @@ const headers = [
const init = async() => {
try {
// Guard: the Forklift dashboard is only available when the Forklift addon is
// enabled. Redirect back to the Harvester dashboard otherwise (e.g. direct URL).
let forkliftEnabled = false;
if (store.getters[`${ inStore.value }/schemaFor`](HCI.ADD_ONS)) {
const addons = await store.dispatch(`${ inStore.value }/findAll`, { type: HCI.ADD_ONS });
forkliftEnabled = addons.find((a) => a.metadata?.name === ADD_ONS.FORKLIFT_OPERATOR)?.spec?.enabled === true;
}
if (!forkliftEnabled) {
await currentRouter().replace({
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: {
product: store.getters['productId'],
cluster: store.getters['clusterId'],
resource: HCI.DASHBOARD,
},
});
return;
}
await Promise.all([
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_PLAN }),
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP }),
@ -189,9 +214,10 @@ init();
>
<template #subHeader>
<div class="mmt-5">
<p class="text-muted">
{{ t('harvester.addons.vmMigration.dashboard.description') }}
</p>
<p
v-clean-html="t('harvester.addons.vmMigration.dashboard.description')"
class="text-muted"
></p>
</div>
</template>
<template #createButton>
@ -213,11 +239,6 @@ init();
:search="false"
key-field="_key"
>
<template #header-left>
<h3 class="table-title m-0">
{{ t('harvester.addons.vmMigration.dashboard.tableTitle') }}
</h3>
</template>
<template #cell:name="{ row }">
<div class="plan-name-cell">
<div class="plan-name">

View File

@ -8,9 +8,11 @@
* @param {String} config.resourceType - The schema ID for addons.
* @param {String} config.navGroup - The group name in the side nav.
* @param {Array<String>} config.types - Array of Resource IDs to show/hide.
* @param {Boolean} [config.requireSchema=true] - When true, only types with an
* accessible schema are shown. Set false for schema-less virtual types.
*/
export function registerAddonSideNav(store, productName, {
addonName, resourceType, navGroup, types
addonName, resourceType, navGroup, types, requireSchema = true
}) {
if (typeof window === 'undefined') {
return;
@ -57,7 +59,7 @@ export function registerAddonSideNav(store, productName, {
// Adds or removes the resource IDs from the product visibility whitelist.
const setMenuVisibility = (visible) => {
const accessibleTypes = visible ? types.filter(hasAccessibleSchema) : [];
const accessibleTypes = visible ? (requireSchema ? types.filter(hasAccessibleSchema) : types) : [];
// Always clear first to remove any previously-registered types that are
// no longer accessible (e.g. partial permission changes like types=[A,B] where B is dropped).