From bb352a3559d75e816d1f9fd9a645fb41dee8ce10 Mon Sep 17 00:00:00 2001 From: Tobias Henkel Date: Sat, 30 Mar 2019 22:17:27 +0100 Subject: [PATCH] Use xterm.js for live log streaming We currently use a very simplistic self written mechanism for live log streaming. This has severe performance drawbacks when dealing with large live logs. The component xterm.js widely used out there that is specialized for this task and handles huge logs just fine. We also don't need the autoscroll checkbox anymore as this is handled automagically by xterm.js. It stops following the stream when scrolling and starts following again after scrolling to the bottom. Further it makes it easy to have clickable links in the live log. Change-Id: I3492e983bf248b4f286edc1bf9db3d52297da993 --- web/package.json | 3 +- web/src/index.css | 24 --------- web/src/pages/Stream.jsx | 105 ++++++++++++++++----------------------- web/yarn.lock | 5 ++ 4 files changed, 50 insertions(+), 87 deletions(-) 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"