mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-08-16 20:59:16 +00:00
feat(forklift): Changed to Wizard
Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
parent
7c8f172012
commit
e703f34879
@ -2,25 +2,20 @@
|
|||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import Loading from '@shell/components/Loading';
|
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 LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { RcItemCard } from '@components/RcItemCard';
|
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 { useI18n } from '@shell/composables/useI18n';
|
||||||
import { HCI } from '../../../../types';
|
import { HCI } from '../../types';
|
||||||
import { PRODUCT_NAME } from '../../../../config/harvester';
|
|
||||||
import { currentRouter, currentRoute } from '../../../../utils/router';
|
|
||||||
|
|
||||||
const schema = {
|
const props = defineProps({
|
||||||
id: HCI.FORKLIFT_NETWORK_MAP,
|
providerName: { type: String, default: '' },
|
||||||
type: SCHEMA,
|
provider: { type: Object, default: null },
|
||||||
attributes: {
|
selectedVms: { type: Array, default: () => [] },
|
||||||
kind: HCI.FORKLIFT_NETWORK_MAP,
|
stepData: { type: Object, required: true },
|
||||||
namespaced: true
|
});
|
||||||
},
|
|
||||||
metadata: { name: HCI.FORKLIFT_NETWORK_MAP },
|
const emit = defineEmits(['ready']);
|
||||||
};
|
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { t } = useI18n(store);
|
const { t } = useI18n(store);
|
||||||
@ -36,11 +31,19 @@ const allNetworkMaps = ref([]);
|
|||||||
const allStorageMaps = ref([]);
|
const allStorageMaps = ref([]);
|
||||||
const selectedNetworkTemplate = ref(NO_TEMPLATE);
|
const selectedNetworkTemplate = ref(NO_TEMPLATE);
|
||||||
const selectedStorageTemplate = ref(NO_TEMPLATE);
|
const selectedStorageTemplate = ref(NO_TEMPLATE);
|
||||||
const errors = ref([]);
|
|
||||||
const loading = ref(true);
|
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 NAMESPACE = 'forklift';
|
||||||
const providerName = computed(() => currentRoute().query.provider || 'vsphere');
|
|
||||||
|
|
||||||
const harvesterNetworkOptions = computed(() => {
|
const harvesterNetworkOptions = computed(() => {
|
||||||
const options = [
|
const options = [
|
||||||
@ -74,7 +77,7 @@ const storageClassOptions = computed(() => {
|
|||||||
|
|
||||||
const networkTemplateOptions = computed(() => {
|
const networkTemplateOptions = computed(() => {
|
||||||
const options = [
|
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));
|
const currentIds = new Set(networkEntries.value.map((e) => e.id).filter(Boolean));
|
||||||
@ -100,7 +103,7 @@ const networkTemplateOptions = computed(() => {
|
|||||||
|
|
||||||
const storageTemplateOptions = computed(() => {
|
const storageTemplateOptions = computed(() => {
|
||||||
const options = [
|
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));
|
const currentIds = new Set(storageEntries.value.map((e) => e.id).filter(Boolean));
|
||||||
@ -192,6 +195,29 @@ const canSave = computed(() => {
|
|||||||
allNetworksMapped.value && allStorageMapped.value;
|
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 buildNetworkEntries = () => {
|
||||||
const networkMap = {};
|
const networkMap = {};
|
||||||
|
|
||||||
@ -239,13 +265,15 @@ const buildStorageEntries = () => {
|
|||||||
name: ds.name || 'Unknown',
|
name: ds.name || 'Unknown',
|
||||||
id: ds.id,
|
id: ds.id,
|
||||||
type: ds.type || '',
|
type: ds.type || '',
|
||||||
capacity: ds.capacity || 0,
|
capacity: 0,
|
||||||
target: '',
|
target: '',
|
||||||
usedBy: [],
|
usedBy: [],
|
||||||
_key: `stor-${ ds.id }`,
|
_key: `stor-${ ds.id }`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
datastoreMap[ds.id].capacity += disk.capacity || 0;
|
||||||
|
|
||||||
if (!datastoreMap[ds.id].usedBy.includes(vmName)) {
|
if (!datastoreMap[ds.id].usedBy.includes(vmName)) {
|
||||||
datastoreMap[ds.id].usedBy.push(vmName);
|
datastoreMap[ds.id].usedBy.push(vmName);
|
||||||
}
|
}
|
||||||
@ -274,27 +302,17 @@ const formatStorageDetail = (entry) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.join(' \u2022 ');
|
return parts.join(' • ');
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const saveAndReturn = async() => {
|
||||||
currentRouter().push({
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
|
||||||
params: {
|
|
||||||
product: store.getters['productId'],
|
|
||||||
cluster: store.getters['clusterId'],
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveMappings = async(buttonCb) => {
|
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
const providerRef = {
|
const providerRef = {
|
||||||
source: {
|
source: {
|
||||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||||
kind: 'Provider',
|
kind: 'Provider',
|
||||||
name: providerName.value,
|
name: props.providerName,
|
||||||
namespace: NAMESPACE,
|
namespace: NAMESPACE,
|
||||||
},
|
},
|
||||||
destination: {
|
destination: {
|
||||||
@ -305,107 +323,85 @@ const saveMappings = async(buttonCb) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
// Create NetworkMap
|
||||||
// Create NetworkMap
|
const networkMapSpec = {
|
||||||
const networkMapSpec = {
|
map: networkEntries.value.map((entry) => {
|
||||||
map: networkEntries.value.map((entry) => {
|
if (entry.target === 'pod') {
|
||||||
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;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
source: { name: entry.name, id: entry.id },
|
source: { name: entry.name, id: entry.id },
|
||||||
destination: {
|
destination: { type: 'pod' },
|
||||||
type: 'multus',
|
|
||||||
name: netName,
|
|
||||||
namespace: netNamespace,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}),
|
|
||||||
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);
|
if (entry.target === 'ignored') {
|
||||||
} catch (err) {
|
return {
|
||||||
errors.value = [err.message || err];
|
source: { name: entry.name, id: entry.id },
|
||||||
buttonCb(false);
|
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 init = async() => {
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
@ -433,28 +429,17 @@ const init = async() => {
|
|||||||
allStorageMaps.value = [];
|
allStorageMaps.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
vms.value = props.selectedVms;
|
||||||
await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
|
||||||
} catch (e) {
|
|
||||||
// Provider may already be in store from previous step
|
|
||||||
}
|
|
||||||
|
|
||||||
const allProviders = store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || [];
|
if (networkEntries.value.length === 0) {
|
||||||
const provider = allProviders.find((p) => p.metadata.name === providerName.value && p.metadata.namespace === NAMESPACE);
|
|
||||||
|
|
||||||
const vmsParam = currentRoute().query.vms;
|
|
||||||
|
|
||||||
if (vmsParam) {
|
|
||||||
try {
|
try {
|
||||||
const vmIds = JSON.parse(vmsParam);
|
const providerUid = props.provider?.metadata?.uid;
|
||||||
const providerUid = provider?.metadata?.uid;
|
const providerType = props.provider?.spec?.type || 'vsphere';
|
||||||
const providerType = provider?.spec?.type || 'vsphere';
|
|
||||||
const baseUrl = `https://forklift-apir.13.48.147.135.sslip.io/providers/${ providerType }/${ providerUid }`;
|
const baseUrl = `https://forklift-apir.13.48.147.135.sslip.io/providers/${ providerType }/${ providerUid }`;
|
||||||
|
|
||||||
const [allVms, networksData, datastoresData] = await Promise.all([
|
const [networksData, datastoresData] = await Promise.all([
|
||||||
fetch(`${ baseUrl }/vms`).then((r) => r.json()).catch(() => []),
|
|
||||||
fetch(`${ baseUrl }/networks`).then((r) => r.json()).catch(() => []),
|
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) => {
|
const networkNameMap = (Array.isArray(networksData) ? networksData : []).reduce((map, n) => {
|
||||||
@ -462,51 +447,48 @@ const init = async() => {
|
|||||||
|
|
||||||
return map;
|
return map;
|
||||||
}, {});
|
}, {});
|
||||||
const datastoreNameMap = (Array.isArray(datastoresData) ? datastoresData : []).reduce((map, d) => {
|
const datastoreInfoMap = (Array.isArray(datastoresData) ? datastoresData : []).reduce((map, d) => {
|
||||||
map[d.id] = d.name;
|
map[d.id] = { name: d.name, type: d.type || '' };
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const vmList = Array.isArray(allVms) ? allVms : (allVms?.data || []);
|
vms.value = vms.value.map((vm) => {
|
||||||
|
const resolved = { ...vm };
|
||||||
|
|
||||||
vms.value = vmIds.map((id) => {
|
if (resolved.networks) {
|
||||||
const found = vmList.find((vm) => vm.id === id);
|
resolved.networks = resolved.networks.map((n) => ({
|
||||||
|
...n,
|
||||||
|
name: n.name || networkNameMap[n.id] || n.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
if (found) {
|
if (resolved.disks) {
|
||||||
// Resolve network names
|
resolved.disks = resolved.disks.map((d) => {
|
||||||
if (found.networks) {
|
const dsInfo = d.datastore ? datastoreInfoMap[d.datastore.id] : null;
|
||||||
found.networks = found.networks.map((n) => ({
|
|
||||||
...n,
|
|
||||||
name: n.name || networkNameMap[n.id] || n.id,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve datastore names
|
return {
|
||||||
if (found.disks) {
|
|
||||||
found.disks = found.disks.map((d) => ({
|
|
||||||
...d,
|
...d,
|
||||||
datastore: d.datastore ? {
|
datastore: 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,
|
} : d.datastore,
|
||||||
}));
|
};
|
||||||
}
|
});
|
||||||
|
|
||||||
return found;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return resolved;
|
||||||
id, name: id, networks: [], disks: []
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
vms.value = [];
|
// Name resolution failed — entries will use IDs as fallback
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
buildNetworkEntries();
|
buildNetworkEntries();
|
||||||
buildStorageEntries();
|
}
|
||||||
|
if (storageEntries.value.length === 0) {
|
||||||
|
buildStorageEntries();
|
||||||
|
}
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -519,34 +501,24 @@ init();
|
|||||||
v-else
|
v-else
|
||||||
class="configure-mappings"
|
class="configure-mappings"
|
||||||
>
|
>
|
||||||
<Masthead
|
<p class="text-muted line-height-20">
|
||||||
:schema="schema"
|
{{ t('harvester.addons.vmMigration.configureMappings.description') }}
|
||||||
:resource="schema.id"
|
</p>
|
||||||
:type-display="t('harvester.addons.forklift.configureMappings.title')"
|
|
||||||
:is-creatable="false"
|
|
||||||
>
|
|
||||||
<template #subHeader>
|
|
||||||
<p class="text-muted line-height-20 mmt-5">
|
|
||||||
{{ t('harvester.addons.forklift.configureMappings.description') }}
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
</Masthead>
|
|
||||||
|
|
||||||
<div class="mappings-columns">
|
<div class="mappings-columns">
|
||||||
<!-- Network Mapping -->
|
<!-- Network Mapping -->
|
||||||
<div class="mapping-column">
|
<div class="mapping-column">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mapping-section-title">
|
<h3 class="mapping-section-title">
|
||||||
{{ t('harvester.addons.forklift.configureMappings.networkMapping.title') }}
|
{{ t('harvester.addons.vmMigration.configureMappings.networkMapping.title') }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-muted line-height-20">
|
<p class="text-muted line-height-20">
|
||||||
{{ t('harvester.addons.forklift.configureMappings.networkMapping.description') }}
|
{{ t('harvester.addons.vmMigration.configureMappings.networkMapping.description') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="selectedNetworkTemplate"
|
v-model:value="selectedNetworkTemplate"
|
||||||
:label="t('harvester.addons.forklift.configureMappings.networkMapping.template')"
|
:label="t('harvester.addons.vmMigration.configureMappings.networkMapping.template')"
|
||||||
:options="networkTemplateOptions"
|
:options="networkTemplateOptions"
|
||||||
:reduce="(opt) => opt.value"
|
:reduce="(opt) => opt.value"
|
||||||
class="mb-10"
|
class="mb-10"
|
||||||
@ -574,7 +546,7 @@ init();
|
|||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="entry.target"
|
v-model:value="entry.target"
|
||||||
:options="harvesterNetworkOptions"
|
:options="harvesterNetworkOptions"
|
||||||
:placeholder="t('harvester.addons.forklift.configureMappings.networkMapping.placeholder')"
|
:placeholder="t('harvester.addons.vmMigration.configureMappings.networkMapping.placeholder')"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -593,16 +565,16 @@ init();
|
|||||||
<div class="mapping-column">
|
<div class="mapping-column">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mapping-section-title">
|
<h3 class="mapping-section-title">
|
||||||
{{ t('harvester.addons.forklift.configureMappings.storageMapping.title') }}
|
{{ t('harvester.addons.vmMigration.configureMappings.storageMapping.title') }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
{{ t('harvester.addons.forklift.configureMappings.storageMapping.description') }}
|
{{ t('harvester.addons.vmMigration.configureMappings.storageMapping.description') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="selectedStorageTemplate"
|
v-model:value="selectedStorageTemplate"
|
||||||
:label="t('harvester.addons.forklift.configureMappings.storageMapping.template')"
|
:label="t('harvester.addons.vmMigration.configureMappings.storageMapping.template')"
|
||||||
:options="storageTemplateOptions"
|
:options="storageTemplateOptions"
|
||||||
:reduce="(opt) => opt.value"
|
:reduce="(opt) => opt.value"
|
||||||
class="mb-10"
|
class="mb-10"
|
||||||
@ -633,7 +605,7 @@ init();
|
|||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="entry.target"
|
v-model:value="entry.target"
|
||||||
:options="storageClassOptions"
|
:options="storageClassOptions"
|
||||||
:placeholder="t('harvester.addons.forklift.configureMappings.storageMapping.placeholder')"
|
:placeholder="t('harvester.addons.vmMigration.configureMappings.storageMapping.placeholder')"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -648,30 +620,10 @@ init();
|
|||||||
</RcItemCard>
|
</RcItemCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions-footer">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary"
|
|
||||||
@click="cancel"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
<AsyncButton
|
|
||||||
:disabled="!canSave"
|
|
||||||
:action-label="t('harvester.addons.forklift.configureMappings.save')"
|
|
||||||
:waiting-label="t('harvester.addons.forklift.configureMappings.save')"
|
|
||||||
:success-label="t('harvester.addons.forklift.configureMappings.save')"
|
|
||||||
:error-label="t('harvester.addons.forklift.configureMappings.save')"
|
|
||||||
@click="saveMappings"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.configure-mappings {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mappings-columns {
|
.mappings-columns {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -738,13 +690,6 @@ init();
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-height-20 {
|
.line-height-20 {
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
@ -754,4 +699,14 @@ init();
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.configure-mappings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
.line-height-20 {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@ -5,28 +5,19 @@ import { useStore } from 'vuex';
|
|||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
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 { randomStr } from '@shell/utils/string';
|
||||||
import { useI18n } from '@shell/composables/useI18n';
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
import { HCI } from '../../../../types';
|
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 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const CREATE_NEW = '__create_new__';
|
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 store = useStore();
|
||||||
const { t } = useI18n(store);
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
@ -41,19 +32,67 @@ const skipTlsVerify = ref(false);
|
|||||||
const testResult = ref(null);
|
const testResult = ref(null);
|
||||||
const testError = ref(null);
|
const testError = ref(null);
|
||||||
const errors = ref([]);
|
const errors = ref([]);
|
||||||
|
const testBtnRef = ref(null);
|
||||||
const createdProvider = ref(null);
|
const createdProvider = ref(null);
|
||||||
const createdSecret = ref(null);
|
const createdSecret = ref(null);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const testPassed = ref(false);
|
const testPassed = ref(false);
|
||||||
const testing = 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 isExistingProvider = computed(() => selectedProvider.value !== CREATE_NEW);
|
||||||
const isFormValid = computed(() => !!providerName.value && !!url.value && !!username.value && !!password.value);
|
const isFormValid = computed(() => !!providerName.value && !!url.value && !!username.value && !!password.value);
|
||||||
|
|
||||||
const providerOptions = computed(() => {
|
const providerOptions = computed(() => {
|
||||||
const options = [
|
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) => {
|
allProviders.value.forEach((p) => {
|
||||||
@ -134,15 +173,22 @@ watch([providerName, url, username, password], () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const cancel = () => {
|
// Emit ready/complete when testPassed changes
|
||||||
currentRouter().push({
|
watch(testPassed, (val) => {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
emit('ready', val);
|
||||||
params: {
|
if (val) {
|
||||||
product: store.getters['productId'],
|
emit('complete', { providerName: providerName.value, provider: createdProvider.value });
|
||||||
cluster: store.getters['clusterId'],
|
}
|
||||||
}
|
}, { 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) => {
|
const testConnection = async(buttonCb) => {
|
||||||
testResult.value = null;
|
testResult.value = null;
|
||||||
@ -150,7 +196,7 @@ const testConnection = async(buttonCb) => {
|
|||||||
testing.value = true;
|
testing.value = true;
|
||||||
|
|
||||||
if (!providerName.value || !url.value || !username.value || !password.value) {
|
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;
|
testing.value = false;
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
|
|
||||||
@ -205,16 +251,16 @@ const testConnection = async(buttonCb) => {
|
|||||||
|
|
||||||
if (connected) {
|
if (connected) {
|
||||||
testPassed.value = true;
|
testPassed.value = true;
|
||||||
testResult.value = t('harvester.addons.forklift.configureProvider.testSuccess');
|
testResult.value = t('harvester.addons.vmMigration.configureProvider.testSuccess');
|
||||||
testing.value = false;
|
testing.value = false;
|
||||||
buttonCb(true);
|
buttonCb(true);
|
||||||
} else {
|
} else {
|
||||||
testError.value = errorMsg || t('harvester.addons.forklift.configureProvider.testTimeout');
|
testError.value = errorMsg || t('harvester.addons.vmMigration.configureProvider.testTimeout');
|
||||||
testing.value = false;
|
testing.value = false;
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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;
|
testing.value = false;
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
@ -329,7 +375,7 @@ const testConnection = async(buttonCb) => {
|
|||||||
|
|
||||||
if (connected) {
|
if (connected) {
|
||||||
testPassed.value = true;
|
testPassed.value = true;
|
||||||
testResult.value = t('harvester.addons.forklift.configureProvider.testSuccess');
|
testResult.value = t('harvester.addons.vmMigration.configureProvider.testSuccess');
|
||||||
testing.value = false;
|
testing.value = false;
|
||||||
buttonCb(true);
|
buttonCb(true);
|
||||||
} else {
|
} else {
|
||||||
@ -342,7 +388,7 @@ const testConnection = async(buttonCb) => {
|
|||||||
createdSecret.value = null;
|
createdSecret.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
testError.value = errorMsg || t('harvester.addons.forklift.configureProvider.testTimeout');
|
testError.value = errorMsg || t('harvester.addons.vmMigration.configureProvider.testTimeout');
|
||||||
testing.value = false;
|
testing.value = false;
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
@ -360,47 +406,12 @@ const testConnection = async(buttonCb) => {
|
|||||||
createdSecret.value = null;
|
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;
|
testing.value = false;
|
||||||
buttonCb(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 init = async() => {
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
@ -414,171 +425,139 @@ const init = async() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
|
const clickTestButton = () => {
|
||||||
|
testBtnRef.value?.$el?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ testConnection, clickTestButton });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="configure-provider">
|
<div class="configure-provider-step">
|
||||||
<Masthead
|
<p class="text-muted line-height-20">
|
||||||
:schema="schema"
|
{{ t('harvester.addons.vmMigration.configureProvider.description') }}
|
||||||
:resource="schema.id"
|
</p>
|
||||||
:type-display="t('harvester.addons.forklift.configureProvider.title')"
|
|
||||||
:is-creatable="false"
|
<div class="configure-provider-step-content">
|
||||||
>
|
<h3 class="table-title m-0">
|
||||||
<template #subHeader>
|
<b>{{ t('harvester.addons.vmMigration.configureProvider.connectionDetails') }}</b>
|
||||||
<div class="mmt-5">
|
</h3>
|
||||||
<p class="text-muted">
|
|
||||||
{{ t('harvester.addons.forklift.configureProvider.description') }}
|
<Banner
|
||||||
|
class="requirements-banner m-0"
|
||||||
|
color="info"
|
||||||
|
>
|
||||||
|
<div class="requirements-banner-content">
|
||||||
|
<span class="requirements-banner-title">{{ t('harvester.addons.vmMigration.configureProvider.requirementsTitle') }}</span>
|
||||||
|
<br>
|
||||||
|
<span>{{ t('harvester.addons.vmMigration.configureProvider.requirementsText') }}</span>
|
||||||
|
</div>
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<div class="configure-provider-step-form">
|
||||||
|
<div>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="selectedProvider"
|
||||||
|
:label="t('harvester.addons.vmMigration.configureProvider.providerSelect')"
|
||||||
|
:options="providerOptions"
|
||||||
|
:reduce="(opt) => opt.value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!isExistingProvider"
|
||||||
|
>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="providerName"
|
||||||
|
:label="t('harvester.addons.vmMigration.configureProvider.name')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="url"
|
||||||
|
:label="t('harvester.addons.vmMigration.configureProvider.urlLabel')"
|
||||||
|
:placeholder="t('harvester.addons.vmMigration.configureProvider.urlPlaceholder')"
|
||||||
|
:disabled="isExistingProvider"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p class="text-muted mt-5">
|
||||||
|
{{ t('harvester.addons.vmMigration.configureProvider.urlHint') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</Masthead>
|
|
||||||
|
|
||||||
<h3 class="mmt-3 mmb-3 table-title">
|
<div class="row">
|
||||||
<b>{{ t('harvester.addons.forklift.configureProvider.connectionDetails') }}</b>
|
<div class="col span-6">
|
||||||
</h3>
|
<LabeledInput
|
||||||
|
v-model:value="username"
|
||||||
|
:label="t('harvester.addons.vmMigration.fields.username')"
|
||||||
|
:disabled="isExistingProvider"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="password"
|
||||||
|
type="password"
|
||||||
|
:label="t('harvester.addons.vmMigration.fields.password')"
|
||||||
|
:disabled="isExistingProvider"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Banner
|
<div>
|
||||||
class="requirements-banner mt-0 mmb-3"
|
<Checkbox
|
||||||
color="info"
|
v-model:value="skipTlsVerify"
|
||||||
>
|
:label="t('harvester.addons.vmMigration.configureProvider.skipSsl')"
|
||||||
<div class="requirements-banner-content">
|
:disabled="isExistingProvider"
|
||||||
<span class="requirements-banner-title">{{ t('harvester.addons.forklift.configureProvider.requirementsTitle') }}</span>
|
/>
|
||||||
<br>
|
<p class="text-muted ml-20">
|
||||||
<span>{{ t('harvester.addons.forklift.configureProvider.requirementsText') }}</span>
|
{{ t('harvester.addons.vmMigration.configureProvider.skipSslHint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<AsyncButton
|
||||||
|
ref="testBtnRef"
|
||||||
|
mode="test"
|
||||||
|
:disabled="!isFormValid"
|
||||||
|
:action-label="t('harvester.addons.vmMigration.configureProvider.testConnection.action')"
|
||||||
|
:waiting-label="t('harvester.addons.vmMigration.configureProvider.testConnection.waiting')"
|
||||||
|
:success-label="t('harvester.addons.vmMigration.configureProvider.testConnection.success')"
|
||||||
|
:error-label="t('harvester.addons.vmMigration.configureProvider.testConnection.error')"
|
||||||
|
:action-color="'role-secondary'"
|
||||||
|
:waiting-color="'role-disabled'"
|
||||||
|
@click="testConnection"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-if="testResult"
|
||||||
|
color="success"
|
||||||
|
>
|
||||||
|
{{ testResult }}
|
||||||
|
</Banner>
|
||||||
|
<Banner
|
||||||
|
v-if="testError"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ testError }}
|
||||||
|
</Banner>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ err }}
|
||||||
|
</Banner>
|
||||||
</div>
|
</div>
|
||||||
</Banner>
|
|
||||||
|
|
||||||
<div class="mb-20">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="selectedProvider"
|
|
||||||
:label="t('harvester.addons.forklift.configureProvider.providerSelect')"
|
|
||||||
:options="providerOptions"
|
|
||||||
:reduce="(opt) => opt.value"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="!isExistingProvider"
|
|
||||||
class="mb-20"
|
|
||||||
>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="providerName"
|
|
||||||
:label="t('harvester.addons.forklift.configureProvider.name')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-20">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="url"
|
|
||||||
:label="t('harvester.addons.forklift.configureProvider.urlLabel')"
|
|
||||||
:placeholder="t('harvester.addons.forklift.configureProvider.urlPlaceholder')"
|
|
||||||
:disabled="isExistingProvider"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<p class="text-muted mt-5">
|
|
||||||
{{ t('harvester.addons.forklift.configureProvider.urlHint') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="username"
|
|
||||||
:label="t('harvester.addons.forklift.fields.username')"
|
|
||||||
:disabled="isExistingProvider"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="password"
|
|
||||||
type="password"
|
|
||||||
:label="t('harvester.addons.forklift.fields.password')"
|
|
||||||
:disabled="isExistingProvider"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-20">
|
|
||||||
<Checkbox
|
|
||||||
v-model:value="skipTlsVerify"
|
|
||||||
:label="t('harvester.addons.forklift.configureProvider.skipSsl')"
|
|
||||||
:disabled="isExistingProvider"
|
|
||||||
/>
|
|
||||||
<p class="text-muted ml-20">
|
|
||||||
{{ t('harvester.addons.forklift.configureProvider.skipSslHint') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="mb-20">
|
|
||||||
<button
|
|
||||||
v-if="testPassed"
|
|
||||||
class="btn role-secondary test-passed-btn"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
<i class="icon icon-checkmark mr-10" /> {{ t('harvester.addons.forklift.configureProvider.testConnection') }}
|
|
||||||
</button>
|
|
||||||
<AsyncButton
|
|
||||||
v-else
|
|
||||||
mode="test"
|
|
||||||
:disabled="!isFormValid || testing || saving"
|
|
||||||
:action-label="t('harvester.addons.forklift.configureProvider.testConnection')"
|
|
||||||
:waiting-label="t('harvester.addons.forklift.configureProvider.testConnection')"
|
|
||||||
:success-label="t('harvester.addons.forklift.configureProvider.testConnection')"
|
|
||||||
:error-label="t('harvester.addons.forklift.configureProvider.testConnection')"
|
|
||||||
class="btn role-secondary"
|
|
||||||
@click="testConnection"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Banner
|
|
||||||
v-if="testResult"
|
|
||||||
color="success"
|
|
||||||
class="mb-10"
|
|
||||||
>
|
|
||||||
{{ testResult }}
|
|
||||||
</Banner>
|
|
||||||
<Banner
|
|
||||||
v-if="testError"
|
|
||||||
color="error"
|
|
||||||
class="mb-10"
|
|
||||||
>
|
|
||||||
{{ testError }}
|
|
||||||
</Banner>
|
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
class="mb-10"
|
|
||||||
>
|
|
||||||
{{ err }}
|
|
||||||
</Banner>
|
|
||||||
|
|
||||||
<div class="provider-actions">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary"
|
|
||||||
@click="cancel"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
<AsyncButton
|
|
||||||
:disabled="!isFormValid || testing || saving"
|
|
||||||
:action-label="isExistingProvider ? t('harvester.addons.forklift.configureProvider.saveExisting') : t('harvester.addons.forklift.configureProvider.save')"
|
|
||||||
:waiting-label="isExistingProvider ? t('harvester.addons.forklift.configureProvider.saveExisting') : t('harvester.addons.forklift.configureProvider.save')"
|
|
||||||
:success-label="isExistingProvider ? t('harvester.addons.forklift.configureProvider.saveExisting') : t('harvester.addons.forklift.configureProvider.save')"
|
|
||||||
:error-label="isExistingProvider ? t('harvester.addons.forklift.configureProvider.saveExisting') : t('harvester.addons.forklift.configureProvider.save')"
|
|
||||||
@click="saveProvider"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.configure-provider {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.requirements-banner {
|
.requirements-banner {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
@ -587,10 +566,25 @@ init();
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-actions {
|
.configure-provider-step {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 24px;
|
||||||
margin-top: 30px;
|
|
||||||
|
.line-height-20 {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.configure-provider-step-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.configure-provider-step-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
510
pkg/harvester/components/vm-migration/ReviewMigrationStep.vue
Normal file
510
pkg/harvester/components/vm-migration/ReviewMigrationStep.vue
Normal file
@ -0,0 +1,510 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { RcItemCard } from '@components/RcItemCard';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import MappingsCell from '../MappingsCell';
|
||||||
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
providerName: { type: String, default: '' },
|
||||||
|
provider: { type: Object, default: null },
|
||||||
|
selectedVms: { type: Array, default: () => [] },
|
||||||
|
networkMapName: { type: String, default: '' },
|
||||||
|
storageMapName: { type: String, default: '' },
|
||||||
|
mappingEntries: { type: Object, default: null },
|
||||||
|
stepData: { type: Object, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['ready']);
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
|
const vms = ref([]);
|
||||||
|
const networkMappings = ref([]);
|
||||||
|
const storageMappings = ref([]);
|
||||||
|
const planName = ref('');
|
||||||
|
const errors = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
|
||||||
|
// Restore persisted state
|
||||||
|
planName.value = props.stepData.planName;
|
||||||
|
|
||||||
|
const NAMESPACE = 'forklift';
|
||||||
|
const TARGET_NAMESPACE = 'default';
|
||||||
|
|
||||||
|
watch(planName, (val) => {
|
||||||
|
props.stepData.planName = val;
|
||||||
|
emit('ready', !!val);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const totalVCpu = computed(() => vms.value.reduce((sum, vm) => sum + (vm.cpuCount || vm.numCPU || 0), 0));
|
||||||
|
|
||||||
|
const totalMemoryGB = computed(() => {
|
||||||
|
const totalMB = vms.value.reduce((sum, vm) => sum + (vm.memoryMB || vm.memory || 0), 0);
|
||||||
|
|
||||||
|
return Math.round(totalMB / 1024);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalStorageGB = computed(() => {
|
||||||
|
let totalBytes = 0;
|
||||||
|
|
||||||
|
vms.value.forEach((vm) => {
|
||||||
|
if (vm.disks && vm.disks.length > 0) {
|
||||||
|
totalBytes += vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Math.round(totalBytes / (1024 * 1024 * 1024));
|
||||||
|
});
|
||||||
|
|
||||||
|
const vmCards = computed(() => {
|
||||||
|
return vms.value.map((vm) => {
|
||||||
|
const cpus = vm.cpuCount || vm.numCPU || 0;
|
||||||
|
const memMB = vm.memoryMB || vm.memory || 0;
|
||||||
|
const memGB = memMB ? `${ Math.round(memMB / 1024) } GB` : '-';
|
||||||
|
|
||||||
|
let totalDiskBytes = 0;
|
||||||
|
|
||||||
|
if (vm.disks && vm.disks.length > 0) {
|
||||||
|
totalDiskBytes = vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const diskDisplay = totalDiskBytes ? `${ Math.round(totalDiskBytes / (1024 * 1024 * 1024)) } GB` : '-';
|
||||||
|
const os = vm.guestName || vm.guestOS || vm.os || '-';
|
||||||
|
|
||||||
|
const vmNetworkMappings = networkMappings.value.filter((m) => m.usedBy && m.usedBy.includes(vm.name || vm.id));
|
||||||
|
const vmStorageMappings = storageMappings.value.filter((m) => m.usedBy && m.usedBy.includes(vm.name || vm.id));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: vm.id,
|
||||||
|
name: vm.name || vm.id,
|
||||||
|
os,
|
||||||
|
cpus,
|
||||||
|
memGB,
|
||||||
|
diskDisplay,
|
||||||
|
networkMappings: vmNetworkMappings,
|
||||||
|
storageMappings: vmStorageMappings,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildNetworkMapSpec = () => {
|
||||||
|
const entries = props.mappingEntries?.networkEntries || [];
|
||||||
|
|
||||||
|
return entries.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;
|
||||||
|
|
||||||
|
return {
|
||||||
|
source: { name: entry.name, id: entry.id },
|
||||||
|
destination: {
|
||||||
|
type: 'multus', name: netName, namespace: netNamespace
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildStorageMapSpec = () => {
|
||||||
|
const entries = props.mappingEntries?.storageEntries || [];
|
||||||
|
|
||||||
|
return entries.map((entry) => ({
|
||||||
|
source: { name: entry.name, id: entry.id },
|
||||||
|
destination: { storageClass: entry.target },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const startMigrationAction = async() => {
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const providerRef = {
|
||||||
|
source: {
|
||||||
|
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||||
|
kind: 'Provider',
|
||||||
|
name: props.providerName,
|
||||||
|
namespace: NAMESPACE,
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||||
|
kind: 'Provider',
|
||||||
|
name: 'host',
|
||||||
|
namespace: NAMESPACE,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const networkMapName = `${ planName.value }-network-map`;
|
||||||
|
const storageMapName = `${ planName.value }-storage-map`;
|
||||||
|
|
||||||
|
const networkMap = await store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: HCI.FORKLIFT_NETWORK_MAP,
|
||||||
|
metadata: { name: networkMapName, namespace: NAMESPACE },
|
||||||
|
spec: { map: buildNetworkMapSpec(), provider: providerRef },
|
||||||
|
});
|
||||||
|
|
||||||
|
await networkMap.save();
|
||||||
|
|
||||||
|
const storageMap = await store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: HCI.FORKLIFT_STORAGE_MAP,
|
||||||
|
metadata: { name: storageMapName, namespace: NAMESPACE },
|
||||||
|
spec: { map: buildStorageMapSpec(), provider: providerRef },
|
||||||
|
});
|
||||||
|
|
||||||
|
await storageMap.save();
|
||||||
|
|
||||||
|
const plan = await store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: HCI.FORKLIFT_PLAN,
|
||||||
|
metadata: { name: planName.value, namespace: NAMESPACE },
|
||||||
|
spec: {
|
||||||
|
provider: providerRef,
|
||||||
|
map: {
|
||||||
|
network: {
|
||||||
|
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||||
|
kind: 'NetworkMap',
|
||||||
|
name: networkMapName,
|
||||||
|
namespace: NAMESPACE,
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||||
|
kind: 'StorageMap',
|
||||||
|
name: storageMapName,
|
||||||
|
namespace: NAMESPACE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetNamespace: 'default',
|
||||||
|
vms: vms.value.map((vm) => ({ id: vm.id, name: vm.name || vm.id })),
|
||||||
|
warm: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await plan.save();
|
||||||
|
|
||||||
|
const planOwnerRef = {
|
||||||
|
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||||
|
kind: 'Plan',
|
||||||
|
name: plan.metadata.name,
|
||||||
|
uid: plan.metadata.uid,
|
||||||
|
blockOwnerDeletion: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const migration = await store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: HCI.FORKLIFT_MIGRATION,
|
||||||
|
metadata: {
|
||||||
|
name: `${ planName.value }-migration-${ Math.random().toString(36).substring(2, 7) }`,
|
||||||
|
namespace: NAMESPACE,
|
||||||
|
ownerReferences: [planOwnerRef],
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
plan: {
|
||||||
|
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||||
|
kind: 'Plan',
|
||||||
|
name: planName.value,
|
||||||
|
namespace: NAMESPACE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await migration.save();
|
||||||
|
|
||||||
|
networkMap.metadata.ownerReferences = [planOwnerRef];
|
||||||
|
await networkMap.save();
|
||||||
|
storageMap.metadata.ownerReferences = [planOwnerRef];
|
||||||
|
await storageMap.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
vms.value = props.selectedVms;
|
||||||
|
|
||||||
|
if (props.mappingEntries) {
|
||||||
|
networkMappings.value = (props.mappingEntries.networkEntries || []).map((entry) => ({
|
||||||
|
source: entry.name || entry.id,
|
||||||
|
target: entry.target || '',
|
||||||
|
usedBy: entry.usedBy || [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
storageMappings.value = (props.mappingEntries.storageEntries || []).map((entry) => ({
|
||||||
|
source: entry.name || entry.id,
|
||||||
|
target: entry.target || '',
|
||||||
|
usedBy: entry.usedBy || [],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
defineExpose({ startMigration: startMigrationAction });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="review-migration"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="review-migration-content"
|
||||||
|
>
|
||||||
|
<!-- Migration Details Summary -->
|
||||||
|
<div class="migration-details">
|
||||||
|
<h3 class="section-title m-0">
|
||||||
|
{{ t('harvester.addons.vmMigration.reviewMigration.migrationDetails') }}
|
||||||
|
</h3>
|
||||||
|
<div class="span-9">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="planName"
|
||||||
|
:label="t('harvester.addons.vmMigration.reviewMigration.planName')"
|
||||||
|
:placeholder="t('harvester.addons.vmMigration.reviewMigration.planNamePlaceholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="details-grid span-9">
|
||||||
|
<div class="detail-column">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.totalVms') }}</span>
|
||||||
|
<span class="detail-value detail-value-large">{{ vms.length }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-column">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.source') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-value">{{ providerName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-column">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.targetNamespace') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-value">{{ TARGET_NAMESPACE }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-column">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.vcpu') }}</span>
|
||||||
|
<span class="detail-value">{{ totalVCpu }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.memory') }}</span>
|
||||||
|
<span class="detail-value">{{ totalMemoryGB }} GB</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.storage') }}</span>
|
||||||
|
<span class="detail-value">{{ totalStorageGB }} GB</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-column grid-column-2">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.migrationMode') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-value">
|
||||||
|
{{ t('harvester.addons.vmMigration.reviewMigration.coldMigration') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Virtual Machines -->
|
||||||
|
<div class="vm-section">
|
||||||
|
<h3 class="section-title m-0">
|
||||||
|
{{ t('harvester.addons.vmMigration.reviewMigration.virtualMachines') }} ({{ vms.length }})
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="vm-cards-grid">
|
||||||
|
<RcItemCard
|
||||||
|
v-for="vm in vmCards"
|
||||||
|
:id="vm.id"
|
||||||
|
:key="vm.id"
|
||||||
|
:variant="'small'"
|
||||||
|
:header="{ title: { text: vm.name }, statuses: [{ icon: 'icon-notify-tick', color: 'text-success' }] }"
|
||||||
|
>
|
||||||
|
<template #item-card-content>
|
||||||
|
<div class="vm-card-content">
|
||||||
|
<div class="vm-card-specs">
|
||||||
|
<span class="vm-os text-muted">{{ vm.os }}</span>
|
||||||
|
<span class="vm-resources">
|
||||||
|
<i class="icon icon-disk" />
|
||||||
|
{{ vm.cpus }} vCPU • {{ vm.memGB }} • {{ vm.diskDisplay }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<MappingsCell
|
||||||
|
:network-entries="vm.networkMappings.map(m => `${m.source} → ${m.target}`)"
|
||||||
|
:storage-entries="vm.storageMappings.map(m => `${m.source} → ${m.target}`)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RcItemCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cold Migration Warning -->
|
||||||
|
<Banner
|
||||||
|
color="warning"
|
||||||
|
class="m-0"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span class="banner-title">{{ t('harvester.addons.vmMigration.reviewMigration.warningTitle') }}</span><br>
|
||||||
|
{{ t('harvester.addons.vmMigration.reviewMigration.warningMessage') }}
|
||||||
|
</div>
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<!-- Error banner -->
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.review-migration {
|
||||||
|
gap: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-migration-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(230px, 1fr) 1fr 1fr;
|
||||||
|
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 12px 128px;
|
||||||
|
width: 747px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&.grid-column-2 {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&.detail-value-large {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-cards-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-card-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-card-specs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.vm-os {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-resources {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-card-mappings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-text {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #973C00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,31 +1,24 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, nextTick } from 'vue';
|
import {
|
||||||
|
ref, computed, watch, nextTick, onBeforeUnmount
|
||||||
|
} from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
|
||||||
import SortableTable from '@shell/components/SortableTable';
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
import { BadgeState } from '@components/BadgeState';
|
import { BadgeState } from '@components/BadgeState';
|
||||||
import { SCHEMA } from '@shell/config/types';
|
|
||||||
import { useI18n } from '@shell/composables/useI18n';
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
import { HCI } from '../../../../types';
|
|
||||||
import { PRODUCT_NAME } from '../../../../config/harvester';
|
|
||||||
import { currentRouter, currentRoute } from '../../../../utils/router';
|
|
||||||
|
|
||||||
const schema = {
|
const props = defineProps({
|
||||||
id: HCI.FORKLIFT_PROVIDER,
|
providerName: { type: String, default: '' },
|
||||||
type: SCHEMA,
|
provider: { type: Object, default: null },
|
||||||
attributes: {
|
stepData: { type: Object, required: true },
|
||||||
kind: HCI.FORKLIFT_PROVIDER,
|
});
|
||||||
namespaced: true
|
|
||||||
},
|
const emit = defineEmits(['complete', 'loading']);
|
||||||
metadata: { name: HCI.FORKLIFT_PROVIDER },
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { t } = useI18n(store);
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
const allProviders = ref([]);
|
|
||||||
const provider = ref(null);
|
|
||||||
const discoveredVMs = ref([]);
|
const discoveredVMs = ref([]);
|
||||||
const selectedVMs = ref([]);
|
const selectedVMs = ref([]);
|
||||||
const tableRows = ref([]);
|
const tableRows = ref([]);
|
||||||
@ -37,7 +30,65 @@ const allVMsSelected = ref(false);
|
|||||||
const selectedVMIds = ref(new Set());
|
const selectedVMIds = ref(new Set());
|
||||||
let skipNextSelectionEvent = false;
|
let skipNextSelectionEvent = false;
|
||||||
|
|
||||||
const providerName = computed(() => provider.value?.metadata?.name || currentRoute().query.provider || '');
|
const lastFetchedAt = ref(null);
|
||||||
|
const now = ref(Date.now());
|
||||||
|
|
||||||
|
const formatElapsed = (ms) => {
|
||||||
|
const totalMinutes = Math.floor(ms / 60000);
|
||||||
|
const hours = Math.floor(totalMinutes / 60);
|
||||||
|
const minutes = totalMinutes % 60;
|
||||||
|
|
||||||
|
if (hours > 0 && minutes > 0) {
|
||||||
|
return `${ hours } ${ hours === 1 ? 'hour' : 'hours' } and ${ minutes } ${ minutes === 1 ? 'minute' : 'minutes' }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${ hours } ${ hours === 1 ? 'hour' : 'hours' }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${ Math.max(1, minutes) } ${ minutes <= 1 ? 'minute' : 'minutes' }`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const lastSyncedTime = computed(() => {
|
||||||
|
if (!lastFetchedAt.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatElapsed(now.value - lastFetchedAt.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const nowTimer = setInterval(() => {
|
||||||
|
now.value = Date.now();
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(nowTimer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore from stepData
|
||||||
|
if (props.stepData.discoveredVMs.length > 0) {
|
||||||
|
discoveredVMs.value = props.stepData.discoveredVMs;
|
||||||
|
}
|
||||||
|
if (props.stepData.selectedVMIds.size > 0) {
|
||||||
|
selectedVMIds.value = props.stepData.selectedVMIds;
|
||||||
|
selectedVMs.value = discoveredVMs.value.filter((vm) => selectedVMIds.value.has(vm.id));
|
||||||
|
allVMsSelected.value = selectedVMIds.value.size === discoveredVMs.value.length;
|
||||||
|
}
|
||||||
|
if (props.stepData.tableRows.length > 0) {
|
||||||
|
tableRows.value = props.stepData.tableRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 vmCount = computed(() => discoveredVMs.value.length);
|
||||||
const selectedCount = computed(() => selectedVMs.value.length);
|
const selectedCount = computed(() => selectedVMs.value.length);
|
||||||
|
|
||||||
@ -62,39 +113,39 @@ const theadElement = computed(() => sortableTableRef.value?.$el?.querySelector('
|
|||||||
const headers = [
|
const headers = [
|
||||||
{
|
{
|
||||||
name: 'vmName',
|
name: 'vmName',
|
||||||
labelKey: 'harvester.addons.forklift.selectVms.columns.vmName',
|
labelKey: 'harvester.addons.vmMigration.selectVms.columns.vmName',
|
||||||
value: 'vmName',
|
value: 'vmName',
|
||||||
sort: ['vmName'],
|
sort: ['vmName'],
|
||||||
subLabel: 'Identifier',
|
subLabel: 'Identifier',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'os',
|
name: 'os',
|
||||||
labelKey: 'harvester.addons.forklift.selectVms.columns.os',
|
labelKey: 'harvester.addons.vmMigration.selectVms.columns.os',
|
||||||
value: 'os',
|
value: 'os',
|
||||||
sort: ['os'],
|
sort: ['os'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'resources',
|
name: 'resources',
|
||||||
labelKey: 'harvester.addons.forklift.selectVms.columns.resources',
|
labelKey: 'harvester.addons.vmMigration.selectVms.columns.resources',
|
||||||
value: 'resources',
|
value: 'resources',
|
||||||
sort: false,
|
sort: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'powerState',
|
name: 'powerState',
|
||||||
labelKey: 'harvester.addons.forklift.selectVms.columns.powerState',
|
labelKey: 'harvester.addons.vmMigration.selectVms.columns.powerState',
|
||||||
value: 'powerState',
|
value: 'powerState',
|
||||||
sort: ['powerState'],
|
sort: ['powerState'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'network',
|
name: 'network',
|
||||||
labelKey: 'harvester.addons.forklift.selectVms.columns.network',
|
labelKey: 'harvester.addons.vmMigration.selectVms.columns.network',
|
||||||
value: 'network',
|
value: 'network',
|
||||||
sort: ['network'],
|
sort: ['network'],
|
||||||
subLabel: 'Identifier',
|
subLabel: 'Identifier',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'datastore',
|
name: 'datastore',
|
||||||
labelKey: 'harvester.addons.forklift.selectVms.columns.datastore',
|
labelKey: 'harvester.addons.vmMigration.selectVms.columns.datastore',
|
||||||
value: 'datastore',
|
value: 'datastore',
|
||||||
sort: ['datastore'],
|
sort: ['datastore'],
|
||||||
subLabel: 'Identifier',
|
subLabel: 'Identifier',
|
||||||
@ -185,6 +236,8 @@ const onSelect = (rows) => {
|
|||||||
|
|
||||||
allVMsSelected.value = selectedVMIds.value.size === discoveredVMs.value.length;
|
allVMsSelected.value = selectedVMIds.value.size === discoveredVMs.value.length;
|
||||||
selectedVMs.value = discoveredVMs.value.filter((vm) => selectedVMIds.value.has(vm.id));
|
selectedVMs.value = discoveredVMs.value.filter((vm) => selectedVMIds.value.has(vm.id));
|
||||||
|
|
||||||
|
emit('complete', { selectedVMs: selectedVMs.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearSelection = () => {
|
const clearSelection = () => {
|
||||||
@ -230,112 +283,150 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const cancel = () => {
|
const refreshing = ref(false);
|
||||||
currentRouter().push({
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
const isLoading = computed(() => loading.value || refreshing.value);
|
||||||
params: {
|
|
||||||
product: store.getters['productId'],
|
watch(isLoading, (val) => {
|
||||||
cluster: store.getters['clusterId'],
|
emit('loading', val);
|
||||||
}
|
}, { immediate: true });
|
||||||
});
|
|
||||||
|
const fetchVMs = async() => {
|
||||||
|
if (!props.provider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 [vmsResp, networksResp, datastoresResp] = await Promise.all([
|
||||||
|
fetch(`${ baseUrl }/vms`).then((r) => r.json()),
|
||||||
|
fetch(`${ baseUrl }/networks`).then((r) => r.json()).catch(() => []),
|
||||||
|
fetch(`${ baseUrl }/datastores`).then((r) => r.json()).catch(() => []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (Array.isArray(vmsResp)) {
|
||||||
|
discoveredVMs.value = vmsResp;
|
||||||
|
} else if (vmsResp?.data && Array.isArray(vmsResp.data)) {
|
||||||
|
discoveredVMs.value = vmsResp.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const networks = Array.isArray(networksResp) ? networksResp : (networksResp?.data || []);
|
||||||
|
const datastores = Array.isArray(datastoresResp) ? datastoresResp : (datastoresResp?.data || []);
|
||||||
|
|
||||||
|
networkMap.value = networks.reduce((map, n) => {
|
||||||
|
map[n.id] = n.name;
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
datastoreMap.value = datastores.reduce((map, d) => {
|
||||||
|
map[d.id] = d.name;
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
lastFetchedAt.value = Date.now();
|
||||||
|
tableRows.value = buildTableRows();
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveSelection = () => {
|
const refreshVMs = async() => {
|
||||||
const vmIds = selectedVMs.value.map((vm) => vm.id || vm.vmId || vm.metadata?.name);
|
refreshing.value = true;
|
||||||
|
skipNextSelectionEvent = true;
|
||||||
|
|
||||||
currentRouter().push({
|
const previousSelectedIds = new Set(selectedVMIds.value);
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-configure-mappings`,
|
|
||||||
params: {
|
try {
|
||||||
product: store.getters['productId'],
|
await fetchVMs();
|
||||||
cluster: store.getters['clusterId'],
|
} catch (e) {
|
||||||
},
|
discoveredVMs.value = [];
|
||||||
query: {
|
tableRows.value = [];
|
||||||
provider: providerName.value,
|
}
|
||||||
vms: JSON.stringify(vmIds),
|
|
||||||
|
selectedVMIds.value = new Set(
|
||||||
|
discoveredVMs.value
|
||||||
|
.filter((vm) => previousSelectedIds.has(vm.id))
|
||||||
|
.map((vm) => vm.id)
|
||||||
|
);
|
||||||
|
selectedVMs.value = discoveredVMs.value.filter((vm) => selectedVMIds.value.has(vm.id));
|
||||||
|
allVMsSelected.value = selectedVMIds.value.size > 0 && selectedVMIds.value.size === discoveredVMs.value.length;
|
||||||
|
|
||||||
|
props.stepData.discoveredVMs = discoveredVMs.value;
|
||||||
|
props.stepData.selectedVMIds = selectedVMIds.value;
|
||||||
|
props.stepData.tableRows = tableRows.value;
|
||||||
|
emit('complete', { selectedVMs: selectedVMs.value });
|
||||||
|
refreshing.value = false;
|
||||||
|
|
||||||
|
const table = sortableTableRef.value;
|
||||||
|
|
||||||
|
if (table) {
|
||||||
|
const rowsToReselect = (table.pagedRows || []).filter((row) => selectedVMIds.value.has(row._original?.id));
|
||||||
|
|
||||||
|
if (rowsToReselect.length > 0) {
|
||||||
|
table.update(rowsToReselect, []);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const init = async() => {
|
const init = async() => {
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
if (discoveredVMs.value.length > 0) {
|
||||||
|
if (!lastFetchedAt.value) {
|
||||||
allProviders.value = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
lastFetchedAt.value = Date.now();
|
||||||
|
|
||||||
const queryProvider = currentRoute().query.provider;
|
|
||||||
|
|
||||||
if (queryProvider) {
|
|
||||||
provider.value = allProviders.value.find(
|
|
||||||
(p) => p.metadata.name === queryProvider && p.metadata.namespace === 'forklift'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider.value) {
|
|
||||||
try {
|
|
||||||
const providerUid = provider.value.metadata.uid;
|
|
||||||
const providerType = provider.value.spec?.type || 'vsphere';
|
|
||||||
const baseUrl = `https://forklift-apir.13.48.147.135.sslip.io/providers/${ providerType }/${ providerUid }`;
|
|
||||||
|
|
||||||
const [vmsResp, networksResp, datastoresResp] = await Promise.all([
|
|
||||||
fetch(`${ baseUrl }/vms`).then((r) => r.json()),
|
|
||||||
fetch(`${ baseUrl }/networks`).then((r) => r.json()).catch(() => []),
|
|
||||||
fetch(`${ baseUrl }/datastores`).then((r) => r.json()).catch(() => []),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (Array.isArray(vmsResp)) {
|
|
||||||
discoveredVMs.value = vmsResp;
|
|
||||||
} else if (vmsResp?.data && Array.isArray(vmsResp.data)) {
|
|
||||||
discoveredVMs.value = vmsResp.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const networks = Array.isArray(networksResp) ? networksResp : (networksResp?.data || []);
|
|
||||||
const datastores = Array.isArray(datastoresResp) ? datastoresResp : (datastoresResp?.data || []);
|
|
||||||
|
|
||||||
networkMap.value = networks.reduce((map, n) => {
|
|
||||||
map[n.id] = n.name;
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}, {});
|
|
||||||
datastoreMap.value = datastores.reduce((map, d) => {
|
|
||||||
map[d.id] = d.name;
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}, {});
|
|
||||||
} catch (e) {
|
|
||||||
discoveredVMs.value = [];
|
|
||||||
}
|
}
|
||||||
|
tableRows.value = buildTableRows();
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetchVMs();
|
||||||
|
} catch (e) {
|
||||||
|
discoveredVMs.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
tableRows.value = buildTableRows();
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Emit initial complete if VMs are already selected (restored from stepData)
|
||||||
|
if (selectedVMs.value.length > 0) {
|
||||||
|
emit('complete', { selectedVMs: selectedVMs.value });
|
||||||
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<Loading v-if="refreshing" />
|
||||||
<Loading v-if="loading" />
|
<Loading v-if="loading" />
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
|
class="select-vms-step"
|
||||||
>
|
>
|
||||||
<Masthead
|
<p class="text-muted line-height-20">
|
||||||
:schema="schema"
|
{{ t('harvester.addons.vmMigration.selectVms.discovered', { count: vmCount }) }}
|
||||||
:resource="schema.id"
|
<router-link
|
||||||
:type-display="t('harvester.addons.forklift.selectVms.title')"
|
v-if="provider"
|
||||||
:is-creatable="false"
|
class="provider-link"
|
||||||
>
|
:to="provider._detailLocation"
|
||||||
<template #subHeader>
|
>
|
||||||
<p class="text-muted mmt-5">
|
{{ providerName }}
|
||||||
{{ t('harvester.addons.forklift.selectVms.discovered', { count: vmCount }) }}
|
</router-link>
|
||||||
<a
|
<br>
|
||||||
v-if="providerName"
|
<span
|
||||||
class="provider-link"
|
v-if="lastSyncedTime"
|
||||||
>{{ providerName }}</a>
|
>
|
||||||
<br>
|
{{ t('harvester.addons.vmMigration.selectVms.lastSynced', { time: lastSyncedTime }) }}
|
||||||
<span class="text-small">{{ t('harvester.addons.forklift.selectVms.lastSynced') }}</span>
|
<a
|
||||||
</p>
|
role="button"
|
||||||
</template>
|
class="text-bold"
|
||||||
</Masthead>
|
:class="{ disabled: refreshing }"
|
||||||
|
@click.prevent="refreshVMs"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmMigration.selectVms.refreshNow') }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
<!-- Discovered VMs table -->
|
<!-- Discovered VMs table -->
|
||||||
<SortableTable
|
<SortableTable
|
||||||
ref="sortableTableRef"
|
ref="sortableTableRef"
|
||||||
@ -353,9 +444,9 @@ init();
|
|||||||
<template #header-left>
|
<template #header-left>
|
||||||
<div class="vm-table-title">
|
<div class="vm-table-title">
|
||||||
<h3 class="m-0">
|
<h3 class="m-0">
|
||||||
{{ t('harvester.addons.forklift.selectVms.availableVms') }}
|
{{ t('harvester.addons.vmMigration.selectVms.availableVms') }}
|
||||||
</h3>
|
</h3>
|
||||||
<span class="text-muted">{{ selectedCount }} {{ t('harvester.addons.forklift.selectVms.selected') }}</span>
|
<span class="text-muted">{{ selectedCount }} {{ t('harvester.addons.vmMigration.selectVms.selected') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #cell:vmName="{ row }">
|
<template #cell:vmName="{ row }">
|
||||||
@ -410,51 +501,30 @@ init();
|
|||||||
class="select-all-banner-cell"
|
class="select-all-banner-cell"
|
||||||
>
|
>
|
||||||
<template v-if="allVMsSelected">
|
<template v-if="allVMsSelected">
|
||||||
<span>{{ t('harvester.addons.forklift.selectVms.selectAllBanner.allSelected') }}</span>
|
<span>{{ t('harvester.addons.vmMigration.selectVms.selectAllBanner.allSelected') }}</span>
|
||||||
<a
|
<a
|
||||||
role="button"
|
role="button"
|
||||||
@click.prevent="clearSelection"
|
@click.prevent="clearSelection"
|
||||||
>
|
>
|
||||||
{{ t('harvester.addons.forklift.selectVms.selectAllBanner.clearSelection') }}
|
{{ t('harvester.addons.vmMigration.selectVms.selectAllBanner.clearSelection') }}
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span>{{ t('harvester.addons.forklift.selectVms.selectAllBanner.pageOnly') }}</span>
|
<span>{{ t('harvester.addons.vmMigration.selectVms.selectAllBanner.pageOnly') }}</span>
|
||||||
<a
|
<a
|
||||||
role="button"
|
role="button"
|
||||||
@click.prevent="selectAllVMs"
|
@click.prevent="selectAllVMs"
|
||||||
>
|
>
|
||||||
{{ t('harvester.addons.forklift.selectVms.selectAllBanner.selectAll', { count: vmCount }) }}
|
{{ t('harvester.addons.vmMigration.selectVms.selectAllBanner.selectAll', { count: vmCount }) }}
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
<div class="actions-footer">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary"
|
|
||||||
@click="cancel"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn role-primary"
|
|
||||||
:disabled="selectedCount === 0"
|
|
||||||
@click="saveSelection"
|
|
||||||
>
|
|
||||||
{{ t('harvester.addons.forklift.selectVms.saveSelection') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.provider-link {
|
|
||||||
color: var(--primary);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vm-table-title {
|
.vm-table-title {
|
||||||
h4 {
|
h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -480,6 +550,17 @@ init();
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select-vms-step {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.line-height-20 {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.select-all-banner-row) {
|
:deep(.select-all-banner-row) {
|
||||||
.select-all-banner-cell {
|
.select-all-banner-cell {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -500,11 +581,4 @@ init();
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@ -1329,7 +1329,7 @@ export function init($plugin, store) {
|
|||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Forklift Addon UI Flow
|
// Forklift Addon UI Flow
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
weightGroup('forklift', 0, false);
|
weightGroup('vmMigration', 0, false);
|
||||||
|
|
||||||
// Provider
|
// Provider
|
||||||
headers(HCI.FORKLIFT_PROVIDER, [
|
headers(HCI.FORKLIFT_PROVIDER, [
|
||||||
@ -1349,8 +1349,8 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
virtualType({
|
virtualType({
|
||||||
name: HCI.FORKLIFT_PROVIDER,
|
name: HCI.FORKLIFT_PROVIDER,
|
||||||
labelKey: 'harvester.addons.forklift.labels.provider',
|
labelKey: 'harvester.addons.vmMigration.labels.provider',
|
||||||
group: 'forklift::Advanced',
|
group: 'vmMigration::Advanced',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
@ -1376,8 +1376,8 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
virtualType({
|
virtualType({
|
||||||
name: HCI.FORKLIFT_NETWORK_MAP,
|
name: HCI.FORKLIFT_NETWORK_MAP,
|
||||||
labelKey: 'harvester.addons.forklift.labels.networkMap',
|
labelKey: 'harvester.addons.vmMigration.labels.networkMap',
|
||||||
group: 'forklift::Advanced',
|
group: 'vmMigration::Advanced',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
@ -1403,8 +1403,8 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
virtualType({
|
virtualType({
|
||||||
name: HCI.FORKLIFT_STORAGE_MAP,
|
name: HCI.FORKLIFT_STORAGE_MAP,
|
||||||
labelKey: 'harvester.addons.forklift.labels.storageMap',
|
labelKey: 'harvester.addons.vmMigration.labels.storageMap',
|
||||||
group: 'forklift::Advanced',
|
group: 'vmMigration::Advanced',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
@ -1431,8 +1431,8 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
virtualType({
|
virtualType({
|
||||||
name: HCI.FORKLIFT_PLAN,
|
name: HCI.FORKLIFT_PLAN,
|
||||||
labelKey: 'harvester.addons.forklift.labels.plan',
|
labelKey: 'harvester.addons.vmMigration.labels.plan',
|
||||||
group: 'forklift::Advanced',
|
group: 'vmMigration::Advanced',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
@ -1457,8 +1457,8 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
virtualType({
|
virtualType({
|
||||||
name: HCI.FORKLIFT_MIGRATION,
|
name: HCI.FORKLIFT_MIGRATION,
|
||||||
labelKey: 'harvester.addons.forklift.labels.migration',
|
labelKey: 'harvester.addons.vmMigration.labels.migration',
|
||||||
group: 'forklift::Advanced',
|
group: 'vmMigration::Advanced',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
@ -1468,11 +1468,11 @@ export function init($plugin, store) {
|
|||||||
configureType('forklift-create', { subTypes: [HCI.FORKLIFT_PLAN] });
|
configureType('forklift-create', { subTypes: [HCI.FORKLIFT_PLAN] });
|
||||||
virtualType({
|
virtualType({
|
||||||
name: 'forklift-create',
|
name: 'forklift-create',
|
||||||
labelKey: 'harvester.addons.forklift.labels.dashboard',
|
labelKey: 'harvester.addons.vmMigration.labels.dashboard',
|
||||||
group: 'forklift',
|
group: 'vmMigration',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
name: `${ PRODUCT_NAME }-c-cluster-vm-migration`,
|
||||||
params: {}
|
params: {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1481,7 +1481,7 @@ export function init($plugin, store) {
|
|||||||
registerAddonSideNav(store, PRODUCT_NAME, {
|
registerAddonSideNav(store, PRODUCT_NAME, {
|
||||||
addonName: ADD_ONS.FORKLIFT_OPERATOR,
|
addonName: ADD_ONS.FORKLIFT_OPERATOR,
|
||||||
resourceType: HCI.ADD_ONS,
|
resourceType: HCI.ADD_ONS,
|
||||||
navGroup: 'forklift',
|
navGroup: 'vmMigration',
|
||||||
types: [
|
types: [
|
||||||
'forklift-create',
|
'forklift-create',
|
||||||
]
|
]
|
||||||
@ -1489,7 +1489,7 @@ export function init($plugin, store) {
|
|||||||
registerAddonSideNav(store, PRODUCT_NAME, {
|
registerAddonSideNav(store, PRODUCT_NAME, {
|
||||||
addonName: ADD_ONS.FORKLIFT_OPERATOR,
|
addonName: ADD_ONS.FORKLIFT_OPERATOR,
|
||||||
resourceType: HCI.ADD_ONS,
|
resourceType: HCI.ADD_ONS,
|
||||||
navGroup: 'forklift::Advanced',
|
navGroup: 'vmMigration::Advanced',
|
||||||
types: [
|
types: [
|
||||||
HCI.FORKLIFT_PROVIDER,
|
HCI.FORKLIFT_PROVIDER,
|
||||||
HCI.FORKLIFT_NETWORK_MAP,
|
HCI.FORKLIFT_NETWORK_MAP,
|
||||||
|
|||||||
@ -238,7 +238,7 @@ export const VM_IMPORT_SOURCE_OVA_STATUS = {
|
|||||||
// Provider type column in forklift.konveyor.io.provider list page
|
// Provider type column in forklift.konveyor.io.provider list page
|
||||||
export const FORKLIFT_PROVIDER_TYPE = {
|
export const FORKLIFT_PROVIDER_TYPE = {
|
||||||
name: 'providerType',
|
name: 'providerType',
|
||||||
labelKey: 'harvester.tableHeaders.forkliftProviderType',
|
labelKey: 'harvester.tableHeaders.vmMigrationProviderType',
|
||||||
value: 'spec.type',
|
value: 'spec.type',
|
||||||
sort: 'spec.type',
|
sort: 'spec.type',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@ -247,7 +247,7 @@ export const FORKLIFT_PROVIDER_TYPE = {
|
|||||||
// Provider URL column in forklift.konveyor.io.provider list page
|
// Provider URL column in forklift.konveyor.io.provider list page
|
||||||
export const FORKLIFT_PROVIDER_URL = {
|
export const FORKLIFT_PROVIDER_URL = {
|
||||||
name: 'providerUrl',
|
name: 'providerUrl',
|
||||||
labelKey: 'harvester.tableHeaders.forkliftProviderUrl',
|
labelKey: 'harvester.tableHeaders.vmMigrationProviderUrl',
|
||||||
value: 'spec.url',
|
value: 'spec.url',
|
||||||
sort: 'spec.url',
|
sort: 'spec.url',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@ -256,7 +256,7 @@ export const FORKLIFT_PROVIDER_URL = {
|
|||||||
// Source provider column in forklift network/storage map list page
|
// Source provider column in forklift network/storage map list page
|
||||||
export const FORKLIFT_MAP_SOURCE_PROVIDER = {
|
export const FORKLIFT_MAP_SOURCE_PROVIDER = {
|
||||||
name: 'sourceProvider',
|
name: 'sourceProvider',
|
||||||
labelKey: 'harvester.tableHeaders.forkliftMapSourceProvider',
|
labelKey: 'harvester.tableHeaders.vmMigrationMapSourceProvider',
|
||||||
value: 'spec.provider.source.name',
|
value: 'spec.provider.source.name',
|
||||||
sort: 'spec.provider.source.name',
|
sort: 'spec.provider.source.name',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@ -265,7 +265,7 @@ export const FORKLIFT_MAP_SOURCE_PROVIDER = {
|
|||||||
// Destination provider column in forklift network/storage map list page
|
// Destination provider column in forklift network/storage map list page
|
||||||
export const FORKLIFT_MAP_DEST_PROVIDER = {
|
export const FORKLIFT_MAP_DEST_PROVIDER = {
|
||||||
name: 'destProvider',
|
name: 'destProvider',
|
||||||
labelKey: 'harvester.tableHeaders.forkliftMapDestProvider',
|
labelKey: 'harvester.tableHeaders.vmMigrationMapDestProvider',
|
||||||
value: 'spec.provider.destination.name',
|
value: 'spec.provider.destination.name',
|
||||||
sort: 'spec.provider.destination.name',
|
sort: 'spec.provider.destination.name',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@ -274,7 +274,7 @@ export const FORKLIFT_MAP_DEST_PROVIDER = {
|
|||||||
// Target namespace column in forklift.konveyor.io.plan list page
|
// Target namespace column in forklift.konveyor.io.plan list page
|
||||||
export const FORKLIFT_PLAN_TARGET_NS = {
|
export const FORKLIFT_PLAN_TARGET_NS = {
|
||||||
name: 'targetNamespace',
|
name: 'targetNamespace',
|
||||||
labelKey: 'harvester.tableHeaders.forkliftPlanTargetNs',
|
labelKey: 'harvester.tableHeaders.vmMigrationPlanTargetNs',
|
||||||
value: 'spec.targetNamespace',
|
value: 'spec.targetNamespace',
|
||||||
sort: 'spec.targetNamespace',
|
sort: 'spec.targetNamespace',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@ -283,7 +283,7 @@ export const FORKLIFT_PLAN_TARGET_NS = {
|
|||||||
// VM count column in forklift.konveyor.io.plan list page
|
// VM count column in forklift.konveyor.io.plan list page
|
||||||
export const FORKLIFT_PLAN_VM_COUNT = {
|
export const FORKLIFT_PLAN_VM_COUNT = {
|
||||||
name: 'vmCount',
|
name: 'vmCount',
|
||||||
labelKey: 'harvester.tableHeaders.forkliftPlanVmCount',
|
labelKey: 'harvester.tableHeaders.vmMigrationPlanVmCount',
|
||||||
value: 'spec.vms.length',
|
value: 'spec.vms.length',
|
||||||
sort: 'spec.vms.length',
|
sort: 'spec.vms.length',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@ -292,7 +292,7 @@ export const FORKLIFT_PLAN_VM_COUNT = {
|
|||||||
// Plan reference column in forklift.konveyor.io.migration list page
|
// Plan reference column in forklift.konveyor.io.migration list page
|
||||||
export const FORKLIFT_MIGRATION_PLAN = {
|
export const FORKLIFT_MIGRATION_PLAN = {
|
||||||
name: 'plan',
|
name: 'plan',
|
||||||
labelKey: 'harvester.tableHeaders.forkliftMigrationPlan',
|
labelKey: 'harvester.tableHeaders.vmMigrationMigrationPlan',
|
||||||
value: 'spec.plan.name',
|
value: 'spec.plan.name',
|
||||||
sort: 'spec.plan.name',
|
sort: 'spec.plan.name',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
|
wizard:
|
||||||
|
create: Create Migration
|
||||||
|
next: Proceed
|
||||||
|
previous: Back
|
||||||
|
|
||||||
generic:
|
generic:
|
||||||
tip: Tip
|
tip: Tip
|
||||||
resourceExternalLinkTips: 'External Link'
|
resourceExternalLinkTips: 'External Link'
|
||||||
@ -22,7 +27,7 @@ nav:
|
|||||||
Logging: Logging
|
Logging: Logging
|
||||||
'Monitoring and Logging': Monitoring and Logging
|
'Monitoring and Logging': Monitoring and Logging
|
||||||
vmimport: Virtual Machine Imports
|
vmimport: Virtual Machine Imports
|
||||||
forklift: Forklift Migration
|
vmMigration: VM Migration
|
||||||
|
|
||||||
resourceTable:
|
resourceTable:
|
||||||
groupBy:
|
groupBy:
|
||||||
@ -353,13 +358,13 @@ harvester:
|
|||||||
v4ip: V4 IP
|
v4ip: V4 IP
|
||||||
v6ip: V6 IP
|
v6ip: V6 IP
|
||||||
eipName: EIP Name
|
eipName: EIP Name
|
||||||
forkliftProviderType: Type
|
vmMigrationProviderType: Type
|
||||||
forkliftProviderUrl: URL
|
vmMigrationProviderUrl: URL
|
||||||
forkliftMapSourceProvider: Source Provider
|
vmMigrationMapSourceProvider: Source Provider
|
||||||
forkliftMapDestProvider: Destination Provider
|
vmMigrationMapDestProvider: Destination Provider
|
||||||
forkliftPlanTargetNs: Target Namespace
|
vmMigrationPlanTargetNs: Target Namespace
|
||||||
forkliftPlanVmCount: VMs
|
vmMigrationPlanVmCount: VMs
|
||||||
forkliftMigrationPlan: Plan
|
vmMigrationMigrationPlan: Plan
|
||||||
tab:
|
tab:
|
||||||
volume: Volumes
|
volume: Volumes
|
||||||
network: Networks
|
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.
|
'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.'
|
'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:
|
labels:
|
||||||
dashboard: Migrations
|
dashboard: Migrations
|
||||||
provider: Providers
|
provider: Providers
|
||||||
@ -1815,6 +1820,21 @@ harvester:
|
|||||||
storageMap: Storage Maps
|
storageMap: Storage Maps
|
||||||
plan: Migration Plans
|
plan: Migration Plans
|
||||||
migration: Migrations
|
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:
|
fields:
|
||||||
username: Username
|
username: Username
|
||||||
password: Password
|
password: Password
|
||||||
@ -1832,7 +1852,11 @@ harvester:
|
|||||||
urlHint: Enter the full URL including https://
|
urlHint: Enter the full URL including https://
|
||||||
skipSsl: Skip SSL certificate verification
|
skipSsl: Skip SSL certificate verification
|
||||||
skipSslHint: Not recommended for production environments
|
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
|
testSuccess: Connection test passed
|
||||||
testFailed: Connection test failed
|
testFailed: Connection test failed
|
||||||
testMissingFields: Please fill in all required fields before testing
|
testMissingFields: Please fill in all required fields before testing
|
||||||
@ -1841,8 +1865,10 @@ harvester:
|
|||||||
saveExisting: Check Provider and Continue
|
saveExisting: Check Provider and Continue
|
||||||
selectVms:
|
selectVms:
|
||||||
title: Select Virtual Machines
|
title: Select Virtual Machines
|
||||||
|
description: Select the virtual machines you want to migrate from the source provider
|
||||||
discovered: "{count} VMs discovered from"
|
discovered: "{count} VMs discovered from"
|
||||||
lastSynced: Last synced 5 minutes ago
|
lastSynced: "Last synced {time} ago • "
|
||||||
|
refreshNow: Refresh now
|
||||||
availableVms: Available Virtual Machines
|
availableVms: Available Virtual Machines
|
||||||
selected: selected
|
selected: selected
|
||||||
saveSelection: Save Selection and Continue
|
saveSelection: Save Selection and Continue
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { PRODUCT_NAME } from '../config/harvester';
|
|||||||
export default class ForkliftPlan extends HarvesterResource {
|
export default class ForkliftPlan extends HarvesterResource {
|
||||||
get listLocation() {
|
get listLocation() {
|
||||||
return {
|
return {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
name: `${ PRODUCT_NAME }-c-cluster-vm-migration`,
|
||||||
params: {
|
params: {
|
||||||
product: this.$rootGetters['productId'],
|
product: this.$rootGetters['productId'],
|
||||||
cluster: this.$rootGetters['clusterId'],
|
cluster: this.$rootGetters['clusterId'],
|
||||||
|
|||||||
@ -1,654 +0,0 @@
|
|||||||
<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 AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import { RcItemCard } from '@components/RcItemCard';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
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 { currentRouter, currentRoute } from '../../../../utils/router';
|
|
||||||
|
|
||||||
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 vms = ref([]);
|
|
||||||
const networkMappings = ref([]);
|
|
||||||
const storageMappings = ref([]);
|
|
||||||
const planName = ref('');
|
|
||||||
const errors = ref([]);
|
|
||||||
const loading = ref(true);
|
|
||||||
|
|
||||||
const NAMESPACE = 'forklift';
|
|
||||||
const TARGET_NAMESPACE = 'default';
|
|
||||||
const providerName = computed(() => currentRoute().query.provider || 'vsphere');
|
|
||||||
const networkMapName = computed(() => currentRoute().query.networkMap || '');
|
|
||||||
const storageMapName = computed(() => currentRoute().query.storageMap || '');
|
|
||||||
|
|
||||||
const totalVCpu = computed(() => vms.value.reduce((sum, vm) => sum + (vm.cpuCount || vm.numCPU || 0), 0));
|
|
||||||
|
|
||||||
const totalMemoryGB = computed(() => {
|
|
||||||
const totalMB = vms.value.reduce((sum, vm) => sum + (vm.memoryMB || vm.memory || 0), 0);
|
|
||||||
|
|
||||||
return Math.round(totalMB / 1024);
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalStorageGB = computed(() => {
|
|
||||||
let totalBytes = 0;
|
|
||||||
|
|
||||||
vms.value.forEach((vm) => {
|
|
||||||
if (vm.disks && vm.disks.length > 0) {
|
|
||||||
totalBytes += vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Math.round(totalBytes / (1024 * 1024 * 1024));
|
|
||||||
});
|
|
||||||
|
|
||||||
const vmCards = computed(() => {
|
|
||||||
return vms.value.map((vm) => {
|
|
||||||
const cpus = vm.cpuCount || vm.numCPU || 0;
|
|
||||||
const memMB = vm.memoryMB || vm.memory || 0;
|
|
||||||
const memGB = memMB ? `${ Math.round(memMB / 1024) } GB` : '-';
|
|
||||||
|
|
||||||
let totalDiskBytes = 0;
|
|
||||||
|
|
||||||
if (vm.disks && vm.disks.length > 0) {
|
|
||||||
totalDiskBytes = vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const diskDisplay = totalDiskBytes ? `${ Math.round(totalDiskBytes / (1024 * 1024 * 1024)) } GB` : '-';
|
|
||||||
const os = vm.guestName || vm.guestOS || vm.os || '-';
|
|
||||||
|
|
||||||
const vmNetworkMappings = networkMappings.value.filter((m) => m.usedBy && m.usedBy.includes(vm.name || vm.id));
|
|
||||||
const vmStorageMappings = storageMappings.value.filter((m) => m.usedBy && m.usedBy.includes(vm.name || vm.id));
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: vm.id,
|
|
||||||
name: vm.name || vm.id,
|
|
||||||
os,
|
|
||||||
cpus,
|
|
||||||
memGB,
|
|
||||||
diskDisplay,
|
|
||||||
networkMappings: vmNetworkMappings,
|
|
||||||
storageMappings: vmStorageMappings,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
currentRouter().push({
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
|
||||||
params: {
|
|
||||||
product: store.getters['productId'],
|
|
||||||
cluster: store.getters['clusterId'],
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const startMigration = async(buttonCb) => {
|
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Rename NetworkMap and StorageMap to use the plan name
|
|
||||||
const allNetworkMaps = store.getters[`${ inStore }/all`](HCI.FORKLIFT_NETWORK_MAP) || [];
|
|
||||||
const allStorageMaps = store.getters[`${ inStore }/all`](HCI.FORKLIFT_STORAGE_MAP) || [];
|
|
||||||
|
|
||||||
const currentNetworkMap = allNetworkMaps.find((nm) => nm.metadata.name === networkMapName.value && nm.metadata.namespace === NAMESPACE);
|
|
||||||
const currentStorageMap = allStorageMaps.find((sm) => sm.metadata.name === storageMapName.value && sm.metadata.namespace === NAMESPACE);
|
|
||||||
|
|
||||||
const newNetworkMapName = `${ planName.value }-network-map`;
|
|
||||||
const newStorageMapName = `${ planName.value }-storage-map`;
|
|
||||||
|
|
||||||
// Recreate NetworkMap with new name
|
|
||||||
let finalNetworkMapName = networkMapName.value;
|
|
||||||
|
|
||||||
if (currentNetworkMap) {
|
|
||||||
const networkMapData = await store.dispatch(`${ inStore }/create`, {
|
|
||||||
type: HCI.FORKLIFT_NETWORK_MAP,
|
|
||||||
metadata: {
|
|
||||||
name: newNetworkMapName,
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
spec: currentNetworkMap.spec,
|
|
||||||
});
|
|
||||||
|
|
||||||
await networkMapData.save();
|
|
||||||
await currentNetworkMap.remove();
|
|
||||||
finalNetworkMapName = newNetworkMapName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recreate StorageMap with new name
|
|
||||||
let finalStorageMapName = storageMapName.value;
|
|
||||||
|
|
||||||
if (currentStorageMap) {
|
|
||||||
const storageMapData = await store.dispatch(`${ inStore }/create`, {
|
|
||||||
type: HCI.FORKLIFT_STORAGE_MAP,
|
|
||||||
metadata: {
|
|
||||||
name: newStorageMapName,
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
spec: currentStorageMap.spec,
|
|
||||||
});
|
|
||||||
|
|
||||||
await storageMapData.save();
|
|
||||||
await currentStorageMap.remove();
|
|
||||||
finalStorageMapName = newStorageMapName;
|
|
||||||
}
|
|
||||||
|
|
||||||
const planSpec = {
|
|
||||||
provider: {
|
|
||||||
source: {
|
|
||||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
|
||||||
kind: 'Provider',
|
|
||||||
name: providerName.value,
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
destination: {
|
|
||||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
|
||||||
kind: 'Provider',
|
|
||||||
name: 'host',
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
map: {
|
|
||||||
network: {
|
|
||||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
|
||||||
kind: 'NetworkMap',
|
|
||||||
name: finalNetworkMapName,
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
storage: {
|
|
||||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
|
||||||
kind: 'StorageMap',
|
|
||||||
name: finalStorageMapName,
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
targetNamespace: 'default',
|
|
||||||
vms: vms.value.map((vm) => ({
|
|
||||||
id: vm.id,
|
|
||||||
name: vm.name || vm.id,
|
|
||||||
})),
|
|
||||||
warm: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const plan = await store.dispatch(`${ inStore }/create`, {
|
|
||||||
type: HCI.FORKLIFT_PLAN,
|
|
||||||
metadata: {
|
|
||||||
name: planName.value,
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
spec: planSpec,
|
|
||||||
});
|
|
||||||
|
|
||||||
await plan.save();
|
|
||||||
|
|
||||||
// Build ownerReference pointing to the Plan
|
|
||||||
const planOwnerRef = {
|
|
||||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
|
||||||
kind: 'Plan',
|
|
||||||
name: plan.metadata.name,
|
|
||||||
uid: plan.metadata.uid,
|
|
||||||
blockOwnerDeletion: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create Migration owned by Plan
|
|
||||||
const migrationName = `${ planName.value }-migration-${ Math.random().toString(36).substring(2, 7) }`;
|
|
||||||
|
|
||||||
const migration = await store.dispatch(`${ inStore }/create`, {
|
|
||||||
type: HCI.FORKLIFT_MIGRATION,
|
|
||||||
metadata: {
|
|
||||||
name: migrationName,
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
ownerReferences: [planOwnerRef],
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
plan: {
|
|
||||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
|
||||||
kind: 'Plan',
|
|
||||||
name: planName.value,
|
|
||||||
namespace: NAMESPACE,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await migration.save();
|
|
||||||
|
|
||||||
// Set Plan as owner of the newly created NetworkMap and StorageMap
|
|
||||||
const updatedNetworkMaps = store.getters[`${ inStore }/all`](HCI.FORKLIFT_NETWORK_MAP) || [];
|
|
||||||
const updatedStorageMaps = store.getters[`${ inStore }/all`](HCI.FORKLIFT_STORAGE_MAP) || [];
|
|
||||||
|
|
||||||
const newNetworkMap = updatedNetworkMaps.find((nm) => nm.metadata.name === finalNetworkMapName && nm.metadata.namespace === NAMESPACE);
|
|
||||||
const newStorageMap = updatedStorageMaps.find((sm) => sm.metadata.name === finalStorageMapName && sm.metadata.namespace === NAMESPACE);
|
|
||||||
|
|
||||||
if (newNetworkMap) {
|
|
||||||
newNetworkMap.metadata.ownerReferences = [planOwnerRef];
|
|
||||||
await newNetworkMap.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newStorageMap) {
|
|
||||||
newStorageMap.metadata.ownerReferences = [planOwnerRef];
|
|
||||||
await newStorageMap.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
currentRouter().push({
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
|
||||||
params: {
|
|
||||||
product: store.getters['productId'],
|
|
||||||
cluster: store.getters['clusterId'],
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
buttonCb(true);
|
|
||||||
} catch (err) {
|
|
||||||
errors.value = [err.message || err];
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = async() => {
|
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER }).catch(() => {});
|
|
||||||
await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP }).catch(() => {});
|
|
||||||
await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_STORAGE_MAP }).catch(() => {});
|
|
||||||
|
|
||||||
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) {
|
|
||||||
try {
|
|
||||||
const vmIds = JSON.parse(vmsParam);
|
|
||||||
const providerUid = provider?.metadata?.uid;
|
|
||||||
const providerType = provider?.spec?.type || 'vsphere';
|
|
||||||
const baseUrl = `https://forklift-apir.13.48.147.135.sslip.io/providers/${ providerType }/${ providerUid }`;
|
|
||||||
const allVms = await fetch(`${ baseUrl }/vms`).then((r) => r.json()).catch(() => []);
|
|
||||||
const vmList = Array.isArray(allVms) ? allVms : (allVms?.data || []);
|
|
||||||
|
|
||||||
vms.value = vmIds.map((id) => {
|
|
||||||
const found = vmList.find((vm) => vm.id === id);
|
|
||||||
|
|
||||||
return found || {
|
|
||||||
id, name: id, networks: [], disks: [], cpuCount: 0, memoryMB: 0, guestName: ''
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
vms.value = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build mapping display data from the actual NetworkMap/StorageMap resources
|
|
||||||
const allNetworkMaps = store.getters[`${ inStore }/all`](HCI.FORKLIFT_NETWORK_MAP) || [];
|
|
||||||
const allStorageMaps = store.getters[`${ inStore }/all`](HCI.FORKLIFT_STORAGE_MAP) || [];
|
|
||||||
|
|
||||||
const networkMap = allNetworkMaps.find((nm) => nm.metadata.name === networkMapName.value && nm.metadata.namespace === NAMESPACE);
|
|
||||||
const storageMap = allStorageMaps.find((sm) => sm.metadata.name === storageMapName.value && sm.metadata.namespace === NAMESPACE);
|
|
||||||
|
|
||||||
if (networkMap?.spec?.map) {
|
|
||||||
networkMappings.value = networkMap.spec.map.map((m) => {
|
|
||||||
const source = m.source?.name || m.source?.id || 'Unknown';
|
|
||||||
let target = '';
|
|
||||||
|
|
||||||
if (m.destination?.type === 'pod') {
|
|
||||||
target = 'Pod Networking';
|
|
||||||
} else if (m.destination?.type === 'ignored') {
|
|
||||||
target = 'Ignored';
|
|
||||||
} else if (m.destination?.type === 'multus' && m.destination?.name) {
|
|
||||||
target = m.destination.namespace ? `${ m.destination.namespace }/${ m.destination.name }` : m.destination.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find which VMs use this network
|
|
||||||
const sourceId = m.source?.id;
|
|
||||||
const usedBy = vms.value
|
|
||||||
.filter((vm) => vm.networks?.some((n) => n.id === sourceId || n.name === m.source?.name))
|
|
||||||
.map((vm) => vm.name || vm.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
source, target, usedBy
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (storageMap?.spec?.map) {
|
|
||||||
storageMappings.value = storageMap.spec.map.map((m) => {
|
|
||||||
const source = m.source?.name || m.source?.id || 'Unknown';
|
|
||||||
const target = m.destination?.storageClass || '';
|
|
||||||
|
|
||||||
// Find which VMs use this datastore
|
|
||||||
const sourceId = m.source?.id;
|
|
||||||
const usedBy = vms.value
|
|
||||||
.filter((vm) => vm.disks?.some((d) => d.datastore?.id === sourceId || d.datastore?.name === m.source?.name))
|
|
||||||
.map((vm) => vm.name || vm.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
source, target, usedBy
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
init();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Loading v-if="loading" />
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="review-migration"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="review-migration-content"
|
|
||||||
>
|
|
||||||
<Masthead
|
|
||||||
:schema="schema"
|
|
||||||
:resource="schema.id"
|
|
||||||
:type-display="t('harvester.addons.forklift.reviewMigration.title')"
|
|
||||||
:is-creatable="false"
|
|
||||||
class="line-height-32"
|
|
||||||
>
|
|
||||||
<template #subHeader>
|
|
||||||
<p class="text-muted mt-5">
|
|
||||||
{{ t('harvester.addons.forklift.reviewMigration.description') }}
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
</Masthead>
|
|
||||||
|
|
||||||
<!-- Migration Details Summary -->
|
|
||||||
<div class="migration-details">
|
|
||||||
<h3 class="section-title m-0">
|
|
||||||
{{ t('harvester.addons.forklift.reviewMigration.migrationDetails') }}
|
|
||||||
</h3>
|
|
||||||
<div class="span-9">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="planName"
|
|
||||||
:label="t('harvester.addons.forklift.reviewMigration.planName')"
|
|
||||||
:placeholder="t('harvester.addons.forklift.reviewMigration.planNamePlaceholder')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="details-grid span-9">
|
|
||||||
<div class="detail-column">
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.totalVms') }}</span>
|
|
||||||
<span class="detail-value detail-value-large">{{ vms.length }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="detail-column">
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.source') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-value">{{ providerName }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="detail-column">
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.targetNamespace') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-value">{{ TARGET_NAMESPACE }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="detail-column">
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.vcpu') }}</span>
|
|
||||||
<span class="detail-value">{{ totalVCpu }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.memory') }}</span>
|
|
||||||
<span class="detail-value">{{ totalMemoryGB }} GB</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.storage') }}</span>
|
|
||||||
<span class="detail-value">{{ totalStorageGB }} GB</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="detail-column grid-column-2">
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.migrationMode') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-value">
|
|
||||||
{{ t('harvester.addons.forklift.reviewMigration.coldMigration') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Virtual Machines -->
|
|
||||||
<div class="vm-section">
|
|
||||||
<h3 class="section-title m-0">
|
|
||||||
{{ t('harvester.addons.forklift.reviewMigration.virtualMachines') }} ({{ vms.length }})
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="vm-cards-grid">
|
|
||||||
<RcItemCard
|
|
||||||
v-for="vm in vmCards"
|
|
||||||
:id="vm.id"
|
|
||||||
:key="vm.id"
|
|
||||||
:variant="'small'"
|
|
||||||
:header="{ title: { text: vm.name }, statuses: [{ icon: 'icon-notify-tick', color: 'text-success' }] }"
|
|
||||||
>
|
|
||||||
<template #item-card-content>
|
|
||||||
<div class="vm-card-content">
|
|
||||||
<div class="vm-card-specs">
|
|
||||||
<span class="vm-os text-muted">{{ vm.os }}</span>
|
|
||||||
<span class="vm-resources">
|
|
||||||
<i class="icon icon-disk" />
|
|
||||||
{{ vm.cpus }} vCPU • {{ vm.memGB }} • {{ vm.diskDisplay }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<MappingsCell
|
|
||||||
:network-entries="vm.networkMappings.map(m => `${m.source} → ${m.target}`)"
|
|
||||||
:storage-entries="vm.storageMappings.map(m => `${m.source} → ${m.target}`)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</RcItemCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Cold Migration Warning -->
|
|
||||||
<Banner
|
|
||||||
color="warning"
|
|
||||||
class="m-0"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<span class="banner-title">{{ t('harvester.addons.forklift.reviewMigration.warningTitle') }}</span><br>
|
|
||||||
{{ t('harvester.addons.forklift.reviewMigration.warningMessage') }}
|
|
||||||
</div>
|
|
||||||
</Banner>
|
|
||||||
|
|
||||||
<!-- Error banner -->
|
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- Actions -->
|
|
||||||
<div class="actions-footer">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary"
|
|
||||||
@click="cancel"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
<AsyncButton
|
|
||||||
:disabled="!planName"
|
|
||||||
:action-label="t('harvester.addons.forklift.reviewMigration.startMigration')"
|
|
||||||
:waiting-label="t('harvester.addons.forklift.reviewMigration.startMigration')"
|
|
||||||
:success-label="t('harvester.addons.forklift.reviewMigration.startMigration')"
|
|
||||||
:error-label="t('harvester.addons.forklift.reviewMigration.startMigration')"
|
|
||||||
@click="startMigration"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.review-migration {
|
|
||||||
padding: 20px;
|
|
||||||
gap: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.review-migration-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(230px, 1fr) 1fr 1fr;
|
|
||||||
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
|
||||||
gap: 12px 128px;
|
|
||||||
width: 747px;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-column {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
&.grid-column-2 {
|
|
||||||
grid-column: span 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.detail-label {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-value {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
&.detail-value-large {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.vm-cards-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vm-card-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vm-card-specs {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.vm-os {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vm-resources {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--muted);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.vm-card-mappings {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mapping-line {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-text {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 16px;
|
|
||||||
color: #973C00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-title {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.migration-details {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vm-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-height-32 {
|
|
||||||
line-height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@ -127,7 +127,7 @@ const rows = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const createLocation = computed(() => ({
|
const createLocation = computed(() => ({
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-configure-provider`,
|
name: `${ PRODUCT_NAME }-c-cluster-vm-migration-wizard`,
|
||||||
params: {
|
params: {
|
||||||
product: store.getters['productId'],
|
product: store.getters['productId'],
|
||||||
cluster: store.getters['clusterId'],
|
cluster: store.getters['clusterId'],
|
||||||
@ -135,21 +135,21 @@ const createLocation = computed(() => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
{ ...STATE, labelKey: 'harvester.addons.forklift.dashboard.columns.status' },
|
{ ...STATE, labelKey: 'harvester.addons.vmMigration.dashboard.columns.status' },
|
||||||
{
|
{
|
||||||
...NAME_COL,
|
...NAME_COL,
|
||||||
labelKey: 'harvester.addons.forklift.dashboard.columns.plan',
|
labelKey: 'harvester.addons.vmMigration.dashboard.columns.plan',
|
||||||
},
|
},
|
||||||
{ ...FORKLIFT_PLAN_VM_COUNT, width: 105 },
|
{ ...FORKLIFT_PLAN_VM_COUNT, width: 105 },
|
||||||
{
|
{
|
||||||
name: 'progress',
|
name: 'progress',
|
||||||
labelKey: 'harvester.addons.forklift.dashboard.columns.progress',
|
labelKey: 'harvester.addons.vmMigration.dashboard.columns.progress',
|
||||||
value: 'progress',
|
value: 'progress',
|
||||||
width: 500,
|
width: 500,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'mappings',
|
name: 'mappings',
|
||||||
labelKey: 'harvester.addons.forklift.dashboard.columns.mappings',
|
labelKey: 'harvester.addons.vmMigration.dashboard.columns.mappings',
|
||||||
value: 'mappingsDisplay',
|
value: 'mappingsDisplay',
|
||||||
},
|
},
|
||||||
{ ...AGE },
|
{ ...AGE },
|
||||||
@ -174,12 +174,12 @@ init();
|
|||||||
<Masthead
|
<Masthead
|
||||||
:schema="schema"
|
:schema="schema"
|
||||||
:resource="schema.id"
|
:resource="schema.id"
|
||||||
:type-display="t('harvester.addons.forklift.dashboard.title')"
|
:type-display="t('harvester.addons.vmMigration.dashboard.title')"
|
||||||
>
|
>
|
||||||
<template #subHeader>
|
<template #subHeader>
|
||||||
<div class="mmt-5">
|
<div class="mmt-5">
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
{{ t('harvester.addons.forklift.dashboard.description') }}
|
{{ t('harvester.addons.vmMigration.dashboard.description') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -188,7 +188,7 @@ init();
|
|||||||
:to="createLocation"
|
:to="createLocation"
|
||||||
class="btn role-primary"
|
class="btn role-primary"
|
||||||
>
|
>
|
||||||
{{ t('harvester.addons.forklift.dashboard.createPlan') }}
|
{{ t('harvester.addons.vmMigration.dashboard.createPlan') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
</Masthead>
|
</Masthead>
|
||||||
@ -200,13 +200,11 @@ init();
|
|||||||
:groupable="false"
|
:groupable="false"
|
||||||
:table-actions="false"
|
:table-actions="false"
|
||||||
:search="false"
|
:search="false"
|
||||||
default-sort-by="state"
|
|
||||||
:default-sort-descending="true"
|
|
||||||
key-field="_key"
|
key-field="_key"
|
||||||
>
|
>
|
||||||
<template #header-left>
|
<template #header-left>
|
||||||
<h3 class="table-title m-0">
|
<h3 class="table-title m-0">
|
||||||
{{ t('harvester.addons.forklift.dashboard.tableTitle') }}
|
{{ t('harvester.addons.vmMigration.dashboard.tableTitle') }}
|
||||||
</h3>
|
</h3>
|
||||||
</template>
|
</template>
|
||||||
<template #cell:name="{ row }">
|
<template #cell:name="{ row }">
|
||||||
@ -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>
|
||||||
@ -16,11 +16,8 @@ import HarvesterMembers from '../pages/c/_cluster/members/index.vue';
|
|||||||
import ProjectNamespaces from '../pages/c/_cluster/projectsnamespaces.vue';
|
import ProjectNamespaces from '../pages/c/_cluster/projectsnamespaces.vue';
|
||||||
import HarvesterAlertmanagerReceiver from '../pages/c/_cluster/alertmanagerconfig/_alertmanagerconfigid/receiver.vue';
|
import HarvesterAlertmanagerReceiver from '../pages/c/_cluster/alertmanagerconfig/_alertmanagerconfigid/receiver.vue';
|
||||||
import HarvesterUnsupported from '../pages/c/_cluster/unsupported/index.vue';
|
import HarvesterUnsupported from '../pages/c/_cluster/unsupported/index.vue';
|
||||||
import ForkliftDashboard from '../pages/c/_cluster/forklift/index.vue';
|
import ForkliftDashboard from '../pages/c/_cluster/vm-migration/index.vue';
|
||||||
import ForkliftConfigureProvider from '../pages/c/_cluster/forklift/configure-provider.vue';
|
import ForkliftVmMigrationWizard from '../pages/c/_cluster/vm-migration/vm-migration-wizard.vue';
|
||||||
import ForkliftSelectVms from '../pages/c/_cluster/forklift/select-vms.vue';
|
|
||||||
import ForkliftConfigureMappings from '../pages/c/_cluster/forklift/configure-mappings.vue';
|
|
||||||
import ForkliftReviewMigration from '../pages/c/_cluster/forklift/review-migration.vue';
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@ -89,25 +86,13 @@ const routes = [
|
|||||||
path: `/:product/c/:cluster/projectsnamespaces`,
|
path: `/:product/c/:cluster/projectsnamespaces`,
|
||||||
component: ProjectNamespaces,
|
component: ProjectNamespaces,
|
||||||
}, {
|
}, {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
name: `${ PRODUCT_NAME }-c-cluster-vm-migration`,
|
||||||
path: `/:product/c/:cluster/forklift`,
|
path: `/:product/c/:cluster/vm-migration`,
|
||||||
component: ForkliftDashboard,
|
component: ForkliftDashboard,
|
||||||
}, {
|
}, {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-configure-provider`,
|
name: `${ PRODUCT_NAME }-c-cluster-vm-migration-wizard`,
|
||||||
path: `/:product/c/:cluster/forklift/configure-provider`,
|
path: `/:product/c/:cluster/vm-migration/wizard`,
|
||||||
component: ForkliftConfigureProvider,
|
component: ForkliftVmMigrationWizard,
|
||||||
}, {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-select-vms`,
|
|
||||||
path: `/:product/c/:cluster/forklift/select-vms`,
|
|
||||||
component: ForkliftSelectVms,
|
|
||||||
}, {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-configure-mappings`,
|
|
||||||
path: `/:product/c/:cluster/forklift/configure-mappings`,
|
|
||||||
component: ForkliftConfigureMappings,
|
|
||||||
}, {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-review-migration`,
|
|
||||||
path: `/:product/c/:cluster/forklift/review-migration`,
|
|
||||||
component: ForkliftReviewMigration,
|
|
||||||
}, {
|
}, {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
path: `/:product/c/:cluster/:resource`,
|
path: `/:product/c/:cluster/:resource`,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user