diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index 1610691a..1ea89490 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -842,6 +842,8 @@ harvester: =1 {1 image is uploading, please do not refresh or close the page.} other {{count} images are uploading, please do not refresh or close the page.} } + osUpgrade: + uploading: "{name} is uploading, please do not refresh or close the page." checksum: Checksum checksumTip: Validate the image using the SHA512 checksum, if specified. @@ -1080,7 +1082,7 @@ harvester: addConfig: Add Configuration upgrade: - selectExitImage: Please select the OS image to upgrade. + unknownImageName: Image name is not found. imageUrl: Please input a valid image URL. chooseFile: Please select to upload an image. checksum: Checksum diff --git a/pkg/harvester/list/harvesterhci.io.setting.vue b/pkg/harvester/list/harvesterhci.io.setting.vue index 4567d10d..c7fe5797 100644 --- a/pkg/harvester/list/harvesterhci.io.setting.vue +++ b/pkg/harvester/list/harvesterhci.io.setting.vue @@ -2,13 +2,12 @@ import { mapGetters } from 'vuex'; import { Banner } from '@components/Banner'; import Loading from '@shell/components/Loading'; -import { VIEW_IN_API, DEV } from '@shell/store/prefs'; import { MANAGEMENT } from '@shell/config/types'; import { allHash } from '@shell/utils/promise'; import Tabbed from '@shell/components/Tabbed/index.vue'; import Tab from '@shell/components/Tabbed/Tab.vue'; import Settings from '@pkg/harvester/components/SettingList.vue'; -import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING } from '../config/settings'; +import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings'; import { HCI } from '../types'; export default { @@ -21,14 +20,6 @@ export default { }, async fetch() { - let isDev; - - try { - isDev = this.$store.getters['prefs/get'](VIEW_IN_API); - } catch { - isDev = this.$store.getters['prefs/get'](DEV); - } - const isSingleProduct = !!this.$store.getters['isSingleProduct']; const inStore = this.$store.getters['currentProduct'].inStore; @@ -77,7 +68,7 @@ export default { }; s.hide = s.canHide = (s.kind === 'json' || s.kind === 'multiline' || s.customFormatter === 'json' || s.data.customFormatter === 'json'); - s.hasActions = !s.readOnly || isDev; + s.hasActions = s.id === HCI_SETTING.SERVER_VERSION ? true : !s.readOnly; initSettings.push(s); }); diff --git a/pkg/harvester/models/harvesterhci.io.setting.js b/pkg/harvester/models/harvesterhci.io.setting.js index fe0d4de3..46c016ba 100644 --- a/pkg/harvester/models/harvesterhci.io.setting.js +++ b/pkg/harvester/models/harvesterhci.io.setting.js @@ -28,6 +28,7 @@ export default class HciSetting extends HarvesterResource { } const schema = this.$getters['schemaFor'](HCI.UPGRADE); + const hasUpgradeAccess = !!schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())); if (this.id === HCI_SETTING.SERVER_VERSION && hasUpgradeAccess) { @@ -38,7 +39,7 @@ export default class HciSetting extends HarvesterResource { enabled: true, icon: 'icon icon-refresh', label: this.t('harvester.upgradePage.upgrade'), - disabled: !!latestUpgrade && !latestUpgrade?.isUpgradeSucceeded + disabled: !!latestUpgrade && latestUpgrade?.isUpgradeSucceeded }); } diff --git a/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js b/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js index e101b6ce..f3107a03 100644 --- a/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js +++ b/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js @@ -305,7 +305,7 @@ export default class HciVmImage extends HarvesterResource { } get uploadImage() { - return async(file) => { + return async(file, opt) => { const formData = new FormData(); formData.append('chunk', file); @@ -319,6 +319,7 @@ export default class HciVmImage extends HarvesterResource { 'File-Size': file.size, }, params: { size: file.size }, + signal: opt.signal, }); } catch (err) { this.$ctx.commit('harvester-common/uploadError', { name: this.name, message: err.message }, { root: true }); diff --git a/pkg/harvester/pages/c/_cluster/airgapupgrade/index.vue b/pkg/harvester/pages/c/_cluster/airgapupgrade/index.vue index b54255c4..2a1e4e5f 100644 --- a/pkg/harvester/pages/c/_cluster/airgapupgrade/index.vue +++ b/pkg/harvester/pages/c/_cluster/airgapupgrade/index.vue @@ -7,9 +7,10 @@ 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'; const IMAGE_METHOD = { NEW: 'new', @@ -22,7 +23,7 @@ const UPLOAD = 'upload'; export default { name: 'HarvesterAirgapUpgrade', components: { - Checkbox, CruResource, LabeledSelect, LabeledInput, RadioGroup, UpgradeInfo + Checkbox, CruResource, LabeledSelect, LabeledInput, RadioGroup, UpgradeInfo, ImagePercentageBar, Banner }, inheritAttrs: false, @@ -58,16 +59,24 @@ export default { this.imageValue = imageValue; }, + beforeUnmount() { + if (this.uploadController) { + this.uploadController.abort(); + } + }, + data() { return { - value: null, - file: {}, - imageId: '', - imageSource: IMAGE_METHOD.NEW, - sourceType: UPLOAD, - imageValue: null, - errors: [], - enableLogging: true, + value: null, + file: {}, + uploadImageId: '', + imageId: '', + imageSource: IMAGE_METHOD.NEW, + sourceType: UPLOAD, + uploadController: null, + imageValue: null, + errors: [], + enableLogging: true, IMAGE_METHOD }; }, @@ -100,10 +109,43 @@ export default { 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; + }, + + enableSave() { + if (this.sourceType === DOWNLOAD) { + return true; + } + + if (this.sourceType === UPLOAD) { + return this.fileName === '' ? true : this.uploadProgress === 100; + } + + return true; + }, + + showProgressBar() { + return this.sourceType === UPLOAD && this.fileName !== ''; + }, + + showUploadingWarningBanner() { + return this.fileName !== '' && this.uploadProgress !== 100; + }, + + disableUploadButton() { + return this.sourceType === UPLOAD && this.fileName !== '' && this.uploadProgress !== 100; + }, }, methods: { done() { + if (this.uploadController) { + this.uploadController.abort(); + } this.$router.push({ name: this.doneRoute, params: { resource: HCI.SETTING, product: 'harvester' } @@ -125,24 +167,8 @@ export default { if (this.imageSource === IMAGE_METHOD.NEW) { this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True'; - if (this.sourceType === UPLOAD) { - this.imageValue.spec.sourceType = UPLOAD; - const file = this.file; - - if (!file.name) { - this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.selectExitImage')); - buttonCb(false); - - return; - } - - this.imageValue.spec.url = ''; - - this.imageValue.metadata.annotations[HCI_ANNOTATIONS.IMAGE_NAME] = file.name; - - res = await this.imageValue.save(); - - res.uploadImage(file); + 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) { @@ -153,9 +179,8 @@ export default { } res = await this.imageValue.save(); + this.value.spec.image = res.id; } - - 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')); @@ -179,8 +204,39 @@ export default { } }, - handleFileUpload() { + 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; + } + + this.imageValue.spec.url = ''; + this.imageValue.metadata.annotations[HCI_ANNOTATIONS.IMAGE_NAME] = fileName; + + 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 }); + } catch (e) { + this.errors = exceptionToErrorsArray(e); + } + }, + + async handleFileUpload() { this.file = this.$refs.file.files[0]; + this.errors = []; + await this.uploadFile(this.file); }, selectFile() { @@ -196,15 +252,15 @@ export default { const splitName = suffixName?.split('.') || []; const fileSuffix = splitName?.pop()?.toLowerCase(); - if (splitName.length > 1 && fileSuffix === 'iso' && !this.imageValue.spec.displayName) { + if (splitName.length > 1 && fileSuffix === 'iso' && suffixName !== this.imageValue.spec.displayName) { this.imageValue.spec.displayName = suffixName; } }, deep: true }, - file(neu) { - if (!this.imageValue.spec.displayName && neu.name) { + // update name input if select new image + if (neu.name && neu.name !== this.imageValue.spec.displayName) { this.imageValue.spec.displayName = neu.name; } } @@ -227,6 +283,7 @@ export default { :errors="errors" :can-yaml="false" finish-button-mode="upgrade" + :validation-passed="enableSave" :cancel-event="true" @finish="save" @cancel="done" @@ -244,12 +301,16 @@ export default { t('harvester.upgradePage.selectExisting'), ]" /> - +
{{ t('harvester.image.uploadFile') }} @@ -318,6 +380,11 @@ export default { {{ fileName ? fileName : t('harvester.generic.noFileChosen') }}
+ #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;