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();
>
@@ -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 {