From e703f348798b465cf2e8103ddf3210d93cc06dae Mon Sep 17 00:00:00 2001 From: Marcelo Fukumoto Date: Thu, 21 May 2026 10:53:28 +0200 Subject: [PATCH] feat(forklift): Changed to Wizard Signed-off-by: Marcelo Fukumoto --- .../vm-migration/ConfigureMappingsStep.vue} | 395 +++++------ .../vm-migration/ConfigureProviderStep.vue} | 442 ++++++------ .../vm-migration/ReviewMigrationStep.vue | 510 ++++++++++++++ .../vm-migration/SelectVmsStep.vue} | 360 ++++++---- pkg/harvester/config/harvester-cluster.js | 32 +- pkg/harvester/config/table-headers.js | 14 +- pkg/harvester/l10n/en-us.yaml | 48 +- .../models/forklift.konveyor.io.plan.js | 2 +- .../c/_cluster/forklift/review-migration.vue | 654 ------------------ .../{forklift => vm-migration}/index.vue | 20 +- .../vm-migration/vm-migration-wizard.vue | 294 ++++++++ pkg/harvester/routing/harvester-routing.js | 29 +- 12 files changed, 1491 insertions(+), 1309 deletions(-) rename pkg/harvester/{pages/c/_cluster/forklift/configure-mappings.vue => components/vm-migration/ConfigureMappingsStep.vue} (63%) rename pkg/harvester/{pages/c/_cluster/forklift/configure-provider.vue => components/vm-migration/ConfigureProviderStep.vue} (58%) create mode 100644 pkg/harvester/components/vm-migration/ReviewMigrationStep.vue rename pkg/harvester/{pages/c/_cluster/forklift/select-vms.vue => components/vm-migration/SelectVmsStep.vue} (57%) delete mode 100644 pkg/harvester/pages/c/_cluster/forklift/review-migration.vue rename pkg/harvester/pages/c/_cluster/{forklift => vm-migration}/index.vue (93%) create mode 100644 pkg/harvester/pages/c/_cluster/vm-migration/vm-migration-wizard.vue diff --git a/pkg/harvester/pages/c/_cluster/forklift/configure-mappings.vue b/pkg/harvester/components/vm-migration/ConfigureMappingsStep.vue similarity index 63% rename from pkg/harvester/pages/c/_cluster/forklift/configure-mappings.vue rename to pkg/harvester/components/vm-migration/ConfigureMappingsStep.vue index 0ec75c33..54f1bbb6 100644 --- a/pkg/harvester/pages/c/_cluster/forklift/configure-mappings.vue +++ b/pkg/harvester/components/vm-migration/ConfigureMappingsStep.vue @@ -2,25 +2,20 @@ import { ref, computed, watch } from 'vue'; import { useStore } from 'vuex'; import Loading from '@shell/components/Loading'; -import Masthead from '@shell/components/ResourceList/Masthead'; -import AsyncButton from '@shell/components/AsyncButton'; import LabeledSelect from '@shell/components/form/LabeledSelect'; import { RcItemCard } from '@components/RcItemCard'; -import { SCHEMA, STORAGE_CLASS, NETWORK_ATTACHMENT } from '@shell/config/types'; +import { STORAGE_CLASS, NETWORK_ATTACHMENT } from '@shell/config/types'; import { useI18n } from '@shell/composables/useI18n'; -import { HCI } from '../../../../types'; -import { PRODUCT_NAME } from '../../../../config/harvester'; -import { currentRouter, currentRoute } from '../../../../utils/router'; +import { HCI } from '../../types'; -const schema = { - id: HCI.FORKLIFT_NETWORK_MAP, - type: SCHEMA, - attributes: { - kind: HCI.FORKLIFT_NETWORK_MAP, - namespaced: true - }, - metadata: { name: HCI.FORKLIFT_NETWORK_MAP }, -}; +const props = defineProps({ + providerName: { type: String, default: '' }, + provider: { type: Object, default: null }, + selectedVms: { type: Array, default: () => [] }, + stepData: { type: Object, required: true }, +}); + +const emit = defineEmits(['ready']); const store = useStore(); const { t } = useI18n(store); @@ -36,11 +31,19 @@ const allNetworkMaps = ref([]); const allStorageMaps = ref([]); const selectedNetworkTemplate = ref(NO_TEMPLATE); const selectedStorageTemplate = ref(NO_TEMPLATE); -const errors = ref([]); const loading = ref(true); +// Restore from stepData +if (props.stepData.networkEntries.length > 0) { + networkEntries.value = props.stepData.networkEntries; +} +if (props.stepData.storageEntries.length > 0) { + storageEntries.value = props.stepData.storageEntries; +} +selectedNetworkTemplate.value = props.stepData.selectedNetworkTemplate; +selectedStorageTemplate.value = props.stepData.selectedStorageTemplate; + const NAMESPACE = 'forklift'; -const providerName = computed(() => currentRoute().query.provider || 'vsphere'); const harvesterNetworkOptions = computed(() => { const options = [ @@ -74,7 +77,7 @@ const storageClassOptions = computed(() => { const networkTemplateOptions = computed(() => { const options = [ - { label: t('harvester.addons.forklift.configureMappings.noTemplate'), value: NO_TEMPLATE } + { label: t('harvester.addons.vmMigration.configureMappings.noTemplate'), value: NO_TEMPLATE } ]; const currentIds = new Set(networkEntries.value.map((e) => e.id).filter(Boolean)); @@ -100,7 +103,7 @@ const networkTemplateOptions = computed(() => { const storageTemplateOptions = computed(() => { const options = [ - { label: t('harvester.addons.forklift.configureMappings.noTemplate'), value: NO_TEMPLATE } + { label: t('harvester.addons.vmMigration.configureMappings.noTemplate'), value: NO_TEMPLATE } ]; const currentIds = new Set(storageEntries.value.map((e) => e.id).filter(Boolean)); @@ -192,6 +195,29 @@ const canSave = computed(() => { allNetworksMapped.value && allStorageMapped.value; }); +watch(canSave, (val) => { + emit('ready', val); +}); + +// After restore, check if ready +if (canSave.value) { + emit('ready', true); +} + +// Sync state back to stepData +watch(networkEntries, (val) => { + props.stepData.networkEntries = val; +}, { deep: true }); +watch(storageEntries, (val) => { + props.stepData.storageEntries = val; +}, { deep: true }); +watch(selectedNetworkTemplate, (val) => { + props.stepData.selectedNetworkTemplate = val; +}); +watch(selectedStorageTemplate, (val) => { + props.stepData.selectedStorageTemplate = val; +}); + const buildNetworkEntries = () => { const networkMap = {}; @@ -239,13 +265,15 @@ const buildStorageEntries = () => { name: ds.name || 'Unknown', id: ds.id, type: ds.type || '', - capacity: ds.capacity || 0, + capacity: 0, target: '', usedBy: [], _key: `stor-${ ds.id }`, }; } + datastoreMap[ds.id].capacity += disk.capacity || 0; + if (!datastoreMap[ds.id].usedBy.includes(vmName)) { datastoreMap[ds.id].usedBy.push(vmName); } @@ -274,27 +302,17 @@ const formatStorageDetail = (entry) => { } } - return parts.join(' \u2022 '); + return parts.join(' • '); }; -const cancel = () => { - currentRouter().push({ - name: `${ PRODUCT_NAME }-c-cluster-forklift`, - params: { - product: store.getters['productId'], - cluster: store.getters['clusterId'], - } - }); -}; - -const saveMappings = async(buttonCb) => { +const saveAndReturn = async() => { const inStore = store.getters['currentProduct'].inStore; const providerRef = { source: { apiVersion: 'forklift.konveyor.io/v1beta1', kind: 'Provider', - name: providerName.value, + name: props.providerName, namespace: NAMESPACE, }, destination: { @@ -305,107 +323,85 @@ const saveMappings = async(buttonCb) => { }, }; - try { - // Create NetworkMap - const networkMapSpec = { - map: networkEntries.value.map((entry) => { - if (entry.target === 'pod') { - return { - source: { name: entry.name, id: entry.id }, - destination: { type: 'pod' }, - }; - } - - if (entry.target === 'ignored') { - return { - source: { name: entry.name, id: entry.id }, - destination: { type: 'ignored' }, - }; - } - - const parts = entry.target.split('/'); - const netName = parts.length > 1 ? parts[1] : parts[0]; - const netNamespace = parts.length > 1 ? parts[0] : NAMESPACE; - + // Create NetworkMap + const networkMapSpec = { + map: networkEntries.value.map((entry) => { + if (entry.target === 'pod') { return { source: { name: entry.name, id: entry.id }, - destination: { - type: 'multus', - name: netName, - namespace: netNamespace, - }, + destination: { type: 'pod' }, }; - }), - provider: providerRef, - }; - - // Fetch the Provider to build ownerReference - const allProviders = store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || []; - const provider = allProviders.find((p) => p.metadata.name === providerName.value && p.metadata.namespace === NAMESPACE); - - const providerOwnerRef = provider ? [{ - apiVersion: 'forklift.konveyor.io/v1beta1', - kind: 'Provider', - name: provider.metadata.name, - uid: provider.metadata.uid, - blockOwnerDeletion: true, - }] : []; - - const networkMap = await store.dispatch(`${ inStore }/create`, { - type: HCI.FORKLIFT_NETWORK_MAP, - metadata: { - name: `${ providerName.value }-network-map-${ Math.random().toString(36).substring(2, 7) }`, - namespace: NAMESPACE, - ownerReferences: providerOwnerRef, - }, - spec: networkMapSpec, - }); - - await networkMap.save(); - - // Create StorageMap - const storageMapSpec = { - map: storageEntries.value.map((entry) => ({ - source: { name: entry.name, id: entry.id }, - destination: { storageClass: entry.target }, - })), - provider: providerRef, - }; - - const storageMap = await store.dispatch(`${ inStore }/create`, { - type: HCI.FORKLIFT_STORAGE_MAP, - metadata: { - name: `${ providerName.value }-storage-map-${ Math.random().toString(36).substring(2, 7) }`, - namespace: NAMESPACE, - ownerReferences: providerOwnerRef, - }, - spec: storageMapSpec, - }); - - await storageMap.save(); - - // Navigate to review page - currentRouter().push({ - name: `${ PRODUCT_NAME }-c-cluster-forklift-review-migration`, - params: { - product: store.getters['productId'], - cluster: store.getters['clusterId'], - }, - query: { - provider: providerName.value, - vms: currentRoute().query.vms, - networkMap: networkMap.metadata.name, - storageMap: storageMap.metadata.name, } - }); - buttonCb(true); - } catch (err) { - errors.value = [err.message || err]; - buttonCb(false); - } + if (entry.target === 'ignored') { + return { + source: { name: entry.name, id: entry.id }, + destination: { type: 'ignored' }, + }; + } + + const parts = entry.target.split('/'); + const netName = parts.length > 1 ? parts[1] : parts[0]; + const netNamespace = parts.length > 1 ? parts[0] : NAMESPACE; + + return { + source: { name: entry.name, id: entry.id }, + destination: { + type: 'multus', + name: netName, + namespace: netNamespace, + }, + }; + }), + provider: providerRef, + }; + + const providerOwnerRef = props.provider ? [{ + apiVersion: 'forklift.konveyor.io/v1beta1', + kind: 'Provider', + name: props.provider.metadata.name, + uid: props.provider.metadata.uid, + blockOwnerDeletion: true, + }] : []; + + const networkMap = await store.dispatch(`${ inStore }/create`, { + type: HCI.FORKLIFT_NETWORK_MAP, + metadata: { + name: `${ props.providerName }-network-map-${ Math.random().toString(36).substring(2, 7) }`, + namespace: NAMESPACE, + ownerReferences: providerOwnerRef, + }, + spec: networkMapSpec, + }); + + await networkMap.save(); + + // Create StorageMap + const storageMapSpec = { + map: storageEntries.value.map((entry) => ({ + source: { name: entry.name, id: entry.id }, + destination: { storageClass: entry.target }, + })), + provider: providerRef, + }; + + const storageMap = await store.dispatch(`${ inStore }/create`, { + type: HCI.FORKLIFT_STORAGE_MAP, + metadata: { + name: `${ props.providerName }-storage-map-${ Math.random().toString(36).substring(2, 7) }`, + namespace: NAMESPACE, + ownerReferences: providerOwnerRef, + }, + spec: storageMapSpec, + }); + + await storageMap.save(); + + return { networkMapName: networkMap.metadata.name, storageMapName: storageMap.metadata.name }; }; +defineExpose({ saveMappings: saveAndReturn }); + const init = async() => { const inStore = store.getters['currentProduct'].inStore; @@ -433,28 +429,17 @@ const init = async() => { allStorageMaps.value = []; } - try { - await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER }); - } catch (e) { - // Provider may already be in store from previous step - } + vms.value = props.selectedVms; - const allProviders = store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || []; - const provider = allProviders.find((p) => p.metadata.name === providerName.value && p.metadata.namespace === NAMESPACE); - - const vmsParam = currentRoute().query.vms; - - if (vmsParam) { + if (networkEntries.value.length === 0) { try { - const vmIds = JSON.parse(vmsParam); - const providerUid = provider?.metadata?.uid; - const providerType = provider?.spec?.type || 'vsphere'; + const providerUid = props.provider?.metadata?.uid; + const providerType = props.provider?.spec?.type || 'vsphere'; const baseUrl = `https://forklift-apir.13.48.147.135.sslip.io/providers/${ providerType }/${ providerUid }`; - const [allVms, networksData, datastoresData] = await Promise.all([ - fetch(`${ baseUrl }/vms`).then((r) => r.json()).catch(() => []), + const [networksData, datastoresData] = await Promise.all([ fetch(`${ baseUrl }/networks`).then((r) => r.json()).catch(() => []), - fetch(`${ baseUrl }/datastores`).then((r) => r.json()).catch(() => []), + fetch(`${ baseUrl }/datastores?detail=1`).then((r) => r.json()).catch(() => []), ]); const networkNameMap = (Array.isArray(networksData) ? networksData : []).reduce((map, n) => { @@ -462,51 +447,48 @@ const init = async() => { return map; }, {}); - const datastoreNameMap = (Array.isArray(datastoresData) ? datastoresData : []).reduce((map, d) => { - map[d.id] = d.name; + const datastoreInfoMap = (Array.isArray(datastoresData) ? datastoresData : []).reduce((map, d) => { + map[d.id] = { name: d.name, type: d.type || '' }; return map; }, {}); - const vmList = Array.isArray(allVms) ? allVms : (allVms?.data || []); + vms.value = vms.value.map((vm) => { + const resolved = { ...vm }; - vms.value = vmIds.map((id) => { - const found = vmList.find((vm) => vm.id === id); + if (resolved.networks) { + resolved.networks = resolved.networks.map((n) => ({ + ...n, + name: n.name || networkNameMap[n.id] || n.id, + })); + } - if (found) { - // Resolve network names - if (found.networks) { - found.networks = found.networks.map((n) => ({ - ...n, - name: n.name || networkNameMap[n.id] || n.id, - })); - } + if (resolved.disks) { + resolved.disks = resolved.disks.map((d) => { + const dsInfo = d.datastore ? datastoreInfoMap[d.datastore.id] : null; - // Resolve datastore names - if (found.disks) { - found.disks = found.disks.map((d) => ({ + return { ...d, datastore: d.datastore ? { ...d.datastore, - name: d.datastore.name || datastoreNameMap[d.datastore.id] || d.datastore.id, + name: d.datastore.name || dsInfo?.name || d.datastore.id, + type: dsInfo?.type || '', } : d.datastore, - })); - } - - return found; + }; + }); } - return { - id, name: id, networks: [], disks: [] - }; + return resolved; }); } catch (e) { - vms.value = []; + // Name resolution failed — entries will use IDs as fallback } - } - buildNetworkEntries(); - buildStorageEntries(); + buildNetworkEntries(); + } + if (storageEntries.value.length === 0) { + buildStorageEntries(); + } loading.value = false; }; @@ -519,34 +501,24 @@ init(); v-else class="configure-mappings" > - - - - +

