Merge branch 'master' into linux-support

This commit is contained in:
Guillaume Hain 2015-12-01 11:36:42 +01:00
Родитель de0b2efd2a 5f1b34ff92
Коммит 4e4516da6a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: D2B2A8B99474CF36
22 изменённых файлов: 202 добавлений и 277 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -2,6 +2,7 @@
.swp
build
dist
release
installer
node_modules
coverage

Просмотреть файл

@ -1,31 +0,0 @@
{
"curly": true,
"noempty": true,
"newcap": true,
"eqeqeq": true,
"eqnull": true,
"esnext": true,
"undef": true,
"unused": true,
"devel": true,
"node": true,
"browser": true,
"evil": false,
"latedef": true,
"nonew": true,
"trailing": true,
"immed": true,
"smarttabs": true,
"strict": false,
"quotmark": false,
"nonbsp": true,
"noempty": true,
"camelcase": false,
"jasmine": true,
"globals": {
"define": true,
"jest": true,
"pit": true
},
"predef": [ "-Promise" ]
}

Просмотреть файл

@ -1,22 +1,14 @@
sudo: false
language: node_js
node_js:
- "4.1"
cache:
directories:
- resources
- node_modules
before_install:
- brew unlink node
- brew update
- brew install homebrew/versions/node4-lts
- node_modules
script:
- npm install
- npm test
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && npm run integration || false'
after_script:
- echo $MAC_KEY_CONTENT > mac_key_content.hex && xxd -p -r mac_key_content.hex ~/Library/Keychains/keychain.keychain && rm mac_key_content.hex
- security unlock-keychain -p "$KEY_SECRET" ~/Library/Keychains/keychain.keychain
- grunt release
os:
- osx

Просмотреть файл

@ -209,7 +209,7 @@ module.exports = function (grunt) {
].join(' && '),
},
zip: {
command: 'ditto -c -k --sequesterRsrc --keepParent <%= OSX_FILENAME_ESCAPED %> dist/' + BASENAME + '-' + packagejson.version + '-Mac.zip',
command: 'ditto -c -k --sequesterRsrc --keepParent <%= OSX_FILENAME_ESCAPED %> release/' + BASENAME + '-' + packagejson.version + '-Mac.zip',
}
},
@ -220,7 +220,7 @@ module.exports = function (grunt) {
compress: {
windows: {
options: {
archive: './dist/' + BASENAME + '-' + packagejson.version + '-Windows-Alpha.zip',
archive: './release/' + BASENAME + '-' + packagejson.version + '-Windows-Alpha.zip',
mode: 'zip'
},
files: [{
@ -257,11 +257,8 @@ module.exports = function (grunt) {
});
grunt.registerTask('default', ['newer:babel', 'less', 'newer:copy:dev', 'shell:electron', 'watchChokidar']);
if(process.platform === 'linux') {
grunt.registerTask('release', ['clean:release', 'babel', 'less', 'copy:dev', 'electron:linux']);
} else {
grunt.registerTask('release', ['clean:release', 'babel', 'less', 'copy:dev', 'electron', 'copy:osx', 'shell:sign', 'shell:zip', 'copy:windows', 'rcedit:exes', 'compress']);
}
grunt.registerTask('release', ['clean:release', 'babel', 'less', 'copy:dev', 'electron', 'copy:osx', 'shell:sign', 'shell:zip', 'copy:windows', 'rcedit:exes', 'compress']);
grunt.registerTask('release-mac', ['clean:release', 'babel', 'less', 'copy:dev', 'electron:osx', 'copy:osx', 'shell:sign', 'shell:zip']);
process.on('SIGINT', function () {
grunt.task.run(['shell:electron:kill']);

Просмотреть файл

@ -1,4 +1,4 @@
[![Build Status](https://travis-ci.org/kitematic/kitematic.svg?branch=master)](https://travis-ci.org/kitematic/kitematic)
[![Build Status](https://travis-ci.org/docker/kitematic.svg?branch=master)](https://travis-ci.org/docker/kitematic)
[![Kitematic Logo](https://cloud.githubusercontent.com/assets/251292/5269258/1b229c3c-7a2f-11e4-96f1-e7baf3c86d73.png)](https://kitematic.com)

Просмотреть файл

@ -1,3 +1,14 @@
machine:
node:
version: 4.1.2
xcode:
version: "7.0"
dependencies:
cache_directories:
- "node_modules"
deployment:
release:
tag: /v.*/
owner: docker
commands:
- echo $CIRCLE_BRANCH

Просмотреть файл

@ -1,4 +1,4 @@
FROM docs/base:hugo-github-linking
FROM docs/base:latest
MAINTAINER Mary Anthony <mary@docker.com> (@moxiegirl)
RUN svn checkout https://github.com/docker/compose/trunk/docs /docs/content/compose
@ -9,7 +9,8 @@ RUN svn checkout https://github.com/docker/tutorials/trunk/docs /docs/content
RUN svn checkout https://github.com/docker/opensource/trunk/docs /docs/content/opensource
RUN svn checkout https://github.com/docker/machine/trunk/docs /docs/content/machine
ENV PROJECT=kitematic
# To get the git info for this repo
COPY . /src
COPY . /docs/content/kitematic/
COPY . /docs/content/$PROJECT/

Просмотреть файл

@ -17,7 +17,7 @@ using Kitematic and Docker.
### Create Minecraft Server Container
First, if you haven't yet done so, [download and start
Kitematic](/). Once installed and running, the app should look like this:
Kitematic](index.md). Once installed and running, the app should look like this:
Create a container from the recommended Minecraft image by clicking the "Create"
button.

Просмотреть файл

@ -25,7 +25,7 @@ Let's get to it!
#### Running the Nginx Web Server Container
First, if you haven't yet done so, [download and start
Kitematic](/). Once installed and running, the app should look like this:
Kitematic](index.md). Once installed and running, the app should look like this:
![Nginx create](images/nginx-create.png)

Просмотреть файл

@ -19,7 +19,7 @@ In this tutorial, you will:
### Setting up RethinkDB in Kitematic
First, if you haven't yet done so, [download and start
Kitematic](/). Once open, the app should look like
Kitematic](index.md). Once open, the app should look like
this:
![Rethink create button](images/rethink-create.png)

Просмотреть файл

@ -30,7 +30,7 @@ stream logs, and single click terminal into your Docker container all from the
GUI.
First, if you haven't yet done so, [download and start
Kitematic](/).
Kitematic](index.md).
## Container list

Просмотреть файл

@ -12,12 +12,15 @@
"bugs": "https://github.com/kitematic/kitematic/issues",
"scripts": {
"start": "grunt",
"start-dev": "NODE_ENV=development grunt",
"test": "jest -c jest-unit.json",
"integration": "jest -c jest-integration.json",
"release": "grunt release",
"release-mac": "grunt release-mac",
"lint": "jsxhint src"
},
"license": "Apache-2.0",
"electron-version": "0.33.6",
"electron-version": "0.33.9",
"dependencies": {
"alt": "^0.16.2",
"ansi-to-html": "0.3.0",
@ -52,7 +55,7 @@
"devDependencies": {
"babel": "^5.8.23",
"babel-jest": "^5.2.0",
"electron-prebuilt": "^0.33.6",
"electron-prebuilt": "^0.33.9",
"eslint": "^1.3.1",
"eslint-plugin-react": "^3.3.0",
"grunt": "^0.4.5",

Просмотреть файл

@ -38,6 +38,10 @@ class ContainerActions {
run (name, repo, tag) {
dockerUtil.run(name, repo, tag);
}
active (name) {
dockerUtil.active(name);
}
}
export default alt.createActions(ContainerActions);

Просмотреть файл

@ -15,7 +15,9 @@ class ContainerServerActions {
'updated',
'waiting',
'kill',
'stopped'
'stopped',
'log',
'logs'
);
}
}

Просмотреть файл

@ -53,6 +53,10 @@ app.on('ready', function () {
show: false
});
if (process.env.NODE_ENV === 'development') {
mainWindow.openDevTools({detach: true});
}
mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, 'index.html')));
app.on('activate-with-no-open-windows', function () {

Просмотреть файл

@ -1,74 +1,44 @@
import $ from 'jquery';
import React from 'react/addons';
import LogStore from '../stores/LogStore';
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({
mixins: [Router.Navigation],
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);
componentDidUpdate: function () {
var node = $('.logs').get()[0];
node.scrollTop = node.scrollHeight;
},
componentWillReceiveProps: function (nextProps) {
if (this.props.container && nextProps.container && this.props.container.Name !== nextProps.container.Name) {
LogStore.detach(this.props.container.Name);
LogStore.fetch(nextProps.container.Name);
containerActions.active(nextProps.container.Name);
}
},
componentWillUnmount: function() {
if (!this.props.container) {
return;
}
componentDidMount: function () {
containerActions.active(this.props.container.Name);
},
LogStore.detach(this.props.container.Name);
LogStore.removeListener(LogStore.SERVER_LOGS_EVENT, this.update);
},
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)
});
componentWillUnmount: function () {
containerActions.active(null);
},
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.";
}
let logs = this.props.container.Logs ?
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, '&nbsp;<wbr>'))}}></div>) :
['0 No logs for this container.'];
return (
<div className="mini-logs wrapper">
<div className="widget">

Просмотреть файл

@ -96,7 +96,7 @@ var ContainerListItem = React.createClass({
return (
<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}
<div className="info">
<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 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);

Просмотреть файл

@ -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] || [];
}
});

Просмотреть файл

@ -8,13 +8,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) {
@ -357,6 +359,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) {
@ -368,16 +449,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);
}

Просмотреть файл

@ -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;