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
This commit is contained in:
parent
5b7c278eb2
commit
bb352a3559
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 (
|
||||
<React.Fragment>
|
||||
<Form inline id='zuulstreamoverlay'>
|
||||
<FormGroup controlId='stream'>
|
||||
<Checkbox
|
||||
checked={this.state.autoscroll}
|
||||
onChange={this.handleCheckBox}>
|
||||
autoscroll
|
||||
</Checkbox>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
<pre id='zuulstreamcontent' ref={this.displayRef} />
|
||||
<div ref={(el) => { this.messagesEnd = el }} />
|
||||
<div ref={ref => this.terminal = ref}/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue