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'),
]"
/>
-
+