mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-08-16 12:49:14 +00:00
feat(forklift): Fixed config based on new figma changes
Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
parent
2259910be1
commit
63d726aa91
16
pkg/harvester/models/forklift.konveyor.io.plan.js
Normal file
16
pkg/harvester/models/forklift.konveyor.io.plan.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import HarvesterResource from './harvester';
|
||||||
|
|
||||||
|
export default class ForkliftPlan extends HarvesterResource {
|
||||||
|
/**
|
||||||
|
* Deleting a Plan cascades via ownerReferences set at creation time.
|
||||||
|
* Kubernetes GC will automatically delete: Migration, NetworkMap, StorageMap, Provider (→ Secret).
|
||||||
|
* Use foreground propagation to ensure children are deleted before the parent.
|
||||||
|
*/
|
||||||
|
remove() {
|
||||||
|
const opt = { ...arguments };
|
||||||
|
|
||||||
|
opt.params = { propagationPolicy: 'Foreground' };
|
||||||
|
|
||||||
|
return this._remove(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
pkg/harvester/models/forklift.konveyor.io.provider.js
Normal file
16
pkg/harvester/models/forklift.konveyor.io.provider.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import HarvesterResource from './harvester';
|
||||||
|
|
||||||
|
export default class ForkliftProvider extends HarvesterResource {
|
||||||
|
/**
|
||||||
|
* Deleting a Provider cascades via ownerReferences set at creation time.
|
||||||
|
* Kubernetes GC will automatically delete: Secret, NetworkMap, StorageMap.
|
||||||
|
* Use foreground propagation to ensure children are deleted before the parent.
|
||||||
|
*/
|
||||||
|
remove() {
|
||||||
|
const opt = { ...arguments };
|
||||||
|
|
||||||
|
opt.params = { propagationPolicy: 'Foreground' };
|
||||||
|
|
||||||
|
return this._remove(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script setup>
|
||||||
import { defineComponent, ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
@ -23,347 +23,337 @@ const schema = {
|
|||||||
metadata: { name: HCI.FORKLIFT_NETWORK_MAP },
|
metadata: { name: HCI.FORKLIFT_NETWORK_MAP },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineComponent({
|
const store = useStore();
|
||||||
name: 'ForkliftConfigureMappings',
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
components: {
|
const vms = ref([]);
|
||||||
Loading,
|
const harvesterNetworks = ref([]);
|
||||||
Masthead,
|
const storageClasses = ref([]);
|
||||||
AsyncButton,
|
const networkEntries = ref([]);
|
||||||
LabeledSelect,
|
const storageEntries = ref([]);
|
||||||
RcItemCard,
|
const errors = ref([]);
|
||||||
},
|
const loading = ref(true);
|
||||||
|
|
||||||
setup() {
|
const NAMESPACE = 'forklift';
|
||||||
const store = useStore();
|
const providerName = computed(() => route.query.provider || 'vsphere');
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const { t } = useI18n(store);
|
|
||||||
|
|
||||||
const vms = ref([]);
|
const harvesterNetworkOptions = computed(() => {
|
||||||
const harvesterNetworks = ref([]);
|
const options = [
|
||||||
const storageClasses = ref([]);
|
{ label: 'Pod Networking', value: 'pod' },
|
||||||
const networkEntries = ref([]);
|
{ label: 'Ignored', value: 'ignored' },
|
||||||
const storageEntries = ref([]);
|
];
|
||||||
const errors = ref([]);
|
|
||||||
const loading = ref(true);
|
|
||||||
|
|
||||||
const providerName = computed(() => route.query.provider || 'vsphere');
|
harvesterNetworks.value.forEach((net) => {
|
||||||
const namespace = computed(() => route.query.namespace || 'default');
|
const ns = net.metadata?.namespace || '';
|
||||||
|
const name = net.metadata?.name || net.id;
|
||||||
|
const fullName = ns ? `${ ns }/${ name }` : name;
|
||||||
|
|
||||||
const harvesterNetworkOptions = computed(() => {
|
options.push({ label: fullName, value: fullName });
|
||||||
const options = [
|
});
|
||||||
{ label: 'Pod Networking', value: 'pod' },
|
|
||||||
{ label: 'Ignored', value: 'ignored' },
|
|
||||||
];
|
|
||||||
|
|
||||||
harvesterNetworks.value.forEach((net) => {
|
return options;
|
||||||
const ns = net.metadata?.namespace || '';
|
|
||||||
const name = net.metadata?.name || net.id;
|
|
||||||
const fullName = ns ? `${ ns }/${ name }` : name;
|
|
||||||
|
|
||||||
options.push({ label: fullName, value: fullName });
|
|
||||||
});
|
|
||||||
|
|
||||||
return options;
|
|
||||||
});
|
|
||||||
|
|
||||||
const storageClassOptions = computed(() => {
|
|
||||||
const options = storageClasses.value.map((sc) => ({
|
|
||||||
label: sc.metadata?.name || sc.id,
|
|
||||||
value: sc.metadata?.name || sc.id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!options.find((o) => o.value === 'harvester-longhorn')) {
|
|
||||||
options.unshift({ label: 'harvester-longhorn', value: 'harvester-longhorn' });
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
});
|
|
||||||
|
|
||||||
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 canSave = computed(() => {
|
|
||||||
return networkEntries.value.length > 0 && storageEntries.value.length > 0 &&
|
|
||||||
allNetworksMapped.value && allStorageMapped.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const buildNetworkEntries = () => {
|
|
||||||
const entries = [];
|
|
||||||
|
|
||||||
vms.value.forEach((vm) => {
|
|
||||||
if (vm.networks && vm.networks.length > 0) {
|
|
||||||
vm.networks.forEach((net) => {
|
|
||||||
entries.push({
|
|
||||||
vmId: vm.id,
|
|
||||||
vmName: vm.name || vm.id,
|
|
||||||
name: net.name || 'Unknown',
|
|
||||||
id: net.id || '',
|
|
||||||
vlanId: net.vlanId || '',
|
|
||||||
target: 'pod',
|
|
||||||
_key: `net-${ vm.id }-${ net.id || net.name }`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
entries.push({
|
|
||||||
vmId: vm.id,
|
|
||||||
vmName: vm.name || vm.id,
|
|
||||||
name: 'Default Network',
|
|
||||||
id: '',
|
|
||||||
vlanId: '',
|
|
||||||
target: 'pod',
|
|
||||||
_key: `net-${ vm.id }-default`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
networkEntries.value = entries;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildStorageEntries = () => {
|
|
||||||
const entries = [];
|
|
||||||
|
|
||||||
vms.value.forEach((vm) => {
|
|
||||||
if (vm.disks && vm.disks.length > 0) {
|
|
||||||
const seen = new Set();
|
|
||||||
|
|
||||||
vm.disks.forEach((disk) => {
|
|
||||||
const ds = disk.datastore;
|
|
||||||
|
|
||||||
if (ds && ds.id && !seen.has(ds.id)) {
|
|
||||||
seen.add(ds.id);
|
|
||||||
entries.push({
|
|
||||||
vmId: vm.id,
|
|
||||||
vmName: vm.name || vm.id,
|
|
||||||
name: ds.name || 'Unknown',
|
|
||||||
id: ds.id || '',
|
|
||||||
type: ds.type || '',
|
|
||||||
capacity: disk.capacity || 0,
|
|
||||||
target: 'harvester-longhorn',
|
|
||||||
_key: `stor-${ vm.id }-${ ds.id }`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
entries.push({
|
|
||||||
vmId: vm.id,
|
|
||||||
vmName: vm.name || vm.id,
|
|
||||||
name: 'Default Datastore',
|
|
||||||
id: '',
|
|
||||||
type: '',
|
|
||||||
capacity: 0,
|
|
||||||
target: 'harvester-longhorn',
|
|
||||||
_key: `stor-${ vm.id }-default`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
storageEntries.value = entries;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatStorageDetail = (entry) => {
|
|
||||||
const parts = [];
|
|
||||||
|
|
||||||
if (entry.type) {
|
|
||||||
parts.push(entry.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.capacity) {
|
|
||||||
const gb = entry.capacity / (1024 * 1024 * 1024);
|
|
||||||
|
|
||||||
if (gb >= 1024) {
|
|
||||||
parts.push(`${ (gb / 1024).toFixed(1) } TB`);
|
|
||||||
} else {
|
|
||||||
parts.push(`${ Math.round(gb) } GB`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.join(' \u2022 ');
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
router.push({
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
|
||||||
params: {
|
|
||||||
product: route.params.product,
|
|
||||||
cluster: route.params.cluster,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveMappings = async(buttonCb) => {
|
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
const providerRef = {
|
|
||||||
source: {
|
|
||||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
|
||||||
kind: 'Provider',
|
|
||||||
name: providerName.value,
|
|
||||||
namespace: namespace.value,
|
|
||||||
},
|
|
||||||
destination: {
|
|
||||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
|
||||||
kind: 'Provider',
|
|
||||||
name: 'host',
|
|
||||||
namespace: 'forklift',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create NetworkMap
|
|
||||||
const networkMapSpec = {
|
|
||||||
map: networkEntries.value.map((entry) => {
|
|
||||||
if (entry.target === 'pod') {
|
|
||||||
return {
|
|
||||||
source: { name: entry.name, id: entry.id },
|
|
||||||
destination: { type: 'pod' },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.target === 'ignored') {
|
|
||||||
return {
|
|
||||||
source: { name: entry.name, id: entry.id },
|
|
||||||
destination: { type: 'ignored' },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
source: { name: entry.name, id: entry.id },
|
|
||||||
destination: {
|
|
||||||
type: 'multus',
|
|
||||||
name: entry.target,
|
|
||||||
namespace: namespace.value,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
provider: providerRef,
|
|
||||||
};
|
|
||||||
|
|
||||||
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.value,
|
|
||||||
},
|
|
||||||
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.value,
|
|
||||||
},
|
|
||||||
spec: storageMapSpec,
|
|
||||||
});
|
|
||||||
|
|
||||||
await storageMap.save();
|
|
||||||
|
|
||||||
// Navigate to review page with all mapping data
|
|
||||||
const networkMappingsData = networkEntries.value.map((entry) => ({
|
|
||||||
vmId: entry.vmId,
|
|
||||||
source: entry.name,
|
|
||||||
target: entry.target === 'pod' ? 'Pod Networking' : entry.target === 'ignored' ? 'Ignored' : entry.target,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const storageMappingsData = storageEntries.value.map((entry) => ({
|
|
||||||
vmId: entry.vmId,
|
|
||||||
source: entry.name,
|
|
||||||
target: entry.target,
|
|
||||||
}));
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-review-migration`,
|
|
||||||
params: {
|
|
||||||
product: route.params.product,
|
|
||||||
cluster: route.params.cluster,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
provider: providerName.value,
|
|
||||||
namespace: namespace.value,
|
|
||||||
vms: route.query.vms,
|
|
||||||
networkMap: networkMap.metadata.name,
|
|
||||||
storageMap: storageMap.metadata.name,
|
|
||||||
networkMappings: JSON.stringify(networkMappingsData),
|
|
||||||
storageMappings: JSON.stringify(storageMappingsData),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
buttonCb(true);
|
|
||||||
} catch (err) {
|
|
||||||
errors.value = [err.message || err];
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = async() => {
|
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
try {
|
|
||||||
harvesterNetworks.value = await store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT });
|
|
||||||
} catch (e) {
|
|
||||||
harvesterNetworks.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
storageClasses.value = await store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS });
|
|
||||||
} catch (e) {
|
|
||||||
storageClasses.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const vmsParam = route.query.vms;
|
|
||||||
|
|
||||||
if (vmsParam) {
|
|
||||||
try {
|
|
||||||
const vmIds = JSON.parse(vmsParam);
|
|
||||||
|
|
||||||
vms.value = vmIds.map((id) => {
|
|
||||||
const found = vmData.find((vm) => vm.id === id);
|
|
||||||
|
|
||||||
return found || {
|
|
||||||
id, name: id, networks: [], disks: []
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
vms.value = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildNetworkEntries();
|
|
||||||
buildStorageEntries();
|
|
||||||
loading.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
init();
|
|
||||||
|
|
||||||
return {
|
|
||||||
schema,
|
|
||||||
loading,
|
|
||||||
networkEntries,
|
|
||||||
storageEntries,
|
|
||||||
errors,
|
|
||||||
harvesterNetworkOptions,
|
|
||||||
storageClassOptions,
|
|
||||||
canSave,
|
|
||||||
t,
|
|
||||||
formatStorageDetail,
|
|
||||||
cancel,
|
|
||||||
saveMappings,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const storageClassOptions = computed(() => {
|
||||||
|
const options = storageClasses.value.map((sc) => ({
|
||||||
|
label: sc.metadata?.name || sc.id,
|
||||||
|
value: sc.metadata?.name || sc.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!options.find((o) => o.value === 'harvester-longhorn')) {
|
||||||
|
options.unshift({ label: 'harvester-longhorn', value: 'harvester-longhorn' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
});
|
||||||
|
|
||||||
|
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 canSave = computed(() => {
|
||||||
|
return networkEntries.value.length > 0 && storageEntries.value.length > 0 &&
|
||||||
|
allNetworksMapped.value && allStorageMapped.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildNetworkEntries = () => {
|
||||||
|
const networkMap = {};
|
||||||
|
|
||||||
|
vms.value.forEach((vm) => {
|
||||||
|
const vmName = vm.name || vm.id;
|
||||||
|
|
||||||
|
if (vm.networks && vm.networks.length > 0) {
|
||||||
|
vm.networks.forEach((net) => {
|
||||||
|
const netKey = net.id || net.name || 'default';
|
||||||
|
|
||||||
|
if (!networkMap[netKey]) {
|
||||||
|
networkMap[netKey] = {
|
||||||
|
name: net.name || 'Unknown',
|
||||||
|
id: net.id || '',
|
||||||
|
vlanId: net.vlanId || '',
|
||||||
|
target: '',
|
||||||
|
usedBy: [],
|
||||||
|
_key: `net-${ netKey }`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!networkMap[netKey].usedBy.includes(vmName)) {
|
||||||
|
networkMap[netKey].usedBy.push(vmName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
networkEntries.value = Object.values(networkMap);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildStorageEntries = () => {
|
||||||
|
const datastoreMap = {};
|
||||||
|
|
||||||
|
vms.value.forEach((vm) => {
|
||||||
|
const vmName = vm.name || vm.id;
|
||||||
|
|
||||||
|
if (vm.disks && vm.disks.length > 0) {
|
||||||
|
vm.disks.forEach((disk) => {
|
||||||
|
const ds = disk.datastore;
|
||||||
|
|
||||||
|
if (ds && ds.id) {
|
||||||
|
if (!datastoreMap[ds.id]) {
|
||||||
|
datastoreMap[ds.id] = {
|
||||||
|
name: ds.name || 'Unknown',
|
||||||
|
id: ds.id,
|
||||||
|
type: ds.type || '',
|
||||||
|
capacity: ds.capacity || 0,
|
||||||
|
target: '',
|
||||||
|
usedBy: [],
|
||||||
|
_key: `stor-${ ds.id }`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!datastoreMap[ds.id].usedBy.includes(vmName)) {
|
||||||
|
datastoreMap[ds.id].usedBy.push(vmName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
storageEntries.value = Object.values(datastoreMap);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatStorageDetail = (entry) => {
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
if (entry.type) {
|
||||||
|
parts.push(entry.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.capacity) {
|
||||||
|
const gb = entry.capacity / (1024 * 1024 * 1024);
|
||||||
|
|
||||||
|
if (gb >= 1024) {
|
||||||
|
parts.push(`${ (gb / 1024).toFixed(1) } TB`);
|
||||||
|
} else {
|
||||||
|
parts.push(`${ Math.round(gb) } GB`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' \u2022 ');
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = async() => {
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
const allProviders = store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || [];
|
||||||
|
const provider = allProviders.find((p) => p.metadata.name === providerName.value && p.metadata.namespace === NAMESPACE);
|
||||||
|
|
||||||
|
if (provider) {
|
||||||
|
await provider.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||||
|
params: {
|
||||||
|
product: route.params.product,
|
||||||
|
cluster: route.params.cluster,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveMappings = async(buttonCb) => {
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const providerRef = {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create NetworkMap
|
||||||
|
const networkMapSpec = {
|
||||||
|
map: networkEntries.value.map((entry) => {
|
||||||
|
if (entry.target === 'pod') {
|
||||||
|
return {
|
||||||
|
source: { name: entry.name, id: entry.id },
|
||||||
|
destination: { type: 'pod' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.target === 'ignored') {
|
||||||
|
return {
|
||||||
|
source: { name: entry.name, id: entry.id },
|
||||||
|
destination: { type: 'ignored' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
source: { name: entry.name, id: entry.id },
|
||||||
|
destination: {
|
||||||
|
type: 'multus',
|
||||||
|
name: entry.target,
|
||||||
|
namespace: NAMESPACE,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
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 with all mapping data
|
||||||
|
const networkMappingsData = networkEntries.value.map((entry) => ({
|
||||||
|
source: entry.name,
|
||||||
|
target: entry.target === 'pod' ? 'Pod Networking' : entry.target === 'ignored' ? 'Ignored' : entry.target,
|
||||||
|
usedBy: entry.usedBy,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const storageMappingsData = storageEntries.value.map((entry) => ({
|
||||||
|
source: entry.name,
|
||||||
|
target: entry.target,
|
||||||
|
usedBy: entry.usedBy,
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-forklift-review-migration`,
|
||||||
|
params: {
|
||||||
|
product: route.params.product,
|
||||||
|
cluster: route.params.cluster,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
provider: providerName.value,
|
||||||
|
vms: route.query.vms,
|
||||||
|
networkMap: networkMap.metadata.name,
|
||||||
|
storageMap: storageMap.metadata.name,
|
||||||
|
networkMappings: JSON.stringify(networkMappingsData),
|
||||||
|
storageMappings: JSON.stringify(storageMappingsData),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonCb(true);
|
||||||
|
} catch (err) {
|
||||||
|
errors.value = [err.message || err];
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = async() => {
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
harvesterNetworks.value = await store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT });
|
||||||
|
} catch (e) {
|
||||||
|
harvesterNetworks.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
storageClasses.value = await store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS });
|
||||||
|
} catch (e) {
|
||||||
|
storageClasses.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
||||||
|
} catch (e) {
|
||||||
|
// Provider may already be in store from previous step
|
||||||
|
}
|
||||||
|
|
||||||
|
const vmsParam = route.query.vms;
|
||||||
|
|
||||||
|
if (vmsParam) {
|
||||||
|
try {
|
||||||
|
const vmIds = JSON.parse(vmsParam);
|
||||||
|
|
||||||
|
vms.value = vmIds.map((id) => {
|
||||||
|
const found = vmData.find((vm) => vm.id === id);
|
||||||
|
|
||||||
|
return found || {
|
||||||
|
id, name: id, networks: [], disks: []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
vms.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildNetworkEntries();
|
||||||
|
buildStorageEntries();
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -402,25 +392,31 @@ export default defineComponent({
|
|||||||
:id="entry._key"
|
:id="entry._key"
|
||||||
:key="entry._key"
|
:key="entry._key"
|
||||||
:variant="'small'"
|
:variant="'small'"
|
||||||
:header="{ title: { text: entry.vmId }, statuses: entry.target ? [{ icon: 'icon-notify-tick', color: 'text-success' }] : [] }"
|
:header="{}"
|
||||||
>
|
>
|
||||||
<template #item-card-content>
|
<template #item-card-content>
|
||||||
<div class="card-content-row">
|
<div class="card-content-column">
|
||||||
<div class="mapping-source">
|
<div class="card-content-row">
|
||||||
<span class="source-name">{{ entry.name }}</span>
|
<div class="mapping-source">
|
||||||
<span class="source-id text-muted">{{ entry.id }}</span>
|
<span class="source-name">{{ entry.name }}</span>
|
||||||
<span class="source-detail text-muted">VLAN {{ entry.vlanId || '0' }}</span>
|
<span class="source-detail text-muted">VLAN {{ entry.vlanId || '0' }}</span>
|
||||||
|
</div>
|
||||||
|
<div :class="['mapping-arrow', entry.target ? 'text-success' : 'text-muted']">
|
||||||
|
<i class="icon icon-upgrade-alt" />
|
||||||
|
</div>
|
||||||
|
<div class="mapping-target">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="entry.target"
|
||||||
|
:options="harvesterNetworkOptions"
|
||||||
|
:placeholder="t('harvester.addons.forklift.configureMappings.networkMapping.placeholder')"
|
||||||
|
:searchable="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mapping-arrow">
|
<div>
|
||||||
→
|
<span class="used-by">
|
||||||
</div>
|
Used by: <b>{{ entry.usedBy.join(', ') }}</b>
|
||||||
<div class="mapping-target">
|
</span>
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="entry.target"
|
|
||||||
:options="harvesterNetworkOptions"
|
|
||||||
:placeholder="t('harvester.addons.forklift.configureMappings.networkMapping.placeholder')"
|
|
||||||
:searchable="true"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -443,28 +439,34 @@ export default defineComponent({
|
|||||||
:id="entry._key"
|
:id="entry._key"
|
||||||
:key="entry._key"
|
:key="entry._key"
|
||||||
:variant="'small'"
|
:variant="'small'"
|
||||||
:header="{ title: { text: entry.vmId }, statuses: entry.target ? [{ icon: 'icon-notify-tick', color: 'text-success' }] : [] }"
|
:header="{}"
|
||||||
>
|
>
|
||||||
<template #item-card-content>
|
<template #item-card-content>
|
||||||
<div class="card-content-row">
|
<div class="card-content-column">
|
||||||
<div class="mapping-source">
|
<div class="card-content-row">
|
||||||
<span class="source-name">{{ entry.name }}</span>
|
<div class="mapping-source">
|
||||||
<span class="source-id text-muted">{{ entry.id }}</span>
|
<span class="source-name">{{ entry.name }}</span>
|
||||||
<span
|
<span
|
||||||
v-if="entry.type || entry.capacity"
|
v-if="entry.type || entry.capacity"
|
||||||
class="source-detail text-muted"
|
class="source-detail text-muted"
|
||||||
>{{ formatStorageDetail(entry) }}</span>
|
>{{ formatStorageDetail(entry) }}</span>
|
||||||
|
</div>
|
||||||
|
<div :class="['mapping-arrow', entry.target ? 'text-success' : 'text-muted']">
|
||||||
|
<i class="icon icon-upgrade-alt" />
|
||||||
|
</div>
|
||||||
|
<div class="mapping-target">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="entry.target"
|
||||||
|
:options="storageClassOptions"
|
||||||
|
:placeholder="t('harvester.addons.forklift.configureMappings.storageMapping.placeholder')"
|
||||||
|
:searchable="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mapping-arrow">
|
<div>
|
||||||
→
|
<span class="used-by">
|
||||||
</div>
|
Used by: <b>{{ entry.usedBy.join(', ') }}</b>
|
||||||
<div class="mapping-target">
|
</span>
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="entry.target"
|
|
||||||
:options="storageClassOptions"
|
|
||||||
:placeholder="t('harvester.addons.forklift.configureMappings.storageMapping.placeholder')"
|
|
||||||
:searchable="true"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -506,6 +508,10 @@ export default defineComponent({
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
:deep(.item-card-header) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapping-section-title {
|
.mapping-section-title {
|
||||||
@ -521,6 +527,13 @@ export default defineComponent({
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-content-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.mapping-source {
|
.mapping-source {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -530,13 +543,20 @@ export default defineComponent({
|
|||||||
|
|
||||||
.source-name {
|
.source-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.used-by {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapping-arrow {
|
.mapping-arrow {
|
||||||
color: var(--muted);
|
font-size: 22px;
|
||||||
font-size: 16px;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapping-target {
|
.mapping-target {
|
||||||
@ -553,4 +573,5 @@ export default defineComponent({
|
|||||||
.line-height-20 {
|
.line-height-20 {
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script setup>
|
||||||
import { defineComponent, ref, computed } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
@ -23,302 +23,248 @@ const schema = {
|
|||||||
metadata: { name: HCI.FORKLIFT_PROVIDER },
|
metadata: { name: HCI.FORKLIFT_PROVIDER },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineComponent({
|
const store = useStore();
|
||||||
name: 'ForkliftConfigureProvider',
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
components: {
|
const allSecrets = ref([]);
|
||||||
LabeledInput,
|
const providerName = ref('');
|
||||||
Checkbox,
|
const url = ref('');
|
||||||
Banner,
|
const username = ref('');
|
||||||
Masthead,
|
const password = ref('');
|
||||||
AsyncButton,
|
const skipTlsVerify = ref(false);
|
||||||
},
|
const testResult = ref(null);
|
||||||
|
const testError = ref(null);
|
||||||
|
const errors = ref([]);
|
||||||
|
const createdProvider = ref(null);
|
||||||
|
const createdSecret = ref(null);
|
||||||
|
const loading = ref(true);
|
||||||
|
const testPassed = ref(false);
|
||||||
|
|
||||||
setup() {
|
const isFormValid = computed(() => !!providerName.value && !!url.value && !!username.value && !!password.value);
|
||||||
const store = useStore();
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const { t } = useI18n(store);
|
|
||||||
|
|
||||||
const allSecrets = ref([]);
|
// Reset test state when any field changes
|
||||||
const providerName = ref('');
|
watch([providerName, url, username, password], () => {
|
||||||
const url = ref('');
|
testPassed.value = false;
|
||||||
const username = ref('');
|
testResult.value = null;
|
||||||
const password = ref('');
|
});
|
||||||
const skipTlsVerify = ref(false);
|
|
||||||
const testResult = ref(null);
|
|
||||||
const testError = ref(null);
|
|
||||||
const errors = ref([]);
|
|
||||||
const createdProvider = ref(null);
|
|
||||||
const createdSecret = ref(null);
|
|
||||||
const loading = ref(true);
|
|
||||||
|
|
||||||
const isFormValid = computed(() => !!providerName.value && !!url.value && !!username.value && !!password.value);
|
const cancel = async() => {
|
||||||
|
if (createdProvider.value) {
|
||||||
|
await createdProvider.value.remove();
|
||||||
|
createdProvider.value = null;
|
||||||
|
} else if (createdSecret.value) {
|
||||||
|
await createdSecret.value.remove();
|
||||||
|
createdSecret.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
const cancel = () => {
|
router.push({
|
||||||
router.push({
|
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
params: {
|
||||||
params: {
|
product: route.params.product,
|
||||||
product: route.params.product,
|
cluster: route.params.cluster,
|
||||||
cluster: route.params.cluster,
|
}
|
||||||
}
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const testConnection = async(buttonCb) => {
|
||||||
|
testResult.value = null;
|
||||||
|
testError.value = null;
|
||||||
|
|
||||||
|
if (!providerName.value || !url.value || !username.value || !password.value) {
|
||||||
|
testError.value = t('harvester.addons.forklift.configureProvider.testMissingFields');
|
||||||
|
buttonCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Delete previous provider (cascades to secret via ownerReferences)
|
||||||
|
if (createdProvider.value) {
|
||||||
|
await createdProvider.value.remove();
|
||||||
|
createdProvider.value = null;
|
||||||
|
createdSecret.value = null;
|
||||||
|
} else if (createdSecret.value) {
|
||||||
|
await createdSecret.value.remove();
|
||||||
|
createdSecret.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const namespace = 'forklift';
|
||||||
|
const secretName = `${ providerName.value }-creds-${ randomStr(4).toLowerCase() }`;
|
||||||
|
|
||||||
|
// Create Provider first so we have its UID for the ownerReference on the Secret
|
||||||
|
const provider = await store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: HCI.FORKLIFT_PROVIDER,
|
||||||
|
metadata: {
|
||||||
|
name: providerName.value,
|
||||||
|
namespace,
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
type: 'vsphere',
|
||||||
|
url: url.value,
|
||||||
|
secret: {
|
||||||
|
name: secretName,
|
||||||
|
namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.save();
|
||||||
|
createdProvider.value = provider;
|
||||||
|
|
||||||
|
// Create Secret with ownerReference already set (avoids an extra PUT)
|
||||||
|
const newSecret = await store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: SECRET,
|
||||||
|
metadata: {
|
||||||
|
name: secretName,
|
||||||
|
namespace,
|
||||||
|
labels: {
|
||||||
|
createdForProviderType: 'vsphere',
|
||||||
|
createdForResourceType: 'providers',
|
||||||
|
},
|
||||||
|
ownerReferences: [
|
||||||
|
{
|
||||||
|
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||||
|
kind: 'Provider',
|
||||||
|
name: provider.metadata.name,
|
||||||
|
uid: provider.metadata.uid,
|
||||||
|
blockOwnerDeletion: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newSecret['_type'] = 'Opaque';
|
||||||
|
newSecret['data'] = {
|
||||||
|
user: btoa(username.value),
|
||||||
|
password: btoa(password.value),
|
||||||
|
insecureSkipVerify: btoa(String(skipTlsVerify.value)),
|
||||||
|
url: btoa(url.value),
|
||||||
|
};
|
||||||
|
|
||||||
|
await newSecret.save();
|
||||||
|
createdSecret.value = newSecret;
|
||||||
|
|
||||||
|
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 testConnection = async(buttonCb) => {
|
const conditions = refreshed?.status?.conditions || [];
|
||||||
testResult.value = null;
|
const readyCondition = conditions.find((c) => c.type === 'Ready');
|
||||||
testError.value = null;
|
const connectionCondition = conditions.find((c) => c.type === 'ConnectionTestSucceeded');
|
||||||
|
|
||||||
if (!providerName.value || !url.value || !username.value || !password.value) {
|
if (connectionCondition) {
|
||||||
testError.value = t('harvester.addons.forklift.configureProvider.testMissingFields');
|
if (connectionCondition.status === 'True') {
|
||||||
buttonCb(false);
|
connected = true;
|
||||||
|
break;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (createdProvider.value) {
|
|
||||||
await createdProvider.value.remove();
|
|
||||||
createdProvider.value = null;
|
|
||||||
}
|
|
||||||
if (createdSecret.value) {
|
|
||||||
await createdSecret.value.remove();
|
|
||||||
createdSecret.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const namespace = 'default';
|
|
||||||
const secretName = `${ providerName.value }-creds-${ randomStr(4).toLowerCase() }`;
|
|
||||||
|
|
||||||
const newSecret = await store.dispatch(`${ inStore }/create`, {
|
|
||||||
type: SECRET,
|
|
||||||
metadata: {
|
|
||||||
name: secretName,
|
|
||||||
namespace,
|
|
||||||
labels: {
|
|
||||||
createdForProviderType: 'vsphere',
|
|
||||||
createdForResourceType: 'providers',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
newSecret['_type'] = 'Opaque';
|
|
||||||
newSecret['data'] = {
|
|
||||||
user: btoa(username.value),
|
|
||||||
password: btoa(password.value),
|
|
||||||
insecureSkipVerify: btoa(String(skipTlsVerify.value)),
|
|
||||||
url: btoa(url.value),
|
|
||||||
};
|
|
||||||
|
|
||||||
await newSecret.save();
|
|
||||||
createdSecret.value = newSecret;
|
|
||||||
|
|
||||||
const provider = await store.dispatch(`${ inStore }/create`, {
|
|
||||||
type: HCI.FORKLIFT_PROVIDER,
|
|
||||||
metadata: {
|
|
||||||
name: providerName.value,
|
|
||||||
namespace,
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
type: 'vsphere',
|
|
||||||
url: url.value,
|
|
||||||
secret: {
|
|
||||||
name: secretName,
|
|
||||||
namespace,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await provider.save();
|
|
||||||
createdProvider.value = provider;
|
|
||||||
|
|
||||||
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) {
|
|
||||||
testResult.value = t('harvester.addons.forklift.configureProvider.testSuccess');
|
|
||||||
buttonCb(true);
|
|
||||||
} else {
|
} else {
|
||||||
if (createdProvider.value) {
|
errorMsg = connectionCondition.message || 'Connection failed';
|
||||||
await createdProvider.value.remove();
|
break;
|
||||||
createdProvider.value = null;
|
|
||||||
}
|
|
||||||
if (createdSecret.value) {
|
|
||||||
await createdSecret.value.remove();
|
|
||||||
createdSecret.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
testError.value = errorMsg || t('harvester.addons.forklift.configureProvider.testTimeout');
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
if (createdProvider.value) {
|
|
||||||
try {
|
|
||||||
await createdProvider.value.remove();
|
|
||||||
} catch (e) {}
|
|
||||||
createdProvider.value = null;
|
|
||||||
}
|
|
||||||
if (createdSecret.value) {
|
|
||||||
try {
|
|
||||||
await createdSecret.value.remove();
|
|
||||||
} catch (e) {}
|
|
||||||
createdSecret.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
testError.value = err.message || t('harvester.addons.forklift.configureProvider.testFailed');
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const saveProvider = async(buttonCb) => {
|
if (readyCondition) {
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
if (readyCondition.status === 'True') {
|
||||||
|
connected = true;
|
||||||
errors.value = [];
|
break;
|
||||||
|
} else if (readyCondition.status === 'False') {
|
||||||
try {
|
errorMsg = readyCondition.message || 'Provider not ready';
|
||||||
if (createdProvider.value) {
|
break;
|
||||||
router.push({
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-select-vms`,
|
|
||||||
params: {
|
|
||||||
product: route.params.product,
|
|
||||||
cluster: route.params.cluster,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
provider: providerName.value,
|
|
||||||
namespace: createdProvider.value.metadata.namespace,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const namespace = 'default';
|
if (connected) {
|
||||||
const secretName = `${ providerName.value }-creds-${ randomStr(4).toLowerCase() }`;
|
testPassed.value = true;
|
||||||
|
testResult.value = t('harvester.addons.forklift.configureProvider.testSuccess');
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
if (createdProvider.value) {
|
||||||
|
await createdProvider.value.remove();
|
||||||
|
createdProvider.value = null;
|
||||||
|
createdSecret.value = null;
|
||||||
|
} else if (createdSecret.value) {
|
||||||
|
await createdSecret.value.remove();
|
||||||
|
createdSecret.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
const newSecret = await store.dispatch(`${ inStore }/create`, {
|
testError.value = errorMsg || t('harvester.addons.forklift.configureProvider.testTimeout');
|
||||||
type: SECRET,
|
buttonCb(false);
|
||||||
metadata: {
|
}
|
||||||
name: secretName,
|
} catch (err) {
|
||||||
namespace,
|
if (createdProvider.value) {
|
||||||
labels: {
|
try {
|
||||||
createdForProviderType: 'vsphere',
|
await createdProvider.value.remove();
|
||||||
createdForResourceType: 'providers',
|
} catch (e) {}
|
||||||
}
|
createdProvider.value = null;
|
||||||
}
|
}
|
||||||
});
|
if (createdSecret.value) {
|
||||||
|
try {
|
||||||
|
await createdSecret.value.remove();
|
||||||
|
} catch (e) {}
|
||||||
|
createdSecret.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
newSecret['_type'] = 'Opaque';
|
testError.value = err.message || t('harvester.addons.forklift.configureProvider.testFailed');
|
||||||
newSecret['data'] = {
|
buttonCb(false);
|
||||||
user: btoa(username.value),
|
}
|
||||||
password: btoa(password.value),
|
};
|
||||||
insecureSkipVerify: btoa(String(skipTlsVerify.value)),
|
|
||||||
url: btoa(url.value),
|
|
||||||
};
|
|
||||||
|
|
||||||
await newSecret.save();
|
const saveProvider = async(buttonCb) => {
|
||||||
|
errors.value = [];
|
||||||
const provider = await store.dispatch(`${ inStore }/create`, {
|
|
||||||
type: HCI.FORKLIFT_PROVIDER,
|
|
||||||
metadata: {
|
|
||||||
name: providerName.value,
|
|
||||||
namespace,
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
type: 'vsphere',
|
|
||||||
url: url.value,
|
|
||||||
secret: {
|
|
||||||
name: secretName,
|
|
||||||
namespace,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await provider.save();
|
|
||||||
|
|
||||||
|
if (!testPassed.value) {
|
||||||
|
// Test hasn't passed yet — run it first
|
||||||
|
await testConnection((success) => {
|
||||||
|
if (success) {
|
||||||
router.push({
|
router.push({
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-select-vms`,
|
name: `${ PRODUCT_NAME }-c-cluster-forklift-select-vms`,
|
||||||
params: {
|
params: {
|
||||||
product: route.params.product,
|
product: route.params.product,
|
||||||
cluster: route.params.cluster,
|
cluster: route.params.cluster,
|
||||||
},
|
},
|
||||||
query: {
|
query: { provider: providerName.value }
|
||||||
provider: providerName.value,
|
|
||||||
namespace,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} else {
|
||||||
errors.value = [err.message || err];
|
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const init = async() => {
|
return;
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
}
|
||||||
|
|
||||||
allSecrets.value = await store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
router.push({
|
||||||
loading.value = false;
|
name: `${ PRODUCT_NAME }-c-cluster-forklift-select-vms`,
|
||||||
};
|
params: {
|
||||||
|
product: route.params.product,
|
||||||
|
cluster: route.params.cluster,
|
||||||
|
},
|
||||||
|
query: { provider: providerName.value }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
init();
|
const init = async() => {
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
return {
|
allSecrets.value = await store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
||||||
schema,
|
loading.value = false;
|
||||||
allSecrets,
|
};
|
||||||
providerName,
|
|
||||||
url,
|
init();
|
||||||
username,
|
|
||||||
password,
|
|
||||||
skipTlsVerify,
|
|
||||||
testResult,
|
|
||||||
testError,
|
|
||||||
errors,
|
|
||||||
loading,
|
|
||||||
isFormValid,
|
|
||||||
t,
|
|
||||||
cancel,
|
|
||||||
testConnection,
|
|
||||||
saveProvider,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -402,8 +348,17 @@ export default defineComponent({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-20">
|
<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
|
<AsyncButton
|
||||||
|
v-else
|
||||||
mode="test"
|
mode="test"
|
||||||
|
:disabled="!isFormValid"
|
||||||
:action-label="t('harvester.addons.forklift.configureProvider.testConnection')"
|
:action-label="t('harvester.addons.forklift.configureProvider.testConnection')"
|
||||||
:waiting-label="t('harvester.addons.forklift.configureProvider.testConnection')"
|
:waiting-label="t('harvester.addons.forklift.configureProvider.testConnection')"
|
||||||
:success-label="t('harvester.addons.forklift.configureProvider.testConnection')"
|
:success-label="t('harvester.addons.forklift.configureProvider.testConnection')"
|
||||||
@ -474,4 +429,9 @@ export default defineComponent({
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.test-passed-btn {
|
||||||
|
color: var(--success) !important;
|
||||||
|
border-color: var(--success) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script setup>
|
||||||
import { defineComponent, ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
@ -24,135 +24,122 @@ const schema = {
|
|||||||
metadata: { name: HCI.FORKLIFT_PLAN },
|
metadata: { name: HCI.FORKLIFT_PLAN },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineComponent({
|
const store = useStore();
|
||||||
name: 'ForkliftMigrationDashboard',
|
const route = useRoute();
|
||||||
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
components: {
|
const loading = ref(true);
|
||||||
Loading,
|
|
||||||
Masthead,
|
|
||||||
MappingsCell,
|
|
||||||
PercentageBar,
|
|
||||||
ResourceTable,
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
const inStore = computed(() => store.getters['currentProduct'].inStore);
|
||||||
const store = useStore();
|
|
||||||
const route = useRoute();
|
|
||||||
const { t } = useI18n(store);
|
|
||||||
|
|
||||||
const loading = ref(true);
|
const allPlans = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_PLAN));
|
||||||
|
const allNetworkMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_NETWORK_MAP));
|
||||||
|
const allStorageMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_STORAGE_MAP));
|
||||||
|
|
||||||
const inStore = computed(() => store.getters['currentProduct'].inStore);
|
const rows = computed(() => {
|
||||||
|
return allPlans.value.map((plan) => {
|
||||||
|
const netMapName = plan.spec?.map?.network?.name;
|
||||||
|
const netMapNs = plan.spec?.map?.network?.namespace || plan.metadata.namespace;
|
||||||
|
const storMapName = plan.spec?.map?.storage?.name;
|
||||||
|
const storMapNs = plan.spec?.map?.storage?.namespace || plan.metadata.namespace;
|
||||||
|
|
||||||
const allPlans = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_PLAN));
|
const netMap = allNetworkMaps.value.find((m) => m.metadata.name === netMapName && m.metadata.namespace === netMapNs);
|
||||||
const allNetworkMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_NETWORK_MAP));
|
const storMap = allStorageMaps.value.find((m) => m.metadata.name === storMapName && m.metadata.namespace === storMapNs);
|
||||||
const allStorageMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_STORAGE_MAP));
|
|
||||||
|
|
||||||
const rows = computed(() => {
|
plan.networkEntries = (netMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.type === 'pod' ? 'Pod Network' : (e.destination?.name || '-') }`);
|
||||||
return allPlans.value.map((plan) => {
|
plan.storageEntries = (storMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.storageClass || '-' }`);
|
||||||
const netMapName = plan.spec?.map?.network?.name;
|
plan.networkDisplay = plan.networkEntries.join(', ') || '-';
|
||||||
const netMapNs = plan.spec?.map?.network?.namespace || plan.metadata.namespace;
|
plan.storageDisplay = plan.storageEntries.join(', ') || '-';
|
||||||
const storMapName = plan.spec?.map?.storage?.name;
|
|
||||||
const storMapNs = plan.spec?.map?.storage?.namespace || plan.metadata.namespace;
|
|
||||||
|
|
||||||
const netMap = allNetworkMaps.value.find((m) => m.metadata.name === netMapName && m.metadata.namespace === netMapNs);
|
plan.vmIdsDisplay = (plan.spec?.vms || []).map((vm) => vm.id || vm.name || '').filter(Boolean).join(', ') || '-';
|
||||||
const storMap = allStorageMaps.value.find((m) => m.metadata.name === storMapName && m.metadata.namespace === storMapNs);
|
|
||||||
|
|
||||||
plan.networkEntries = (netMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.type === 'pod' ? 'Pod Network' : (e.destination?.name || '-') }`);
|
const vms = plan.status?.migration?.vms || [];
|
||||||
plan.storageEntries = (storMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.storageClass || '-' }`);
|
|
||||||
plan.networkDisplay = plan.networkEntries.join(', ') || '-';
|
|
||||||
plan.storageDisplay = plan.storageEntries.join(', ') || '-';
|
|
||||||
|
|
||||||
plan.vmIdsDisplay = (plan.spec?.vms || []).map((vm) => vm.id || vm.name || '').filter(Boolean).join(', ') || '-';
|
plan.vmProgress = vms.map((vm) => {
|
||||||
|
const pipeline = vm.pipeline || [];
|
||||||
|
const totalSteps = pipeline.length || 1;
|
||||||
|
let overallProgress = 0;
|
||||||
|
let currentStep = '';
|
||||||
|
let errorMsg = '';
|
||||||
|
|
||||||
const vms = plan.status?.migration?.vms || [];
|
pipeline.forEach((step, idx) => {
|
||||||
|
const stepWeight = 100 / totalSteps;
|
||||||
|
|
||||||
plan.vmProgress = vms.map((vm) => {
|
if (step.phase === 'Completed') {
|
||||||
const pipeline = vm.pipeline || [];
|
overallProgress += stepWeight;
|
||||||
const totalSteps = pipeline.length;
|
} else {
|
||||||
const completedSteps = pipeline.filter((s) => s.phase === 'Completed').length;
|
const stepPct = (step.progress?.completed && step.progress?.total) ? (step.progress.completed / step.progress.total) * 100 : 0;
|
||||||
|
|
||||||
return {
|
overallProgress += (stepPct / 100) * stepWeight;
|
||||||
name: vm.id || vm.name || 'Unknown',
|
|
||||||
steps: pipeline.map((step) => ({
|
|
||||||
name: step.name || 'Unknown',
|
|
||||||
progress: step.phase === 'Completed' ? 100 : (step.progress?.completed && step.progress?.total ? Math.round((step.progress.completed / step.progress.total) * 100) : 0),
|
|
||||||
hasError: !!step.error,
|
|
||||||
reason: (step.error?.reasons || []).join('; ') || '',
|
|
||||||
})),
|
|
||||||
progress: totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
let totalSteps = 0;
|
if (!currentStep) {
|
||||||
let completedSteps = 0;
|
currentStep = step.name || `Step ${ idx + 1 }`;
|
||||||
|
}
|
||||||
|
|
||||||
vms.forEach((vm) => {
|
if (step.error) {
|
||||||
const pipeline = vm.pipeline || [];
|
errorMsg = (step.error.reasons || []).join('; ') || 'Error';
|
||||||
|
}
|
||||||
totalSteps += pipeline.length;
|
}
|
||||||
completedSteps += pipeline.filter((s) => s.phase === 'Completed').length;
|
|
||||||
});
|
|
||||||
|
|
||||||
plan.progress = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
|
|
||||||
|
|
||||||
return plan;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
overallProgress = Math.round(overallProgress);
|
||||||
|
|
||||||
|
return {
|
||||||
|
vmName: vm.name || vm.id || 'Unknown',
|
||||||
|
vmId: vm.id || '',
|
||||||
|
progress: overallProgress,
|
||||||
|
currentStep,
|
||||||
|
errorMsg,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const createLocation = computed(() => ({
|
plan.progress = plan.vmProgress.length > 0 ? Math.round(plan.vmProgress.reduce((sum, vm) => sum + vm.progress, 0) / plan.vmProgress.length) : 0;
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-configure-provider`,
|
|
||||||
params: {
|
|
||||||
product: route.params.product,
|
|
||||||
cluster: route.params.cluster,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const headers = [
|
return plan;
|
||||||
{ ...STATE, labelKey: 'harvester.addons.forklift.dashboard.columns.status' },
|
});
|
||||||
{
|
|
||||||
...NAME_COL,
|
|
||||||
labelKey: 'harvester.addons.forklift.dashboard.columns.plan',
|
|
||||||
subLabel: 'VM IDs',
|
|
||||||
},
|
|
||||||
{ ...FORKLIFT_PLAN_VM_COUNT, width: 105 },
|
|
||||||
{
|
|
||||||
name: 'progress',
|
|
||||||
labelKey: 'harvester.addons.forklift.dashboard.columns.progress',
|
|
||||||
value: 'progress',
|
|
||||||
width: 299,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'mappings',
|
|
||||||
labelKey: 'harvester.addons.forklift.dashboard.columns.mappings',
|
|
||||||
value: 'mappingsDisplay',
|
|
||||||
width: 387,
|
|
||||||
},
|
|
||||||
{ ...AGE },
|
|
||||||
];
|
|
||||||
|
|
||||||
const init = async() => {
|
|
||||||
await Promise.all([
|
|
||||||
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_PLAN }),
|
|
||||||
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP }),
|
|
||||||
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_STORAGE_MAP }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
loading.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
init();
|
|
||||||
|
|
||||||
return {
|
|
||||||
schema,
|
|
||||||
loading,
|
|
||||||
rows,
|
|
||||||
createLocation,
|
|
||||||
headers,
|
|
||||||
t,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createLocation = computed(() => ({
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-forklift-configure-provider`,
|
||||||
|
params: {
|
||||||
|
product: route.params.product,
|
||||||
|
cluster: route.params.cluster,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
{ ...STATE, labelKey: 'harvester.addons.forklift.dashboard.columns.status' },
|
||||||
|
{
|
||||||
|
...NAME_COL,
|
||||||
|
labelKey: 'harvester.addons.forklift.dashboard.columns.plan',
|
||||||
|
subLabel: 'VM IDs',
|
||||||
|
},
|
||||||
|
{ ...FORKLIFT_PLAN_VM_COUNT, width: 105 },
|
||||||
|
{
|
||||||
|
name: 'progress',
|
||||||
|
labelKey: 'harvester.addons.forklift.dashboard.columns.progress',
|
||||||
|
value: 'progress',
|
||||||
|
width: 500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mappings',
|
||||||
|
labelKey: 'harvester.addons.forklift.dashboard.columns.mappings',
|
||||||
|
value: 'mappingsDisplay',
|
||||||
|
},
|
||||||
|
{ ...AGE },
|
||||||
|
];
|
||||||
|
|
||||||
|
const init = async() => {
|
||||||
|
await Promise.all([
|
||||||
|
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_PLAN }),
|
||||||
|
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP }),
|
||||||
|
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_STORAGE_MAP }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -210,41 +197,43 @@ export default defineComponent({
|
|||||||
<template #cell:progress="{ row }">
|
<template #cell:progress="{ row }">
|
||||||
<div
|
<div
|
||||||
v-if="row.vmProgress && row.vmProgress.length"
|
v-if="row.vmProgress && row.vmProgress.length"
|
||||||
class="progress-steps"
|
class="progress-cells"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="vm in row.vmProgress"
|
v-for="vm in row.vmProgress"
|
||||||
:key="vm.name"
|
:key="vm.vmId"
|
||||||
class="vm-progress"
|
class="vm-progress"
|
||||||
>
|
>
|
||||||
<div class="vm-name">
|
<div class="vm-progress-header">
|
||||||
{{ vm.name }}
|
<div class="vm-name-block">
|
||||||
|
<span class="vm-name">{{ vm.vmName }}</span>
|
||||||
|
<span class="text-muted vm-id">id: {{ vm.vmId }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="vm-pct">{{ vm.progress }}%</span>
|
||||||
|
</div>
|
||||||
|
<PercentageBar
|
||||||
|
:model-value="vm.progress"
|
||||||
|
:color-stops="vm.errorMsg ? { 0: '--error' } : { 99: '--primary', 100: '--success' }"
|
||||||
|
preferred-direction="MORE"
|
||||||
|
class="vm-bar"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="vm.progress === 100"
|
||||||
|
class="step-label text-muted"
|
||||||
|
>
|
||||||
|
Finished Successfully
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="step in vm.steps"
|
v-else-if="vm.errorMsg"
|
||||||
:key="step.name"
|
class="step-label text-error"
|
||||||
class="progress-step"
|
|
||||||
>
|
>
|
||||||
<span
|
{{ vm.errorMsg }}
|
||||||
class="step-name"
|
|
||||||
:class="{ 'text-error': step.hasError }"
|
|
||||||
>
|
|
||||||
{{ step.name }}
|
|
||||||
</span>
|
|
||||||
<PercentageBar
|
|
||||||
:model-value="step.progress"
|
|
||||||
:color-stops="step.hasError ? { 0: '--error' } : { 99: '--primary', 100: '--success' }"
|
|
||||||
preferred-direction="MORE"
|
|
||||||
class="step-bar"
|
|
||||||
/>
|
|
||||||
<span class="step-pct">{{ step.progress }}%</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="step in vm.steps.filter(s => s.hasError)"
|
v-else-if="vm.currentStep"
|
||||||
:key="`error-${step.name}`"
|
class="step-label text-muted"
|
||||||
class="step-error-msg"
|
|
||||||
>
|
>
|
||||||
<span class="text-error">{{ step.reason }}</span>
|
{{ vm.currentStep }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -274,50 +263,54 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-steps {
|
.progress-cells {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-progress {
|
.vm-progress {
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
padding-bottom: 6px;
|
padding-bottom: 8px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vm-progress-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.vm-name {
|
.vm-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-step {
|
.vm-name-block {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: row;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
margin-bottom: 4px;
|
|
||||||
padding-right: 13px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-error-msg {
|
.vm-id {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-pct {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-bar {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-label {
|
||||||
|
font-size: 12px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
font-size: 12px;
|
line-height: 20px;
|
||||||
|
|
||||||
.text-error {
|
|
||||||
color: var(--error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-name {
|
|
||||||
font-size: 12px;
|
|
||||||
width: 110px;
|
|
||||||
min-width: 110px;
|
|
||||||
max-width: 110px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
&.text-error {
|
&.text-error {
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
@ -325,17 +318,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-bar {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-pct {
|
|
||||||
font-size: 12px;
|
|
||||||
min-width: 35px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-title {
|
.table-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script setup>
|
||||||
import { defineComponent, ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
@ -24,246 +24,247 @@ const schema = {
|
|||||||
metadata: { name: HCI.FORKLIFT_PLAN },
|
metadata: { name: HCI.FORKLIFT_PLAN },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineComponent({
|
const store = useStore();
|
||||||
name: 'ForkliftReviewMigration',
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
components: {
|
const vms = ref([]);
|
||||||
Loading,
|
const networkMappings = ref([]);
|
||||||
Masthead,
|
const storageMappings = ref([]);
|
||||||
AsyncButton,
|
const errors = ref([]);
|
||||||
Banner,
|
const loading = ref(true);
|
||||||
MappingsCell,
|
|
||||||
RcItemCard,
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
const NAMESPACE = 'forklift';
|
||||||
const store = useStore();
|
const TARGET_NAMESPACE = 'default';
|
||||||
const route = useRoute();
|
const providerName = computed(() => route.query.provider || 'vsphere');
|
||||||
const router = useRouter();
|
const networkMapName = computed(() => route.query.networkMap || '');
|
||||||
const { t } = useI18n(store);
|
const storageMapName = computed(() => route.query.storageMap || '');
|
||||||
|
|
||||||
const vms = ref([]);
|
const totalVCpu = computed(() => vms.value.reduce((sum, vm) => sum + (vm.cpuCount || vm.numCPU || 0), 0));
|
||||||
const networkMappings = ref([]);
|
|
||||||
const storageMappings = ref([]);
|
|
||||||
const errors = ref([]);
|
|
||||||
const loading = ref(true);
|
|
||||||
|
|
||||||
const providerName = computed(() => route.query.provider || 'vsphere');
|
const totalMemoryGB = computed(() => {
|
||||||
const namespace = computed(() => route.query.namespace || 'default');
|
const totalMB = vms.value.reduce((sum, vm) => sum + (vm.memoryMB || vm.memory || 0), 0);
|
||||||
const networkMapName = computed(() => route.query.networkMap || '');
|
|
||||||
const storageMapName = computed(() => route.query.storageMap || '');
|
|
||||||
|
|
||||||
const totalVCpu = computed(() => vms.value.reduce((sum, vm) => sum + (vm.cpuCount || vm.numCPU || 0), 0));
|
return Math.round(totalMB / 1024);
|
||||||
|
});
|
||||||
|
|
||||||
const totalMemoryGB = computed(() => {
|
const totalStorageGB = computed(() => {
|
||||||
const totalMB = vms.value.reduce((sum, vm) => sum + (vm.memoryMB || vm.memory || 0), 0);
|
let totalBytes = 0;
|
||||||
|
|
||||||
return Math.round(totalMB / 1024);
|
vms.value.forEach((vm) => {
|
||||||
});
|
if (vm.disks && vm.disks.length > 0) {
|
||||||
|
totalBytes += vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const totalStorageGB = computed(() => {
|
return Math.round(totalBytes / (1024 * 1024 * 1024));
|
||||||
let totalBytes = 0;
|
});
|
||||||
|
|
||||||
vms.value.forEach((vm) => {
|
const vmCards = computed(() => {
|
||||||
if (vm.disks && vm.disks.length > 0) {
|
return vms.value.map((vm) => {
|
||||||
totalBytes += vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
const cpus = vm.cpuCount || vm.numCPU || 0;
|
||||||
}
|
const memMB = vm.memoryMB || vm.memory || 0;
|
||||||
});
|
const memGB = memMB ? `${ Math.round(memMB / 1024) } GB` : '-';
|
||||||
|
|
||||||
return Math.round(totalBytes / (1024 * 1024 * 1024));
|
let totalDiskBytes = 0;
|
||||||
});
|
|
||||||
|
|
||||||
const vmCards = computed(() => {
|
if (vm.disks && vm.disks.length > 0) {
|
||||||
return vms.value.map((vm) => {
|
totalDiskBytes = vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
||||||
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;
|
const diskDisplay = totalDiskBytes ? `${ Math.round(totalDiskBytes / (1024 * 1024 * 1024)) } GB` : '-';
|
||||||
|
const os = vm.guestName || vm.guestOS || vm.os || '-';
|
||||||
|
|
||||||
if (vm.disks && vm.disks.length > 0) {
|
const vmNetworkMappings = networkMappings.value.filter((m) => m.usedBy && m.usedBy.includes(vm.name || vm.id));
|
||||||
totalDiskBytes = vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
const vmStorageMappings = storageMappings.value.filter((m) => m.usedBy && m.usedBy.includes(vm.name || vm.id));
|
||||||
}
|
|
||||||
|
|
||||||
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.vmId === vm.id);
|
|
||||||
const vmStorageMappings = storageMappings.value.filter((m) => m.vmId === vm.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: vm.id,
|
|
||||||
name: vm.name || vm.id,
|
|
||||||
os,
|
|
||||||
cpus,
|
|
||||||
memGB,
|
|
||||||
diskDisplay,
|
|
||||||
networkMappings: vmNetworkMappings,
|
|
||||||
storageMappings: vmStorageMappings,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
router.push({
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
|
||||||
params: {
|
|
||||||
product: route.params.product,
|
|
||||||
cluster: route.params.cluster,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const startMigration = async(buttonCb) => {
|
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const planName = `${ providerName.value }-plan-${ Math.random().toString(36).substring(2, 7) }`;
|
|
||||||
|
|
||||||
const planSpec = {
|
|
||||||
provider: {
|
|
||||||
source: {
|
|
||||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
|
||||||
kind: 'Provider',
|
|
||||||
name: providerName.value,
|
|
||||||
namespace: namespace.value,
|
|
||||||
},
|
|
||||||
destination: {
|
|
||||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
|
||||||
kind: 'Provider',
|
|
||||||
name: 'host',
|
|
||||||
namespace: 'forklift',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
map: {
|
|
||||||
network: {
|
|
||||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
|
||||||
kind: 'NetworkMap',
|
|
||||||
name: networkMapName.value,
|
|
||||||
namespace: namespace.value,
|
|
||||||
},
|
|
||||||
storage: {
|
|
||||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
|
||||||
kind: 'StorageMap',
|
|
||||||
name: storageMapName.value,
|
|
||||||
namespace: namespace.value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
targetNamespace: namespace.value,
|
|
||||||
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,
|
|
||||||
namespace: namespace.value,
|
|
||||||
},
|
|
||||||
spec: planSpec,
|
|
||||||
});
|
|
||||||
|
|
||||||
await plan.save();
|
|
||||||
|
|
||||||
const migrationName = `${ planName }-migration-${ Math.random().toString(36).substring(2, 7) }`;
|
|
||||||
|
|
||||||
const migration = await store.dispatch(`${ inStore }/create`, {
|
|
||||||
type: HCI.FORKLIFT_MIGRATION,
|
|
||||||
metadata: {
|
|
||||||
name: migrationName,
|
|
||||||
namespace: namespace.value,
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
plan: {
|
|
||||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
|
||||||
kind: 'Plan',
|
|
||||||
name: planName,
|
|
||||||
namespace: namespace.value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await migration.save();
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
|
||||||
params: {
|
|
||||||
product: route.params.product,
|
|
||||||
cluster: route.params.cluster,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
buttonCb(true);
|
|
||||||
} catch (err) {
|
|
||||||
errors.value = [err.message || err];
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = async() => {
|
|
||||||
const vmsParam = route.query.vms;
|
|
||||||
|
|
||||||
if (vmsParam) {
|
|
||||||
try {
|
|
||||||
const vmIds = JSON.parse(vmsParam);
|
|
||||||
|
|
||||||
vms.value = vmIds.map((id) => {
|
|
||||||
const found = vmData.find((vm) => vm.id === id);
|
|
||||||
|
|
||||||
return found || {
|
|
||||||
id, name: id, networks: [], disks: [], cpuCount: 0, memoryMB: 0, guestName: ''
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
vms.value = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const networkMappingsParam = route.query.networkMappings;
|
|
||||||
|
|
||||||
if (networkMappingsParam) {
|
|
||||||
try {
|
|
||||||
networkMappings.value = JSON.parse(networkMappingsParam);
|
|
||||||
} catch (e) {
|
|
||||||
networkMappings.value = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const storageMappingsParam = route.query.storageMappings;
|
|
||||||
|
|
||||||
if (storageMappingsParam) {
|
|
||||||
try {
|
|
||||||
storageMappings.value = JSON.parse(storageMappingsParam);
|
|
||||||
} catch (e) {
|
|
||||||
storageMappings.value = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
init();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
schema,
|
id: vm.id,
|
||||||
loading,
|
name: vm.name || vm.id,
|
||||||
vms,
|
os,
|
||||||
errors,
|
cpus,
|
||||||
providerName,
|
memGB,
|
||||||
namespace,
|
diskDisplay,
|
||||||
totalVCpu,
|
networkMappings: vmNetworkMappings,
|
||||||
totalMemoryGB,
|
storageMappings: vmStorageMappings,
|
||||||
totalStorageGB,
|
|
||||||
vmCards,
|
|
||||||
t,
|
|
||||||
cancel,
|
|
||||||
startMigration,
|
|
||||||
};
|
};
|
||||||
},
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cancel = async() => {
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
const allProviders = store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || [];
|
||||||
|
const provider = allProviders.find((p) => p.metadata.name === providerName.value && p.metadata.namespace === NAMESPACE);
|
||||||
|
|
||||||
|
if (provider) {
|
||||||
|
await provider.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||||
|
params: {
|
||||||
|
product: route.params.product,
|
||||||
|
cluster: route.params.cluster,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const startMigration = async(buttonCb) => {
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const planName = `${ providerName.value }-plan-${ Math.random().toString(36).substring(2, 7) }`;
|
||||||
|
|
||||||
|
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: networkMapName.value,
|
||||||
|
namespace: NAMESPACE,
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||||
|
kind: 'StorageMap',
|
||||||
|
name: storageMapName.value,
|
||||||
|
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,
|
||||||
|
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 }-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,
|
||||||
|
namespace: NAMESPACE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await migration.save();
|
||||||
|
|
||||||
|
// Set Plan as owner of Provider (Provider already owns NetworkMap, StorageMap, Secret)
|
||||||
|
const allProviders = store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || [];
|
||||||
|
const provider = allProviders.find((p) => p.metadata.name === providerName.value && p.metadata.namespace === NAMESPACE);
|
||||||
|
|
||||||
|
if (provider) {
|
||||||
|
provider.metadata.ownerReferences = [
|
||||||
|
...(provider.metadata.ownerReferences || []),
|
||||||
|
planOwnerRef,
|
||||||
|
];
|
||||||
|
await provider.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||||
|
params: {
|
||||||
|
product: route.params.product,
|
||||||
|
cluster: route.params.cluster,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonCb(true);
|
||||||
|
} catch (err) {
|
||||||
|
errors.value = [err.message || err];
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = async() => {
|
||||||
|
const vmsParam = route.query.vms;
|
||||||
|
|
||||||
|
if (vmsParam) {
|
||||||
|
try {
|
||||||
|
const vmIds = JSON.parse(vmsParam);
|
||||||
|
|
||||||
|
vms.value = vmIds.map((id) => {
|
||||||
|
const found = vmData.find((vm) => vm.id === id);
|
||||||
|
|
||||||
|
return found || {
|
||||||
|
id, name: id, networks: [], disks: [], cpuCount: 0, memoryMB: 0, guestName: ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
vms.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const networkMappingsParam = route.query.networkMappings;
|
||||||
|
|
||||||
|
if (networkMappingsParam) {
|
||||||
|
try {
|
||||||
|
networkMappings.value = JSON.parse(networkMappingsParam);
|
||||||
|
} catch (e) {
|
||||||
|
networkMappings.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageMappingsParam = route.query.storageMappings;
|
||||||
|
|
||||||
|
if (storageMappingsParam) {
|
||||||
|
try {
|
||||||
|
storageMappings.value = JSON.parse(storageMappingsParam);
|
||||||
|
} catch (e) {
|
||||||
|
storageMappings.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -310,7 +311,7 @@ export default defineComponent({
|
|||||||
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.targetNamespace') }}</span>
|
<span class="detail-label">{{ t('harvester.addons.forklift.reviewMigration.targetNamespace') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<span class="detail-value">{{ namespace }}</span>
|
<span class="detail-value">{{ TARGET_NAMESPACE }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-column">
|
<div class="detail-column">
|
||||||
@ -334,9 +335,6 @@ export default defineComponent({
|
|||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<span class="detail-value">
|
<span class="detail-value">
|
||||||
{{ t('harvester.addons.forklift.reviewMigration.coldMigration') }}
|
{{ t('harvester.addons.forklift.reviewMigration.coldMigration') }}
|
||||||
<span
|
|
||||||
class="ml-10 small-text"
|
|
||||||
>{{ t('harvester.addons.forklift.reviewMigration.vmsWillShutDown') }}</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
<script>
|
<script setup>
|
||||||
import { defineComponent, ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||||
import SortableTable from '@shell/components/SortableTable';
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { BadgeState } from '@components/BadgeState';
|
||||||
import { SCHEMA } from '@shell/config/types';
|
import { SCHEMA } from '@shell/config/types';
|
||||||
import { useI18n } from '@shell/composables/useI18n';
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
import { HCI } from '../../../../types';
|
import { HCI } from '../../../../types';
|
||||||
@ -22,257 +22,198 @@ const schema = {
|
|||||||
metadata: { name: HCI.FORKLIFT_PROVIDER },
|
metadata: { name: HCI.FORKLIFT_PROVIDER },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineComponent({
|
const store = useStore();
|
||||||
name: 'ForkliftSelectVms',
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
components: {
|
const allProviders = ref([]);
|
||||||
Loading,
|
const provider = ref(null);
|
||||||
Masthead,
|
const discoveredVMs = ref([]);
|
||||||
SortableTable,
|
const selectedVMs = ref([]);
|
||||||
LabeledInput,
|
const tableRows = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
|
||||||
|
const providerName = computed(() => provider.value?.metadata?.name || route.query.provider || '');
|
||||||
|
const vmCount = computed(() => discoveredVMs.value.length);
|
||||||
|
const selectedCount = computed(() => selectedVMs.value.length);
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
{
|
||||||
|
name: 'vmName',
|
||||||
|
labelKey: 'harvester.addons.forklift.selectVms.columns.vmName',
|
||||||
|
value: 'vmName',
|
||||||
|
sort: ['vmName'],
|
||||||
|
subLabel: 'Identifier',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'os',
|
||||||
|
labelKey: 'harvester.addons.forklift.selectVms.columns.os',
|
||||||
|
value: 'os',
|
||||||
|
sort: ['os'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'resources',
|
||||||
|
labelKey: 'harvester.addons.forklift.selectVms.columns.resources',
|
||||||
|
value: 'resources',
|
||||||
|
sort: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'powerState',
|
||||||
|
labelKey: 'harvester.addons.forklift.selectVms.columns.powerState',
|
||||||
|
value: 'powerState',
|
||||||
|
sort: ['powerState'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'network',
|
||||||
|
labelKey: 'harvester.addons.forklift.selectVms.columns.network',
|
||||||
|
value: 'network',
|
||||||
|
sort: ['network'],
|
||||||
|
subLabel: 'Identifier',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'datastore',
|
||||||
|
labelKey: 'harvester.addons.forklift.selectVms.columns.datastore',
|
||||||
|
value: 'datastore',
|
||||||
|
sort: ['datastore'],
|
||||||
|
subLabel: 'Identifier',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
setup() {
|
const buildTableRows = () => {
|
||||||
const store = useStore();
|
return discoveredVMs.value.map((vm) => {
|
||||||
const route = useRoute();
|
const cpus = vm.cpuCount || vm.numCPU || '-';
|
||||||
const router = useRouter();
|
const memMB = vm.memoryMB || vm.memory || 0;
|
||||||
const { t } = useI18n(store);
|
const memGB = memMB ? `${ Math.round(memMB / 1024) } GB` : '-';
|
||||||
|
|
||||||
const allProviders = ref([]);
|
let totalDiskBytes = 0;
|
||||||
const provider = ref(null);
|
|
||||||
const discoveredVMs = ref([]);
|
|
||||||
const selectedVMs = ref([]);
|
|
||||||
const manualVMs = ref([]);
|
|
||||||
const newVmId = ref('');
|
|
||||||
const tableRows = ref([]);
|
|
||||||
const loading = ref(true);
|
|
||||||
|
|
||||||
const providerName = computed(() => provider.value?.metadata?.name || route.query.provider || '');
|
if (vm.disks && vm.disks.length > 0) {
|
||||||
const vmCount = computed(() => discoveredVMs.value.length);
|
totalDiskBytes = vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
||||||
const showManualEntry = computed(() => discoveredVMs.value.length === 0);
|
}
|
||||||
const selectedCount = computed(() => showManualEntry.value ? manualVMs.value.length : selectedVMs.value.length);
|
|
||||||
|
|
||||||
const manualHeaders = [
|
const diskDisplay = totalDiskBytes ? `${ Math.round(totalDiskBytes / (1024 * 1024 * 1024)) } GB` : '-';
|
||||||
{
|
const rawPowerState = vm.powerState || vm.status?.phase || '-';
|
||||||
name: 'id',
|
const powerState = rawPowerState.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (c) => c.toUpperCase());
|
||||||
labelKey: 'harvester.addons.forklift.selectVms.columns.vmId',
|
|
||||||
value: 'id',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const headers = [
|
let networks = [];
|
||||||
{
|
|
||||||
name: 'vmName',
|
|
||||||
labelKey: 'harvester.addons.forklift.selectVms.columns.vmName',
|
|
||||||
value: 'vmName',
|
|
||||||
subLabel: 'Identifier',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'os',
|
|
||||||
labelKey: 'harvester.addons.forklift.selectVms.columns.os',
|
|
||||||
value: 'os',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'resources',
|
|
||||||
labelKey: 'harvester.addons.forklift.selectVms.columns.resources',
|
|
||||||
value: 'resources',
|
|
||||||
sort: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'powerState',
|
|
||||||
labelKey: 'harvester.addons.forklift.selectVms.columns.powerState',
|
|
||||||
value: 'powerState',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'network',
|
|
||||||
labelKey: 'harvester.addons.forklift.selectVms.columns.network',
|
|
||||||
value: 'network',
|
|
||||||
subLabel: 'Identifier',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'datastore',
|
|
||||||
labelKey: 'harvester.addons.forklift.selectVms.columns.datastore',
|
|
||||||
value: 'datastore',
|
|
||||||
subLabel: 'Identifier',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const buildTableRows = () => {
|
if (vm.networks && vm.networks.length > 0) {
|
||||||
return discoveredVMs.value.map((vm) => {
|
networks = vm.networks.map((n) => ({
|
||||||
const cpus = vm.cpuCount || vm.numCPU || '-';
|
name: n.name || n.id || n,
|
||||||
const memMB = vm.memoryMB || vm.memory || 0;
|
id: n.id || '',
|
||||||
const memGB = memMB ? `${ Math.round(memMB / 1024) } GB` : '-';
|
vlanId: n.vlanId || '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
let totalDiskBytes = 0;
|
const datastores = [];
|
||||||
|
|
||||||
if (vm.disks && vm.disks.length > 0) {
|
if (vm.disks && vm.disks.length > 0) {
|
||||||
totalDiskBytes = vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
const seen = new Set();
|
||||||
}
|
|
||||||
|
|
||||||
const diskDisplay = totalDiskBytes ? `${ Math.round(totalDiskBytes / (1024 * 1024 * 1024)) } GB` : '-';
|
vm.disks.forEach((d) => {
|
||||||
const powerState = vm.powerState || vm.status?.phase || '-';
|
const dsId = d.datastore?.id || d.datastore?.name;
|
||||||
|
|
||||||
let networks = [];
|
if (dsId && !seen.has(dsId)) {
|
||||||
|
seen.add(dsId);
|
||||||
if (vm.networks && vm.networks.length > 0) {
|
datastores.push({
|
||||||
networks = vm.networks.map((n) => ({
|
name: d.datastore?.name || dsId,
|
||||||
name: n.name || n.id || n,
|
id: d.datastore?.id || '',
|
||||||
id: n.id || '',
|
type: d.datastore?.type || '',
|
||||||
vlanId: n.vlanId || '',
|
capacity: d.capacity || 0,
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const datastores = [];
|
|
||||||
|
|
||||||
if (vm.disks && vm.disks.length > 0) {
|
|
||||||
const seen = new Set();
|
|
||||||
|
|
||||||
vm.disks.forEach((d) => {
|
|
||||||
const dsId = d.datastore?.id || d.datastore?.name;
|
|
||||||
|
|
||||||
if (dsId && !seen.has(dsId)) {
|
|
||||||
seen.add(dsId);
|
|
||||||
datastores.push({
|
|
||||||
name: d.datastore?.name || dsId,
|
|
||||||
id: d.datastore?.id || '',
|
|
||||||
type: d.datastore?.type || '',
|
|
||||||
capacity: d.capacity || 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
_original: vm,
|
|
||||||
_key: vm.id || vm.vmId || vm.metadata?.name,
|
|
||||||
vmName: vm.name || vm.metadata?.name || '-',
|
|
||||||
vmId: vm.id || vm.vmId || vm.metadata?.name || '-',
|
|
||||||
os: vm.guestName || vm.guestOS || vm.os || '-',
|
|
||||||
resourcesDisplay: `${ cpus } vCPU`,
|
|
||||||
resourcesSub: `${ memGB } • ${ diskDisplay }`,
|
|
||||||
powerState,
|
|
||||||
powerStateClass: powerState.toLowerCase().includes('on') || powerState.toLowerCase().includes('running') ? 'power-on' : 'power-off',
|
|
||||||
networks,
|
|
||||||
network: networks.map((n) => n.name).join(', ') || '-',
|
|
||||||
datastores,
|
|
||||||
datastore: datastores.map((d) => d.name).join(', ') || '-',
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
const addManualVm = () => {
|
|
||||||
if (!newVmId.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
manualVMs.value.push({
|
|
||||||
id: newVmId.value.trim(),
|
|
||||||
name: newVmId.value.trim(),
|
|
||||||
_key: `manual-${ newVmId.value.trim() }`,
|
|
||||||
});
|
|
||||||
|
|
||||||
newVmId.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeManualVm = (index) => {
|
|
||||||
manualVMs.value.splice(index, 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSelect = (rows) => {
|
|
||||||
selectedVMs.value = rows.map((r) => r._original);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
router.push({
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
|
||||||
params: {
|
|
||||||
product: route.params.product,
|
|
||||||
cluster: route.params.cluster,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveSelection = () => {
|
|
||||||
let vmIds;
|
|
||||||
|
|
||||||
if (showManualEntry.value) {
|
|
||||||
vmIds = manualVMs.value.map((vm) => vm.id);
|
|
||||||
} else {
|
|
||||||
vmIds = selectedVMs.value.map((vm) => vm.id || vm.vmId || vm.metadata?.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-configure-mappings`,
|
|
||||||
params: {
|
|
||||||
product: route.params.product,
|
|
||||||
cluster: route.params.cluster,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
provider: providerName.value,
|
|
||||||
namespace: provider.value?.metadata?.namespace || 'default',
|
|
||||||
vms: JSON.stringify(vmIds),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = async() => {
|
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
allProviders.value = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
|
||||||
|
|
||||||
const queryProvider = route.query.provider;
|
|
||||||
const namespace = route.query.namespace || 'default';
|
|
||||||
|
|
||||||
if (queryProvider) {
|
|
||||||
provider.value = allProviders.value.find(
|
|
||||||
(p) => p.metadata.name === queryProvider && p.metadata.namespace === namespace
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider.value) {
|
|
||||||
try {
|
|
||||||
const inventoryType = 'forklift-inventory.konveyor.io.vspherevm';
|
|
||||||
|
|
||||||
discoveredVMs.value = await store.dispatch(`${ inStore }/findAll`, { type: inventoryType });
|
|
||||||
} catch (e) {
|
|
||||||
discoveredVMs.value = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (discoveredVMs.value.length === 0) {
|
|
||||||
discoveredVMs.value = vmData;
|
|
||||||
}
|
|
||||||
|
|
||||||
tableRows.value = buildTableRows();
|
|
||||||
loading.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
init();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
schema,
|
_original: vm,
|
||||||
loading,
|
_key: vm.id || vm.vmId || vm.metadata?.name,
|
||||||
provider,
|
vmName: vm.name || vm.metadata?.name || '-',
|
||||||
providerName,
|
vmId: vm.id || vm.vmId || vm.metadata?.name || '-',
|
||||||
discoveredVMs,
|
os: vm.guestName || vm.guestOS || vm.os || '-',
|
||||||
selectedVMs,
|
resourcesDisplay: `${ cpus } vCPU`,
|
||||||
manualVMs,
|
resourcesSub: `${ memGB } • ${ diskDisplay }`,
|
||||||
newVmId,
|
powerState,
|
||||||
tableRows,
|
powerStateClass: powerState.toLowerCase().includes('on') || powerState.toLowerCase().includes('running') ? 'power-on' : 'power-off',
|
||||||
vmCount,
|
networks,
|
||||||
showManualEntry,
|
network: networks.map((n) => n.name).join(', ') || '-',
|
||||||
selectedCount,
|
datastores,
|
||||||
manualHeaders,
|
datastore: datastores.map((d) => d.name).join(', ') || '-',
|
||||||
headers,
|
|
||||||
t,
|
|
||||||
addManualVm,
|
|
||||||
removeManualVm,
|
|
||||||
onSelect,
|
|
||||||
cancel,
|
|
||||||
saveSelection,
|
|
||||||
};
|
};
|
||||||
},
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const onSelect = (rows) => {
|
||||||
|
selectedVMs.value = rows.map((r) => r._original);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = async() => {
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
const providers = store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || [];
|
||||||
|
const found = providers.find((p) => p.metadata.name === providerName.value && p.metadata.namespace === 'forklift');
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
await found.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||||
|
params: {
|
||||||
|
product: route.params.product,
|
||||||
|
cluster: route.params.cluster,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSelection = () => {
|
||||||
|
const vmIds = selectedVMs.value.map((vm) => vm.id || vm.vmId || vm.metadata?.name);
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-forklift-configure-mappings`,
|
||||||
|
params: {
|
||||||
|
product: route.params.product,
|
||||||
|
cluster: route.params.cluster,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
provider: providerName.value,
|
||||||
|
vms: JSON.stringify(vmIds),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = async() => {
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
allProviders.value = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
||||||
|
|
||||||
|
const queryProvider = route.query.provider;
|
||||||
|
|
||||||
|
if (queryProvider) {
|
||||||
|
provider.value = allProviders.value.find(
|
||||||
|
(p) => p.metadata.name === queryProvider && p.metadata.namespace === 'forklift'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider.value) {
|
||||||
|
try {
|
||||||
|
const inventoryType = 'forklift-inventory.konveyor.io.vspherevm';
|
||||||
|
|
||||||
|
discoveredVMs.value = await store.dispatch(`${ inStore }/findAll`, { type: inventoryType });
|
||||||
|
} catch (e) {
|
||||||
|
discoveredVMs.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (discoveredVMs.value.length === 0) {
|
||||||
|
discoveredVMs.value = vmData;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableRows.value = buildTableRows();
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -299,66 +240,8 @@ export default defineComponent({
|
|||||||
</template>
|
</template>
|
||||||
</Masthead>
|
</Masthead>
|
||||||
|
|
||||||
<!-- Manual VM entry when no discovered VMs -->
|
|
||||||
<div v-if="showManualEntry">
|
|
||||||
<h3 class="m-0 mb-10">
|
|
||||||
{{ t('harvester.addons.forklift.selectVms.manualEntry.title') }}
|
|
||||||
</h3>
|
|
||||||
<p class="text-muted mb-20">
|
|
||||||
{{ t('harvester.addons.forklift.selectVms.manualEntry.description') }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="manual-vm-form">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="newVmId"
|
|
||||||
:label="t('harvester.addons.forklift.selectVms.manualEntry.vmId')"
|
|
||||||
:placeholder="t('harvester.addons.forklift.selectVms.manualEntry.vmIdPlaceholder')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-2 add-btn-col">
|
|
||||||
<button
|
|
||||||
class="btn role-primary"
|
|
||||||
:disabled="!newVmId"
|
|
||||||
@click="addManualVm"
|
|
||||||
>
|
|
||||||
{{ t('harvester.addons.forklift.selectVms.manualEntry.add') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SortableTable
|
|
||||||
:rows="manualVMs"
|
|
||||||
:headers="manualHeaders"
|
|
||||||
:search="false"
|
|
||||||
:table-actions="false"
|
|
||||||
:row-actions="false"
|
|
||||||
:groupable="false"
|
|
||||||
key-field="_key"
|
|
||||||
class="mt-20"
|
|
||||||
>
|
|
||||||
<template #header-left>
|
|
||||||
<span class="text-muted">{{ manualVMs.length }} {{ t('harvester.addons.forklift.selectVms.manualEntry.added') }}</span>
|
|
||||||
</template>
|
|
||||||
<template #cell:id="{ row }">
|
|
||||||
<div class="manual-vm-row">
|
|
||||||
<span>{{ row.id }}</span>
|
|
||||||
<button
|
|
||||||
class="btn btn-sm role-link"
|
|
||||||
@click="removeManualVm(manualVMs.indexOf(row))"
|
|
||||||
>
|
|
||||||
<i class="icon icon-close" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</SortableTable>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Discovered VMs table -->
|
<!-- Discovered VMs table -->
|
||||||
<SortableTable
|
<SortableTable
|
||||||
v-else
|
|
||||||
:rows="tableRows"
|
:rows="tableRows"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
:search="true"
|
:search="true"
|
||||||
@ -387,10 +270,10 @@ export default defineComponent({
|
|||||||
<span class="text-muted">{{ row.resourcesSub }}</span>
|
<span class="text-muted">{{ row.resourcesSub }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template #cell:powerState="{ row }">
|
<template #cell:powerState="{ row }">
|
||||||
<span
|
<BadgeState
|
||||||
class="power-badge"
|
:label="row.powerState"
|
||||||
:class="row.powerStateClass"
|
:color="row.powerStateClass === 'power-on' ? 'bg-warning' : 'bg-darker'"
|
||||||
>{{ row.powerState }}</span>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #cell:network="{ row }">
|
<template #cell:network="{ row }">
|
||||||
<div>
|
<div>
|
||||||
@ -456,59 +339,17 @@ export default defineComponent({
|
|||||||
.vm-name-cell {
|
.vm-name-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
.vm-name {
|
.vm-name {
|
||||||
font-weight: 600;
|
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-id {
|
.vm-id {
|
||||||
font-size: 12px;
|
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.power-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
&.power-on {
|
|
||||||
background-color: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.power-off {
|
|
||||||
background-color: #fff3cd;
|
|
||||||
color: #856404;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.manual-vm-form {
|
|
||||||
.add-btn-col {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.manual-vm-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multi-line-cell {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-4 {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-footer {
|
.actions-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user