diff --git a/.travis.yml b/.travis.yml index dabf931807..a9888c7891 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,4 @@ language: node_js -node_js: - - "0.10" sudo: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3fe22bdc17..f0a4c54814 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,29 +6,54 @@ Before you fil an issue or a pull request, quickly read of the following tips on ## Table of Contents - - [Development](#development) + - [Getting Started](#getting-started) + - [Architecture](#architecture) - [GitHub Issues](#github-issues) - [Pull Requests](#pull-requests) - [Code Guidelines](#code-guidelines) - [Testing](#testing) - [License](#license) -### Development - -- `npm install` - -To run the app in development: - -- `npm start` - -### Building & Release - -- `npm run release` - -### Unit Tests - -- `npm test` - +### Getting Started + +- `npm install` + +To run the app in development: + +- `npm start` + +### Building & Release + +- `npm run release` + +### Unit Tests + +- `npm test` + +## Architecture + +### Overview + +**Note: This architecture is work in progress and doesn't reflect the current state of the app, yet!** + +Kitematic is an application built using [atom-shell](https://github.com/atom/atom-shell) and is powered by the [Docker Engine](https://github.com/docker/docker). While it's work in progress, the goal is to make Kitematic a high-performance, portable Javascript ES6 application built with React and Reflux. It adopts a single data flow pattern: + +``` +╔═════════╗ ╔════════╗ ╔═════════════════╗ +║ Actions ║──────>║ Stores ║──────>║ View Components ║ +╚═════════╝ ╚════════╝ ╚═════════════════╝ + ^ │ + └──────────────────────────────────────┘ +``` + +As explained in the [Reflux docs](https://github.com/spoike/refluxjs), there are three primary types of objects: +- **Actions**: The main logic workhorses of the application. These objects interact with the Docker API and other endpoints to fetch new data and flowing it into the stores, which in turn create events that cause views to update. +- **Views**: Views make up the UI, and trigger available actions. +- **Stores**: Stores store the state of the application. + +### Guidelines + +- Avoid asynchronous code in Stores or Views. Instead, put code involving callbacks, promises or generators in actions. ## GitHub Issues @@ -54,9 +79,15 @@ We're thrilled to receive pull requests of any kind. Anything from bug fix, test That said, please let us know what you're planning to do! For large changes always create a proposal. Maintainers will love to give you advice on building it and it keeps the app's design coherent. ### Pull Request Requirements: -- Tests +- Includes tests - [Signed Off](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work) +## Testing + +Please try to test any new code. +- Tests can be run using `npm test` +- Kitematic uses the [Jest framework](https://facebook.github.io/jest/) by Facebook. To keep tests fast, please mock as much as possible. + ## Code Guidelines ### Javascript @@ -70,17 +101,7 @@ Kitematic is es6 ready. Please use es6 constructs where possible, they are power Run `npm run lint` before committing to ensure your javascript is up to standard. Feel free to suggest changes to the lint spec in `.jshint`. -### React - -- Use tags and elements appropriate for React / an HTML5 doctype (e.g., self-closing tags) -- Try to avoid using JQuery or manually changing the DOM. Use React instead. -- Try to build self-contained components that listen and emit events. This is definitely nowhere near perfect yet for the existing codebase. - -## Testing - -While the project is early, please try to test any new code. -- Tests can be run using `npm test` -- Kitematic uses the [Jest framework](https://facebook.github.io/jest/) by Facebook. To keep tests fast, please mock as much as possible. +We designed Kitematic to be easy to build, extend and distribute for developers. ## License diff --git a/README.md b/README.md index f363f71fe7..65e8ecef80 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Build Status](https://travis-ci.org/kitematic/kitematic.svg?branch=master)](https://travis-ci.org/kitematic/kitematic) [![Coverage Status](https://coveralls.io/repos/kitematic/kitematic/badge.svg?branch=master)](https://coveralls.io/r/kitematic/kitematic?branch=master) -[![bitHound Score](https://app.bithound.io/kitematic/kitematic/badges/score.svg)](http://app.bithound.io/kitematic/kitematic) -[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/kitematic/kitematic?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +[![bitHound Score](https://www.bithound.io/github/kitematic/kitematic/badges/score.svg)](https://www.bithound.io/github/kitematic/kitematic) + ![Kitematic Logo](https://cloud.githubusercontent.com/assets/251292/5269258/1b229c3c-7a2f-11e4-96f1-e7baf3c86d73.png) @@ -27,9 +27,9 @@ Please read through our [Contributing Guidelines](https://github.com/kitematic/k ## Community -- For questions on how to use Kitematic, see our [user forum](https://forums.docker.com/c/kitematic). +- [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/kitematic/kitematic?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +- Ask questions on our [user forum](https://forums.docker.com/c/kitematic). - **#kitematic** on IRC. [Join the channel](http://webchat.freenode.net/?channels=%23kitematic&uio=d4). -- Join the Kitematic [Gitter Channel](https://gitter.im/kitematic/kitematic) - Follow [@kitematic on Twitter](https://twitter.com/kitematic). - Read and subscribe to [the Kitematic Blog](http://blog.kitematic.com). diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 9933147169..0000000000 --- a/circle.yml +++ /dev/null @@ -1,10 +0,0 @@ -machine: - node: - version: 0.10.36 -dependencies: - cache_directories: - - "resources" - - "node_modules" -notify: - webhooks: - - url: https://coveralls.io/webhook diff --git a/gulpfile.js b/gulpfile.js index d08a68aa5a..3cb1dff9d9 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -18,7 +18,7 @@ var shell = require('gulp-shell'); var sourcemaps = require('gulp-sourcemaps'); var dependencies = Object.keys(packagejson.dependencies); -var isBeta = process.argv.indexOf('--beta') !== -1; +var argv = require('minimist')(process.argv.slice(2)); var settings; try { @@ -26,15 +26,15 @@ try { } catch (err) { settings = {}; } -settings.beta = isBeta; +settings.beta = argv.beta; var options = { dev: process.argv.indexOf('release') === -1, - beta: isBeta, - appFilename: isBeta ? 'Kitematic (Beta).app' : 'Kitematic.app', - appName: isBeta ? 'Kitematic (Beta)' : 'Kitematic', + beta: argv.beta, + appFilename: argv.beta ? 'Kitematic (Beta).app' : 'Kitematic.app', + appName: argv.beta ? 'Kitematic (Beta)' : 'Kitematic', name: 'Kitematic', - icon: isBeta ? './util/kitematic-beta.icns' : './util/kitematic.icns', + icon: argv.beta ? './util/kitematic-beta.icns' : './util/kitematic.icns', bundle: 'com.kitematic.kitematic' }; @@ -64,7 +64,6 @@ gulp.task('styles', function () { return gulp.src('styles/main.less') .pipe(plumber(function(error) { gutil.log(gutil.colors.red('Error (' + error.plugin + '): ' + error.message)); - // emit the end event, to properly end the task this.emit('end'); })) .pipe(gulpif(options.dev, changed('./build'))) @@ -107,6 +106,7 @@ gulp.task('dist', function () { 'mkdir -p dist/osx/<%= filename %>/Contents/Resources/app/resources', 'cp -v resources/* dist/osx/<%= filename %>/Contents/Resources/app/resources/ || :', 'cp <%= icon %> dist/osx/<%= filename %>/Contents/Resources/atom.icns', + 'cp ./util/Info.plist dist/osx/<%= filename %>/Contents/Info.plist', '/usr/libexec/PlistBuddy -c "Set :CFBundleVersion <%= version %>" dist/osx/<%= filename %>/Contents/Info.plist', '/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName <%= name %>" dist/osx/<%= filename %>/Contents/Info.plist', '/usr/libexec/PlistBuddy -c "Set :CFBundleName <%= name %>" dist/osx/<%= filename %>/Contents/Info.plist', diff --git a/package.json b/package.json index 34f55d73ad..f68216f331 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Kitematic", - "version": "0.5.13", + "version": "0.5.15", "author": "Kitematic", "description": "Simple Docker Container management for Mac OS X.", "homepage": "https://kitematic.com/", @@ -12,7 +12,7 @@ "bugs": "https://github.com/kitematic/kitematic/issues", "scripts": { "start": "gulp", - "test": "jest --coverage", + "test": "jest", "release": "gulp release", "release:beta": "gulp release --beta", "lint": "jsxhint src && jsxhint browser", @@ -44,9 +44,9 @@ "/node_modules/bluebird" ] }, - "docker-version": "1.5.0", - "docker-machine-version": "0.1.0-kitematic-0.5.10", - "atom-shell-version": "0.21.3", + "docker-version": "1.6.0", + "docker-machine-version": "0.2.0", + "atom-shell-version": "0.23.0", "virtualbox-version": "4.3.26", "virtualbox-filename": "VirtualBox-4.3.26.pkg", "virtualbox-filename-win": "VirtualBox-4.3.26.exe", @@ -56,7 +56,7 @@ "ansi-to-html": "0.3.0", "any-promise": "^0.1.0", "async": "^0.9.0", - "bluebird": "^2.9.12", + "bluebird": "^2.9.24", "bugsnag-js": "^2.4.7", "coveralls": "^2.11.2", "dockerode": "^2.1.1", @@ -64,41 +64,40 @@ "fs-extra": "^0.17.0", "fs-promise": "^0.3.1", "jquery": "^2.1.3", - "minimist": "^1.1.0", - "mixpanel": "0.0.20", - "node-uuid": "^1.4.2", + "mixpanel": "0.2.0", + "node-uuid": "^1.4.3", "object-assign": "^2.0.0", - "open": "0.0.5", - "react": "^0.12.2", - "react-bootstrap": "^0.15.1", + "react": "^0.13.1", + "react-bootstrap": "^0.20.3", "react-retina-image": "^1.1.2", - "react-router": "^0.12.4", - "request": "^2.53.0", + "react-router": "^0.13.2", + "request": "^2.55.0", "request-progress": "^0.3.1", - "rimraf": "^2.2.8", - "underscore": "^1.8.2" + "rimraf": "^2.3.2", + "underscore": "^1.8.3" }, "devDependencies": { - "babel": "^4.5.5", + "babel": "^5.1.10", "gulp": "^3.8.11", - "gulp-babel": "^4.0.0", - "gulp-changed": "^1.1.1", + "gulp-babel": "^5.1.0", + "gulp-changed": "^1.2.1", "gulp-concat": "^2.5.2", "gulp-cssmin": "^0.1.6", "gulp-download-atom-shell": "0.0.4", "gulp-if": "^1.2.5", "gulp-insert": "^0.4.0", - "gulp-less": "^3.0.1", + "gulp-less": "^3.0.2", "gulp-livereload": "^3.8.0", - "gulp-plumber": "^0.6.6", - "gulp-react": "^2.0.0", - "gulp-rename": "^1.2.0", - "gulp-shell": "^0.3.0", - "gulp-sourcemaps": "^1.5.0", + "gulp-plumber": "^1.0.0", + "gulp-react": "^3.0.1", + "gulp-shell": "^0.4.1", + "gulp-sourcemaps": "^1.5.2", "gulp-util": "^3.0.4", + "gulp": "^3.8.11", "jest-cli": "kitematic/jest", - "jsxhint": "^0.12.1", - "react-tools": "^0.12.2", + "jsxhint": "^0.14.0", + "minimist": "^1.1.1", + "react-tools": "^0.13.1", "run-sequence": "^1.0.2" } } diff --git a/src/ContainerHome.react.js b/src/ContainerHome.react.js index 4cb9df636b..d46c480c56 100644 --- a/src/ContainerHome.react.js +++ b/src/ContainerHome.react.js @@ -112,7 +112,7 @@ var ContainerHome = React.createClass({
- +
@@ -138,7 +138,7 @@ var ContainerHome = React.createClass({
- +
{right}
diff --git a/src/ContainerHomeLogs.react.js b/src/ContainerHomeLogs.react.js index d400a48d72..fba6a1b4f4 100644 --- a/src/ContainerHomeLogs.react.js +++ b/src/ContainerHomeLogs.react.js @@ -6,51 +6,54 @@ var metrics = require('./Metrics'); var _prevBottom = 0; -var ContainerHomeLogs = React.createClass({ - mixins: [Router.State, Router.Navigation], +module.exports = React.createClass({ + mixins: [Router.Navigation], getInitialState: function () { return { logs: [] }; }, - componentWillReceiveProps: function () { - this.init(); - }, componentDidMount: function() { - this.init(); - LogStore.on(LogStore.SERVER_LOGS_EVENT, this.updateLogs); + 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() { - LogStore.removeListener(LogStore.SERVER_LOGS_EVENT, this.updateLogs); + if (!this.props.container) { + return; + } + + LogStore.detach(this.props.container.Name); + LogStore.removeListener(LogStore.SERVER_LOGS_EVENT, this.update); }, componentDidUpdate: function () { - // Scroll logs to bottom + this.scrollToBottom(); + }, + scrollToBottom: function () { var parent = $('.logs'); if (parent.scrollTop() >= _prevBottom - 50) { parent.scrollTop(parent[0].scrollHeight - parent.height()); } _prevBottom = parent[0].scrollHeight - parent.height(); }, - init: function () { - this.updateLogs(); - }, - updateLogs: function (name) { - if (name && name !== this.getParams().name) { - return; - } - this.setState({ - logs: LogStore.logs(this.getParams().name) - }); - }, handleClickLogs: function () { metrics.track('Viewed Logs', { from: 'preview' }); - this.transitionTo('containerLogs', {name: this.getParams().name}); + this.transitionTo('containerLogs', {name: this.props.container.Name}); + }, + update: function () { + this.setState({ + logs: LogStore.logs(this.props.container.Name) + }); }, render: function () { var logs = this.state.logs.map(function (l, i) { - return

; + return ; }); if (logs.length === 0) { logs = "No logs for this container."; @@ -62,11 +65,8 @@ var ContainerHomeLogs = React.createClass({
{logs}
-
View Logs
-
+
View Logs
); } }); - -module.exports = ContainerHomeLogs; diff --git a/src/ContainerLogs.react.js b/src/ContainerLogs.react.js index e20910cc49..f7b0b24064 100644 --- a/src/ContainerLogs.react.js +++ b/src/ContainerLogs.react.js @@ -1,49 +1,46 @@ var $ = require('jquery'); var React = require('react/addons'); var LogStore = require('./LogStore'); -var Router = require('react-router'); var _prevBottom = 0; -var ContainerLogs = React.createClass({ - mixins: [Router.State], +module.exports = React.createClass({ getInitialState: function () { return { logs: [] }; }, - componentWillReceiveProps: function () { - this.init(); - }, componentDidMount: function() { - this.init(); - LogStore.on(LogStore.SERVER_LOGS_EVENT, this.updateLogs); + 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() { - LogStore.removeListener(LogStore.SERVER_LOGS_EVENT, this.updateLogs); + LogStore.detach(this.props.container.Name); + LogStore.removeListener(LogStore.SERVER_LOGS_EVENT, this.update); }, componentDidUpdate: function () { - // Scroll logs to bottom + 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(); }, - init: function () { - this.updateLogs(); - }, - updateLogs: function (name) { - if (name && name !== this.getParams().name) { - return; - } + update: function () { this.setState({ - logs: LogStore.logs(this.getParams().name) + logs: LogStore.logs(this.props.container.Name) }); }, render: function () { var logs = this.state.logs.map(function (l, i) { - return

; + return ; }); if (logs.length === 0) { logs = "No logs for this container."; @@ -55,5 +52,3 @@ var ContainerLogs = React.createClass({ ); } }); - -module.exports = ContainerLogs; diff --git a/src/LogStore.js b/src/LogStore.js index 9735b5b98c..fa14b34e11 100644 --- a/src/LogStore.js +++ b/src/LogStore.js @@ -2,59 +2,79 @@ var EventEmitter = require('events').EventEmitter; var assign = require('object-assign'); var Convert = require('ansi-to-html'); var docker = require('./Docker'); +var stream = require('stream'); var _convert = new Convert(); var _logs = {}; var _streams = {}; -var LogStore = assign(Object.create(EventEmitter.prototype), { +var MAX_LOG_SIZE = 3000; + +module.exports = assign(Object.create(EventEmitter.prototype), { SERVER_LOGS_EVENT: 'server_logs_event', - _escapeHTML: function (html) { + _escape: function (html) { var text = document.createTextNode(html); var div = document.createElement('div'); div.appendChild(text); return div.innerHTML; }, - fetchLogs: function (name) { + fetch: function (name) { if (!name || !docker.client()) { return; } - var index = 0; - var self = this; docker.client().getContainer(name).logs({ - follow: true, stdout: true, stderr: true, - timestamps: true - }, function (err, stream) { - if (_streams[name]) { - return; - } - _streams[name] = stream; + timestamps: false, + tail: MAX_LOG_SIZE, + follow: false + }, (err, logStream) => { if (err) { - return; + throw err; } - _logs[name] = []; - stream.setEncoding('utf8'); - stream.on('data', function (buf) { - // Every other message is a header - if (index % 2 === 1) { - //var time = buf.substr(0,buf.indexOf(' ')); - var msg = buf.substr(buf.indexOf(' ')+1); - _logs[name].push(_convert.toHtml(self._escapeHTML(msg))); - self.emit(self.SERVER_LOGS_EVENT); - } - index += 1; + 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))); }); - stream.on('end', function () { - delete _streams[name]; + logStream.on('end', () => { + _logs[name] = logs; + this.emit(this.SERVER_LOGS_EVENT); + this.attach(name); }); }); }, - logs: function (name) { - if (!_streams[name]) { - this.fetchLogs(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) { + throw err; + } + 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); + }); + }); + }, + detach: function (name) { + if (_streams[name]) { + _streams[name].destroy(); + } + }, + logs: function (name) { return _logs[name] || []; }, rename: function (name, newName) { @@ -63,5 +83,3 @@ var LogStore = assign(Object.create(EventEmitter.prototype), { } } }); - -module.exports = LogStore; diff --git a/src/SetupStore-test.js b/src/SetupStore-test.js index b8ddc79892..eea446151b 100644 --- a/src/SetupStore-test.js +++ b/src/SetupStore-test.js @@ -81,19 +81,14 @@ describe('SetupStore', function () { machine.isoversion.mockReturnValue('1.0'); machine.stop.mockReturnValue(Promise.resolve()); machine.start.mockReturnValue(Promise.resolve()); - machine.regenerateCerts.mockReturnValue(Promise.resolve()); machine.upgrade.mockReturnValue(Promise.resolve()); setupUtil.compareVersions.mockReturnValue(-1); machine.create.mockClear(); machine.upgrade.mockClear(); - machine.stop.mockClear(); machine.start.mockClear(); - machine.regenerateCerts.mockClear(); return setupStore.steps().init.run(() => {}).then(() => { expect(machine.create).not.toBeCalled(); - expect(machine.stop).toBeCalled(); expect(machine.upgrade).toBeCalled(); - expect(machine.regenerateCerts).toBeCalled(); expect(machine.start).toBeCalled(); }); }); diff --git a/src/SetupStore.js b/src/SetupStore.js index f1c93cdf30..ea62de9860 100644 --- a/src/SetupStore.js +++ b/src/SetupStore.js @@ -99,19 +99,14 @@ var _steps = [{ return; } - if ((yield machine.state()) === 'Saved') { - yield virtualBox.wake(machine.name()); - } - var isoversion = machine.isoversion(); var packagejson = util.packagejson(); if (!isoversion || setupUtil.compareVersions(isoversion, packagejson['docker-version']) < 0) { - yield machine.stop(); + yield machine.start(); yield machine.upgrade(); } if ((yield machine.state()) !== 'Running') { yield machine.start(); - yield machine.regenerateCerts(); } }) }]; diff --git a/util/deps b/util/deps index 4bd6a249bc..ecd9322500 100755 --- a/util/deps +++ b/util/deps @@ -21,9 +21,7 @@ fi if [ ! -f $DOCKER_MACHINE_CLI_FILE ]; then echo "-----> Downloading Docker Machine CLI..." rm -rf docker-machine* - # Use temporary, new version of docker-machine that has some important fixes - # curl -L -o $DOCKER_MACHINE_CLI_FILE https://github.com/docker/machine/releases/download/v$DOCKER_MACHINE_CLI_VERSION/docker-machine_darwin-amd64 - curl -L -o $DOCKER_MACHINE_CLI_FILE https://github.com/kitematic/kitematic/releases/download/v0.5.10/docker-machine_darwin-amd64 + curl -L -o $DOCKER_MACHINE_CLI_FILE https://github.com/docker/machine/releases/download/v$DOCKER_MACHINE_CLI_VERSION/docker-machine_darwin-amd64 chmod +x $DOCKER_MACHINE_CLI_FILE fi