From 0781bde1885169213b6d68cd8f8d2c0a054a4f24 Mon Sep 17 00:00:00 2001 From: Jack Yu Date: Mon, 26 Jan 2026 14:07:28 +0800 Subject: [PATCH] feat: add warning message when disabling a device that have not been detached in the backend (#675) * feat: add wanring message when disabling a device that haven not been detached in the backend Signed-off-by: Jack Yu * fix: remove unused en-us key Signed-off-by: Jack Yu --------- Signed-off-by: Jack Yu --- .../VirtualMachinePciDevices/index.vue | 5 +++++ .../VirtualMachineUSBDevices/index.vue | 7 ++++++ .../VirtualMachineVGpuDevices/index.vue | 7 ++++++ pkg/harvester/l10n/en-us.yaml | 9 ++++++++ .../devices.harvesterhci.io.pcidevice.vue | 4 ++++ .../devices.harvesterhci.io.usbdevice.vue | 8 ++++++- .../devices.harvesterhci.io.vgpudevice.vue | 4 ++++ .../devices.harvesterhci.io.pcidevice.js | 22 +++++++++++++++++++ .../devices.harvesterhci.io.usbdevice.js | 22 +++++++++++++++++++ .../devices.harvesterhci.io.vgpudevice.js | 22 +++++++++++++++++++ 10 files changed, 109 insertions(+), 1 deletion(-) diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachinePciDevices/index.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachinePciDevices/index.vue index 4db1987d..e3534f89 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachinePciDevices/index.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachinePciDevices/index.vue @@ -54,6 +54,11 @@ export default { const vmDevices = this.value?.domain?.devices?.hostDevices || []; const otherDevices = this.otherDevices(vmDevices).map(({ name }) => name); + const vmDeviceNames = vmDevices.map(({ name }) => name); + + this.pciDevices.forEach((row) => { + row.allowDisable = !vmDeviceNames.includes(row.metadata.name); + }); vmDevices.forEach(({ name, deviceName }) => { const checkName = (deviceName || '').split('/')?.[1]; diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/index.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/index.vue index 56646a96..a2afacca 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/index.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/index.vue @@ -48,6 +48,13 @@ export default { this[key] = res[key]; } + const vmDevices = this.value?.domain?.devices?.hostDevices || []; + const vmDeviceNames = vmDevices.map(({ name }) => name); + + this.devices.forEach((row) => { + row.allowDisable = !vmDeviceNames.includes(row.metadata.name); + }); + this.selectedDevices = (this.value?.domain?.devices?.hostDevices || []) .map(({ name }) => name) .filter((name) => this.enabledDevices.find((device) => device?.metadata?.name === name)); diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVGpuDevices/index.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVGpuDevices/index.vue index fe3ed759..77ec1b1a 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVGpuDevices/index.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVGpuDevices/index.vue @@ -46,6 +46,13 @@ export default { this[key] = res[key]; } + const vmDevices = this.value?.domain?.devices?.gpus || []; + const vmDeviceNames = vmDevices.map(({ name }) => name); + + this.devices.forEach((row) => { + row.allowDisable = !vmDeviceNames.includes(row.metadata.name); + }); + const vGpus = this.vm.isOff ? [ ...(this.value?.domain?.devices?.gpus || []).map(({ name }) => name), ] : [ diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index 8979af8d..bd8db38d 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -369,6 +369,9 @@ harvester: claimError: Error enabling passthrough on {name} unclaimError: Error disabling passthrough on {name} cantUnclaim: You cannot disable passthrough on a device claimed by another user. + detachWarning: + title: Cannot Disable Passthrough + message: Please detach the device from the VM and save it first before disabling passthrough. enableGroup: Enable Group disableGroup: Disable Group labelRequired: "This rule should not be manually altered: it ensures that the PCI devices selected for this virtual machine are available on the virtual machine's host." @@ -1821,6 +1824,9 @@ harvester: vgpu: label: vGPU Devices noPermission: Please contact system administrator to add Harvester add-ons first. + detachWarning: + title: Cannot Disable vGPU + message: Please detach the device from the VM and save it first before disabling this vGPU device. goSetting: prefix: The nvidia-driver-toolkit add-on is not enabled, click middle: here @@ -1855,6 +1861,9 @@ harvester: claimError: Error enabling passthrough on {name} unclaimError: Error disabling passthrough on {name} cantUnclaim: You cannot disable passthrough on a device claimed by another user. + detachWarning: + title: Cannot Disable Passthrough + message: Please detach the device from the VM and save it first before disabling passthrough. enablePassthroughWarning: 'Please re-enable the USB device if the device path changes in the following situations:
 1) Re-plugging the USB device.
 2) Rebooting the node.

