Skip to content

Commit

Permalink
Merge pull request #103 from andreadelrio/new-terminal
Browse files Browse the repository at this point in the history
Bug 1147963 - [Fix] Log viewer (Inspect Task) has very poor UX
  • Loading branch information
walac authored Jun 21, 2016
2 parents f6fd10c + 64d6718 commit 6aade50
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 80 deletions.
55 changes: 55 additions & 0 deletions lib/ui/log-fetcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module.exports = function (self) {
var request;
var dataOffset = 0;

function divide(data) {
return data.split('\n');
}

function onData(e) {
var resp = {};
if (request.responseText != null) {
// Check if we have new data
var length = request.responseText.length;
if (length > dataOffset) {
resp.data = divide(request.responseText);
resp.done = false;
}
}
// When request is done
if (request.readyState === request.DONE) {
resp.done = true;
// Write an error, if request failed
if (request.status !== 200) {
resp.data = ["\r\n[task-inspector] Failed to fetch log!\r\n"];
} else {
resp.data = divide(request.responseText);
}
}
postMessage(resp);
if (resp.done) {
close();
}
}

function abort() {
request.removeEventListener('progress', onData);
request.removeEventListener('load', onData);
request.abort();
close();
}

self.addEventListener('message', function(e) {
if (e.data.url) {
request = new XMLHttpRequest();
request.open('get', e.data.url, true);
request.addEventListener('loadstart', onData);
request.addEventListener('progress', onData);
request.addEventListener('load', onData);
request.send();
};
if (e.data.abort) {
abort();
}
});
};
221 changes: 141 additions & 80 deletions lib/ui/terminalview.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
var React = require('react');
var Terminal = require('term.js/src/term');
var utils = require('../utils');

var ansi_up = require('ansi_up');
var work = require('webworkify');
var ansiRegex = require('ansi-regex');


