Skip to content

consoles: Redesign #2008

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion node_modules
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
},
"dependencies": {
"@patternfly/patternfly": "6.1.0",
"@patternfly/react-console": "6.0.0",
"@patternfly/react-core": "6.1.0",
"@patternfly/react-icons": "6.1.0",
"@patternfly/react-styles": "6.1.0",
Expand Down
51 changes: 50 additions & 1 deletion src/components/common/needsShutdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,47 @@ export function needsShutdownSpice(vm) {
return vm.hasSpice !== vm.inactiveXML.hasSpice;
}

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

const active_vnc = find_vnc(vm);
const inactive_vnc = find_vnc(vm.inactiveXML);

if (inactive_vnc) {
if (!active_vnc)
return true;

// The active_vnc.port value is the actual port allocated at
// machine start, it is never -1. Thus, we can't just compare
// inactive_vnc.port with active_vnc.port here when
// inactive_vnc.port is -1. Also, when inactive_vnc.port _is_
// -1, we can't tell whether active_vnc.port has been
// allocated based on some old fixed port in inactive_vnc.port
// (in which case we might want to shutdown and restart), or
// whether it was allocated dynamically (in which case we
// don't want to). But luckily that doesn't really matter and
// a shutdown would not have any useful effect anyway, so we
// don't have to worry that we are missing a notification for
// a pending shutdown.
//
if (inactive_vnc.port != -1 && active_vnc.port != inactive_vnc.port)
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.


if (active_vnc.password != inactive_vnc.password)
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

}

return false;
}

export function needsShutdownSerialConsole(vm) {
const serials = vm.displays && vm.displays.filter(display => display.type == 'pty');
const inactive_serials = vm.inactiveXML.displays && vm.inactiveXML.displays.filter(display => display.type == 'pty');
return serials.length != inactive_serials.length;
}

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

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

// Serial console
if (needsShutdownSerialConsole(vm))
devices.push(_("Text console"));

