fix: show VNC console dialogs in full screen and restore deleting custom shortcut keys (#1222)

* fix: show the VNC console's dialogs in full screen

Only the console went full screen and dialogs render outside of it, so the
Ctrl+Alt+Del and Soft Reboot confirmations, among others, stayed hidden behind
it until full screen was left. Put the whole page in full screen instead.

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

* fix: bring back deleting saved VNC console shortcut keys

The Vue 3 migration dropped the close button from the list of saved shortcuts,
which left no way to delete one. Restore it, and remove by position in the list:
the old code parsed the displayed HTML to find the shortcut, which cannot match
one with the Space key in it, such as Ctrl+Space.

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

* fix: tidy the VNC console's custom shortcut keys

Give the saved shortcuts in the menu the same space as the separator above
them, and name the dialog's buttons Cancel and Save, which say what they do.

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

---------

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
(cherry picked from commit 73eaccc2ad)
This commit is contained in:
Marcelo Fukumoto
2026-11-09 17:30:03 +01:00
parent 3322162d9a
commit 5f2ee088e7
4 changed files with 126 additions and 20 deletions
@@ -136,20 +136,15 @@ export default {
this.addedShortcutKeys.push([].concat(this.keysRecord)); this.addedShortcutKeys.push([].concat(this.keysRecord));
}, },
removeKey(keys) { // The index is the one in displayedKeys, which lists the added shortcuts first.
const key = keys.replace(/(\s*)<code>|<\/code>/g, '').replace(/\s*,\s*/g, ','); removeKey(index) {
const added = this.addedShortcutKeys.length;
this.addedShortcutKeys = this.addedShortcutKeys.filter((item) => { if (index < added) {
const formatkey = item.map((K) => K.key.charAt(0).toUpperCase() + K.key.slice(1)).join(','); this.addedShortcutKeys.splice(index, 1);
} else {
return formatkey !== key; this.preferredShortcutKeys.splice(index - added, 1);
}); }
this.preferredShortcutKeys = this.preferredShortcutKeys.filter((item) => {
const formatkey = item.map((K) => K.key.charAt(0).toUpperCase() + K.key.slice(1)).join(',');
return formatkey !== key;
});
}, },
async saveKeys(buttonCb) { async saveKeys(buttonCb) {
@@ -220,6 +215,8 @@ export default {
<Banner <Banner
v-for="(keys,index) in displayedKeys" v-for="(keys,index) in displayedKeys"
:key="index" :key="index"
:closable="true"
@close="removeKey(index)"
> >
<span v-clean-html="keys"></span> <span v-clean-html="keys"></span>
</Banner> </Banner>
@@ -233,10 +230,10 @@ export default {
class="btn role-secondary mr-20" class="btn role-secondary mr-20"
@click.prevent="closeRecordingModal" @click.prevent="closeRecordingModal"
> >
<t k="generic.close" /> <t k="generic.cancel" />
</button> </button>
<AsyncButton <AsyncButton
mode="done" mode="edit"
@click="saveKeys" @click="saveKeys"
/> />
</div> </div>
@@ -428,7 +428,8 @@ export default {
if (document.fullscreenElement) { if (document.fullscreenElement) {
await document.exitFullscreen(); await document.exitFullscreen();
} else { } else {
await this.$refs.consoleContainer.requestFullscreen(); // The whole page and not only the console, because dialogs render outside of it.
await document.documentElement.requestFullscreen();
// Keyboard lock lets shortcuts such as Ctrl+W and F11 reach the guest. // Keyboard lock lets shortcuts such as Ctrl+W and F11 reach the guest.
await navigator.keyboard?.lock?.(['KeyW', 'F11']); await navigator.keyboard?.lock?.(['KeyW', 'F11']);
} }
@@ -454,10 +455,7 @@ export default {
<template> <template>
<div id="app"> <div id="app">
<div <div class="vm-console">
ref="consoleContainer"
class="vm-console"
>
<div class="combination-keys"> <div class="combination-keys">
<div class="toolbar-group"> <div class="toolbar-group">
<v-dropdown <v-dropdown
@@ -518,6 +516,7 @@ export default {
<div <div
v-for="(keys, index) in preferredShortcutKeys" v-for="(keys, index) in preferredShortcutKeys"
:key="index" :key="index"
class="custom-shortcut"
> >
<button <button
class="btn btn-sm bg-primary" class="btn btn-sm bg-primary"
@@ -735,6 +734,11 @@ export default {
width: 100%; width: 100%;
} }
// The space around the separator above the list.
.custom-shortcut + .custom-shortcut {
margin-top: 7px;
}
.console-status { .console-status {
color: #fff; color: #fff;
font-size: 12px; font-size: 12px;
@@ -0,0 +1,92 @@
import { shallowMount } from '@vue/test-utils';
import Banner from '@components/Banner/Banner.vue';
import NovncConsoleCustomKeys from '../NovncConsoleCustomKeys.vue';
const PREFERED_SHORTCUT_KEYS = 'prefered-shortcut-keys';
const key = (value) => ({ key: value, code: value });
const CTRL_F2 = [key('Control'), key('F2')];
const CTRL_SPACE = [key('Control'), key(' ')];
const ALT_T = [key('Alt'), key('t')];
const mountKeys = (saved = []) => {
const preference = {
data: { [PREFERED_SHORTCUT_KEYS]: JSON.stringify(saved) },
save: jest.fn().mockResolvedValue(),
};
const wrapper = shallowMount(NovncConsoleCustomKeys, {
global: {
mocks: { $store: { getters: { 'management/all': () => [preference] } } },
stubs: { ModalWithCard: { template: '<div><slot name="content" /></div>' } },
},
});
return { wrapper, preference };
};
const listed = (wrapper) => wrapper.find('.displayed-banners').findAllComponents(Banner);
const closeBanner = async(wrapper, index) => {
listed(wrapper)[index].vm.$emit('close');
await wrapper.vm.$nextTick();
};
const savedAfterDone = async(wrapper, preference) => {
await wrapper.vm.saveKeys(jest.fn());
return JSON.parse(preference.data[PREFERED_SHORTCUT_KEYS]);
};
describe('component: NovncConsoleCustomKeys', () => {
it('should offer to remove every listed shortcut', () => {
const { wrapper } = mountKeys([CTRL_F2, ALT_T]);
expect(listed(wrapper).map((banner) => banner.props('closable'))).toStrictEqual([true, true]);
});
it('should save without the shortcut that was removed', async() => {
const { wrapper, preference } = mountKeys([CTRL_F2, ALT_T]);
await closeBanner(wrapper, 0);
expect(listed(wrapper)).toHaveLength(1);
expect(preference.save).not.toHaveBeenCalled();
await expect(savedAfterDone(wrapper, preference)).resolves.toStrictEqual([ALT_T]);
expect(preference.save).toHaveBeenCalledTimes(1);
});
it('should remove a shortcut with the Space key in it', async() => {
const { wrapper, preference } = mountKeys([CTRL_SPACE, ALT_T]);
await closeBanner(wrapper, 0);
await expect(savedAfterDone(wrapper, preference)).resolves.toStrictEqual([ALT_T]);
});
it('should tell a shortcut that was just added from the saved ones', async() => {
const { wrapper, preference } = mountKeys([CTRL_F2]);
wrapper.vm.keysRecord = ALT_T;
wrapper.vm.addShortcutKey();
await wrapper.vm.$nextTick();
// Just added shortcuts are listed first.
await closeBanner(wrapper, 1);
await expect(savedAfterDone(wrapper, preference)).resolves.toStrictEqual([ALT_T]);
});
it('should keep the saved shortcuts when it is closed without saving', async() => {
const { wrapper, preference } = mountKeys([CTRL_F2, ALT_T]);
await closeBanner(wrapper, 0);
wrapper.vm.closeRecordingModal();
expect(wrapper.emitted('close')).toHaveLength(1);
expect(preference.save).not.toHaveBeenCalled();
expect(JSON.parse(preference.data[PREFERED_SHORTCUT_KEYS])).toStrictEqual([CTRL_F2, ALT_T]);
});
});
@@ -92,4 +92,17 @@ describe('component: NovncConsoleWrapper', () => {
expect(wrapper.vm.confirmAction).toBeNull(); expect(wrapper.vm.confirmAction).toBeNull();
}); });
it('should put the whole page in full screen, so that dialogs rendered outside the console show', async() => {
const wrapper = mountWrapper();
const requestFullscreen = jest.fn().mockResolvedValue();
document.documentElement.requestFullscreen = requestFullscreen;
await wrapper.vm.toggleFullscreen();
expect(requestFullscreen).toHaveBeenCalledTimes(1);
delete document.documentElement.requestFullscreen;
});
}); });