1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 12:46:49 +00:00

feat(console): one VNC console page, driven from the panel's bar

The VM page offered a lite and a full VNC console. Lite lacked a clipboard,
display settings and a way to reconnect; full put noVNC's own side bar under
the panel's bar, with a Fullscreen item that called a function noVNC does
not have, status messages hidden behind the bar, saved noVNC settings that
overrode the Access tab's, connection settings that cannot apply, and a
Connect button that reused the page's two-minute token and failed.

console-vnc.html and static/js/console_vnc.js replace both, on noVNC's RFB
with Bootstrap controls, and keep what the full view did:
- Status in the bar (Connecting, Connected, Disconnected, Connection lost,
  Could not connect, Authentication failed with the server's reason on
  hover); Disconnect, then Reconnect, which loads the page again for a new
  token.
- The VNC password in a dialog (with a username when the server asks for
  one), not a browser prompt; without passwordless console it is not in
  the page.
- Send key(s) gains Hold Ctrl / Alt / Windows (noVNC's extra keys).
- Clipboard: what the VM copies, sending text to the VM's clipboard, and
  typing it key by key (new: works in firmware, installers and login
  prompts; Shift for the characters a US keyboard shifts, 25 ms a key as
  firmware drops faster keys, verified in a UEFI shell).
- View: scaling (none, fit, resize the VM's screen), clip, drag a clipped
  screen, view only, dot cursor, quality, compression; it starts as
  Settings > Console says (a touch screen starts fitted).
- A Keyboard item on touch devices (noVNC's on-screen keyboard input,
  key events and Android's text-only input), Fullscreen with keyboard
  lock, the bell.
Dropped with the full view: noVNC's websocket host/port/path, repeater ID
and log level (the panel fixes them), automatic reconnect (the token is
single use in time), XVP power buttons (QEMU has no XVP) and shared-mode off.

The bar is compact (about 45 px in the 850x600 window). The Access tab has
one Console button instead of the Lite/Full split and four switches, and
it and the VM list open one window per VM (opening it again reloads it).
This commit is contained in:
catborise 2026-10-27 00:25:27 +03:00
parent 3a560e9f85
commit 0532471759
12 changed files with 520 additions and 843 deletions

View file

@ -21,7 +21,7 @@
padding: 0;
width: 100%;
max-width: none;
height: 100vh;
height: 100dvh;
display: flex;
flex-direction: column;
background-color: #494949;
@ -42,9 +42,10 @@
<body>
<div id="main_container">
<nav class="navbar sticky-top navbar-expand-md navbar-dark bg-primary" aria-label="console navbar">
<nav class="navbar navbar-expand-md navbar-dark bg-primary py-1" aria-label="{% trans "Console" %}">
<div class="container-fluid">
<a class="navbar-brand">{{ instance.name }}</a>
<span class="navbar-brand py-0">{{ instance.name }}</span>
{% block navbarstatus %}{% endblock %}
{% block navbar %}
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse"
aria-expanded="false" aria-label="Toggle navigation">
@ -52,9 +53,10 @@
</button>
<div class="collapse navbar-collapse">
<ul class="navbar-nav me-auto my-2 my-lg-0">
<ul class="navbar-nav me-auto">
{% block navbarmenu %}{% endblock %}
</ul>
{% block navbarend %}{% endblock %}
</div>
{% endblock %}
</div>

View file

@ -5,7 +5,7 @@
id="dropdownMenuLink" aria-haspopup="true" aria-expanded="false">
{% trans "Send key(s)" %}
</a>
<div class="dropdown-menu shadow" aria-labelledby="dropdownMenuLink" style="min-width: 250px;">
<div class="dropdown-menu shadow overflow-auto" aria-labelledby="dropdownMenuLink" style="min-width: 250px; max-height: calc(100dvh - 4rem);">
{% if not serial %}
<h6 class="dropdown-header">{% trans "System" %}</h6>
<a class="dropdown-item" id="ctrlaltdel" href="#">Ctrl+Alt+Del</a>
@ -45,6 +45,11 @@
<div class="col-3"><a class="btn btn-sm btn-outline-secondary w-100 py-1" id="ctrlaltf12" href="#">F12</a></div>
</div>
</div>
<div class="dropdown-divider"></div>
<h6 class="dropdown-header">{% trans "Hold until chosen again" %}</h6>
<a class="dropdown-item" id="holdctrl" href="#" role="button" aria-pressed="false">Ctrl</a>
<a class="dropdown-item" id="holdalt" href="#" role="button" aria-pressed="false">Alt</a>
<a class="dropdown-item" id="holdsuper" href="#" role="button" aria-pressed="false">Windows / Super Key</a>
{% endif %}
</div>
</li>

View file

@ -1,382 +0,0 @@
{% extends "console-base.html" %}
{% load i18n %}
{% load static %}
{% block head %}
<!--
noVNC example: simple example using default UI
Copyright (C) 2018 The noVNC Authors
noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
Connect parameters are provided in query string:
http://example.com/?host=HOST&port=PORT&encrypt=1
or the fragment:
http://example.com/#host=HOST&port=PORT&encrypt=1
-->
<meta charset="utf-8" />
<!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame
Remove this if you use the .htaccess -->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="icon" type="image/x-icon" href="{% static "js/novnc/app/images/icons/novnc.ico" %}">
<link rel="icon" sizes="any" type="image/svg+xml" href="{% static "js/novnc/app/images/icons/novnc-icon.svg" %}">
<!-- Apple iOS Safari settings -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<!-- iOS Home Screen Icons -->
<link rel="apple-touch-icon" sizes="40x40" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-ios-40.png" %}">
<link rel="apple-touch-icon" sizes="58x58" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-ios-58.png" %}">
<link rel="apple-touch-icon" sizes="60x60" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-ios-60.png" %}">
<link rel="apple-touch-icon" sizes="76x76" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-ios-76.png" %}">
<link rel="apple-touch-icon" sizes="80x80" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-ios-80.png" %}">
<link rel="apple-touch-icon" sizes="87x87" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-ios-87.png" %}">
<link rel="apple-touch-icon" sizes="120x120" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-ios-120.png" %}">
<link rel="apple-touch-icon" sizes="152x152" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-ios-152.png" %}">
<link rel="apple-touch-icon" sizes="167x167" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-ios-167.png" %}">
<link rel="apple-touch-icon" sizes="180x180" type="image/png" href="{% static "js/novnc/app/images/icons/novnc-ios-180.png" %}">
<!-- Stylesheets -->
<link rel="stylesheet" href="{% static "js/novnc/app/styles/constants.css" %}">
<link rel="stylesheet" href="{% static "js/novnc/app/styles/base.css" %}">
<link rel="stylesheet" href="{% static "js/novnc/app/styles/input.css" %}">
<!-- Images that will later appear via CSS -->
<link rel="preload" as="image" href="{% static "js/novnc/app/images/info.svg" %}">
<link rel="preload" as="image" href="{% static "js/novnc/app/images/error.svg" %}">
<link rel="preload" as="image" href="{% static "js/novnc/app/images/warning.svg" %}">
<script type="module" crossorigin="anonymous" src="{% static "js/novnc/app/error-handler.js" %}"></script>
<!-- actual script modules -->
<script type="module" crossorigin="anonymous">
import UI from '{% static "js/novnc/app/ui.js" %}';
import { bindKeyAction, bindSendKeys } from '{% static "js/console_keys.js" %}';
bindKeyAction('fullscreen_button', () => {
if (typeof UI.fullscreen === 'function') {
UI.fullscreen();
}
});
bindSendKeys(() => UI.rfb);
// novncd behind the panel's own address (nginx's /novncd/)
let fullPath = 'novncd/';
const token = '{{ token|escapejs }}';
if (token) {
fullPath += (fullPath.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(token);
}
const defaults = {
autoconnect: true,
resize: '{% if resize_session %}remote{% elif scale %}scale{% else %}off{% endif %}',
view_only: {{ view_only|yesno:"true,false" }},
view_clip: {{ clip_viewport|yesno:"true,false" }},
{% if perms.instances.passwordless_console and console_passwd %}
password: '{{ console_passwd|escapejs }}',
{% endif %}
};
const https = window.location.protocol === 'https:';
const mandatory = {
host: window.location.hostname,
port: Number(window.location.port || (https ? 443 : 80)),
path: fullPath,
encrypt: https,
};
UI.start({ settings: { defaults, mandatory } });
</script>
<!-- end scripts -->
{% endblock %}
{% block navbarmenu %}{% include "console-menu.html" %}{% endblock %}
{% block content %}
<div id="noVNC_fallback_error" class="noVNC_center">
<div>
<div>{% trans 'noVNC encountered an error' %}:</div>
<br>
<div id="noVNC_fallback_errormsg"></div>
</div>
</div>
<!-- noVNC Control Bar -->
<div id="noVNC_control_bar_anchor" class="noVNC_vcenter">
<div id="noVNC_control_bar">
<div id="noVNC_control_bar_handle" title="Hide/Show the control bar">
<div></div>
</div>
<div class="noVNC_scroll">
<h1 class="noVNC_logo" translate="no"><span>no</span><br>VNC</h1>
<!-- Drag/Pan the viewport -->
<input type="image" alt="viewport drag" src="{% static "js/novnc/app/images/drag.svg" %}"
id="noVNC_view_drag_button" class="noVNC_button noVNC_hidden" title="Move/Drag Viewport" />
<!--noVNC Touch Device only buttons-->
<div id="noVNC_mobile_buttons">
<input type="image" alt="Keyboard" src="{% static "js/novnc/app/images/keyboard.svg" %}"
id="noVNC_keyboard_button" class="noVNC_button" title="Show Keyboard">
</div>
<!-- Extra manual keys -->
<input type="image" alt="Extra keys" src="{% static "js/novnc/app/images/toggleextrakeys.svg" %}"
id="noVNC_toggle_extra_keys_button" class="noVNC_button" title="Show Extra Keys" />
<div class="noVNC_vcenter">
<div id="noVNC_modifiers" class="noVNC_panel">
<input type="image" alt="Ctrl" src="{% static "js/novnc/app/images/ctrl.svg" %}"
id="noVNC_toggle_ctrl_button" class="noVNC_button" title="Toggle Ctrl" />
<input type="image" alt="Alt" src="{% static "js/novnc/app/images/alt.svg" %}"
id="noVNC_toggle_alt_button" class="noVNC_button" title="Toggle Alt" />
<input type="image" alt="Windows" src="{% static "js/novnc/app/images/windows.svg" %}"
id="noVNC_toggle_windows_button" class="noVNC_button" title="Toggle Windows">
<input type="image" alt="Tab" src="{% static "js/novnc/app/images/tab.svg" %}"
id="noVNC_send_tab_button" class="noVNC_button" title="Send Tab" />
<input type="image" alt="Esc" src="{% static "js/novnc/app/images/esc.svg" %}"
id="noVNC_send_esc_button" class="noVNC_button" title="Send Escape" />
<input type="image" alt="Ctrl+Alt+Del" src="{% static "js/novnc/app/images/ctrlaltdel.svg" %}"
id="noVNC_send_ctrl_alt_del_button" class="noVNC_button" title="Send Ctrl-Alt-Del" />
</div>
</div>
<!-- Shutdown/Reboot -->
<input type="image" alt="Shutdown/Reboot" src="{% static "js/novnc/app/images/power.svg" %}"
id="noVNC_power_button" class="noVNC_button" title="Shutdown/Reboot..." />
<div class="noVNC_vcenter">
<div id="noVNC_power" class="noVNC_panel">
<div class="noVNC_heading">
<img src="{% static "js/novnc/app/images/power.svg" %}"> Power
</div>
<input type="button" id="noVNC_shutdown_button" value="Shutdown" />
<input type="button" id="noVNC_reboot_button" value="Reboot" />
<input type="button" id="noVNC_reset_button" value="Reset" />
</div>
</div>
<!-- Clipboard -->
<input type="image" alt="Clipboard" src="{% static "js/novnc/app/images/clipboard.svg" %}"
id="noVNC_clipboard_button" class="noVNC_button" title="Clipboard" />
<div class="noVNC_vcenter">
<div id="noVNC_clipboard" class="noVNC_panel">
<div class="noVNC_heading">
<img src="{% static "js/novnc/app/images/clipboard.svg" %}"> Clipboard
</div>
<textarea id="noVNC_clipboard_text"rows=5></textarea>
<br />
<input id="noVNC_clipboard_clear_button" type="button" value="Clear" class="noVNC_submit" />
</div>
</div>
<!-- Toggle fullscreen -->
<input type="image" alt="Fullscreen" src="{% static "js/novnc/app/images/fullscreen.svg" %}"
id="noVNC_fullscreen_button" class="noVNC_button noVNC_hidden" title="Fullscreen" />
<!-- Settings -->
<input type="image" alt="Settings" src="{% static "js/novnc/app/images/settings.svg" %}"
id="noVNC_settings_button" class="noVNC_button" title="Settings" />
<div class="noVNC_vcenter">
<div id="noVNC_settings" class="noVNC_panel">
<ul>
<li class="noVNC_heading">
<img src="{% static "js/novnc/app/images/settings.svg" %}"> Settings
</li>
<li>
<label><input id="noVNC_setting_shared" type="checkbox" /> Shared Mode</label>
</li>
<li>
<label><input id="noVNC_setting_view_only" type="checkbox" /> View Only</label>
</li>
<li>
<hr>
</li>
<li>
<label><input id="noVNC_setting_view_clip" type="checkbox" /> Clip to Window</label>
</li>
<li>
<label for="noVNC_setting_resize">Scaling Mode:</label>
<select id="noVNC_setting_resize" name="vncResize">
<option value="off">None</option>
<option value="scale">Local Scaling</option>
<option value="remote">Remote Resizing</option>
</select>
</li>
<li>
<hr>
</li>
<li>
<div class="noVNC_expander">Advanced</div>
<div><ul>
<li>
<label for="noVNC_setting_quality">Quality:</label>
<input id="noVNC_setting_quality" type="range" min="0" max="9" value="6">
</li>
<li>
<label for="noVNC_setting_compression">Compression level:</label>
<input id="noVNC_setting_compression" type="range" min="0" max="9" value="2">
</li>
<li><hr></li>
<li>
<label for="noVNC_setting_repeaterID">Repeater ID:</label>
<input id="noVNC_setting_repeaterID" type="text" value="">
</li>
<li>
<div class="noVNC_expander">WebSocket</div>
<div><ul>
<li>
<label><input id="noVNC_setting_encrypt" type="checkbox"> Encrypt</label>
</li>
<li>
<label for="noVNC_setting_host">Host:</label>
<input id="noVNC_setting_host" type="text">
</li>
<li>
<label for="noVNC_setting_port">Port:</label>
<input id="noVNC_setting_port" type="number">
</li>
<li>
<label for="noVNC_setting_path">Path:</label>
<!-- <input id="noVNC_setting_path" type="text" value="websockify"> -->
<input id="noVNC_setting_path" type="text">
</li>
</ul></div>
</li>
<li><hr></li>
<li>
<label><input id="noVNC_setting_reconnect" type="checkbox"> Automatic Reconnect</label>
</li>
<li>
<label for="noVNC_setting_reconnect_delay">Reconnect Delay (ms):</label>
<input id="noVNC_setting_reconnect_delay" type="number">
</li>
<li><hr></li>
<li>
<label><input id="noVNC_setting_show_dot" type="checkbox"> Show Dot when No Cursor</label>
</li>
<li><hr></li>
<!-- Logging selection dropdown -->
<li>
<label>Logging:
<select id="noVNC_setting_logging" name="vncLogging">
</select>
</label>
</li>
</ul></div>
</li>
<li class="noVNC_version_separator"><hr></li>
<li class="noVNC_version_wrapper">
<span>Version:</span>
<span class="noVNC_version"></span>
</li>
</ul>
</div>
</div>
<!-- Connection Controls -->
<input type="image" alt="Disconnect" src="{% static "js/novnc/app/images/disconnect.svg" %}"
id="noVNC_disconnect_button" class="noVNC_button" title="Disconnect" />
</div>
</div>
</div> <!-- End of noVNC_control_bar -->
<div id="noVNC_hint_anchor" class="noVNC_vcenter">
<div id="noVNC_control_bar_hint">
</div>
</div>
<!-- Status Dialog -->
<div id="noVNC_status"></div>
<!-- Connect button -->
<div class="noVNC_center">
<div id="noVNC_connect_dlg">
<p class="noVNC_logo" translate="no"><span>no</span>VNC</p>
<div>
<button id="noVNC_connect_button">
<img alt="" src="{% static "js/novnc/app/images/connect.svg" %}"> {% trans 'Connect' %}
</button>
</div>
</div>
</div>
<!-- Server key verification dialog -->
<div class="noVNC_center noVNC_connect_layer">
<div id="noVNC_verify_server_dlg" class="noVNC_panel"><form>
<div class="noVNC_heading">
{% trans 'Server identity' %}
</div>
<div>
{% trans 'The server has provided the following identifying information:' %}
</div>
<div id="noVNC_fingerprint_block">
{% trans 'Fingerprint:' %}
<span id="noVNC_fingerprint"></span>
</div>
<div>
{% trans 'Please verify that the information is correct and press "Approve". Otherwise press "Reject".' %}
</div>
<div class="button_row">
<input id="noVNC_approve_server_button" type="submit" value="{% trans 'Approve' %}">
<input id="noVNC_reject_server_button" type="button" value="{% trans 'Reject' %}">
</div>
</form></div>
</div>
<!-- Password dialog -->
<div class="noVNC_center noVNC_connect_layer">
<div id="noVNC_credentials_dlg" class="noVNC_panel"><form>
<div class="noVNC_heading">
{% trans 'Credentials' %}
</div>
<div id="noVNC_username_block">
<label for="noVNC_username_input">{% trans 'Username' %}:</label>
<input id="noVNC_username_input">
</div>
<div id="noVNC_password_block">
<label for="noVNC_password_input">{% trans 'Password' %}:</label>
{% if perms.instances.passwordless_console %}
<input id="noVNC_password_input" type="password" value="{{ console_passwd|default_if_none:"" }}">
{% else %}
<input id="noVNC_password_input" type="password">
{% endif %}
</div>
<div class="button_row">
<input id="noVNC_credentials_button" type="submit" value="{% trans 'Send credentials' %}">
</div>
</form></div>
</div>
<!-- Transition Screens -->
<div id="noVNC_transition">
<div id="noVNC_transition_text"></div>
<div>
<input type="button" id="noVNC_cancel_reconnect_button" value="Cancel" class="noVNC_submit" />
</div>
<div class="noVNC_spinner"></div>
</div>
<!-- This is where the RFB elements will attach -->
<div id="noVNC_container">
<!-- Note that Google Chrome on Android doesn't respect any of these,
html attributes which attempt to disable text suggestions on the
on-screen keyboard. Let's hope Chrome implements the ime-mode
style for example -->
<textarea id="noVNC_keyboardinput" autocapitalize="off" autocomplete="off" spellcheck="false"
tabindex="-1"></textarea>
</div>
<audio id="noVNC_bell">
<source src="{% static "js/novnc/app/sounds/bell.oga" %}" type="audio/ogg">
<source src="{% static "js/novnc/app/sounds/bell.mp3" %}" type="audio/mpeg">
</audio>
{% endblock %}

View file

@ -1,207 +0,0 @@
{% extends "console-base.html" %}
{% load i18n %}
{% load static %}
{% block head %}
<!--
noVNC example: lightweight example using minimal UI and features
This is a self-contained file which doesn't import WebUtil or external CSS.
Copyright (C) 2019 The noVNC Authors
noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
Connect parameters are provided in query string:
http://example.com/?scale=true (the proxy host, port and path come from the server)
-->
<!-- Stylesheets -->
<link rel="stylesheet" href='{% static "js/novnc/app/styles/lite.css" %}'>
<!-- actual script modules -->
<script type="module" crossorigin="anonymous">
// RFB holds the API to connect and communicate with a VNC server
import RFB from '{% static "js/novnc/core/rfb.js" %}';
import { bindKeyAction, bindSendKeys } from '{% static "js/console_keys.js" %}';
let rfb;
let desktopName;
// When this function is called we have
// successfully connected to a server
function connectedToServer(e) {
status("Connected to " + desktopName);
}
// This function is called when we are disconnected
function disconnectedFromServer(e) {
if (e.detail.clean) {
status("Disconnected");
} else {
status("Something went wrong, connection is closed");
}
}
// When this function is called, the server requires
// credentials to authenticate
function credentialsAreRequired(e) {
const password = prompt("Password Required:");
rfb.sendCredentials({ password: password });
}
// When this function is called we have received
// a desktop name from the server
function updateDesktopName(e) {
desktopName = e.detail.name;
}
function fullscreen() {
if (document.fullscreenElement || // alternative standard method
document.mozFullScreenElement || // currently working methods
document.webkitFullscreenElement ||
document.msFullscreenElement) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
} else {
const el = document.documentElement;
const request = el.requestFullscreen ||
el.mozRequestFullScreen ||
el.webkitRequestFullscreen ||
el.msRequestFullscreen;
if (request) {
request.call(el).catch(() => {});
}
}
return false;
}
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement) {
if ('keyboard' in navigator && typeof navigator.keyboard.lock === 'function') {
navigator.keyboard.lock(['AltLeft', 'AltRight', 'Tab', 'Escape']).catch(() => {});
}
} else {
if ('keyboard' in navigator && typeof navigator.keyboard.unlock === 'function') {
navigator.keyboard.unlock();
}
}
});
function machineShutdown() {
rfb.machineShutdown();
}
function machineReboot() {
rfb.machineReboot();
}
function machineReset() {
rfb.machineReset();
}
// Show a status text in the top bar
function status(text) {
document.getElementById('noVNC_status').textContent = text;
}
bindKeyAction('machineShutdownButton', machineShutdown);
bindKeyAction('machineRebootButton', machineReboot);
bindKeyAction('machineResetButton', machineReset);
bindKeyAction('fullscreen_button', fullscreen);
// fit the remote screen to the window, or show it 1:1 with scrollbars
bindKeyAction('scale_button', () => {
rfb.scaleViewport = !rfb.scaleViewport;
const link = document.querySelector('#scale_button a');
link.classList.toggle('active', rfb.scaleViewport);
link.setAttribute('aria-pressed', rfb.scaleViewport);
});
bindSendKeys(() => rfb);
// novncd behind the panel's own address (nginx's /novncd/), never a host
// from the link: the page sends it a console token
const host = window.location.hostname;
let port = window.location.port;
// the VNC password, for a user who may open the console without it;
// otherwise the server asks for it (credentialsrequired)
{% if perms.instances.passwordless_console and console_passwd %}
const credentials = { password: '{{ console_passwd|escapejs }}' };
{% else %}
const credentials = {};
{% endif %}
const path = 'novncd/';
// | | | | | |
// | | | Connect | | |
// v v v v v v
status("Connecting");
// Build the websocket URL used to connect
let url;
if (window.location.protocol === "https:") {
url = 'wss';
} else {
url = 'ws';
}
url += '://' + host;
if (port) {
url += ':' + port;
}
if (path) {
url += path.startsWith('/') ? path : ('/' + path);
} else {
url += '/';
}
const token = '{{ token|escapejs }}';
if (token) {
url += (url.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(token);
}
// Creating a new RFB object will start a new connection
rfb = new RFB(document.getElementById('noVNC_container'), url,
{ credentials: credentials });
// Add listeners to important events from the RFB module
rfb.addEventListener("connect", connectedToServer);
rfb.addEventListener("disconnect", disconnectedFromServer);
rfb.addEventListener("credentialsrequired", credentialsAreRequired);
rfb.addEventListener("desktopname", updateDesktopName);
// Set parameters that can be changed on an active connection
rfb.scaleViewport = {{ scale|yesno:"true,false" }};
rfb.viewOnly = {{ view_only|yesno:"true,false" }};
rfb.resizeSession = {{ resize_session|yesno:"true,false" }};
rfb.clipViewport = {{ clip_viewport|yesno:"true,false" }};
</script>
{% endblock %}
{% block navbarmenu %}
{% include "console-menu.html" %}
<li class="nav-item" id="scale_button">
<a class="nav-link{% if scale %} active{% endif %}" href="#" role="button" aria-pressed="{{ scale|yesno:"true,false" }}">{% trans "Scale" %}</a>
</li>
{% endblock %}
{% block content %}
<div id="noVNC_status_bar">
<div id="noVNC_left_dummy_elem"></div>
<div id="noVNC_status">{% trans 'Loading' %}</div>
<div id="noVNC_buttons">
<input type="button" value="Send CtrlAltDel" id="sendCtrlAltDelButton" class="noVNC_shown">
<span id="noVNC_power_buttons" class="noVNC_hidden">
<input type="button" value="Shutdown" id="machineShutdownButton">
<input type="button" value="Reboot" id="machineRebootButton">
<input type="button" value="Reset" id="machineResetButton">
</span>
</div>
</div>
<div id="noVNC_container">
<!-- This is where the remote screen will appear -->
</div>
{% endblock %}

View file

@ -0,0 +1,121 @@
{% extends "console-base.html" %}
{% load i18n %}
{% load static %}
{% block head %}
<style>
#screen { flex: 1 1 auto; min-height: 0; overflow: hidden; }
</style>
{{ vnc|json_script:"vnc-config" }}
<script type="module" src="{% static "js/console_vnc.js" %}"></script>
{% endblock %}
{% block navbarstatus %}
<span id="status" class="badge text-bg-secondary" role="status"
data-connected="{% trans "Connected" %}" data-disconnected="{% trans "Disconnected" %}"
data-lost="{% trans "Connection lost" %}" data-failed="{% trans "Could not connect" %}"
data-refused="{% trans "Authentication failed" %}">{% trans "Connecting" %}</span>
{% endblock %}
{% block navbarmenu %}
{% include "console-menu.html" %}
<li class="nav-item">
<a class="nav-link" href="#" role="button" data-bs-toggle="modal" data-bs-target="#clipboard_dialog">{% trans "Clipboard" %}</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" id="viewMenuLink" data-bs-toggle="dropdown"
data-bs-auto-close="outside" aria-expanded="false">{% trans "View" %}</a>
<form class="dropdown-menu shadow p-3" id="view_menu" aria-labelledby="viewMenuLink" style="min-width: 280px;">
<label class="form-label" for="view_resize">{% trans "Scaling" %}</label>
<select class="form-select form-select-sm mb-3" id="view_resize">
<option value="off">{% trans "None" %}</option>
<option value="scale">{% trans "Fit to the window" %}</option>
<option value="remote">{% trans "Resize the VM's screen to the window" %}</option>
</select>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="view_clip">
<label class="form-check-label" for="view_clip">{% trans "Clip to the window" %}</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="view_drag">
<label class="form-check-label" for="view_drag">{% trans "Drag to move a clipped screen" %}</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="view_only">
<label class="form-check-label" for="view_only">{% trans "View only" %}</label>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="view_dot">
<label class="form-check-label" for="view_dot">{% trans "Show a dot when the VM hides the cursor" %}</label>
</div>
<label class="form-label" for="view_quality">{% trans "Quality" %}</label>
<input class="form-range" type="range" id="view_quality" min="0" max="9" value="6">
<label class="form-label" for="view_compression">{% trans "Compression" %}</label>
<input class="form-range" type="range" id="view_compression" min="0" max="9" value="2">
</form>
</li>
<li class="nav-item" id="keyboard_button" hidden>
<a class="nav-link" href="#" role="button">{% trans "Keyboard" %}</a>
</li>
{% endblock %}
{% block navbarend %}
<button type="button" class="btn btn-sm btn-light" id="connection_button"
data-reconnect="{% trans "Reconnect" %}">{% trans "Disconnect" %}</button>
{% endblock %}
{% block content %}
<div id="screen"></div>
<textarea id="keyboard_input" class="visually-hidden" autocapitalize="off" autocomplete="off"
spellcheck="false" tabindex="-1" aria-label="{% trans "Keyboard" %}"></textarea>
<div class="modal fade" id="credentials_dialog" tabindex="-1" aria-labelledby="credentials_title"
data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<form class="modal-content" id="credentials_form">
<div class="modal-header">
<h5 class="modal-title" id="credentials_title">{% trans "The console asks for its password" %}</h5>
</div>
<div class="modal-body">
<div class="mb-3" id="credentials_username_field" hidden>
<label class="form-label" for="credentials_username">{% trans "Username" %}</label>
<input class="form-control" id="credentials_username" autocomplete="off">
</div>
<label class="form-label" for="credentials_password">{% trans "Password" %}</label>
<input class="form-control" type="password" id="credentials_password" autocomplete="off">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="credentials_cancel">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary">{% trans "Connect" %}</button>
</div>
</form>
</div>
</div>
<div class="modal fade" id="clipboard_dialog" tabindex="-1" aria-labelledby="clipboard_title">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="clipboard_title">{% trans "Clipboard" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans "Close" %}"></button>
</div>
<div class="modal-body">
<label class="form-label" for="clipboard_text">{% trans "Text" %}</label>
<textarea class="form-control font-monospace" id="clipboard_text" rows="6"></textarea>
<div class="form-text">
{% trans "Type it: the VM gets it key by key, also in its firmware, an installer or a login prompt. The VM's clipboard takes it only with clipboard support in the VM; what the VM copies shows here." %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="clipboard_send">{% trans "Send to the VM's clipboard" %}</button>
<button type="button" class="btn btn-primary" id="clipboard_type">{% trans "Type it" %}</button>
</div>
</div>
</div>
</div>
<audio id="bell" preload="none">
<source src="{% static "js/novnc/app/sounds/bell.oga" %}" type="audio/ogg">
<source src="{% static "js/novnc/app/sounds/bell.mp3" %}" type="audio/mpeg">
</audio>
{% endblock %}