Fix OS upgrade page (#306)

* fix OS upgrade page

Signed-off-by: Andy Lee <andy.lee@suse.com>

* move uploadFile action

Signed-off-by: Andy Lee <andy.lee@suse.com>

* change os upgrage ui to standard feature

Signed-off-by: a110605 <andy.lee@suse.com>

* Add uploadImage cancel

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
Signed-off-by: a110605 <andy.lee@suse.com>
This commit is contained in:
Andy Lee 2025-05-21 16:51:16 +08:00 committed by GitHub
parent 9e240792e8
commit 6d8f6579c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 118 additions and 51 deletions

View File

@ -848,6 +848,8 @@ harvester:
=1 {1 image is uploading, please do not refresh or close the page.} =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.} 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 checksum: Checksum
checksumTip: Validate the image using the SHA512 checksum, if specified. checksumTip: Validate the image using the SHA512 checksum, if specified.
@ -1086,7 +1088,7 @@ harvester:
addConfig: Add Configuration addConfig: Add Configuration
upgrade: upgrade:
selectExitImage: Please select the OS image to upgrade. unknownImageName: Image name is not found.
imageUrl: Please input a valid image URL. imageUrl: Please input a valid image URL.
chooseFile: Please select to upload an image. chooseFile: Please select to upload an image.
checksum: Checksum checksum: Checksum

View File

@ -2,13 +2,12 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import Loading from '@shell/components/Loading'; import Loading from '@shell/components/Loading';
import { VIEW_IN_API, DEV } from '@shell/store/prefs';
import { MANAGEMENT } from '@shell/config/types'; import { MANAGEMENT } from '@shell/config/types';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
import Tabbed from '@shell/components/Tabbed/index.vue'; import Tabbed from '@shell/components/Tabbed/index.vue';
import Tab from '@shell/components/Tabbed/Tab.vue'; import Tab from '@shell/components/Tabbed/Tab.vue';
import Settings from '@pkg/harvester/components/SettingList.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'; import { HCI } from '../types';
export default { export default {
@ -21,14 +20,6 @@ export default {
}, },
async fetch() { 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 isSingleProduct = !!this.$store.getters['isSingleProduct'];
const inStore = this.$store.getters['currentProduct'].inStore; 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.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); initSettings.push(s);
}); });

View File

@ -28,6 +28,7 @@ export default class HciSetting extends HarvesterResource {
} }
const schema = this.$getters['schemaFor'](HCI.UPGRADE); const schema = this.$getters['schemaFor'](HCI.UPGRADE);
const hasUpgradeAccess = !!schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())); const hasUpgradeAccess = !!schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase()));
if (this.id === HCI_SETTING.SERVER_VERSION && hasUpgradeAccess) { if (this.id === HCI_SETTING.SERVER_VERSION && hasUpgradeAccess) {
@ -38,7 +39,7 @@ export default class HciSetting extends HarvesterResource {
enabled: true, enabled: true,
icon: 'icon icon-refresh', icon: 'icon icon-refresh',
label: this.t('harvester.upgradePage.upgrade'), label: this.t('harvester.upgradePage.upgrade'),
disabled: !!latestUpgrade && !latestUpgrade?.isUpgradeSucceeded disabled: !!latestUpgrade && latestUpgrade?.isUpgradeSucceeded
}); });
} }

View File

