mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-08-16 04:39:15 +00:00
feat: improve VM migration dashboard mapping labels and VM detail link (#1029)
* feat: improve VM migration dashboard mapping labels and VM detail link - Display Management Network / Ignored labels via i18n in mappings column and review card - Add clickable external-link icon to migrated VM detail page after successful migration - Add tooltip to the VM detail external-link icon Signed-off-by: Andy Lee <andy.lee@suse.com> * refactor: modify duration column width Signed-off-by: Andy Lee <andy.lee@suse.com> * refactor: copilot review comment Signed-off-by: Andy Lee <andy.lee@suse.com> --------- Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
parent
11ff7c8a7c
commit
a10d651827
@ -55,6 +55,18 @@ watch([planName, targetNamespace], ([name, namespace]) => {
|
|||||||
emit('ready', !!name && !!namespace);
|
emit('ready', !!name && !!namespace);
|
||||||
}, { immediate: true });
|
}, { 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 totalVCpu = computed(() => vms.value.reduce((sum, vm) => sum + (vm.cpuCount || vm.numCPU || 0), 0));
|
||||||
|
|
||||||
const totalMemoryGB = computed(() => {
|
const totalMemoryGB = computed(() => {
|
||||||
@ -331,7 +343,7 @@ defineExpose({ startMigration: startMigrationAction });
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<MappingsCell
|
<MappingsCell
|
||||||
:network-entries="vm.networkMappings.map(m => `${m.source} → ${m.target}`)"
|
:network-entries="vm.networkMappings.map(m => `${m.source} → ${formatNetworkTarget(m.target)}`)"
|
||||||
:storage-entries="vm.storageMappings.map(m => `${m.source} → ${m.target}`)"
|
:storage-entries="vm.storageMappings.map(m => `${m.source} → ${m.target}`)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -286,7 +286,7 @@ export const FORKLIFT_PLAN_VM_COUNT = {
|
|||||||
labelKey: 'harvester.tableHeaders.vmMigrationPlanVmCount',
|
labelKey: 'harvester.tableHeaders.vmMigrationPlanVmCount',
|
||||||
value: 'spec.vms.length',
|
value: 'spec.vms.length',
|
||||||
sort: 'spec.vms.length',
|
sort: 'spec.vms.length',
|
||||||
align: 'left',
|
align: 'center',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Plan reference column in forklift.konveyor.io.migration list page
|
// Plan reference column in forklift.konveyor.io.migration list page
|
||||||
|
|||||||
@ -1837,6 +1837,7 @@ harvester:
|
|||||||
unknown: Unknown
|
unknown: Unknown
|
||||||
identifier: Identifier
|
identifier: Identifier
|
||||||
podNetwork: Pod Network
|
podNetwork: Pod Network
|
||||||
|
ignored: Ignored
|
||||||
usedBy: "Used by:"
|
usedBy: "Used by:"
|
||||||
vlan: "VLAN {id}"
|
vlan: "VLAN {id}"
|
||||||
vCpu: "{count} vCPU"
|
vCpu: "{count} vCPU"
|
||||||
@ -1957,6 +1958,7 @@ harvester:
|
|||||||
step: "Step {index}"
|
step: "Step {index}"
|
||||||
vmCount: "{count, plural, one {# VM} other {# VMs}}"
|
vmCount: "{count, plural, one {# VM} other {# VMs}}"
|
||||||
vmId: "id: {id}"
|
vmId: "id: {id}"
|
||||||
|
checkMigratedVm: Click to check migrated VM details
|
||||||
configureMappings:
|
configureMappings:
|
||||||
title: Set Mappings
|
title: Set Mappings
|
||||||
description: Map VMware networks and datastores to Harvester and Longhorn target resources
|
description: Map VMware networks and datastores to Harvester and Longhorn target resources
|
||||||
|
|||||||
@ -37,6 +37,7 @@ const inStore = computed(() => store.getters['currentProduct'].inStore);
|
|||||||
const allPlans = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_PLAN));
|
const allPlans = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_PLAN));
|
||||||
const allNetworkMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_NETWORK_MAP));
|
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 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)
|
const escapeHtml = (value = '') => String(value)
|
||||||
.replaceAll('&', '&')
|
.replaceAll('&', '&')
|
||||||
@ -129,8 +130,8 @@ const rows = computed(() => {
|
|||||||
const netMap = allNetworkMaps.value.find((m) => m.metadata.name === netMapName && m.metadata.namespace === netMapNs);
|
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);
|
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.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 || '-' } → ${ e.destination?.storageClass || '-' }`);
|
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(', ') || '-';
|
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') }`;
|
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 {
|
return {
|
||||||
vmName: vm.name || vm.id || t('harvester.addons.vmMigration.generic.unknown'),
|
vmName: vm.name || vm.id || t('harvester.addons.vmMigration.generic.unknown'),
|
||||||
vmId: vm.id || '',
|
vmId: vm.id || '',
|
||||||
@ -191,6 +210,7 @@ const rows = computed(() => {
|
|||||||
pipelineTooltip: formatPipelineTooltip(pipeline),
|
pipelineTooltip: formatPipelineTooltip(pipeline),
|
||||||
errorMsg,
|
errorMsg,
|
||||||
canceled: plan.planCanceled,
|
canceled: plan.planCanceled,
|
||||||
|
vmDetailLocation,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -248,7 +268,7 @@ const headers = [
|
|||||||
labelKey: 'harvester.addons.vmMigration.dashboard.columns.duration',
|
labelKey: 'harvester.addons.vmMigration.dashboard.columns.duration',
|
||||||
value: 'duration',
|
value: 'duration',
|
||||||
sort: 'durationSeconds',
|
sort: 'durationSeconds',
|
||||||
width: 120,
|
width: 90,
|
||||||
},
|
},
|
||||||
{ ...AGE },
|
{ ...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_NETWORK_MAP }),
|
||||||
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_STORAGE_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) {
|
} catch (e) {
|
||||||
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedLoadPlans')];
|
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedLoadPlans')];
|
||||||
} finally {
|
} finally {
|
||||||
@ -366,7 +397,17 @@ init();
|
|||||||
>
|
>
|
||||||
<div class="vm-progress-header">
|
<div class="vm-progress-header">
|
||||||
<div class="vm-name-block">
|
<div class="vm-name-block">
|
||||||
<span class="vm-name">{{ vm.vmName }}</span>
|
<span class="vm-name">
|
||||||
|
{{ vm.vmName }}
|
||||||
|
<router-link
|
||||||
|
v-if="vm.vmDetailLocation"
|
||||||
|
v-clean-tooltip="t('harvester.addons.vmMigration.dashboard.progress.checkMigratedVm')"
|
||||||
|
:to="vm.vmDetailLocation"
|
||||||
|
class="vm-detail-link"
|
||||||
|
>
|
||||||
|
<i class="icon icon-external-link" />
|
||||||
|
</router-link>
|
||||||
|
</span>
|
||||||
<span class="text-muted vm-id">{{ t('harvester.addons.vmMigration.dashboard.progress.vmId', { id: vm.vmId }) }}</span>
|
<span class="text-muted vm-id">{{ t('harvester.addons.vmMigration.dashboard.progress.vmId', { id: vm.vmId }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -457,6 +498,14 @@ init();
|
|||||||
|
|
||||||
.vm-name {
|
.vm-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-detail-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-name-block {
|
.vm-name-block {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user