mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-08-16 20:59:16 +00:00
feat(forklift): small fixes after Copilot
Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
parent
5c7dd1953d
commit
2deb0aa254
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@ -5,5 +5,14 @@
|
||||
".eslintcache": true,
|
||||
".nyc_output": true,
|
||||
"yarn-error.log": true
|
||||
}
|
||||
},
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"typescript",
|
||||
"vue"
|
||||
]
|
||||
}
|
||||
|
||||
@ -159,9 +159,17 @@ watch(selectedProvider, (val) => {
|
||||
);
|
||||
|
||||
if (secret?.data) {
|
||||
username.value = atob(secret.data.user || '');
|
||||
password.value = atob(secret.data.password || '');
|
||||
skipTlsVerify.value = atob(secret.data.insecureSkipVerify || '') === 'true';
|
||||
const decode = (val) => {
|
||||
try {
|
||||
return atob(val || '');
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
username.value = decode(secret.data.user);
|
||||
password.value = decode(secret.data.password);
|
||||
skipTlsVerify.value = decode(secret.data.insecureSkipVerify) === 'true';
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,6 +203,74 @@ watch(testing, (val) => {
|
||||
emit('testing', val);
|
||||
}, { immediate: true });
|
||||
|
||||
const pollProviderReady = async(name) => {
|
||||
const inStore = store.getters['currentProduct'].inStore;
|
||||
const namespace = FORKLIFT_NAMESPACE;
|
||||
const maxAttempts = 15;
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
attempts++;
|
||||
|
||||
const refreshed = await store.dispatch(`${ inStore }/find`, {
|
||||
type: HCI.FORKLIFT_PROVIDER,
|
||||
id: `${ namespace }/${ name }`,
|
||||
opt: { force: true }
|
||||
});
|
||||
|
||||
const conditions = refreshed?.status?.conditions || [];
|
||||
const connectionCondition = conditions.find((c) => c.type === 'ConnectionTestSucceeded');
|
||||
const readyCondition = conditions.find((c) => c.type === 'Ready');
|
||||
|
||||
if (connectionCondition) {
|
||||
if (connectionCondition.status === 'True') {
|
||||
return { connected: true };
|
||||
}
|
||||
|
||||
return { connected: false, errorMsg: connectionCondition.message || t('harvester.addons.vmMigration.errors.connectionFailed') };
|
||||
}
|
||||
|
||||
if (readyCondition) {
|
||||
if (readyCondition.status === 'True') {
|
||||
return { connected: true };
|
||||
} else if (readyCondition.status === 'False') {
|
||||
return { connected: false, errorMsg: readyCondition.message || t('harvester.addons.vmMigration.errors.providerNotReady') };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { connected: false, errorMsg: '' };
|
||||
};
|
||||
|
||||
const handlePollResult = ({ connected, errorMsg }, buttonCb) => {
|
||||
if (connected) {
|
||||
testPassed.value = true;
|
||||
testResult.value = t('harvester.addons.vmMigration.configureProvider.testSuccess');
|
||||
testing.value = false;
|
||||
buttonCb(true);
|
||||
} else {
|
||||
testError.value = errorMsg || t('harvester.addons.vmMigration.configureProvider.testTimeout');
|
||||
testing.value = false;
|
||||
buttonCb(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupCreatedResources = async() => {
|
||||
if (createdProvider.value) {
|
||||
try {
|
||||
await createdProvider.value.remove();
|
||||
} catch (e) {}
|
||||
createdProvider.value = null;
|
||||
createdSecret.value = null;
|
||||
} else if (createdSecret.value) {
|
||||
try {
|
||||
await createdSecret.value.remove();
|
||||
} catch (e) {}
|
||||
createdSecret.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const testConnection = async(buttonCb) => {
|
||||
testResult.value = null;
|
||||
testError.value = null;
|
||||
@ -213,8 +289,6 @@ const testConnection = async(buttonCb) => {
|
||||
// Edit mode: update existing provider URL + secret, then poll
|
||||
if (props.editMode && createdProvider.value) {
|
||||
try {
|
||||
const namespace = FORKLIFT_NAMESPACE;
|
||||
|
||||
createdProvider.value.spec.url = url.value;
|
||||
await createdProvider.value.save();
|
||||
|
||||
@ -230,56 +304,7 @@ const testConnection = async(buttonCb) => {
|
||||
await createdSecret.value.save();
|
||||
}
|
||||
|
||||
const maxAttempts = 15;
|
||||
let attempts = 0;
|
||||
let connected = false;
|
||||
let errorMsg = '';
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
attempts++;
|
||||
|
||||
const refreshed = await store.dispatch(`${ inStore }/find`, {
|
||||
type: HCI.FORKLIFT_PROVIDER,
|
||||
id: `${ namespace }/${ providerName.value }`,
|
||||
opt: { force: true }
|
||||
});
|
||||
|
||||
const conditions = refreshed?.status?.conditions || [];
|
||||
const readyCondition = conditions.find((c) => c.type === 'Ready');
|
||||
const connectionCondition = conditions.find((c) => c.type === 'ConnectionTestSucceeded');
|
||||
|
||||
if (connectionCondition) {
|
||||
if (connectionCondition.status === 'True') {
|
||||
connected = true;
|
||||
break;
|
||||
} else {
|
||||
errorMsg = connectionCondition.message || t('harvester.addons.vmMigration.errors.connectionFailed');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (readyCondition) {
|
||||
if (readyCondition.status === 'True') {
|
||||
connected = true;
|
||||
break;
|
||||
} else if (readyCondition.status === 'False') {
|
||||
errorMsg = readyCondition.message || t('harvester.addons.vmMigration.errors.providerNotReady');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (connected) {
|
||||
testPassed.value = true;
|
||||
testResult.value = t('harvester.addons.vmMigration.configureProvider.testSuccess');
|
||||
testing.value = false;
|
||||
buttonCb(true);
|
||||
} else {
|
||||
testError.value = errorMsg || t('harvester.addons.vmMigration.configureProvider.testTimeout');
|
||||
testing.value = false;
|
||||
buttonCb(false);
|
||||
}
|
||||
handlePollResult(await pollProviderReady(providerName.value), buttonCb);
|
||||
} catch (err) {
|
||||
testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed');
|
||||
testing.value = false;
|
||||
@ -292,57 +317,7 @@ const testConnection = async(buttonCb) => {
|
||||
// For existing providers, just poll for Ready/ConnectionTestSucceeded status
|
||||
if (isExistingProvider.value) {
|
||||
try {
|
||||
const namespace = FORKLIFT_NAMESPACE;
|
||||
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 || t('harvester.addons.vmMigration.errors.connectionFailed');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (readyCondition) {
|
||||
if (readyCondition.status === 'True') {
|
||||
connected = true;
|
||||
break;
|
||||
} else if (readyCondition.status === 'False') {
|
||||
errorMsg = readyCondition.message || t('harvester.addons.vmMigration.errors.providerNotReady');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (connected) {
|
||||
testPassed.value = true;
|
||||
testResult.value = t('harvester.addons.vmMigration.configureProvider.testSuccess');
|
||||
testing.value = false;
|
||||
buttonCb(true);
|
||||
} else {
|
||||
testError.value = errorMsg || t('harvester.addons.vmMigration.configureProvider.testTimeout');
|
||||
testing.value = false;
|
||||
buttonCb(false);
|
||||
}
|
||||
handlePollResult(await pollProviderReady(providerName.value), buttonCb);
|
||||
} catch (err) {
|
||||
testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed');
|
||||
testing.value = false;
|
||||
@ -354,20 +329,11 @@ const testConnection = async(buttonCb) => {
|
||||
|
||||
// For new providers, create provider + secret then poll
|
||||
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;
|
||||
}
|
||||
await cleanupCreatedResources();
|
||||
|
||||
const namespace = FORKLIFT_NAMESPACE;
|
||||
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: {
|
||||
@ -387,7 +353,6 @@ const testConnection = async(buttonCb) => {
|
||||
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: {
|
||||
@ -417,79 +382,15 @@ const testConnection = async(buttonCb) => {
|
||||
await newSecret.save();
|
||||
createdSecret.value = newSecret;
|
||||
|
||||
const maxAttempts = 15;
|
||||
let attempts = 0;
|
||||
let connected = false;
|
||||
let errorMsg = '';
|
||||
const result = await pollProviderReady(providerName.value);
|
||||
|
||||
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 || t('harvester.addons.vmMigration.errors.connectionFailed');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (readyCondition) {
|
||||
if (readyCondition.status === 'True') {
|
||||
connected = true;
|
||||
break;
|
||||
} else if (readyCondition.status === 'False') {
|
||||
errorMsg = readyCondition.message || t('harvester.addons.vmMigration.errors.providerNotReady');
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!result.connected) {
|
||||
await cleanupCreatedResources();
|
||||
}
|
||||
|
||||
if (connected) {
|
||||
testPassed.value = true;
|
||||
testResult.value = t('harvester.addons.vmMigration.configureProvider.testSuccess');
|
||||
testing.value = false;
|
||||
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;
|
||||
}
|
||||
|
||||
testError.value = errorMsg || t('harvester.addons.vmMigration.configureProvider.testTimeout');
|
||||
testing.value = false;
|
||||
buttonCb(false);
|
||||
}
|
||||
handlePollResult(result, buttonCb);
|
||||
} 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;
|
||||
}
|
||||
|
||||
await cleanupCreatedResources();
|
||||
testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed');
|
||||
testing.value = false;
|
||||
buttonCb(false);
|
||||
|
||||
@ -15,8 +15,6 @@ const props = defineProps({
|
||||
providerName: { type: String, default: '' },
|
||||
provider: { type: Object, default: null },
|
||||
selectedVms: { type: Array, default: () => [] },
|
||||
networkMapName: { type: String, default: '' },
|
||||
storageMapName: { type: String, default: '' },
|
||||
mappingEntries: { type: Object, default: null },
|
||||
stepData: { type: Object, required: true },
|
||||
});
|
||||
|
||||
@ -406,8 +406,7 @@ init();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="refreshing" />
|
||||
<Loading v-if="loading" />
|
||||
<Loading v-if="loading || refreshing" />
|
||||
<div
|
||||
v-else
|
||||
class="select-vms-step"
|
||||
|
||||
@ -1821,6 +1821,7 @@ harvester:
|
||||
failedLoadVms: Failed to load virtual machines
|
||||
failedRefreshVms: Failed to refresh virtual machines
|
||||
failedLoadProviders: Failed to load providers
|
||||
failedLoadProvider: "Failed to load provider: {error}"
|
||||
failedResolveDetails: Failed to resolve network and datastore details
|
||||
failedDecodeCredentials: Failed to decode provider credentials
|
||||
connectionFailed: Connection failed
|
||||
@ -1838,13 +1839,13 @@ harvester:
|
||||
wizard:
|
||||
title: VM Migration
|
||||
steps:
|
||||
configureProvider:
|
||||
configureProvider:
|
||||
label: Provider
|
||||
description: Configure Provider
|
||||
selectVms:
|
||||
selectVms:
|
||||
label: VMs
|
||||
description: Select VMs
|
||||
configureMappings:
|
||||
configureMappings:
|
||||
label: Mappings
|
||||
description: Define Mappings
|
||||
reviewMigration:
|
||||
@ -1867,7 +1868,7 @@ harvester:
|
||||
urlHint: Enter the full URL including https://
|
||||
skipSsl: Skip SSL certificate verification
|
||||
skipSslHint: Not recommended for production environments
|
||||
testConnection:
|
||||
testConnection:
|
||||
action: Test Connection
|
||||
success: Connection successful
|
||||
error: Connection failed
|
||||
@ -1921,6 +1922,10 @@ harvester:
|
||||
failed: Failed
|
||||
migration: Migration
|
||||
initializingMigration: Initializing migration
|
||||
finishedSuccessfully: Finished Successfully
|
||||
step: "Step {index}"
|
||||
vmCount: "{count} VMs"
|
||||
vmId: "id: {id}"
|
||||
configureMappings:
|
||||
title: Set Mappings
|
||||
description: Map VMware networks and datastores to Harvester and Longhorn target resources
|
||||
|
||||
@ -69,17 +69,17 @@ const rows = computed(() => {
|
||||
overallProgress += (stepPct / 100) * stepWeight;
|
||||
|
||||
if (!currentStep) {
|
||||
currentStep = step.name || `Step ${ idx + 1 }`;
|
||||
currentStep = step.name || t('harvester.addons.vmMigration.dashboard.progress.step', { index: idx + 1 });
|
||||
}
|
||||
|
||||
if (step.error && !errorMsg) {
|
||||
const reasons = (step.error.reasons || []).join('; ') || t('harvester.addons.vmMigration.plan.states.error');
|
||||
|
||||
errorMsg = `${ step.name || `Step ${ idx + 1 }` }: ${ reasons }`;
|
||||
errorMsg = `${ step.name || t('harvester.addons.vmMigration.dashboard.progress.step', { index: idx + 1 }) }: ${ reasons }`;
|
||||
}
|
||||
|
||||
if (step.phase === 'Failed' && !errorMsg) {
|
||||
errorMsg = `${ step.name || `Step ${ idx + 1 }` }: ${ t('harvester.addons.vmMigration.dashboard.progress.failed') }`;
|
||||
errorMsg = `${ step.name || t('harvester.addons.vmMigration.dashboard.progress.step', { index: idx + 1 }) }: ${ t('harvester.addons.vmMigration.dashboard.progress.failed') }`;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -215,7 +215,7 @@ init();
|
||||
</div>
|
||||
</template>
|
||||
<template #cell:vmCount="{ row }">
|
||||
{{ (row.spec.vms || []).length }} VMs
|
||||
{{ t('harvester.addons.vmMigration.dashboard.progress.vmCount', { count: (row.spec.vms || []).length }) }}
|
||||
</template>
|
||||
<template #cell:progress="{ row }">
|
||||
<div
|
||||
@ -230,7 +230,7 @@ init();
|
||||
<div class="vm-progress-header">
|
||||
<div class="vm-name-block">
|
||||
<span class="vm-name">{{ vm.vmName }}</span>
|
||||
<span class="text-muted vm-id">id: {{ vm.vmId }}</span>
|
||||
<span class="text-muted vm-id">{{ t('harvester.addons.vmMigration.dashboard.progress.vmId', { id: vm.vmId }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vm-pct-block">
|
||||
@ -246,7 +246,7 @@ init();
|
||||
v-if="vm.progress >= 100"
|
||||
class="step-label text-muted"
|
||||
>
|
||||
Finished Successfully
|
||||
{{ t('harvester.addons.vmMigration.dashboard.progress.finishedSuccessfully') }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="vm.errorMsg"
|
||||
@ -258,7 +258,7 @@ init();
|
||||
v-else-if="vm.canceled"
|
||||
class="step-label text-muted"
|
||||
>
|
||||
Canceled
|
||||
{{ t('harvester.addons.vmMigration.plan.states.canceled') }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="vm.currentStep"
|
||||
|
||||
@ -203,13 +203,17 @@ const init = async() => {
|
||||
);
|
||||
|
||||
if (secret?.data) {
|
||||
try {
|
||||
stepData.provider.username = atob(secret.data.user || '');
|
||||
stepData.provider.password = atob(secret.data.password || '');
|
||||
stepData.provider.skipTlsVerify = atob(secret.data.insecureSkipVerify || '') === 'true';
|
||||
} catch (e) {
|
||||
errors.value = [t('harvester.addons.vmMigration.errors.failedDecodeCredentials')];
|
||||
}
|
||||
const decode = (val) => {
|
||||
try {
|
||||
return atob(val || '');
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
stepData.provider.username = decode(secret.data.user);
|
||||
stepData.provider.password = decode(secret.data.password);
|
||||
stepData.provider.skipTlsVerify = decode(secret.data.insecureSkipVerify) === 'true';
|
||||
stepData.provider.createdSecret = secret;
|
||||
}
|
||||
}
|
||||
@ -230,7 +234,7 @@ const init = async() => {
|
||||
// Maps may not exist yet
|
||||
}
|
||||
} catch (err) {
|
||||
errors.value = [`Failed to load provider: ${ err.message || err }`];
|
||||
errors.value = [t('harvester.addons.vmMigration.errors.failedLoadProvider', { error: err.message || err })];
|
||||
}
|
||||
|
||||
initialLoading.value = false;
|
||||
|
||||
@ -15,14 +15,11 @@ const { t } = useI18n(store);
|
||||
|
||||
const cruRef = ref(null);
|
||||
const providerStepRef = ref(null);
|
||||
const mappingsStepRef = ref(null);
|
||||
const reviewStepRef = ref(null);
|
||||
|
||||
const providerName = ref('');
|
||||
const provider = ref(null);
|
||||
const selectedVMs = ref([]);
|
||||
const networkMapName = ref('');
|
||||
const storageMapName = ref('');
|
||||
const errors = ref([]);
|
||||
|
||||
const providerReady = ref(false);
|
||||
@ -263,7 +260,6 @@ const onCancel = () => {
|
||||
</template>
|
||||
<template #configure-mappings>
|
||||
<ConfigureMappingsStep
|
||||
ref="mappingsStepRef"
|
||||
:provider-name="providerName"
|
||||
:provider="provider"
|
||||
:selected-vms="selectedVMs"
|
||||
@ -277,8 +273,6 @@ const onCancel = () => {
|
||||
:provider-name="providerName"
|
||||
:provider="provider"
|
||||
:selected-vms="selectedVMs"
|
||||
:network-map-name="networkMapName"
|
||||
:storage-map-name="storageMapName"
|
||||
:mapping-entries="stepData.mappings"
|
||||
:step-data="stepData.review"
|
||||
@ready="onReviewReady"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user