@ -305,7 +305,7 @@ export default class HciVmImage extends HarvesterResource {
} }
get uploadImage() { get uploadImage() {
return async(file) => { return async(file, opt) => {
const formData = new FormData(); const formData = new FormData();
formData.append('chunk', file); formData.append('chunk', file);
@ -319,6 +319,7 @@ export default class HciVmImage extends HarvesterResource {
'File-Size': file.size, 'File-Size': file.size,
}, },
params: { size: file.size }, params: { size: file.size },
signal: opt.signal,
}); });
} catch (err) { } catch (err) {
this.$ctx.commit('harvester-common/uploadError', { name: this.name, message: err.message }, { root: true }); this.$ctx.commit('harvester-common/uploadError', { name: this.name, message: err.message }, { root: true });

View File

@ -7,9 +7,10 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import UpgradeInfo from '../../../../components/UpgradeInfo'; import UpgradeInfo from '../../../../components/UpgradeInfo';
import { HCI } from '../../../../types'; import { HCI } from '../../../../types';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../../config/harvester'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../../config/harvester';
import ImagePercentageBar from '@shell/components/formatter/ImagePercentageBar';
import { Banner } from '@components/Banner';
const IMAGE_METHOD = { const IMAGE_METHOD = {
NEW: 'new', NEW: 'new',
@ -22,7 +23,7 @@ const UPLOAD = 'upload';
export default { export default {
name: 'HarvesterAirgapUpgrade', name: 'HarvesterAirgapUpgrade',
components: { components: {
Checkbox, CruResource, LabeledSelect, LabeledInput, RadioGroup, UpgradeInfo Checkbox, CruResource, LabeledSelect, LabeledInput, RadioGroup, UpgradeInfo, ImagePercentageBar, Banner
}, },
inheritAttrs: false, inheritAttrs: false,
@ -58,16 +59,24 @@ export default {
this.imageValue = imageValue; this.imageValue = imageValue;
}, },
beforeUnmount() {
if (this.uploadController) {
this.uploadController.abort();
}
},
data() { data() {
return { return {
value: null, value: null,
file: {}, file: {},
imageId: '', uploadImageId: '',
imageSource: IMAGE_METHOD.NEW, imageId: '',
sourceType: UPLOAD, imageSource: IMAGE_METHOD.NEW,
imageValue: null, sourceType: UPLOAD,
errors: [], uploadController: null,
enableLogging: true, imageValue: null,
errors: [],
enableLogging: true,
IMAGE_METHOD IMAGE_METHOD
}; };
}, },
@ -100,10 +109,43 @@ export default {
canEnableLogging() { canEnableLogging() {
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG); 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: { methods: {
done() { done() {
if (this.uploadController) {
this.uploadController.abort();
}
this.$router.push({ this.$router.push({
name: this.doneRoute, name: this.doneRoute,
params: { resource: HCI.SETTING, product: 'harvester' } params: { resource: HCI.SETTING, product: 'harvester' }
@ -125,24 +167,8 @@ export default {
if (this.imageSource === IMAGE_METHOD.NEW) { if (this.imageSource === IMAGE_METHOD.NEW) {
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True'; this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
if (this.sourceType === UPLOAD) { if (this.sourceType === UPLOAD && this.uploadImageId !== '') {
this.imageValue.spec.sourceType = UPLOAD; this.value.spec.image = this.uploadImageId;
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);
} else if (this.sourceType === DOWNLOAD) { } else if (this.sourceType === DOWNLOAD) {
this.imageValue.spec.sourceType = DOWNLOAD; this.imageValue.spec.sourceType = DOWNLOAD;
if (!this.imageValue.spec.url) { if (!this.imageValue.spec.url) {
@ -153,9 +179,8 @@ export default {
} }
res = await this.imageValue.save(); res = await this.imageValue.save();
this.value.spec.image = res.id;
} }
this.value.spec.image = res.id;
} else if (this.imageSource === IMAGE_METHOD.EXIST) { } else if (this.imageSource === IMAGE_METHOD.EXIST) {
if (!this.imageId) { if (!this.imageId) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile')); 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.file = this.$refs.file.files[0];
this.errors = [];
await this.uploadFile(this.file);
}, },
selectFile() { selectFile() {
@ -196,15 +252,15 @@ export default {
const splitName = suffixName?.split('.') || []; const splitName = suffixName?.split('.') || [];
const fileSuffix = splitName?.pop()?.toLowerCase(); 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; this.imageValue.spec.displayName = suffixName;
} }
}, },
deep: true deep: true
}, },
file(neu) { 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; this.imageValue.spec.displayName = neu.name;
} }
} }
@ -227,6 +283,7 @@ export default {
:errors="errors" :errors="errors"
:can-yaml="false" :can-yaml="false"
finish-button-mode="upgrade" finish-button-mode="upgrade"
:validation-passed="enableSave"
:cancel-event="true" :cancel-event="true"
@finish="save" @finish="save"
@cancel="done" @cancel="done"
@ -244,12 +301,16 @@ export default {
t('harvester.upgradePage.selectExisting'), t('harvester.upgradePage.selectExisting'),
]" ]"
/> />
<Banner
v-if="showUploadingWarningBanner"
color="warning"
:label="t('harvester.image.warning.osUpgrade.uploading', { name: file.name })"
/>
<UpgradeInfo /> <UpgradeInfo />
<div v-if="uploadImage"> <div v-if="uploadImage">
<LabeledInput <LabeledInput
v-model.trim="imageValue.spec.displayName" v-model:value.trim="imageValue.spec.displayName"
class="mb-20" class="mb-20"
label-key="harvester.fields.name" label-key="harvester.fields.name"
required required
@ -285,7 +346,7 @@ export default {
<LabeledInput <LabeledInput
v-if="sourceType === 'download'" v-if="sourceType === 'download'"
v-model.trim="imageValue.spec.url" v-model:value.trim="imageValue.spec.url"
class="labeled-input--tooltip" class="labeled-input--tooltip"
required required
label-key="harvester.image.url" label-key="harvester.image.url"
@ -298,6 +359,7 @@ export default {
<button <button
type="button" type="button"
class="btn role-primary" class="btn role-primary"
:disabled="disableUploadButton"
@click="selectFile" @click="selectFile"
> >
{{ t('harvester.image.uploadFile') }} {{ t('harvester.image.uploadFile') }}
@ -318,6 +380,11 @@ export default {
{{ fileName ? fileName : t('harvester.generic.noFileChosen') }} {{ fileName ? fileName : t('harvester.generic.noFileChosen') }}
</span> </span>
</div> </div>
<ImagePercentageBar
v-if="showProgressBar"
class="mt-20"
:value="uploadProgress"
/>
</div> </div>
<LabeledSelect <LabeledSelect
@ -334,12 +401,17 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
#air-gap { #air-gap {
padding: 20px;
:deep() .image-group .radio-group { :deep() .image-group .radio-group {
display: flex; display: flex;
.radio-container { .radio-container {
margin-right: 30px; margin-right: 30px;
} }
} }
.parent {
grid-template-columns:auto 40px;
}
.chooseFile { .chooseFile {
display: flex; display: flex;
align-items: center; align-items: center;