From 4aabf0b7a3efa89c3502d7d58cb23c96311237fb Mon Sep 17 00:00:00 2001 From: Andy Lee Date: Tue, 9 Sep 2025 14:36:23 +0800 Subject: [PATCH] fix: hide VM take backup action if backup target is not available (#512) * fix: hide VM take backup action if backup target is not available Signed-off-by: Andy Lee * refactor: use extracted func Signed-off-by: Andy Lee --------- Signed-off-by: Andy Lee --- .../edit/harvesterhci.io.schedulevmbackup.vue | 19 +++----------- .../harvesterhci.io.virtualmachinebackup.vue | 22 +++------------- .../models/kubevirt.io.virtualmachine.js | 10 +++++++- pkg/harvester/utils/setting.js | 25 +++++++++++++++++++ 4 files changed, 40 insertions(+), 36 deletions(-) create mode 100644 pkg/harvester/utils/setting.js diff --git a/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue b/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue index 685293af..06f1a6c6 100644 --- a/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue +++ b/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue @@ -15,6 +15,7 @@ import { HCI } from '../types'; import { sortBy } from '@shell/utils/sort'; import { BACKUP_TYPE } from '../config/types'; import { _EDIT, _CREATE } from '@shell/config/query-params'; +import { isBackupTargetSettingEmpty, isBackupTargetSettingUnavailable } from '../utils/setting'; export default { name: 'CreateVMSchedule', @@ -93,7 +94,7 @@ export default { return this.settings.find( (O) => O.id === 'backup-target'); }, isEmptyValue() { - return this.getBackupTargetValueIsEmpty(this.backupTargetResource); + return isBackupTargetSettingEmpty(this.backupTargetResource); }, canUpdate() { return this?.backupTargetResource?.canUpdate; @@ -109,7 +110,7 @@ export default { !!this.value.spec.maxFailure; }, isBackupTargetUnAvailable() { - return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && (this.errorMessage || this.isEmptyValue) && this.canUpdate; + return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && isBackupTargetSettingUnavailable(this.backupTargetResource); }, vmOptions() { const nsVmList = this.$store.getters['harvester/all'](HCI.VM).filter((vm) => vm.metadata.namespace === this.value.metadata.namespace); @@ -166,20 +167,6 @@ export default { onTypeChange(newType) { this.value.metadata.name = `svm${ newType }-${ this.value.spec.vmbackup.source.name }`; }, - getBackupTargetValueIsEmpty(resource) { - let out = true; - - if (resource?.value) { - try { - const valueJson = JSON.parse(resource?.value); - - out = !valueJson.type; - } catch (e) {} - } - - return out; - }, - validateFailure(count) { if (this.value.spec.retain && count > this.value.spec.retain) { this.value.spec['maxFailure'] = this.value.spec.retain; diff --git a/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue b/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue index 4851f252..1d6fd491 100644 --- a/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue +++ b/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue @@ -4,7 +4,7 @@ import Loading from '@shell/components/Loading'; import MessageLink from '@shell/components/MessageLink'; import Masthead from '@shell/components/ResourceList/Masthead'; import ResourceTable from '@shell/components/ResourceTable'; - +import { isBackupTargetSettingEmpty } from '../utils/setting'; import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers'; import FilterVMSchedule from '../components/FilterVMSchedule'; import { HCI } from '../types'; @@ -39,7 +39,7 @@ export default { this.settings = hash.settings; if (this.$store.getters[`${ inStore }/schemaFor`](HCI.SETTING)) { const backupTargetResource = hash.settings.find( (O) => O.id === 'backup-target'); - const isEmpty = this.getBackupTargetValueIsEmpty(backupTargetResource); + const isEmpty = isBackupTargetSettingEmpty(backupTargetResource); if (backupTargetResource && !isEmpty) { this.testConnect(); @@ -78,20 +78,6 @@ export default { } }, - getBackupTargetValueIsEmpty(resource) { - let out = true; - - if (resource?.value) { - try { - const valueJson = JSON.parse(resource?.value); - - out = !valueJson.type; - } catch (e) {} - } - - return out; - }, - getRow(row) { return row.status && row.status.source; }, @@ -183,11 +169,9 @@ export default { backupTargetResource() { return this.settings.find((O) => O.id === 'backup-target'); }, - isEmptyValue() { - return this.getBackupTargetValueIsEmpty(this.backupTargetResource); + return isBackupTargetSettingEmpty(this.backupTargetResource); }, - canUpdate() { return this?.backupTargetResource?.canUpdate; }, diff --git a/pkg/harvester/models/kubevirt.io.virtualmachine.js b/pkg/harvester/models/kubevirt.io.virtualmachine.js index 5b9fe2cb..7e538354 100644 --- a/pkg/harvester/models/kubevirt.io.virtualmachine.js +++ b/pkg/harvester/models/kubevirt.io.virtualmachine.js @@ -14,6 +14,7 @@ import { BACKUP_TYPE } from '../config/types'; import { HCI } from '../types'; import HarvesterResource from './harvester'; import { getVmCPUMemoryValues } from '../utils/cpuMemory'; +import { isBackupTargetSettingUnavailable } from '../utils/setting'; export const OFF = 'Off'; @@ -152,7 +153,7 @@ export default class VirtVm extends HarvesterResource { }, { action: 'backupVM', - enabled: !!this.actions?.backup, + enabled: !!this.actions?.backup && !this.isBackupTargetUnavailable, icon: 'icon icon-backup', label: this.t('harvester.action.backup') }, @@ -1239,6 +1240,13 @@ export default class VirtVm extends HarvesterResource { return this.$rootGetters['harvester-common/getFeatureEnabled']('vmMachineTypes'); } + get isBackupTargetUnavailable() { + const allSettings = this.$rootGetters['harvester/all'](HCI.SETTING) || []; + const backupTargetSetting = allSettings.find( (O) => O.id === 'backup-target'); + + return isBackupTargetSettingUnavailable(backupTargetSetting); + } + setInstanceLabels(val) { if ( !this.spec?.template?.metadata?.labels ) { set(this, 'spec.template.metadata.labels', {}); diff --git a/pkg/harvester/utils/setting.js b/pkg/harvester/utils/setting.js new file mode 100644 index 00000000..67954bfd --- /dev/null +++ b/pkg/harvester/utils/setting.js @@ -0,0 +1,25 @@ + +export function isBackupTargetSettingEmpty(setting = {}) { + let isEmpty = true; + + if (setting?.value) { + try { + const valueJson = JSON.parse(setting?.value); + + isEmpty = !valueJson.type; + } catch (e) { + // eslint-disable-next-line no-console + console.error('Failed to parse backup target value:', e); + } + } + + return isEmpty; +} + +export function isBackupTargetSettingUnavailable(setting = {}) { + const errorMessage = setting.errorMessage; + const isEmptyValue = isBackupTargetSettingEmpty(setting); + const canUpdate = setting.canUpdate; + + return (errorMessage || isEmptyValue) && canUpdate; +}