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>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { RcItemCard } from '@components/RcItemCard';
|
import { RcItemCard } from '@components/RcItemCard';
|
||||||
@ -18,8 +17,10 @@ const props = defineProps({
|
|||||||
clearable: { type: Boolean, default: false },
|
clearable: { type: Boolean, default: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectOptions = computed(() => {
|
// Only offer "Remove Map" for entries that already have a target selected;
|
||||||
if (!props.clearable) {
|
// entries without a selection just show the regular options.
|
||||||
|
const optionsFor = (entry) => {
|
||||||
|
if (!props.clearable || !entry.target) {
|
||||||
return props.options;
|
return props.options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,14 +35,9 @@ const selectOptions = computed(() => {
|
|||||||
disabled: true,
|
disabled: true,
|
||||||
kind: 'divider',
|
kind: 'divider',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: `${ props.placeholder }:`,
|
|
||||||
disabled: true,
|
|
||||||
kind: 'title',
|
|
||||||
},
|
|
||||||
...props.options,
|
...props.options,
|
||||||
];
|
];
|
||||||
});
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -85,7 +81,7 @@ const selectOptions = computed(() => {
|
|||||||
<div class="mapping-target">
|
<div class="mapping-target">
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="entry.target"
|
v-model:value="entry.target"
|
||||||
:options="selectOptions"
|
:options="optionsFor(entry)"
|
||||||
:placeholder="placeholder+'...'"
|
:placeholder="placeholder+'...'"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1893,7 +1893,7 @@ harvester:
|
|||||||
providerSelect: Provider
|
providerSelect: Provider
|
||||||
createNew: Create new provider
|
createNew: Create new provider
|
||||||
requirementsTitle: Requirements
|
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
|
name: Name
|
||||||
urlLabel: vCenter/ESXi URL
|
urlLabel: vCenter/ESXi URL
|
||||||
urlPlaceholder: "https://vcenter.example.com/sdk"
|
urlPlaceholder: "https://vcenter.example.com/sdk"
|
||||||
@ -1965,7 +1965,7 @@ harvester:
|
|||||||
description: Map VMware networks and datastores to Harvester and Longhorn target resources
|
description: Map VMware networks and datastores to Harvester and Longhorn target resources
|
||||||
save: Save Mappings and Continue
|
save: Save Mappings and Continue
|
||||||
noTemplate: Start from scratch
|
noTemplate: Start from scratch
|
||||||
removeMap: Remove Map
|
removeMap: Remove mapping
|
||||||
networkMapping:
|
networkMapping:
|
||||||
title: Network Mapping
|
title: Network Mapping
|
||||||
description: Map VMware port groups to Harvester networks
|
description: Map VMware port groups to Harvester networks
|
||||||
@ -1977,7 +1977,7 @@ harvester:
|
|||||||
storageMapping:
|
storageMapping:
|
||||||
title: Storage Mapping
|
title: Storage Mapping
|
||||||
description: Map VMware datastores to Harvester storage classes
|
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
|
template: Use existing storage mapping as template
|
||||||
reviewMigration:
|
reviewMigration:
|
||||||
title: Review Migration Plan
|
title: Review Migration Plan
|
||||||
|
|||||||
@ -179,8 +179,10 @@ const rows = computed(() => {
|
|||||||
|
|
||||||
overallProgress = Math.round(overallProgress * 10) / 10;
|
overallProgress = Math.round(overallProgress * 10) / 10;
|
||||||
|
|
||||||
// If no step-level error but the plan itself is failed, surface it
|
// If no step-level error but the plan itself is failed, surface it.
|
||||||
if (!errorMsg && plan.planFailed) {
|
// 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') }`;
|
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 cluster = routeParams.cluster || store.getters['clusterId'];
|
||||||
const vmNameCandidates = [vm.targetName, vm.name, vm.id].filter(Boolean);
|
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 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 ? {
|
const vmDetailLocation = canNavigateToVm ? {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource-namespace-id`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource-namespace-id`,
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@ -68,40 +68,28 @@ export default defineComponent({
|
|||||||
return (this.value || []).map((plan) => {
|
return (this.value || []).map((plan) => {
|
||||||
const planName = plan?.metadata?.name || '-';
|
const planName = plan?.metadata?.name || '-';
|
||||||
const namespace = plan?.metadata?.namespace || '';
|
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
|
const matchedNetworkMap = existing[HCI.FORKLIFT_NETWORK_MAP].find((resource) => this.ownedByPlan(resource, plan));
|
||||||
.map((entry) => ({
|
const matchedStorageMap = existing[HCI.FORKLIFT_STORAGE_MAP].find((resource) => this.ownedByPlan(resource, plan));
|
||||||
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));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
planName,
|
planName,
|
||||||
namespace,
|
namespace,
|
||||||
migrations: [...migrationsMap.values()],
|
migrations,
|
||||||
networkMap: matchedNetworkMap ? {
|
networkMap: matchedNetworkMap ? {
|
||||||
namespace: networkMap.namespace || namespace,
|
namespace: matchedNetworkMap?.metadata?.namespace || namespace,
|
||||||
name: networkMap.name,
|
name: matchedNetworkMap?.metadata?.name,
|
||||||
} : null,
|
} : null,
|
||||||
storageMap: matchedStorageMap ? {
|
storageMap: matchedStorageMap ? {
|
||||||
namespace: storageMap.namespace || namespace,
|
namespace: matchedStorageMap?.metadata?.namespace || namespace,
|
||||||
name: storageMap.name,
|
name: matchedStorageMap?.metadata?.name,
|
||||||
} : null,
|
} : null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -125,53 +113,57 @@ export default defineComponent({
|
|||||||
methods: {
|
methods: {
|
||||||
resourceNames,
|
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() {
|
buildDeleteTargets() {
|
||||||
|
const existing = this.existingRelatedResources;
|
||||||
const targets = new Map();
|
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 || []) {
|
for (const plan of this.value || []) {
|
||||||
const namespace = plan?.metadata?.namespace || '';
|
existing[HCI.FORKLIFT_NETWORK_MAP]
|
||||||
const networkMap = plan?.spec?.map?.network;
|
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||||
const storageMap = plan?.spec?.map?.storage;
|
.forEach((resource) => addTarget(HCI.FORKLIFT_NETWORK_MAP, resource));
|
||||||
const history = plan?.status?.migration?.history || [];
|
|
||||||
|
|
||||||
if (networkMap?.name) {
|
existing[HCI.FORKLIFT_STORAGE_MAP]
|
||||||
const ns = networkMap.namespace || namespace;
|
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||||
const key = `${ HCI.FORKLIFT_NETWORK_MAP }|${ ns }|${ networkMap.name }`;
|
.forEach((resource) => addTarget(HCI.FORKLIFT_STORAGE_MAP, resource));
|
||||||
|
|
||||||
targets.set(key, {
|
existing[HCI.FORKLIFT_MIGRATION]
|
||||||
type: HCI.FORKLIFT_NETWORK_MAP,
|
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||||
name: networkMap.name,
|
.forEach((resource) => addTarget(HCI.FORKLIFT_MIGRATION, resource));
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...targets.values()];
|
return [...targets.values()];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user