mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-08-16 12:49:14 +00:00
* feat: add VMware VDDK image option to VM migration provider Add an optional VMware VDDK Image field to the Configure Provider step of the VM migration wizard, bound to spec.settings.vddkInitImage when set and left unset when empty. Supports create and edit flows and populates from the existing provider when editing. Signed-off-by: Andy Lee <andy.lee@suse.com> * refactor: copilot review Signed-off-by: Andy Lee <andy.lee@suse.com> --------- Signed-off-by: Andy Lee <andy.lee@suse.com>
319 lines
9.0 KiB
Vue
319 lines
9.0 KiB
Vue
<script setup>
|
|
import { reactive, ref, computed, watch } from 'vue';
|
|
import { useStore } from 'vuex';
|
|
import CruResource from '@shell/components/CruResource';
|
|
import { useI18n } from '@shell/composables/useI18n';
|
|
import ConfigureProviderStep from '@pkg/harvester/components/vm-migration/ConfigureProviderStep.vue';
|
|
import SelectVmsStep from '@pkg/harvester/components/vm-migration/SelectVmsStep.vue';
|
|
import ConfigureMappingsStep from '@pkg/harvester/components/vm-migration/ConfigureMappingsStep.vue';
|
|
import ReviewMigrationStep from '@pkg/harvester/components/vm-migration/ReviewMigrationStep.vue';
|
|
import { PRODUCT_NAME } from '@pkg/harvester/config/harvester';
|
|
import { currentRouter } from '@pkg/harvester/utils/router';
|
|
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
|
|
|
|
const store = useStore();
|
|
const { t } = useI18n(store);
|
|
|
|
const cruRef = ref(null);
|
|
const providerStepRef = ref(null);
|
|
const reviewStepRef = ref(null);
|
|
|
|
const providerName = ref('');
|
|
const provider = ref(null);
|
|
const selectedVMs = ref([]);
|
|
const errors = ref([]);
|
|
|
|
const providerReady = ref(false);
|
|
const providerFormValid = ref(false);
|
|
const providerTesting = ref(false);
|
|
const vmsReady = ref(false);
|
|
const vmsLoading = ref(true);
|
|
const mappingsReady = ref(false);
|
|
const reviewReady = ref(false);
|
|
|
|
const dummyResource = ref({ save: () => Promise.resolve() });
|
|
|
|
const stepData = reactive({
|
|
provider: {
|
|
selectedProvider: '__create_new__',
|
|
providerName: '',
|
|
url: '',
|
|
username: '',
|
|
password: '',
|
|
skipTlsVerify: false,
|
|
vddkInitImage: '',
|
|
testPassed: false,
|
|
testResult: null,
|
|
testError: null,
|
|
createdProvider: null,
|
|
createdSecret: null,
|
|
},
|
|
vms: {
|
|
discoveredVMs: [],
|
|
selectedVMIds: new Set(),
|
|
tableRows: [],
|
|
},
|
|
mappings: {
|
|
networkEntries: [],
|
|
storageEntries: [],
|
|
},
|
|
review: { planName: '', targetNamespace: '' },
|
|
});
|
|
|
|
const steps = reactive([
|
|
{
|
|
name: 'configure-provider',
|
|
label: t('harvester.addons.vmMigration.wizard.steps.configureProvider.label'),
|
|
subtext: t('harvester.addons.vmMigration.wizard.steps.configureProvider.description'),
|
|
ready: false,
|
|
},
|
|
{
|
|
name: 'select-vms',
|
|
label: t('harvester.addons.vmMigration.wizard.steps.selectVms.label'),
|
|
subtext: t('harvester.addons.vmMigration.wizard.steps.selectVms.description'),
|
|
ready: false,
|
|
},
|
|
{
|
|
name: 'configure-mappings',
|
|
label: t('harvester.addons.vmMigration.wizard.steps.configureMappings.label'),
|
|
subtext: t('harvester.addons.vmMigration.wizard.steps.configureMappings.description'),
|
|
ready: false,
|
|
},
|
|
{
|
|
name: 'review-migration',
|
|
label: t('harvester.addons.vmMigration.wizard.steps.reviewMigration.label'),
|
|
subtext: t('harvester.addons.vmMigration.wizard.steps.reviewMigration.description'),
|
|
ready: false,
|
|
},
|
|
]);
|
|
|
|
watch([providerFormValid, providerTesting], () => {
|
|
steps[0].ready = providerFormValid.value && !providerTesting.value;
|
|
}, { immediate: true });
|
|
watch([vmsReady, vmsLoading], () => {
|
|
steps[1].ready = vmsReady.value && !vmsLoading.value;
|
|
});
|
|
watch(mappingsReady, (val) => {
|
|
steps[2].ready = val;
|
|
});
|
|
watch(reviewReady, (val) => {
|
|
steps[3].ready = val;
|
|
});
|
|
|
|
// CruResource exposes its inner Wizard via $refs; we reach into it to drive the
|
|
// "test connection before advancing" handshake, since the provider must be verified
|
|
// before the user can leave the first step.
|
|
const wizardComponent = computed(() => cruRef.value?.$refs?.Wizard);
|
|
|
|
const pendingProceed = ref(false);
|
|
|
|
// Handshake to gate step 1 -> 2 on a successful connection test:
|
|
// 1. When the user tries to advance off the provider step before it's ready,
|
|
// snap back (goToStep is 1-based, so goToStep(1) == the provider step) and
|
|
// programmatically trigger the async "Test connection" button.
|
|
// 2. Once the test flips `providerReady` (watcher below), mark the step ready and
|
|
// advance to the next step (goToStep(2)). If the test fails, `providerTesting`
|
|
// watcher clears `pendingProceed` and the user stays put.
|
|
watch(
|
|
() => wizardComponent.value?.activeStepIndex,
|
|
(newIdx, oldIdx) => {
|
|
if (oldIdx === 0 && newIdx === 1 && !providerReady.value) {
|
|
wizardComponent.value.goToStep(1);
|
|
pendingProceed.value = true;
|
|
providerStepRef.value.clickTestButton();
|
|
}
|
|
}
|
|
);
|
|
|
|
watch(providerReady, (val) => {
|
|
if (val && pendingProceed.value) {
|
|
pendingProceed.value = false;
|
|
steps[0].ready = true;
|
|
wizardComponent.value.goToStep(2);
|
|
}
|
|
});
|
|
|
|
watch(providerTesting, (testing) => {
|
|
if (!testing && pendingProceed.value && !providerReady.value) {
|
|
pendingProceed.value = false;
|
|
}
|
|
});
|
|
|
|
const onProviderComplete = (data) => {
|
|
providerName.value = data.providerName;
|
|
provider.value = data.provider;
|
|
providerReady.value = true;
|
|
};
|
|
|
|
const onProviderFormValid = (valid) => {
|
|
providerFormValid.value = valid;
|
|
};
|
|
|
|
const onProviderTesting = (testing) => {
|
|
providerTesting.value = testing;
|
|
};
|
|
|
|
const onProviderReady = (ready) => {
|
|
providerReady.value = ready;
|
|
};
|
|
|
|
const onVmsComplete = (data) => {
|
|
selectedVMs.value = data.selectedVMs;
|
|
vmsReady.value = data.selectedVMs.length > 0;
|
|
};
|
|
|
|
const onVmsLoading = (val) => {
|
|
vmsLoading.value = val;
|
|
};
|
|
|
|
const onMappingsReady = (ready) => {
|
|
mappingsReady.value = ready;
|
|
};
|
|
|
|
const onReviewReady = (ready) => {
|
|
reviewReady.value = ready;
|
|
};
|
|
|
|
const onEditSelection = () => {
|
|
wizardComponent.value?.goToStep(2);
|
|
};
|
|
|
|
const onEditMappings = () => {
|
|
wizardComponent.value?.goToStep(3);
|
|
};
|
|
|
|
const onRemoveVm = (id) => {
|
|
selectedVMs.value = selectedVMs.value.filter((vm) => vm.id !== id);
|
|
stepData.vms.selectedVMIds.delete(id);
|
|
|
|
vmsReady.value = selectedVMs.value.length > 0;
|
|
if (!vmsReady.value) {
|
|
reviewReady.value = false;
|
|
}
|
|
};
|
|
|
|
// Clear downstream data when provider changes
|
|
watch(() => stepData.provider.providerName, (newVal, oldVal) => {
|
|
if (oldVal && newVal !== oldVal) {
|
|
stepData.vms.discoveredVMs = [];
|
|
stepData.vms.selectedVMIds = new Set();
|
|
stepData.vms.tableRows = [];
|
|
selectedVMs.value = [];
|
|
vmsReady.value = false;
|
|
|
|
stepData.mappings.networkEntries = [];
|
|
stepData.mappings.storageEntries = [];
|
|
mappingsReady.value = false;
|
|
|
|
stepData.review.planName = '';
|
|
reviewReady.value = false;
|
|
}
|
|
});
|
|
|
|
// Clear mappings and review when VMs are added to the selection. Pure removals
|
|
// (e.g. "Remove from Plan" on the review step) keep the existing mappings and plan name.
|
|
watch(selectedVMs, (newVal, oldVal) => {
|
|
const oldIds = new Set(oldVal.map((v) => v.id));
|
|
const added = newVal.some((v) => !oldIds.has(v.id));
|
|
|
|
if (oldVal.length > 0 && added) {
|
|
stepData.mappings.networkEntries = [];
|
|
stepData.mappings.storageEntries = [];
|
|
mappingsReady.value = false;
|
|
|
|
stepData.review.planName = '';
|
|
reviewReady.value = false;
|
|
}
|
|
});
|
|
|
|
const migrationListLocation = {
|
|
name: `${ PRODUCT_NAME }-c-cluster-vm-migration`,
|
|
params: {
|
|
product: store.getters['productId'],
|
|
cluster: store.getters['clusterId'],
|
|
}
|
|
};
|
|
|
|
const onFinish = async(buttonCb) => {
|
|
try {
|
|
await reviewStepRef.value.startMigration();
|
|
buttonCb(true);
|
|
currentRouter().push(migrationListLocation);
|
|
} catch (err) {
|
|
errors.value = exceptionToErrorsArray(err).map((e) => {
|
|
if (typeof e === 'string') {
|
|
return e;
|
|
}
|
|
|
|
return stringify(e);
|
|
});
|
|
buttonCb(false);
|
|
}
|
|
};
|
|
|
|
const onCancel = () => {
|
|
currentRouter().push(migrationListLocation);
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<CruResource
|
|
ref="cruRef"
|
|
:resource="dummyResource"
|
|
:mode="'create'"
|
|
:steps="steps"
|
|
:errors="errors"
|
|
:validation-passed="true"
|
|
:can-yaml="false"
|
|
:cancel-event="true"
|
|
finish-mode="finish"
|
|
finish-button-mode="createAndStart"
|
|
class="wizard"
|
|
@cancel="onCancel"
|
|
@finish="onFinish"
|
|
>
|
|
<template #configure-provider>
|
|
<ConfigureProviderStep
|
|
ref="providerStepRef"
|
|
:step-data="stepData.provider"
|
|
@complete="onProviderComplete"
|
|
@ready="onProviderReady"
|
|
@form-valid="onProviderFormValid"
|
|
@testing="onProviderTesting"
|
|
/>
|
|
</template>
|
|
<template #select-vms>
|
|
<SelectVmsStep
|
|
:provider-name="providerName"
|
|
:provider="provider"
|
|
:step-data="stepData.vms"
|
|
@complete="onVmsComplete"
|
|
@loading="onVmsLoading"
|
|
/>
|
|
</template>
|
|
<template #configure-mappings>
|
|
<ConfigureMappingsStep
|
|
:provider-name="providerName"
|
|
:provider="provider"
|
|
:selected-vms="selectedVMs"
|
|
:step-data="stepData.mappings"
|
|
@ready="onMappingsReady"
|
|
/>
|
|
</template>
|
|
<template #review-migration>
|
|
<ReviewMigrationStep
|
|
ref="reviewStepRef"
|
|
:provider-name="providerName"
|
|
:provider="provider"
|
|
:selected-vms="selectedVMs"
|
|
:mapping-entries="stepData.mappings"
|
|
:step-data="stepData.review"
|
|
@ready="onReviewReady"
|
|
@edit-selection="onEditSelection"
|
|
@edit-mappings="onEditMappings"
|
|
@remove-vm="onRemoveVm"
|
|
/>
|
|
</template>
|
|
</CruResource>
|
|
</template>
|