mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-11-16 05:46:14 +08:00
* chore(deps): bump @rancher/shell to 3.0.14 3.0.14 resolves [[name]] tokens in translations from values registered as l10n globals, which the standalone UI needs to pick the product name. Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * fix: save cloud-config template edits with @rancher/shell 3.0.14 The YamlEditor of shell 3.0.14 no longer emits onChanges, so editing a template saved its old cloud config. Copy the text from update:value, which both shell versions emit. Jest transforms @replit/codemirror-vim-core, an ESM-only dependency of the new YamlEditor, like the dashboard does. Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * fix: keep the YAML editor heights with CodeMirror 6 The min-height rules targeted the CodeMirror 5 element, so the editors shrank to their text. Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * fix: reject IP ranges the network settings don't support The CIDR check of @rancher/shell 3.0.14 also accepts short forms such as 10.1/16, extra parts and IPv6, so the VM migration, RWX and storage network settings saved them. Use a strict IPv4 CIDR check of our own. Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * fix: keep the look of the file selector buttons The FileSelector of @rancher/shell 3.0.14 no longer takes button classes and renders a medium secondary button. Pass the variant and size that match the previous primary and secondary buttons. Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * fix: keep Enter in a migration wizard field on the same step The Wizard of @rancher/shell 3.0.14 renders its step headers as submit buttons, so Enter in a text field clicked the first one and jumped back to step 1. Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * fix: show the created message after a restore, upgrade or upload Since @rancher/shell 3.0.14, save() no longer shows the message for resources with a generated name; callers show it with notifyGeneratedName(). Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * refactor: fold the logging output buffer with the CodeMirror 6 helpers The YAML editor of @rancher/shell 3.0.14 is CodeMirror 6, and its CodeMirror 5 methods are deprecated. Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * fix: keep the shell's table views off in standalone @rancher/shell 3.0.14 turns its table views on for every list in the standalone UI, as the server has no ui-configurable-tables feature and the default is on. The table views drop the filters the lists put in the table header, which also left the Images list empty. Add the feature, disabled, to the management store in standalone only. Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> * fix: reject zero-padded octets in IP ranges Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> --------- Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
292 lines
9.0 KiB
Vue
292 lines
9.0 KiB
Vue
<script setup>
|
|
import { reactive, ref, computed, watch } from 'vue';
|
|
import { useStore } from 'vuex';
|
|
import CruResource from '@shell/components/CruResource';
|
|
import Loading from '@shell/components/Loading';
|
|
import { SECRET } from '@shell/config/types';
|
|
import { useI18n } from '@shell/composables/useI18n';
|
|
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
|
|
import ConfigureProviderStep from '@pkg/harvester/components/vm-migration/ConfigureProviderStep.vue';
|
|
import ConfigureMappingsStep from '@pkg/harvester/components/vm-migration/ConfigureMappingsStep.vue';
|
|
import { PRODUCT_NAME } from '@pkg/harvester/config/harvester';
|
|
import { currentRouter, currentRoute } from '@pkg/harvester/utils/router';
|
|
import { decodeSecretValue } from '@pkg/harvester/utils/forklift';
|
|
import { HCI } from '@pkg/harvester/types';
|
|
import { preventImplicitSubmit } from '@pkg/harvester/utils/wizard';
|
|
|
|
const store = useStore();
|
|
const route = currentRoute();
|
|
const { t } = useI18n(store);
|
|
|
|
const cruRef = ref(null);
|
|
const providerStepRef = ref(null);
|
|
const mappingsStepRef = ref(null);
|
|
|
|
const providerName = ref('');
|
|
const provider = ref(null);
|
|
const errors = ref([]);
|
|
|
|
const providerReady = ref(false);
|
|
const providerFormValid = ref(false);
|
|
const providerTesting = ref(false);
|
|
const mappingsReady = ref(false);
|
|
|
|
const existingNetworkMap = ref(null);
|
|
const existingStorageMap = ref(null);
|
|
const initialLoading = ref(true);
|
|
|
|
const dummyResource = ref({ save: () => Promise.resolve() });
|
|
|
|
const providerId = route.query?.providerId || null;
|
|
const isEditMode = !!providerId;
|
|
|
|
const stepData = reactive({
|
|
provider: {
|
|
selectedProvider: isEditMode ? '' : '__create_new__',
|
|
providerName: '',
|
|
url: '',
|
|
username: '',
|
|
password: '',
|
|
skipTlsVerify: false,
|
|
vddkInitImage: '',
|
|
testPassed: false,
|
|
testResult: null,
|
|
testError: null,
|
|
createdProvider: null,
|
|
createdSecret: null,
|
|
},
|
|
mappings: {
|
|
networkEntries: [],
|
|
storageEntries: [],
|
|
},
|
|
});
|
|
|
|
const steps = reactive([
|
|
{
|
|
name: 'configure-provider',
|
|
label: t('harvester.addons.vmMigration.wizard.steps.configureProvider.label'),
|
|
subtext: t('harvester.addons.vmMigration.wizard.steps.configureProvider.description'),
|
|
ready: false,
|
|
},
|
|
{
|
|
name: 'configure-mappings',
|
|
label: t('harvester.addons.vmMigration.wizard.steps.configureMappings.label'),
|
|
subtext: t('harvester.addons.vmMigration.wizard.steps.configureMappings.description'),
|
|
ready: false,
|
|
},
|
|
]);
|
|
|
|
watch([providerFormValid, providerTesting], () => {
|
|
steps[0].ready = providerFormValid.value && !providerTesting.value;
|
|
}, { immediate: true });
|
|
watch(mappingsReady, (val) => {
|
|
steps[1].ready = val;
|
|
});
|
|
|
|
// CruResource exposes its inner Wizard via $refs; we reach into it to drive the
|
|
// "test connection before advancing" handshake, since the provider must be verified
|
|
// before the user can leave the first step.
|
|
const wizardComponent = computed(() => cruRef.value?.$refs?.Wizard);
|
|
|
|
const pendingProceed = ref(false);
|
|
|
|
// Handshake to gate step 1 -> 2 on a successful connection test:
|
|
// 1. When the user tries to advance off the provider step before it's ready,
|
|
// snap back (goToStep is 1-based, so goToStep(1) == the provider step) and
|
|
// programmatically trigger the async "Test connection" button.
|
|
// 2. Once the test flips `providerReady`, mark the step ready and advance
|
|
// (goToStep(2)). If the test fails, the user simply stays on the provider step.
|
|
watch(
|
|
() => wizardComponent.value?.activeStepIndex,
|
|
(newIdx, oldIdx) => {
|
|
if (oldIdx === 0 && newIdx === 1 && !providerReady.value) {
|
|
wizardComponent.value.goToStep(1);
|
|
pendingProceed.value = true;
|
|
providerStepRef.value.clickTestButton();
|
|
}
|
|
}
|
|
);
|
|
|
|
watch(providerReady, (val) => {
|
|
if (val && pendingProceed.value) {
|
|
pendingProceed.value = false;
|
|
steps[0].ready = true;
|
|
wizardComponent.value.goToStep(2);
|
|
}
|
|
});
|
|
|
|
const wizardTitle = computed(() => {
|
|
return isEditMode ? t('harvester.addons.vmMigration.providerWizard.editTitle') : t('harvester.addons.vmMigration.providerWizard.title');
|
|
});
|
|
|
|
const onProviderComplete = (data) => {
|
|
providerName.value = data.providerName;
|
|
provider.value = data.provider;
|
|
providerReady.value = true;
|
|
};
|
|
|
|
const onProviderFormValid = (valid) => {
|
|
providerFormValid.value = valid;
|
|
};
|
|
|
|
const onProviderTesting = (testing) => {
|
|
providerTesting.value = testing;
|
|
};
|
|
|
|
const onProviderReady = (ready) => {
|
|
providerReady.value = ready;
|
|
};
|
|
|
|
const onMappingsReady = (ready) => {
|
|
mappingsReady.value = ready;
|
|
};
|
|
|
|
// Clear mappings when provider changes
|
|
watch(() => stepData.provider.providerName, (newVal, oldVal) => {
|
|
if (oldVal && newVal !== oldVal) {
|
|
stepData.mappings.networkEntries = [];
|
|
stepData.mappings.storageEntries = [];
|
|
mappingsReady.value = false;
|
|
}
|
|
});
|
|
|
|
const providerListLocation = {
|
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
params: {
|
|
product: store.getters['productId'],
|
|
cluster: store.getters['clusterId'],
|
|
resource: HCI.FORKLIFT_PROVIDER,
|
|
}
|
|
};
|
|
|
|
const onFinish = async(buttonCb) => {
|
|
try {
|
|
await mappingsStepRef.value.saveMappings();
|
|
buttonCb(true);
|
|
currentRouter().push(providerListLocation);
|
|
} catch (err) {
|
|
errors.value = exceptionToErrorsArray(err).map((e) => (typeof e === 'string' ? e : stringify(e)));
|
|
buttonCb(false);
|
|
}
|
|
};
|
|
|
|
const onCancel = () => {
|
|
currentRouter().push(providerListLocation);
|
|
};
|
|
|
|
const init = async() => {
|
|
if (!isEditMode) {
|
|
initialLoading.value = false;
|
|
|
|
return;
|
|
}
|
|
|
|
const inStore = store.getters['currentProduct'].inStore;
|
|
|
|
try {
|
|
const fetchedProvider = await store.dispatch(`${ inStore }/find`, {
|
|
type: HCI.FORKLIFT_PROVIDER,
|
|
id: providerId,
|
|
opt: { force: true },
|
|
});
|
|
|
|
provider.value = fetchedProvider;
|
|
providerName.value = fetchedProvider.metadata.name;
|
|
|
|
stepData.provider.selectedProvider = fetchedProvider.metadata.name;
|
|
stepData.provider.providerName = fetchedProvider.metadata.name;
|
|
stepData.provider.url = fetchedProvider.spec?.url || '';
|
|
stepData.provider.vddkInitImage = fetchedProvider.spec?.settings?.vddkInitImage || '';
|
|
stepData.provider.createdProvider = fetchedProvider;
|
|
|
|
const secretRef = fetchedProvider.spec?.secret;
|
|
|
|
if (secretRef) {
|
|
const allSecrets = await store.dispatch(`${ inStore }/findAll`, {
|
|
type: SECRET,
|
|
opt: { labelSelector: `ui.forklift/created-for-resource-type=${ HCI.FORKLIFT_PROVIDER }` },
|
|
});
|
|
|
|
const secret = allSecrets.find(
|
|
(s) => s.metadata.name === secretRef.name && s.metadata.namespace === secretRef.namespace
|
|
);
|
|
|
|
if (secret?.data) {
|
|
stepData.provider.username = decodeSecretValue(secret.data.user);
|
|
stepData.provider.password = decodeSecretValue(secret.data.password);
|
|
stepData.provider.skipTlsVerify = decodeSecretValue(secret.data.insecureSkipVerify) === 'true';
|
|
stepData.provider.createdSecret = secret;
|
|
}
|
|
}
|
|
|
|
// Fetch existing default maps for this provider
|
|
try {
|
|
const allNetworkMaps = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP });
|
|
const allStorageMaps = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_STORAGE_MAP });
|
|
|
|
existingNetworkMap.value = allNetworkMaps.find(
|
|
(nm) => nm.metadata.name === `${ fetchedProvider.metadata.name }-network-map-default`
|
|
) || null;
|
|
|
|
existingStorageMap.value = allStorageMaps.find(
|
|
(sm) => sm.metadata.name === `${ fetchedProvider.metadata.name }-storage-map-default`
|
|
) || null;
|
|
} catch (e) {
|
|
// Maps may not exist yet
|
|
}
|
|
} catch (err) {
|
|
errors.value = [t('harvester.addons.vmMigration.errors.failedLoadProvider', { error: err.message || stringify(err) })];
|
|
}
|
|
|
|
initialLoading.value = false;
|
|
};
|
|
|
|
init();
|
|
</script>
|
|
|
|
<template>
|
|
<Loading v-if="initialLoading" />
|
|
<CruResource
|
|
v-else
|
|
ref="cruRef"
|
|
:resource="dummyResource"
|
|
:mode="isEditMode ? 'edit' : 'create'"
|
|
:steps="steps"
|
|
:errors="errors"
|
|
:validation-passed="true"
|
|
:can-yaml="false"
|
|
:cancel-event="true"
|
|
:title="wizardTitle"
|
|
finish-mode="finish"
|
|
class="wizard"
|
|
@keydown.enter.capture="preventImplicitSubmit"
|
|
@cancel="onCancel"
|
|
@finish="onFinish"
|
|
>
|
|
<template #configure-provider>
|
|
<ConfigureProviderStep
|
|
ref="providerStepRef"
|
|
:step-data="stepData.provider"
|
|
:create-only="true"
|
|
:edit-mode="isEditMode"
|
|
@complete="onProviderComplete"
|
|
@ready="onProviderReady"
|
|
@form-valid="onProviderFormValid"
|
|
@testing="onProviderTesting"
|
|
/>
|
|
</template>
|
|
<template #configure-mappings>
|
|
<ConfigureMappingsStep
|
|
ref="mappingsStepRef"
|
|
:provider-name="providerName"
|
|
:provider="provider"
|
|
:step-data="stepData.mappings"
|
|
:use-all-provider-data="true"
|
|
:existing-network-map="existingNetworkMap"
|
|
:existing-storage-map="existingStorageMap"
|
|
@ready="onMappingsReady"
|
|
/>
|
|
</template>
|
|
</CruResource>
|
|
</template>
|