diff --git a/pkg/harvester/components/UpgradeInfo.vue b/pkg/harvester/components/UpgradeInfo.vue index 2ea4ec55..798a0525 100644 --- a/pkg/harvester/components/UpgradeInfo.vue +++ b/pkg/harvester/components/UpgradeInfo.vue @@ -43,7 +43,7 @@ export default { {{ t('harvester.upgradePage.upgradeInfo.tip') }}

-

+

{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} -

- -
-
- -
- -
- {{ protip }} +
+
+ +
+
+ +
+ +
+ {{ protip }} +
Harvester Release Notes
. checkReady: I have read and understood the upgrade instructions related to this Harvester version. pending: Pending + upload: + duplicatedFile: The file you are trying to upload already exists. repoInfo: upgradeStatus: Upgrade Status os: OS @@ -1086,6 +1094,11 @@ harvester: imageUrl: Please input a valid image URL. chooseFile: Please select to upload an image. checksum: Checksum + networkError: Unable to upload the image. Resolve network issues that may have occurred and try again. + cancelUpload: Cancelled the image upload. + uploadSuccess: "{name} uploaded successfully. Press Upgrade button to start the cluster upgrade process." + deleteImage: Please select an image to delete. + deleteSuccess: "{name} deleted successfully." harvesterMonitoring: label: Harvester Monitoring section: diff --git a/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js b/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js index 7a4fcb77..5bc70554 100644 --- a/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js +++ b/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js @@ -313,7 +313,7 @@ export default class HciVmImage extends HarvesterResource { try { this.$ctx.commit('harvester-common/uploadStart', this.metadata.name, { root: true }); - await this.doAction('upload', formData, { + const result = await this.doAction('upload', formData, { headers: { 'Content-Type': 'multipart/form-data', 'File-Size': file.size, @@ -321,15 +321,15 @@ export default class HciVmImage extends HarvesterResource { params: { size: file.size }, signal: opt.signal, }); + + return result; } catch (err) { this.$ctx.commit('harvester-common/uploadError', { name: this.name, message: err.message }, { root: true }); - this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true }); - - return Promise.reject(err); + throw err; + } finally { + this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true }); } - - this.$ctx.commit('harvester-common/uploadEnd', this.metadata.name, { root: true }); }; } diff --git a/pkg/harvester/pages/c/_cluster/airgapupgrade/index.vue b/pkg/harvester/pages/c/_cluster/airgapupgrade/index.vue index 2a1e4e5f..c11ff72f 100644 --- a/pkg/harvester/pages/c/_cluster/airgapupgrade/index.vue +++ b/pkg/harvester/pages/c/_cluster/airgapupgrade/index.vue @@ -11,10 +11,12 @@ 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' + NEW: 'new', + EXIST: 'exist', + DELETE: 'delete' }; const DOWNLOAD = 'download'; @@ -40,23 +42,8 @@ export default { spec: { image: '' } }); - const imageValue = await this.$store.dispatch('harvester/create', { - type: HCI.IMAGE, - metadata: { - name: '', - namespace: 'harvester-system', - generateName: 'image-', - annotations: {} - }, - spec: { - sourceType: UPLOAD, - displayName: '', - checksum: '' - }, - }); - + await this.initImageValue(); this.value = value; - this.imageValue = imageValue; }, beforeUnmount() { @@ -67,17 +54,20 @@ export default { data() { return { - value: null, - file: {}, - uploadImageId: '', - imageId: '', - imageSource: IMAGE_METHOD.NEW, - sourceType: UPLOAD, - uploadController: null, - imageValue: null, - errors: [], - enableLogging: true, - IMAGE_METHOD + 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: [], }; }, @@ -86,22 +76,45 @@ export default { return `${ HARVESTER_PRODUCT }-c-cluster-resource`; }, - osImageOptions() { - return this.$store.getters['harvester/all'](HCI.IMAGE) - .filter((I) => I.isOSImage) - .map((I) => { - return { - label: I.spec.displayName, - value: I.id, - disabled: !I.isReady - }; - }); + skipSingleReplicaDetachedVolFeatureEnabled() { + return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol'); }, - uploadImage() { + 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 || ''; }, @@ -116,7 +129,11 @@ export default { return image?.status?.progress; }, - enableSave() { + enableUpgrade() { + if (this.deleteExistImage) { + return false; + } + if (this.sourceType === DOWNLOAD) { return true; } @@ -128,16 +145,28 @@ export default { return true; }, - showProgressBar() { - return this.sourceType === UPLOAD && this.fileName !== ''; - }, - - showUploadingWarningBanner() { + 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.fileName !== '' && this.uploadProgress !== 100; + return this.sourceType === UPLOAD && this.isUploading; }, }, @@ -152,11 +181,30 @@ export default { }); }, + 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.uploadImage) { + + if (!this.imageValue.spec.displayName && this.createNewImage) { this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') })); buttonCb(false); @@ -164,6 +212,28 @@ export default { } try { + if (this.deleteExistImage) { + // if not select image, show error + if (!this.deleteImageId) { + this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.deleteImage')); + buttonCb(false); + + return; + } + + // if select image, delete image + const image = this.$store.getters['harvester/byId'](HCI.IMAGE, this.deleteImageId); + + if (image) { + this.handleImageDelete(image); + buttonCb(true); + + return; + } + + return; + } + if (this.imageSource === IMAGE_METHOD.NEW) { this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True'; @@ -194,12 +264,15 @@ export default { 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 = exceptionToErrorsArray(e); + this.errors = [e?.message] || exceptionToErrorsArray(e); buttonCb(false); } }, @@ -207,36 +280,71 @@ export default { async uploadFile(file) { const fileName = file.name; - this.imageValue.spec.sourceType = UPLOAD; - this.imageValue.spec.displayName = fileName; - this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True'; - 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); - this.imageValue.spec.url = ''; + 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; - await res.uploadImage(file, { signal }); + this.uploadResult = await res.uploadImage(file, { signal }); } catch (e) { - this.errors = exceptionToErrorsArray(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.file = this.$refs.file.files[0]; + this.uploadImageId = ''; this.errors = []; - await this.uploadFile(this.file); + this.file = this.$refs.file?.files[0]; + if (this.file) { + await this.initImageValue(); + await this.uploadFile(this.file); + } }, selectFile() { @@ -246,6 +354,12 @@ export default { }, watch: { + imageSource(neu) { + if (neu !== IMAGE_METHOD.DELETE) { + this.deleteImageId = ''; + } + }, + 'imageValue.spec.url': { handler(neu) { const suffixName = neu?.split('/')?.pop(); @@ -283,10 +397,11 @@ export default { :errors="errors" :can-yaml="false" finish-button-mode="upgrade" - :validation-passed="enableSave" + :validation-passed="enableUpgrade" :cancel-event="true" @finish="save" @cancel="done" + @error="e=>errors = e" > + + + + - -
+
+ +
+ +
+
+ +
- -
- + +
+ + +