Merge "Use xterm.js for live log streaming"
This commit is contained in:
commit
8c4a14675b
|
@ -23,7 +23,8 @@
|
||||||
"react-scripts": "1.1.4",
|
"react-scripts": "1.1.4",
|
||||||
"redux": "<4.0.0",
|
"redux": "<4.0.0",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"sockette": "^2.0.0"
|
"sockette": "^2.0.0",
|
||||||
|
"xterm": "^3.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^5.3.0",
|
"eslint": "^5.3.0",
|
||||||
|
|
|
@ -107,30 +107,6 @@ a.refresh {
|
||||||
animation: progress-bar-stripes 1s linear infinite;
|
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 */
|
/* Job Tree View group gap */
|
||||||
div.tree-view-container ul.list-group {
|
div.tree-view-container ul.list-group {
|
||||||
margin: 0px 0px;
|
margin: 0px 0px;
|
||||||
|
|
|
@ -16,11 +16,17 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { Checkbox, Form, FormGroup } from 'patternfly-react'
|
|
||||||
import Sockette from 'sockette'
|
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'
|
import { getStreamUrl } from '../api'
|
||||||
|
|
||||||
|
Terminal.applyAddon(fit)
|
||||||
|
Terminal.applyAddon(weblinks)
|
||||||
|
|
||||||
class StreamPage extends React.Component {
|
class StreamPage extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -29,10 +35,6 @@ class StreamPage extends React.Component {
|
||||||
tenant: PropTypes.object
|
tenant: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
|
||||||
autoscroll: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
this.receiveBuffer = ''
|
this.receiveBuffer = ''
|
||||||
|
@ -40,59 +42,33 @@ class StreamPage extends React.Component {
|
||||||
this.lines = []
|
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 () {
|
componentWillUnmount () {
|
||||||
if (this.timer) {
|
|
||||||
clearTimeout(this.timer)
|
|
||||||
this.timer = null
|
|
||||||
}
|
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
console.log('Remove ws')
|
console.log('Remove ws')
|
||||||
this.ws.close()
|
this.ws.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onLine = (line) => {
|
onMessage = (message) => {
|
||||||
// Create dom elements
|
this.term.write(message)
|
||||||
const lineDom = document.createElement('p')
|
|
||||||
lineDom.className = 'zuulstreamline'
|
|
||||||
lineDom.appendChild(document.createTextNode(line))
|
|
||||||
this.lines.push(lineDom)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessage = (message) => {
|
onResize = () => {
|
||||||
this.receiveBuffer += message
|
// Note: We call proposeGeometry to get the number of cols and rows that
|
||||||
const lines = this.receiveBuffer.split('\n')
|
// fit into the parent element. However the number of rows is not detected
|
||||||
const lastLine = lines.slice(-1)[0]
|
// correctly so we derive this directly from the window height.
|
||||||
// Append all completed lines
|
const geometry = this.term.proposeGeometry()
|
||||||
lines.slice(0, -1).forEach(line => {
|
if (geometry) {
|
||||||
this.onLine(line)
|
const cellHeight = this.term._core.renderer.dimensions.actualCellHeight
|
||||||
})
|
const height = window.innerHeight - this.term.element.offsetTop - 10
|
||||||
// Check if last chunk is completed
|
|
||||||
if (lastLine && this.receiveBuffer.slice(-1) === '\n') {
|
const rows = Math.max(Math.floor(height / cellHeight), 10)
|
||||||
this.onLine(lastLine)
|
const cols = Math.max(geometry.cols, 10)
|
||||||
this.receiveBuffer = ''
|
|
||||||
} else {
|
if (this.term.rows !== rows || this.term.cols !== cols) {
|
||||||
this.receiveBuffer = lastLine
|
this.term.resize(cols, rows)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.refreshLoop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -105,6 +81,19 @@ class StreamPage extends React.Component {
|
||||||
params.logfile = logfile
|
params.logfile = logfile
|
||||||
}
|
}
|
||||||
document.title = 'Zuul Stream | ' + params.uuid.slice(0, 7)
|
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), {
|
this.ws = new Sockette(getStreamUrl(this.props.tenant.apiPrefix), {
|
||||||
timeout: 5e3,
|
timeout: 5e3,
|
||||||
maxAttempts: 3,
|
maxAttempts: 3,
|
||||||
|
@ -129,26 +118,18 @@ class StreamPage extends React.Component {
|
||||||
console.log('onerror:', e)
|
console.log('onerror:', e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
handleCheckBox = (e) => {
|
this.term = term
|
||||||
this.setState({autoscroll: e.target.checked})
|
|
||||||
|
term.element.style.padding = '5px'
|
||||||
|
this.onResize()
|
||||||
|
window.addEventListener('resize', this.onResize)
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Form inline id='zuulstreamoverlay'>
|
<div ref={ref => this.terminal = ref}/>
|
||||||
<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 }} />
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7985,6 +7985,11 @@ xtend@^4.0.0:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
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:
|
y18n@^3.2.1:
|
||||||
version "3.2.1"
|
version "3.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
|
||||||
|
|
Loading…
Reference in New Issue