mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2025-12-13 13:11:43 +00:00
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:
parent
9e240792e8
commit
6d8f6579c7
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 });
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user