diff --git a/src/components/ContainerLogs.react.js b/src/components/ContainerLogs.react.js
deleted file mode 100644
index 7da5e841..00000000
--- a/src/components/ContainerLogs.react.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import $ from 'jquery';
-import React from 'react/addons';
-import LogStore from '../stores/LogStore';
-
-var _prevBottom = 0;
-
-module.exports = React.createClass({
- getInitialState: function () {
- return {
- logs: []
- };
- },
- componentDidMount: function() {
- if (!this.props.container) {
- return;
- }
- this.update();
- this.scrollToBottom();
- LogStore.on(LogStore.SERVER_LOGS_EVENT, this.update);
- LogStore.fetch(this.props.container.Name);
- },
- componentWillUnmount: function() {
- if (!this.props.container) {
- return;
- }
-
- LogStore.detach(this.props.container.Name);
- LogStore.removeListener(LogStore.SERVER_LOGS_EVENT, this.update);
- },
- componentDidUpdate: function () {
- this.scrollToBottom();
- },
- scrollToBottom: function () {
- var parent = $('.details-logs');
- if (parent.scrollTop() >= _prevBottom - 50) {
- parent.scrollTop(parent[0].scrollHeight - parent.height());
- }
- _prevBottom = parent[0].scrollHeight - parent.height();
- },
- update: function () {
- if (!this.props.container) {
- return;
- }
- this.setState({
- logs: LogStore.logs(this.props.container.Name)
- });
- },
- render: function () {
- var logs = this.state.logs.map(function (l, i) {
- return
;
- });
- if (logs.length === 0) {
- logs = "No logs for this container.";
- }
- return (
-
- {logs}
-
- );
- }
-});
diff --git a/src/stores/ContainerStore.js b/src/stores/ContainerStore.js
index 28a6e306..46ddfce8 100644
--- a/src/stores/ContainerStore.js
+++ b/src/stores/ContainerStore.js
@@ -3,6 +3,8 @@ import alt from '../alt';
import containerServerActions from '../actions/ContainerServerActions';
import containerActions from '../actions/ContainerActions';
+let MAX_LOG_SIZE = 3000;
+
class ContainerStore {
constructor () {
this.bindActions(containerActions);
@@ -102,10 +104,8 @@ class ContainerStore {
if (containers[container.Name] && containers[container.Name].State.Updating) {
return;
}
- // Trigger log update
- // TODO: fix this loading multiple times
- // LogStore.fetch(container.Name);
+ container.Logs = containers[container.Name].Logs;
containers[container.Name] = container;
this.setState({containers});
@@ -141,7 +141,7 @@ class ContainerStore {
}
}
- waiting({name, waiting}) {
+ waiting ({name, waiting}) {
let containers = this.containers;
if (containers[name]) {
containers[name].State.Waiting = waiting;
@@ -158,6 +158,33 @@ class ContainerStore {
this.setState({pending: null});
}
+ log ({name, entry}) {
+ let container = this.containers[name];
+ if (!container) {
+ return;
+ }
+
+ if (!container.Logs) {
+ container.Logs = [];
+ }
+
+ container.Logs.push.apply(container.Logs, entry.split('\n').filter(e => e.length));
+ container.Logs = container.Logs.slice(container.Logs.length - MAX_LOG_SIZE, MAX_LOG_SIZE);
+ this.emitChange();
+ }
+
+ logs ({name, logs}) {
+ let container = this.containers[name];
+
+ if (!container) {
+ return;
+ }
+
+ container.Logs = logs.split('\n');
+ container.Logs = container.Logs.slice(container.Logs.length - MAX_LOG_SIZE, MAX_LOG_SIZE);
+ this.emitChange();
+ }
+
static generateName (repo) {
const base = _.last(repo.split('/'));
const names = _.keys(this.getState().containers);
diff --git a/src/stores/LogStore.js b/src/stores/LogStore.js
deleted file mode 100644
index 89ed09ce..00000000
--- a/src/stores/LogStore.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import {EventEmitter} from 'events';
-import assign from 'object-assign';
-import Convert from 'ansi-to-html';
-import docker from '../utils/DockerUtil';
-import stream from 'stream';
-
-var _convert = new Convert();
-var _logs = {};
-var _streams = {};
-
-var MAX_LOG_SIZE = 3000;
-
-module.exports = assign(Object.create(EventEmitter.prototype), {
- SERVER_LOGS_EVENT: 'server_logs_event',
- _escape: function (html) {
- var text = document.createTextNode(html);
- var div = document.createElement('div');
- div.appendChild(text);
- return div.innerHTML;
- },
- fetch: function (name) {
- if (!name || !docker.client) {
- return;
- }
- docker.client.getContainer(name).logs({
- stdout: true,
- stderr: true,
- timestamps: false,
- tail: MAX_LOG_SIZE,
- follow: false
- }, (err, logStream) => {
- if (err) {
- return;
- }
- var logs = [];
- var outstream = new stream.PassThrough();
- docker.client.modem.demuxStream(logStream, outstream, outstream);
- outstream.on('data', (chunk) => {
- logs.push(_convert.toHtml(this._escape(chunk)));
- });
- logStream.on('end', () => {
- _logs[name] = logs;
- this.emit(this.SERVER_LOGS_EVENT);
- this.attach(name);
- });
- });
- },
- attach: function (name) {
- if (!name || !docker.client || _streams[name]) {
- return;
- }
- docker.client.getContainer(name).attach({
- stdout: true,
- stderr: true,
- logs: false,
- stream: true
- }, (err, logStream) => {
- if (err) {
- return;
- }
- _streams[name] = logStream;
- var outstream = new stream.PassThrough();
- docker.client.modem.demuxStream(logStream, outstream, outstream);
- outstream.on('data', (chunk) => {
- _logs[name].push(_convert.toHtml(this._escape(chunk)));
- if (_logs[name].length > MAX_LOG_SIZE) {
- _logs[name] = _logs[name].slice(_logs[name].length - MAX_LOG_SIZE, MAX_LOG_SIZE);
- }
- this.emit(this.SERVER_LOGS_EVENT);
- });
- logStream.on('end', () => {
- this.detach(name);
- });
- });
- },
- detach: function (name) {
- if (_streams[name]) {
- _streams[name].destroy();
- delete _streams[name];
- }
- },
- logs: function (name) {
- return _logs[name] || [];
- }
-});
diff --git a/src/utils/DockerUtil.js b/src/utils/DockerUtil.js
index 7091482f..c3f1c5f7 100644
--- a/src/utils/DockerUtil.js
+++ b/src/utils/DockerUtil.js
@@ -7,13 +7,15 @@ import util from './Util';
import hubUtil from './HubUtil';
import metrics from '../utils/MetricsUtil';
import containerServerActions from '../actions/ContainerServerActions';
-import Promise from 'bluebird';
import rimraf from 'rimraf';
+import stream from 'stream';
export default {
host: null,
client: null,
placeholders: {},
+ streams: {},
+ activeContainerName: null,
setup (ip, name) {
if (!ip || !name) {
@@ -343,6 +345,85 @@ export default {
});
},
+ active (name) {
+ this.detach();
+ this.activeContainerName = name;
+
+ if (name) {
+ this.logs();
+ }
+ },
+
+ logs () {
+ if (!this.activeContainerName) {
+ return;
+ }
+
+ this.client.getContainer(this.activeContainerName).logs({
+ stdout: true,
+ stderr: true,
+ tail: 1000,
+ follow: false,
+ timestamps: 1
+ }, (err, logStream) => {
+ if (err) {
+ return;
+ }
+
+ let logs = '';
+ logStream.setEncoding('utf8');
+ logStream.on('data', chunk => logs += chunk);
+ logStream.on('end', () => {
+ containerServerActions.logs({name: this.activeContainerName, logs});
+ this.attach();
+ });
+ });
+ },
+
+ attach () {
+ if (!this.activeContainerName) {
+ return;
+ }
+
+ this.client.getContainer(this.activeContainerName).logs({
+ stdout: true,
+ stderr: true,
+ tail: 0,
+ follow: true,
+ timestamps: 1
+ }, (err, logStream) => {
+ if (err) {
+ return;
+ }
+
+ if (this.stream) {
+ this.detach();
+ }
+ this.stream = logStream;
+
+ let timeout = null;
+ let batch = '';
+ logStream.setEncoding('utf8');
+ logStream.on('data', (chunk) => {
+ batch += chunk;
+ if (!timeout) {
+ timeout = setTimeout(() => {
+ containerServerActions.log({name: this.activeContainerName, entry: batch});
+ timeout = null;
+ batch = '';
+ }, 16);
+ }
+ });
+ });
+ },
+
+ detach () {
+ if (this.stream) {
+ this.stream.destroy();
+ this.stream = null;
+ }
+ },
+
listen () {
this.client.getEvents((error, stream) => {
if (error || !stream) {
@@ -354,16 +435,25 @@ export default {
stream.on('data', json => {
let data = JSON.parse(json);
- if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete' || data.status === 'attach') {
+ if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete' || data.status === 'attach') {
return;
}
if (data.status === 'destroy') {
containerServerActions.destroyed({id: data.id});
+ this.detach(data.id);
} else if (data.status === 'kill') {
containerServerActions.kill({id: data.id});
+ this.detach(data.id);
} else if (data.status === 'stop') {
containerServerActions.stopped({id: data.id});
+ this.detach(data.id);
+ } else if (data.status === 'create') {
+ this.logs();
+ this.fetchContainer(data.id);
+ } else if (data.status === 'start') {
+ this.attach();
+ this.fetchContainer(data.id);
} else if (data.id) {
this.fetchContainer(data.id);
}
diff --git a/styles/container-home.less b/styles/container-home.less
index b8dbc3f2..91433584 100644
--- a/styles/container-home.less
+++ b/styles/container-home.less
@@ -7,14 +7,15 @@
flex-direction: row;
padding: 1rem;
.left {
- width: 100%;
+ display: flex;
+ flex: 0.9 1 0;
flex-direction: column;
margin-right: 1rem;
}
.right {
+ display: flex;
+ flex: 0.1 0 300px;
width: 40%;
- min-width: 200px;
- max-width: 600px;
flex-direction: column;
}
.full {
@@ -103,7 +104,6 @@
color: @gray-lightest;
font-family: @font-code;
font-size: 10px;
- white-space: pre-wrap;
-webkit-user-select: text;
padding: 1.2rem 1.2rem 5rem 1.2rem;
overflow: auto;