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();
+ });
+});