Skip to content

Commit 77c4eb9

Browse files
committed
consoles: Redesign and reimplement
- A ToggleGroup in the Card header is used to switch consoles - The DesktopViewer is gone, but there is a footer with a "Launch viewer" button and a "How to connect" popup. - Can't change consoles in the expanded view, but the expanded console keeps the type that was active in the collapsed view. TODO: - Code cleanup, "VncConsole" is now really "GraphicsConsole", etc..
1 parent d2f0841 commit 77c4eb9

File tree

5 files changed

+371
-192
lines changed

5 files changed

+371
-192
lines changed

src/components/vm/consoles/consoles.css

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,21 @@
1818
grid-template-rows: min-content 1fr;
1919
}
2020

21+
.vm-console-footer {
22+
grid-area: 3 / 1 / 4 / 3;
23+
}
24+
2125
.consoles-page-expanded .actions-pagesection .pf-v5-c-page__main-body {
2226
padding-block-end: 0;
2327
}
2428

25-
/* Hide send key button - there is not way to do that from the JS
29+
/* Hide standard VNC actions - there is not way to do that from the JS
2630
* https://github.com/patternfly/patternfly-react/issues/3689
2731
*/
28-
#pf-v5-c-console__send-shortcut {
32+
.pf-v5-c-console__actions-vnc {
2933
display: none;
3034
}
35+
36+
.ct-remote-viewer-popover {
37+
max-inline-size: 60ch;
38+
}

src/components/vm/consoles/consoles.jsx

