Compare commits

...
21 Commits
Author SHA1 Message Date
mergify[bot]andAndy Lee 8fbe1943d8 chore: bump version to v1.7.0 (#658) (#659)
(cherry picked from commit a86302c9d5)

Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-12-22 13:41:18 +08:00
mergify[bot]andfreeze ec6bc4d639 chore: bump version to v1.7.0-rc7 (#656) (#657)
(cherry picked from commit 5fe7e13fcd)

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
2025-12-16 20:27:51 +08:00
renovate[bot] 3824a14730 deps: update dependency @types/node to v20.19.27 (#655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-14 08:59:31 +00:00
mergify[bot]andAndy Lee 0fc8bece02 chore: bump version to v1.7.0-rc6 (#649) (#650)
(cherry picked from commit 5769588633)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-12-11 16:50:51 +08:00
mergify[bot]andAndy Lee 39764af627 fix: failed to create multiple VMs (#647) (#648)
(cherry picked from commit b29950f99c)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-12-11 16:17:02 +08:00
mergify[bot]andAndy Lee bdc87bda0e fix: do not inherit template secret when creating new VM (#643) (#646)
(cherry picked from commit 6c27a46274)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-12-10 17:18:33 +08:00
mergify[bot]andAndy Lee e0dc77624b feat: read addon displayname from label and add descheduler description (#644) (#645)
* refactor: display addon name from addon.harvesterhci.io/displayName label



* refactor: add descheduler description



---------


(cherry picked from commit b03fffbc30)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-12-10 15:16:08 +08:00
renovate[bot] c3ba10bd22 deps: update dependency node-forge to v1.3.3 (#641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-07 09:15:44 +00:00
mergify[bot]andfreeze 5b7d54d0a3 chore: bump to v1.7.0-rc5 (#636) (#637)
(cherry picked from commit 416098ffd8)

Signed-off-by: Vicente Cheng <vicente.cheng@suse.com>
Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com>
2025-12-04 22:45:15 +08:00
renovate[bot] 99a216dfa0 deps: update dependency yaml to v2.8.2 (#634)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-30 16:55:46 +00:00
mergify[bot]andAndy Lee e4c85f510e chore: bump to v1.7.0-rc4 (#621) (#629)
(cherry picked from commit 3d7b96d86d)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-28 13:23:55 +08:00
mergify[bot]andCaio Torres f391f018de fix: create new secret on vm creation (#614) (#628)
* fix: create new secret on vm creation



* fix: ensure parseVM result is immutable



---------


(cherry picked from commit 0b37467f76)

Signed-off-by: Caio Torres <caio.torres@suse.com>
Co-authored-by: Caio Torres <caio.torres@suse.com>
2025-11-27 17:42:46 +08:00
renovate[bot] 4b2e92ea15 deps: update dependency node-forge to v1.3.2 [security] (#627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-27 14:31:50 +08:00
mergify[bot]andAndy Lee 2f956d5946 feat: allow user to attach volume to muliple VMs (#620) (#622)
(cherry picked from commit d94003f8c2)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-27 14:30:44 +08:00
renovate[bot] 5f8d556ea2 deps: update patch digest dependencies (#619)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-23 09:32:21 +00:00
mergify[bot] 396ab48f1c chore: bump version to v1.7.0-rc3 (#612) (#613) 2025-11-20 10:25:03 +08:00
mergify[bot]andYiya Chen 6b8c079018 fix: condition render namespaceOptions (#607) (#611)
(cherry picked from commit 1b183febdc)

Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-11-19 17:40:03 +08:00
mergify[bot]andAndy Lee 7f638e86c8 fix: change migConfiguration model to inherit from harvester resource (#608) (#609)
(cherry picked from commit 70d3b656f7)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-19 17:35:52 +08:00
mergify[bot]andAndy Lee 0c4955a766 feat: create related image storageclass before OS upgrade (#595) (#606)
* feat: create related image SC before upgrade



* refactor: update spec.targetStorageClassName



* refactor: based on comment



---------


(cherry picked from commit 10d19cd329)

Signed-off-by: Andy Lee <andy.lee@suse.com>
Co-authored-by: Andy Lee <andy.lee@suse.com>
2025-11-17 17:34:51 +08:00
mergify[bot]andYiya Chen 3f4ff30275 feat: modified placeholder (#599) (#605)
(cherry picked from commit 1715ae754c)

Co-authored-by: Yiya Chen <yiya.chen@suse.com>
2025-11-17 17:08:20 +08:00
renovate[bot] 8e0332a364 deps: update dependency @types/node to v20.19.25 (#604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-16 13:03:17 +00:00
15 changed files with 224 additions and 741 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "harvester-ui-extension",
"version": "1.7.0-rc2",
"version": "1.7.0",
"private": false,
"engines": {
"node": ">=20.0.0"
@@ -24,9 +24,9 @@
"glob": "7.2.3",
"glob-parent": "6.0.2",
"json5": "2.2.3",
"@types/lodash": "4.17.20",
"@types/lodash": "4.17.21",
"merge": "2.1.1",
"node-forge": "1.3.1",
"node-forge": "1.3.3",
"nth-check": "2.1.1",
"qs": "6.14.0",
"roarr": "7.21.2",
@@ -18,16 +18,20 @@ export default {
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
const response = await this.$store.dispatch('harvester/request', { url });
if (this.customSupportBundleFeatureEnabled) {
try {
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
const response = await this.$store.dispatch('harvester/request', { url });
this.defaultNamespaces = response.data || [];
} catch (error) {
this.defaultNamespaces = response.data || [];
} catch (error) {
this.defaultNamespaces = [];
}
} else {
this.defaultNamespaces = [];
} finally {
this.loading = false;
}
this.loading = false;
},
data() {
@@ -42,24 +46,38 @@ export default {
},
computed: {
customSupportBundleFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
},
allNamespaces() {
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
},
filteredNamespaces() {
if (!this.customSupportBundleFeatureEnabled) {
return this.allNamespaces;
}
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
},
namespaceOptions() {
const mappedNamespaces = this.filteredNamespaces.map((ns) => ({ label: ns, value: ns }));
if (!this.customSupportBundleFeatureEnabled) {
return mappedNamespaces;
}
const allSelected =
this.namespaces.length === this.filteredNamespaces.length &&
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
return [controlOption, ...this.filteredNamespaces];
return [controlOption, ...mappedNamespaces];
}
},
@@ -50,6 +50,7 @@ export const HCI = {
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
ADDON_DISPLAYNAME: 'addon.harvesterhci.io/displayName',
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
@@ -32,7 +32,7 @@ export default {
resources: {
type: Array,
required: true
}
},
},
data() {
@@ -43,7 +43,7 @@ export default {
...mapState('action-menu', ['modalData']),
title() {
return this.modalData.title || 'dialog.promptRemove.title';
return this.modalData?.title || 'dialog.promptRemove.title';
},
formattedType() {
@@ -51,7 +51,7 @@ export default {
},
warningMessage() {
if (this.modalData.warningMessage) return this.modalData.warningMessage;
if (this.modalData?.warningMessage) return this.modalData.warningMessage;
const isPlural = this.type.endsWith('s');
const thisOrThese = isPlural ? 'these' : 'this';
@@ -145,6 +145,7 @@ export default {
try {
for (const resource of this.resources) {
await resource.remove();
if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
}
buttonDone(true);
this.close();
@@ -62,7 +62,7 @@ export default {
return false;
}
return !pvc.attachVM;
return true;
})
.map((pvc) => {
return {
@@ -371,7 +371,7 @@ export default {
<div class="key">
<input
v-model="scope.row.value"
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
:placeholder="t('harvester.subnet.excludeIPs.placeholder')"
/>
</div>
</template>
@@ -99,7 +99,6 @@ export default {
this.allPVCs
.filter( (pvc) => {
let isAvailable = true;
let isBeingUsed = false;
this.rows.forEach( (O) => {
if (O.volumeName === pvc.metadata.name) {
@@ -111,17 +110,16 @@ export default {
return false;
}
// already used as image volume
if (this.idx > 0 && pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
return false;
}
if (pvc.isGoldenImageVolume) {
return false;
}
if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) {
isBeingUsed = false;
} else if (pvc.attachVM) {
isBeingUsed = true;
}
return isAvailable && !isBeingUsed && pvc.isAvailable;
return isAvailable && pvc.isAvailable;
})
.map((pvc) => {
return {
@@ -96,7 +96,6 @@ export default {
templateVersionId: '',
namePrefix: '',
isSingle: true,
useTemplate: false,
isOpen: false,
hostname,
isRestartImmediately,
@@ -490,6 +489,7 @@ export default {
if (this.isSingle) {
if (!this.value.spec.template.spec.hostname) {
this.value.spec.template.spec['hostname'] = this.value.metadata.name;
this.spec.template.spec['hostname'] = this.value.metadata.name;
}
}
+4 -1
View File
@@ -1089,6 +1089,8 @@ harvester:
placeholder: e.g. 172.16.0.0/16
excludeIPs:
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
placeholder: single IP or 192.168.0.100..192.168.0.200
acl:
label: Access Control List
tooltip: The ACL to apply to this Subnet. Must be one of the ACLs in the same namespace.
@@ -1204,7 +1206,7 @@ harvester:
vlan: VLAN ID
exclude:
label: Exclude IPs
placeholder: e.g. 172.16.0.1
placeholder: CIDR format, e.g. 172.16.0.10/32
invalid: '"Exclude list" is invalid.'
addIp: Add Exclude IP
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
@@ -1559,6 +1561,7 @@ harvester:
'harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
'harvester-system/harvester-seeder': harvester-seeder is an add-on that uses IPMI and Redfish to discover hardware information and perform out-of-band operations.
'harvester-csi-driver-lvm': harvester-csi-driver-lvm is an add-on allowing users to create PVC through the LVM with local devices.
'descheduler': 'The virtual machine auto balance optimizes workload scheduling by evicting pods that are not optimally placed according to administrator-defined policies.'
vmImport:
titles:
basic: Basic
+19 -12
View File
@@ -142,6 +142,7 @@ export default {
showYaml: false,
spec: null,
osType: 'linux',
useTemplate: false,
sshKey: [],
maintenanceStrategies,
maintenanceStrategy: 'Migrate',
@@ -273,7 +274,7 @@ export default {
needNewSecret() {
// When creating a template it is always necessary to create a new secret.
return this.isCreate ? true : this.showYaml ? false : this.resourceType === HCI.VM_VERSION;
return this.isCreate || this.showYaml ? false : this.resourceType === HCI.VM_VERSION;
},
defaultTerminationSetting() {
@@ -709,16 +710,7 @@ export default {
disk.forEach( (R, index) => {
const prefixName = this.value.metadata?.name || '';
let dataVolumeName = '';
if (R.source === SOURCE_TYPE.ATTACH_VOLUME) {
dataVolumeName = R.volumeName;
} else if (this.isClone || !this.hasCreateVolumes.includes(R.realName)) {
dataVolumeName = `${ prefixName }-${ R.name }-${ randomStr(5).toLowerCase() }`;
} else {
dataVolumeName = R.realName;
}
const dataVolumeName = this.parseDataVolumeName(R, prefixName);
const _disk = this.parseDisk(R, index);
const _volume = this.parseVolume(R, dataVolumeName);
@@ -1013,6 +1005,20 @@ export default {
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
},
parseDataVolumeName(R, prefixName) {
let dataVolumeName = '';
if (R.source === SOURCE_TYPE.ATTACH_VOLUME) {
dataVolumeName = R.volumeName;
} else if (this.isClone || !this.hasCreateVolumes.includes(R.realName)) {
dataVolumeName = `${ prefixName }-${ R.name }-${ randomStr(5).toLowerCase() }`;
} else {
dataVolumeName = R.realName;
}
return dataVolumeName;
},
parseDisk(R, index) {
const out = { name: R.name };
@@ -1638,7 +1644,8 @@ export default {
secretRef: {
handler(secret) {
if (secret && this.resourceType !== HCI.BACKUP) {
// we should not inherit the secret if it's from VM template.
if (secret && this.resourceType !== HCI.BACKUP && !this.useTemplate) {
this.secretName = secret?.metadata.name;
}
},
@@ -1,12 +1,12 @@
import SteveModel from '@shell/plugins/steve/steve-class';
import { escapeHtml } from '@shell/utils/string';
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
import HarvesterResource from './harvester';
/**
* Class representing vGPU MIGConfiguration resource.
* @extends SteveModal
* @extends HarvesterResource
*/
export default class MIGCONFIGURATION extends SteveModel {
export default class MIGCONFIGURATION extends HarvesterResource {
get _availableActions() {
let out = super._availableActions;
@@ -113,8 +113,9 @@ export default class HciAddonConfig extends HarvesterResource {
get displayName() {
const isExperimental = this.metadata?.labels?.[HCI_ANNOTATIONS.ADDON_EXPERIMENTAL] === 'true';
const name = this.metadata?.labels?.[HCI_ANNOTATIONS.ADDON_DISPLAYNAME] || this.metadata.name;
return isExperimental ? `${ this.metadata.name } (${ this.t('generic.experimental') })` : this.metadata.name;
return isExperimental ? `${ name } (${ this.t('generic.experimental') })` : name;
}
get customValidationRules() {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "harvester",
"description": "Rancher UI Extension for Harvester",
"version": "1.7.0-rc2",
"version": "1.7.0",
"private": false,
"rancher": {
"annotations": {
@@ -12,6 +12,7 @@ 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';
import { STORAGE_CLASS } from '@shell/config/types';
const IMAGE_METHOD = {
NEW: 'new',
@@ -32,6 +33,7 @@ export default {
async fetch() {
await this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE });
await this.$store.dispatch('harvester/findAll', { type: STORAGE_CLASS });
const value = await this.$store.dispatch('harvester/create', {
type: HCI.UPGRADE,
@@ -63,6 +65,7 @@ export default {
sourceType: UPLOAD,
uploadController: null,
uploadResult: null,
storageClassValue: null,
imageValue: null,
enableLogging: true,
IMAGE_METHOD,
@@ -79,7 +82,6 @@ export default {
skipSingleReplicaDetachedVolFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
},
allOSImages() {
return this.$store.getters['harvester/all'](HCI.IMAGE).filter((I) => I.isOSImage) || [];
},
@@ -116,7 +118,7 @@ export default {
},
fileName() {
return this.file?.name || '';
return this.preprocessImageName(this.file?.name || '');
},
canEnableLogging() {
@@ -181,6 +183,38 @@ export default {
});
},
async createImageStorageClass(imageName = '') {
// delete related SC if existed
await this.deleteImageStorageClass(imageName);
const storageClassPayload = {
apiVersion: 'storage.k8s.io/v1',
type: STORAGE_CLASS,
metadata: { name: imageName },
volumeBindingMode: 'Immediate',
reclaimPolicy: 'Delete',
allowVolumeExpansion: true, // must be boolean type
provisioner: 'driver.longhorn.io',
};
this.storageClassValue = await this.$store.dispatch('harvester/create', storageClassPayload);
if (this.storageClassValue && this.storageClassValue.save) {
await this.storageClassValue.save();
}
},
async deleteImageStorageClass(imageName = '') {
const inStore = this.$store.getters['currentProduct'].inStore;
const storageClasses = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
const targetSC = storageClasses.find((sc) => sc.id === imageName);
if (targetSC && targetSC.remove) {
await targetSC.remove();
}
},
async initImageValue() {
this.imageValue = await this.$store.dispatch('harvester/create', {
type: HCI.IMAGE,
@@ -191,6 +225,7 @@ export default {
annotations: {}
},
spec: {
backend: 'cdi',
sourceType: UPLOAD,
displayName: '',
checksum: this.imageValue?.spec?.checksum || '',
@@ -203,8 +238,9 @@ export default {
this.file = {};
this.errors = [];
const imageDisplayName = this.imageValue?.spec?.displayName || '';
if (!this.imageValue.spec.displayName && this.createNewImage) {
if (!imageDisplayName && this.createNewImage) {
this.errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('generic.name') }));
buttonCb(false);
@@ -212,24 +248,31 @@ export default {
}
try {
// Save the image first if creating a new one
if (this.imageSource === IMAGE_METHOD.NEW) {
this.imageValue.metadata.annotations[HCI_ANNOTATIONS.OS_UPGRADE_IMAGE] = 'True';
if (this.sourceType === UPLOAD && this.uploadImageId !== '') { // upload new image
this.value.spec.image = this.uploadImageId;
} else if (this.sourceType === DOWNLOAD) { // give URL to download new image
this.imageValue.spec.sourceType = DOWNLOAD;
// check if URL is provided
if (!this.imageValue.spec.url) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.imageUrl'));
buttonCb(false);
return;
}
// create related image storage class first
await this.createImageStorageClass(imageDisplayName);
this.imageValue.spec.sourceType = DOWNLOAD;
this.imageValue.spec.targetStorageClassName = imageDisplayName;
res = await this.imageValue.save();
this.value.spec.image = res.id;
}
} else if (this.imageSource === IMAGE_METHOD.EXIST) {
} else if (this.imageSource === IMAGE_METHOD.EXIST) { // select existing image
if (!this.imageId) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.chooseFile'));
buttonCb(false);
@@ -239,7 +282,7 @@ export default {
this.value.spec.image = this.imageId;
}
// enable logging or skip single replica detection if checked
if (this.canEnableLogging) {
this.value.spec.logEnabled = this.enableLogging;
}
@@ -252,11 +295,13 @@ export default {
} catch (e) {
this.errors = [e?.message] || exceptionToErrorsArray(e);
buttonCb(false);
// if anything failed, delete the created image storage class
await this.deleteImageStorageClass(imageDisplayName);
}
},
async uploadFile(file) {
const fileName = file.name;
const fileName = this.preprocessImageName(file.name);
if (!fileName) {
this.errors.push(this.$store.getters['i18n/t']('harvester.setting.upgrade.unknownImageName'));
@@ -280,6 +325,10 @@ export default {
this.imageValue.spec.url = '';
try {
// before uploading image, we need to create related image storage class first
await this.createImageStorageClass(fileName);
this.imageValue.spec.targetStorageClassName = fileName;
const res = await this.imageValue.save();
this.uploadImageId = res.id;
@@ -296,20 +345,35 @@ export default {
} else {
this.errors = exceptionToErrorsArray(e);
}
// if upload failed, delete the created image storage class
await this.deleteImageStorageClass(fileName);
this.file = {};
this.uploadImageId = '';
}
},
// replace _ to - to meet storage class name requirement
preprocessImageName(name) {
if (!name) {
return '';
}
return name.toLowerCase().replace(/[_]/g, '-');
},
handleImageDelete(imageId) {
const image = this.allOSImages.find((I) => I.id === imageId);
const imageDisplayName = image?.spec?.displayName || '';
if (image) {
if (image && imageDisplayName) {
this.$store.dispatch('harvester/promptModal', {
resources: [image],
component: 'ConfirmRelatedToRemoveDialog',
needConfirmation: false,
warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: image.displayName })
resources: [image],
component: 'ConfirmRelatedToRemoveDialog',
needConfirmation: false,
warningMessage: this.$store.getters['i18n/t']('harvester.modal.osImage.message', { name: imageDisplayName }),
extraActionAfterRemove: async() => {
await this.deleteImageStorageClass(imageDisplayName);
}
});
this.deleteImageId = '';
}
@@ -419,13 +483,13 @@ export default {
v-if="showUploadSuccessBanner"
color="success"
class="mt-0 mb-30"
:label="t('harvester.setting.upgrade.uploadSuccess', { name: file.name })"
:label="t('harvester.setting.upgrade.uploadSuccess', { name: fileName })"
/>
<Banner
v-if="showUploadingWarningBanner"
color="warning"
class="mt-0 mb-30"
:label="t('harvester.image.warning.osUpgrade.uploading', { name: file.name })"
:label="t('harvester.image.warning.osUpgrade.uploading', { name: fileName })"
/>
<div
+74 -684
View File
File diff suppressed because it is too large Load Diff