An incorrect device path may cause passthrough to fail.' harvesterVlanConfigMigrateDialog: diff --git a/pkg/harvester/list/devices.harvesterhci.io.pcidevice.vue b/pkg/harvester/list/devices.harvesterhci.io.pcidevice.vue index ddb62da8..3bf639c4 100644 --- a/pkg/harvester/list/devices.harvesterhci.io.pcidevice.vue +++ b/pkg/harvester/list/devices.harvesterhci.io.pcidevice.vue @@ -64,6 +64,10 @@ export default { const inStore = this.$store.getters['currentProduct'].inStore; const rows = this.$store.getters[`${ inStore }/all`](HCI.PCI_DEVICE); + rows.forEach((row) => { + row.allowDisable = true; + }); + return rows; } }, diff --git a/pkg/harvester/list/devices.harvesterhci.io.usbdevice.vue b/pkg/harvester/list/devices.harvesterhci.io.usbdevice.vue index e63b1db8..6a1ce306 100644 --- a/pkg/harvester/list/devices.harvesterhci.io.usbdevice.vue +++ b/pkg/harvester/list/devices.harvesterhci.io.usbdevice.vue @@ -54,7 +54,13 @@ export default { devices() { const inStore = this.$store.getters['currentProduct'].inStore; - return this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || []; + const data = this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || []; + + data.forEach((row) => { + row.allowDisable = true; + }); + + return data; } }, diff --git a/pkg/harvester/list/devices.harvesterhci.io.vgpudevice.vue b/pkg/harvester/list/devices.harvesterhci.io.vgpudevice.vue index 53bfa358..80dff8fa 100644 --- a/pkg/harvester/list/devices.harvesterhci.io.vgpudevice.vue +++ b/pkg/harvester/list/devices.harvesterhci.io.vgpudevice.vue @@ -65,6 +65,10 @@ export default { const vGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.VGPU_DEVICE) || []; const srioVGpuDevices = this.$store.getters[`${ inStore }/all`](HCI.SR_IOVGPU_DEVICE) || []; + vGpuDevices.forEach((row) => { + row.allowDisable = true; + }); + if (this.hasSRIOVGPUSchema) { return vGpuDevices.filter((device) => !!srioVGpuDevices.find((s) => s.isEnabled && s.spec?.nodeName === device.spec?.nodeName)); } diff --git a/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js b/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js index 26104a7e..6b83ca19 100644 --- a/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js +++ b/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js @@ -144,6 +144,12 @@ export default class PCIDevice extends SteveModel { // 'disable' passthrough deletes claim // backend should return error if device is in use async disablePassthrough() { + if (!this.allowDisable) { + this.showDetachWarning(); + + return; + } + try { if (!this.claimedByMe) { throw new Error(this.$rootGetters['i18n/t']('harvester.pci.cantUnclaim', { name: escapeHtml(this.metadata.name) })); @@ -169,4 +175,20 @@ export default class PCIDevice extends SteveModel { get groupByDevice() { return this.status?.description; } + + showDetachWarning() { + this.$dispatch('growl/warning', { + title: this.$rootGetters['i18n/t']('harvester.pci.detachWarning.title'), + message: this.$rootGetters['i18n/t']('harvester.pci.detachWarning.message'), + timeout: 5000 + }, { root: true }); + } + + get allowDisable() { + return this._allowDisable; + } + + set allowDisable(value) { + this._allowDisable = value; + } } diff --git a/pkg/harvester/models/devices.harvesterhci.io.usbdevice.js b/pkg/harvester/models/devices.harvesterhci.io.usbdevice.js index c6a995c1..27ab6ccf 100644 --- a/pkg/harvester/models/devices.harvesterhci.io.usbdevice.js +++ b/pkg/harvester/models/devices.harvesterhci.io.usbdevice.js @@ -133,6 +133,12 @@ export default class USBDevice extends SteveModel { // 'disable' passthrough deletes claim // backend should return error if device is in use async disablePassthrough() { + if (!this.allowDisable) { + this.showDetachWarning(); + + return; + } + try { if (!this.claimedByMe) { throw new Error(this.$rootGetters['i18n/t']('harvester.usb.cantUnclaim', { name: escapeHtml(this.metadata.name) })); @@ -158,4 +164,20 @@ export default class USBDevice extends SteveModel { get groupByDevice() { return this.status?.description; } + + showDetachWarning() { + this.$dispatch('growl/warning', { + title: this.$rootGetters['i18n/t']('harvester.usb.detachWarning.title'), + message: this.$rootGetters['i18n/t']('harvester.usb.detachWarning.message'), + timeout: 5000 + }, { root: true }); + } + + get allowDisable() { + return this._allowDisable; + } + + set allowDisable(value) { + this._allowDisable = value; + } } diff --git a/pkg/harvester/models/devices.harvesterhci.io.vgpudevice.js b/pkg/harvester/models/devices.harvesterhci.io.vgpudevice.js index f529bb52..3db4e2cb 100644 --- a/pkg/harvester/models/devices.harvesterhci.io.vgpudevice.js +++ b/pkg/harvester/models/devices.harvesterhci.io.vgpudevice.js @@ -100,6 +100,12 @@ export default class VGpuDevice extends SteveModel { } async disableVGpu() { + if (!this.allowDisable) { + this.showDetachWarning(); + + return; + } + const { vGPUTypeName, enabled } = this.spec; try { @@ -126,4 +132,20 @@ export default class VGpuDevice extends SteveModel { get vGpuAvailableTypes() { return this.status?.availableTypes ? Object.keys(this.status.availableTypes) : []; } + + showDetachWarning() { + this.$dispatch('growl/warning', { + title: this.$rootGetters['i18n/t']('harvester.vgpu.detachWarning.title'), + message: this.$rootGetters['i18n/t']('harvester.vgpu.detachWarning.message'), + timeout: 5000 + }, { root: true }); + } + + get allowDisable() { + return this._allowDisable; + } + + set allowDisable(value) { + this._allowDisable = value; + } }