mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-08-16 12:49:14 +00:00
feat(forklift): added the provider wizard
Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
parent
e703f34879
commit
de7598b7af
@ -13,6 +13,9 @@ const props = defineProps({
|
|||||||
provider: { type: Object, default: null },
|
provider: { type: Object, default: null },
|
||||||
selectedVms: { type: Array, default: () => [] },
|
selectedVms: { type: Array, default: () => [] },
|
||||||
stepData: { type: Object, required: true },
|
stepData: { type: Object, required: true },
|
||||||
|
useAllProviderData: { type: Boolean, default: false },
|
||||||
|
existingNetworkMap: { type: Object, default: null },
|
||||||
|
existingStorageMap: { type: Object, default: null },
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['ready']);
|
const emit = defineEmits(['ready']);
|
||||||
@ -20,8 +23,6 @@ const emit = defineEmits(['ready']);
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { t } = useI18n(store);
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
const NO_TEMPLATE = '__none__';
|
|
||||||
|
|
||||||
const vms = ref([]);
|
const vms = ref([]);
|
||||||
const harvesterNetworks = ref([]);
|
const harvesterNetworks = ref([]);
|
||||||
const storageClasses = ref([]);
|
const storageClasses = ref([]);
|
||||||
@ -29,8 +30,6 @@ const networkEntries = ref([]);
|
|||||||
const storageEntries = ref([]);
|
const storageEntries = ref([]);
|
||||||
const allNetworkMaps = ref([]);
|
const allNetworkMaps = ref([]);
|
||||||
const allStorageMaps = ref([]);
|
const allStorageMaps = ref([]);
|
||||||
const selectedNetworkTemplate = ref(NO_TEMPLATE);
|
|
||||||
const selectedStorageTemplate = ref(NO_TEMPLATE);
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
|
|
||||||
// Restore from stepData
|
// Restore from stepData
|
||||||
@ -40,8 +39,6 @@ if (props.stepData.networkEntries.length > 0) {
|
|||||||
if (props.stepData.storageEntries.length > 0) {
|
if (props.stepData.storageEntries.length > 0) {
|
||||||
storageEntries.value = props.stepData.storageEntries;
|
storageEntries.value = props.stepData.storageEntries;
|
||||||
}
|
}
|
||||||
selectedNetworkTemplate.value = props.stepData.selectedNetworkTemplate;
|
|
||||||
selectedStorageTemplate.value = props.stepData.selectedStorageTemplate;
|
|
||||||
|
|
||||||
const NAMESPACE = 'forklift';
|
const NAMESPACE = 'forklift';
|
||||||
|
|
||||||
@ -75,75 +72,13 @@ const storageClassOptions = computed(() => {
|
|||||||
return options;
|
return options;
|
||||||
});
|
});
|
||||||
|
|
||||||
const networkTemplateOptions = computed(() => {
|
const applyNetworkMapTargets = (mapSpec) => {
|
||||||
const options = [
|
if (!mapSpec) {
|
||||||
{ label: t('harvester.addons.vmMigration.configureMappings.noTemplate'), value: NO_TEMPLATE }
|
|
||||||
];
|
|
||||||
|
|
||||||
const currentIds = new Set(networkEntries.value.map((e) => e.id).filter(Boolean));
|
|
||||||
|
|
||||||
const sorted = [...allNetworkMaps.value].sort(
|
|
||||||
(a, b) => new Date(b.metadata.creationTimestamp) - new Date(a.metadata.creationTimestamp)
|
|
||||||
);
|
|
||||||
|
|
||||||
sorted.forEach((nm) => {
|
|
||||||
const mapSources = (nm.spec?.map || []).map((m) => m.source?.id).filter(Boolean);
|
|
||||||
const hasOverlap = mapSources.some((id) => currentIds.has(id));
|
|
||||||
|
|
||||||
if (hasOverlap) {
|
|
||||||
options.push({
|
|
||||||
label: nm.metadata.name,
|
|
||||||
value: nm.metadata.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return options;
|
|
||||||
});
|
|
||||||
|
|
||||||
const storageTemplateOptions = computed(() => {
|
|
||||||
const options = [
|
|
||||||
{ label: t('harvester.addons.vmMigration.configureMappings.noTemplate'), value: NO_TEMPLATE }
|
|
||||||
];
|
|
||||||
|
|
||||||
const currentIds = new Set(storageEntries.value.map((e) => e.id).filter(Boolean));
|
|
||||||
|
|
||||||
const sorted = [...allStorageMaps.value].sort(
|
|
||||||
(a, b) => new Date(b.metadata.creationTimestamp) - new Date(a.metadata.creationTimestamp)
|
|
||||||
);
|
|
||||||
|
|
||||||
sorted.forEach((sm) => {
|
|
||||||
const mapSources = (sm.spec?.map || []).map((m) => m.source?.id).filter(Boolean);
|
|
||||||
const hasOverlap = mapSources.some((id) => currentIds.has(id));
|
|
||||||
|
|
||||||
if (hasOverlap) {
|
|
||||||
options.push({
|
|
||||||
label: sm.metadata.name,
|
|
||||||
value: sm.metadata.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return options;
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(selectedNetworkTemplate, (val) => {
|
|
||||||
if (val === NO_TEMPLATE) {
|
|
||||||
networkEntries.value.forEach((e) => {
|
|
||||||
e.target = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = allNetworkMaps.value.find((nm) => nm.metadata.name === val);
|
|
||||||
|
|
||||||
if (!template?.spec?.map) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
networkEntries.value.forEach((entry) => {
|
networkEntries.value.forEach((entry) => {
|
||||||
const match = template.spec.map.find(
|
const match = mapSpec.find(
|
||||||
(m) => m.source?.id === entry.id || m.source?.name === entry.name
|
(m) => m.source?.id === entry.id || m.source?.name === entry.name
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -159,25 +94,15 @@ watch(selectedNetworkTemplate, (val) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
watch(selectedStorageTemplate, (val) => {
|
const applyStorageMapTargets = (mapSpec) => {
|
||||||
if (val === NO_TEMPLATE) {
|
if (!mapSpec) {
|
||||||
storageEntries.value.forEach((e) => {
|
|
||||||
e.target = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = allStorageMaps.value.find((sm) => sm.metadata.name === val);
|
|
||||||
|
|
||||||
if (!template?.spec?.map) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
storageEntries.value.forEach((entry) => {
|
storageEntries.value.forEach((entry) => {
|
||||||
const match = template.spec.map.find(
|
const match = mapSpec.find(
|
||||||
(m) => m.source?.id === entry.id || m.source?.name === entry.name
|
(m) => m.source?.id === entry.id || m.source?.name === entry.name
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -185,12 +110,32 @@ watch(selectedStorageTemplate, (val) => {
|
|||||||
entry.target = match.destination.storageClass;
|
entry.target = match.destination.storageClass;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const applyDefaultNetworkMap = () => {
|
||||||
|
const defaultMap = allNetworkMaps.value.find(
|
||||||
|
(nm) => nm.metadata.name === `${ props.providerName }-network-map-default`
|
||||||
|
);
|
||||||
|
|
||||||
|
applyNetworkMapTargets(defaultMap?.spec?.map);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyDefaultStorageMap = () => {
|
||||||
|
const defaultMap = allStorageMaps.value.find(
|
||||||
|
(sm) => sm.metadata.name === `${ props.providerName }-storage-map-default`
|
||||||
|
);
|
||||||
|
|
||||||
|
applyStorageMapTargets(defaultMap?.spec?.map);
|
||||||
|
};
|
||||||
|
|
||||||
const allNetworksMapped = computed(() => networkEntries.value.length > 0 && networkEntries.value.every((e) => !!e.target));
|
const allNetworksMapped = computed(() => networkEntries.value.length > 0 && networkEntries.value.every((e) => !!e.target));
|
||||||
const allStorageMapped = computed(() => storageEntries.value.length > 0 && storageEntries.value.every((e) => !!e.target));
|
const allStorageMapped = computed(() => storageEntries.value.length > 0 && storageEntries.value.every((e) => !!e.target));
|
||||||
|
|
||||||
const canSave = computed(() => {
|
const canSave = computed(() => {
|
||||||
|
if (props.useAllProviderData) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return networkEntries.value.length > 0 && storageEntries.value.length > 0 &&
|
return networkEntries.value.length > 0 && storageEntries.value.length > 0 &&
|
||||||
allNetworksMapped.value && allStorageMapped.value;
|
allNetworksMapped.value && allStorageMapped.value;
|
||||||
});
|
});
|
||||||
@ -211,13 +156,6 @@ watch(networkEntries, (val) => {
|
|||||||
watch(storageEntries, (val) => {
|
watch(storageEntries, (val) => {
|
||||||
props.stepData.storageEntries = val;
|
props.stepData.storageEntries = val;
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
watch(selectedNetworkTemplate, (val) => {
|
|
||||||
props.stepData.selectedNetworkTemplate = val;
|
|
||||||
});
|
|
||||||
watch(selectedStorageTemplate, (val) => {
|
|
||||||
props.stepData.selectedStorageTemplate = val;
|
|
||||||
});
|
|
||||||
|
|
||||||
const buildNetworkEntries = () => {
|
const buildNetworkEntries = () => {
|
||||||
const networkMap = {};
|
const networkMap = {};
|
||||||
|
|
||||||
@ -285,6 +223,29 @@ const buildStorageEntries = () => {
|
|||||||
storageEntries.value = Object.values(datastoreMap);
|
storageEntries.value = Object.values(datastoreMap);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildNetworkEntriesFromProvider = (networksData) => {
|
||||||
|
networkEntries.value = (Array.isArray(networksData) ? networksData : []).map((net) => ({
|
||||||
|
name: net.name || net.id,
|
||||||
|
id: net.id || '',
|
||||||
|
vlanId: net.vlanId || '',
|
||||||
|
target: '',
|
||||||
|
usedBy: [],
|
||||||
|
_key: `net-${ net.id || net.name }`,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildStorageEntriesFromProvider = (datastoresData) => {
|
||||||
|
storageEntries.value = (Array.isArray(datastoresData) ? datastoresData : []).map((ds) => ({
|
||||||
|
name: ds.name || ds.id,
|
||||||
|
id: ds.id || '',
|
||||||
|
type: ds.type || '',
|
||||||
|
capacity: ds.capacity || 0,
|
||||||
|
target: '',
|
||||||
|
usedBy: [],
|
||||||
|
_key: `stor-${ ds.id || ds.name }`,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const formatStorageDetail = (entry) => {
|
const formatStorageDetail = (entry) => {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
|
|
||||||
@ -305,26 +266,8 @@ const formatStorageDetail = (entry) => {
|
|||||||
return parts.join(' • ');
|
return parts.join(' • ');
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveAndReturn = async() => {
|
const buildNetworkMapSpec = (providerRef) => {
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
return {
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create NetworkMap
|
|
||||||
const networkMapSpec = {
|
|
||||||
map: networkEntries.value.map((entry) => {
|
map: networkEntries.value.map((entry) => {
|
||||||
if (entry.target === 'pod') {
|
if (entry.target === 'pod') {
|
||||||
return {
|
return {
|
||||||
@ -355,6 +298,49 @@ const saveAndReturn = async() => {
|
|||||||
}),
|
}),
|
||||||
provider: providerRef,
|
provider: providerRef,
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildStorageMapSpec = (providerRef) => {
|
||||||
|
return {
|
||||||
|
map: storageEntries.value.map((entry) => ({
|
||||||
|
source: { name: entry.name, id: entry.id },
|
||||||
|
destination: { storageClass: entry.target },
|
||||||
|
})),
|
||||||
|
provider: providerRef,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveAndReturn = 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update existing maps in place (edit mode)
|
||||||
|
if (props.existingNetworkMap && props.existingStorageMap) {
|
||||||
|
props.existingNetworkMap.spec = buildNetworkMapSpec(providerRef);
|
||||||
|
await props.existingNetworkMap.save();
|
||||||
|
|
||||||
|
props.existingStorageMap.spec = buildStorageMapSpec(providerRef);
|
||||||
|
await props.existingStorageMap.save();
|
||||||
|
|
||||||
|
return {
|
||||||
|
networkMapName: props.existingNetworkMap.metadata.name,
|
||||||
|
storageMapName: props.existingStorageMap.metadata.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const providerOwnerRef = props.provider ? [{
|
const providerOwnerRef = props.provider ? [{
|
||||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||||
@ -367,32 +353,23 @@ const saveAndReturn = async() => {
|
|||||||
const networkMap = await store.dispatch(`${ inStore }/create`, {
|
const networkMap = await store.dispatch(`${ inStore }/create`, {
|
||||||
type: HCI.FORKLIFT_NETWORK_MAP,
|
type: HCI.FORKLIFT_NETWORK_MAP,
|
||||||
metadata: {
|
metadata: {
|
||||||
name: `${ props.providerName }-network-map-${ Math.random().toString(36).substring(2, 7) }`,
|
name: `${ props.providerName }-network-map-${ props.useAllProviderData ? 'default' : Math.random().toString(36).substring(2, 7) }`,
|
||||||
namespace: NAMESPACE,
|
namespace: NAMESPACE,
|
||||||
ownerReferences: providerOwnerRef,
|
ownerReferences: providerOwnerRef,
|
||||||
},
|
},
|
||||||
spec: networkMapSpec,
|
spec: buildNetworkMapSpec(providerRef),
|
||||||
});
|
});
|
||||||
|
|
||||||
await networkMap.save();
|
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`, {
|
const storageMap = await store.dispatch(`${ inStore }/create`, {
|
||||||
type: HCI.FORKLIFT_STORAGE_MAP,
|
type: HCI.FORKLIFT_STORAGE_MAP,
|
||||||
metadata: {
|
metadata: {
|
||||||
name: `${ props.providerName }-storage-map-${ Math.random().toString(36).substring(2, 7) }`,
|
name: `${ props.providerName }-storage-map-${ props.useAllProviderData ? 'default' : Math.random().toString(36).substring(2, 7) }`,
|
||||||
namespace: NAMESPACE,
|
namespace: NAMESPACE,
|
||||||
ownerReferences: providerOwnerRef,
|
ownerReferences: providerOwnerRef,
|
||||||
},
|
},
|
||||||
spec: storageMapSpec,
|
spec: buildStorageMapSpec(providerRef),
|
||||||
});
|
});
|
||||||
|
|
||||||
await storageMap.save();
|
await storageMap.save();
|
||||||
@ -429,9 +406,7 @@ const init = async() => {
|
|||||||
allStorageMaps.value = [];
|
allStorageMaps.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
vms.value = props.selectedVms;
|
if (networkEntries.value.length === 0 || storageEntries.value.length === 0) {
|
||||||
|
|
||||||
if (networkEntries.value.length === 0) {
|
|
||||||
try {
|
try {
|
||||||
const providerUid = props.provider?.metadata?.uid;
|
const providerUid = props.provider?.metadata?.uid;
|
||||||
const providerType = props.provider?.spec?.type || 'vsphere';
|
const providerType = props.provider?.spec?.type || 'vsphere';
|
||||||
@ -442,6 +417,16 @@ const init = async() => {
|
|||||||
fetch(`${ baseUrl }/datastores?detail=1`).then((r) => r.json()).catch(() => []),
|
fetch(`${ baseUrl }/datastores?detail=1`).then((r) => r.json()).catch(() => []),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (props.useAllProviderData) {
|
||||||
|
if (networkEntries.value.length === 0) {
|
||||||
|
buildNetworkEntriesFromProvider(networksData);
|
||||||
|
}
|
||||||
|
if (storageEntries.value.length === 0) {
|
||||||
|
buildStorageEntriesFromProvider(datastoresData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vms.value = props.selectedVms;
|
||||||
|
|
||||||
const networkNameMap = (Array.isArray(networksData) ? networksData : []).reduce((map, n) => {
|
const networkNameMap = (Array.isArray(networksData) ? networksData : []).reduce((map, n) => {
|
||||||
map[n.id] = n.name;
|
map[n.id] = n.name;
|
||||||
|
|
||||||
@ -480,15 +465,31 @@ const init = async() => {
|
|||||||
|
|
||||||
return resolved;
|
return resolved;
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
// Name resolution failed — entries will use IDs as fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (networkEntries.value.length === 0) {
|
||||||
buildNetworkEntries();
|
buildNetworkEntries();
|
||||||
}
|
}
|
||||||
if (storageEntries.value.length === 0) {
|
if (storageEntries.value.length === 0) {
|
||||||
buildStorageEntries();
|
buildStorageEntries();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Data resolution failed — entries will use IDs as fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.existingNetworkMap?.spec?.map) {
|
||||||
|
applyNetworkMapTargets(props.existingNetworkMap.spec.map);
|
||||||
|
} else {
|
||||||
|
applyDefaultNetworkMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.existingStorageMap?.spec?.map) {
|
||||||
|
applyStorageMapTargets(props.existingStorageMap.spec.map);
|
||||||
|
} else {
|
||||||
|
applyDefaultStorageMap();
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -516,14 +517,6 @@ init();
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="selectedNetworkTemplate"
|
|
||||||
:label="t('harvester.addons.vmMigration.configureMappings.networkMapping.template')"
|
|
||||||
:options="networkTemplateOptions"
|
|
||||||
:reduce="(opt) => opt.value"
|
|
||||||
class="mb-10"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RcItemCard
|
<RcItemCard
|
||||||
v-for="entry in networkEntries"
|
v-for="entry in networkEntries"
|
||||||
:id="entry._key"
|
:id="entry._key"
|
||||||
@ -551,7 +544,7 @@ init();
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="!useAllProviderData && entry.usedBy.length">
|
||||||
<span class="used-by">
|
<span class="used-by">
|
||||||
Used by: <b>{{ entry.usedBy.join(', ') }}</b>
|
Used by: <b>{{ entry.usedBy.join(', ') }}</b>
|
||||||
</span>
|
</span>
|
||||||
@ -572,14 +565,6 @@ init();
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="selectedStorageTemplate"
|
|
||||||
:label="t('harvester.addons.vmMigration.configureMappings.storageMapping.template')"
|
|
||||||
:options="storageTemplateOptions"
|
|
||||||
:reduce="(opt) => opt.value"
|
|
||||||
class="mb-10"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RcItemCard
|
<RcItemCard
|
||||||
v-for="entry in storageEntries"
|
v-for="entry in storageEntries"
|
||||||
:id="entry._key"
|
:id="entry._key"
|
||||||
@ -610,7 +595,7 @@ init();
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="!useAllProviderData && entry.usedBy.length">
|
||||||
<span class="used-by">
|
<span class="used-by">
|
||||||
Used by: <b>{{ entry.usedBy.join(', ') }}</b>
|
Used by: <b>{{ entry.usedBy.join(', ') }}</b>
|
||||||
</span>
|
</span>
|
||||||
@ -626,10 +611,23 @@ init();
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
.mappings-columns {
|
.mappings-columns {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
|
||||||
|
@media only screen and (min-width: map-get($breakpoints, '--viewport-7')) {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: map-get($breakpoints, '--viewport-9')) {
|
||||||
|
gap: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: map-get($breakpoints, '--viewport-12')) {
|
||||||
gap: 64px;
|
gap: 64px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mapping-column {
|
.mapping-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -650,7 +648,7 @@ init();
|
|||||||
.card-content-row {
|
.card-content-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 40px;
|
gap: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -664,13 +662,23 @@ init();
|
|||||||
.mapping-source {
|
.mapping-source {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 160px;
|
flex: 1;
|
||||||
|
min-width: 100px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
|
||||||
.source-name {
|
.source-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-detail {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.used-by {
|
.used-by {
|
||||||
@ -686,7 +694,8 @@ init();
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mapping-target {
|
.mapping-target {
|
||||||
flex: 1;
|
flex: 3;
|
||||||
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,11 @@ import { HCI } from '../../types';
|
|||||||
|
|
||||||
const CREATE_NEW = '__create_new__';
|
const CREATE_NEW = '__create_new__';
|
||||||
|
|
||||||
const props = defineProps({ stepData: { type: Object, required: true } });
|
const props = defineProps({
|
||||||
|
stepData: { type: Object, required: true },
|
||||||
|
createOnly: { type: Boolean, default: false },
|
||||||
|
editMode: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['complete', 'ready', 'form-valid', 'testing']);
|
const emit = defineEmits(['complete', 'ready', 'form-valid', 'testing']);
|
||||||
|
|
||||||
@ -205,6 +209,85 @@ const testConnection = async(buttonCb) => {
|
|||||||
|
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
// Edit mode: update existing provider URL + secret, then poll
|
||||||
|
if (props.editMode && createdProvider.value) {
|
||||||
|
try {
|
||||||
|
const namespace = 'forklift';
|
||||||
|
|
||||||
|
createdProvider.value.spec.url = url.value;
|
||||||
|
await createdProvider.value.save();
|
||||||
|
|
||||||
|
const secretRef = createdProvider.value.spec?.secret;
|
||||||
|
|
||||||
|
if (secretRef && createdSecret.value) {
|
||||||
|
createdSecret.value.data = {
|
||||||
|
user: btoa(username.value),
|
||||||
|
password: btoa(password.value),
|
||||||
|
insecureSkipVerify: btoa(String(skipTlsVerify.value)),
|
||||||
|
url: btoa(url.value),
|
||||||
|
};
|
||||||
|
await createdSecret.value.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxAttempts = 15;
|
||||||
|
let attempts = 0;
|
||||||
|
let connected = false;
|
||||||
|
let errorMsg = '';
|
||||||
|
|
||||||
|
while (attempts < maxAttempts) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
const refreshed = await store.dispatch(`${ inStore }/find`, {
|
||||||
|
type: HCI.FORKLIFT_PROVIDER,
|
||||||
|
id: `${ namespace }/${ providerName.value }`,
|
||||||
|
opt: { force: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
const conditions = refreshed?.status?.conditions || [];
|
||||||
|
const readyCondition = conditions.find((c) => c.type === 'Ready');
|
||||||
|
const connectionCondition = conditions.find((c) => c.type === 'ConnectionTestSucceeded');
|
||||||
|
|
||||||
|
if (connectionCondition) {
|
||||||
|
if (connectionCondition.status === 'True') {
|
||||||
|
connected = true;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
errorMsg = connectionCondition.message || 'Connection failed';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readyCondition) {
|
||||||
|
if (readyCondition.status === 'True') {
|
||||||
|
connected = true;
|
||||||
|
break;
|
||||||
|
} else if (readyCondition.status === 'False') {
|
||||||
|
errorMsg = readyCondition.message || 'Provider not ready';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
testPassed.value = true;
|
||||||
|
testResult.value = t('harvester.addons.vmMigration.configureProvider.testSuccess');
|
||||||
|
testing.value = false;
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
testError.value = errorMsg || t('harvester.addons.vmMigration.configureProvider.testTimeout');
|
||||||
|
testing.value = false;
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed');
|
||||||
|
testing.value = false;
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// For existing providers, just poll for Ready/ConnectionTestSucceeded status
|
// For existing providers, just poll for Ready/ConnectionTestSucceeded status
|
||||||
if (isExistingProvider.value) {
|
if (isExistingProvider.value) {
|
||||||
try {
|
try {
|
||||||
@ -456,7 +539,7 @@ defineExpose({ testConnection, clickTestButton });
|
|||||||
</Banner>
|
</Banner>
|
||||||
|
|
||||||
<div class="configure-provider-step-form">
|
<div class="configure-provider-step-form">
|
||||||
<div>
|
<div v-if="!createOnly && !editMode">
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="selectedProvider"
|
v-model:value="selectedProvider"
|
||||||
:label="t('harvester.addons.vmMigration.configureProvider.providerSelect')"
|
:label="t('harvester.addons.vmMigration.configureProvider.providerSelect')"
|
||||||
@ -466,11 +549,12 @@ defineExpose({ testConnection, clickTestButton });
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!isExistingProvider"
|
v-if="!isExistingProvider || editMode"
|
||||||
>
|
>
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-model:value="providerName"
|
v-model:value="providerName"
|
||||||
:label="t('harvester.addons.vmMigration.configureProvider.name')"
|
:label="t('harvester.addons.vmMigration.configureProvider.name')"
|
||||||
|
:disabled="editMode"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -480,7 +564,7 @@ defineExpose({ testConnection, clickTestButton });
|
|||||||
v-model:value="url"
|
v-model:value="url"
|
||||||
:label="t('harvester.addons.vmMigration.configureProvider.urlLabel')"
|
:label="t('harvester.addons.vmMigration.configureProvider.urlLabel')"
|
||||||
:placeholder="t('harvester.addons.vmMigration.configureProvider.urlPlaceholder')"
|
:placeholder="t('harvester.addons.vmMigration.configureProvider.urlPlaceholder')"
|
||||||
:disabled="isExistingProvider"
|
:disabled="isExistingProvider && !editMode"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p class="text-muted mt-5">
|
<p class="text-muted mt-5">
|
||||||
@ -493,7 +577,7 @@ defineExpose({ testConnection, clickTestButton });
|
|||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-model:value="username"
|
v-model:value="username"
|
||||||
:label="t('harvester.addons.vmMigration.fields.username')"
|
:label="t('harvester.addons.vmMigration.fields.username')"
|
||||||
:disabled="isExistingProvider"
|
:disabled="isExistingProvider && !editMode"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -502,7 +586,7 @@ defineExpose({ testConnection, clickTestButton });
|
|||||||
v-model:value="password"
|
v-model:value="password"
|
||||||
type="password"
|
type="password"
|
||||||
:label="t('harvester.addons.vmMigration.fields.password')"
|
:label="t('harvester.addons.vmMigration.fields.password')"
|
||||||
:disabled="isExistingProvider"
|
:disabled="isExistingProvider && !editMode"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -512,7 +596,7 @@ defineExpose({ testConnection, clickTestButton });
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
v-model:value="skipTlsVerify"
|
v-model:value="skipTlsVerify"
|
||||||
:label="t('harvester.addons.vmMigration.configureProvider.skipSsl')"
|
:label="t('harvester.addons.vmMigration.configureProvider.skipSsl')"
|
||||||
:disabled="isExistingProvider"
|
:disabled="isExistingProvider && !editMode"
|
||||||
/>
|
/>
|
||||||
<p class="text-muted ml-20">
|
<p class="text-muted ml-20">
|
||||||
{{ t('harvester.addons.vmMigration.configureProvider.skipSslHint') }}
|
{{ t('harvester.addons.vmMigration.configureProvider.skipSslHint') }}
|
||||||
|
|||||||
@ -395,8 +395,7 @@ defineExpose({ startMigration: startMigrationAction });
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(230px, 1fr) 1fr 1fr;
|
grid-template-columns: minmax(230px, 1fr) 1fr 1fr;
|
||||||
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
gap: 12px 128px;
|
gap: 12px clamp(64px, 8vw, 128px);
|
||||||
width: 747px;
|
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1350,8 +1350,9 @@ export function init($plugin, store) {
|
|||||||
virtualType({
|
virtualType({
|
||||||
name: HCI.FORKLIFT_PROVIDER,
|
name: HCI.FORKLIFT_PROVIDER,
|
||||||
labelKey: 'harvester.addons.vmMigration.labels.provider',
|
labelKey: 'harvester.addons.vmMigration.labels.provider',
|
||||||
group: 'vmMigration::Advanced',
|
group: 'vmMigration',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
|
weight: 100,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.FORKLIFT_PROVIDER }
|
params: { resource: HCI.FORKLIFT_PROVIDER }
|
||||||
@ -1471,6 +1472,7 @@ export function init($plugin, store) {
|
|||||||
labelKey: 'harvester.addons.vmMigration.labels.dashboard',
|
labelKey: 'harvester.addons.vmMigration.labels.dashboard',
|
||||||
group: 'vmMigration',
|
group: 'vmMigration',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
|
weight: 200,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-vm-migration`,
|
name: `${ PRODUCT_NAME }-c-cluster-vm-migration`,
|
||||||
params: {}
|
params: {}
|
||||||
@ -1484,6 +1486,7 @@ export function init($plugin, store) {
|
|||||||
navGroup: 'vmMigration',
|
navGroup: 'vmMigration',
|
||||||
types: [
|
types: [
|
||||||
'forklift-create',
|
'forklift-create',
|
||||||
|
HCI.FORKLIFT_PROVIDER,
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
registerAddonSideNav(store, PRODUCT_NAME, {
|
registerAddonSideNav(store, PRODUCT_NAME, {
|
||||||
@ -1491,7 +1494,6 @@ export function init($plugin, store) {
|
|||||||
resourceType: HCI.ADD_ONS,
|
resourceType: HCI.ADD_ONS,
|
||||||
navGroup: 'vmMigration::Advanced',
|
navGroup: 'vmMigration::Advanced',
|
||||||
types: [
|
types: [
|
||||||
HCI.FORKLIFT_PROVIDER,
|
|
||||||
HCI.FORKLIFT_NETWORK_MAP,
|
HCI.FORKLIFT_NETWORK_MAP,
|
||||||
HCI.FORKLIFT_STORAGE_MAP,
|
HCI.FORKLIFT_STORAGE_MAP,
|
||||||
HCI.FORKLIFT_PLAN,
|
HCI.FORKLIFT_PLAN,
|
||||||
|
|||||||
24
pkg/harvester/edit/forklift.konveyor.io.provider.vue
Normal file
24
pkg/harvester/edit/forklift.konveyor.io.provider.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script>
|
||||||
|
import { h } from 'vue';
|
||||||
|
import { PRODUCT_NAME } from '../config/harvester';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ForkliftProviderEdit',
|
||||||
|
|
||||||
|
created() {
|
||||||
|
const { namespace, id } = this.$route.params;
|
||||||
|
const query = namespace && id ? { providerId: `${ namespace }/${ id }` } : {};
|
||||||
|
|
||||||
|
this.$router.replace({
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-vm-migration-provider-wizard`,
|
||||||
|
params: {
|
||||||
|
product: this.$route.params.product,
|
||||||
|
cluster: this.$route.params.cluster,
|
||||||
|
},
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: () => h('div'),
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
wizard:
|
wizard:
|
||||||
create: Create Migration
|
create: Create
|
||||||
next: Proceed
|
next: Proceed
|
||||||
previous: Back
|
previous: Back
|
||||||
|
|
||||||
@ -1820,6 +1820,9 @@ harvester:
|
|||||||
storageMap: Storage Maps
|
storageMap: Storage Maps
|
||||||
plan: Migration Plans
|
plan: Migration Plans
|
||||||
migration: Migrations
|
migration: Migrations
|
||||||
|
providerWizard:
|
||||||
|
title: Create Provider
|
||||||
|
editTitle: Edit Provider
|
||||||
wizard:
|
wizard:
|
||||||
title: VM Migration
|
title: VM Migration
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@ -128,7 +128,7 @@ export default class ForkliftPlan extends HarvesterResource {
|
|||||||
get isForkliftDashboard() {
|
get isForkliftDashboard() {
|
||||||
const route = this.currentRouter()?.currentRoute?.value;
|
const route = this.currentRouter()?.currentRoute?.value;
|
||||||
|
|
||||||
return route?.name?.endsWith('-forklift');
|
return route?.name?.endsWith('-vm-migration');
|
||||||
}
|
}
|
||||||
|
|
||||||
get _availableActions() {
|
get _availableActions() {
|
||||||
|
|||||||
@ -1,6 +1,28 @@
|
|||||||
import HarvesterResource from './harvester';
|
import HarvesterResource from './harvester';
|
||||||
|
import { PRODUCT_NAME } from '../config/harvester';
|
||||||
|
|
||||||
export default class ForkliftProvider extends HarvesterResource {
|
export default class ForkliftProvider extends HarvesterResource {
|
||||||
|
get _createLocation() {
|
||||||
|
return {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-vm-migration-provider-wizard`,
|
||||||
|
params: {
|
||||||
|
product: this.$rootGetters['productId'],
|
||||||
|
cluster: this.$rootGetters['clusterId'],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get _editLocation() {
|
||||||
|
return {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-vm-migration-provider-wizard`,
|
||||||
|
params: {
|
||||||
|
product: this.$rootGetters['productId'],
|
||||||
|
cluster: this.$rootGetters['clusterId'],
|
||||||
|
},
|
||||||
|
query: { providerId: `${ this.metadata.namespace }/${ this.metadata.name }` },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deleting a Provider cascades via ownerReferences set at creation time.
|
* Deleting a Provider cascades via ownerReferences set at creation time.
|
||||||
* Kubernetes GC will automatically delete: Secret, NetworkMap, StorageMap.
|
* Kubernetes GC will automatically delete: Secret, NetworkMap, StorageMap.
|
||||||
|
|||||||
281
pkg/harvester/pages/c/_cluster/vm-migration/provider-wizard.vue
Normal file
281
pkg/harvester/pages/c/_cluster/vm-migration/provider-wizard.vue
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactive, ref, computed, watch } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import { SECRET } from '@shell/config/types';
|
||||||
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
|
import ConfigureProviderStep from '../../../../components/vm-migration/ConfigureProviderStep';
|
||||||
|
import ConfigureMappingsStep from '../../../../components/vm-migration/ConfigureMappingsStep';
|
||||||
|
import { PRODUCT_NAME } from '../../../../config/harvester';
|
||||||
|
import { currentRouter } from '../../../../utils/router';
|
||||||
|
import { HCI } from '../../../../types';
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const route = useRoute();
|
||||||
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
|
const cruRef = ref(null);
|
||||||
|
const providerStepRef = ref(null);
|
||||||
|
const mappingsStepRef = ref(null);
|
||||||
|
|
||||||
|
const providerName = ref('');
|
||||||
|
const provider = ref(null);
|
||||||
|
const errors = ref([]);
|
||||||
|
|
||||||
|
const providerReady = ref(false);
|
||||||
|
const providerFormValid = ref(false);
|
||||||
|
const providerTesting = ref(false);
|
||||||
|
const mappingsReady = ref(false);
|
||||||
|
|
||||||
|
const existingNetworkMap = ref(null);
|
||||||
|
const existingStorageMap = ref(null);
|
||||||
|
const initialLoading = ref(true);
|
||||||
|
|
||||||
|
const dummyResource = ref({ save: () => Promise.resolve() });
|
||||||
|
|
||||||
|
const providerId = route.query?.providerId || null;
|
||||||
|
const isEditMode = !!providerId;
|
||||||
|
|
||||||
|
const stepData = reactive({
|
||||||
|
provider: {
|
||||||
|
selectedProvider: isEditMode ? '' : '__create_new__',
|
||||||
|
providerName: '',
|
||||||
|
url: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
skipTlsVerify: false,
|
||||||
|
testPassed: false,
|
||||||
|
testResult: null,
|
||||||
|
testError: null,
|
||||||
|
createdProvider: null,
|
||||||
|
createdSecret: null,
|
||||||
|
},
|
||||||
|
mappings: {
|
||||||
|
networkEntries: [],
|
||||||
|
storageEntries: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const steps = reactive([
|
||||||
|
{
|
||||||
|
name: 'configure-provider',
|
||||||
|
label: '',
|
||||||
|
subtext: '',
|
||||||
|
ready: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'configure-mappings',
|
||||||
|
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.configureMappings.label');
|
||||||
|
steps[1].subtext = t('harvester.addons.vmMigration.wizard.steps.configureMappings.description');
|
||||||
|
|
||||||
|
watch([providerFormValid, providerTesting], () => {
|
||||||
|
steps[0].ready = providerFormValid.value && !providerTesting.value;
|
||||||
|
}, { immediate: true });
|
||||||
|
watch(mappingsReady, (val) => {
|
||||||
|
steps[1].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 wizardTitle = computed(() => {
|
||||||
|
return isEditMode ? t('harvester.addons.vmMigration.providerWizard.editTitle') : t('harvester.addons.vmMigration.providerWizard.title');
|
||||||
|
});
|
||||||
|
|
||||||
|
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 onMappingsReady = (ready) => {
|
||||||
|
mappingsReady.value = ready;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear mappings when provider changes
|
||||||
|
watch(() => stepData.provider.providerName, (newVal, oldVal) => {
|
||||||
|
if (oldVal && newVal !== oldVal) {
|
||||||
|
stepData.mappings.networkEntries = [];
|
||||||
|
stepData.mappings.storageEntries = [];
|
||||||
|
mappingsReady.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const providerListLocation = {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: {
|
||||||
|
product: store.getters['productId'],
|
||||||
|
cluster: store.getters['clusterId'],
|
||||||
|
resource: HCI.FORKLIFT_PROVIDER,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFinish = async(buttonCb) => {
|
||||||
|
try {
|
||||||
|
await mappingsStepRef.value.saveMappings();
|
||||||
|
buttonCb(true);
|
||||||
|
currentRouter().push(providerListLocation);
|
||||||
|
} catch (err) {
|
||||||
|
errors.value = [err instanceof Error ? err.message : String(err)];
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
currentRouter().push(providerListLocation);
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = async() => {
|
||||||
|
if (!isEditMode) {
|
||||||
|
initialLoading.value = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fetchedProvider = await store.dispatch(`${ inStore }/find`, {
|
||||||
|
type: HCI.FORKLIFT_PROVIDER,
|
||||||
|
id: providerId,
|
||||||
|
opt: { force: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
provider.value = fetchedProvider;
|
||||||
|
providerName.value = fetchedProvider.metadata.name;
|
||||||
|
|
||||||
|
stepData.provider.selectedProvider = fetchedProvider.metadata.name;
|
||||||
|
stepData.provider.providerName = fetchedProvider.metadata.name;
|
||||||
|
stepData.provider.url = fetchedProvider.spec?.url || '';
|
||||||
|
stepData.provider.createdProvider = fetchedProvider;
|
||||||
|
|
||||||
|
const secretRef = fetchedProvider.spec?.secret;
|
||||||
|
|
||||||
|
if (secretRef) {
|
||||||
|
const allSecrets = await store.dispatch(`${ inStore }/findAll`, {
|
||||||
|
type: SECRET,
|
||||||
|
opt: { labelSelector: `ui.forklift/created-for-resource-type=${ HCI.FORKLIFT_PROVIDER }` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const secret = allSecrets.find(
|
||||||
|
(s) => s.metadata.name === secretRef.name && s.metadata.namespace === secretRef.namespace
|
||||||
|
);
|
||||||
|
|
||||||
|
if (secret?.data) {
|
||||||
|
stepData.provider.username = atob(secret.data.user || '');
|
||||||
|
stepData.provider.password = atob(secret.data.password || '');
|
||||||
|
stepData.provider.skipTlsVerify = atob(secret.data.insecureSkipVerify || '') === 'true';
|
||||||
|
stepData.provider.createdSecret = secret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch existing default maps for this provider
|
||||||
|
try {
|
||||||
|
const allNetworkMaps = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP });
|
||||||
|
const allStorageMaps = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_STORAGE_MAP });
|
||||||
|
|
||||||
|
existingNetworkMap.value = allNetworkMaps.find(
|
||||||
|
(nm) => nm.metadata.name === `${ fetchedProvider.metadata.name }-network-map-default`
|
||||||
|
) || null;
|
||||||
|
|
||||||
|
existingStorageMap.value = allStorageMaps.find(
|
||||||
|
(sm) => sm.metadata.name === `${ fetchedProvider.metadata.name }-storage-map-default`
|
||||||
|
) || null;
|
||||||
|
} catch (e) {
|
||||||
|
// Maps may not exist yet
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errors.value = [`Failed to load provider: ${ err.message || err }`];
|
||||||
|
}
|
||||||
|
|
||||||
|
initialLoading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="initialLoading" />
|
||||||
|
<CruResource
|
||||||
|
v-else
|
||||||
|
ref="cruRef"
|
||||||
|
:resource="dummyResource"
|
||||||
|
:mode="isEditMode ? 'edit' : 'create'"
|
||||||
|
:steps="steps"
|
||||||
|
:errors="errors"
|
||||||
|
:validation-passed="true"
|
||||||
|
:can-yaml="false"
|
||||||
|
:cancel-event="true"
|
||||||
|
:title="wizardTitle"
|
||||||
|
finish-mode="finish"
|
||||||
|
class="wizard"
|
||||||
|
@cancel="onCancel"
|
||||||
|
@finish="onFinish"
|
||||||
|
>
|
||||||
|
<template #configure-provider>
|
||||||
|
<ConfigureProviderStep
|
||||||
|
ref="providerStepRef"
|
||||||
|
:step-data="stepData.provider"
|
||||||
|
:create-only="true"
|
||||||
|
:edit-mode="isEditMode"
|
||||||
|
@complete="onProviderComplete"
|
||||||
|
@ready="onProviderReady"
|
||||||
|
@form-valid="onProviderFormValid"
|
||||||
|
@testing="onProviderTesting"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #configure-mappings>
|
||||||
|
<ConfigureMappingsStep
|
||||||
|
ref="mappingsStepRef"
|
||||||
|
:provider-name="providerName"
|
||||||
|
:provider="provider"
|
||||||
|
:step-data="stepData.mappings"
|
||||||
|
:use-all-provider-data="true"
|
||||||
|
:existing-network-map="existingNetworkMap"
|
||||||
|
:existing-storage-map="existingStorageMap"
|
||||||
|
@ready="onMappingsReady"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
@ -57,8 +57,6 @@ const stepData = reactive({
|
|||||||
mappings: {
|
mappings: {
|
||||||
networkEntries: [],
|
networkEntries: [],
|
||||||
storageEntries: [],
|
storageEntries: [],
|
||||||
selectedNetworkTemplate: '__none__',
|
|
||||||
selectedStorageTemplate: '__none__',
|
|
||||||
},
|
},
|
||||||
review: { planName: '' },
|
review: { planName: '' },
|
||||||
});
|
});
|
||||||
@ -181,8 +179,6 @@ watch(() => stepData.provider.providerName, (newVal, oldVal) => {
|
|||||||
|
|
||||||
stepData.mappings.networkEntries = [];
|
stepData.mappings.networkEntries = [];
|
||||||
stepData.mappings.storageEntries = [];
|
stepData.mappings.storageEntries = [];
|
||||||
stepData.mappings.selectedNetworkTemplate = '__none__';
|
|
||||||
stepData.mappings.selectedStorageTemplate = '__none__';
|
|
||||||
mappingsReady.value = false;
|
mappingsReady.value = false;
|
||||||
|
|
||||||
stepData.review.planName = '';
|
stepData.review.planName = '';
|
||||||
@ -195,8 +191,6 @@ watch(selectedVMs, (newVal, oldVal) => {
|
|||||||
if (oldVal.length > 0 && JSON.stringify(newVal.map((v) => v.id).sort()) !== JSON.stringify(oldVal.map((v) => v.id).sort())) {
|
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.networkEntries = [];
|
||||||
stepData.mappings.storageEntries = [];
|
stepData.mappings.storageEntries = [];
|
||||||
stepData.mappings.selectedNetworkTemplate = '__none__';
|
|
||||||
stepData.mappings.selectedStorageTemplate = '__none__';
|
|
||||||
mappingsReady.value = false;
|
mappingsReady.value = false;
|
||||||
|
|
||||||
stepData.review.planName = '';
|
stepData.review.planName = '';
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import HarvesterAlertmanagerReceiver from '../pages/c/_cluster/alertmanagerconfi
|
|||||||
import HarvesterUnsupported from '../pages/c/_cluster/unsupported/index.vue';
|
import HarvesterUnsupported from '../pages/c/_cluster/unsupported/index.vue';
|
||||||
import ForkliftDashboard from '../pages/c/_cluster/vm-migration/index.vue';
|
import ForkliftDashboard from '../pages/c/_cluster/vm-migration/index.vue';
|
||||||
import ForkliftVmMigrationWizard from '../pages/c/_cluster/vm-migration/vm-migration-wizard.vue';
|
import ForkliftVmMigrationWizard from '../pages/c/_cluster/vm-migration/vm-migration-wizard.vue';
|
||||||
|
import ForkliftProviderWizard from '../pages/c/_cluster/vm-migration/provider-wizard.vue';
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@ -93,6 +94,10 @@ const routes = [
|
|||||||
name: `${ PRODUCT_NAME }-c-cluster-vm-migration-wizard`,
|
name: `${ PRODUCT_NAME }-c-cluster-vm-migration-wizard`,
|
||||||
path: `/:product/c/:cluster/vm-migration/wizard`,
|
path: `/:product/c/:cluster/vm-migration/wizard`,
|
||||||
component: ForkliftVmMigrationWizard,
|
component: ForkliftVmMigrationWizard,
|
||||||
|
}, {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-vm-migration-provider-wizard`,
|
||||||
|
path: `/:product/c/:cluster/vm-migration/provider-wizard`,
|
||||||
|
component: ForkliftProviderWizard,
|
||||||
}, {
|
}, {
|
||||||
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