From 8c8b600ab8ed8733af9e632d767cd80991d33ee9 Mon Sep 17 00:00:00 2001 From: Vladimir Polyakov Date: Wed, 18 Nov 2015 23:14:46 +0300 Subject: [PATCH] some terminal improvements --- data/projects/project1/config.yaml | 2 +- db.js | 8 ++ distributor.js | 88 +++++++++---------- static/css/sources/components/terminal.less | 72 +++++++++++---- .../js/app/components/terminal/terminal.jade | 6 +- static/js/app/components/terminal/terminal.js | 42 ++++----- .../app/components/terminal/test/index.jade | 3 + .../js/app/components/terminal/test/index.js | 19 ++++ static/js/app/stores/terminal.js | 4 +- 9 files changed, 151 insertions(+), 93 deletions(-) create mode 100644 static/js/app/components/terminal/test/index.jade create mode 100644 static/js/app/components/terminal/test/index.js diff --git a/data/projects/project1/config.yaml b/data/projects/project1/config.yaml index 632e5ed..9887d78 100644 --- a/data/projects/project1/config.yaml +++ b/data/projects/project1/config.yaml @@ -50,7 +50,7 @@ steps: - cmd: cat 1.txt 2.txt - shell: /bin/bash cmd: > - for i in {1..10}; do + for i in {1..1000}; do echo "tick $i"; sleep 0.3; done; diff --git a/db.js b/db.js index 10ef5be..e95f83a 100644 --- a/db.js +++ b/db.js @@ -94,6 +94,14 @@ exports.init = function(dbPath, params, callback) { value: function(logLine) { return _(logLine).pick('number', 'text'); } + }, + { + key: { + buildId: 1 + }, + value: function(logLine) { + return _(logLine).pick('number', 'text'); + } } ], withUniqueId: false diff --git a/distributor.js b/distributor.js index 583cef3..4b785ce 100644 --- a/distributor.js +++ b/distributor.js @@ -49,28 +49,27 @@ exports.init = function(app, callback) { } var buildDataResource = app.dataio.resource('build' + buildId); buildDataResource.on('connection', function(client) { - var callback = this.async(), - buildLogPath = getBuildLogPath(buildId); - - var stream = fs.createReadStream(buildLogPath, { - encoding: 'utf8' - }); - - stream - .on('readable', function() { - var data = stream.read(); - while (data) { - client.emit('sync', 'data', {lines: [{text: data}]}); - data = stream.read(); + var callback = this.async(); + Steppy( + function() { + db.logLines.find({ + start: {buildId: buildId, numberStr: 0}, + }, this.slot()); + }, + function(err, lines) { + client.emit('sync', 'data', {lines: lines}); + this.pass(true); + }, + function(err) { + if (err) { + logger.error( + 'error during read log for "' + buildId + '":', + err.stack || err + ); } - }) - .on('end', callback) - .on('error', function(err) { - logger.error( - 'Error during read "' + buildLogPath + '":', - err.stack || err - ); - }); + callback(); + } + ); }); buildDataResourcesHash[buildId] = buildDataResource; }; @@ -105,16 +104,16 @@ exports.init = function(app, callback) { var buildLogLineNumbersHash = {}; distributor.on('buildData', function(build, data) { - if (!/\n$/.test(data)) { - data += '\n'; - } + var lines = data.trim().split('\n'), + logLineNumber = buildLogLineNumbersHash[build.id] || 0; - if (buildLogLineNumbersHash[build.id]) { - buildLogLineNumbersHash[build.id]++; - } else { - buildLogLineNumbersHash[build.id] = 1; - } - var logLineNumber = buildLogLineNumbersHash[build.id]; + lines = _(lines).map(function(line, index) { + return { + number: logLineNumber + index, + text: line + }; + }); + buildLogLineNumbersHash[build.id] = logLineNumber + lines.length; var filePath = getBuildLogPath(build.id); @@ -134,24 +133,23 @@ exports.init = function(app, callback) { app.dataio.resource('build' + build.id).clientEmitSync( 'data', - {lines: [{number: logLineNumber, text: data}]} + {lines: lines} ); // write build logs to db - - db.logLines.put({ - buildId: build.id, - number: logLineNumber, - text: data - }, function(err) { - if (err) { - logger.error( - 'Error during write log line "' + logLineNumber + - '" for build "' + build.id + '":', - err.stack || err - ); - } - }); + _(lines).each(function(line) { + db.logLines.put(_({ + buildId: build.id, + }).extend(line), function(err) { + if (err) { + logger.error( + 'Error during write log line "' + logLineNumber + + '" for build "' + build.id + '":', + err.stack || err + ); + } + }); + }) }); callback(null, distributor); diff --git a/static/css/sources/components/terminal.less b/static/css/sources/components/terminal.less index e1f8053..e9bc91c 100644 --- a/static/css/sources/components/terminal.less +++ b/static/css/sources/components/terminal.less @@ -1,25 +1,63 @@ .terminal { box-sizing: border-box; + position: relative; + + &_header { + + } &_code { - height: 420px; - padding: 8px 12px; - box-sizing: border-box; + clear: left; + height: 500px; + min-height: 42px; + padding: 15px 0; + color: #F1F1F1; + font-family: monospace; + font-size: 12px; + line-height: 19px; + white-space: pre-wrap; + word-wrap: break-word; + background-color: #2a2a2a; + counter-reset: line-numbering; + margin-top: 0; overflow-y: scroll; - border: none; - color: white; - background-color: black; - line-height: 0.8; - font-size: 13px; - font-family: 'Ubuntu', sans-serif; + } + + &_footer { - &_newline { - display: block; - margin: 0.8em 0; - white-space: pre; - &:first-child { - margin-top: 0; - } - } + } +} + +.code-line { + position: relative; + padding: 0 15px 0 55px; + margin: 0; + min-height: 16px; + + &_counter { + display: inline-block; + text-align: right; + min-width: 40px; + margin-left: -33px; + cursor: pointer; + text-decoration: none; + color: darken(@gray-lighter, 15%); + &:before { + content: counter(line-numbering); + counter-increment: line-numbering; + padding-right: 1em; + } + &:hover { + text-decoration: none; + color: @gray-lighter; + } + } + + &_body { + display: inline-block; + } + + &:hover { + background-color: #444; } } diff --git a/static/js/app/components/terminal/terminal.jade b/static/js/app/components/terminal/terminal.jade index 27507ad..4525c89 100644 --- a/static/js/app/components/terminal/terminal.jade +++ b/static/js/app/components/terminal/terminal.jade @@ -1,2 +1,6 @@ .terminal - .terminal_code(ref="code", onScroll=this.onScroll)!= this.state.data + pre.terminal_code(ref="code") + each row, index in this.state.data + .code-line(key=index) + span.code-line_counter + .code-line_body!= row diff --git a/static/js/app/components/terminal/terminal.js b/static/js/app/components/terminal/terminal.js index 61adaf2..2f369dd 100644 --- a/static/js/app/components/terminal/terminal.js +++ b/static/js/app/components/terminal/terminal.js @@ -10,48 +10,36 @@ define([ ], function(_, React, Reflux, terminalStore, ansiUp, template) { var Component = React.createClass({ mixins: [Reflux.ListenerMixin], - scrollOnData: true, + shouldScrollBottom: true, ignoreScrollEvent: false, componentDidMount: function() { this.listenTo(terminalStore, this.updateItems); }, - ensureScrollPosition: function() { - if (this.scrollOnData) { - var codeNode = this.refs.code.getDOMNode(); - this.ignoreScrollEvent = true; - codeNode.scrollTop = codeNode.scrollHeight - codeNode.offsetHeight; - } - }, - onScroll: function() { - if (!this.ignoreScrollEvent) { - var codeNode = this.refs.code.getDOMNode(); - if (codeNode.offsetHeight + codeNode.scrollTop >= codeNode.scrollHeight) { - this.scrollOnData = true; - } else { - this.scrollOnData = false; - } - } - this.ignoreScrollEvent = false; - }, prepareOutput: function(output) { - var text = output.replace( - /(.*)\n/gi, - '$1' - ); - return ansiUp.ansi_to_html(text); + return output.map(function(row) { + return ansiUp.ansi_to_html(row.text); + }); + }, + componentWillUpdate: function() { + var node = this.refs.code.getDOMNode(); + this.shouldScrollBottom = node.scrollTop + node.offsetHeight >= node.scrollHeight; + }, + componentDidUpdate: function() { + if (this.shouldScrollBottom) { + var node = this.refs.code.getDOMNode(); + node.scrollTop = node.scrollHeight; + } }, updateItems: function(build) { // listen just our console update if (build.buildId === this.props.build) { this.setState({data: this.prepareOutput(build.data)}); - _.defer(this.ensureScrollPosition); - this.ensureScrollPosition(); } }, render: template, getInitialState: function() { return { - data: '' + data: [] }; } }); diff --git a/static/js/app/components/terminal/test/index.jade b/static/js/app/components/terminal/test/index.jade new file mode 100644 index 0000000..09b3d3d --- /dev/null +++ b/static/js/app/components/terminal/test/index.jade @@ -0,0 +1,3 @@ +p 123 + +Terminal(lines=this.state.lines) diff --git a/static/js/app/components/terminal/test/index.js b/static/js/app/components/terminal/test/index.js new file mode 100644 index 0000000..5511fbb --- /dev/null +++ b/static/js/app/components/terminal/test/index.js @@ -0,0 +1,19 @@ +'use strict'; + +define([ + 'react', + '../terminal', + 'templates/app/components/terminal/test/index' +], function(React, TerminalComponent, template) { + template = template.locals({ + Terminal: TerminalComponent + }); + return React.createClass({ + getInitialState: function() { + return { + lines: [1, 2, 3] + }; + }, + render: template + }); +}); diff --git a/static/js/app/stores/terminal.js b/static/js/app/stores/terminal.js index de5cd0d..a943ab3 100644 --- a/static/js/app/stores/terminal.js +++ b/static/js/app/stores/terminal.js @@ -16,7 +16,7 @@ define([ onReadTerminalOutput: function(build) { var self = this, - output = '', + output = [], resourceName = 'build' + build.id; var connectToBuildDataResource = function() { @@ -29,7 +29,7 @@ define([ } connect.resource(resourceName).subscribe('data', function(data) { - output += _(data.lines).pluck('text').join(''); + output = output.concat(data.lines); self.trigger({ buildId: build.id,