/** Display terminal output */
var TerminalView = React.createClass({
Expand All @@ -14,123 +16,182 @@ var TerminalView = React.createClass({
})
],

getDefaultProps: function() {
getDefaultProps() {
return {
url: undefined, // No URL to display at this point
options: {
cols: 120,
rows: 40,
cursorBlink: true,
visualBell: false,
popOnBell: false,
screenKeys: false,
scrollback: 50000,
debug: false,
useStyle: true
}
rows: 40,
scrollDown: false,
};
},

getInitialState() {
return {
lines: ['Loading log...'],
fromBottom: 0,
};
},

propTypes: {
url: React.PropTypes.string,
options: React.PropTypes.object.isRequired
},

// Refresh the currently displayed file
refresh: function() {
refresh() {
this.open();
},

/** Open a URL in the terminal */
open: function() {
// Destroy existing terminal if there is one
if (this.term) {
this.term.destroy();
this.term = null;
}
open() {
// Abort previous request if any
if (this.request) {
this.abortRequest();
}

// Create new terminal
this.term = new Terminal(this.props.options);
this.term.open(this.refs.term);
// term.js does a setTimeout() then calls element.focus()
// This is truly annoying. To avoid it we could remove the tabindex property
// but then we can copy out text.
// this.term.element.removeAttribute('tabindex');
// So the solution is to be naughty and monkey patch the element, we'll then
// restore it to it's former glory when it is called the first time.
// This is super ugly and fragile, but it works...
var focusMethod = this.term.element.focus;
var element = this.term.element;
element.focus = function() {
element.focus = focusMethod;
};

// If not given a URL we'll just stop here with an empty terminal
if (!this.props.url) {
return;
}

// Open a new request
this.dataOffset = 0;
this.request = new XMLHttpRequest();
this.request.open('get', this.props.url, true);
this.request.addEventListener('progress', this.onData);
this.request.addEventListener('load', this.onData);
this.request.send();
this.dataOffset = 0;
this.worker = work(require('./log-fetcher.js'));
this.worker.addEventListener('message', this.onData);
this.worker.postMessage({ url: this.props.url });
},

onData: function() {
/* Communicate with the worker */
onData(e) {
var response = e.data;
// Write data to term if there is any data
if (this.request.responseText !== null ||
this.request.responseText !== undefined) {
// Check if we have new data
var length = this.request.responseText.length;
if (length > this.dataOffset) {
// Find new data
var data = this.request.responseText.slice(this.dataOffset, length);
// Update dataOffset
this.dataOffset = length;
// Write to term
this.term.write(data);
if (response.data) {
var newFromBottom = 0;
if (!this.props.scrollDown) {
// we don't expect the data to get shrunk
// since it's a log, it can only grow
newFromBottom += response.data.length - this.state.lines.length;
}
this.setState({
lines: response.data,
fromBottom: newFromBottom,
});
}
// When request is done
if (this.request.readyState === this.request.DONE) {
// Stop cursor from blinking
this.term.cursorBlink = false;
if (this.term._blink) {
clearInterval(this.term._blink);
}
this.term.showCursor();
},

// Write an error, if request failed
if (this.request.status !== 200) {
this.term.write("\r\n[task-inspector] Failed to fetch log!\r\n");
}
abortRequest() {
if (this.worker) {
this.worker.postMessage({ abort: true });
this.worker = null;
}
},

abortRequest: function() {
this.request.removeEventListener('progress', this.onData);
this.request.removeEventListener('load', this.onData);
this.request.abort();
this.request = null;
componentWillUnmount() {
if (this.worker) {
this.abortRequest();
}
},

componentWillUnmount: function() {
if (this.request) {
this.abortRequest();
/* Some methods to work with the scrollbar. *
* Maybe should become a separate class? */
scrollbarHeight() {
if (!this.refs.buffer) {
return 0;
}
var ratio = this.props.rows / this.state.lines.length;
if (ratio > 1) {
ratio = 1;
}
var height = ratio * this.refs.buffer.offsetHeight;
return Math.max(height, 10);
},

scrollbarMargin() {
if (!this.refs.buffer) {
return 0;
}
this.term.destroy();
this.term = null;
var ratio = (this.state.lines.length - this.state.fromBottom - this.props.rows) / this.state.lines.length;
return ratio * (this.refs.buffer.offsetHeight - this.scrollbarHeight());
},

render: function() {
return <div className="terminal-view" ref="term"></div>;
scrollbarSet(newState) {
newState = Math.floor(newState);
newState = Math.max(0, newState);
newState = Math.min(this.state.lines.length - this.props.rows, newState);
if (newState != this.state.fromBottom) {
this.setState({ fromBottom: newState });
}
},

scrollbarMove(dist) {
this.scrollbarSet(this.state.fromBottom - dist);
},

onMouseWheel(e) {
e.preventDefault();
this.scrollbarMove(Math.sign(e.deltaY));
},

onMouseMove(e) {
if (this.dragging) {
var diff = e.pageY - this.startY;
var space = this.refs.buffer.offsetHeight;
var margin = this.margin + diff;
var currentTop = (margin + this.scrollbarHeight()) / space * this.state.lines.length;
this.scrollbarSet(this.state.lines.length - currentTop);
}
},

onMouseDown(e) {
e.preventDefault();
if (e.button == 0) {
this.dragging = true;
this.startY = e.pageY;
this.margin = this.scrollbarMargin();
this.startOffset = this.state.fromBottom;
}
},

onMouseUp(e) {
if (e.button == 0) {
this.dragging = false;
}
},

componentDidMount() {
this.refs.scrollbar.addEventListener('mousedown', this.onMouseDown);
window.addEventListener('mouseup', this.onMouseUp);
window.addEventListener('mousemove', this.onMouseMove);
},

render() {
var start = this.state.lines.length - this.state.fromBottom - this.props.rows;
if (start < 0) {
start = 0;
}
var paddingRows = 0;
//Check if the log has less lines than the number of rows or if we are displaying the beggining of the log
if (this.props.rows <= this.state.lines.length && this.state.lines[start] !== this.state.lines[0]) {
paddingRows = 15;
}
var frame = this.state.lines.slice(start + paddingRows, start + this.props.rows + paddingRows);
return <div className="viewer" onWheel={this.onMouseWheel}>
<div className="buffer" ref="buffer">
{
frame.map(function(line) {
// Check if there are any ansi colors/styles
if (ansiRegex().test(line)) {
var new_line = ansi_up.ansi_to_html(line);
return <div key={start++} dangerouslySetInnerHTML={{__html: new_line}}></div>;
} else {
return <div key={start++}>{(line)}</div>;
};
})
}
</div>
<div className="scrollbar" style={{
height: this.scrollbarHeight(),
marginTop: this.scrollbarMargin()
}} ref="scrollbar"/>
</div>;
}
});

Expand Down
28 changes: 28 additions & 0 deletions lib/ui/terminalview.less
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@
.viewer {
width: 100%;
}

.viewer > div {
display: inline-block;
vertical-align: top;
overflow-y: hidden;
}

.viewer > .buffer {
background-color: black;
width: calc(~'100% - 20px');
overflow-x: auto;
overflow-y: hidden;
height: 600px;
padding-left: 7px;
}

.viewer > .buffer > div {
font-family: monospace;
color: white;
margin-bottom: 2px;
margin-top: 2px;
min-height: 1em;
font-size: 0.8em;
}

.viewer > .scrollbar {
width: 20px;
background-color: #7f7f7f;
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
},
"dependencies": {
"assume": "^1.4.1",
"ansi-regex": "^2.0.0",
"ansi_up": "^1.3.0",
"babel-plugin-object-assign": "1.2.1",
"babelify": "6.0.2",
"bluebird": "^3.4.0",
Expand Down Expand Up @@ -62,6 +64,7 @@
"slugid": "^1.1.0",
"taskcluster-client": "^0.23.17",
"term.js": "0.0.4",
"webworkify": "^1.2.1",
"www-authenticate": "^0.6.2",
"xml2js": "^0.4.16"
},
Expand Down

0 comments on commit 6aade50

Please sign in to comment.