зеркало из https://github.com/docker/kitematic.git
Merge branch 'master' of github.com:docker/kitematic
This commit is contained in:
Коммит
e5ddc4d041
|
@ -38,6 +38,10 @@ class ContainerActions {
|
||||||
run (name, repo, tag) {
|
run (name, repo, tag) {
|
||||||
dockerUtil.run(name, repo, tag);
|
dockerUtil.run(name, repo, tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
active (name) {
|
||||||
|
dockerUtil.active(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default alt.createActions(ContainerActions);
|
export default alt.createActions(ContainerActions);
|
||||||
|
|
|
@ -15,7 +15,9 @@ class ContainerServerActions {
|
||||||
'updated',
|
'updated',
|
||||||
'waiting',
|
'waiting',
|
||||||
'kill',
|
'kill',
|
||||||
'stopped'
|
'stopped',
|
||||||
|
'log',
|
||||||
|
'logs'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,74 +1,44 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import React from 'react/addons';
|
import React from 'react/addons';
|
||||||
import LogStore from '../stores/LogStore';
|
|
||||||
import Router from 'react-router';
|
import Router from 'react-router';
|
||||||
import metrics from '../utils/MetricsUtil';
|
import containerActions from '../actions/ContainerActions';
|
||||||
|
import Convert from 'ansi-to-html';
|
||||||
|
|
||||||
var _prevBottom = 0;
|
let escape = function (html) {
|
||||||
|
var text = document.createTextNode(html);
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.appendChild(text);
|
||||||
|
return div.innerHTML;
|
||||||
|
};
|
||||||
|
|
||||||
|
let convert = new Convert();
|
||||||
|
let prevBottom = 0;
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
mixins: [Router.Navigation],
|
|
||||||
getInitialState: function () {
|
componentDidUpdate: function () {
|
||||||
return {
|
var node = $('.logs').get()[0];
|
||||||
logs: []
|
node.scrollTop = node.scrollHeight;
|
||||||
};
|
|
||||||
},
|
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function (nextProps) {
|
componentWillReceiveProps: function (nextProps) {
|
||||||
if (this.props.container && nextProps.container && this.props.container.Name !== nextProps.container.Name) {
|
if (this.props.container && nextProps.container && this.props.container.Name !== nextProps.container.Name) {
|
||||||
LogStore.detach(this.props.container.Name);
|
containerActions.active(nextProps.container.Name);
|
||||||
LogStore.fetch(nextProps.container.Name);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentDidMount: function () {
|
||||||
if (!this.props.container) {
|
containerActions.active(this.props.container.Name);
|
||||||
return;
|
},
|
||||||
}
|
|
||||||
|
|
||||||
LogStore.detach(this.props.container.Name);
|
componentWillUnmount: function () {
|
||||||
LogStore.removeListener(LogStore.SERVER_LOGS_EVENT, this.update);
|
containerActions.active(null);
|
||||||
},
|
|
||||||
componentDidUpdate: function () {
|
|
||||||
this.scrollToBottom();
|
|
||||||
},
|
|
||||||
scrollToBottom: function () {
|
|
||||||
var parent = $('.logs');
|
|
||||||
if (parent[0].scrollHeight - parent.height() >= _prevBottom - 50) {
|
|
||||||
parent.scrollTop(parent[0].scrollHeight - parent.height());
|
|
||||||
}
|
|
||||||
_prevBottom = parent[0].scrollHeight - parent.height();
|
|
||||||
},
|
|
||||||
handleClickLogs: function () {
|
|
||||||
metrics.track('Viewed Logs', {
|
|
||||||
from: 'preview'
|
|
||||||
});
|
|
||||||
this.context.router.transitionTo('containerLogs', {name: this.props.container.Name});
|
|
||||||
},
|
|
||||||
update: function () {
|
|
||||||
if (!this.props.container) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
logs: LogStore.logs(this.props.container.Name)
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function () {
|
render: function () {
|
||||||
var logs = this.state.logs.map(function (l, i) {
|
let logs = this.props.container.Logs ?
|
||||||
return <span key={i} dangerouslySetInnerHTML={{__html: l}}></span>;
|
this.props.container.Logs.map((l) => <div key={l.substr(0,l.indexOf(' '))} dangerouslySetInnerHTML={{__html: convert.toHtml(escape(l.substr(l.indexOf(' ')+1)).replace(/ /g, ' <wbr>'))}}></div>) :
|
||||||
});
|
['0 No logs for this container.'];
|
||||||
if (logs.length === 0) {
|
|
||||||
logs = "No logs for this container.";
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div className="mini-logs wrapper">
|
<div className="mini-logs wrapper">
|
||||||
<div className="widget">
|
<div className="widget">
|
||||||
|
|
|
@ -96,7 +96,7 @@ var ContainerListItem = React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router.Link to="container" params={{name: container.Name}}>
|
<Router.Link to="container" params={{name: container.Name}}>
|
||||||
<li onMouseEnter={self.handleItemMouseEnter} onMouseLeave={self.handleItemMouseLeave}>
|
<li onMouseEnter={self.handleItemMouseEnter} onMouseLeave={self.handleItemMouseLeave} onClick={self.handleClick}>
|
||||||
{state}
|
{state}
|
||||||
<div className="info">
|
<div className="info">
|
||||||
<div className="name">
|
<div className="name">
|
||||||
|
|
|
@ -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 <span key={i} dangerouslySetInnerHTML={{__html: l}}></span>;
|
|
||||||
});
|
|
||||||
if (logs.length === 0) {
|
|
||||||
logs = "No logs for this container.";
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="details-panel details-logs logs">
|
|
||||||
{logs}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -3,6 +3,8 @@ import alt from '../alt';
|
||||||
import containerServerActions from '../actions/ContainerServerActions';
|
import containerServerActions from '../actions/ContainerServerActions';
|
||||||
import containerActions from '../actions/ContainerActions';
|
import containerActions from '../actions/ContainerActions';
|
||||||
|
|
||||||
|
let MAX_LOG_SIZE = 3000;
|
||||||
|
|
||||||
class ContainerStore {
|
class ContainerStore {
|
||||||
constructor () {
|
constructor () {
|
||||||
this.bindActions(containerActions);
|
this.bindActions(containerActions);
|
||||||
|
@ -102,10 +104,8 @@ class ContainerStore {
|
||||||
if (containers[container.Name] && containers[container.Name].State.Updating) {
|
if (containers[container.Name] && containers[container.Name].State.Updating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Trigger log update
|
|
||||||
// TODO: fix this loading multiple times
|
|
||||||
// LogStore.fetch(container.Name);
|
|
||||||
|
|
||||||
|
container.Logs = containers[container.Name].Logs;
|
||||||
containers[container.Name] = container;
|
containers[container.Name] = container;
|
||||||
|
|
||||||
this.setState({containers});
|
this.setState({containers});
|
||||||
|
@ -141,7 +141,7 @@ class ContainerStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
waiting({name, waiting}) {
|
waiting ({name, waiting}) {
|
||||||
let containers = this.containers;
|
let containers = this.containers;
|
||||||
if (containers[name]) {
|
if (containers[name]) {
|
||||||
containers[name].State.Waiting = waiting;
|
containers[name].State.Waiting = waiting;
|
||||||
|
@ -158,6 +158,33 @@ class ContainerStore {
|
||||||
this.setState({pending: null});
|
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) {
|
static generateName (repo) {
|
||||||
const base = _.last(repo.split('/'));
|
const base = _.last(repo.split('/'));
|
||||||
const names = _.keys(this.getState().containers);
|
const names = _.keys(this.getState().containers);
|
||||||
|
|
|
@ -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] || [];
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -7,13 +7,15 @@ import util from './Util';
|
||||||
import hubUtil from './HubUtil';
|
import hubUtil from './HubUtil';
|
||||||
import metrics from '../utils/MetricsUtil';
|
import metrics from '../utils/MetricsUtil';
|
||||||
import containerServerActions from '../actions/ContainerServerActions';
|
import containerServerActions from '../actions/ContainerServerActions';
|
||||||
import Promise from 'bluebird';
|
|
||||||
import rimraf from 'rimraf';
|
import rimraf from 'rimraf';
|
||||||
|
import stream from 'stream';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
host: null,
|
host: null,
|
||||||
client: null,
|
client: null,
|
||||||
placeholders: {},
|
placeholders: {},
|
||||||
|
streams: {},
|
||||||
|
activeContainerName: null,
|
||||||
|
|
||||||
setup (ip, name) {
|
setup (ip, name) {
|
||||||
if (!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 () {
|
listen () {
|
||||||
this.client.getEvents((error, stream) => {
|
this.client.getEvents((error, stream) => {
|
||||||
if (error || !stream) {
|
if (error || !stream) {
|
||||||
|
@ -354,16 +435,25 @@ export default {
|
||||||
stream.on('data', json => {
|
stream.on('data', json => {
|
||||||
let data = JSON.parse(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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.status === 'destroy') {
|
if (data.status === 'destroy') {
|
||||||
containerServerActions.destroyed({id: data.id});
|
containerServerActions.destroyed({id: data.id});
|
||||||
|
this.detach(data.id);
|
||||||
} else if (data.status === 'kill') {
|
} else if (data.status === 'kill') {
|
||||||
containerServerActions.kill({id: data.id});
|
containerServerActions.kill({id: data.id});
|
||||||
|
this.detach(data.id);
|
||||||
} else if (data.status === 'stop') {
|
} else if (data.status === 'stop') {
|
||||||
containerServerActions.stopped({id: data.id});
|
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) {
|
} else if (data.id) {
|
||||||
this.fetchContainer(data.id);
|
this.fetchContainer(data.id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,15 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
.left {
|
.left {
|
||||||
width: 100%;
|
display: flex;
|
||||||
|
flex: 0.9 1 0;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
.right {
|
.right {
|
||||||
|
display: flex;
|
||||||
|
flex: 0.1 0 300px;
|
||||||
width: 40%;
|
width: 40%;
|
||||||
min-width: 200px;
|
|
||||||
max-width: 600px;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.full {
|
.full {
|
||||||
|
@ -103,7 +104,6 @@
|
||||||
color: @gray-lightest;
|
color: @gray-lightest;
|
||||||
font-family: @font-code;
|
font-family: @font-code;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
white-space: pre-wrap;
|
|
||||||
-webkit-user-select: text;
|
-webkit-user-select: text;
|
||||||
padding: 1.2rem 1.2rem 5rem 1.2rem;
|
padding: 1.2rem 1.2rem 5rem 1.2rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
Загрузка…
Ссылка в новой задаче