+ {{ t('harvester.addons.vmMigration.configureMappings.description') }} +

- {{ t('harvester.addons.forklift.configureMappings.networkMapping.title') }} + {{ t('harvester.addons.vmMigration.configureMappings.networkMapping.title') }}

- {{ t('harvester.addons.forklift.configureMappings.networkMapping.description') }} + {{ t('harvester.addons.vmMigration.configureMappings.networkMapping.description') }}

@@ -593,16 +565,16 @@ init();

- {{ t('harvester.addons.forklift.configureMappings.storageMapping.title') }} + {{ t('harvester.addons.vmMigration.configureMappings.storageMapping.title') }}

- {{ t('harvester.addons.forklift.configureMappings.storageMapping.description') }} + {{ t('harvester.addons.vmMigration.configureMappings.storageMapping.description') }}

@@ -648,30 +620,10 @@ init();
- - diff --git a/pkg/harvester/pages/c/_cluster/forklift/configure-provider.vue b/pkg/harvester/components/vm-migration/ConfigureProviderStep.vue similarity index 58% rename from pkg/harvester/pages/c/_cluster/forklift/configure-provider.vue rename to pkg/harvester/components/vm-migration/ConfigureProviderStep.vue index 4ddcdbd0..1888867b 100644 --- a/pkg/harvester/pages/c/_cluster/forklift/configure-provider.vue +++ b/pkg/harvester/components/vm-migration/ConfigureProviderStep.vue @@ -5,28 +5,19 @@ import { useStore } from 'vuex'; import { LabeledInput } from '@components/Form/LabeledInput'; import { Checkbox } from '@components/Form/Checkbox'; import { Banner } from '@components/Banner'; -import Masthead from '@shell/components/ResourceList/Masthead'; import AsyncButton from '@shell/components/AsyncButton'; import LabeledSelect from '@shell/components/form/LabeledSelect'; -import { SCHEMA, SECRET } from '@shell/config/types'; +import { SECRET } from '@shell/config/types'; import { randomStr } from '@shell/utils/string'; import { useI18n } from '@shell/composables/useI18n'; -import { HCI } from '../../../../types'; -import { PRODUCT_NAME } from '../../../../config/harvester'; -import { currentRouter } from '../../../../utils/router'; - -const schema = { - id: HCI.FORKLIFT_PROVIDER, - type: SCHEMA, - attributes: { - kind: HCI.FORKLIFT_PROVIDER, - namespaced: true - }, - metadata: { name: HCI.FORKLIFT_PROVIDER }, -}; +import { HCI } from '../../types'; const CREATE_NEW = '__create_new__'; +const props = defineProps({ stepData: { type: Object, required: true } }); + +const emit = defineEmits(['complete', 'ready', 'form-valid', 'testing']); + const store = useStore(); const { t } = useI18n(store); @@ -41,19 +32,67 @@ const skipTlsVerify = ref(false); const testResult = ref(null); const testError = ref(null); const errors = ref([]); +const testBtnRef = ref(null); const createdProvider = ref(null); const createdSecret = ref(null); const loading = ref(true); const testPassed = ref(false); const testing = ref(false); -const saving = ref(false); + +// Restore state from stepData on mount +selectedProvider.value = props.stepData.selectedProvider; +providerName.value = props.stepData.providerName; +url.value = props.stepData.url; +username.value = props.stepData.username; +password.value = props.stepData.password; +skipTlsVerify.value = props.stepData.skipTlsVerify; +testPassed.value = props.stepData.testPassed; +testResult.value = props.stepData.testResult; +testError.value = props.stepData.testError; +createdProvider.value = props.stepData.createdProvider; +createdSecret.value = props.stepData.createdSecret; + +// Sync state back to stepData +watch(selectedProvider, (val) => { + props.stepData.selectedProvider = val; +}); +watch(providerName, (val) => { + props.stepData.providerName = val; +}); +watch(url, (val) => { + props.stepData.url = val; +}); +watch(username, (val) => { + props.stepData.username = val; +}); +watch(password, (val) => { + props.stepData.password = val; +}); +watch(skipTlsVerify, (val) => { + props.stepData.skipTlsVerify = val; +}); +watch(testPassed, (val) => { + props.stepData.testPassed = val; +}); +watch(testResult, (val) => { + props.stepData.testResult = val; +}); +watch(testError, (val) => { + props.stepData.testError = val; +}); +watch(createdProvider, (val) => { + props.stepData.createdProvider = val; +}); +watch(createdSecret, (val) => { + props.stepData.createdSecret = val; +}); const isExistingProvider = computed(() => selectedProvider.value !== CREATE_NEW); const isFormValid = computed(() => !!providerName.value && !!url.value && !!username.value && !!password.value); const providerOptions = computed(() => { const options = [ - { label: t('harvester.addons.forklift.configureProvider.createNew'), value: CREATE_NEW } + { label: t('harvester.addons.vmMigration.configureProvider.createNew'), value: CREATE_NEW } ]; allProviders.value.forEach((p) => { @@ -134,15 +173,22 @@ watch([providerName, url, username, password], () => { } }); -const cancel = () => { - currentRouter().push({ - name: `${ PRODUCT_NAME }-c-cluster-forklift`, - params: { - product: store.getters['productId'], - cluster: store.getters['clusterId'], - } - }); -}; +// Emit ready/complete when testPassed changes +watch(testPassed, (val) => { + emit('ready', val); + if (val) { + emit('complete', { providerName: providerName.value, provider: createdProvider.value }); + } +}, { immediate: true }); + +// Emit form-valid when form validity changes +watch(isFormValid, (val) => { + emit('form-valid', val); +}, { immediate: true }); + +watch(testing, (val) => { + emit('testing', val); +}, { immediate: true }); const testConnection = async(buttonCb) => { testResult.value = null; @@ -150,7 +196,7 @@ const testConnection = async(buttonCb) => { testing.value = true; if (!providerName.value || !url.value || !username.value || !password.value) { - testError.value = t('harvester.addons.forklift.configureProvider.testMissingFields'); + testError.value = t('harvester.addons.vmMigration.configureProvider.testMissingFields'); testing.value = false; buttonCb(false); @@ -205,16 +251,16 @@ const testConnection = async(buttonCb) => { if (connected) { testPassed.value = true; - testResult.value = t('harvester.addons.forklift.configureProvider.testSuccess'); + testResult.value = t('harvester.addons.vmMigration.configureProvider.testSuccess'); testing.value = false; buttonCb(true); } else { - testError.value = errorMsg || t('harvester.addons.forklift.configureProvider.testTimeout'); + testError.value = errorMsg || t('harvester.addons.vmMigration.configureProvider.testTimeout'); testing.value = false; buttonCb(false); } } catch (err) { - testError.value = err.message || t('harvester.addons.forklift.configureProvider.testFailed'); + testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed'); testing.value = false; buttonCb(false); } @@ -329,7 +375,7 @@ const testConnection = async(buttonCb) => { if (connected) { testPassed.value = true; - testResult.value = t('harvester.addons.forklift.configureProvider.testSuccess'); + testResult.value = t('harvester.addons.vmMigration.configureProvider.testSuccess'); testing.value = false; buttonCb(true); } else { @@ -342,7 +388,7 @@ const testConnection = async(buttonCb) => { createdSecret.value = null; } - testError.value = errorMsg || t('harvester.addons.forklift.configureProvider.testTimeout'); + testError.value = errorMsg || t('harvester.addons.vmMigration.configureProvider.testTimeout'); testing.value = false; buttonCb(false); } @@ -360,47 +406,12 @@ const testConnection = async(buttonCb) => { createdSecret.value = null; } - testError.value = err.message || t('harvester.addons.forklift.configureProvider.testFailed'); + testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed'); testing.value = false; buttonCb(false); } }; -const saveProvider = async(buttonCb) => { - errors.value = []; - saving.value = true; - - if (!testPassed.value) { - // Test hasn't passed yet — run it first - await testConnection((success) => { - if (success) { - currentRouter().push({ - name: `${ PRODUCT_NAME }-c-cluster-forklift-select-vms`, - params: { - product: store.getters['productId'], - cluster: store.getters['clusterId'], - }, - query: { provider: providerName.value } - }); - } else { - saving.value = false; - buttonCb(false); - } - }); - - return; - } - - currentRouter().push({ - name: `${ PRODUCT_NAME }-c-cluster-forklift-select-vms`, - params: { - product: store.getters['productId'], - cluster: store.getters['clusterId'], - }, - query: { provider: providerName.value } - }); -}; - const init = async() => { const inStore = store.getters['currentProduct'].inStore; @@ -414,171 +425,139 @@ const init = async() => { }; init(); + +const clickTestButton = () => { + testBtnRef.value?.$el?.click(); +}; + +defineExpose({ testConnection, clickTestButton }); diff --git a/pkg/harvester/components/vm-migration/ReviewMigrationStep.vue b/pkg/harvester/components/vm-migration/ReviewMigrationStep.vue new file mode 100644 index 00000000..4a65ccb5 --- /dev/null +++ b/pkg/harvester/components/vm-migration/ReviewMigrationStep.vue @@ -0,0 +1,510 @@ + + + + + diff --git a/pkg/harvester/pages/c/_cluster/forklift/select-vms.vue b/pkg/harvester/components/vm-migration/SelectVmsStep.vue similarity index 57% rename from pkg/harvester/pages/c/_cluster/forklift/select-vms.vue rename to pkg/harvester/components/vm-migration/SelectVmsStep.vue index 78169321..02f6a58e 100644 --- a/pkg/harvester/pages/c/_cluster/forklift/select-vms.vue +++ b/pkg/harvester/components/vm-migration/SelectVmsStep.vue @@ -1,31 +1,24 @@ diff --git a/pkg/harvester/config/harvester-cluster.js b/pkg/harvester/config/harvester-cluster.js index 856990e8..67008579 100644 --- a/pkg/harvester/config/harvester-cluster.js +++ b/pkg/harvester/config/harvester-cluster.js @@ -1329,7 +1329,7 @@ export function init($plugin, store) { // =========================================================================== // Forklift Addon UI Flow // =========================================================================== - weightGroup('forklift', 0, false); + weightGroup('vmMigration', 0, false); // Provider headers(HCI.FORKLIFT_PROVIDER, [ @@ -1349,8 +1349,8 @@ export function init($plugin, store) { }); virtualType({ name: HCI.FORKLIFT_PROVIDER, - labelKey: 'harvester.addons.forklift.labels.provider', - group: 'forklift::Advanced', + labelKey: 'harvester.addons.vmMigration.labels.provider', + group: 'vmMigration::Advanced', namespaced: true, route: { name: `${ PRODUCT_NAME }-c-cluster-resource`, @@ -1376,8 +1376,8 @@ export function init($plugin, store) { }); virtualType({ name: HCI.FORKLIFT_NETWORK_MAP, - labelKey: 'harvester.addons.forklift.labels.networkMap', - group: 'forklift::Advanced', + labelKey: 'harvester.addons.vmMigration.labels.networkMap', + group: 'vmMigration::Advanced', namespaced: true, route: { name: `${ PRODUCT_NAME }-c-cluster-resource`, @@ -1403,8 +1403,8 @@ export function init($plugin, store) { }); virtualType({ name: HCI.FORKLIFT_STORAGE_MAP, - labelKey: 'harvester.addons.forklift.labels.storageMap', - group: 'forklift::Advanced', + labelKey: 'harvester.addons.vmMigration.labels.storageMap', + group: 'vmMigration::Advanced', namespaced: true, route: { name: `${ PRODUCT_NAME }-c-cluster-resource`, @@ -1431,8 +1431,8 @@ export function init($plugin, store) { }); virtualType({ name: HCI.FORKLIFT_PLAN, - labelKey: 'harvester.addons.forklift.labels.plan', - group: 'forklift::Advanced', + labelKey: 'harvester.addons.vmMigration.labels.plan', + group: 'vmMigration::Advanced', namespaced: true, route: { name: `${ PRODUCT_NAME }-c-cluster-resource`, @@ -1457,8 +1457,8 @@ export function init($plugin, store) { }); virtualType({ name: HCI.FORKLIFT_MIGRATION, - labelKey: 'harvester.addons.forklift.labels.migration', - group: 'forklift::Advanced', + labelKey: 'harvester.addons.vmMigration.labels.migration', + group: 'vmMigration::Advanced', namespaced: true, route: { name: `${ PRODUCT_NAME }-c-cluster-resource`, @@ -1468,11 +1468,11 @@ export function init($plugin, store) { configureType('forklift-create', { subTypes: [HCI.FORKLIFT_PLAN] }); virtualType({ name: 'forklift-create', - labelKey: 'harvester.addons.forklift.labels.dashboard', - group: 'forklift', + labelKey: 'harvester.addons.vmMigration.labels.dashboard', + group: 'vmMigration', namespaced: true, route: { - name: `${ PRODUCT_NAME }-c-cluster-forklift`, + name: `${ PRODUCT_NAME }-c-cluster-vm-migration`, params: {} } }); @@ -1481,7 +1481,7 @@ export function init($plugin, store) { registerAddonSideNav(store, PRODUCT_NAME, { addonName: ADD_ONS.FORKLIFT_OPERATOR, resourceType: HCI.ADD_ONS, - navGroup: 'forklift', + navGroup: 'vmMigration', types: [ 'forklift-create', ] @@ -1489,7 +1489,7 @@ export function init($plugin, store) { registerAddonSideNav(store, PRODUCT_NAME, { addonName: ADD_ONS.FORKLIFT_OPERATOR, resourceType: HCI.ADD_ONS, - navGroup: 'forklift::Advanced', + navGroup: 'vmMigration::Advanced', types: [ HCI.FORKLIFT_PROVIDER, HCI.FORKLIFT_NETWORK_MAP, diff --git a/pkg/harvester/config/table-headers.js b/pkg/harvester/config/table-headers.js index 7c7bd3aa..5615a6a4 100644 --- a/pkg/harvester/config/table-headers.js +++ b/pkg/harvester/config/table-headers.js @@ -238,7 +238,7 @@ export const VM_IMPORT_SOURCE_OVA_STATUS = { // Provider type column in forklift.konveyor.io.provider list page export const FORKLIFT_PROVIDER_TYPE = { name: 'providerType', - labelKey: 'harvester.tableHeaders.forkliftProviderType', + labelKey: 'harvester.tableHeaders.vmMigrationProviderType', value: 'spec.type', sort: 'spec.type', align: 'left', @@ -247,7 +247,7 @@ export const FORKLIFT_PROVIDER_TYPE = { // Provider URL column in forklift.konveyor.io.provider list page export const FORKLIFT_PROVIDER_URL = { name: 'providerUrl', - labelKey: 'harvester.tableHeaders.forkliftProviderUrl', + labelKey: 'harvester.tableHeaders.vmMigrationProviderUrl', value: 'spec.url', sort: 'spec.url', align: 'left', @@ -256,7 +256,7 @@ export const FORKLIFT_PROVIDER_URL = { // Source provider column in forklift network/storage map list page export const FORKLIFT_MAP_SOURCE_PROVIDER = { name: 'sourceProvider', - labelKey: 'harvester.tableHeaders.forkliftMapSourceProvider', + labelKey: 'harvester.tableHeaders.vmMigrationMapSourceProvider', value: 'spec.provider.source.name', sort: 'spec.provider.source.name', align: 'left', @@ -265,7 +265,7 @@ export const FORKLIFT_MAP_SOURCE_PROVIDER = { // Destination provider column in forklift network/storage map list page export const FORKLIFT_MAP_DEST_PROVIDER = { name: 'destProvider', - labelKey: 'harvester.tableHeaders.forkliftMapDestProvider', + labelKey: 'harvester.tableHeaders.vmMigrationMapDestProvider', value: 'spec.provider.destination.name', sort: 'spec.provider.destination.name', align: 'left', @@ -274,7 +274,7 @@ export const FORKLIFT_MAP_DEST_PROVIDER = { // Target namespace column in forklift.konveyor.io.plan list page export const FORKLIFT_PLAN_TARGET_NS = { name: 'targetNamespace', - labelKey: 'harvester.tableHeaders.forkliftPlanTargetNs', + labelKey: 'harvester.tableHeaders.vmMigrationPlanTargetNs', value: 'spec.targetNamespace', sort: 'spec.targetNamespace', align: 'left', @@ -283,7 +283,7 @@ export const FORKLIFT_PLAN_TARGET_NS = { // VM count column in forklift.konveyor.io.plan list page export const FORKLIFT_PLAN_VM_COUNT = { name: 'vmCount', - labelKey: 'harvester.tableHeaders.forkliftPlanVmCount', + labelKey: 'harvester.tableHeaders.vmMigrationPlanVmCount', value: 'spec.vms.length', sort: 'spec.vms.length', align: 'left', @@ -292,7 +292,7 @@ export const FORKLIFT_PLAN_VM_COUNT = { // Plan reference column in forklift.konveyor.io.migration list page export const FORKLIFT_MIGRATION_PLAN = { name: 'plan', - labelKey: 'harvester.tableHeaders.forkliftMigrationPlan', + labelKey: 'harvester.tableHeaders.vmMigrationMigrationPlan', value: 'spec.plan.name', sort: 'spec.plan.name', align: 'left', diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index 87e7a06d..a40e0b58 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -1,3 +1,8 @@ +wizard: + create: Create Migration + next: Proceed + previous: Back + generic: tip: Tip resourceExternalLinkTips: 'External Link' @@ -22,7 +27,7 @@ nav: Logging: Logging 'Monitoring and Logging': Monitoring and Logging vmimport: Virtual Machine Imports - forklift: Forklift Migration + vmMigration: VM Migration resourceTable: groupBy: @@ -353,13 +358,13 @@ harvester: v4ip: V4 IP v6ip: V6 IP eipName: EIP Name - forkliftProviderType: Type - forkliftProviderUrl: URL - forkliftMapSourceProvider: Source Provider - forkliftMapDestProvider: Destination Provider - forkliftPlanTargetNs: Target Namespace - forkliftPlanVmCount: VMs - forkliftMigrationPlan: Plan + vmMigrationProviderType: Type + vmMigrationProviderUrl: URL + vmMigrationMapSourceProvider: Source Provider + vmMigrationMapDestProvider: Destination Provider + vmMigrationPlanTargetNs: Target Namespace + vmMigrationPlanVmCount: VMs + vmMigrationMigrationPlan: Plan tab: volume: Volumes network: Networks @@ -1807,7 +1812,7 @@ harvester: 'harvester-csi-driver-lvm': harvester-csi-driver-lvm is an add-on allowing users to create PVC through the LVM with local devices. 'descheduler': 'The virtual machine auto balance optimizes workload scheduling by evicting pods that are not optimally placed according to administrator-defined policies.' - forklift: + vmMigration: labels: dashboard: Migrations provider: Providers @@ -1815,6 +1820,21 @@ harvester: storageMap: Storage Maps plan: Migration Plans migration: Migrations + wizard: + title: VM Migration + steps: + configureProvider: + label: Provider + description: Configure Provider + selectVms: + label: VMs + description: Select VMs + configureMappings: + label: Mappings + description: Define Mappings + reviewMigration: + label: Migration Plan + description: Review Migration Plan fields: username: Username password: Password @@ -1832,7 +1852,11 @@ harvester: urlHint: Enter the full URL including https:// skipSsl: Skip SSL certificate verification skipSslHint: Not recommended for production environments - testConnection: Test Connection + testConnection: + action: Test Connection + success: Connection successful + error: Connection failed + waiting: Testing connection... testSuccess: Connection test passed testFailed: Connection test failed testMissingFields: Please fill in all required fields before testing @@ -1841,8 +1865,10 @@ harvester: saveExisting: Check Provider and Continue selectVms: title: Select Virtual Machines + description: Select the virtual machines you want to migrate from the source provider discovered: "{count} VMs discovered from" - lastSynced: Last synced 5 minutes ago + lastSynced: "Last synced {time} ago • " + refreshNow: Refresh now availableVms: Available Virtual Machines selected: selected saveSelection: Save Selection and Continue diff --git a/pkg/harvester/models/forklift.konveyor.io.plan.js b/pkg/harvester/models/forklift.konveyor.io.plan.js index fd13603a..0ffeca3f 100644 --- a/pkg/harvester/models/forklift.konveyor.io.plan.js +++ b/pkg/harvester/models/forklift.konveyor.io.plan.js @@ -5,7 +5,7 @@ import { PRODUCT_NAME } from '../config/harvester'; export default class ForkliftPlan extends HarvesterResource { get listLocation() { return { - name: `${ PRODUCT_NAME }-c-cluster-forklift`, + name: `${ PRODUCT_NAME }-c-cluster-vm-migration`, params: { product: this.$rootGetters['productId'], cluster: this.$rootGetters['clusterId'], diff --git a/pkg/harvester/pages/c/_cluster/forklift/review-migration.vue b/pkg/harvester/pages/c/_cluster/forklift/review-migration.vue deleted file mode 100644 index 954f0db2..00000000 --- a/pkg/harvester/pages/c/_cluster/forklift/review-migration.vue +++ /dev/null @@ -1,654 +0,0 @@ - - - - - diff --git a/pkg/harvester/pages/c/_cluster/forklift/index.vue b/pkg/harvester/pages/c/_cluster/vm-migration/index.vue similarity index 93% rename from pkg/harvester/pages/c/_cluster/forklift/index.vue rename to pkg/harvester/pages/c/_cluster/vm-migration/index.vue index bd1be7e0..b27d0b7d 100644 --- a/pkg/harvester/pages/c/_cluster/forklift/index.vue +++ b/pkg/harvester/pages/c/_cluster/vm-migration/index.vue @@ -127,7 +127,7 @@ const rows = computed(() => { }); const createLocation = computed(() => ({ - name: `${ PRODUCT_NAME }-c-cluster-forklift-configure-provider`, + name: `${ PRODUCT_NAME }-c-cluster-vm-migration-wizard`, params: { product: store.getters['productId'], cluster: store.getters['clusterId'], @@ -135,21 +135,21 @@ const createLocation = computed(() => ({ })); const headers = [ - { ...STATE, labelKey: 'harvester.addons.forklift.dashboard.columns.status' }, + { ...STATE, labelKey: 'harvester.addons.vmMigration.dashboard.columns.status' }, { ...NAME_COL, - labelKey: 'harvester.addons.forklift.dashboard.columns.plan', + labelKey: 'harvester.addons.vmMigration.dashboard.columns.plan', }, { ...FORKLIFT_PLAN_VM_COUNT, width: 105 }, { name: 'progress', - labelKey: 'harvester.addons.forklift.dashboard.columns.progress', + labelKey: 'harvester.addons.vmMigration.dashboard.columns.progress', value: 'progress', width: 500, }, { name: 'mappings', - labelKey: 'harvester.addons.forklift.dashboard.columns.mappings', + labelKey: 'harvester.addons.vmMigration.dashboard.columns.mappings', value: 'mappingsDisplay', }, { ...AGE }, @@ -174,12 +174,12 @@ init(); @@ -188,7 +188,7 @@ init(); :to="createLocation" class="btn role-primary" > - {{ t('harvester.addons.forklift.dashboard.createPlan') }} + {{ t('harvester.addons.vmMigration.dashboard.createPlan') }} @@ -200,13 +200,11 @@ init(); :groupable="false" :table-actions="false" :search="false" - default-sort-by="state" - :default-sort-descending="true" key-field="_key" >