Replace term.js with xterm.js

This commit is contained in:
Justin Stephenson 2019-08-16 14:18:13 -04:00
parent 229671f485
commit cddcb1f40a
6 changed files with 57 additions and 257 deletions

View file

@ -63,6 +63,6 @@
"raw-loader": "^0.5.1",
"react": "^16.4.2",
"react-dom": "^16.4.2",
"term.js-cockpit": "0.0.10"
"xterm": "^3.14.5"
}
}

View file

@ -1,3 +1,4 @@
@import "~xterm/lib/xterm.css";
@import "term.css";
/* Our terminal or logs */

View file

@ -18,13 +18,13 @@
*/
"use strict";
import React from 'react';
import './console.css';
import { Terminal as Term } from 'xterm';
let cockpit = require("cockpit");
let _ = cockpit.gettext;
let moment = require("moment");
let Term = require("term.js-cockpit");
let Journal = require("journal");
let $ = require("jquery");
require("console.css");
require("bootstrap-slider");
let padInt = function (n, w) {
@ -1143,7 +1143,9 @@ export class Player extends React.Component {
cols: this.state.cols,
rows: this.state.rows,
screenKeys: true,
useStyle: true
useStyle: true,
/* Exposes the xterm-accessibility-tree */
screenReaderMode: true,
});
term.on('title', this.handleTitleChange);
@ -1242,35 +1244,35 @@ export class Player extends React.Component {
</div>
<div className="panel-footer">
<Slider length={this.buf.pos} mark={this.state.currentTsPost} fastForwardFunc={this.fastForwardToTS} play={this.play} pause={this.pause} paused={this.state.paused} />
<button title="Play/Pause - Hotkey: p" type="button" ref="playbtn"
<button id="player-play-pause" title="Play/Pause - Hotkey: p" type="button" ref="playbtn"
className="btn btn-default btn-lg margin-right-btn play-btn"
onClick={this.playPauseToggle}>
<i className={"fa fa-" + (this.state.paused ? "play" : "pause")}
aria-hidden="true" />
</button>
<button title="Skip Frame - Hotkey: ." type="button"
<button id="player-skip-frame" title="Skip Frame - Hotkey: ." type="button"
className="btn btn-default btn-lg margin-right-btn"
onClick={this.skipFrame}>
<i className="fa fa-step-forward" aria-hidden="true" />
</button>
<button title="Restart Playback - Hotkey: Shift-R" type="button"
<button id="player-restart" title="Restart Playback - Hotkey: Shift-R" type="button"
className="btn btn-default btn-lg" onClick={this.rewindToStart}>
<i className="fa fa-fast-backward" aria-hidden="true" />
</button>
<button title="Fast-forward to end - Hotkey: Shift-G" type="button"
<button id="player-fast-forward" title="Fast-forward to end - Hotkey: Shift-G" type="button"
className="btn btn-default btn-lg margin-right-btn"
onClick={this.fastForwardToEnd}>
<i className="fa fa-fast-forward" aria-hidden="true" />
</button>
<button title="Speed /2 - Hotkey: {" type="button"
<button id="player-speed-down" title="Speed /2 - Hotkey: {" type="button"
className="btn btn-default btn-lg" onClick={this.speedDown}>
/2
</button>
<button title="Reset Speed - Hotkey: Backspace" type="button"
<button id="player-speed-reset" title="Reset Speed - Hotkey: Backspace" type="button"
className="btn btn-default btn-lg" onClick={this.speedReset}>
1:1
</button>
<button title="Speed x2 - Hotkey: }" type="button"
<button id="player-speed-up" title="Speed x2 - Hotkey: }" type="button"
className="btn btn-default btn-lg margin-right-btn"
onClick={this.speedUp}>
x2
@ -1278,16 +1280,16 @@ export class Player extends React.Component {
<span>{speedStr}</span>
<span style={to_right}>
<span className="session_time">{formatDuration(this.state.currentTsPost)} / {formatDuration(this.buf.pos)}</span>
<button title="Drag'n'Pan" type="button" className="btn btn-default btn-lg"
<button id="player-drag-pan" title="Drag'n'Pan" type="button" className="btn btn-default btn-lg"
onClick={this.dragPan}>
<i className={"fa fa-" + (this.state.drag_pan ? "hand-rock-o" : "hand-paper-o")}
aria-hidden="true" /></button>
<button title="Zoom In - Hotkey: =" type="button" className="btn btn-default btn-lg"
<button id="player-zoom-in" title="Zoom In - Hotkey: =" type="button" className="btn btn-default btn-lg"
onClick={this.zoomIn} disabled={this.state.term_zoom_max}>
<i className="fa fa-search-plus" aria-hidden="true" /></button>
<button title="Fit To - Hotkey: Z" type="button" className="btn btn-default btn-lg"
<button id="player-fit-to" title="Fit To - Hotkey: Z" type="button" className="btn btn-default btn-lg"
onClick={this.fitTo}><i className="fa fa-expand" aria-hidden="true" /></button>
<button title="Zoom Out - Hotkey: -" type="button" className="btn btn-default btn-lg"
<button id="player-zoom-out" title="Zoom Out - Hotkey: -" type="button" className="btn btn-default btn-lg"
onClick={this.zoomOut} disabled={this.state.term_zoom_min}>
<i className="fa fa-search-minus" aria-hidden="true" /></button>
</span>

View file

@ -1,192 +0,0 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2016 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 Player from "./player";
"use strict";
var React = require("react");
var Term = require("term");
let $ = require("jquery");
require("console.css");
require("jquery-resizable");
require("jquery-resizable/resizable.css");
/*
* A terminal component that communicates over a cockpit channel.
*
* The only required property is 'channel', which must point to a cockpit
* stream channel.
*
* The size of the terminal can be set with the 'rows' and 'cols'
* properties. If those properties are not given, the terminal will fill
* its container.
*
* If the 'onTitleChanged' callback property is set, it will be called whenever
* the title of the terminal changes.
*
* Call focus() to set the input focus on the terminal.
*/
var Terminal = React.createClass({
propTypes: {
// cols: React.PropTypes.number,
rows: React.PropTypes.number,
channel: React.PropTypes.object.isRequired,
onTitleChanged: React.PropTypes.func
},
componentWillMount: function () {
var term = new Term({
cols: this.state.cols || 80,
rows: this.state.rows || 25,
screenKeys: true,
useStyle: true
});
term.on('data', function(data) {
if (this.props.channel.valid)
this.props.channel.send(data);
}.bind(this));
if (this.props.onTitleChanged)
term.on('title', this.props.onTitleChanged);
this.setState({ terminal: term });
},
componentDidMount: function () {
this.state.terminal.open(this.refs.terminal);
this.connectChannel();
let term = this.refs.terminal;
let onWindowResize = this.onWindowResize;
$(function() {
$(term).resizable({
direction: ['right', 'bottom'],
stop: function() {
onWindowResize();
},
});
});
if (!this.props.rows) {
window.addEventListener('resize', this.onWindowResize);
this.onWindowResize();
}
},
componentWillUpdate: function (nextProps, nextState) {
if (nextState.cols !== this.state.cols || nextState.rows !== this.state.rows) {
this.state.terminal.resize(nextState.cols, nextState.rows);
this.props.channel.control({
window: {
rows: nextState.rows,
cols: nextState.cols
}
});
}
if (nextProps.channel !== this.props.channel) {
this.state.terminal.reset();
this.disconnectChannel();
}
},
componentDidUpdate: function (prevProps) {
if (prevProps.channel !== this.props.channel)
this.connectChannel();
},
render: function () {
let style = {
'min-width': '300px',
'min-height': '100px',
};
// ensure react never reuses this div by keying it with the terminal widget
return <div ref="terminal" style={style} className="console-ct" key={this.state.terminal} />;
},
componentWillUnmount: function () {
this.disconnectChannel();
this.state.terminal.destroy();
},
onChannelMessage: function (event, data) {
if (this.state.terminal) {
this.state.terminal.write(data);
}
},
onChannelClose: function (event, options) {
var term = this.state.terminal;
term.write('\x1b[31m' + (options.problem || 'disconnected') + '\x1b[m\r\n');
term.cursorHidden = true;
term.refresh(term.y, term.y);
},
connectChannel: function () {
var channel = this.props.channel;
if (channel && channel.valid) {
channel.addEventListener('message', this.onChannelMessage.bind(this));
channel.addEventListener('close', this.onChannelClose.bind(this));
}
},
disconnectChannel: function () {
if (this.props.channel) {
this.props.channel.removeEventListener('message', this.onChannelMessage);
this.props.channel.removeEventListener('close', this.onChannelClose);
}
},
focus: function () {
if (this.state.terminal)
this.state.terminal.focus();
},
onWindowResize: function () {
if (this.refs) {
var padding = 2 * 11;
var node = this.getDOMNode();
var terminal = this.refs.terminal.querySelector('.terminal');
var ch = document.createElement('div');
ch.textContent = 'M';
terminal.appendChild(ch);
var height = ch.offsetHeight; // offsetHeight is only correct for block elements
ch.style.display = 'inline';
var width = ch.offsetWidth;
terminal.removeChild(ch);
this.setState({
rows: Math.floor((node.parentElement.clientHeight - padding) / height),
cols: Math.floor((node.parentElement.clientWidth - padding) / width)
});
}
},
send: function(value) {
this.state.terminal.send(value);
}
});
// module.exports = { Terminal: Terminal };
export class Terminal;

View file

@ -15,45 +15,35 @@ import testlib
# Test with pre-recorded journal with tlog UID 981
class TestApplication(testlib.MachineCase):
def testPlay(self):
term_first_line = "#recording-wrap > div > div > div.panel-body > div > div > div > div:nth-child(1)"
play_btn = "button.margin-right-btn:nth-child(3)"
term_first_line = ".xterm-accessibility-tree div:nth-child(1)"
b = self.browser
m = self.machine
self.login_and_go("/session-recording")
b.wait_present(".content-header-extra")
b.wait_present("#user")
b.click(".listing-ct-item")
b.wait_present(play_btn)
b.click(play_btn)
b.click("#player-play-pause")
b.wait_timeout(30000)
b.wait_in_text(term_first_line, "localhost")
def testFastforwardControls(self):
fast_forward_btn = "button.btn:nth-child(6)"
restart_btn = "#recording-wrap > div > div > div.panel-footer > button:nth-child(5)"
slider = "div.slider-handle:nth-child(5)"
term_line = "#recording-wrap > div > div > div.panel-body > div > div > div > div:nth-child(26)"
last_term_line = ".xterm-accessibility-tree > div:nth-child(26)"
slider = ".slider > .min-slider-handle"
b = self.browser
m = self.machine
self.login_and_go("/session-recording")
b.wait_present(".content-header-extra")
b.wait_present("#user")
b.click(".listing-ct-item")
b.wait_present(fast_forward_btn)
b.click(fast_forward_btn)
b.wait_present(slider)
b.click("#player-fast-forward")
b.wait_in_text(last_term_line, "logout")
b.wait_attr(slider, "style", "left: 100%;")
b.wait_in_text(term_line, "logout")
# test restart playback
b.wait_present(restart_btn)
b.click(restart_btn)
b.wait_text(".terminal-cursor", " ")
b.click("#player-restart")
b.wait_text(".xterm-accessibility-tree > div:nth-child(1)", "Blank line")
b.wait_attr(slider, "style", "left: 100%;")
def testSpeedControls(self):
speed_up_btn = "button.btn:nth-child(9)"
speed_down_btn = "button.btn:nth-child(7)"
speed_restore_btn = "button.btn:nth-child(8)"
speed_val = ".panel-footer > span:nth-child(10)"
b = self.browser
m = self.machine
@ -62,48 +52,49 @@ class TestApplication(testlib.MachineCase):
b.wait_present("#user")
b.click(".listing-ct-item")
# increase speed
b.wait_present(speed_up_btn)
b.click(speed_up_btn)
b.wait_present("#player-speed-up")
b.click("#player-speed-up")
b.wait_present(speed_val)
b.wait_text(speed_val, "x2")
b.click(speed_up_btn)
b.click("#player-speed-up")
b.wait_text(speed_val, "x4")
b.click(speed_up_btn)
b.click("#player-speed-up")
b.wait_text(speed_val, "x8")
b.click(speed_up_btn)
b.click("#player-speed-up")
b.wait_text(speed_val, "x16")
# decrease speed
b.click(speed_down_btn)
b.click("#player-speed-down")
b.wait_text(speed_val, "x8")
b.click(speed_down_btn)
b.click("#player-speed-down")
b.wait_text(speed_val, "x4")
b.click(speed_down_btn)
b.click("#player-speed-down")
b.wait_text(speed_val, "x2")
b.click(speed_down_btn)
b.click("#player-speed-down")
b.wait_text(speed_val, "")
b.click(speed_down_btn)
b.click("#player-speed-down")
b.wait_text(speed_val, "/2")
b.click(speed_down_btn)
b.click("#player-speed-down")
b.wait_text(speed_val, "/4")
b.click(speed_down_btn)
b.click("#player-speed-down")
b.wait_text(speed_val, "/8")
b.click(speed_down_btn)
b.click("#player-speed-down")
b.wait_text(speed_val, "/16")
# restore speed
b.click(speed_restore_btn)
b.click("#player-speed-reset")
b.wait_text(speed_val, "")
def testZoomControls(self):
zoom_in_btn = "#recording-wrap > div > div > div.panel-footer > span:nth-child(11) > button:nth-child(3)"
zoom_out_btn = "#recording-wrap > div > div > div.panel-footer > span:nth-child(11) > button:nth-child(5)"
zoom_reset_btn = "#recording-wrap > div > div > div.panel-footer > span:nth-child(11) > button:nth-child(4)"
default_scale_sel = '.console-ct[style^="transform: scale(1)"]'
zoom_one_scale_sel = '.console-ct[style^="transform: scale(1.1)"]'
zoom_two_scale_sel = '.console-ct[style^="transform: scale(1.2)"]'
zoom_three_scale_sel = '.console-ct[style^="transform: scale(1.3)"]'
zoom_reset_scale_sel = '.console-ct[style^="transform: scale(0.823864)"]'
b = self.browser
m = self.machine
if m.image in ["fedora-29"] or m.image in ["rhel-8.0"] or m.image in ["rhel-8.1"]:
zoom_reset_scale_sel = '.console-ct[style*="transform: scale(0.8"]'
else:
zoom_reset_scale_sel = '.console-ct[style*="transform: scale(0.9"]'
self.login_and_go("/session-recording")
b.wait_present(".content-header-extra")
b.wait_present("#user")
@ -111,34 +102,32 @@ class TestApplication(testlib.MachineCase):
# Wait for terminal with scale(1)
b.wait_present(default_scale_sel)
# Zoom in x3
b.click(zoom_in_btn)
b.click("#player-zoom-in")
b.wait_present(zoom_one_scale_sel)
b.click(zoom_in_btn)
b.click("#player-zoom-in")
b.wait_present(zoom_two_scale_sel)
b.click(zoom_in_btn)
b.click("#player-zoom-in")
b.wait_present(zoom_three_scale_sel)
# Zoom Out
b.click(zoom_out_btn)
b.click("#player-zoom-out")
b.wait_present(zoom_two_scale_sel)
# Reset Zoom
b.click(zoom_reset_btn)
b.click("#player-fit-to")
b.wait_present(zoom_reset_scale_sel)
def testSkipFrame(self):
skip_frame_btn = "#recording-wrap > div > div > div.panel-footer > button:nth-child(4)"
term_first_line = "#recording-wrap > div > div > div.panel-body > div > div > div > div:nth-child(1)"
term_first_line = ".xterm-accessibility-tree div:nth-child(1)"
b = self.browser
m = self.machine
self.login_and_go("/session-recording")
b.wait_present(".content-header-extra")
b.wait_present("#user")
b.click(".listing-ct-item")
b.wait_present(skip_frame_btn)
b.click(skip_frame_btn)
b.click(skip_frame_btn)
b.click(skip_frame_btn)
b.click(skip_frame_btn)
b.click(skip_frame_btn)
b.click("#player-skip-frame")
b.click("#player-skip-frame")
b.click("#player-skip-frame")
b.click("#player-skip-frame")
b.click("#player-skip-frame")
b.wait_timeout(20)
b.wait_in_text(term_first_line, "localhost")

View file

@ -37,10 +37,10 @@ var info = {
"index.html",
"config.html",
"player.jsx",
"console.css",
"recordings.jsx",
"recordings.css",
"table.css",
"terminal.jsx",
"manifest.json",
"timer.css",
"./pkg/lib/listing.less",