diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/DiskPerformanceOptions.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/DiskPerformanceOptions.vue index 7abbf961..c37c693a 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/DiskPerformanceOptions.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/DiskPerformanceOptions.vue @@ -40,6 +40,8 @@ export default { // Bus the disk used before the "High Performance" preset switched it to // virtio, so returning to "Default" can put it back. busBeforeHighProfile: null, + // True only while the user has explicitly picked "Custom" in the selector. + userSelectedCustom: false, }; }, @@ -103,13 +105,13 @@ export default { watch: { // Keep the profile selector in sync if the row is repopulated (e.g. editing an existing VM). 'value.cache'() { - this.profile = this.detectProfile(); + this.syncProfile(); }, 'value.io'() { - this.profile = this.detectProfile(); + this.syncProfile(); }, 'value.dedicatedIOThread'() { - this.profile = this.detectProfile(); + this.syncProfile(); }, }, @@ -128,6 +130,16 @@ export default { return CUSTOM; }, + syncProfile() { + // An explicit "Custom" choice sticks, even if the options happen to + // match the Default or High Performance preset. An inferred "Custom" + // still follows the disk values. + if (this.userSelectedCustom) { + return; + } + this.profile = this.detectProfile(); + }, + cacheLabel(value) { if (value === '') { return this.t('harvester.virtualMachine.volume.performance.cacheMode.default'); @@ -140,6 +152,7 @@ export default { const previous = this.profile; this.profile = profile; + this.userSelectedCustom = profile === CUSTOM; if (profile === DEFAULT) { this.value.cache = ''; @@ -231,7 +244,7 @@ export default {
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/VMPerformanceOptions.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/VMPerformanceOptions.vue index 08491e4b..29de9c19 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/VMPerformanceOptions.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/VMPerformanceOptions.vue @@ -135,7 +135,7 @@ export default {
@@ -171,7 +171,7 @@ export default { diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/__tests__/DiskPerformanceOptions.test.ts b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/__tests__/DiskPerformanceOptions.test.ts index 59f20006..9e659173 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/__tests__/DiskPerformanceOptions.test.ts +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/__tests__/DiskPerformanceOptions.test.ts @@ -1,5 +1,7 @@ import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { _EDIT } from '@shell/config/query-params'; +import { Banner } from '@components/Banner'; import DiskPerformanceOptions from '../DiskPerformanceOptions.vue'; const mountOptions = (value, { featureEnabled = true } = {}) => ({ @@ -122,4 +124,59 @@ describe('component: DiskPerformanceOptions', () => { expect(disabledByValue['writeback']).toBe(true); expect(disabledByValue['writethrough']).toBe(true); }); + + it.each([ + ['Default', { + cache: '', io: '', dedicatedIOThread: false + }], + ['High Performance', { + cache: 'none', io: 'native', dedicatedIOThread: true + }], + ])('stays on Custom when the options match the %s preset', async(_label, opts) => { + const value = hardDisk({ cache: 'writeback' }); + const wrapper = mount(DiskPerformanceOptions, mountOptions(value)); + + wrapper.vm.onProfileChange('custom'); + await wrapper.setProps({ value: { ...value, ...opts } }); + await nextTick(); + + expect(wrapper.vm.profile).toBe('custom'); + }); + + it('detects the profile from an existing disk', async() => { + const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk())); + + expect(wrapper.vm.profile).toBe('default'); + + await wrapper.setProps({ + value: hardDisk({ + cache: 'none', io: 'native', dedicatedIOThread: true + }) + }); + + expect(wrapper.vm.profile).toBe('highPerformance'); + }); + + it('updates an automatically detected Custom profile when preset values are loaded', async() => { + const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk({ cache: 'writeback' }))); + + expect(wrapper.vm.profile).toBe('custom'); + + await wrapper.setProps({ + value: hardDisk({ + cache: 'none', io: 'native', dedicatedIOThread: true + }) + }); + + expect(wrapper.vm.profile).toBe('highPerformance'); + }); + + it('passes the Native I/O hint as a label key so its quotes are not double-escaped', () => { + const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk({ io: 'native' }))); + const banner = wrapper.findAllComponents(Banner) + .find((b) => b.props('labelKey') === 'harvester.virtualMachine.volume.performance.ioMode.nativeRequiresNoCacheTip'); + + expect(banner).toBeDefined(); + expect(banner?.props('label')).toBeFalsy(); + }); }); diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/__tests__/VMPerformanceOptions.test.js b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/__tests__/VMPerformanceOptions.test.js new file mode 100644 index 00000000..f8c0efe1 --- /dev/null +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/__tests__/VMPerformanceOptions.test.js @@ -0,0 +1,61 @@ +import { mount } from '@vue/test-utils'; +import { _CREATE } from '@shell/config/query-params'; +import { Banner } from '@components/Banner'; +import VMPerformanceOptions from '../VMPerformanceOptions.vue'; + +const AUTO_ENABLED_TIP = 'harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.autoEnabledTip'; +const NO_VIRTIO_TIP = 'harvester.virtualMachine.volume.diskPerformance.blockMultiQueue.noVirtioTip'; + +function mountOptions(props = {}) { + return mount(VMPerformanceOptions, { + props: { mode: _CREATE, ...props }, + global: { + mocks: { + $store: { + getters: { + 'harvester-common/getFeatureEnabled': () => true, + 'i18n/t': (key) => key, + 'i18n/exists': () => true, + } + }, + t: (key) => key, + }, + }, + }); +} + +async function expand(wrapper) { + await wrapper.find('.expand-toggle').trigger('click'); +} + +function bannerFor(wrapper, labelKey) { + return wrapper.findAllComponents(Banner).find((b) => b.props('labelKey') === labelKey); +} + +describe('component: VMPerformanceOptions', () => { + it('should pass the I/O threads policy hint as a label key so its quotes are not double-escaped', async() => { + const wrapper = mountOptions({ hasDedicatedIothread: true }); + + await expand(wrapper); + const banner = bannerFor(wrapper, AUTO_ENABLED_TIP); + + expect(banner).toBeDefined(); + expect(banner.props('label')).toBeFalsy(); + }); + + it('should not show the I/O threads policy hint when a policy is set', async() => { + const wrapper = mountOptions({ hasDedicatedIothread: true, ioThreadsPolicy: 'shared' }); + + expect(bannerFor(wrapper, AUTO_ENABLED_TIP)).toBeUndefined(); + }); + + it('should pass the no virtio disk hint as a label key', async() => { + const wrapper = mountOptions({ hasVirtioDisk: false }); + + await expand(wrapper); + const banner = bannerFor(wrapper, NO_VIRTIO_TIP); + + expect(banner).toBeDefined(); + expect(banner.props('label')).toBeFalsy(); + }); +});