mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2025-12-13 13:11:43 +00:00
561 lines
15 KiB
Vue
561 lines
15 KiB
Vue
<script>
|
|
import CruResource from '@shell/components/CruResource';
|
|
import { RadioGroup } from '@components/Form/Radio';
|
|
import { Checkbox } from '@components/Form/Checkbox';
|
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
|
import UpgradeInfo from '../../../../components/UpgradeInfo';
|
|
import { HCI } from '../../../../types';
|
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../../config/harvester';
|
|
import ImagePercentageBar from '@shell/components/formatter/ImagePercentageBar';
|
|
import { Banner } from '@components/Banner';
|
|
import isEmpty from 'lodash/isEmpty';
|
|
|
|
const IMAGE_METHOD = {
|
|
NEW: 'new',
|
|
EXIST: 'exist',
|
|
DELETE: 'delete'
|
|
};
|
|
|
|
const DOWNLOAD = 'download';
|
|
const UPLOAD = 'upload';
|
|
|
|
export default {
|
|
name: 'HarvesterAirgapUpgrade',
|
|
components: {
|
|
Checkbox, CruResource, LabeledSelect, LabeledInput, RadioGroup, UpgradeInfo, ImagePercentageBar, Banner
|
|
},
|
|
|
|
inheritAttrs: false,
|
|
|
|
async fetch() {
|
|
await this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE });
|
|
|
|
const value = await this.$store.dispatch('harvester/create', {
|
|
type: HCI.UPGRADE,
|
|
metadata: {
|
|
generateName: 'hvst-upgrade-',
|
|
namespace: 'harvester-system',
|
|
},
|
|
spec: { image: '' }
|
|
});
|
|
|
|
await this.initImageValue();
|
|
this.value = value;
|
|
},
|
|
|
|
beforeUnmount() {
|
|
if (this.uploadController) {
|
|
this.uploadController.abort();
|
|
}
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
value: null,
|
|
file: {},
|
|
uploadImageId: '',
|
|
imageId: '',
|
|
deleteImageId: '',
|
|
imageSource: IMAGE_METHOD.NEW,
|
|
sourceType: UPLOAD,
|
|
uploadController: null,
|
|
uploadResult: null,
|
|
imageValue: null,
|
|
enableLogging: true,
|
|
IMAGE_METHOD,
|
|
skipSingleReplicaDetachedVol: false,
|
|
errors: [],
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
doneRoute() {
|
|
return `${ HARVESTER_PRODUCT }-c-cluster-resource`;
|
|
},
|
|
|
|
skipSingleReplicaDetachedVolFeatureEnabled() {
|
|
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
|
|
},
|
|
|
|
allOSImages() {
|
|
return this.$store.getters['harvester/all'](HCI.IMAGE).filter((I) => I.isOSImage) || [];
|
|
},
|
|
|
|
deleteOSImageOptions() {
|
|
return this.allOSImages.map((I) => {
|
|
return {
|
|
label: I.spec.displayName,
|
|
value: I.id,
|
|
};
|
|
});
|
|
},
|
|
|
|
osImageOptions() {
|
|
return this.allOSImages.map((I) => {
|
|
return {
|
|
label: I.spec.displayName,
|
|
value: I.id,
|
|
disabled: !I.isReady
|
|
};
|
|
});
|
|
},
|
|
|
|
createNewImage() {
|
|
return this.imageSource === IMAGE_METHOD.NEW;
|
|
},
|
|
|
|
selectExistImage() {
|
|
return this.imageSource === IMAGE_METHOD.EXIST;
|
|
},
|
|
|
|
deleteExistImage() {
|
|
return this.imageSource === IMAGE_METHOD.DELETE;
|
|
},
|
|
|
|
fileName() {
|
|
return this.file?.name || '';
|
|
},
|
|
|
|
canEnableLogging() {
|
|
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
|
|
},
|
|
|
|
uploadProgress() {
|
|
const image = this.$store.getters['harvester/byId'](HCI.IMAGE, this.imageValue.id);
|
|
|
|
return image?.status?.progress;
|
|
},
|
|
|
|
enableUpgrade() {
|
|
if (this.deleteExistImage) {
|
|
return false;
|
|
}
|
|
|
|
if (this.sourceType === DOWNLOAD) {
|
|
return true;
|
|
}
|
|
|
|
if (this.sourceType === UPLOAD) {
|
|
return this.fileName === '' ? true : this.uploadProgress === 100;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
isUploading() {
|
|
return this.fileName !== '' && this.uploadProgress !== 100;
|
|
},
|
|
|
|
showProgressBar() {
|
|
return this.createNewImage && this.sourceType === UPLOAD && this.isUploading;
|
|
},
|
|
|
|
showUploadSuccessBanner() {
|
|
return this.createNewImage && this.fileName !== '' && isEmpty(this.errors) && !this.showUploadingWarningBanner && this.uploadResult?._status === 200;
|
|
},
|
|
|
|
showUploadingWarningBanner() {
|
|
return this.createNewImage && this.isUploading;
|
|
},
|
|
|
|
showUpgradeOptions() {
|
|
return this.createNewImage || this.selectExistImage;
|
|
},
|
|
|
|
disableUploadButton() {
|
|
return this.sourceType === UPLOAD && this.isUploading;
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
done() {
|
|
if (this.uploadController) {
|
|
this.uploadController.abort();
|
|
}
|
|
this.$router.push({
|
|
name: this.doneRoute,
|
|
params: { resource: HCI.SETTING, product: 'harvester' }
|
|
});
|
|
},
|
|
|
|
async initImageValue() {
|
|
this.imageValue = await this.$store.dispatch('harvester/create', {
|
|
type: HCI.IMAGE,
|
|
metadata: {
|
|
name: '',
|
|
namespace: 'harvester-system',
|
|
generateName: 'image-',
|
|
annotations: {}
|
|
},
|
|
spec: {
|
|
sourceType: UPLOAD,
|
|
displayName: '',
|
|
checksum: ''
|
|
},
|
|
});
|
|
},
|
|
|
|
async save(buttonCb) {
|
|
let res = null;
|
|
|
|
this.file = {};
|
|
this.errors = [];
|
|
|
|
if (!this.imageValue.spec.displayName && this.createNewImage) {
|
|
this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') }));
|
|
buttonCb(false);
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (this.imageSource === IMAGE_METHOD.NEW) {
|
|
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
|
|
|
|
if (this.sourceType === UPLOAD && this.uploadImageId !== '') {
|
|
this.value.spec.image = this.uploadImageId;
|
|
} else if (this.sourceType === DOWNLOAD) {
|
|
this.imageValue.spec.sourceType = DOWNLOAD;
|
|
|
|
if (!this.imageValue.spec.url) {
|
|
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
|
|
buttonCb(false);
|
|
|
|
return;
|
|
}
|
|
|
|
res = await this.imageValue.save();
|
|
this.value.spec.image = res.id;
|
|
}
|
|
} else if (this.imageSource === IMAGE_METHOD.EXIST) {
|
|
if (!this.imageId) {
|
|
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
|
|
buttonCb(false);
|
|
|
|
return;
|
|
}
|
|
|
|
this.value.spec.image = this.imageId;
|
|
}
|
|
|
|
if (this.canEnableLogging) {
|
|
this.value.spec.logEnabled = this.enableLogging;
|
|
}
|
|
if (this.skipSingleReplicaDetachedVolFeatureEnabled) {
|
|
this.value.metadata.annotations = { [HCI_ANNOTATIONS.SKIP_SINGLE_REPLICA_DETACHED_VOL]: JSON.stringify(this.skipSingleReplicaDetachedVol) };
|
|
}
|
|
|
|
await this.value.save();
|
|
this.done();
|
|
buttonCb(true);
|
|
} catch (e) {
|
|
this.errors = [e?.message] || exceptionToErrorsArray(e);
|
|
buttonCb(false);
|
|
}
|
|
},
|
|
|
|
async uploadFile(file) {
|
|
const fileName = file.name;
|
|
|
|
if (!fileName) {
|
|
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.unknownImageName'));
|
|
|
|
return;
|
|
}
|
|
const isDuplicatedFile = this.allOSImages.some((I) => I.spec.displayName === fileName);
|
|
|
|
if (isDuplicatedFile) {
|
|
this.errors.push(this.$store.getters['i18n/t']('harvester.upgradePage.upload.duplicatedFile'));
|
|
this.file = {};
|
|
|
|
return;
|
|
}
|
|
|
|
this.errors = [];
|
|
this.imageValue.spec.sourceType = UPLOAD;
|
|
this.imageValue.spec.displayName = fileName;
|
|
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
|
|
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.IMAGE_NAME] = fileName;
|
|
this.imageValue.spec.url = '';
|
|
|
|
try {
|
|
const res = await this.imageValue.save();
|
|
|
|
this.uploadImageId = res.id;
|
|
this.uploadController = new AbortController();
|
|
|
|
const signal = this.uploadController.signal;
|
|
|
|
this.uploadResult = await res.uploadImage(file, { signal });
|
|
} catch (e) {
|
|
if (e?.code === 'ERR_NETWORK') {
|
|
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.networkError'));
|
|
} else if (e?.code === 'ERR_CANCELED') {
|
|
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.cancelUpload'));
|
|
} else {
|
|
this.errors = [e?.message] || exceptionToErrorsArray(e);
|
|
}
|
|
this.file = {};
|
|
this.uploadImageId = '';
|
|
}
|
|
},
|
|
|
|
handleImageDelete(imageId) {
|
|
const image = this.allOSImages.find((I) => I.id === imageId);
|
|
|
|
if (image) {
|
|
this.$store.dispatch('harvester/promptModal', {
|
|
resources: [image],
|
|
component: 'ConfirmRelatedToRemoveDialog',
|
|
needConfirmation: false,
|
|
warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: image.displayName })
|
|
});
|
|
this.deleteImageId = '';
|
|
}
|
|
},
|
|
|
|
async handleFileUpload() {
|
|
this.uploadImageId = '';
|
|
this.errors = [];
|
|
this.file = this.$refs.file?.files[0];
|
|
if (this.file) {
|
|
await this.initImageValue();
|
|
await this.uploadFile(this.file);
|
|
}
|
|
},
|
|
|
|
selectFile() {
|
|
this.$refs.file.value = null;
|
|
this.$refs.file.click();
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
imageSource(neu) {
|
|
if (neu !== IMAGE_METHOD.DELETE) {
|
|
this.deleteImageId = '';
|
|
}
|
|
},
|
|
|
|
'imageValue.spec.url': {
|
|
handler(neu) {
|
|
const suffixName = neu?.split('/')?.pop();
|
|
const splitName = suffixName?.split('.') || [];
|
|
const fileSuffix = splitName?.pop()?.toLowerCase();
|
|
|
|
if (splitName.length > 1 && fileSuffix === 'iso' && suffixName !== this.imageValue.spec.displayName) {
|
|
this.imageValue.spec.displayName = suffixName;
|
|
}
|
|
},
|
|
deep: true
|
|
},
|
|
file(neu) {
|
|
// update name input if select new image
|
|
if (neu.name && neu.name !== this.imageValue.spec.displayName) {
|
|
this.imageValue.spec.displayName = neu.name;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
v-if="value"
|
|
id="air-gap"
|
|
>
|
|
<h3 class="mb-20">
|
|
{{ t('harvester.upgradePage.osUpgrade') }}
|
|
</h3>
|
|
<CruResource
|
|
:done-route="doneRoute"
|
|
:resource="value"
|
|
mode="create"
|
|
:errors="errors"
|
|
:can-yaml="false"
|
|
finish-button-mode="upgrade"
|
|
:validation-passed="enableUpgrade"
|
|
:cancel-event="true"
|
|
@finish="save"
|
|
@cancel="done"
|
|
@error="e=>errors = e"
|
|
>
|
|
<RadioGroup
|
|
v-model:value="imageSource"
|
|
class="image-group"
|
|
name="image"
|
|
:options="[
|
|
IMAGE_METHOD.NEW,
|
|
IMAGE_METHOD.EXIST,
|
|
IMAGE_METHOD.DELETE,
|
|
]"
|
|
:labels="[
|
|
t('harvester.upgradePage.uploadNew'),
|
|
t('harvester.upgradePage.selectExisting'),
|
|
t('harvester.upgradePage.deleteExisting'),
|
|
]"
|
|
/>
|
|
|
|
<UpgradeInfo v-if="createNewImage || selectExistImage" />
|
|
|
|
<Banner
|
|
v-if="showUploadSuccessBanner"
|
|
color="success"
|
|
class="mt-0 mb-30"
|
|
:label="t('harvester.setting.upgrade.uploadSuccess', { name: file.name })"
|
|
/>
|
|
<Banner
|
|
v-if="showUploadingWarningBanner"
|
|
color="warning"
|
|
class="mt-0 mb-30"
|
|
:label="t('harvester.image.warning.osUpgrade.uploading', { name: file.name })"
|
|
/>
|
|
|
|
<div
|
|
v-if="showUpgradeOptions"
|
|
class="mt-10 mb-10"
|
|
>
|
|
<Checkbox
|
|
v-if="canEnableLogging"
|
|
v-model:value="enableLogging"
|
|
class="check mb-20"
|
|
type="checkbox"
|
|
:label="t('harvester.upgradePage.enableLogging')"
|
|
/>
|
|
<div
|
|
v-if="skipSingleReplicaDetachedVolFeatureEnabled"
|
|
class="mb-20"
|
|
>
|
|
<Checkbox
|
|
v-model:value="skipSingleReplicaDetachedVol"
|
|
class="check"
|
|
type="checkbox"
|
|
:label="t('harvester.upgradePage.skipSingleReplicaDetachedVol')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="createNewImage">
|
|
<LabeledInput
|
|
v-model:value.trim="imageValue.spec.displayName"
|
|
class="mb-20"
|
|
label-key="harvester.fields.name"
|
|
required
|
|
/>
|
|
|
|
<LabeledInput
|
|
v-model:value="imageValue.spec.checksum"
|
|
class="mb-10"
|
|
label-key="harvester.setting.upgrade.checksum"
|
|
/>
|
|
|
|
<RadioGroup
|
|
v-model:value="sourceType"
|
|
class="mb-20 image-group"
|
|
name="sourceType"
|
|
:options="[
|
|
'upload',
|
|
'download',
|
|
]"
|
|
:labels="[
|
|
t('harvester.image.sourceType.upload'),
|
|
t('harvester.image.sourceType.download')
|
|
]"
|
|
/>
|
|
|
|
<LabeledInput
|
|
v-if="sourceType === 'download'"
|
|
v-model:value.trim="imageValue.spec.url"
|
|
class="labeled-input--tooltip"
|
|
required
|
|
label-key="harvester.image.url"
|
|
/>
|
|
|
|
<div
|
|
v-else
|
|
class="chooseFile"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="btn role-primary"
|
|
:disabled="disableUploadButton"
|
|
@click="selectFile"
|
|
>
|
|
{{ t('harvester.image.uploadFile') }}
|
|
<input
|
|
v-show="false"
|
|
id="file"
|
|
ref="file"
|
|
type="file"
|
|
accept=".iso"
|
|
@change="handleFileUpload()"
|
|
/>
|
|
</button>
|
|
|
|
<span
|
|
:class="{ 'text-muted': !fileName }"
|
|
class="ml-20"
|
|
>
|
|
{{ fileName ? fileName : t('harvester.generic.noFileChosen') }}
|
|
</span>
|
|
</div>
|
|
<ImagePercentageBar
|
|
v-if="showProgressBar"
|
|
class="mt-20"
|
|
:value="uploadProgress"
|
|
/>
|
|
</div>
|
|
<LabeledSelect
|
|
v-if="selectExistImage"
|
|
v-model:value="imageId"
|
|
:options="osImageOptions"
|
|
required
|
|
class="mb-20"
|
|
label-key="harvester.fields.image"
|
|
/>
|
|
|
|
<div
|
|
v-if="deleteExistImage"
|
|
class="mt-20"
|
|
>
|
|
<Banner
|
|
color="info"
|
|
class="mt-10 mb-30"
|
|
:label="t('harvester.upgradePage.deleteHeader')"
|
|
/>
|
|
<LabeledSelect
|
|
v-model:value="deleteImageId"
|
|
:options="deleteOSImageOptions"
|
|
required
|
|
class="mb-20"
|
|
label-key="harvester.fields.image"
|
|
@update:value="handleImageDelete"
|
|
/>
|
|
</div>
|
|
</CruResource>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
#air-gap {
|
|
padding: 20px;
|
|
|
|
:deep() .image-group .radio-group {
|
|
display: flex;
|
|
.radio-container {
|
|
margin-right: 30px;
|
|
}
|
|
}
|
|
.parent {
|
|
grid-template-columns:auto 40px;
|
|
}
|
|
.chooseFile {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
}
|
|
</style>
|