Lines changed: 120 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { AccessConsoles } from "@patternfly/react-console";
2323
import { Button } from "@patternfly/react-core/dist/esm/components/Button";
2424
import { Card, CardBody, CardFooter, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card';
2525
import { ExpandIcon, HelpIcon } from '@patternfly/react-icons';
26+
import { ToggleGroup, ToggleGroupItem } from '@patternfly/react-core/dist/esm/components/ToggleGroup';
2627

2728
import SerialConsole from './serialConsole.jsx';
2829
import Vnc, { VncState } from './vnc.jsx';
@@ -39,132 +40,118 @@ import './consoles.css';
3940

4041
const _ = cockpit.gettext;
4142

42-
class Consoles extends React.Component {
43-
constructor (props) {
44-
super(props);
43+
export function console_default(vm) {
44+
const serials = vm.displays && vm.displays.filter(display => display.type == 'pty');
45+
const vnc = vm.displays && vm.displays.find(display => display.type == 'vnc');
4546

46-
this.state = {
47-
serial: props.vm.displays && props.vm.displays.filter(display => display.type == 'pty'),
48-
};
49-
50-
this.getDefaultConsole = this.getDefaultConsole.bind(this);
51-
this.onDesktopConsoleDownload = this.onDesktopConsoleDownload.bind(this);
52-
}
53-
54-
static getDerivedStateFromProps(nextProps, prevState) {
55-
const oldSerial = prevState.serial;
56-
const newSerial = nextProps.vm.displays && nextProps.vm.displays.filter(display => display.type == 'pty');
57-
58-
if (newSerial.length !== oldSerial.length || oldSerial.some((pty, index) => pty.alias !== newSerial[index].alias))
59-
return { serial: newSerial };
47+
if (vnc || serials.length == 0)
48+
return "vnc";
49+
else
50+
return "serial0";
51+
}
6052

61-
return null;
53+
export function console_name(vm, type) {
54+
if (!type)
55+
type = console_default(vm);
56+
57+
if (type.startsWith("serial")) {
58+
const serials = vm.displays && vm.displays.filter(display => display.type == 'pty');
59+
if (serials.length == 1)
60+
return _("Serial console");
61+
const idx = Number(type.substr(6));
62+
return cockpit.format(_("Serial console ($0)"), serials[idx]?.alias || idx);
63+
} else if (type == "vnc") {
64+
return _("Graphical console");
65+
} else {
66+
return _("Console");
6267
}
68+
}
6369

64-
getDefaultConsole () {
65-
const { vm } = this.props;
66-
67-
if (vm.displays) {
68-
if (vm.displays.find(display => display.type == "vnc")) {
69-
return 'VncConsole';
70-
}
71-
if (vm.displays.find(display => display.type == "spice")) {
72-
return 'DesktopViewer';
73-
}
70+
function connection_address() {
71+
let address;
72+
if (cockpit.transport.host == "localhost") {
73+
const app = cockpit.transport.application();
74+
if (app.startsWith("cockpit+=")) {
75+
address = app.substr(9);
76+
} else {
77+
address = window.location.hostname;
7478
}
75-
76-
const serialConsoleCommand = domainSerialConsoleCommand({ vm });
77-
if (serialConsoleCommand) {
78-
return 'SerialConsole';
79+
} else {
80+
address = cockpit.transport.host;
81+
const pos = address.indexOf("@");
82+
if (pos >= 0) {
83+
address = address.substr(pos + 1);
7984
}
80-
81-
// no console defined, but the VncConsole is always there and
82-
// will instruct people how to enable it for real.
83-
return 'VncConsole';
8485
}
86+
return address;
87+
}
8588

86-
onDesktopConsoleDownload (type) {
87-
const { vm } = this.props;
88-
// fire download of the .vv file
89-
const consoleDetail = vm.displays.find(display => display.type == type);
90-
91-
let address;
92-
if (cockpit.transport.host == "localhost") {
93-
const app = cockpit.transport.application();
94-
if (app.startsWith("cockpit+=")) {
95-
address = app.substr(9);
96-
} else {
97-
address = window.location.hostname;
98-
}
99-
} else {
100-
address = cockpit.transport.host;
101-
const pos = address.indexOf("@");
102-
if (pos >= 0) {
103-
address = address.substr(pos + 1);
104-
}
105-
}
89+
function console_launch(vm, consoleDetail) {
90+
// fire download of the .vv file
91+
domainDesktopConsole({ name: vm.name, consoleDetail: { ...consoleDetail, address: connection_address() } });
92+
}
93+
94+
export const Console = ({ vm, config, type, onAddErrorNotification, isExpanded }) => {
95+
let con = null;
10696

107-
domainDesktopConsole({ name: vm.name, consoleDetail: { ...consoleDetail, address } });
97+
if (!type)
98+
type = console_default(vm);
99+
100+
if (vm.state != "running") {
101+
const vnc = vm.inactiveXML.displays && vm.inactiveXML.displays.find(display => display.type == 'vnc');
102+
const spice = vm.inactiveXML.displays && vm.inactiveXML.displays.find(display => display.type == 'spice');
103+
return <VncState vm={vm} vnc={vnc} spice={spice} />;
108104
}
109105

110-
render () {
111-
const { vm, onAddErrorNotification, isExpanded } = this.props;
112-
const { serial } = this.state;
113-
const spice = vm.displays && vm.displays.find(display => display.type == 'spice');
106+
if (type.startsWith("serial")) {
107+
const serials = vm.displays && vm.displays.filter(display => display.type == 'pty');
108+
const idx = Number(type.substr(6));
109+
if (serials.length > idx)
110+
con = <SerialConsole
111+
type={type}
112+
connectionName={vm.connectionName}
113+
vmName={vm.name}
114+
spawnArgs={domainSerialConsoleCommand({ vm, alias: serials[idx].alias })} />;
115+
} else if (type == "vnc") {
114116
const vnc = vm.displays && vm.displays.find(display => display.type == 'vnc');
115117
const inactive_vnc = vm.inactiveXML.displays && vm.inactiveXML.displays.find(display => display.type == 'vnc');
118+
const spice = vm.displays && vm.displays.find(display => display.type == 'spice');
116119

117-
if (!domainCanConsole || !domainCanConsole(vm.state)) {
118-
return (
119-
<div id="vm-not-running-message">
120-
<VncState vm={vm} vnc={inactive_vnc} />
121-
</div>
122-
);
123-
}
124-
125-
const onDesktopConsole = () => { // prefer spice over vnc
126-
this.onDesktopConsoleDownload(spice ? 'spice' : 'vnc');
127-
};
120+
con = <Vnc
121+
type="VncConsole"
122+
vm={vm}
123+
consoleDetail={vnc}
124+
spiceDetail={spice}
125+
inactiveConsoleDetail={inactive_vnc}
126+
onAddErrorNotification={onAddErrorNotification}
127+
onLaunch={() => console_launch(vm, vnc || spice)}
128+
connectionAddress={connection_address()}
129+
isExpanded={isExpanded} />;
130+
}
128131

132+
if (con) {
129133
return (
130-
<AccessConsoles preselectedType={this.getDefaultConsole()}
131-
textSelectConsoleType={_("Select console type")}
132-
textSerialConsole={_("Serial console")}
133-
textVncConsole={_("Graphical console")}
134-
textDesktopViewerConsole={_("Desktop viewer")}>
135-
{serial.map((pty, idx) => (<SerialConsole type={serial.length == 1 ? "SerialConsole" : cockpit.format(_("Serial console ($0)"), pty.alias || idx)}
136-
key={"pty-" + idx}
137-
connectionName={vm.connectionName}
138-
vmName={vm.name}
139-
spawnArgs={domainSerialConsoleCommand({ vm, alias: pty.alias })} />))}
140-
<Vnc type="VncConsole"
141-
vm={vm}
142-
consoleDetail={vnc}
143-
inactiveConsoleDetail={inactive_vnc}
144-
onAddErrorNotification={onAddErrorNotification}
145-
isExpanded={isExpanded} />
146-
{(vnc || spice) &&
147-
<DesktopConsole type="DesktopViewer"
148-
onDesktopConsole={onDesktopConsole}
149-
vnc={vnc}
150-
spice={spice} />}
151-
</AccessConsoles>
134+
<div className="pf-v5-c-console">
135+
{con}
136+
</div>
152137
);
153138
}
154-
}
155-
156-
Consoles.propTypes = {
157-
vm: PropTypes.object.isRequired,
158-
onAddErrorNotification: PropTypes.func.isRequired,
159139
};
160140

161-
export default Consoles;
141+
export const ConsoleCard = ({ vm, config, type, setType, onAddErrorNotification }) => {
142+
const serials = vm.displays && vm.displays.filter(display => display.type == 'pty');
162143

163-
export const ConsoleCard = ({ vm, config, onAddErrorNotification }) => {
164-
let actions = null;
165-
if (vm.state != "shut off") {
166-
actions = (
144+
if (!type)
145+
type = console_default(vm);
146+
147+
const actions = [];
148+
const tabs = [];
149+
let body;
150+
151+
if (vm.state == "running") {
152+
actions.push(
167153
<Button
154+
key="expand"
168155
variant="link"
169156
onClick={() => {
170157
const urlOptions = { name: vm.name, connection: vm.connectionName };
@@ -174,6 +161,33 @@ export const ConsoleCard = ({ vm, config, onAddErrorNotification }) => {
174161
iconPosition="right">{_("Expand")}
175162
</Button>
176163
);
164+
165+
if (serials.length > 0)
166+
tabs.push(<ToggleGroupItem
167+
key="vnc"
168+
text={_("Graphical")}
169+
isSelected={type == "vnc"}
170+
onChange={() => setType("vnc")} />);
171+
172+
serials.forEach((pty, idx) => {
173+
const t = "serial" + idx;
174+
tabs.push(<ToggleGroupItem
175+
key={t}
176+
text={serials.length == 1 ? _("Serial") : cockpit.format(_("Serial ($0)"), pty.alias || idx)}
177+
isSelected={type == t}
178+
onChange={() => setType(t)} />);
179+
})
180+
181+
body = <Console
182+
vm={vm}
183+
config={config}
184+
onAddErrorNotification={onAddErrorNotification}
185+
type={type}
186+
isExpanded={false} />
187+
} else {
188+
const vnc = vm.inactiveXML.displays && vm.inactiveXML.displays.find(display => display.type == 'vnc');
189+
const spice = vm.inactiveXML.displays && vm.inactiveXML.displays.find(display => display.type == 'spice');
190+
body = <VncState vm={vm} vnc={vnc} spice={spice} />;
177191
}
178192

179193
return (
@@ -184,9 +198,10 @@ export const ConsoleCard = ({ vm, config, onAddErrorNotification }) => {
184198
isClickable>
185199
<CardHeader actions={{ actions }}>
186200
<CardTitle component="h2">{_("Console")}</CardTitle>
201+
<ToggleGroup>{tabs}</ToggleGroup>
187202
</CardHeader>
188203
<CardBody>
189-
<Consoles vm={vm} config={config} onAddErrorNotification={onAddErrorNotification} />
204+
{body}
190205
</CardBody>
191206
<CardFooter />
192207
</Card>

src/components/vm/consoles/serialConsole.jsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
2121
import cockpit from 'cockpit';
2222

2323
import { Button } from "@patternfly/react-core/dist/esm/components/Button";
24+
import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";
2425
import { Terminal } from "cockpit-components-terminal.jsx";
2526

2627
const _ = cockpit.gettext;
@@ -89,15 +90,21 @@ class SerialConsoleCockpit extends React.Component {
8990

9091
return (
9192
<>
92-
<div className="pf-v5-c-console__actions-serial">
93-
{this.state.channel
94-
? <Button id={this.props.vmName + "-serialconsole-disconnect"} variant="secondary" onClick={this.onDisconnect}>{_("Disconnect")}</Button>
95-
: <Button id={this.props.vmName + "-serialconsole-connect"} variant="secondary" onClick={() => this.createChannel(this.props.spawnArgs)}>{_("Connect")}</Button>
96-
}
97-
</div>
9893
<div id={pid} className="vm-terminal pf-v5-c-console__serial">
9994
{t}
10095
</div>
96+
<div className="vm-console-footer">
97+
<Split>
98+
<SplitItem isFilled>
99+
</SplitItem>
100+
<SplitItem>
101+
{this.state.channel
102+
? <Button id={this.props.vmName + "-serialconsole-disconnect"} variant="secondary" onClick={this.onDisconnect}>{_("Disconnect")}</Button>
103+
: <Button id={this.props.vmName + "-serialconsole-connect"} variant="secondary" onClick={() => this.createChannel(this.props.spawnArgs)}>{_("Connect")}</Button>
104+
}
105+
</SplitItem>
106+
</Split>
107+
</div>
101108
</>
102109
);
103110
}

0 commit comments

Comments
 (0)