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"> <div v-if="!createOnly && !editMode">
<LabeledSelect <LabeledSelect
v-model:value="selectedProvider" v-model:value="selectedProvider"
required
:label="t('harvester.addons.vmMigration.configureProvider.providerSelect')" :label="t('harvester.addons.vmMigration.configureProvider.providerSelect')"
:options="providerOptions" :options="providerOptions"
:reduce="(opt) => opt.value" :reduce="(opt) => opt.value"

View File

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

View File

@ -1378,7 +1378,7 @@ export function init($plugin, store) {
virtualType({ virtualType({
name: HCI.FORKLIFT_NETWORK_MAP, name: HCI.FORKLIFT_NETWORK_MAP,
labelKey: 'harvester.addons.vmMigration.labels.networkMap', labelKey: 'harvester.addons.vmMigration.labels.networkMap',
group: 'vmMigration::Advanced', group: 'vmMigration::Configurations',
namespaced: true, namespaced: true,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
@ -1405,7 +1405,7 @@ export function init($plugin, store) {
virtualType({ virtualType({
name: HCI.FORKLIFT_STORAGE_MAP, name: HCI.FORKLIFT_STORAGE_MAP,
labelKey: 'harvester.addons.vmMigration.labels.storageMap', labelKey: 'harvester.addons.vmMigration.labels.storageMap',
group: 'vmMigration::Advanced', group: 'vmMigration::Configurations',
namespaced: true, namespaced: true,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
@ -1433,7 +1433,7 @@ export function init($plugin, store) {
virtualType({ virtualType({
name: HCI.FORKLIFT_PLAN, name: HCI.FORKLIFT_PLAN,
labelKey: 'harvester.addons.vmMigration.labels.plan', labelKey: 'harvester.addons.vmMigration.labels.plan',
group: 'vmMigration::Advanced', group: 'vmMigration::Configurations',
namespaced: true, namespaced: true,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
@ -1459,7 +1459,7 @@ export function init($plugin, store) {
virtualType({ virtualType({
name: HCI.FORKLIFT_MIGRATION, name: HCI.FORKLIFT_MIGRATION,
labelKey: 'harvester.addons.vmMigration.labels.migration', labelKey: 'harvester.addons.vmMigration.labels.migration',
group: 'vmMigration::Advanced', group: 'vmMigration::Configurations',
namespaced: true, namespaced: true,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, 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 // 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, { registerAddonSideNav(store, PRODUCT_NAME, {
addonName: ADD_ONS.FORKLIFT_OPERATOR, addonName: ADD_ONS.FORKLIFT_OPERATOR,
resourceType: HCI.ADD_ONS, resourceType: HCI.ADD_ONS,
@ -1495,7 +1500,7 @@ export function init($plugin, store) {
registerAddonSideNav(store, PRODUCT_NAME, { registerAddonSideNav(store, PRODUCT_NAME, {
addonName: ADD_ONS.FORKLIFT_OPERATOR, addonName: ADD_ONS.FORKLIFT_OPERATOR,
resourceType: HCI.ADD_ONS, resourceType: HCI.ADD_ONS,
navGroup: 'vmMigration::Advanced', navGroup: 'vmMigration::Configurations',
types: [ types: [
HCI.FORKLIFT_NETWORK_MAP, HCI.FORKLIFT_NETWORK_MAP,
HCI.FORKLIFT_STORAGE_MAP, HCI.FORKLIFT_STORAGE_MAP,

View File

@ -36,7 +36,8 @@ nav:
Logging: Logging Logging: Logging
'Monitoring and Logging': Monitoring and Logging 'Monitoring and Logging': Monitoring and Logging
vmimport: Virtual Machine Imports vmimport: Virtual Machine Imports
vmMigration: VM Migration vmMigration: Virtual Machine Migration
Configurations: Configurations
resourceTable: resourceTable:
groupBy: groupBy:
@ -378,7 +379,7 @@ harvester:
vmMigrationMapSourceProvider: Source Provider vmMigrationMapSourceProvider: Source Provider
vmMigrationMapDestProvider: Destination Provider vmMigrationMapDestProvider: Destination Provider
vmMigrationPlanTargetNs: Target Namespace vmMigrationPlanTargetNs: Target Namespace
vmMigrationPlanVmCount: VMs vmMigrationPlanVmCount: VM Count
vmMigrationMigrationPlan: Plan vmMigrationMigrationPlan: Plan
tab: tab:
volume: Volumes volume: Volumes
@ -1855,7 +1856,7 @@ harvester:
connectionFailed: Connection failed connectionFailed: Connection failed
providerNotReady: Provider not ready providerNotReady: Provider not ready
labels: labels:
dashboard: Migrations dashboard: Migration Overview
provider: Providers provider: Providers
networkMap: Network Maps networkMap: Network Maps
storageMap: Storage Maps storageMap: Storage Maps
@ -1937,8 +1938,8 @@ harvester:
network: Network network: Network
datastore: Datastore datastore: Datastore
dashboard: dashboard:
title: VMware to Harvester Migration 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. 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 createPlan: Create Migration
tableTitle: Migrations tableTitle: Migrations
columns: 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() { get planFailed() {
const conditions = this.status?.conditions || []; 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. * 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. * Use foreground propagation to ensure children are deleted before the parent.
*/ */
remove(opt = {}) { remove(opt = {}) {

View File

@ -11,6 +11,8 @@ import { SCHEMA } from '@shell/config/types';
import { useI18n } from '@shell/composables/useI18n'; import { useI18n } from '@shell/composables/useI18n';
import { HCI } from '../../../../types'; import { HCI } from '../../../../types';
import { PRODUCT_NAME } from '../../../../config/harvester'; 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 { FORKLIFT_PLAN_VM_COUNT } from '../../../../config/table-headers';
import { STATE, NAME as NAME_COL, AGE } from '@shell/config/table-headers'; import { STATE, NAME as NAME_COL, AGE } from '@shell/config/table-headers';
@ -158,6 +160,29 @@ const headers = [
const init = async() => { const init = async() => {
try { 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([ await Promise.all([
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_PLAN }), store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_PLAN }),
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP }), store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP }),
@ -189,9 +214,10 @@ init();
> >
<template #subHeader> <template #subHeader>
<div class="mmt-5"> <div class="mmt-5">
<p class="text-muted"> <p
{{ t('harvester.addons.vmMigration.dashboard.description') }} v-clean-html="t('harvester.addons.vmMigration.dashboard.description')"
</p> class="text-muted"
></p>
</div> </div>
</template> </template>
<template #createButton> <template #createButton>
@ -213,11 +239,6 @@ init();
:search="false" :search="false"
key-field="_key" key-field="_key"
> >
<template #header-left>
<h3 class="table-title m-0">
{{ t('harvester.addons.vmMigration.dashboard.tableTitle') }}
</h3>
</template>
<template #cell:name="{ row }"> <template #cell:name="{ row }">
<div class="plan-name-cell"> <div class="plan-name-cell">
<div class="plan-name"> <div class="plan-name">

View File

@ -8,9 +8,11 @@
* @param {String} config.resourceType - The schema ID for addons. * @param {String} config.resourceType - The schema ID for addons.
* @param {String} config.navGroup - The group name in the side nav. * @param {String} config.navGroup - The group name in the side nav.
* @param {Array<String>} config.types - Array of Resource IDs to show/hide. * @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, { export function registerAddonSideNav(store, productName, {
addonName, resourceType, navGroup, types addonName, resourceType, navGroup, types, requireSchema = true
}) { }) {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return; return;
@ -57,7 +59,7 @@ export function registerAddonSideNav(store, productName, {
// Adds or removes the resource IDs from the product visibility whitelist. // Adds or removes the resource IDs from the product visibility whitelist.
const setMenuVisibility = (visible) => { 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 // 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). // no longer accessible (e.g. partial permission changes like types=[A,B] where B is dropped).