Skip to content

Commit

Permalink
consoles: Allow adding and editing VNC for a running machine
Browse files Browse the repository at this point in the history
(Except for editing when there is an active VNC inline console. Then
we don't have a place for the button yet.)
  • Loading branch information
mvollmer committed Jan 27, 2025
1 parent e3ce3da commit 1880020
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 80 deletions.
11 changes: 11 additions & 0 deletions src/components/common/needsShutdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ export function needsShutdownSpice(vm) {
return vm.hasSpice !== vm.inactiveXML.hasSpice;
}

export function needsShutdownVnc(vm) {
function has_vnc(v) {
return v.displays && v.displays.find(d => d.type == "vnc") != null;
}
return has_vnc(vm) != has_vnc(vm.inactiveXML);
}

export function getDevicesRequiringShutdown(vm) {
if (!vm.persistent)
return [];
Expand Down Expand Up @@ -125,6 +132,10 @@ export function getDevicesRequiringShutdown(vm) {
if (needsShutdownSpice(vm))
devices.push(_("SPICE"));

// VNC
if (needsShutdownVnc(vm))
devices.push(_("VNC"));

// TPM
if (needsShutdownTpm(vm))
devices.push(_("TPM"));
Expand Down
76 changes: 10 additions & 66 deletions src/components/vm/consoles/consoles.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ import PropTypes from 'prop-types';
import cockpit from 'cockpit';
import { AccessConsoles } from "@patternfly/react-console";
import { Button } from "@patternfly/react-core/dist/esm/components/Button";

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import Button.
import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";

import { useDialogs } from 'dialogs.jsx';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import useDialogs.
import SerialConsole from './serialConsole.jsx';
import Vnc from './vnc.jsx';
import Vnc, { VncState } from './vnc.jsx';
import DesktopConsole from './desktopConsole.jsx';
import { AddVNC } from './vncAdd.jsx';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import AddVNC.
import { EditVNCModal } from './vncEdit.jsx';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import EditVNCModal.
Expand All @@ -40,63 +39,6 @@ import './consoles.css';

const _ = cockpit.gettext;

const VmNotRunning = ({ vm, vnc }) => {
const Dialogs = useDialogs();

function add_vnc() {
Dialogs.show(<AddVNC
idPrefix="add-vnc"
vm={vm} />);
}

function edit_vnc() {
Dialogs.show(<EditVNCModal
idPrefix="edit-vnc"
consoleDetail={vnc}
vmName={vm.name}
vmId={vm.id}
connectionName={vm.connectionName} />);
}

let vnc_info;
let vnc_action;

if (!vnc) {
vnc_info = _("not supported");
vnc_action = (
<Button variant="link" isInline onClick={add_vnc}>
{_("Add support")}
</Button>
);
} else {
if (vnc.port == -1)
vnc_info = _("VNC, dynamic port");
else
vnc_info = cockpit.format(_("VNC, port $0"), vnc.port);

vnc_action = (
<Button variant="link" isInline onClick={edit_vnc}>
{_("Edit")}
</Button>
);
}

return (
<div id="vm-not-running-message">
<p>{_("Please start the virtual machine to access its console.")}</p>
<br />
<Split hasGutter>
<SplitItem isFilled>
<span><b>{_("Graphical console:")}</b> {vnc_info}</span>
</SplitItem>
<SplitItem>
{vnc_action}
</SplitItem>
</Split>
</div>
);
};

class Consoles extends React.Component {
constructor (props) {
super(props);
Expand Down Expand Up @@ -152,33 +94,35 @@ class Consoles extends React.Component {
const { serial } = this.state;
const spice = vm.displays && vm.displays.find(display => display.type == 'spice');
const vnc = vm.displays && vm.displays.find(display => display.type == 'vnc');
const inactive_vnc = vm.inactiveXML.displays && vm.inactiveXML.displays.find(display => display.type == 'vnc');

if (!domainCanConsole || !domainCanConsole(vm.state)) {
return (<VmNotRunning vm={vm} vnc={vnc} />);
return (
<div id="vm-not-running-message">
<VncState vm={vm} vnc={inactive_vnc} />
</div>
);
}

const onDesktopConsole = () => { // prefer spice over vnc
this.onDesktopConsoleDownload(spice ? 'spice' : 'vnc');
};

console.log("R");

return (
<AccessConsoles preselectedType={this.getDefaultConsole()}
textSelectConsoleType={_("Select console type")}
textSerialConsole={_("Serial console")}
textVncConsole={_("Graphical console (VNC)")}
textVncConsole={_("Graphical console")}
textDesktopViewerConsole={_("Desktop viewer")}>
{serial.map((pty, idx) => (<SerialConsole type={serial.length == 1 ? "SerialConsole" : cockpit.format(_("Serial console ($0)"), pty.alias || idx)}
key={"pty-" + idx}
connectionName={vm.connectionName}
vmName={vm.name}
spawnArgs={domainSerialConsoleCommand({ vm, alias: pty.alias })} />))}
<Vnc type="VncConsole"
vmName={vm.name}
vmId={vm.id}
connectionName={vm.connectionName}
vm={vm}
consoleDetail={vnc}
inactiveConsoleDetail={inactive_vnc}
onAddErrorNotification={onAddErrorNotification}
isExpanded={isExpanded} />
{(vnc || spice) &&
Expand Down
106 changes: 94 additions & 12 deletions src/components/vm/consoles/vnc.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ import cockpit from 'cockpit';
import { VncConsole } from '@patternfly/react-console';
import { Dropdown, DropdownItem, DropdownList } from "@patternfly/react-core/dist/esm/components/Dropdown";
import { MenuToggle } from "@patternfly/react-core/dist/esm/components/MenuToggle";
import { Button } from "@patternfly/react-core/dist/esm/components/Button";
import { Divider } from "@patternfly/react-core/dist/esm/components/Divider";
import { EmptyState, EmptyStateBody } from "@patternfly/react-core/dist/esm/components/EmptyState";
import { EmptyState, EmptyStateBody, EmptyStateFooter } from "@patternfly/react-core/dist/esm/components/EmptyState";
import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";

import { useDialogs } from 'dialogs.jsx';

import { logDebug } from '../../../helpers.js';
import { domainSendKey } from '../../../libvirtApi/domain.js';
import { AddVNC } from './vncAdd.jsx';
import { EditVNCModal } from './vncEdit.jsx';

const _ = cockpit.gettext;
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
Expand All @@ -49,6 +55,84 @@ const Enum = {
KEY_DELETE: 111,
};

export const VncState = ({ vm, vnc }) => {
const Dialogs = useDialogs();

function add_vnc() {
Dialogs.show(<AddVNC idPrefix="add-vnc" vm={vm} />);
}

function edit_vnc() {
Dialogs.show(<EditVNCModal
idPrefix="edit-vnc"
consoleDetail={vnc}
vmName={vm.name}
vmId={vm.id}
connectionName={vm.connectionName} />);
}

if (vm.state == "running" && !vnc) {
return (
<EmptyState>
<EmptyStateBody>
{_("Graphical support not enabled.")}
</EmptyStateBody>
<EmptyStateFooter>
<Button variant="secondary" onClick={add_vnc}>
{_("Add VNC")}
</Button>
</EmptyStateFooter>
</EmptyState>
);
}

let vnc_info;
let vnc_action;

if (!vnc) {
vnc_info = _("not supported");
vnc_action = (
<Button variant="link" isInline onClick={add_vnc}>
{_("Add support")}
</Button>
);
} else {
if (vnc.port == -1)
vnc_info = _("VNC, dynamic port");
else
vnc_info = cockpit.format(_("VNC, port $0"), vnc.port);

vnc_action = (
<Button variant="link" isInline onClick={edit_vnc}>
{_("Edit")}
</Button>
);
}

return (
<>
<p>
{
vm.state == "running"
? _("Shut down and restart the virtual machine to access the graphical console.")
: _("Please start the virtual machine to access its console.")
}
</p>
<br />
<div>
<Split hasGutter>
<SplitItem isFilled>
<span><b>{_("Graphical console:")}</b> {vnc_info}</span>
</SplitItem>
<SplitItem>
{vnc_action}
</SplitItem>
</Split>
</div>
</>
);
};

class Vnc extends React.Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -116,19 +200,17 @@ class Vnc extends React.Component {
}

render() {
const { consoleDetail, connectionName, vmName, vmId, onAddErrorNotification, isExpanded } = this.props;
const { consoleDetail, inactiveConsoleDetail, vm, onAddErrorNotification, isExpanded } = this.props;
const { path, isActionOpen } = this.state;

if (!consoleDetail) {
return (
<div className="pf-v5-c-console__vnc">
<EmptyState>
<EmptyStateBody>
{_("Graphical console not supported. Shut down the virtual machine to add support.")}
</EmptyStateBody>
</EmptyState>
<VncState vm={vm} vnc={inactiveConsoleDetail} />
</div>
);
}

if (!path) {
// postpone rendering until consoleDetail is known and channel ready
return null;
Expand All @@ -141,11 +223,11 @@ class Vnc extends React.Component {
id={cockpit.format("ctrl-alt-$0", keyName)}
key={cockpit.format("ctrl-alt-$0", keyName)}
onClick={() => {
return domainSendKey({ connectionName, id: vmId, keyCodes: [Enum.KEY_LEFTCTRL, Enum.KEY_LEFTALT, Enum[cockpit.format("KEY_$0", keyName.toUpperCase())]] })
return domainSendKey({ connectionName: vm.connectionName, id: vm.id, keyCodes: [Enum.KEY_LEFTCTRL, Enum.KEY_LEFTALT, Enum[cockpit.format("KEY_$0", keyName.toUpperCase())]] })
.catch(ex => onAddErrorNotification({
text: cockpit.format(_("Failed to send key Ctrl+Alt+$0 to VM $1"), keyName, vmName),
text: cockpit.format(_("Failed to send key Ctrl+Alt+$0 to VM $1"), keyName, vm.name),
detail: ex.message,
resourceId: vmId,
resourceId: vm.id,
}));
}}>
{cockpit.format(_("Ctrl+Alt+$0"), keyName)}
Expand All @@ -159,10 +241,10 @@ class Vnc extends React.Component {
];
const additionalButtons = [
<Dropdown onSelect={this.onExtraKeysDropdownToggle}
key={cockpit.format("$0-$1-vnc-sendkey", vmName, connectionName)}
key={cockpit.format("$0-$1-vnc-sendkey", vm.name, vm.connectionName)}
toggle={(toggleRef) => (
<MenuToggle
id={cockpit.format("$0-$1-vnc-sendkey", vmName, connectionName)}
id={cockpit.format("$0-$1-vnc-sendkey", vm.name, vm.connectionName)}
ref={toggleRef}
onClick={(_event) => this.setState({ isActionOpen: !isActionOpen })}>
{_("Send key")}
Expand Down
2 changes: 1 addition & 1 deletion src/components/vm/consoles/vncBody.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const VncRow = ({ idPrefix, onValueChanged, dialogValues, validationError
export function validateDialogValues(values) {
const res = { };

if (!values.vncPort.match("^[0-9]+$") || Number(values.vncPort) < 5900)
if (values.vncCustomPort && (!values.vncPort.match("^[0-9]+$") || Number(values.vncPort) < 5900))
res.vncPort = _("Port must be 5900 or larger.");

return Object.keys(res).length > 0 ? res : null;
Expand Down
2 changes: 1 addition & 1 deletion src/components/vm/consoles/vncEdit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class EditVNCModal extends React.Component {
const vncParams = {
connectionName: this.state.connectionName,
vmName: this.state.vmName,
vncAddress: this.props.consoleDetail.address,
vncAddress: this.props.consoleDetail.address || "",
vncPort: this.state.vncCustomPort ? this.state.vncPort : "",
vncPassword: this.state.vncPassword || "",
};
Expand Down

0 comments on commit 1880020

Please sign in to comment.