feat: align the storage performance options with the Rancher machine pool form (#1212)

* feat: align the storage performance options with the Rancher machine pool form

Show the storage performance profile on every volume instead of behind a
collapsible toggle, and move the VM-wide options to a plain Storage
Performance section in the Advanced Options tab. Put the Native I/O tip in
a live region so screen readers announce the cache mode change, use an
integer input for the I/O thread count, and use the same copy as the
Rancher form.

Skip the view-only backup row in the image and container volumes when it
has nothing to show, so it no longer adds an empty gap above the profile.

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* fix: announce the storage performance warnings and name the virtio requirement

- Put the bus tip and the Filesystem warning in an always-rendered status
  region. They appear above the field being changed, so screen readers need
  them announced.
- Say in the bus tip that a dedicated I/O thread needs the virtio bus, because
  KubeVirt rejects it on any other bus.
- Mark I/O Thread Count as required while editing, like the Rancher form.

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

* fix: state only the dedicated I/O thread requirement in the bus tip

Neither the Harvester docs nor KubeVirt tie cache mode or I/O mode to the bus,
so stop saying that those options work best on virtio.

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>

---------

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
marcelofukumoto
2026-11-05 18:51:46 +01:00
committed by GitHub
parent 840f9de73e
commit 12c28c08fa
14 changed files with 373 additions and 274 deletions
@@ -21,6 +21,7 @@ import { HCI } from '../types';
import VM_MIXIN from '../mixins/harvester-vm';
import Reserved from './kubevirt.io.virtualmachine/VirtualMachineReserved';
import Volume from './kubevirt.io.virtualmachine/VirtualMachineVolume';
import VMPerformanceOptions from './kubevirt.io.virtualmachine/VirtualMachineVolume/VMPerformanceOptions';
import Network from './kubevirt.io.virtualmachine/VirtualMachineNetwork';
import CpuMemory from './kubevirt.io.virtualmachine/VirtualMachineCpuMemory';
import CpuModel from './kubevirt.io.virtualmachine/VirtualMachineCpuModel';
@@ -37,6 +38,7 @@ export default {
Tab,
SSHKey,
Volume,
VMPerformanceOptions,
Tabbed,
Network,
Checkbox,
@@ -390,9 +392,6 @@ export default {
>
<Volume
v-model:value="diskRows"
v-model:block-multi-queue="blockMultiQueue"
v-model:io-threads-policy="ioThreadsPolicy"
v-model:io-thread-count="ioThreadCount"
:mode="mode"
:namespace="value.metadata.namespace"
:existing-volume-disabled="true"
@@ -578,6 +577,16 @@ export default {
</div>
</div>
<VMPerformanceOptions
v-model:block-multi-queue="blockMultiQueue"
v-model:io-threads-policy="ioThreadsPolicy"
v-model:io-thread-count="ioThreadCount"
:has-virtio-disk="hasVirtioDisk"
:has-dedicated-iothread="hasDedicatedIOThread"
:mode="mode"
class="mb-20"
/>
<CloudConfig
ref="yamlEditor"
:mode="mode"
@@ -2,7 +2,6 @@
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { Checkbox } from '@components/Form/Checkbox';
import { Banner } from '@components/Banner';
import { _VIEW } from '@shell/config/query-params';
import { DISK_CACHE_MODE, DISK_IO_MODE, DISK_PERFORMANCE_PROFILE } from '../../../config/harvester-map';
const { DEFAULT, HIGH, CUSTOM } = DISK_PERFORMANCE_PROFILE;
@@ -31,11 +30,8 @@ export default {
},
data() {
const hasPerf = !!(this.value.cache || this.value.io || this.value.dedicatedIOThread);
return {
DISK_PERFORMANCE_PROFILE,
expanded: hasPerf,
profile: this.detectProfile(),
// Bus the disk used before the "High Performance" preset switched it to
// virtio, so returning to "Default" can put it back.
@@ -46,10 +42,6 @@ export default {
},
computed: {
isView() {
return this.mode === _VIEW;
},
storagePerformanceEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('highPerformanceStorage');
},
@@ -206,51 +198,49 @@ export default {
v-if="!isCdRom && storagePerformanceEnabled"
class="disk-performance"
>
<button
v-if="!isView"
type="button"
class="btn btn-sm role-link expand-toggle"
:aria-expanded="expanded"
:aria-label="expanded ? t('harvester.virtualMachine.volume.performance.collapse') : t('harvester.virtualMachine.volume.performance.expand')"
@click.prevent="expanded = !expanded"
>
<i
class="icon"
:class="expanded ? 'icon-chevron-down' : 'icon-chevron-right'"
/>
{{ t('harvester.virtualMachine.volume.performance.title') }}
</button>
<div
v-if="expanded || isView"
class="perf-body mt-10"
>
<div class="row mb-20">
<div
class="col span-6"
data-testid="input-disk-perf-profile"
>
<LabeledSelect
:value="profile"
:label="t('harvester.virtualMachine.volume.performance.profile.label')"
:tooltip="t('harvester.virtualMachine.volume.performance.profile.tip')"
:options="profileOptions"
:mode="mode"
@update:value="onProfileChange"
/>
</div>
</div>
<Banner
v-if="profile === DISK_PERFORMANCE_PROFILE.HIGH"
color="info"
label-key="harvester.virtualMachine.volume.performance.highProfileTip"
/>
<div class="row mb-20">
<div
v-if="isCustom || isView"
class="row mb-20"
class="col span-6"
data-testid="input-disk-perf-profile"
>
<LabeledSelect
:value="profile"
:label="t('harvester.virtualMachine.volume.performance.profile.label')"
:tooltip="t('harvester.virtualMachine.volume.performance.profile.tip')"
:options="profileOptions"
:mode="mode"
@update:value="onProfileChange"
/>
</div>
</div>
<Banner
v-if="profile === DISK_PERFORMANCE_PROFILE.HIGH"
color="info"
label-key="harvester.virtualMachine.volume.performance.highProfileTip"
/>
<!-- Always rendered, so screen readers announce these warnings when changing the fields below makes them appear
above the focused field -->
<div
role="status"
data-testid="disk-perf-warning-status"
>
<Banner
v-if="showBusTip"
color="warning"
data-testid="disk-perf-bus-tip"
:label="t('harvester.virtualMachine.volume.performance.busTip', { bus: value.bus }, true)"
/>
<Banner
v-if="showFilesystemCacheWarning"
color="warning"
label-key="harvester.virtualMachine.volume.performance.cacheMode.filesystemWarning"
/>
</div>
<template v-if="isCustom">
<div class="row mb-20">
<div
class="col span-6"
data-testid="input-disk-cache"
@@ -279,10 +269,7 @@ export default {
</div>
</div>
<div
v-if="isCustom || isView"
class="row mb-10"
>
<div class="row mb-20">
<div
class="col span-12"
data-testid="input-disk-dedicated-iothread"
@@ -296,39 +283,19 @@ export default {
/>
</div>
</div>
</template>
<!-- Always rendered, also outside Custom, so screen readers announce the tip when picking Native I/O changes
the cache mode, even if the new values switch the profile to High Performance -->
<div
role="status"
data-testid="disk-perf-native-status"
>
<Banner
v-if="isNativeIo"
color="info"
label-key="harvester.virtualMachine.volume.performance.ioMode.nativeRequiresNoCacheTip"
/>
<Banner
v-if="showFilesystemCacheWarning"
color="warning"
label-key="harvester.virtualMachine.volume.performance.cacheMode.filesystemWarning"
/>
<Banner
v-if="showBusTip"
color="warning"
label-key="harvester.virtualMachine.volume.performance.busTip"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.disk-performance {
border-top: 1px solid var(--border);
margin-top: 10px;
padding-top: 10px;
.expand-toggle {
padding: 0;
font-weight: 600;
.icon {
margin-right: 4px;
}
}
}
</style>
@@ -1,5 +1,4 @@
<script>
import InfoBox from '@shell/components/InfoBox';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import { Checkbox } from '@components/Form/Checkbox';
@@ -11,7 +10,7 @@ export default {
name: 'VMPerformanceOptions',
components: {
InfoBox, LabeledSelect, LabeledInput, Checkbox, Banner
LabeledSelect, LabeledInput, Checkbox, Banner
},
emits: ['update:blockMultiQueue', 'update:ioThreadsPolicy', 'update:ioThreadCount'],
@@ -50,12 +49,6 @@ export default {
},
},
data() {
// These are specialist tuning options, so keep the panel collapsed unless the
// VM already has something configured.
return { expanded: !!(this.blockMultiQueue || this.ioThreadsPolicy) };
},
computed: {
isView() {
return this.mode === _VIEW;
@@ -87,105 +80,71 @@ export default {
</script>
<template>
<InfoBox
<div
v-if="storagePerformanceEnabled && (!isView || blockMultiQueue || !!ioThreadsPolicy)"
class="vm-perf mt-10"
class="vm-perf"
>
<button
v-if="!isView"
type="button"
class="btn btn-sm role-link expand-toggle"
:aria-expanded="expanded"
:aria-label="expanded ? t('harvester.virtualMachine.volume.diskPerformance.collapse') : t('harvester.virtualMachine.volume.diskPerformance.expand')"
@click.prevent="expanded = !expanded"
>
<i
class="icon"
:class="expanded ? 'icon-chevron-down' : 'icon-chevron-right'"
/>
{{ t('harvester.virtualMachine.volume.diskPerformance.title') }}
</button>
<h3
v-else
class="mb-5"
>
<h3>
{{ t('harvester.virtualMachine.volume.diskPerformance.title') }}
</h3>
<p class="text-muted mb-10">
{{ t('harvester.virtualMachine.volume.diskPerformance.description') }}
</p>
<template v-if="expanded || isView">
<p class="text-muted mb-15 mt-10">
{{ t('harvester.virtualMachine.volume.diskPerformance.description') }}
</p>
<div class="row mb-10">
<div
class="col span-12"
data-testid="input-vm-block-multi-queue"
>
<Checkbox
:value="blockMultiQueue"
:label="t('harvester.virtualMachine.volume.diskPerformance.blockMultiQueue.label')"
:tooltip="t('harvester.virtualMachine.volume.diskPerformance.blockMultiQueue.tip')"
:mode="mode"
:disabled="!hasVirtioDisk"
@update:value="$emit('update:blockMultiQueue', $event)"
/>
</div>
</div>
<Banner
v-if="!hasVirtioDisk && !isView"
color="info"
label-key="harvester.virtualMachine.volume.diskPerformance.blockMultiQueue.noVirtioTip"
<div
class="mb-10"
data-testid="input-vm-block-multi-queue"
>
<Checkbox
:value="blockMultiQueue"
:label="t('harvester.virtualMachine.volume.diskPerformance.blockMultiQueue.label')"
:tooltip="t('harvester.virtualMachine.volume.diskPerformance.blockMultiQueue.tip')"
:mode="mode"
:disabled="!hasVirtioDisk"
@update:value="$emit('update:blockMultiQueue', $event)"
/>
</div>
<Banner
v-if="!hasVirtioDisk && !isView"
color="info"
label-key="harvester.virtualMachine.volume.diskPerformance.blockMultiQueue.noVirtioTip"
/>
<div class="row mb-10">
<div
class="col span-6"
data-testid="input-vm-iothreads-policy"
>
<LabeledSelect
:value="ioThreadsPolicy"
:label="t('harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.label')"
:tooltip="t('harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.tip')"
:options="ioThreadsPolicyOptions"
:mode="mode"
@update:value="$emit('update:ioThreadsPolicy', $event)"
/>
</div>
<div
v-if="ioThreadsPolicy === 'supplementalPool'"
class="col span-6"
data-testid="input-vm-iothread-count"
>
<LabeledInput
type="number"
min="1"
:value="ioThreadCount"
:label="t('harvester.virtualMachine.volume.diskPerformance.ioThreadCount.label')"
:tooltip="t('harvester.virtualMachine.volume.diskPerformance.ioThreadCount.tip')"
:mode="mode"
@update:value="$emit('update:ioThreadCount', Number($event))"
/>
</div>
<div class="row mb-20">
<div
class="col span-6"
data-testid="input-vm-iothreads-policy"
>
<LabeledSelect
:value="ioThreadsPolicy"
:label="t('harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.label')"
:tooltip="t('harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.tip')"
:options="ioThreadsPolicyOptions"
:mode="mode"
@update:value="$emit('update:ioThreadsPolicy', $event)"
/>
</div>
<Banner
v-if="hasDedicatedIothread && !ioThreadsPolicy"
color="info"
label-key="harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.autoEnabledTip"
/>
</template>
</InfoBox>
<div
v-if="ioThreadsPolicy === 'supplementalPool'"
class="col span-6"
data-testid="input-vm-iothread-count"
>
<LabeledInput
type="integer"
min="1"
:required="!isView"
:value="ioThreadCount"
:label="t('harvester.virtualMachine.volume.diskPerformance.ioThreadCount.label')"
:tooltip="t('harvester.virtualMachine.volume.diskPerformance.ioThreadCount.tip')"
:mode="mode"
@update:value="$emit('update:ioThreadCount', Number($event))"
/>
</div>
</div>
<Banner
v-if="hasDedicatedIothread && !ioThreadsPolicy"
color="info"
label-key="harvester.virtualMachine.volume.diskPerformance.ioThreadsPolicy.autoEnabledTip"
/>
</div>
</template>
<style lang="scss" scoped>
.vm-perf {
.expand-toggle {
padding: 0;
font-weight: 600;
.icon {
margin-right: 4px;
}
}
}
</style>
@@ -1,3 +1,6 @@
import fs from 'fs';
import path from 'path';
import { load } from 'js-yaml';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { _EDIT } from '@shell/config/query-params';
@@ -24,6 +27,12 @@ const hardDisk = (overrides = {}) => ({
type: 'disk', bus: 'virtio', cache: '', io: '', dedicatedIOThread: false, ...overrides
});
const nativeTip = (wrapper) => wrapper.findAllComponents(Banner)
.find((b) => b.props('labelKey') === 'harvester.virtualMachine.volume.performance.ioMode.nativeRequiresNoCacheTip');
const filesystemWarning = (wrapper) => wrapper.findAllComponents(Banner)
.find((b) => b.props('labelKey') === 'harvester.virtualMachine.volume.performance.cacheMode.filesystemWarning');
describe('component: DiskPerformanceOptions', () => {
it('is hidden when the highPerformanceStorage feature is disabled', () => {
const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk(), { featureEnabled: false }));
@@ -37,6 +46,102 @@ describe('component: DiskPerformanceOptions', () => {
expect(wrapper.find('.disk-performance').exists()).toBe(true);
});
it('shows the profile without a toggle, and the individual fields only for Custom', () => {
const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk()));
expect(wrapper.find('.expand-toggle').exists()).toBe(false);
expect(wrapper.find('[data-testid="input-disk-perf-profile"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="input-disk-cache"]').exists()).toBe(false);
expect(wrapper.find('[data-testid="input-disk-dedicated-iothread"]').exists()).toBe(false);
});
it('shows the individual fields for a disk with custom options', () => {
const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk({ cache: 'writeback', io: 'threads' })));
expect(wrapper.find('[data-testid="input-disk-cache"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="input-disk-io"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="input-disk-dedicated-iothread"]').exists()).toBe(true);
});
it('is hidden for a CD-ROM', () => {
const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk({ type: 'cd-rom' })));
expect(wrapper.find('.disk-performance').exists()).toBe(false);
});
it('names the bus in the bus tip once the disk uses an option', () => {
const plain = mount(DiskPerformanceOptions, mountOptions(hardDisk({ bus: 'sata' })));
const tuned = mount(DiskPerformanceOptions, mountOptions(hardDisk({ bus: 'sata', cache: 'writeback' })));
expect(plain.find('[data-testid="disk-perf-bus-tip"]').exists()).toBe(false);
expect(tuned.find('[data-testid="disk-perf-bus-tip"]').exists()).toBe(true);
});
it('says in the bus tip that a dedicated I/O thread needs the virtio bus', () => {
const translations = load(fs.readFileSync(path.join(__dirname, '../../../../l10n/en-us.yaml'), 'utf8'));
const { busTip } = translations.harvester.virtualMachine.volume.performance;
expect(busTip).toContain('{bus}');
expect(busTip).toContain('A dedicated I/O thread needs the virtio bus');
});
it('shows the bus tip and the Filesystem warning inside a live region that is there before they appear', async() => {
const disk = { bus: 'sata', volumeMode: 'Filesystem' };
const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk(disk)));
const status = wrapper.find('[data-testid="disk-perf-warning-status"]');
expect(status.attributes('role')).toBe('status');
expect(status.find('[data-testid="disk-perf-bus-tip"]').exists()).toBe(false);
expect(filesystemWarning(wrapper)).toBeUndefined();
await wrapper.setProps({ value: hardDisk({ ...disk, cache: 'none' }) });
expect(wrapper.find('[data-testid="disk-perf-warning-status"]').element).toBe(status.element);
expect(status.find('[data-testid="disk-perf-bus-tip"]').exists()).toBe(true);
expect(status.element.contains(filesystemWarning(wrapper)?.element)).toBe(true);
});
it('shows the Native I/O tip inside a live region that is there before Native I/O is picked', async() => {
const value = hardDisk({ cache: 'writeback', io: 'threads' });
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
const status = wrapper.find('[data-testid="disk-perf-native-status"]');
expect(status.attributes('role')).toBe('status');
expect(nativeTip(wrapper)).toBeUndefined();
wrapper.vm.onIoChange('native');
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="disk-perf-native-status"]').element).toBe(status.element);
expect(status.element.contains(nativeTip(wrapper)?.element)).toBe(true);
});
it('keeps the live region and the Native I/O tip when picking Native I/O switches Custom to High Performance', async() => {
const value = hardDisk({
cache: 'writeback', io: 'threads', dedicatedIOThread: true
});
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
const status = wrapper.find('[data-testid="disk-perf-native-status"]');
expect(wrapper.vm.profile).toBe('custom');
wrapper.vm.onIoChange('native');
await wrapper.vm.$nextTick();
expect(wrapper.vm.profile).toBe('highPerformance');
expect(wrapper.find('[data-testid="input-disk-cache"]').exists()).toBe(false);
expect(wrapper.find('[data-testid="disk-perf-native-status"]').element).toBe(status.element);
expect(status.element.contains(nativeTip(wrapper)?.element)).toBe(true);
});
it('passes the Filesystem warning as a label key so its quotes are not double-escaped', () => {
const wrapper = mount(DiskPerformanceOptions, mountOptions(hardDisk({ cache: 'none', volumeMode: 'Filesystem' })));
const banner = filesystemWarning(wrapper);
expect(banner).toBeDefined();
expect(banner?.props('label')).toBeFalsy();
});
it('applies the High Performance preset and emits update', () => {
const value = hardDisk();
const wrapper = mount(DiskPerformanceOptions, mountOptions(value));
@@ -173,8 +278,7 @@ describe('component: DiskPerformanceOptions', () => {
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');
const banner = nativeTip(wrapper);
expect(banner).toBeDefined();
expect(banner?.props('label')).toBeFalsy();
@@ -1,19 +1,19 @@
import { mount } from '@vue/test-utils';
import { _CREATE } from '@shell/config/query-params';
import { _CREATE, _VIEW } 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 = {}) {
function mountOptions(props = {}, { featureEnabled = true } = {}) {
return mount(VMPerformanceOptions, {
props: { mode: _CREATE, ...props },
global: {
mocks: {
$store: {
getters: {
'harvester-common/getFeatureEnabled': () => true,
'harvester-common/getFeatureEnabled': () => featureEnabled,
'i18n/t': (key) => key,
'i18n/exists': () => true,
}
@@ -24,35 +24,79 @@ function mountOptions(props = {}) {
});
}
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 });
it('should be hidden when the highPerformanceStorage feature is disabled', () => {
const wrapper = mountOptions({}, { featureEnabled: false });
await expand(wrapper);
expect(wrapper.find('.vm-perf').exists()).toBe(false);
});
it('should show the section with its title and fields, without a toggle', () => {
const wrapper = mountOptions();
expect(wrapper.find('h3').text()).toBe('harvester.virtualMachine.volume.diskPerformance.title');
expect(wrapper.find('.expand-toggle').exists()).toBe(false);
expect(wrapper.find('[data-testid="input-vm-block-multi-queue"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="input-vm-iothreads-policy"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="input-vm-iothread-count"]').exists()).toBe(false);
});
it('should only accept whole numbers as the supplemental pool thread count', () => {
const wrapper = mountOptions({ ioThreadsPolicy: 'supplementalPool', ioThreadCount: 4 });
const input = wrapper.find('[data-testid="input-vm-iothread-count"] input');
expect(input.attributes('inputmode')).toBe('numeric');
});
it.each([
[_CREATE, true],
[_VIEW, false],
])('in %s mode, should mark the supplemental pool thread count as required: %p', (mode, required) => {
const wrapper = mountOptions({
mode, ioThreadsPolicy: 'supplementalPool', ioThreadCount: 4
});
expect(wrapper.find('[data-testid="input-vm-iothread-count"] .required').exists()).toBe(required);
});
it.each([
[{}, false],
[{ blockMultiQueue: true }, true],
[{ ioThreadsPolicy: 'auto' }, true],
])('in view mode with %p, should show the section: %p', (props, shown) => {
const wrapper = mountOptions({ ...props, mode: _VIEW });
expect(wrapper.find('.vm-perf').exists()).toBe(shown);
});
it('should clear Block Multi-Queue when the last virtio disk goes away', async() => {
const wrapper = mountOptions({ blockMultiQueue: true, hasVirtioDisk: true });
await wrapper.setProps({ hasVirtioDisk: false });
expect(wrapper.emitted('update:blockMultiQueue')).toStrictEqual([[false]]);
});
it('should pass the I/O threads policy hint as a label key so its quotes are not double-escaped', () => {
const wrapper = mountOptions({ hasDedicatedIothread: true });
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() => {
it('should not show the I/O threads policy hint when a policy is set', () => {
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() => {
it('should pass the no virtio disk hint as a label key', () => {
const wrapper = mountOptions({ hasVirtioDisk: false });
await expand(wrapper);
const banner = bannerFor(wrapper, NO_VIRTIO_TIP);
expect(banner).toBeDefined();
@@ -7,7 +7,6 @@ import UnitInput from '@shell/components/form/UnitInput';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import ModalWithCard from '@shell/components/ModalWithCard';
import VMPerformanceOptions from './VMPerformanceOptions';
import { VOLUME_HOTPLUG_ACTION, SOURCE_TYPE } from '../../../config/harvester-map';
import { PVC, STORAGE_CLASS } from '@shell/config/types';
import { clone } from '@shell/utils/object';
@@ -24,10 +23,10 @@ import { EMPTY_IMAGE } from '../../../utils/vm';
const { READ_WRITE_MANY } = ACCESS_MODE;
export default {
emits: ['update:value', 'update:blockMultiQueue', 'update:ioThreadsPolicy', 'update:ioThreadCount'],
emits: ['update:value'],
components: {
Banner, BadgeStateFormatter, VueDraggableNext, InfoBox, LabeledInput, UnitInput, LabeledSelect, ModalWithCard, VMPerformanceOptions
Banner, BadgeStateFormatter, VueDraggableNext, InfoBox, LabeledInput, UnitInput, LabeledSelect, ModalWithCard
},
props: {
@@ -78,22 +77,6 @@ export default {
resourceType: {
type: String,
default: ''
},
// KubeVirt high-performance VM-level features
blockMultiQueue: {
type: Boolean,
default: false
},
ioThreadsPolicy: {
type: String,
default: ''
},
ioThreadCount: {
type: Number,
default: 2
}
},
@@ -164,14 +147,6 @@ export default {
isLHV2VolExpansionFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
},
hasVirtioDisk() {
return this.rows.some((R) => R.type === 'disk' && R.bus === 'virtio');
},
hasDedicatedIOThread() {
return this.rows.some((R) => R.dedicatedIOThread);
},
},
watch: {
@@ -533,18 +508,6 @@ export default {
</button>
</div>
<VMPerformanceOptions
:block-multi-queue="blockMultiQueue"
:io-threads-policy="ioThreadsPolicy"
:io-thread-count="ioThreadCount"
:has-virtio-disk="hasVirtioDisk"
:has-dedicated-iothread="hasDedicatedIOThread"
:mode="mode"
@update:block-multi-queue="$emit('update:blockMultiQueue', $event)"
@update:io-threads-policy="$emit('update:ioThreadsPolicy', $event)"
@update:io-thread-count="$emit('update:ioThreadCount', $event)"
/>
<ModalWithCard
v-if="isOpen"
name="deleteTip"
@@ -51,4 +51,11 @@ describe('component: HarvesterEditContainer', () => {
expect(wrapper.emitted('update')).toHaveLength(1);
});
it('should not render an empty row when there is no backup to show', () => {
const wrapper = mount(HarvesterEditContainer, mountOptions());
const emptyRows = wrapper.findAll('.row').filter((row) => row.element.children.length === 0);
expect(emptyRows).toHaveLength(0);
});
});
@@ -133,4 +133,30 @@ describe('component: HarvesterEditVMImage', () => {
expect(wrapper.emitted('update')).toHaveLength(1);
});
it('should not render an empty row when there is no backup to show', () => {
const wrapper = mount(HarvesterEditVMImage, {
props: {
mode: _CREATE,
isVirtualType: false,
newCreateId: '123',
validateRequired: true,
idx: 1
},
global: {
mocks: {
$store: {
getters: {
'harvester-common/getFeatureEnabled': () => true,
'harvester/all': jest.fn(),
'i18n/t': jest.fn(),
}
},
}
}
});
const emptyRows = wrapper.findAll('.row').filter((row) => row.element.children.length === 0);
expect(emptyRows).toHaveLength(0);
});
});
@@ -132,11 +132,11 @@ export default {
</InputOrDisplay>
</div>
</div>
<div class="row mb-20">
<div
v-if="value.volumeBackups"
class="col span-6"
>
<div
v-if="value.volumeBackups"
class="row mb-20"
>
<div class="col span-6">
<InputOrDisplay
:name="t('harvester.virtualMachine.volume.readyToUse')"
:value="value.volumeBackups.readyToUse"
@@ -461,11 +461,11 @@ export default {
/>
</div>
</div>
<div class="row mb-20">
<div
v-if="value.volumeBackups && isView"
class="col span-3"
>
<div
v-if="value.volumeBackups && isView"
class="row mb-20"
>
<div class="col span-3">
<LabelValue
:name="t('harvester.virtualMachine.volume.readyToUse')"
:value="readyToUse"
@@ -34,6 +34,7 @@ import CpuMemory from './VirtualMachineCpuMemory';
import CpuModel from './VirtualMachineCpuModel';
import Network from './VirtualMachineNetwork';
import Volume from './VirtualMachineVolume';
import VMPerformanceOptions from './VirtualMachineVolume/VMPerformanceOptions';
import SSHKey from './VirtualMachineSSHKey';
import Reserved from './VirtualMachineReserved';
import Filesystem from './VirtualMachineFilesystem';
@@ -55,6 +56,7 @@ export default {
LabeledSelect,
NameNsDescription,
Volume,
VMPerformanceOptions,
SSHKey,
Network,
CpuMemory,
@@ -872,9 +874,6 @@ export default {
>
<Volume
v-model:value="diskRows"
v-model:block-multi-queue="blockMultiQueue"
v-model:io-threads-policy="ioThreadsPolicy"
v-model:io-thread-count="ioThreadCount"
:mode="mode"
:custom-volume-mode="customVolumeMode"
:namespace="value.metadata.namespace"
@@ -1146,6 +1145,16 @@ export default {
</div>
</div>
<VMPerformanceOptions
v-model:block-multi-queue="blockMultiQueue"
v-model:io-threads-policy="ioThreadsPolicy"
v-model:io-thread-count="ioThreadCount"
:has-virtio-disk="hasVirtioDisk"
:has-dedicated-iothread="hasDedicatedIOThread"
:mode="mode"
class="mb-20"
/>
<CloudConfig
ref="yamlEditor"
:user-script="userScript"