Skip to content

Commit a192cf0

Browse files
committed
consoles: Redesign and reimplement
- A ToggleGroup in the Card header is used to switch consoles - The console switcher is also there for shut-off machines. This gives us a place type-specific actions that also make sense for a shut-off machine, like editing VNC server settings. - The DesktopViewer is gone, but there is a footer with a "Launch viewer" button and a "How to connect" popover. - Actions for the Graphics and Serial consoles are collected into kebab menus. - The expanded console has less UI around it, and it keeps the type that was active in the collapsed view. Instead of the breadcrumb it has a "Collapse" button. - When there is no actual console for a given type, there is now a EmptyState component where you can activate it. (VNC and serial) - It is possible to change VNC server settings via the "How to connect" popup.g - The SPICE console invites you to replace it with VNC. - The size of the expanded console is now always controlled by the browser window and never overflows in height.
1 parent 6cab937 commit a192cf0

15 files changed

+1499
-494
lines changed

src/components/common/needsShutdown.jsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,47 @@ export function needsShutdownSpice(vm) {
8585
return vm.hasSpice !== vm.inactiveXML.hasSpice;
8686
}
8787

88+
export function needsShutdownVnc(vm) {
89+
function find_vnc(v) {
90+
return v.displays && v.displays.find(d => d.type == "vnc");
91+
}
92+
93+
const active_vnc = find_vnc(vm);
94+
const inactive_vnc = find_vnc(vm.inactiveXML);
95+
96+
if (inactive_vnc) {
97+
if (!active_vnc)
98+
return true;
99+
100+
// The active_vnc.port value is the actual port allocated at
101+
// machine start, it is never -1. Thus, we can't just compare
102+
// inactive_vnc.port with active_vnc.port here when
103+
// inactive_vnc.port is -1. Also, when inactive_vnc.port _is_
104+
// -1, we can't tell whether active_vnc.port has been
105+
// allocated based on some old fixed port in inactive_vnc.port
106+
// (in which case we might want to shutdown and restart), or
107+
// whether it was allocated dynamically (in which case we
108+
// don't want to). But luckily that doesn't really matter and
109+
// a shutdown would not have any useful effect anyway, so we
110+
// don't have to worry that we are missing a notification for
111+
// a pending shutdown.
112+
//
113+
if (inactive_vnc.port != -1 && active_vnc.port != inactive_vnc.port)
114+
return true;
115+
116+
if (active_vnc.password != inactive_vnc.password)
117+
return true;
118+
}
119+
120+
return false;
121+
}
122+
123+
export function needsShutdownSerialConsole(vm) {
124+
const serials = vm.displays && vm.displays.filter(display => display.type == 'pty');
125+
const inactive_serials = vm.inactiveXML.displays && vm.inactiveXML.displays.filter(display => display.type == 'pty');
126+
return serials.length != inactive_serials.length;
127+
}
128+
88129
export function getDevicesRequiringShutdown(vm) {
89130
if (!vm.persistent)
90131
return [];
@@ -125,6 +166,14 @@ export function getDevicesRequiringShutdown(vm) {
125166
if (needsShutdownSpice(vm))
126167
devices.push(_("SPICE"));
127168

169+
// VNC
170+
if (needsShutdownVnc(vm))
171+
devices.push(_("VNC"));
172+
173+
// Serial console
174+
if (needsShutdownSerialConsole(vm))
175+
devices.push(_("Text console"));
176+
128177
// TPM
129178
if (needsShutdownTpm(vm))
130179
devices.push(_("TPM"));

src/components/vm/consoles/common.jsx

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* This file is part of Cockpit.
3+
*
4+
* Copyright (C) 2025 Red Hat, Inc.
5+
*
6+
* Cockpit is free software; you can redistribute it and/or modify it
7+
* under the terms of the GNU Lesser General Public License as published by
8+
* the Free Software Foundation; either version 2.1 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* Cockpit is distributed in the hope that it will be useful, but
12+
* WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
import React from 'react';
21+
import cockpit from 'cockpit';
22+
23+
import { Button } from "@patternfly/react-core/dist/esm/components/Button";
24+
import { Text, TextContent, TextVariants, TextList, TextListItem, TextListItemVariants, TextListVariants } from "@patternfly/react-core/dist/esm/components/Text";
25+
import { ClipboardCopy } from "@patternfly/react-core/dist/esm/components/ClipboardCopy/index.js";
26+
import { fmt_to_fragments } from 'utils.jsx';
27+
import { domainDesktopConsole } from '../../../libvirtApi/domain.js';
28+
29+
const _ = cockpit.gettext;
30+
31+
export function connection_address() {
32+
let address;
33+
if (cockpit.transport.host == "localhost") {
34+
const app = cockpit.transport.application();
35+
if (app.startsWith("cockpit+=")) {
36+
address = app.substr(9);
37+
} else {
38+
address = window.location.hostname;
39+
}
40+
} else {
41+
address = cockpit.transport.host;
42+
const pos = address.indexOf("@");
43+
if (pos >= 0) {
44+
address = address.substr(pos + 1);
45+
}
46+
}
47+
return address;
48+
}
49+
50+
export function console_launch(vm, consoleDetail) {
51+
// fire download of the .vv file
52+
domainDesktopConsole({ name: vm.name, consoleDetail: { ...consoleDetail, address: connection_address() } });
53+
}
54+
55+
export const RemoteConnectionInfo = ({ hide, url, onEdit }) => {
56+
function TLI(term, description) {
57+
// What is this? Java?
58+
return (
59+
<>
60+
<TextListItem component={TextListItemVariants.dt}>{term}</TextListItem>
61+
<TextListItem component={TextListItemVariants.dd}>{description}</TextListItem>
62+
</>
63+
);
64+
}
65+
66+
return (
67+
<TextContent>
68+
<Text component={TextVariants.p}>
69+
{fmt_to_fragments(_("Clicking \"Launch viewer\" will download a $0 file and launch the Remote Viewer application on your system."), <code>.vv</code>)}
70+
</Text>
71+
<Text component={TextVariants.p}>
72+
{_("Remote Viewer is available for most operating systems. To install it, search for \"Remote Viewer\" in GNOME Software, KDE Discover, or run the following:")}
73+
</Text>
74+
<TextList component={TextListVariants.dl}>
75+
{TLI(_("RHEL, CentOS"), <code>sudo yum install virt-viewer</code>)}
76+
{TLI(_("Fedora"), <code>sudo dnf install virt-viewer</code>)}
77+
{TLI(_("Ubuntu, Debian"), <code>sudo apt-get install virt-viewer</code>)}
78+
{TLI(_("Windows"),
79+
fmt_to_fragments(
80+
_("Download the MSI from $0"),
81+
<a href="https://virt-manager.org/download" target="_blank" rel="noopener noreferrer">
82+
virt-manager.org
83+
</a>))}
84+
</TextList>
85+
{ url &&
86+
<>
87+
<Text component={TextVariants.p}>
88+
{_("Other remote viewer applications can connect to the following address:")}
89+
</Text>
90+
<ClipboardCopy
91+
hoverTip={_("Copy to clipboard")}
92+
clickTip={_("Successfully copied to clipboard!")}
93+
variant="inline-compact"
94+
>
95+
{url}
96+
</ClipboardCopy>
97+
<Text component={TextVariants.p} />
98+
</>
99+
}
100+
{ onEdit &&
101+
<Button isInline variant="link" onClick={() => { hide(); onEdit() }}>
102+
{_("Set port and password")}
103+
</Button>
104+
}
105+
</TextContent>
106+
);
107+
};

src/components/vm/consoles/consoles.css

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
.pf-v5-c-console__vnc > div > div {
77
inline-size: 100%;
88
block-size: 100%;
9+
max-block-size: 100%;
10+
}
11+
12+
.pf-v5-c-console {
13+
row-gap: 0;
914
}
1015

1116
.pf-v5-c-console__actions {
@@ -18,13 +23,52 @@
1823
grid-template-rows: min-content 1fr;
1924
}
2025

26+
.vm-console-footer {
27+
grid-area: 3 / 1 / 4 / 3;
28+
margin-block-start: var(--pf-v5-global--spacer--md);
29+
}
30+
2131
.consoles-page-expanded .actions-pagesection .pf-v5-c-page__main-body {
22-
padding-block-end: 0;
32+
padding-block-end: 0;
2333
}
2434

25-
/* Hide send key button - there is not way to do that from the JS
35+
/* Hide standard VNC actions - there is not way to do that from the JS
2636
* https://github.com/patternfly/patternfly-react/issues/3689
2737
*/
28-
#pf-v5-c-console__send-shortcut {
38+
.pf-v5-c-console__actions-vnc {
2939
display: none;
3040
}
41+
42+
.consoles-card .pf-v5-c-empty-state__icon {
43+
color: var(--pf-v5-global--custom-color--200);
44+
}
45+
46+
.ct-remote-viewer-popover {
47+
max-inline-size: 60ch;
48+
}
49+
50+
.consoles-card {
51+
block-size: 100%;
52+
max-block-size: 100%;
53+
}
54+
55+
.consoles-page-expanded .pf-v5-c-console {
56+
display: block;
57+
}
58+
59+
.consoles-page-expanded .pf-v5-c-console__vnc {
60+
margin-block-end: 15px;
61+
}
62+
63+
.consoles-page-expanded .consoles-card .pf-v5-c-card__body {
64+
/* 100% - card header height (padding-top + title line height + padding-bottom) */
65+
max-block-size: calc(100% - (var(--pf-v5-c-card--first-child--PaddingTop) + (var(--pf-v5-c-card__title-text--LineHeight) * var(--pf-v5-global--FontSize--2xl)) + var(--pf-v5-c-card__title--not--last-child--PaddingBottom)));
66+
}
67+
68+
.consoles-card .pf-v5-c-card__footer:empty {
69+
display: none;
70+
}
71+
72+
.consoles-card .pf-v5-c-console__serial {
73+
block-size: 100%;
74+
}

0 commit comments

Comments
 (0)