mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-06 22:26:05 +08:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8a0885fe6 | ||
|
|
a0f21d5902 | ||
|
|
f0929100f0 | ||
|
|
78ca4bb663 | ||
|
|
eb917c9d18 |
@@ -28,4 +28,8 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
id-token: write
|
id-token: write
|
||||||
with:
|
with:
|
||||||
|
<<<<<<< HEAD
|
||||||
CI_BRANCH: ${{github.ref_name}}
|
CI_BRANCH: ${{github.ref_name}}
|
||||||
|
=======
|
||||||
|
CI_BRANCH: ${{ github.event_name == 'push' && github.ref_name || '' }}
|
||||||
|
>>>>>>> 719c33f (ci: fix CI_BRANCH ternary so PR builds skip upload gate (#1062))
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const {
|
|||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
skipTlsVerify,
|
skipTlsVerify,
|
||||||
|
vddkInitImage,
|
||||||
testPassed,
|
testPassed,
|
||||||
testResult,
|
testResult,
|
||||||
testError,
|
testError,
|
||||||
@@ -97,6 +98,7 @@ watch(selectedProvider, (val) => {
|
|||||||
username.value = '';
|
username.value = '';
|
||||||
password.value = '';
|
password.value = '';
|
||||||
skipTlsVerify.value = false;
|
skipTlsVerify.value = false;
|
||||||
|
vddkInitImage.value = '';
|
||||||
createdProvider.value = null;
|
createdProvider.value = null;
|
||||||
createdSecret.value = null;
|
createdSecret.value = null;
|
||||||
} else {
|
} else {
|
||||||
@@ -107,6 +109,7 @@ watch(selectedProvider, (val) => {
|
|||||||
if (provider) {
|
if (provider) {
|
||||||
providerName.value = provider.metadata.name;
|
providerName.value = provider.metadata.name;
|
||||||
url.value = provider.spec?.url || '';
|
url.value = provider.spec?.url || '';
|
||||||
|
vddkInitImage.value = provider.spec?.settings?.vddkInitImage || '';
|
||||||
|
|
||||||
const secretRef = provider.spec?.secret;
|
const secretRef = provider.spec?.secret;
|
||||||
|
|
||||||
@@ -239,6 +242,18 @@ const testConnection = async(buttonCb) => {
|
|||||||
if (props.editMode && createdProvider.value) {
|
if (props.editMode && createdProvider.value) {
|
||||||
try {
|
try {
|
||||||
createdProvider.value.spec.url = url.value;
|
createdProvider.value.spec.url = url.value;
|
||||||
|
|
||||||
|
const vddkImage = vddkInitImage.value?.trim();
|
||||||
|
|
||||||
|
if (vddkImage) {
|
||||||
|
createdProvider.value.spec.settings = {
|
||||||
|
...(createdProvider.value.spec.settings || {}),
|
||||||
|
vddkInitImage: vddkImage,
|
||||||
|
};
|
||||||
|
} else if (createdProvider.value.spec.settings) {
|
||||||
|
delete createdProvider.value.spec.settings.vddkInitImage;
|
||||||
|
}
|
||||||
|
|
||||||
await createdProvider.value.save();
|
await createdProvider.value.save();
|
||||||
|
|
||||||
const secretRef = createdProvider.value.spec?.secret;
|
const secretRef = createdProvider.value.spec?.secret;
|
||||||
@@ -283,20 +298,28 @@ const testConnection = async(buttonCb) => {
|
|||||||
const namespace = FORKLIFT_NAMESPACE;
|
const namespace = FORKLIFT_NAMESPACE;
|
||||||
const secretName = `${ providerName.value }-creds-${ randomStr(4).toLowerCase() }`;
|
const secretName = `${ providerName.value }-creds-${ randomStr(4).toLowerCase() }`;
|
||||||
|
|
||||||
const provider = await store.dispatch(`${ inStore }/create`, {
|
const spec = {
|
||||||
type: HCI.FORKLIFT_PROVIDER,
|
|
||||||
metadata: {
|
|
||||||
name: providerName.value,
|
|
||||||
namespace,
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
type: 'vsphere',
|
type: 'vsphere',
|
||||||
url: url.value,
|
url: url.value,
|
||||||
secret: {
|
secret: {
|
||||||
name: secretName,
|
name: secretName,
|
||||||
namespace,
|
namespace,
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const vddkImage = vddkInitImage.value?.trim();
|
||||||
|
|
||||||
|
if (vddkImage) {
|
||||||
|
spec.settings = { vddkInitImage: vddkImage };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const provider = await store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: HCI.FORKLIFT_PROVIDER,
|
||||||
|
metadata: {
|
||||||
|
name: providerName.value,
|
||||||
|
namespace,
|
||||||
|
},
|
||||||
|
spec,
|
||||||
});
|
});
|
||||||
|
|
||||||
await provider.save();
|
await provider.save();
|
||||||
@@ -420,7 +443,8 @@ defineExpose({ testConnection, clickTestButton });
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-model:value="url"
|
v-model:value="url"
|
||||||
:label="t('harvester.addons.vmMigration.configureProvider.urlLabel')"
|
:label="t('harvester.addons.vmMigration.configureProvider.urlLabel')"
|
||||||
@@ -432,6 +456,16 @@ defineExpose({ testConnection, clickTestButton });
|
|||||||
{{ t('harvester.addons.vmMigration.configureProvider.urlHint') }}
|
{{ t('harvester.addons.vmMigration.configureProvider.urlHint') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="vddkInitImage"
|
||||||
|
:label="t('harvester.addons.vmMigration.configureProvider.vddkImageLabel')"
|
||||||
|
:placeholder="t('harvester.addons.vmMigration.configureProvider.vddkImagePlaceholder')"
|
||||||
|
:tooltip="t('harvester.addons.vmMigration.configureProvider.vddkImageTooltip')"
|
||||||
|
:disabled="isExistingProvider && !editMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { RcItemCard } from '@components/RcItemCard';
|
import { RcItemCard } from '@components/RcItemCard';
|
||||||
@@ -18,8 +17,10 @@ const props = defineProps({
|
|||||||
clearable: { type: Boolean, default: false },
|
clearable: { type: Boolean, default: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectOptions = computed(() => {
|
// Only offer "Remove Map" for entries that already have a target selected;
|
||||||
if (!props.clearable) {
|
// entries without a selection just show the regular options.
|
||||||
|
const optionsFor = (entry) => {
|
||||||
|
if (!props.clearable || !entry.target) {
|
||||||
return props.options;
|
return props.options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,14 +35,9 @@ const selectOptions = computed(() => {
|
|||||||
disabled: true,
|
disabled: true,
|
||||||
kind: 'divider',
|
kind: 'divider',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: `${ props.placeholder }:`,
|
|
||||||
disabled: true,
|
|
||||||
kind: 'title',
|
|
||||||
},
|
|
||||||
...props.options,
|
...props.options,
|
||||||
];
|
];
|
||||||
});
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -85,7 +81,7 @@ const selectOptions = computed(() => {
|
|||||||
<div class="mapping-target">
|
<div class="mapping-target">
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="entry.target"
|
v-model:value="entry.target"
|
||||||
:options="selectOptions"
|
:options="optionsFor(entry)"
|
||||||
:placeholder="placeholder+'...'"
|
:placeholder="placeholder+'...'"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { RcItemCard } from '@components/RcItemCard';
|
import { BadgeState } from '@components/BadgeState';
|
||||||
|
import { RcDropdown, RcDropdownItem, RcDropdownTrigger } from '@components/RcDropdown';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import MappingsCell from '../MappingsCell';
|
|
||||||
import { useI18n } from '@shell/composables/useI18n';
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
import { NAMESPACE } from '@shell/config/types';
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
@@ -23,17 +24,19 @@ const props = defineProps({
|
|||||||
stepData: { type: Object, required: true },
|
stepData: { type: Object, required: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['ready']);
|
const emit = defineEmits(['ready', 'edit-selection', 'edit-mappings', 'remove-vm']);
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { t } = useI18n(store);
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
const vms = ref([]);
|
const vms = computed(() => props.selectedVms);
|
||||||
const networkMappings = ref([]);
|
const networkMappings = ref([]);
|
||||||
const storageMappings = ref([]);
|
const storageMappings = ref([]);
|
||||||
const planName = ref('');
|
const planName = ref('');
|
||||||
const targetNamespace = ref('');
|
const targetNamespace = ref('');
|
||||||
const namespaceOptions = ref([]);
|
const namespaceOptions = ref([]);
|
||||||
|
const existingProviderNames = ref([]);
|
||||||
|
const existingPlanNames = ref([]);
|
||||||
const errors = ref([]);
|
const errors = ref([]);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
|
|
||||||
@@ -43,6 +46,24 @@ targetNamespace.value = props.stepData.targetNamespace || '';
|
|||||||
|
|
||||||
const NS = FORKLIFT_NAMESPACE;
|
const NS = FORKLIFT_NAMESPACE;
|
||||||
|
|
||||||
|
const nameConflictError = computed(() => {
|
||||||
|
const name = (planName.value || '').trim();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingProviderNames.value.includes(name)) {
|
||||||
|
return t('harvester.addons.vmMigration.reviewMigration.planNameProviderConflict', { name });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingPlanNames.value.includes(name)) {
|
||||||
|
return t('harvester.addons.vmMigration.reviewMigration.planNamePlanConflict', { name });
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
watch(planName, (val) => {
|
watch(planName, (val) => {
|
||||||
props.stepData.planName = val;
|
props.stepData.planName = val;
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
@@ -51,8 +72,8 @@ watch(targetNamespace, (val) => {
|
|||||||
props.stepData.targetNamespace = val;
|
props.stepData.targetNamespace = val;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch([planName, targetNamespace], ([name, namespace]) => {
|
watch([planName, targetNamespace, nameConflictError], () => {
|
||||||
emit('ready', !!name && !!namespace);
|
emit('ready', !!planName.value && !!targetNamespace.value && !nameConflictError.value);
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
const formatNetworkTarget = (target = '') => {
|
const formatNetworkTarget = (target = '') => {
|
||||||
@@ -87,7 +108,7 @@ const totalStorageGB = computed(() => {
|
|||||||
return bytesToGB(totalBytes);
|
return bytesToGB(totalBytes);
|
||||||
});
|
});
|
||||||
|
|
||||||
const vmCards = computed(() => {
|
const vmRows = computed(() => {
|
||||||
return vms.value.map((vm) => {
|
return vms.value.map((vm) => {
|
||||||
const cpus = vm.cpuCount || vm.numCPU || 0;
|
const cpus = vm.cpuCount || vm.numCPU || 0;
|
||||||
const memMB = vm.memoryMB || vm.memory || 0;
|
const memMB = vm.memoryMB || vm.memory || 0;
|
||||||
@@ -101,23 +122,93 @@ const vmCards = computed(() => {
|
|||||||
|
|
||||||
const diskDisplay = totalDiskBytes ? t('harvester.addons.vmMigration.generic.memoryGb', { value: bytesToGB(totalDiskBytes) }) : '-';
|
const diskDisplay = totalDiskBytes ? t('harvester.addons.vmMigration.generic.memoryGb', { value: bytesToGB(totalDiskBytes) }) : '-';
|
||||||
const os = vm.guestName || vm.guestOS || vm.os || '-';
|
const os = vm.guestName || vm.guestOS || vm.os || '-';
|
||||||
|
const rawPowerState = vm.powerState || vm.status?.phase || '-';
|
||||||
|
const powerState = rawPowerState.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (c) => c.toUpperCase());
|
||||||
|
const powerStateClass = powerState.toLowerCase().includes('on') || powerState.toLowerCase().includes('running') ? 'power-on' : 'power-off';
|
||||||
|
|
||||||
const vmNetworkMappings = networkMappings.value.filter((m) => m.usedBy && m.usedBy.includes(vm.name || vm.id));
|
const vmNetworkMappings = networkMappings.value.filter((m) => m.usedBy && m.usedBy.includes(vm.name || vm.id));
|
||||||
const vmStorageMappings = storageMappings.value.filter((m) => m.usedBy && m.usedBy.includes(vm.name || vm.id));
|
const vmStorageMappings = storageMappings.value.filter((m) => m.usedBy && m.usedBy.includes(vm.name || vm.id));
|
||||||
|
|
||||||
|
const networkMappingSort = vmNetworkMappings.map((m) => `${ m.source } ${ formatNetworkTarget(m.target) }`).join(', ');
|
||||||
|
const storageMappingSort = vmStorageMappings.map((m) => `${ m.source } ${ m.target }`).join(', ');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
_key: vm.id || vm.vmId || vm.metadata?.name,
|
||||||
id: vm.id,
|
id: vm.id,
|
||||||
name: vm.name || vm.id,
|
name: vm.name || vm.id,
|
||||||
|
identifier: vm.id || vm.vmId || vm.metadata?.name || '-',
|
||||||
os,
|
os,
|
||||||
cpus,
|
resources: t('harvester.addons.vmMigration.generic.vCpu', { count: cpus }),
|
||||||
memGB,
|
resourcesSub: `${ memGB } • ${ diskDisplay }`,
|
||||||
diskDisplay,
|
powerState,
|
||||||
|
powerStateClass,
|
||||||
networkMappings: vmNetworkMappings,
|
networkMappings: vmNetworkMappings,
|
||||||
storageMappings: vmStorageMappings,
|
storageMappings: vmStorageMappings,
|
||||||
|
networkMapping: networkMappingSort,
|
||||||
|
storageMapping: storageMappingSort,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const vmHeaders = [
|
||||||
|
{
|
||||||
|
name: 'vmName',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.reviewMigration.columns.vmName',
|
||||||
|
value: 'name',
|
||||||
|
sort: ['name'],
|
||||||
|
subLabel: t('harvester.addons.vmMigration.generic.identifier'),
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'os',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.reviewMigration.columns.os',
|
||||||
|
value: 'os',
|
||||||
|
sort: ['os'],
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'resources',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.reviewMigration.columns.resources',
|
||||||
|
value: 'resources',
|
||||||
|
sort: false,
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'powerState',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.reviewMigration.columns.powerState',
|
||||||
|
value: 'powerState',
|
||||||
|
sort: ['powerState'],
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'networkMapping',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.reviewMigration.columns.networkMapping',
|
||||||
|
value: 'networkMapping',
|
||||||
|
sort: ['networkMapping'],
|
||||||
|
subLabel: t('harvester.addons.vmMigration.reviewMigration.columns.sourceTarget'),
|
||||||
|
width: 220,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'storageMapping',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.reviewMigration.columns.storageMapping',
|
||||||
|
value: 'storageMapping',
|
||||||
|
sort: ['storageMapping'],
|
||||||
|
subLabel: t('harvester.addons.vmMigration.reviewMigration.columns.sourceTarget'),
|
||||||
|
width: 220,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
label: ' ',
|
||||||
|
align: 'right',
|
||||||
|
sort: false,
|
||||||
|
width: 40,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const onEditSelection = () => emit('edit-selection');
|
||||||
|
const onEditMappings = () => emit('edit-mappings');
|
||||||
|
const onRemoveVm = (id) => emit('remove-vm', id);
|
||||||
|
|
||||||
const startMigrationAction = async() => {
|
const startMigrationAction = async() => {
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
@@ -201,8 +292,6 @@ const startMigrationAction = async() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const init = async() => {
|
const init = async() => {
|
||||||
vms.value = props.selectedVms;
|
|
||||||
|
|
||||||
const inStore = store.getters['currentProduct'].inStore;
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -213,6 +302,22 @@ const init = async() => {
|
|||||||
namespaceOptions.value = [];
|
namespaceOptions.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
existingProviderNames.value = (store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || [])
|
||||||
|
.map((p) => p.metadata?.name)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PLAN });
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
existingPlanNames.value = (store.getters[`${ inStore }/all`](HCI.FORKLIFT_PLAN) || [])
|
||||||
|
.map((p) => p.metadata?.name)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
if (props.mappingEntries) {
|
if (props.mappingEntries) {
|
||||||
networkMappings.value = (props.mappingEntries.networkEntries || []).map((entry) => ({
|
networkMappings.value = (props.mappingEntries.networkEntries || []).map((entry) => ({
|
||||||
source: entry.name || entry.id,
|
source: entry.name || entry.id,
|
||||||
@@ -244,12 +349,31 @@ defineExpose({ startMigration: startMigrationAction });
|
|||||||
<div
|
<div
|
||||||
class="review-migration-content"
|
class="review-migration-content"
|
||||||
>
|
>
|
||||||
|
<Banner
|
||||||
|
v-if="nameConflictError"
|
||||||
|
color="error"
|
||||||
|
:label="nameConflictError"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Migration Details Summary -->
|
<!-- Migration Details Summary -->
|
||||||
<div class="migration-details">
|
<div class="migration-details">
|
||||||
|
<div class="section-header">
|
||||||
<h3 class="section-title m-0">
|
<h3 class="section-title m-0">
|
||||||
{{ t('harvester.addons.vmMigration.reviewMigration.migrationDetails') }}
|
{{ t('harvester.addons.vmMigration.reviewMigration.migrationDetails') }}
|
||||||
</h3>
|
</h3>
|
||||||
|
<span class="section-description text-deemphasized">
|
||||||
|
{{ t('harvester.addons.vmMigration.reviewMigration.migrationDetailsDescription') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="name-row">
|
<div class="name-row">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="planName"
|
||||||
|
class="name-input"
|
||||||
|
:label="t('harvester.addons.vmMigration.reviewMigration.planName')"
|
||||||
|
:placeholder="t('harvester.addons.vmMigration.reviewMigration.planNamePlaceholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="targetNamespace"
|
v-model:value="targetNamespace"
|
||||||
class="namespace-select"
|
class="namespace-select"
|
||||||
@@ -258,110 +382,144 @@ defineExpose({ startMigration: startMigrationAction });
|
|||||||
:options="namespaceOptions"
|
:options="namespaceOptions"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<LabeledInput
|
|
||||||
v-model:value="planName"
|
|
||||||
class="name-input"
|
|
||||||
:label="t('harvester.addons.vmMigration.reviewMigration.planName')"
|
|
||||||
:placeholder="t('harvester.addons.vmMigration.reviewMigration.planNamePlaceholder')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="details-grid span-9">
|
|
||||||
<div class="detail-column">
|
<div class="summary-row">
|
||||||
<div class="detail-item">
|
<div class="migration-mode-column">
|
||||||
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.totalVms') }}</span>
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.migrationMode') }}</span>
|
||||||
<span class="detail-value detail-value-large">{{ vms.length }}</span>
|
<span class="cold-migration-badge">{{ t('harvester.addons.vmMigration.reviewMigration.coldMigration') }}</span>
|
||||||
|
<span class="text-deemphasized">{{ t('harvester.addons.vmMigration.reviewMigration.coldMigrationDescription') }}</span>
|
||||||
|
<span class="text-deemphasized">{{ t('harvester.addons.vmMigration.reviewMigration.coldMigrationHint') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="detail-column">
|
<div class="stats-column">
|
||||||
<div class="detail-item">
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.provider') }}</span>
|
||||||
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.source') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-value">{{ providerName }}</span>
|
<span class="detail-value">{{ providerName }}</span>
|
||||||
</div>
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.totalVms') }}</span>
|
||||||
</div>
|
<span class="detail-value">{{ vms.length }}</span>
|
||||||
<div class="detail-column">
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.targetNamespace') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-value">{{ targetNamespace }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="detail-column">
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.vcpu') }}</span>
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.vcpu') }}</span>
|
||||||
<span class="detail-value">{{ totalVCpu }}</span>
|
<span class="detail-value">{{ totalVCpu }}</span>
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.memory') }}</span>
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.memory') }}</span>
|
||||||
<span class="detail-value">{{ t('harvester.addons.vmMigration.generic.memoryGb', { value: totalMemoryGB }) }}</span>
|
<span class="detail-value">{{ t('harvester.addons.vmMigration.generic.memoryGb', { value: totalMemoryGB }) }}</span>
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.storage') }}</span>
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.storage') }}</span>
|
||||||
<span class="detail-value">{{ t('harvester.addons.vmMigration.generic.memoryGb', { value: totalStorageGB }) }}</span>
|
<span class="detail-value">{{ t('harvester.addons.vmMigration.generic.memoryGb', { value: totalStorageGB }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-column grid-column-2">
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.migrationMode') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<span class="detail-value">
|
|
||||||
{{ t('harvester.addons.vmMigration.reviewMigration.coldMigration') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Virtual Machines -->
|
<!-- Virtual Machines -->
|
||||||
<div class="vm-section">
|
<div class="vm-section">
|
||||||
<h3 class="section-title m-0">
|
<SortableTable
|
||||||
{{ t('harvester.addons.vmMigration.reviewMigration.virtualMachines') }} ({{ vms.length }})
|
class="vm-sortable-table"
|
||||||
</h3>
|
:rows="vmRows"
|
||||||
|
:headers="vmHeaders"
|
||||||
<div class="vm-cards-grid">
|
:search="false"
|
||||||
<RcItemCard
|
:table-actions="false"
|
||||||
v-for="vm in vmCards"
|
:row-actions="false"
|
||||||
:id="vm.id"
|
:groupable="false"
|
||||||
:key="vm.id"
|
:paging="true"
|
||||||
:variant="'small'"
|
key-field="_key"
|
||||||
:header="{ title: { text: vm.name } }"
|
|
||||||
>
|
>
|
||||||
<template #item-card-content>
|
<template #header-left>
|
||||||
<div class="vm-card-content">
|
<h4 class="section-title m-0">
|
||||||
<div class="vm-card-specs">
|
{{ t('harvester.addons.vmMigration.reviewMigration.virtualMachines') }} ({{ vms.length }})
|
||||||
<span class="vm-os text-deemphasized">{{ vm.os }}</span>
|
</h4>
|
||||||
<span class="vm-resources">
|
</template>
|
||||||
<i
|
<template #header-right>
|
||||||
class="icon icon-disk"
|
<a
|
||||||
aria-hidden="true"
|
href="#"
|
||||||
/>
|
class="edit-selection-link"
|
||||||
{{ t('harvester.addons.vmMigration.generic.vCpu', { count: vm.cpus }) }} • {{ vm.memGB }} • {{ vm.diskDisplay }}
|
@click.prevent="onEditSelection"
|
||||||
</span>
|
>
|
||||||
</div>
|
{{ t('harvester.addons.vmMigration.reviewMigration.editSelection') }}
|
||||||
<MappingsCell
|
</a>
|
||||||
:network-entries="vm.networkMappings.map(m => `${m.source} → ${formatNetworkTarget(m.target)}`)"
|
</template>
|
||||||
:storage-entries="vm.storageMappings.map(m => `${m.source} → ${m.target}`)"
|
<template #cell:vmName="{ row }">
|
||||||
/>
|
<div class="vm-name-cell">
|
||||||
|
<span class="vm-name">{{ row.name }}</span>
|
||||||
|
<span class="vm-id text-deemphasized">{{ row.identifier }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</RcItemCard>
|
<template #cell:resources="{ row }">
|
||||||
</div>
|
<span>{{ row.resources }}</span><br>
|
||||||
</div>
|
<span class="text-deemphasized">{{ row.resourcesSub }}</span>
|
||||||
|
</template>
|
||||||
<!-- Cold Migration Warning -->
|
<template #cell:powerState="{ row }">
|
||||||
<Banner
|
<BadgeState
|
||||||
color="warning"
|
:label="row.powerState"
|
||||||
class="m-0"
|
:color="row.powerStateClass === 'power-on' ? 'bg-warning' : 'bg-darker'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #cell:networkMapping="{ row }">
|
||||||
|
<template v-if="row.networkMappings.length">
|
||||||
|
<div
|
||||||
|
v-for="(m, i) in row.networkMappings"
|
||||||
|
:key="'net-' + i"
|
||||||
|
class="mapping-cell"
|
||||||
>
|
>
|
||||||
<div>
|
<span class="mapping-source">{{ m.source }}</span>
|
||||||
<span class="banner-title">{{ t('harvester.addons.vmMigration.reviewMigration.warningTitle') }}</span><br>
|
<span class="mapping-target text-deemphasized">→ {{ formatNetworkTarget(m.target) }}</span>
|
||||||
{{ t('harvester.addons.vmMigration.reviewMigration.warningMessage') }}
|
</div>
|
||||||
|
</template>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-deemphasized"
|
||||||
|
>-</span>
|
||||||
|
</template>
|
||||||
|
<template #cell:storageMapping="{ row }">
|
||||||
|
<template v-if="row.storageMappings.length">
|
||||||
|
<div
|
||||||
|
v-for="(m, i) in row.storageMappings"
|
||||||
|
:key="'stor-' + i"
|
||||||
|
class="mapping-cell"
|
||||||
|
>
|
||||||
|
<span class="mapping-source">{{ m.source }}</span>
|
||||||
|
<span class="mapping-target text-deemphasized">→ {{ m.target }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-deemphasized"
|
||||||
|
>-</span>
|
||||||
|
</template>
|
||||||
|
<template #cell:actions="{ row }">
|
||||||
|
<rc-dropdown :aria-label="t('harvester.addons.vmMigration.reviewMigration.actions.menuLabel')">
|
||||||
|
<rc-dropdown-trigger
|
||||||
|
variant="ghost"
|
||||||
|
:aria-label="t('harvester.addons.vmMigration.reviewMigration.actions.menuLabel')"
|
||||||
|
>
|
||||||
|
<i class="icon icon-actions" />
|
||||||
|
</rc-dropdown-trigger>
|
||||||
|
<template #dropdownCollection>
|
||||||
|
<rc-dropdown-item @click="onEditMappings">
|
||||||
|
{{ t('harvester.addons.vmMigration.reviewMigration.actions.editMappings') }}
|
||||||
|
</rc-dropdown-item>
|
||||||
|
<rc-dropdown-item
|
||||||
|
class="text-error"
|
||||||
|
@click="onRemoveVm(row.id)"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmMigration.reviewMigration.actions.removeFromPlan') }}
|
||||||
|
</rc-dropdown-item>
|
||||||
|
</template>
|
||||||
|
</rc-dropdown>
|
||||||
|
</template>
|
||||||
|
<template #no-rows>
|
||||||
|
<tr class="no-rows">
|
||||||
|
<td
|
||||||
|
:colspan="vmHeaders.length"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmMigration.reviewMigration.noVmSelected') }}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="edit-selection-link"
|
||||||
|
@click.prevent="onEditSelection"
|
||||||
|
>{{ t('harvester.addons.vmMigration.reviewMigration.noVmSelectedLink') }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</SortableTable>
|
||||||
</div>
|
</div>
|
||||||
</Banner>
|
|
||||||
|
|
||||||
<!-- Error banner -->
|
<!-- Error banner -->
|
||||||
<Banner
|
<Banner
|
||||||
@@ -390,43 +548,62 @@ defineExpose({ startMigration: startMigrationAction });
|
|||||||
}
|
}
|
||||||
|
|
||||||
.name-row {
|
.name-row {
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
.namespace-select {
|
|
||||||
max-width: 280px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-grid {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(230px, 1fr) 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
gap: 12px clamp(12px, 1vw, 96px);
|
||||||
gap: 12px clamp(64px, 8vw, 128px);
|
align-items: start;
|
||||||
line-height: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-column {
|
.summary-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px clamp(12px, 1vw, 96px);
|
||||||
|
line-height: 20px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
||||||
&.grid-column-2 {
|
.section-description {
|
||||||
grid-column: span 2;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-item {
|
.migration-mode-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.cold-migration-badge {
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 0.8;
|
||||||
|
font-weight: 600;
|
||||||
|
background: rgba(224, 191, 90, 0.16);
|
||||||
|
color: #E0BF5A;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
.theme-light & {
|
||||||
|
background: rgba(168, 128, 26, 0.16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-column {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
justify-content: start;
|
||||||
|
align-content: start;
|
||||||
|
column-gap: clamp(24px, 4vw, 64px);
|
||||||
|
row-gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-label {
|
.detail-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -436,45 +613,58 @@ defineExpose({ startMigration: startMigrationAction });
|
|||||||
.detail-value {
|
.detail-value {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
||||||
&.detail-value-large {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-selection-link {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--link);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-sortable-table {
|
||||||
|
:deep(.fixed-header-actions) {
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(table) {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(td) {
|
||||||
|
vertical-align: middle;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(th:first-child),
|
||||||
|
:deep(td:first-child) {
|
||||||
|
padding-left: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-cards-grid {
|
.vm-name-cell {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vm-card-content {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
|
||||||
line-height: 20px;
|
.vm-name {
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-card-specs {
|
.vm-id {
|
||||||
display: flex;
|
font-size: 12px;
|
||||||
flex-direction: row;
|
}
|
||||||
gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.vm-os {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-resources {
|
.mapping-cell {
|
||||||
font-size: 14px;
|
.mapping-target {
|
||||||
color: var(--muted);
|
margin-left: 4px;
|
||||||
display: inline-flex;
|
white-space: nowrap;
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mapping-cell + .mapping-cell {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.banner-title {
|
.banner-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1893,11 +1893,14 @@ harvester:
|
|||||||
providerSelect: Provider
|
providerSelect: Provider
|
||||||
createNew: Create new provider
|
createNew: Create new provider
|
||||||
requirementsTitle: Requirements
|
requirementsTitle: Requirements
|
||||||
requirementsText: "To migrate virtual machines from VMware, you will need a vCenter Server running version 6.5 or higher with a user account that has at least read permissions, and the source environment must be network-accessible from Rancher."
|
requirementsText: "To migrate virtual machines from VMware, you will need a vCenter Server running version 6.5 or higher with a user account that has at least read permissions, and the source environment must be network-accessible from Harvester Node."
|
||||||
name: Name
|
name: Name
|
||||||
urlLabel: vCenter/ESXi URL
|
urlLabel: vCenter/ESXi URL
|
||||||
urlPlaceholder: "https://vcenter.example.com/sdk"
|
urlPlaceholder: "https://vcenter.example.com/sdk"
|
||||||
urlHint: Enter the full URL including https://
|
urlHint: Enter the full URL including https://
|
||||||
|
vddkImageLabel: VMware VDDK Image
|
||||||
|
vddkImagePlaceholder: "[your-private-registry]/vddk-image:latest"
|
||||||
|
vddkImageTooltip: Specify the VMware VDDK (Virtual Disk Development Kit) Image to accelerate and optimize VMware virtual machine migration.
|
||||||
skipSsl: Skip SSL certificate verification
|
skipSsl: Skip SSL certificate verification
|
||||||
skipSslHint: Not recommended for production environments
|
skipSslHint: Not recommended for production environments
|
||||||
testConnection:
|
testConnection:
|
||||||
@@ -1965,7 +1968,7 @@ harvester:
|
|||||||
description: Map VMware networks and datastores to Harvester and Longhorn target resources
|
description: Map VMware networks and datastores to Harvester and Longhorn target resources
|
||||||
save: Save Mappings and Continue
|
save: Save Mappings and Continue
|
||||||
noTemplate: Start from scratch
|
noTemplate: Start from scratch
|
||||||
removeMap: Remove Map
|
removeMap: Remove mapping
|
||||||
networkMapping:
|
networkMapping:
|
||||||
title: Network Mapping
|
title: Network Mapping
|
||||||
description: Map VMware port groups to Harvester networks
|
description: Map VMware port groups to Harvester networks
|
||||||
@@ -1977,24 +1980,45 @@ harvester:
|
|||||||
storageMapping:
|
storageMapping:
|
||||||
title: Storage Mapping
|
title: Storage Mapping
|
||||||
description: Map VMware datastores to Harvester storage classes
|
description: Map VMware datastores to Harvester storage classes
|
||||||
placeholder: Choose a Longhorn Storage Volume
|
placeholder: Choose a Harvester Storage Class
|
||||||
template: Use existing storage mapping as template
|
template: Use existing storage mapping as template
|
||||||
reviewMigration:
|
reviewMigration:
|
||||||
title: Review Migration Plan
|
title: Review Migration Plan
|
||||||
description: Confirm your migration settings before starting the transfer of VMs to the target cluster.
|
description: Confirm your migration settings before starting the transfer of VMs to the target cluster.
|
||||||
planName: Name
|
planName: Name
|
||||||
planNamePlaceholder: "e.g. my-migration-plan"
|
planNamePlaceholder: "e.g. my-migration-plan"
|
||||||
|
planNameProviderConflict: 'The name “{name}” is already used by an existing provider. Please input a different plan name.'
|
||||||
|
planNamePlanConflict: 'The name “{name}” is already used by an existing migration plan. Please input a different plan name.'
|
||||||
migrationDetails: Migration Details
|
migrationDetails: Migration Details
|
||||||
|
migrationDetailsDescription: Name this plan and choose the namespace where migrated VMs will be created.
|
||||||
totalVms: Total VMs
|
totalVms: Total VMs
|
||||||
vcpu: vCPU
|
vcpu: vCPU
|
||||||
memory: Memory
|
memory: Memory
|
||||||
storage: Storage
|
storage: Storage
|
||||||
source: Source
|
source: Source
|
||||||
|
provider: Provider
|
||||||
targetNamespace: Namespace
|
targetNamespace: Namespace
|
||||||
targetNamespacePlaceholder: "Select target namespace"
|
targetNamespacePlaceholder: "Select target namespace"
|
||||||
migrationMode: Migration Mode
|
migrationMode: Migration Mode
|
||||||
coldMigration: Cold Migration
|
coldMigration: Cold Migration
|
||||||
|
coldMigrationDescription: All source VMs will be powered off during migration.
|
||||||
|
coldMigrationHint: Plan a maintenance window and notify relevant stakeholders before starting.
|
||||||
virtualMachines: Virtual Machines
|
virtualMachines: Virtual Machines
|
||||||
|
editSelection: Edit selection
|
||||||
|
noVmSelected: "No VMs selected. Return to "
|
||||||
|
noVmSelectedLink: "Step 2 to select virtual machines."
|
||||||
|
columns:
|
||||||
|
vmName: VM Name
|
||||||
|
os: Operating System
|
||||||
|
resources: Resources
|
||||||
|
powerState: Power State
|
||||||
|
networkMapping: Network Mapping
|
||||||
|
storageMapping: Storage Mapping
|
||||||
|
sourceTarget: "Source \u2192 Target"
|
||||||
|
actions:
|
||||||
|
menuLabel: Actions
|
||||||
|
editMappings: Edit Mappings
|
||||||
|
removeFromPlan: Remove from Plan
|
||||||
warningTitle: "Cold Migration Warning"
|
warningTitle: "Cold Migration Warning"
|
||||||
warningMessage: "All source VMs will be powered off, and downtime will occur during migration. Plan your maintenance window accordingly and notify relevant stakeholders"
|
warningMessage: "All source VMs will be powered off, and downtime will occur during migration. Plan your maintenance window accordingly and notify relevant stakeholders"
|
||||||
startMigration: Start Migration Plan
|
startMigration: Start Migration Plan
|
||||||
|
|||||||
@@ -179,8 +179,10 @@ const rows = computed(() => {
|
|||||||
|
|
||||||
overallProgress = Math.round(overallProgress * 10) / 10;
|
overallProgress = Math.round(overallProgress * 10) / 10;
|
||||||
|
|
||||||
// If no step-level error but the plan itself is failed, surface it
|
// If no step-level error but the plan itself is failed, surface it.
|
||||||
if (!errorMsg && plan.planFailed) {
|
// Skip VMs that already reached 100% (they finished successfully before
|
||||||
|
// the plan failed) so their bar stays green instead of turning red.
|
||||||
|
if (!errorMsg && plan.planFailed && overallProgress < 100) {
|
||||||
errorMsg = `${ currentStep || t('harvester.addons.vmMigration.dashboard.progress.migration') }: ${ t('harvester.addons.vmMigration.dashboard.progress.failed') }`;
|
errorMsg = `${ currentStep || t('harvester.addons.vmMigration.dashboard.progress.migration') }: ${ t('harvester.addons.vmMigration.dashboard.progress.failed') }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +192,11 @@ const rows = computed(() => {
|
|||||||
const cluster = routeParams.cluster || store.getters['clusterId'];
|
const cluster = routeParams.cluster || store.getters['clusterId'];
|
||||||
const vmNameCandidates = [vm.targetName, vm.name, vm.id].filter(Boolean);
|
const vmNameCandidates = [vm.targetName, vm.name, vm.id].filter(Boolean);
|
||||||
const targetVm = allVMs.value.find((item) => vmNameCandidates.includes(item.metadata?.name) && (!vmNamespace || item.metadata?.namespace === vmNamespace));
|
const targetVm = allVMs.value.find((item) => vmNameCandidates.includes(item.metadata?.name) && (!vmNamespace || item.metadata?.namespace === vmNamespace));
|
||||||
const canNavigateToVm = overallProgress >= 100 && !errorMsg && !plan.planFailed && !plan.planCanceled && !!targetVm && !!product && !!cluster;
|
// A VM that individually reached 100% without error should be navigable
|
||||||
|
// even when the overall plan failed (other VMs in the plan may have
|
||||||
|
// failed); `overallProgress >= 100 && !errorMsg && targetVm` already
|
||||||
|
// guarantees this VM migrated successfully.
|
||||||
|
const canNavigateToVm = overallProgress >= 100 && !errorMsg && !plan.planCanceled && !!targetVm && !!product && !!cluster;
|
||||||
const vmDetailLocation = canNavigateToVm ? {
|
const vmDetailLocation = canNavigateToVm ? {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource-namespace-id`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource-namespace-id`,
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const stepData = reactive({
|
|||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
skipTlsVerify: false,
|
skipTlsVerify: false,
|
||||||
|
vddkInitImage: '',
|
||||||
testPassed: false,
|
testPassed: false,
|
||||||
testResult: null,
|
testResult: null,
|
||||||
testError: null,
|
testError: null,
|
||||||
@@ -193,6 +194,7 @@ const init = async() => {
|
|||||||
stepData.provider.selectedProvider = fetchedProvider.metadata.name;
|
stepData.provider.selectedProvider = fetchedProvider.metadata.name;
|
||||||
stepData.provider.providerName = fetchedProvider.metadata.name;
|
stepData.provider.providerName = fetchedProvider.metadata.name;
|
||||||
stepData.provider.url = fetchedProvider.spec?.url || '';
|
stepData.provider.url = fetchedProvider.spec?.url || '';
|
||||||
|
stepData.provider.vddkInitImage = fetchedProvider.spec?.settings?.vddkInitImage || '';
|
||||||
stepData.provider.createdProvider = fetchedProvider;
|
stepData.provider.createdProvider = fetchedProvider;
|
||||||
|
|
||||||
const secretRef = fetchedProvider.spec?.secret;
|
const secretRef = fetchedProvider.spec?.secret;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const stepData = reactive({
|
|||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
skipTlsVerify: false,
|
skipTlsVerify: false,
|
||||||
|
vddkInitImage: '',
|
||||||
testPassed: false,
|
testPassed: false,
|
||||||
testResult: null,
|
testResult: null,
|
||||||
testError: null,
|
testError: null,
|
||||||
@@ -173,6 +174,24 @@ const onReviewReady = (ready) => {
|
|||||||
reviewReady.value = ready;
|
reviewReady.value = ready;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onEditSelection = () => {
|
||||||
|
wizardComponent.value?.goToStep(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditMappings = () => {
|
||||||
|
wizardComponent.value?.goToStep(3);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveVm = (id) => {
|
||||||
|
selectedVMs.value = selectedVMs.value.filter((vm) => vm.id !== id);
|
||||||
|
stepData.vms.selectedVMIds.delete(id);
|
||||||
|
|
||||||
|
vmsReady.value = selectedVMs.value.length > 0;
|
||||||
|
if (!vmsReady.value) {
|
||||||
|
reviewReady.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Clear downstream data when provider changes
|
// Clear downstream data when provider changes
|
||||||
watch(() => stepData.provider.providerName, (newVal, oldVal) => {
|
watch(() => stepData.provider.providerName, (newVal, oldVal) => {
|
||||||
if (oldVal && newVal !== oldVal) {
|
if (oldVal && newVal !== oldVal) {
|
||||||
@@ -191,13 +210,13 @@ watch(() => stepData.provider.providerName, (newVal, oldVal) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear mappings and review when VM selection changes
|
// Clear mappings and review when VMs are added to the selection. Pure removals
|
||||||
|
// (e.g. "Remove from Plan" on the review step) keep the existing mappings and plan name.
|
||||||
watch(selectedVMs, (newVal, oldVal) => {
|
watch(selectedVMs, (newVal, oldVal) => {
|
||||||
const newIds = new Set(newVal.map((v) => v.id));
|
|
||||||
const oldIds = new Set(oldVal.map((v) => v.id));
|
const oldIds = new Set(oldVal.map((v) => v.id));
|
||||||
const changed = newIds.size !== oldIds.size || [...newIds].some((id) => !oldIds.has(id));
|
const added = newVal.some((v) => !oldIds.has(v.id));
|
||||||
|
|
||||||
if (oldVal.length > 0 && changed) {
|
if (oldVal.length > 0 && added) {
|
||||||
stepData.mappings.networkEntries = [];
|
stepData.mappings.networkEntries = [];
|
||||||
stepData.mappings.storageEntries = [];
|
stepData.mappings.storageEntries = [];
|
||||||
mappingsReady.value = false;
|
mappingsReady.value = false;
|
||||||
@@ -290,6 +309,9 @@ const onCancel = () => {
|
|||||||
:mapping-entries="stepData.mappings"
|
:mapping-entries="stepData.mappings"
|
||||||
:step-data="stepData.review"
|
:step-data="stepData.review"
|
||||||
@ready="onReviewReady"
|
@ready="onReviewReady"
|
||||||
|
@edit-selection="onEditSelection"
|
||||||
|
@edit-mappings="onEditMappings"
|
||||||
|
@remove-vm="onRemoveVm"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</CruResource>
|
</CruResource>
|
||||||
|
|||||||
@@ -68,40 +68,28 @@ export default defineComponent({
|
|||||||
return (this.value || []).map((plan) => {
|
return (this.value || []).map((plan) => {
|
||||||
const planName = plan?.metadata?.name || '-';
|
const planName = plan?.metadata?.name || '-';
|
||||||
const namespace = plan?.metadata?.namespace || '';
|
const namespace = plan?.metadata?.namespace || '';
|
||||||
const networkMap = plan?.spec?.map?.network;
|
|
||||||
const storageMap = plan?.spec?.map?.storage;
|
|
||||||
const history = plan?.status?.migration?.history || [];
|
|
||||||
|
|
||||||
const migrationsMap = new Map();
|
const migrations = existing[HCI.FORKLIFT_MIGRATION]
|
||||||
|
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||||
|
.map((resource) => ({
|
||||||
|
name: resource?.metadata?.name,
|
||||||
|
namespace: resource?.metadata?.namespace || namespace,
|
||||||
|
}));
|
||||||
|
|
||||||
history
|
const matchedNetworkMap = existing[HCI.FORKLIFT_NETWORK_MAP].find((resource) => this.ownedByPlan(resource, plan));
|
||||||
.map((entry) => ({
|
const matchedStorageMap = existing[HCI.FORKLIFT_STORAGE_MAP].find((resource) => this.ownedByPlan(resource, plan));
|
||||||
name: entry?.migration?.name,
|
|
||||||
namespace: entry?.migration?.namespace || namespace,
|
|
||||||
}))
|
|
||||||
.filter((entry) => !!entry.name)
|
|
||||||
.forEach((entry) => {
|
|
||||||
const matched = existing[HCI.FORKLIFT_MIGRATION].find((resource) => resource?.metadata?.name === entry.name && resource?.metadata?.namespace === entry.namespace);
|
|
||||||
|
|
||||||
if (matched) {
|
|
||||||
migrationsMap.set(`${ entry.namespace }/${ entry.name }`, entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const matchedNetworkMap = networkMap?.name && existing[HCI.FORKLIFT_NETWORK_MAP].find((resource) => resource?.metadata?.name === networkMap.name && resource?.metadata?.namespace === (networkMap.namespace || namespace));
|
|
||||||
const matchedStorageMap = storageMap?.name && existing[HCI.FORKLIFT_STORAGE_MAP].find((resource) => resource?.metadata?.name === storageMap.name && resource?.metadata?.namespace === (storageMap.namespace || namespace));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
planName,
|
planName,
|
||||||
namespace,
|
namespace,
|
||||||
migrations: [...migrationsMap.values()],
|
migrations,
|
||||||
networkMap: matchedNetworkMap ? {
|
networkMap: matchedNetworkMap ? {
|
||||||
namespace: networkMap.namespace || namespace,
|
namespace: matchedNetworkMap?.metadata?.namespace || namespace,
|
||||||
name: networkMap.name,
|
name: matchedNetworkMap?.metadata?.name,
|
||||||
} : null,
|
} : null,
|
||||||
storageMap: matchedStorageMap ? {
|
storageMap: matchedStorageMap ? {
|
||||||
namespace: storageMap.namespace || namespace,
|
namespace: matchedStorageMap?.metadata?.namespace || namespace,
|
||||||
name: storageMap.name,
|
name: matchedStorageMap?.metadata?.name,
|
||||||
} : null,
|
} : null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -125,53 +113,57 @@ export default defineComponent({
|
|||||||
methods: {
|
methods: {
|
||||||
resourceNames,
|
resourceNames,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether a related resource (migration / network map / storage map)
|
||||||
|
* is owned by the given plan by inspecting its `metadata.ownerReferences`.
|
||||||
|
* Matches on `kind: Plan` and the plan name; when both sides expose a uid it
|
||||||
|
* must match too, to disambiguate same-named plans.
|
||||||
|
*/
|
||||||
|
ownedByPlan(resource, plan) {
|
||||||
|
const planName = plan?.metadata?.name;
|
||||||
|
const planUid = plan?.metadata?.uid;
|
||||||
|
const owners = resource?.metadata?.ownerReferences || [];
|
||||||
|
|
||||||
|
if (!planName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return owners.some((owner) => owner?.kind === 'Plan' &&
|
||||||
|
owner?.name === planName &&
|
||||||
|
(!planUid || !owner?.uid || owner.uid === planUid));
|
||||||
|
},
|
||||||
|
|
||||||
buildDeleteTargets() {
|
buildDeleteTargets() {
|
||||||
|
const existing = this.existingRelatedResources;
|
||||||
const targets = new Map();
|
const targets = new Map();
|
||||||
|
|
||||||
|
const addTarget = (type, resource) => {
|
||||||
|
const name = resource?.metadata?.name;
|
||||||
|
const ns = resource?.metadata?.namespace;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `${ type }|${ ns }|${ name }`;
|
||||||
|
|
||||||
|
targets.set(key, {
|
||||||
|
type, name, namespace: ns
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
for (const plan of this.value || []) {
|
for (const plan of this.value || []) {
|
||||||
const namespace = plan?.metadata?.namespace || '';
|
existing[HCI.FORKLIFT_NETWORK_MAP]
|
||||||
const networkMap = plan?.spec?.map?.network;
|
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||||
const storageMap = plan?.spec?.map?.storage;
|
.forEach((resource) => addTarget(HCI.FORKLIFT_NETWORK_MAP, resource));
|
||||||
const history = plan?.status?.migration?.history || [];
|
|
||||||
|
|
||||||
if (networkMap?.name) {
|
existing[HCI.FORKLIFT_STORAGE_MAP]
|
||||||
const ns = networkMap.namespace || namespace;
|
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||||
const key = `${ HCI.FORKLIFT_NETWORK_MAP }|${ ns }|${ networkMap.name }`;
|
.forEach((resource) => addTarget(HCI.FORKLIFT_STORAGE_MAP, resource));
|
||||||
|
|
||||||
targets.set(key, {
|
existing[HCI.FORKLIFT_MIGRATION]
|
||||||
type: HCI.FORKLIFT_NETWORK_MAP,
|
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||||
name: networkMap.name,
|
.forEach((resource) => addTarget(HCI.FORKLIFT_MIGRATION, resource));
|
||||||
namespace: ns,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (storageMap?.name) {
|
|
||||||
const ns = storageMap.namespace || namespace;
|
|
||||||
const key = `${ HCI.FORKLIFT_STORAGE_MAP }|${ ns }|${ storageMap.name }`;
|
|
||||||
|
|
||||||
targets.set(key, {
|
|
||||||
type: HCI.FORKLIFT_STORAGE_MAP,
|
|
||||||
name: storageMap.name,
|
|
||||||
namespace: ns,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of history) {
|
|
||||||
const migrationName = entry?.migration?.name;
|
|
||||||
|
|
||||||
if (!migrationName) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const migrationNs = entry?.migration?.namespace || namespace;
|
|
||||||
const key = `${ HCI.FORKLIFT_MIGRATION }|${ migrationNs }|${ migrationName }`;
|
|
||||||
|
|
||||||
targets.set(key, {
|
|
||||||
type: HCI.FORKLIFT_MIGRATION,
|
|
||||||
name: migrationName,
|
|
||||||
namespace: migrationNs,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...targets.values()];
|
return [...targets.values()];
|
||||||
|
|||||||
Reference in New Issue
Block a user