diff --git a/web/package.json b/web/package.json index 9185ef53aa..35aa258a84 100644 --- a/web/package.json +++ b/web/package.json @@ -23,7 +23,8 @@ "react-scripts": "1.1.4", "redux": "<4.0.0", "redux-thunk": "^2.3.0", - "sockette": "^2.0.0" + "sockette": "^2.0.0", + "xterm": "^3.12.0" }, "devDependencies": { "eslint": "^5.3.0", diff --git a/web/src/index.css b/web/src/index.css index 64a2f9cce2..20db5185be 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -107,30 +107,6 @@ a.refresh { animation: progress-bar-stripes 1s linear infinite; } -/* Stream page */ -#zuulstreamoverlay { - float: right; - position: fixed; - top: 70px; - right: 5px; - background-color: white; - padding: 2px 0px 0px 2px; - color: black; -} - -pre#zuulstreamcontent { - font-family: monospace; - white-space: pre; - margin: 0px 10px; - background-color: black; - color: lightgrey; - border: none; -} -p.zuulstreamline { - margin: 0px 0px; - line-height: 1.4; -} - /* Job Tree View group gap */ div.tree-view-container ul.list-group { margin: 0px 0px; diff --git a/web/src/pages/Stream.jsx b/web/src/pages/Stream.jsx index 3ae7d3e9d5..9035fb6d8c 100644 --- a/web/src/pages/Stream.jsx +++ b/web/src/pages/Stream.jsx @@ -16,11 +16,17 @@ import * as React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import { Checkbox, Form, FormGroup } from 'patternfly-react' import Sockette from 'sockette' +import 'xterm/dist/xterm.css' +import { Terminal } from 'xterm' +import * as fit from 'xterm/lib/addons/fit/fit' +import * as weblinks from 'xterm/lib/addons/webLinks/webLinks' + import { getStreamUrl } from '../api' +Terminal.applyAddon(fit) +Terminal.applyAddon(weblinks) class StreamPage extends React.Component { static propTypes = { @@ -29,10 +35,6 @@ class StreamPage extends React.Component { tenant: PropTypes.object } - state = { - autoscroll: true, - } - constructor() { super() this.receiveBuffer = '' @@ -40,59 +42,33 @@ class StreamPage extends React.Component { this.lines = [] } - refreshLoop = () => { - if (this.displayRef.current) { - let newLine = false - this.lines.forEach(line => { - newLine = true - this.displayRef.current.appendChild(line) - }) - this.lines = [] - if (newLine) { - const { autoscroll } = this.state - if (autoscroll) { - this.messagesEnd.scrollIntoView({ behavior: 'instant' }) - } - } - } - this.timer = setTimeout(this.refreshLoop, 250) - } - componentWillUnmount () { - if (this.timer) { - clearTimeout(this.timer) - this.timer = null - } if (this.ws) { console.log('Remove ws') this.ws.close() } } - onLine = (line) => { - // Create dom elements - const lineDom = document.createElement('p') - lineDom.className = 'zuulstreamline' - lineDom.appendChild(document.createTextNode(line)) - this.lines.push(lineDom) + onMessage = (message) => { + this.term.write(message) } - onMessage = (message) => { - this.receiveBuffer += message - const lines = this.receiveBuffer.split('\n') - const lastLine = lines.slice(-1)[0] - // Append all completed lines - lines.slice(0, -1).forEach(line => { - this.onLine(line) - }) - // Check if last chunk is completed - if (lastLine && this.receiveBuffer.slice(-1) === '\n') { - this.onLine(lastLine) - this.receiveBuffer = '' - } else { - this.receiveBuffer = lastLine + onResize = () => { + // Note: We call proposeGeometry to get the number of cols and rows that + // fit into the parent element. However the number of rows is not detected + // correctly so we derive this directly from the window height. + const geometry = this.term.proposeGeometry() + if (geometry) { + const cellHeight = this.term._core.renderer.dimensions.actualCellHeight + const height = window.innerHeight - this.term.element.offsetTop - 10 + + const rows = Math.max(Math.floor(height / cellHeight), 10) + const cols = Math.max(geometry.cols, 10) + + if (this.term.rows !== rows || this.term.cols !== cols) { + this.term.resize(cols, rows) + } } - this.refreshLoop() } componentDidMount() { @@ -105,6 +81,19 @@ class StreamPage extends React.Component { params.logfile = logfile } document.title = 'Zuul Stream | ' + params.uuid.slice(0, 7) + + const term = new Terminal() + + term.webLinksInit() + term.setOption('fontSize', 12) + term.setOption('scrollback', 1000000) + term.setOption('disableStdin', true) + term.setOption('convertEol', true) + + term.attachCustomKeyEventHandler(function () {return false}) + + term.open(this.terminal) + this.ws = new Sockette(getStreamUrl(this.props.tenant.apiPrefix), { timeout: 5e3, maxAttempts: 3, @@ -129,26 +118,18 @@ class StreamPage extends React.Component { console.log('onerror:', e) } }) - } - handleCheckBox = (e) => { - this.setState({autoscroll: e.target.checked}) + this.term = term + + term.element.style.padding = '5px' + this.onResize() + window.addEventListener('resize', this.onResize) } render () { return ( -
- - - autoscroll - - -
-
-        
{ this.messagesEnd = el }} /> +
this.terminal = ref}/> ) } diff --git a/web/yarn.lock b/web/yarn.lock index 54e5a3c9da..dd2337a15c 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -7985,6 +7985,11 @@ xtend@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" +xterm@^3.12.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.12.0.tgz#74cc54013140cf0fd38a05a0d5d49e013e8a53bd" + integrity sha512-U5w1NJdrqAtnNju4W05uOxLzNgMD1sk0AnIkZ//Wa7xRdQTi9Dl1qkPdAaxWJ1a7A8xzNM4ogrX/4oSVl15qOw== + y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"