// TPM
if (needsShutdownTpm(vm))
devices.push(_("TPM"));
Expand Down Expand Up @@ -172,7 +221,7 @@ export const VmNeedsShutdown = ({ vm }) => {
position="bottom"
hasAutoWidth
bodyContent={body}>
<Label className="resource-state-text" color="teal" id={`vm-${vm.name}-needs-shutdown`}
<Label className="resource-state-text" status="custom" id={`vm-${vm.name}-needs-shutdown`}
icon={<PendingIcon />} onClick={() => null}>
{_("Changes pending")}
</Label>
Expand Down
153 changes: 153 additions & 0 deletions src/components/vm/consoles/VncConsole.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*

MIT License

Copyright (c) 2025 Red Hat, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

*/

import React from 'react';

import { initLogging } from '@novnc/novnc/lib/util/logging';
import RFB_module from '@novnc/novnc/lib/rfb';
const RFB = RFB_module.default;

export const VncConsole = ({
children,
host,
port = '80',
path = '',
encrypt = false,
resizeSession = true,
clipViewport = false,
dragViewport = false,
scaleViewport = false,
viewOnly = false,
shared = false,
credentials,
repeaterID = '',
vncLogging = 'warn',
consoleContainerId,
onDisconnected = () => {},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

onInitFailed,
onSecurityFailure,
}) => {
const rfb = React.useRef();

const novncElem = React.useRef(null);

const onConnected = () => {
};

const _onDisconnected = React.useCallback(
(e) => {
onDisconnected(e);
},
[onDisconnected]
);

const _onSecurityFailure = React.useCallback(
(e) => {
onSecurityFailure(e);
Comment on lines +68 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test.

},
[onSecurityFailure]
);

const addEventListeners = React.useCallback(() => {
if (rfb.current) {
rfb.current?.addEventListener('connect', onConnected);
rfb.current?.addEventListener('disconnect', _onDisconnected);
rfb.current?.addEventListener('securityfailure', _onSecurityFailure);
}
}, [rfb, _onDisconnected, _onSecurityFailure]);

const removeEventListeners = React.useCallback(() => {
if (rfb.current) {
rfb.current.removeEventListener('connect', onConnected);
rfb.current.removeEventListener('disconnect', _onDisconnected);
rfb.current.removeEventListener('securityfailure', _onSecurityFailure);
}
}, [rfb, _onDisconnected, _onSecurityFailure]);

const connect = React.useCallback(() => {
const protocol = encrypt ? 'wss' : 'ws';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

const url = `${protocol}://${host}:${port}/${path}`;

const options = {
repeaterID,
shared,
credentials
};
rfb.current = new RFB(novncElem.current, url, options);
addEventListeners();
rfb.current.viewOnly = viewOnly;
rfb.current.clipViewport = clipViewport;
rfb.current.dragViewport = dragViewport;
rfb.current.scaleViewport = scaleViewport;
rfb.current.resizeSession = resizeSession;
}, [
addEventListeners,
host,
path,
port,
resizeSession,
clipViewport,
dragViewport,
scaleViewport,
viewOnly,
encrypt,
rfb,
repeaterID,
shared,
credentials
]);

React.useEffect(() => {
initLogging(vncLogging);
try {
connect();
} catch (e) {
onInitFailed && onInitFailed(e);
rfb.current = undefined;
Comment on lines +127 to +129
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 3 added lines are not executed by any test.

}

return () => {
disconnect();
removeEventListeners();
rfb.current = undefined;
};
}, [connect, onInitFailed, removeEventListeners, vncLogging]);

const disconnect = () => {
if (!rfb.current) {
return;
Comment on lines +140 to +141
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test.

}
rfb.current.disconnect();
};

return (
<div className="vm-console-vnc">
<div id={consoleContainerId} ref={novncElem} />
</div>
);
};

VncConsole.displayName = 'VncConsole';
127 changes: 127 additions & 0 deletions src/components/vm/consoles/common.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2025 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/

import React from 'react';
import cockpit from 'cockpit';

import { Button } from "@patternfly/react-core/dist/esm/components/Button";
import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content";
import { ClipboardCopy } from "@patternfly/react-core/dist/esm/components/ClipboardCopy/index.js";
import { Popover } from "@patternfly/react-core/dist/esm/components/Popover";
import { HelpIcon } from "@patternfly/react-icons";
import { fmt_to_fragments } from 'utils.jsx';
import { domainDesktopConsole } from '../../../libvirtApi/domain.js';

const _ = cockpit.gettext;

export function connection_address() {
let address;
if (cockpit.transport.host == "localhost") {
const app = cockpit.transport.application();
if (app.startsWith("cockpit+=")) {
address = app.substr(9);
Comment on lines +37 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test.

} else {
address = window.location.hostname;
}
} else {
address = cockpit.transport.host;
const pos = address.indexOf("@");
if (pos >= 0) {
address = address.substr(pos + 1);
}
}
return address;
}

export function console_launch(vm, consoleDetail) {
// fire download of the .vv file
domainDesktopConsole({ name: vm.name, consoleDetail: { ...consoleDetail, address: connection_address() } });
}

const RemoteConnectionInfo = ({ hide, url, onEdit }) => {
function CDD(term, description) {
// What is this? Java?
return (
<>
<Content component={ContentVariants.dt}>{term}</Content>
<Content component={ContentVariants.dd}>{description}</Content>
</>
);
}

return (
<>
<Content component={ContentVariants.p}>
{fmt_to_fragments(_("Clicking \"Launch viewer\" will download a $0 file and launch the Remote Viewer application on your system."), <code>.vv</code>)}
</Content>
<Content component={ContentVariants.p}>
{_("Remote Viewer is available for most operating systems. To install it, search for \"Remote Viewer\" in GNOME Software, KDE Discover, or run the following:")}
</Content>
<Content component={ContentVariants.dl}>
{CDD(_("RHEL, CentOS"), <code>sudo yum install virt-viewer</code>)}
{CDD(_("Fedora"), <code>sudo dnf install virt-viewer</code>)}
{CDD(_("Ubuntu, Debian"), <code>sudo apt-get install virt-viewer</code>)}
{CDD(_("Windows"),
fmt_to_fragments(
_("Download the MSI from $0"),
<a href="https://virt-manager.org/download" target="_blank" rel="noopener noreferrer">
virt-manager.org
</a>))}
</Content>
{ url &&
<>
<Content component={ContentVariants.p}>
{_("Other remote viewer applications can connect to the following address:")}
</Content>
<ClipboardCopy
hoverTip={_("Copy to clipboard")}
clickTip={_("Successfully copied to clipboard!")}
variant="inline-compact"
>
{url}
</ClipboardCopy>
<Content component={ContentVariants.p} />
</>
}
{ onEdit &&
<Button isInline variant="link" onClick={() => { hide(); onEdit() }}>
{_("Set port and password")}
</Button>
}
</>
);
};

export const RemoteConnectionPopover = ({ url, onEdit }) => {
return (
<Popover
className="ct-remote-viewer-popover"
headerContent={_("Remote viewer")}
bodyContent={(hide) =>
<RemoteConnectionInfo
hide={hide}
url={url}
onEdit={onEdit}
/>
}
>
<Button icon={<HelpIcon />} variant="plain" />
</Popover>
);
};
Loading
Loading