mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-08-16 12:49:14 +00:00
* refactor: modify relatedResources using ownerReferences uid * refactor: wordings * fix(vm-migration): keep completed VMs green and linkable in failed plans When a plan fails overall, VMs that already reached 100% were marked with an error: their progress bar turned red while the label still read "Finished Successfully", and the migrated-VM detail link was hidden. Skip the plan-level failure fallback for VMs at 100% and drop the !planFailed guard from the detail-link condition so successfully migrated VMs stay green and stay navigable. --------- (cherry picked from commit 2d747e435aebd0958f355535a13f4aeb7d0991ba) Signed-off-by: Andy Lee <andy.lee@suse.com> Co-authored-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
parent
8815c70361
commit
eb917c9d18
@ -1,5 +1,4 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { RcItemCard } from '@components/RcItemCard';
|
||||
@ -18,8 +17,10 @@ const props = defineProps({
|
||||
clearable: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const selectOptions = computed(() => {
|
||||
if (!props.clearable) {
|
||||
// Only offer "Remove Map" for entries that already have a target selected;
|
||||
// entries without a selection just show the regular options.
|
||||
const optionsFor = (entry) => {
|
||||
if (!props.clearable || !entry.target) {
|
||||
return props.options;
|
||||
}
|
||||
|
||||
@ -34,14 +35,9 @@ const selectOptions = computed(() => {
|
||||
disabled: true,
|
||||
kind: 'divider',
|
||||
},
|
||||
{
|
||||
label: `${ props.placeholder }:`,
|
||||
disabled: true,
|
||||
kind: 'title',
|
||||
},
|
||||
...props.options,
|
||||
];
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -85,7 +81,7 @@ const selectOptions = computed(() => {
|
||||
<div class="mapping-target">
|
||||
<LabeledSelect
|
||||
v-model:value="entry.target"
|
||||
:options="selectOptions"
|
||||
:options="optionsFor(entry)"
|
||||
:placeholder="placeholder+'...'"
|
||||
:searchable="true"
|
||||
/>
|
||||
|
||||
@ -1893,7 +1893,7 @@ harvester:
|
||||
providerSelect: Provider
|
||||
createNew: Create new provider
|
||||
requirementsTitle: Requirements
|
||||
requirementsText: "To migrate virtual machines from VMware, you will need a vCenter Server running version 6.5 or higher with a user account that has at least read permissions, and the source environment must be network-accessible from Rancher."
|
||||
requirementsText: "To migrate virtual machines from VMware, you will need a vCenter Server running version 6.5 or higher with a user account that has at least read permissions, and the source environment must be network-accessible from Harvester Node."
|
||||
name: Name
|
||||
urlLabel: vCenter/ESXi URL
|
||||
urlPlaceholder: "https://vcenter.example.com/sdk"
|
||||
@ -1965,7 +1965,7 @@ harvester:
|
||||
description: Map VMware networks and datastores to Harvester and Longhorn target resources
|
||||
save: Save Mappings and Continue
|
||||
noTemplate: Start from scratch
|
||||
removeMap: Remove Map
|
||||
removeMap: Remove mapping
|
||||
networkMapping:
|
||||
title: Network Mapping
|
||||
description: Map VMware port groups to Harvester networks
|
||||
@ -1977,7 +1977,7 @@ harvester:
|
||||
storageMapping:
|
||||
title: Storage Mapping
|
||||
description: Map VMware datastores to Harvester storage classes
|
||||
placeholder: Choose a Longhorn Storage Volume
|
||||
placeholder: Choose a Harvester Storage Class
|
||||
template: Use existing storage mapping as template
|
||||
reviewMigration:
|
||||
title: Review Migration Plan
|
||||
|
||||
@ -179,8 +179,10 @@ const rows = computed(() => {
|
||||
|
||||
overallProgress = Math.round(overallProgress * 10) / 10;
|
||||
|
||||
// If no step-level error but the plan itself is failed, surface it
|
||||
if (!errorMsg && plan.planFailed) {
|
||||
// If no step-level error but the plan itself is failed, surface it.
|
||||
// Skip VMs that already reached 100% (they finished successfully before
|
||||
// the plan failed) so their bar stays green instead of turning red.
|
||||
if (!errorMsg && plan.planFailed && overallProgress < 100) {
|
||||
errorMsg = `${ currentStep || t('harvester.addons.vmMigration.dashboard.progress.migration') }: ${ t('harvester.addons.vmMigration.dashboard.progress.failed') }`;
|
||||
}
|
||||
|
||||
@ -190,7 +192,11 @@ const rows = computed(() => {
|
||||
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;
|
||||
// A VM that individually reached 100% without error should be navigable
|
||||
// even when the overall plan failed (other VMs in the plan may have
|
||||
// failed); `overallProgress >= 100 && !errorMsg && targetVm` already
|
||||
// guarantees this VM migrated successfully.
|
||||
const canNavigateToVm = overallProgress >= 100 && !errorMsg && !plan.planCanceled && !!targetVm && !!product && !!cluster;
|
||||
const vmDetailLocation = canNavigateToVm ? {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource-namespace-id`,
|
||||
params: {
|
||||
|
||||
@ -68,40 +68,28 @@ export default defineComponent({
|
||||
return (this.value || []).map((plan) => {
|
||||
const planName = plan?.metadata?.name || '-';
|
||||
const namespace = plan?.metadata?.namespace || '';
|
||||
const networkMap = plan?.spec?.map?.network;
|
||||
const storageMap = plan?.spec?.map?.storage;
|
||||
const history = plan?.status?.migration?.history || [];
|
||||
|
||||
const migrationsMap = new Map();
|
||||
const migrations = existing[HCI.FORKLIFT_MIGRATION]
|
||||
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||
.map((resource) => ({
|
||||
name: resource?.metadata?.name,
|
||||
namespace: resource?.metadata?.namespace || namespace,
|
||||
}));
|
||||
|
||||
history
|
||||
.map((entry) => ({
|
||||
name: entry?.migration?.name,
|
||||
namespace: entry?.migration?.namespace || namespace,
|
||||
}))
|
||||
.filter((entry) => !!entry.name)
|
||||
.forEach((entry) => {
|
||||
const matched = existing[HCI.FORKLIFT_MIGRATION].find((resource) => resource?.metadata?.name === entry.name && resource?.metadata?.namespace === entry.namespace);
|
||||
|
||||
if (matched) {
|
||||
migrationsMap.set(`${ entry.namespace }/${ entry.name }`, entry);
|
||||
}
|
||||
});
|
||||
|
||||
const matchedNetworkMap = networkMap?.name && existing[HCI.FORKLIFT_NETWORK_MAP].find((resource) => resource?.metadata?.name === networkMap.name && resource?.metadata?.namespace === (networkMap.namespace || namespace));
|
||||
const matchedStorageMap = storageMap?.name && existing[HCI.FORKLIFT_STORAGE_MAP].find((resource) => resource?.metadata?.name === storageMap.name && resource?.metadata?.namespace === (storageMap.namespace || namespace));
|
||||
const matchedNetworkMap = existing[HCI.FORKLIFT_NETWORK_MAP].find((resource) => this.ownedByPlan(resource, plan));
|
||||
const matchedStorageMap = existing[HCI.FORKLIFT_STORAGE_MAP].find((resource) => this.ownedByPlan(resource, plan));
|
||||
|
||||
return {
|
||||
planName,
|
||||
namespace,
|
||||
migrations: [...migrationsMap.values()],
|
||||
migrations,
|
||||
networkMap: matchedNetworkMap ? {
|
||||
namespace: networkMap.namespace || namespace,
|
||||
name: networkMap.name,
|
||||
namespace: matchedNetworkMap?.metadata?.namespace || namespace,
|
||||
name: matchedNetworkMap?.metadata?.name,
|
||||
} : null,
|
||||
storageMap: matchedStorageMap ? {
|
||||
namespace: storageMap.namespace || namespace,
|
||||
name: storageMap.name,
|
||||
namespace: matchedStorageMap?.metadata?.namespace || namespace,
|
||||
name: matchedStorageMap?.metadata?.name,
|
||||
} : null,
|
||||
};
|
||||
});
|
||||
@ -125,53 +113,57 @@ export default defineComponent({
|
||||
methods: {
|
||||
resourceNames,
|
||||
|
||||
/**
|
||||
* Determine whether a related resource (migration / network map / storage map)
|
||||
* is owned by the given plan by inspecting its `metadata.ownerReferences`.
|
||||
* Matches on `kind: Plan` and the plan name; when both sides expose a uid it
|
||||
* must match too, to disambiguate same-named plans.
|
||||
*/
|
||||
ownedByPlan(resource, plan) {
|
||||
const planName = plan?.metadata?.name;
|
||||
const planUid = plan?.metadata?.uid;
|
||||
const owners = resource?.metadata?.ownerReferences || [];
|
||||
|
||||
if (!planName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return owners.some((owner) => owner?.kind === 'Plan' &&
|
||||
owner?.name === planName &&
|
||||
(!planUid || !owner?.uid || owner.uid === planUid));
|
||||
},
|
||||
|
||||
buildDeleteTargets() {
|
||||
const existing = this.existingRelatedResources;
|
||||
const targets = new Map();
|
||||
|
||||
const addTarget = (type, resource) => {
|
||||
const name = resource?.metadata?.name;
|
||||
const ns = resource?.metadata?.namespace;
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `${ type }|${ ns }|${ name }`;
|
||||
|
||||
targets.set(key, {
|
||||
type, name, namespace: ns
|
||||
});
|
||||
};
|
||||
|
||||
for (const plan of this.value || []) {
|
||||
const namespace = plan?.metadata?.namespace || '';
|
||||
const networkMap = plan?.spec?.map?.network;
|
||||
const storageMap = plan?.spec?.map?.storage;
|
||||
const history = plan?.status?.migration?.history || [];
|
||||
existing[HCI.FORKLIFT_NETWORK_MAP]
|
||||
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||
.forEach((resource) => addTarget(HCI.FORKLIFT_NETWORK_MAP, resource));
|
||||
|
||||
if (networkMap?.name) {
|
||||
const ns = networkMap.namespace || namespace;
|
||||
const key = `${ HCI.FORKLIFT_NETWORK_MAP }|${ ns }|${ networkMap.name }`;
|
||||
existing[HCI.FORKLIFT_STORAGE_MAP]
|
||||
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||
.forEach((resource) => addTarget(HCI.FORKLIFT_STORAGE_MAP, resource));
|
||||
|
||||
targets.set(key, {
|
||||
type: HCI.FORKLIFT_NETWORK_MAP,
|
||||
name: networkMap.name,
|
||||
namespace: ns,
|
||||
});
|
||||
}
|
||||
|
||||
if (storageMap?.name) {
|
||||
const ns = storageMap.namespace || namespace;
|
||||
const key = `${ HCI.FORKLIFT_STORAGE_MAP }|${ ns }|${ storageMap.name }`;
|
||||
|
||||
targets.set(key, {
|
||||
type: HCI.FORKLIFT_STORAGE_MAP,
|
||||
name: storageMap.name,
|
||||
namespace: ns,
|
||||
});
|
||||
}
|
||||
|
||||
for (const entry of history) {
|
||||
const migrationName = entry?.migration?.name;
|
||||
|
||||
if (!migrationName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const migrationNs = entry?.migration?.namespace || namespace;
|
||||
const key = `${ HCI.FORKLIFT_MIGRATION }|${ migrationNs }|${ migrationName }`;
|
||||
|
||||
targets.set(key, {
|
||||
type: HCI.FORKLIFT_MIGRATION,
|
||||
name: migrationName,
|
||||
namespace: migrationNs,
|
||||
});
|
||||
}
|
||||
existing[HCI.FORKLIFT_MIGRATION]
|
||||
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||
.forEach((resource) => addTarget(HCI.FORKLIFT_MIGRATION, resource));
|
||||
}
|
||||
|
||||
return [...targets.values()];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user