-
Notifications
You must be signed in to change notification settings - Fork 83
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
base: main
Are you sure you want to change the base?
consoles: Redesign #2008
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
||
if (active_vnc.password != inactive_vnc.password) | ||
return true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 []; | ||
|
@@ -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")); | ||
|
@@ -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> | ||
|
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 = () => {}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'; |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
); | ||
}; |
There was a problem hiding this comment.
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.