fix: bugs in VM selection table

Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
Andy Lee 2026-07-31 14:25:18 +08:00
parent facddcb9a7
commit 3817269c0a
No known key found for this signature in database
GPG Key ID: 39DC4436AE3564D5
4 changed files with 79 additions and 45 deletions

View File

@ -126,6 +126,10 @@ const applyStorageMapTargets = (mapSpec, { markOverridden = false, captureInheri
if (match?.destination?.storageClass) { if (match?.destination?.storageClass) {
entry.target = match.destination.storageClass; entry.target = match.destination.storageClass;
if (captureInherited) {
entry.inheritedFromProvider = true;
}
if (match.destination.volumeMode) { if (match.destination.volumeMode) {
entry.volumeMode = match.destination.volumeMode; entry.volumeMode = match.destination.volumeMode;
@ -252,18 +256,19 @@ const buildStorageEntries = () => {
if (ds && ds.id) { if (ds && ds.id) {
if (!datastoreMap[ds.id]) { if (!datastoreMap[ds.id]) {
datastoreMap[ds.id] = { datastoreMap[ds.id] = {
name: ds.name || t('harvester.addons.vmMigration.generic.unknown'), name: ds.name || t('harvester.addons.vmMigration.generic.unknown'),
id: ds.id, id: ds.id,
type: ds.type || '', type: ds.type || '',
capacity: 0, capacity: 0,
target: '', target: '',
volumeMode: DEFAULT_VOLUME_MODE, volumeMode: DEFAULT_VOLUME_MODE,
accessModes: [...DEFAULT_ACCESS_MODES], accessModes: [...DEFAULT_ACCESS_MODES],
inheritedVolumeMode: DEFAULT_VOLUME_MODE, inheritedVolumeMode: DEFAULT_VOLUME_MODE,
inheritedAccessModes: [...DEFAULT_ACCESS_MODES], inheritedAccessModes: [...DEFAULT_ACCESS_MODES],
overridden: false, inheritedFromProvider: false,
usedBy: [], overridden: false,
_key: `stor-${ ds.id }`, usedBy: [],
_key: `stor-${ ds.id }`,
}; };
} }
@ -293,18 +298,19 @@ const buildNetworkEntriesFromProvider = (networksData) => {
const buildStorageEntriesFromProvider = (datastoresData) => { const buildStorageEntriesFromProvider = (datastoresData) => {
storageEntries.value = (Array.isArray(datastoresData) ? datastoresData : []).map((ds) => ({ storageEntries.value = (Array.isArray(datastoresData) ? datastoresData : []).map((ds) => ({
name: ds.name || ds.id, name: ds.name || ds.id,
id: ds.id || '', id: ds.id || '',
type: ds.type || '', type: ds.type || '',
capacity: ds.capacity || 0, capacity: ds.capacity || 0,
target: '', target: '',
volumeMode: DEFAULT_VOLUME_MODE, volumeMode: DEFAULT_VOLUME_MODE,
accessModes: [...DEFAULT_ACCESS_MODES], accessModes: [...DEFAULT_ACCESS_MODES],
inheritedVolumeMode: DEFAULT_VOLUME_MODE, inheritedVolumeMode: DEFAULT_VOLUME_MODE,
inheritedAccessModes: [...DEFAULT_ACCESS_MODES], inheritedAccessModes: [...DEFAULT_ACCESS_MODES],
overridden: false, inheritedFromProvider: false,
usedBy: [], overridden: false,
_key: `stor-${ ds.id || ds.name }`, usedBy: [],
_key: `stor-${ ds.id || ds.name }`,
})); }));
}; };

View File

@ -108,7 +108,7 @@ const formatModes = (entry) => {
<div class="storage-defaults-info"> <div class="storage-defaults-info">
<span class="storage-defaults-summary">{{ formatModes(entry) }}</span> <span class="storage-defaults-summary">{{ formatModes(entry) }}</span>
<span <span
v-if="inheritedProviderName && !entry.overridden" v-if="inheritedProviderName && entry.inheritedFromProvider && !entry.overridden"
class="text-deemphasized storage-defaults-inherited" class="text-deemphasized storage-defaults-inherited"
> >
{{ t('harvester.addons.vmMigration.storageDefaults.inherited', { provider: inheritedProviderName }) }} {{ t('harvester.addons.vmMigration.storageDefaults.inherited', { provider: inheritedProviderName }) }}

View File

@ -25,8 +25,9 @@ const { discoveredVMs, selectedVMIds, tableRows } = toRefs(props.stepData);
const selectedVMs = ref([]); const selectedVMs = ref([]);
const loading = ref(true); const loading = ref(true);
const networkMap = ref({}); // Restored from stepData so friendly names survive step navigation / remount.
const datastoreMap = ref({}); const networkMap = ref(props.stepData.networkMap || {});
const datastoreMap = ref(props.stepData.datastoreMap || {});
const sortableTableRef = ref(null); const sortableTableRef = ref(null);
const allVMsSelected = ref(false); const allVMsSelected = ref(false);
const errors = ref([]); const errors = ref([]);
@ -240,12 +241,22 @@ const clearSelection = () => {
if (table) { if (table) {
table.clearSelection(); table.clearSelection();
if (typeof table.setPage === 'function') {
table.setPage(1);
}
} }
}); });
}; };
// Re-sorts the table so currently-selected VMs appear on the first pages. const selectAllVMs = () => {
const showSelectedFirst = () => { allVMsSelected.value = true;
selectedVMIds.value = new Set(discoveredVMs.value.map((vm) => vm.id));
selectedVMs.value = discoveredVMs.value.slice();
};
// Moves currently-selected VMs to the first page(s) and re-checks them.
const sortSelectedToFront = () => {
if (selectedVMIds.value.size === 0) { if (selectedVMIds.value.size === 0) {
return; return;
} }
@ -258,19 +269,33 @@ const showSelectedFirst = () => {
h.name === 'vmName' ? { ...h, sort: ['selectedSort', 'vmName'] } : h h.name === 'vmName' ? { ...h, sort: ['selectedSort', 'vmName'] } : h
)); ));
skipNextSelectionEvent = true;
nextTick(() => { nextTick(() => {
const table = sortableTableRef.value; const table = sortableTableRef.value;
if (table) { if (!table) {
table.page = 1; skipNextSelectionEvent = false;
}
});
};
const selectAllVMs = () => { return;
allVMsSelected.value = true; }
selectedVMIds.value = new Set(discoveredVMs.value.map((vm) => vm.id));
selectedVMs.value = discoveredVMs.value.slice(); if (typeof table.changeSort === 'function') {
table.changeSort('vmName', false);
} else if (typeof table.setPage === 'function') {
table.setPage(1);
}
nextTick(() => {
const rowsToReselect = (table.pagedRows || []).filter((row) => selectedVMIds.value.has(row._original?.id));
if (rowsToReselect.length > 0) {
table.update(rowsToReselect, []);
}
skipNextSelectionEvent = false;
});
});
}; };
watch( watch(
@ -342,6 +367,9 @@ const fetchVMs = async() => {
return map; return map;
}, {}); }, {});
props.stepData.networkMap = networkMap.value;
props.stepData.datastoreMap = datastoreMap.value;
lastFetchedAt.value = Date.now(); lastFetchedAt.value = Date.now();
tableRows.value = buildTableRows(); tableRows.value = buildTableRows();
}; };
@ -398,6 +426,10 @@ const init = async() => {
tableRows.value = buildTableRows(); tableRows.value = buildTableRows();
loading.value = false; loading.value = false;
// Returning to the step with an existing selection: bring selected VMs forward and re-check them.
await nextTick();
sortSelectedToFront();
return; return;
} }
@ -465,7 +497,6 @@ init();
:row-actions="false" :row-actions="false"
:groupable="false" :groupable="false"
:paging="true" :paging="true"
:rows-per-page="20"
key-field="_key" key-field="_key"
@selection="onSelect" @selection="onSelect"
> >
@ -475,13 +506,9 @@ init();
{{ t('harvester.addons.vmMigration.selectVms.availableVms') }} {{ t('harvester.addons.vmMigration.selectVms.availableVms') }}
</h3> </h3>
<span class="selected-actions"> <span class="selected-actions">
<a <span class="text-deemphasized">
role="button"
:class="{ disabled: selectedCount === 0 }"
@click.prevent="showSelectedFirst"
>
{{ selectedCount }} {{ t('harvester.addons.vmMigration.selectVms.selected') }} {{ selectedCount }} {{ t('harvester.addons.vmMigration.selectVms.selected') }}
</a> </span>
<template v-if="selectedCount > 0"> <template v-if="selectedCount > 0">
<span class="text-deemphasized">|</span> <span class="text-deemphasized">|</span>
<a <a

View File

@ -136,6 +136,7 @@ const cancel = () => {
.storage-defaults-card { .storage-defaults-card {
margin: 0; margin: 0;
padding: 20px;
} }
.description { .description {