Skip to content
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

consoles: Some redesign #1972

Closed
wants to merge 7 commits into from
Closed
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
8 changes: 8 additions & 0 deletions src/components/vm/consoles/consoles.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
grid-template-rows: min-content 1fr;
}

.vm-console-main {
grid-area: main;
}

.vm-console-footer {
grid-area: 3 / 1 / 4 / 3;
}

.consoles-page-expanded .actions-pagesection .pf-v5-c-page__main-body {
padding-block-end: 0;
}
Expand Down
72 changes: 58 additions & 14 deletions src/components/vm/consoles/consoles.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@ import React from 'react';
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";

import { useDialogs } from 'dialogs.jsx';
import SerialConsole from './serialConsole.jsx';
import Vnc from './vnc.jsx';
import DesktopConsole from './desktopConsole.jsx';
import { ReplaceSpiceDialog } from '../vmReplaceSpiceDialog.jsx';
import { AddVNC } from './vncAdd.jsx';
import { EditVNCModal } from './vncEdit.jsx';

import {
domainCanConsole,
domainDesktopConsole,
Expand All @@ -34,10 +40,50 @@ import './consoles.css';

const _ = cockpit.gettext;

const VmNotRunning = () => {
const VmNotRunning = ({ vm, vnc, spice }) => {
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} />);
}

