mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-07 06:36:09 +08:00
feat(forklift): Changed to Wizard
Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import PercentageBar from '@shell/components/PercentageBar';
|
||||
import MappingsCell from '../../../../components/MappingsCell';
|
||||
import { SCHEMA } from '@shell/config/types';
|
||||
import { useI18n } from '@shell/composables/useI18n';
|
||||
import { HCI } from '../../../../types';
|
||||
import { PRODUCT_NAME } from '../../../../config/harvester';
|
||||
import { FORKLIFT_PLAN_VM_COUNT } from '../../../../config/table-headers';
|
||||
import { STATE, NAME as NAME_COL, AGE } from '@shell/config/table-headers';
|
||||
|
||||
const schema = {
|
||||
id: HCI.FORKLIFT_PLAN,
|
||||
type: SCHEMA,
|
||||
attributes: {
|
||||
kind: HCI.FORKLIFT_PLAN,
|
||||
namespaced: true
|
||||
},
|
||||
metadata: { name: HCI.FORKLIFT_PLAN },
|
||||
};
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n(store);
|
||||
|
||||
const loading = ref(true);
|
||||
|
||||
const inStore = computed(() => store.getters['currentProduct'].inStore);
|
||||
|
||||
const allPlans = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_PLAN));
|
||||
const allNetworkMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_NETWORK_MAP));
|
||||
const allStorageMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_STORAGE_MAP));
|
||||
|
||||
const rows = computed(() => {
|
||||
return allPlans.value.map((plan) => {
|
||||
const netMapName = plan.spec?.map?.network?.name;
|
||||
const netMapNs = plan.spec?.map?.network?.namespace || plan.metadata.namespace;
|
||||
const storMapName = plan.spec?.map?.storage?.name;
|
||||
const storMapNs = plan.spec?.map?.storage?.namespace || plan.metadata.namespace;
|
||||
|
||||
const netMap = allNetworkMaps.value.find((m) => m.metadata.name === netMapName && m.metadata.namespace === netMapNs);
|
||||
const storMap = allStorageMaps.value.find((m) => m.metadata.name === storMapName && m.metadata.namespace === storMapNs);
|
||||
|
||||
plan.networkEntries = (netMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.type === 'pod' ? 'Pod Network' : (e.destination?.name || '-') }`);
|
||||
plan.storageEntries = (storMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.storageClass || '-' }`);
|
||||
|
||||
plan.vmIdsDisplay = (plan.spec?.vms || []).map((vm) => vm.id || vm.name || '').filter(Boolean).join(', ') || '-';
|
||||
|
||||
const vms = plan.status?.migration?.vms || [];
|
||||
|
||||
plan.vmProgress = vms.map((vm) => {
|
||||
const pipeline = vm.pipeline || [];
|
||||
const totalSteps = pipeline.length || 1;
|
||||
let overallProgress = 0;
|
||||
let currentStep = '';
|
||||
let errorMsg = '';
|
||||
|
||||
pipeline.forEach((step, idx) => {
|
||||
const stepWeight = 100 / totalSteps;
|
||||
|
||||
if (step.phase === 'Completed') {
|
||||
overallProgress += stepWeight;
|
||||
} else {
|
||||
const stepPct = (step.progress?.completed && step.progress?.total) ? (step.progress.completed / step.progress.total) * 100 : 0;
|
||||
|
||||
overallProgress += (stepPct / 100) * stepWeight;
|
||||
|
||||
if (!currentStep) {
|
||||
currentStep = step.name || `Step ${ idx + 1 }`;
|
||||
}
|
||||
|
||||
if (step.error && !errorMsg) {
|
||||
const reasons = (step.error.reasons || []).join('; ') || 'Error';
|
||||
|
||||
errorMsg = `${ step.name || `Step ${ idx + 1 }` }: ${ reasons }`;
|
||||
}
|
||||
|
||||
if (step.phase === 'Failed' && !errorMsg) {
|
||||
errorMsg = `${ step.name || `Step ${ idx + 1 }` }: Failed`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fallback to VM-level error if no step-level error found
|
||||
if (!errorMsg && vm.error) {
|
||||
const reasons = (vm.error.reasons || []).join('; ') || 'Failed';
|
||||
|
||||
errorMsg = `${ currentStep || vm.error.phase || 'Migration' }: ${ reasons }`;
|
||||
}
|
||||
|
||||
overallProgress = Math.round(overallProgress * 10) / 10;
|
||||
|
||||
// If no step-level error but the plan itself is failed, surface it
|
||||
if (!errorMsg && plan.planFailed) {
|
||||
errorMsg = `${ currentStep || 'Migration' }: Failed`;
|
||||
}
|
||||
|
||||
return {
|
||||
vmName: vm.name || vm.id || 'Unknown',
|
||||
vmId: vm.id || '',
|
||||
progress: overallProgress,
|
||||
currentStep,
|
||||
errorMsg,
|
||||
canceled: plan.planCanceled,
|
||||
};
|
||||
});
|
||||
|
||||
// If no migration progress but we have VMs in the spec, show them at 0%
|
||||
if (plan.vmProgress.length === 0 && plan.spec?.vms?.length > 0) {
|
||||
plan.vmProgress = plan.spec.vms.map((vm) => ({
|
||||
vmName: vm.name || vm.id || 'Unknown',
|
||||
vmId: vm.id || '',
|
||||
progress: 0,
|
||||
currentStep: 'Initializing migration',
|
||||
errorMsg: '',
|
||||
canceled: false,
|
||||
}));
|
||||
}
|
||||
|
||||
plan.progress = plan.vmProgress.length > 0 ? Math.round(plan.vmProgress.reduce((sum, vm) => sum + vm.progress, 0) / plan.vmProgress.length * 10) / 10 : 0;
|
||||
|
||||
return plan;
|
||||
});
|
||||
});
|
||||
|
||||
const createLocation = computed(() => ({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-vm-migration-wizard`,
|
||||
params: {
|
||||
product: store.getters['productId'],
|
||||
cluster: store.getters['clusterId'],
|
||||
}
|
||||
}));
|
||||
|
||||
const headers = [
|
||||
{ ...STATE, labelKey: 'harvester.addons.vmMigration.dashboard.columns.status' },
|
||||
{
|
||||
...NAME_COL,
|
||||
labelKey: 'harvester.addons.vmMigration.dashboard.columns.plan',
|
||||
},
|
||||
{ ...FORKLIFT_PLAN_VM_COUNT, width: 105 },
|
||||
{
|
||||
name: 'progress',
|
||||
labelKey: 'harvester.addons.vmMigration.dashboard.columns.progress',
|
||||
value: 'progress',
|
||||
width: 500,
|
||||
},
|
||||
{
|
||||
name: 'mappings',
|
||||
labelKey: 'harvester.addons.vmMigration.dashboard.columns.mappings',
|
||||
value: 'mappingsDisplay',
|
||||
},
|
||||
{ ...AGE },
|
||||
];
|
||||
|
||||
const init = async() => {
|
||||
await Promise.all([
|
||||
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_PLAN }),
|
||||
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP }),
|
||||
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_STORAGE_MAP }),
|
||||
]);
|
||||
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="loading" />
|
||||
<div v-else>
|
||||
<Masthead
|
||||
:schema="schema"
|
||||
:resource="schema.id"
|
||||
:type-display="t('harvester.addons.vmMigration.dashboard.title')"
|
||||
>
|
||||
<template #subHeader>
|
||||
<div class="mmt-5">
|
||||
<p class="text-muted">
|
||||
{{ t('harvester.addons.vmMigration.dashboard.description') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #createButton>
|
||||
<router-link
|
||||
:to="createLocation"
|
||||
class="btn role-primary"
|
||||
>
|
||||
{{ t('harvester.addons.vmMigration.dashboard.createPlan') }}
|
||||
</router-link>
|
||||
</template>
|
||||
</Masthead>
|
||||
|
||||
<ResourceTable
|
||||
:schema="schema"
|
||||
:rows="rows"
|
||||
:headers="headers"
|
||||
:groupable="false"
|
||||
:table-actions="false"
|
||||
:search="false"
|
||||
key-field="_key"
|
||||
>
|
||||
<template #header-left>
|
||||
<h3 class="table-title m-0">
|
||||
{{ t('harvester.addons.vmMigration.dashboard.tableTitle') }}
|
||||
</h3>
|
||||
</template>
|
||||
<template #cell:name="{ row }">
|
||||
<div class="plan-name-cell">
|
||||
<div class="plan-name">
|
||||
{{ row.metadata.name }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell:vmCount="{ row }">
|
||||
{{ (row.spec.vms || []).length }} VMs
|
||||
</template>
|
||||
<template #cell:progress="{ row }">
|
||||
<div
|
||||
v-if="row.vmProgress && row.vmProgress.length"
|
||||
class="progress-cells"
|
||||
>
|
||||
<div
|
||||
v-for="vm in row.vmProgress"
|
||||
:key="vm.vmId"
|
||||
class="vm-progress"
|
||||
>
|
||||
<div class="vm-progress-header">
|
||||
<div class="vm-name-block">
|
||||
<span class="vm-name">{{ vm.vmName }}</span>
|
||||
<span class="text-muted vm-id">id: {{ vm.vmId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vm-pct-block">
|
||||
<PercentageBar
|
||||
:model-value="vm.progress"
|
||||
:color-stops="vm.errorMsg ? { 100: '--error' } : vm.canceled ? { 100: '--darker' } : vm.progress >= 100 ? { 100: '--success' } : { 100: '--primary' }"
|
||||
preferred-direction="MORE"
|
||||
class="vm-bar"
|
||||
/>
|
||||
<span class="vm-pct text-muted mr-10">{{ vm.progress }}%</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="vm.progress >= 100"
|
||||
class="step-label text-muted"
|
||||
>
|
||||
Finished Successfully
|
||||
</div>
|
||||
<div
|
||||
v-else-if="vm.errorMsg"
|
||||
class="step-label text-error"
|
||||
>
|
||||
{{ vm.errorMsg }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="vm.canceled"
|
||||
class="step-label text-muted"
|
||||
>
|
||||
Canceled
|
||||
</div>
|
||||
<div
|
||||
v-else-if="vm.currentStep"
|
||||
class="step-label text-muted"
|
||||
>
|
||||
{{ vm.currentStep }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="!row.vmProgress.length">-</span>
|
||||
</template>
|
||||
<template #cell:mappings="{ row }">
|
||||
<MappingsCell
|
||||
:network-entries="row.networkEntries"
|
||||
:storage-entries="row.storageEntries"
|
||||
/>
|
||||
</template>
|
||||
</ResourceTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.plan-name-cell {
|
||||
.plan-name {
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.plan-vms {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: var(--muted);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-cells {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.vm-progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vm-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.vm-name-block {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vm-id {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.vm-pct {
|
||||
font-size: 14px;
|
||||
min-width: 40px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.vm-pct-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.vm-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 13px;
|
||||
margin-top: 4px;
|
||||
line-height: 20px;
|
||||
|
||||
&.text-error {
|
||||
color: var(--error);
|
||||
}
|
||||
}
|
||||
|
||||
.table-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,294 @@
|
||||
<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 '../../../../components/vm-migration/ConfigureProviderStep';
|
||||
import SelectVmsStep from '../../../../components/vm-migration/SelectVmsStep';
|
||||
import ConfigureMappingsStep from '../../../../components/vm-migration/ConfigureMappingsStep';
|
||||
import ReviewMigrationStep from '../../../../components/vm-migration/ReviewMigrationStep';
|
||||
import { PRODUCT_NAME } from '../../../../config/harvester';
|
||||
import { currentRouter } from '../../../../utils/router';
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n(store);
|
||||
|
||||
const cruRef = ref(null);
|
||||
const providerStepRef = ref(null);
|
||||
const mappingsStepRef = ref(null);
|
||||
const reviewStepRef = ref(null);
|
||||
|
||||
const providerName = ref('');
|
||||
const provider = ref(null);
|
||||
const selectedVMs = ref([]);
|
||||
const networkMapName = ref('');
|
||||
const storageMapName = 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,
|
||||
testPassed: false,
|
||||
testResult: null,
|
||||
testError: null,
|
||||
createdProvider: null,
|
||||
createdSecret: null,
|
||||
},
|
||||
vms: {
|
||||
discoveredVMs: [],
|
||||
selectedVMIds: new Set(),
|
||||
tableRows: [],
|
||||
},
|
||||
mappings: {
|
||||
networkEntries: [],
|
||||
storageEntries: [],
|
||||
selectedNetworkTemplate: '__none__',
|
||||
selectedStorageTemplate: '__none__',
|
||||
},
|
||||
review: { planName: '' },
|
||||
});
|
||||
|
||||
const steps = reactive([
|
||||
{
|
||||
name: 'configure-provider',
|
||||
label: '',
|
||||
subtext: '',
|
||||
ready: false,
|
||||
},
|
||||
{
|
||||
name: 'select-vms',
|
||||
label: '',
|
||||
subtext: '',
|
||||
ready: false,
|
||||
},
|
||||
{
|
||||
name: 'configure-mappings',
|
||||
label: '',
|
||||
subtext: '',
|
||||
ready: false,
|
||||
},
|
||||
{
|
||||
name: 'review-migration',
|
||||
label: '',
|
||||
subtext: '',
|
||||
ready: false,
|
||||
},
|
||||
]);
|
||||
|
||||
steps[0].label = t('harvester.addons.vmMigration.wizard.steps.configureProvider.label');
|
||||
steps[0].subtext = t('harvester.addons.vmMigration.wizard.steps.configureProvider.description');
|
||||
steps[1].label = t('harvester.addons.vmMigration.wizard.steps.selectVms.label');
|
||||
steps[1].subtext = t('harvester.addons.vmMigration.wizard.steps.selectVms.description');
|
||||
steps[2].label = t('harvester.addons.vmMigration.wizard.steps.configureMappings.label');
|
||||
steps[2].subtext = t('harvester.addons.vmMigration.wizard.steps.configureMappings.description');
|
||||
steps[3].label = t('harvester.addons.vmMigration.wizard.steps.reviewMigration.label');
|
||||
steps[3].subtext = t('harvester.addons.vmMigration.wizard.steps.reviewMigration.description');
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
const wizardComponent = computed(() => cruRef.value?.$refs?.Wizard);
|
||||
|
||||
const pendingProceed = ref(false);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
// 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 = [];
|
||||
stepData.mappings.selectedNetworkTemplate = '__none__';
|
||||
stepData.mappings.selectedStorageTemplate = '__none__';
|
||||
mappingsReady.value = false;
|
||||
|
||||
stepData.review.planName = '';
|
||||
reviewReady.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Clear mappings and review when VM selection changes
|
||||
watch(selectedVMs, (newVal, oldVal) => {
|
||||
if (oldVal.length > 0 && JSON.stringify(newVal.map((v) => v.id).sort()) !== JSON.stringify(oldVal.map((v) => v.id).sort())) {
|
||||
stepData.mappings.networkEntries = [];
|
||||
stepData.mappings.storageEntries = [];
|
||||
stepData.mappings.selectedNetworkTemplate = '__none__';
|
||||
stepData.mappings.selectedStorageTemplate = '__none__';
|
||||
mappingsReady.value = false;
|
||||
|
||||
stepData.review.planName = '';
|
||||
reviewReady.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
const onFinish = async(buttonCb) => {
|
||||
try {
|
||||
await reviewStepRef.value.startMigration();
|
||||
buttonCb(true);
|
||||
|
||||
currentRouter().push({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-vm-migration`,
|
||||
params: {
|
||||
product: store.getters['productId'],
|
||||
cluster: store.getters['clusterId'],
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
errors.value = [err instanceof Error ? err.message : String(err)];
|
||||
buttonCb(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
currentRouter().push({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-vm-migration`,
|
||||
params: {
|
||||
product: store.getters['productId'],
|
||||
cluster: store.getters['clusterId'],
|
||||
}
|
||||
});
|
||||
};
|
||||
</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"
|
||||
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
|
||||
ref="mappingsStepRef"
|
||||
: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"
|
||||
:network-map-name="networkMapName"
|
||||
:storage-map-name="storageMapName"
|
||||
:mapping-entries="stepData.mappings"
|
||||
:step-data="stepData.review"
|
||||
@ready="onReviewReady"
|
||||
/>
|
||||
</template>
|
||||
</CruResource>
|
||||
</template>
|
||||
Reference in New Issue
Block a user