feat(forklift): small fixes

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
Marcelo Fukumoto
2026-07-07 13:37:12 +02:00
parent 3ecb6bfc4a
commit 5c7dd1953d
11 changed files with 133 additions and 57 deletions
@@ -2,9 +2,12 @@
import { ref, computed, watch } from 'vue';
import { useStore } from 'vuex';
import Loading from '@shell/components/Loading';
import { Banner } from '@components/Banner';
import { STORAGE_CLASS, NETWORK_ATTACHMENT } from '@shell/config/types';
import { useI18n } from '@shell/composables/useI18n';
import { randomStr } from '@shell/utils/string';
import { HCI } from '../../types';
import { FORKLIFT_NAMESPACE } from '../../config/harvester-map';
import MappingColumn from './MappingColumn.vue';
const props = defineProps({
@@ -29,6 +32,7 @@ const networkEntries = ref([]);
const storageEntries = ref([]);
const allNetworkMaps = ref([]);
const allStorageMaps = ref([]);
const errors = ref([]);
const loading = ref(true);
// Restore from stepData
@@ -39,12 +43,12 @@ if (props.stepData.storageEntries.length > 0) {
storageEntries.value = props.stepData.storageEntries;
}
const NAMESPACE = 'forklift';
const NAMESPACE = FORKLIFT_NAMESPACE;
const harvesterNetworkOptions = computed(() => {
const options = [
{ label: 'Pod Networking', value: 'pod' },
{ label: 'Ignored', value: 'ignored' },
{ label: t('harvester.addons.vmMigration.configureMappings.networkMapping.options.podNetworking'), value: 'pod' },
{ label: t('harvester.addons.vmMigration.configureMappings.networkMapping.options.ignored'), value: 'ignored' },
];
harvesterNetworks.value.forEach((net) => {
@@ -167,7 +171,7 @@ const buildNetworkEntries = () => {
if (!networkMap[netKey]) {
networkMap[netKey] = {
name: net.name || 'Unknown',
name: net.name || t('harvester.addons.vmMigration.generic.unknown'),
id: net.id || '',
vlanId: net.vlanId || '',
target: '',
@@ -199,7 +203,7 @@ const buildStorageEntries = () => {
if (ds && ds.id) {
if (!datastoreMap[ds.id]) {
datastoreMap[ds.id] = {
name: ds.name || 'Unknown',
name: ds.name || t('harvester.addons.vmMigration.generic.unknown'),
id: ds.id,
type: ds.type || '',
capacity: 0,
@@ -352,7 +356,7 @@ const saveAndReturn = async() => {
const networkMap = await store.dispatch(`${ inStore }/create`, {
type: HCI.FORKLIFT_NETWORK_MAP,
metadata: {
name: `${ props.providerName }-network-map-${ props.useAllProviderData ? 'default' : Math.random().toString(36).substring(2, 7) }`,
name: `${ props.providerName }-network-map-${ props.useAllProviderData ? 'default' : randomStr(5).toLowerCase() }`,
namespace: NAMESPACE,
ownerReferences: providerOwnerRef,
},
@@ -364,7 +368,7 @@ const saveAndReturn = async() => {
const storageMap = await store.dispatch(`${ inStore }/create`, {
type: HCI.FORKLIFT_STORAGE_MAP,
metadata: {
name: `${ props.providerName }-storage-map-${ props.useAllProviderData ? 'default' : Math.random().toString(36).substring(2, 7) }`,
name: `${ props.providerName }-storage-map-${ props.useAllProviderData ? 'default' : randomStr(5).toLowerCase() }`,
namespace: NAMESPACE,
ownerReferences: providerOwnerRef,
},
@@ -473,7 +477,7 @@ const init = async() => {
}
}
} catch (e) {
// Data resolution failed — entries will use IDs as fallback
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedResolveDetails')];
}
}
@@ -508,6 +512,12 @@ init();
v-else
class="configure-mappings"
>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
<p class="text-deemphasized line-height-20">
{{ t('harvester.addons.vmMigration.configureMappings.description') }}
</p>
@@ -11,6 +11,7 @@ import { SECRET } from '@shell/config/types';
import { randomStr } from '@shell/utils/string';
import { useI18n } from '@shell/composables/useI18n';
import { HCI } from '../../types';
import { FORKLIFT_NAMESPACE } from '../../config/harvester-map';
const CREATE_NEW = '__create_new__';
@@ -143,7 +144,7 @@ watch(selectedProvider, (val) => {
createdSecret.value = null;
} else {
const provider = allProviders.value.find(
(p) => p.metadata.name === val && p.metadata.namespace === 'forklift'
(p) => p.metadata.name === val && p.metadata.namespace === FORKLIFT_NAMESPACE
);
if (provider) {
@@ -212,7 +213,7 @@ const testConnection = async(buttonCb) => {
// Edit mode: update existing provider URL + secret, then poll
if (props.editMode && createdProvider.value) {
try {
const namespace = 'forklift';
const namespace = FORKLIFT_NAMESPACE;
createdProvider.value.spec.url = url.value;
await createdProvider.value.save();
@@ -253,7 +254,7 @@ const testConnection = async(buttonCb) => {
connected = true;
break;
} else {
errorMsg = connectionCondition.message || 'Connection failed';
errorMsg = connectionCondition.message || t('harvester.addons.vmMigration.errors.connectionFailed');
break;
}
}
@@ -263,7 +264,7 @@ const testConnection = async(buttonCb) => {
connected = true;
break;
} else if (readyCondition.status === 'False') {
errorMsg = readyCondition.message || 'Provider not ready';
errorMsg = readyCondition.message || t('harvester.addons.vmMigration.errors.providerNotReady');
break;
}
}
@@ -291,7 +292,7 @@ const testConnection = async(buttonCb) => {
// For existing providers, just poll for Ready/ConnectionTestSucceeded status
if (isExistingProvider.value) {
try {
const namespace = 'forklift';
const namespace = FORKLIFT_NAMESPACE;
const maxAttempts = 15;
let attempts = 0;
let connected = false;
@@ -316,7 +317,7 @@ const testConnection = async(buttonCb) => {
connected = true;
break;
} else {
errorMsg = connectionCondition.message || 'Connection failed';
errorMsg = connectionCondition.message || t('harvester.addons.vmMigration.errors.connectionFailed');
break;
}
}
@@ -326,7 +327,7 @@ const testConnection = async(buttonCb) => {
connected = true;
break;
} else if (readyCondition.status === 'False') {
errorMsg = readyCondition.message || 'Provider not ready';
errorMsg = readyCondition.message || t('harvester.addons.vmMigration.errors.providerNotReady');
break;
}
}
@@ -363,7 +364,7 @@ const testConnection = async(buttonCb) => {
createdSecret.value = null;
}
const namespace = 'forklift';
const namespace = FORKLIFT_NAMESPACE;
const secretName = `${ providerName.value }-creds-${ randomStr(4).toLowerCase() }`;
// Create Provider first so we have its UID for the ownerReference on the Secret
@@ -440,7 +441,7 @@ const testConnection = async(buttonCb) => {
connected = true;
break;
} else {
errorMsg = connectionCondition.message || 'Connection failed';
errorMsg = connectionCondition.message || t('harvester.addons.vmMigration.errors.connectionFailed');
break;
}
}
@@ -450,7 +451,7 @@ const testConnection = async(buttonCb) => {
connected = true;
break;
} else if (readyCondition.status === 'False') {
errorMsg = readyCondition.message || 'Provider not ready';
errorMsg = readyCondition.message || t('harvester.addons.vmMigration.errors.providerNotReady');
break;
}
}
@@ -498,12 +499,17 @@ const testConnection = async(buttonCb) => {
const init = async() => {
const inStore = store.getters['currentProduct'].inStore;
allProviders.value = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
try {
allProviders.value = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
allSecrets.value = await store.dispatch(`${ inStore }/findAll`, {
type: SECRET,
opt: { labelSelector: `ui.forklift/created-for-resource-type=${ HCI.FORKLIFT_PROVIDER }` }
});
} catch (e) {
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedLoadProviders')];
}
allSecrets.value = await store.dispatch(`${ inStore }/findAll`, {
type: SECRET,
opt: { labelSelector: `ui.forklift/created-for-resource-type=${ HCI.FORKLIFT_PROVIDER }` }
});
loading.value = false;
};
@@ -77,7 +77,10 @@ const selectOptions = computed(() => {
/>
</div>
<div :class="['mapping-arrow', entry.target ? 'text-success' : 'text-deemphasized']">
<i class="icon icon-right-arrow-alt" />
<i
class="icon icon-right-arrow-alt"
aria-hidden="true"
/>
</div>
<div class="mapping-target">
<LabeledSelect
@@ -7,7 +7,9 @@ import { RcItemCard } from '@components/RcItemCard';
import { LabeledInput } from '@components/Form/LabeledInput';
import MappingsCell from '../MappingsCell';
import { useI18n } from '@shell/composables/useI18n';
import { randomStr } from '@shell/utils/string';
import { HCI } from '../../types';
import { FORKLIFT_NAMESPACE } from '../../config/harvester-map';
const props = defineProps({
providerName: { type: String, default: '' },
@@ -34,7 +36,7 @@ const loading = ref(true);
// Restore persisted state
planName.value = props.stepData.planName;
const NAMESPACE = 'forklift';
const NAMESPACE = FORKLIFT_NAMESPACE;
const TARGET_NAMESPACE = 'default';
watch(planName, (val) => {
@@ -202,7 +204,7 @@ const startMigrationAction = async() => {
const migration = await store.dispatch(`${ inStore }/create`, {
type: HCI.FORKLIFT_MIGRATION,
metadata: {
name: `${ planName.value }-migration-${ Math.random().toString(36).substring(2, 7) }`,
name: `${ planName.value }-migration-${ randomStr(5).toLowerCase() }`,
namespace: NAMESPACE,
ownerReferences: [planOwnerRef],
},
@@ -333,14 +335,17 @@ defineExpose({ startMigration: startMigrationAction });
:id="vm.id"
:key="vm.id"
:variant="'small'"
:header="{ title: { text: vm.name }, statuses: [{ icon: 'icon-notify-tick', color: 'text-success' }] }"
:header="{ title: { text: vm.name } }"
>
<template #item-card-content>
<div class="vm-card-content">
<div class="vm-card-specs">
<span class="vm-os text-deemphasized">{{ vm.os }}</span>
<span class="vm-resources">
<i class="icon icon-disk" />
<i
class="icon icon-disk"
aria-hidden="true"
/>
{{ vm.cpus }} vCPU &bull; {{ vm.memGB }} &bull; {{ vm.diskDisplay }}
</span>
</div>
@@ -5,6 +5,7 @@ import {
import { useStore } from 'vuex';
import Loading from '@shell/components/Loading';
import SortableTable from '@shell/components/SortableTable';
import { Banner } from '@components/Banner';
import { BadgeState } from '@components/BadgeState';
import { useI18n } from '@shell/composables/useI18n';
@@ -28,6 +29,7 @@ const datastoreMap = ref({});
const sortableTableRef = ref(null);
const allVMsSelected = ref(false);
const selectedVMIds = ref(new Set());
const errors = ref([]);
let skipNextSelectionEvent = false;
const lastFetchedAt = ref(null);
@@ -81,13 +83,13 @@ if (props.stepData.tableRows.length > 0) {
// Sync back to stepData
watch(discoveredVMs, (val) => {
props.stepData.discoveredVMs = val;
}, { deep: true });
});
watch(selectedVMIds, (val) => {
props.stepData.selectedVMIds = val;
}, { deep: true });
});
watch(tableRows, (val) => {
props.stepData.tableRows = val;
}, { deep: true });
});
const vmCount = computed(() => discoveredVMs.value.length);
const selectedCount = computed(() => selectedVMs.value.length);
@@ -116,7 +118,7 @@ const headers = [
labelKey: 'harvester.addons.vmMigration.selectVms.columns.vmName',
value: 'vmName',
sort: ['vmName'],
subLabel: 'Identifier',
subLabel: t('harvester.addons.vmMigration.generic.identifier'),
},
{
name: 'os',
@@ -141,14 +143,14 @@ const headers = [
labelKey: 'harvester.addons.vmMigration.selectVms.columns.network',
value: 'network',
sort: ['network'],
subLabel: 'Identifier',
subLabel: t('harvester.addons.vmMigration.generic.identifier'),
},
{
name: 'datastore',
labelKey: 'harvester.addons.vmMigration.selectVms.columns.datastore',
value: 'datastore',
sort: ['datastore'],
subLabel: 'Identifier',
subLabel: t('harvester.addons.vmMigration.generic.identifier'),
},
];
@@ -337,10 +339,12 @@ const refreshVMs = async() => {
const previousSelectedIds = new Set(selectedVMIds.value);
try {
errors.value = [];
await fetchVMs();
} catch (e) {
discoveredVMs.value = [];
tableRows.value = [];
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedRefreshVms')];
}
selectedVMIds.value = new Set(
@@ -387,6 +391,7 @@ const init = async() => {
await fetchVMs();
} catch (e) {
discoveredVMs.value = [];
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedLoadVms')];
}
loading.value = false;
@@ -407,6 +412,12 @@ init();
v-else
class="select-vms-step"
>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
<p class="text-deemphasized line-height-20">
{{ t('harvester.addons.vmMigration.selectVms.discovered', { count: vmCount }) }}
<router-link