return (
<div id="vm-not-running-message">
{_("Please start the virtual machine to access its console.")}
<div>{_("Please start the virtual machine to access its console.")}</div>
{ vnc
? <div>
<b>{_("VNC")}</b> {vnc.address}:{vnc.port}
<Button variant="link" onClick={edit_vnc}>
{_("Edit")}
</Button>
</div>
: <div>
<b>{_("VNC")}</b> {_("not supported.")}
<Button variant="link" onClick={add_vnc}>
{_("Add support")}
</Button>
</div>
}
{ spice
? <div>
<b>{_("Spice")}</b> {spice.address}:{spice.port}
<Button variant="link" onClick={() => Dialogs.show(<ReplaceSpiceDialog vm={vm} vms={[vm]} />)}>
{_("Replace SPICE devices")}
</Button>
</div>
: null
}
</div>
);
};
Expand Down Expand Up @@ -98,7 +144,7 @@ class Consoles extends React.Component {
const vnc = vm.displays && vm.displays.find(display => display.type == 'vnc');

if (!domainCanConsole || !domainCanConsole(vm.state)) {
return (<VmNotRunning />);
return (<VmNotRunning vm={vm} vnc={vnc} spice={spice} />);
}

const onDesktopConsole = () => { // prefer spice over vnc
Expand All @@ -110,25 +156,23 @@ class Consoles extends React.Component {
textSelectConsoleType={_("Select console type")}
textSerialConsole={_("Serial console")}
textVncConsole={_("VNC console")}
textDesktopViewerConsole={_("Desktop viewer")}>
textDesktopViewerConsole={_("Spice console")}>
{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 &&
<Vnc type="VncConsole"
vmName={vm.name}
vmId={vm.id}
connectionName={vm.connectionName}
vm={vm}
consoleDetail={vnc}
onLaunch={() => this.onDesktopConsoleDownload('vnc')}
onAddErrorNotification={onAddErrorNotification}
isExpanded={isExpanded} />}
{(vnc || spice) &&
<DesktopConsole type="DesktopViewer"
onDesktopConsole={onDesktopConsole}
vnc={vnc}
spice={spice} />}
isExpanded={isExpanded} />
{spice &&
<DesktopConsole type="DesktopViewer"
vm={vm}
onDesktopConsole={() => this.onDesktopConsoleDownload('spice')}
spice={spice} />}
</AccessConsoles>
);
}
Expand Down
60 changes: 22 additions & 38 deletions src/components/vm/consoles/desktopConsole.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,51 +17,35 @@
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { DesktopViewer } from '@patternfly/react-console';
import { Button } from "@patternfly/react-core/dist/esm/components/Button";
import { EmptyState, EmptyStateBody, EmptyStateFooter } from "@patternfly/react-core/dist/esm/components/EmptyState";

import { useDialogs } from 'dialogs.jsx';
import { ReplaceSpiceDialog } from '../vmReplaceSpiceDialog.jsx';

import cockpit from "cockpit";

const _ = cockpit.gettext;

function fmt_to_fragments(fmt) {
const args = Array.prototype.slice.call(arguments, 1);

function replace(part) {
if (part[0] == "$") {
return args[parseInt(part.slice(1))];
} else
return part;
}

return React.createElement.apply(null, [React.Fragment, { }].concat(fmt.split(/(\$[0-9]+)/g).map(replace)));
}
const DesktopConsoleDownload = ({ vm, spice, onDesktopConsole }) => {
const Dialogs = useDialogs();

const DesktopConsoleDownload = ({ vnc, spice, onDesktopConsole }) => {
return (
<DesktopViewer spice={spice}
vnc={vnc}
onDownload={onDesktopConsole}
textManualConnection={_("Manual connection")}
textNoProtocol={_("No connection available")}
textConnectWith={_("Connect with any viewer application for following protocols")}
textAddress={_("Address")}
textSpiceAddress={_("SPICE address")}
textVNCAddress={_("VNC address")}
textSpicePort={_("SPICE port")}
textVNCPort={_("VNC port")}
textSpiceTlsPort={_("SPICE TLS port")}
textVNCTlsPort={_("VNC TLS port")}
textConnectWithRemoteViewer={_("Launch remote viewer")}
textMoreInfo={_("Remote viewer details")}
textMoreInfoContent={<>
<p>
{fmt_to_fragments(_("Clicking \"Launch remote viewer\" will download a .vv file and launch $0."), <i>Remote Viewer</i>)}
</p>
<p>
{fmt_to_fragments(_("$0 is available for most operating systems. To install it, search for it in GNOME Software or run the following:"), <i>Remote Viewer</i>)}
</p>
</>}
/>
<>
<div className="vm-console-main">
<EmptyState>
<EmptyStateBody><b>{_("Spice")}</b> {spice.address}:{spice.port}</EmptyStateBody>
<EmptyStateFooter>
<Button variant="primary" onClick={onDesktopConsole}>
{_("Launch viewer")}
</Button>
<Button variant="link" onClick={() => Dialogs.show(<ReplaceSpiceDialog vm={vm} vms={[vm]} />)}>
{_("Replace with VNC")}
</Button>
</EmptyStateFooter>
</EmptyState>
</div>
</>
);
};

Expand Down
75 changes: 52 additions & 23 deletions src/components/vm/consoles/vnc.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
import cockpit from 'cockpit';

import { VncConsole } from '@patternfly/react-console';
import { Button } from "@patternfly/react-core/dist/esm/components/Button";
import { Dropdown, DropdownItem, DropdownList } from "@patternfly/react-core/dist/esm/components/Dropdown";
import { MenuToggle } from "@patternfly/react-core/dist/esm/components/MenuToggle";
import { Divider } from "@patternfly/react-core/dist/esm/components/Divider";
import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";
import { EmptyState, EmptyStateBody, EmptyStateFooter } from "@patternfly/react-core/dist/esm/components/EmptyState";

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import EmptyStateFooter.

import { logDebug } from '../../../helpers.js';
import { domainSendKey } from '../../../libvirtApi/domain.js';
Expand Down Expand Up @@ -115,12 +118,35 @@
}

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

if (!consoleDetail) {
return (
<div className="vm-console-main">
<EmptyState>
<EmptyStateBody>{_("VNC support not enabled. Shut down the virtual machine to add support.")}</EmptyStateBody>
</EmptyState>
</div>
);
}

if (!consoleDetail || !path) {
// postpone rendering until consoleDetail is known and channel ready
return null;
}

const detail = (
<Split>
<SplitItem isFilled>
<b>{_("VNC")}</b> {consoleDetail.address}:{consoleDetail.port}
</SplitItem>
<SplitItem>
<Button variant="secondary" onClick={this.props.onLaunch}>{_("Launch viewer")}</Button>
</SplitItem>
</Split>
);

const credentials = consoleDetail.password ? { password: consoleDetail.password } : undefined;
const encrypt = this.getEncrypt();
const renderDropdownItem = keyName => {
Expand All @@ -129,11 +155,11 @@
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 @@ -147,9 +173,9 @@
];
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)} ref={toggleRef} onClick={(_event, isOpen) => this.setState({ isActionOpen: isOpen })}>
<MenuToggle id={cockpit.format("$0-$1-vnc-sendkey", vm.name, vm.connectionName)} ref={toggleRef} onClick={(_event, isOpen) => this.setState({ isActionOpen: isOpen })}>
{_("Send key")}
</MenuToggle>
)}
Expand All @@ -162,23 +188,26 @@
];

return (
<VncConsole host={window.location.hostname}
port={window.location.port || (encrypt ? '443' : '80')}
path={path}
encrypt={encrypt}
shared
credentials={credentials}
vncLogging='warn'
onDisconnected={this.onDisconnected}
onInitFailed={this.onInitFailed}
additionalButtons={additionalButtons}
textConnecting={_("Connecting")}
textDisconnected={_("Disconnected")}
textDisconnect={_("Disconnect")}
consoleContainerId={isExpanded ? "vnc-display-container-expanded" : "vnc-display-container-minimized"}
resizeSession
scaleViewport
/>
<>
<VncConsole host={window.location.hostname}
port={window.location.port || (encrypt ? '443' : '80')}
path={path}
encrypt={encrypt}
shared
credentials={credentials}
vncLogging='warn'
onDisconnected={this.onDisconnected}
onInitFailed={this.onInitFailed}
additionalButtons={additionalButtons}
textConnecting={_("Connecting")}
textDisconnected={_("Disconnected")}
textDisconnect={_("Disconnect")}
consoleContainerId={isExpanded ? "vnc-display-container-expanded" : "vnc-display-container-minimized"}
resizeSession
scaleViewport
/>
<div className="vm-console-footer">{detail}</div>
</>
);
}
}
Expand Down
Loading