diff --git a/pkg/harvester/components/vm-migration/ReviewMigrationStep.vue b/pkg/harvester/components/vm-migration/ReviewMigrationStep.vue index 2bdd4c3a..dd57e8e2 100644 --- a/pkg/harvester/components/vm-migration/ReviewMigrationStep.vue +++ b/pkg/harvester/components/vm-migration/ReviewMigrationStep.vue @@ -55,6 +55,18 @@ watch([planName, targetNamespace], ([name, namespace]) => { emit('ready', !!name && !!namespace); }, { immediate: true }); +const formatNetworkTarget = (target = '') => { + if (target === 'pod') { + return t('harvester.addons.vmMigration.configureMappings.networkMapping.options.podNetworking'); + } + + if (target === 'ignored') { + return t('harvester.addons.vmMigration.generic.ignored'); + } + + return target; +}; + const totalVCpu = computed(() => vms.value.reduce((sum, vm) => sum + (vm.cpuCount || vm.numCPU || 0), 0)); const totalMemoryGB = computed(() => { @@ -331,7 +343,7 @@ defineExpose({ startMigration: startMigrationAction }); diff --git a/pkg/harvester/config/table-headers.js b/pkg/harvester/config/table-headers.js index 5615a6a4..c93fb920 100644 --- a/pkg/harvester/config/table-headers.js +++ b/pkg/harvester/config/table-headers.js @@ -286,7 +286,7 @@ export const FORKLIFT_PLAN_VM_COUNT = { labelKey: 'harvester.tableHeaders.vmMigrationPlanVmCount', value: 'spec.vms.length', sort: 'spec.vms.length', - align: 'left', + align: 'center', }; // Plan reference column in forklift.konveyor.io.migration list page diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index 741df89b..aa162f72 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -1837,6 +1837,7 @@ harvester: unknown: Unknown identifier: Identifier podNetwork: Pod Network + ignored: Ignored usedBy: "Used by:" vlan: "VLAN {id}" vCpu: "{count} vCPU" @@ -1957,6 +1958,7 @@ harvester: step: "Step {index}" vmCount: "{count, plural, one {# VM} other {# VMs}}" vmId: "id: {id}" + checkMigratedVm: Click to check migrated VM details configureMappings: title: Set Mappings description: Map VMware networks and datastores to Harvester and Longhorn target resources diff --git a/pkg/harvester/pages/c/_cluster/vm-migration/index.vue b/pkg/harvester/pages/c/_cluster/vm-migration/index.vue index f325f8f1..ea0ecb4f 100644 --- a/pkg/harvester/pages/c/_cluster/vm-migration/index.vue +++ b/pkg/harvester/pages/c/_cluster/vm-migration/index.vue @@ -37,6 +37,7 @@ const inStore = computed(() => store.getters['currentProduct'].inStore); const allPlans = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_PLAN)); const allNetworkMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_NETWORK_MAP)); const allStorageMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_STORAGE_MAP)); +const allVMs = computed(() => store.getters[`${ inStore.value }/all`](HCI.VM)); const escapeHtml = (value = '') => String(value) .replaceAll('&', '&') @@ -129,8 +130,8 @@ const rows = computed(() => { const netMap = allNetworkMaps.value.find((m) => m.metadata.name === netMapName && m.metadata.namespace === netMapNs); const storMap = allStorageMaps.value.find((m) => m.metadata.name === storMapName && m.metadata.namespace === storMapNs); - plan.networkEntries = (netMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.type === 'pod' ? t('harvester.addons.vmMigration.generic.podNetwork') : (e.destination?.name || '-') }`); - plan.storageEntries = (storMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.storageClass || '-' }`); + plan.networkEntries = (netMap?.spec?.map || []).map((e) => `${ e.source?.id || 'Ignored' } → ${ e.destination?.type === 'pod' ? t('harvester.addons.vmMigration.configureMappings.networkMapping.options.podNetworking') : (e.destination?.name || 'Ignored') }`); + plan.storageEntries = (storMap?.spec?.map || []).map((e) => `${ e.source?.id || 'Ignored' } → ${ e.destination?.storageClass || 'Ignored' }`); plan.vmIdsDisplay = (plan.spec?.vms || []).map((vm) => vm.id || vm.name || '').filter(Boolean).join(', ') || '-'; @@ -183,6 +184,24 @@ const rows = computed(() => { errorMsg = `${ currentStep || t('harvester.addons.vmMigration.dashboard.progress.migration') }: ${ t('harvester.addons.vmMigration.dashboard.progress.failed') }`; } + const vmNamespace = vm.namespace || plan.spec?.targetNamespace || plan.metadata?.namespace; + const routeParams = currentRouter().currentRoute?.value?.params || {}; + const product = routeParams.product || store.getters['productId']; + const cluster = routeParams.cluster || store.getters['clusterId']; + const vmNameCandidates = [vm.targetName, vm.name, vm.id].filter(Boolean); + const targetVm = allVMs.value.find((item) => vmNameCandidates.includes(item.metadata?.name) && (!vmNamespace || item.metadata?.namespace === vmNamespace)); + const canNavigateToVm = overallProgress >= 100 && !errorMsg && !plan.planFailed && !plan.planCanceled && !!targetVm && !!product && !!cluster; + const vmDetailLocation = canNavigateToVm ? { + name: `${ PRODUCT_NAME }-c-cluster-resource-namespace-id`, + params: { + product, + cluster, + resource: HCI.VM, + namespace: targetVm.metadata.namespace, + id: targetVm.metadata.name, + } + } : null; + return { vmName: vm.name || vm.id || t('harvester.addons.vmMigration.generic.unknown'), vmId: vm.id || '', @@ -191,6 +210,7 @@ const rows = computed(() => { pipelineTooltip: formatPipelineTooltip(pipeline), errorMsg, canceled: plan.planCanceled, + vmDetailLocation, }; }); @@ -248,7 +268,7 @@ const headers = [ labelKey: 'harvester.addons.vmMigration.dashboard.columns.duration', value: 'duration', sort: 'durationSeconds', - width: 120, + width: 90, }, { ...AGE }, ]; @@ -283,6 +303,17 @@ const init = async() => { store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP }), store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_STORAGE_MAP }), ]); + + // VMs are only needed to conditionally show the post-migration VM-detail + // link, so load them separately and non-fatally: a failure here (or the + // cost of findAll on large clusters) should not block plans/maps from + // rendering. + try { + await store.dispatch(`${ inStore.value }/findAll`, { type: HCI.VM }); + } catch (e) { + // Intentionally ignored: the dashboard still works without the VM list, + // the migrated VM-detail link simply won't be shown. + } } catch (e) { errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedLoadPlans')]; } finally { @@ -366,7 +397,17 @@ init(); >
- {{ vm.vmName }} + + {{ vm.vmName }} + + + + {{ t('harvester.addons.vmMigration.dashboard.progress.vmId', { id: vm.vmId }) }}
@@ -457,6 +498,14 @@ init(); .vm-name { font-size: 14px; + display: inline-flex; + align-items: center; + gap: 4px; + } + + .vm-detail-link { + display: inline-flex; + align-items: center; } .vm-name-block {