diff --git a/app/ContainerDetails.react.js b/app/ContainerDetails.react.js index 86c66974dc..63c0e733c8 100644 --- a/app/ContainerDetails.react.js +++ b/app/ContainerDetails.react.js @@ -13,25 +13,12 @@ var docker = require('./docker.js'); var ContainerDetails = React.createClass({ mixins: [Router.State], - componentDidMount: function () { - ContainerStore.addChangeListener(this.update); + getInitialState: function () { + return { + logs: [] + }; }, - componentWillUnmount: function () { - ContainerStore.removeChangeListener(this.update); - }, - update: function () { - var containerId = this.getParams().Id; - this.setState({ - container: ContainerStore.containers()[containerId] - }); - }, - _escapeHTML: function (html) { - var text = document.createTextNode(html); - var div = document.createElement('div'); - div.appendChild(text); - return div.innerHTML; - }, - componentWillReceiveProps: function () { + componentWillMount: function () { this.update(); var self = this; var logs = []; @@ -74,15 +61,27 @@ var ContainerDetails = React.createClass({ }); }); }, + componentDidMount: function () { + ContainerStore.addChangeListener(this.update); + }, + componentWillUnmount: function () { + ContainerStore.removeChangeListener(this.update); + }, + update: function () { + var containerId = this.getParams().Id; + this.setState({ + container: ContainerStore.containers()[containerId] + }); + }, + _escapeHTML: function (html) { + var text = document.createTextNode(html); + var div = document.createElement('div'); + div.appendChild(text); + return div.innerHTML; + }, render: function () { var self = this; - if (!this.state || !this.state.logs) { - return false; - } - - // console.log(container); - if (!this.state) { return
; } diff --git a/app/ContainerStore.js b/app/ContainerStore.js index 973247bf17..3c0dc93ed9 100644 --- a/app/ContainerStore.js +++ b/app/ContainerStore.js @@ -3,22 +3,49 @@ var async = require('async'); var assign = require('react/lib/Object.assign'); var docker = require('./docker.js'); var $ = require('jquery'); +var _ = require('underscore'); // Merge our store with Node's Event Emitter var ContainerStore = assign(EventEmitter.prototype, { _containers: {}, - init: function () { - // TODO: Load cached data from leveldb - // Check if the pulled image is working + _logs: {}, + init: function (callback) { + // TODO: Load cached data from db on loading // Refresh with docker & hook into events var self = this; this.update(function (err) { + callback(); + var downloading = _.filter(_.values(self._containers), function (container) { + var env = container.Config.Env; + return _.indexOf(env, 'KITEMATIC_DOWNLOADING=true') !== -1; + }); + + // Recover any pulls that were happening + downloading.forEach(function (container) { + var env = _.object(container.Config.Env.map(function (e) { + return e.split('='); + })); + docker.client().pull(env.KITEMATIC_DOWNLOADING_IMAGE, function (err, stream) { + stream.setEncoding('utf8'); + stream.on('data', function (data) { + console.log(data); + }); + stream.on('end', function () { + self._createContainer(env.KITEMATIC_DOWNLOADING_IMAGE, container.Name.replace('/', ''), function () { + console.log('RECOVERED'); + }); + }); + }); + }); + docker.client().getEvents(function (err, stream) { stream.setEncoding('utf8'); stream.on('data', function (data) { - self.update(function (err) { + // TODO: Make + self.update(function (err) { + console.log('Updated container data.'); }); }); }); @@ -45,57 +72,133 @@ var ContainerStore = assign(EventEmitter.prototype, { containers[r.Id] = r; }); self._containers = containers; + console.log(containers); self.emit('change'); callback(null); }); }); }, - _createContainer: function (image) { - docker.client().createContainer({ - Image: image, - Tty: false - }, function (err, container) { + _pullScratchImage: function (callback) { + var image = docker.client().getImage('scratch:latest'); + image.inspect(function (err, data) { if (err) { - callback(err, null); + callback(err); return; } - console.log('Created container: ' + container.id); - container.start({ - PublishAllPorts: true - }, function (err) { - if (err) { callback(err, null); return; } - console.log('Started container: ' + container.id); - callback(null, container); + if (!data) { + docker.client().pull('scratch:latest', function (err, stream) { + if (err) { + callback(err); + return; + } + stream.setEncoding('utf8'); + stream.on('data', function (data) {}); + stream.on('end', function () { + callback(); + }); + }); + } else { + callback(); + } + }); + }, + _createContainer: function (image, name, callback) { + var existing = docker.client().getContainer(name); + existing.remove(function (err, data) { + console.log('Placeholder removed.'); + docker.client().createContainer({ + Image: image, + Tty: false, + name: name + }, function (err, container) { + if (err) { + callback(err, null); + return; + } + console.log('Created container: ' + container.id); + container.start({ + PublishAllPorts: true + }, function (err) { + if (err) { callback(err, null); return; } + console.log('Started container: ' + container.id); + callback(null, container); + }); }); }); }, + _createPlaceholderContainer: function (imageName, name, callback) { + console.log('_createPlaceholderContainer', imageName, name); + this._pullScratchImage(function (err) { + docker.client().createContainer({ + Image: 'scratch:latest', + Tty: false, + Env: [ + 'KITEMATIC_DOWNLOADING=true', + 'KITEMATIC_DOWNLOADING_IMAGE=' + imageName + ], + Cmd: 'placeholder', + name: name + }, function (err, container) { + callback(err, container); + }); + }); + }, + _generateName: function (repository) { + var base = _.last(repository.split('/')); + var count = 1; + var name = base; + while (true) { + var exists = _.findWhere(_.values(this._containers), {Name: '/' + name}) || _.findWhere(_.values(this._containers), {Name: name}); + if (!exists) { + return name; + } else { + count++; + name = base + '-' + count; + } + } + }, create: function (repository, tag, callback) { + + console.log('create', repository, tag); + + var containerName = this._generateName(repository); tag = tag || 'latest'; - var name = repository + ':' + tag; + var imageName = repository + ':' + tag; // Check if image is not local or already being downloaded console.log('Creating container.'); var self = this; - var image = docker.client().getImage(name); + var image = docker.client().getImage(imageName); + console.log(image); image.inspect(function (err, data) { + // TODO: Get image size from registry API /*$.get('https://registry.hub.docker.com/v1/repositories/' + repository + '/tags/' + tag, function (data) { });*/ - if (data === null) { + if (!data) { // Pull image - docker.client().pull(name, function (err, stream) { - stream.setEncoding('utf8'); - stream.on('data', function (data) { - console.log(data); - }); - stream.on('end', function () { - self._createContainer(name); + self._createPlaceholderContainer(imageName, containerName, function (err, container) { + if (err) { + console.log(err); + } + console.log('Placeholder container created.'); + docker.client().pull(imageName, function (err, stream) { + stream.setEncoding('utf8'); + stream.on('data', function (data) { + console.log(data); + }); + stream.on('end', function () { + self._createContainer(imageName, containerName, function () { + }); + }); }); }); // Create placeholder container } else { // If not then directly create the container - self._createContainer(name); + self._createContainer(imageName, containerName, function () { + console.log('done'); + }); } }); // If so then create a container w/ kitematic-only 'downloading state' diff --git a/app/Containers.react.js b/app/Containers.react.js index c1c81e5274..c9a562e43e 100644 --- a/app/Containers.react.js +++ b/app/Containers.react.js @@ -24,10 +24,17 @@ var ContainerList = React.createClass({ }; }, handleClick: function () { - console.log('hi'); + }, componentDidMount: function () { + this.update(); ContainerStore.addChangeListener(this.update); + if (this.state.containers.length > 0) { + this.transitionTo('container', {Id: this.state.containers[0].Id}); + } + }, + componentWillMount: function () { + this._start = Date.now(); }, componentWillUnmount: function () { ContainerStore.removeChangeListener(this.update); @@ -36,24 +43,50 @@ var ContainerList = React.createClass({ var containers = _.values(ContainerStore.containers()).sort(function (a, b) { return a.Name.localeCompare(b.Name); }); - console.log(containers); - if (containers.length > 0) { - this.transitionTo('container', {Id: containers[0].Id, container: containers[0]}); - } this.setState({ containers: containers }); }, render: function () { + var self = this; var containers = this.state.containers.map(function (container) { + var downloadingImage = null, downloading = false; + var env = container.Config.Env; + if (env.length) { + var obj = _.object(env.map(function (e) { + return e.split('='); + })); + if (obj.KITEMATIC_DOWNLOADING) { + downloading = true; + } + downloadingImage = obj.KITEMATIC_DOWNLOADING_IMAGE || null; + } + + var imageName = downloadingImage || container.Config.Image; + var state; - if (container.State.Running) { - state =
; + + // Synchronize all animations + var style = { + WebkitAnimationDelay: (self._start - Date.now()) + 'ms' + }; + + if (downloading) { + state =
; + } else if (container.State.Running && !container.State.Paused) { + state =
; + } else if (container.State.Restarting) { + state =
; + } else if (container.State.Paused) { + state =
; + } else if (container.State.ExitCode) { + // state =
; + state =
; } else { - state =
; + state =
; } return ( - +
  • {state}
    @@ -61,7 +94,7 @@ var ContainerList = React.createClass({ {container.Name.replace('/', '')}
    - {container.Config.Image} + {imageName}
  • @@ -94,7 +127,7 @@ var Containers = React.createClass({ } }, handleClick: function () { - // ContainerStore.create('jbfink/wordpress', 'latest'); + ContainerStore.create('dockerfile/ghost', 'latest', 'testghost'); }, render: function () { var sidebarHeaderClass = 'sidebar-header'; @@ -102,12 +135,12 @@ var Containers = React.createClass({ sidebarHeaderClass += ' sep'; } return ( -
    +
    -

    containers

    +

    containers

    }>
    diff --git a/app/Setup.react.js b/app/Setup.react.js index 934b86e948..f04f58a969 100644 --- a/app/Setup.react.js +++ b/app/Setup.react.js @@ -9,6 +9,7 @@ var boot2docker = require('./boot2docker.js'); var virtualbox = require('./virtualbox.js'); var util = require('./util.js'); var docker = require('./docker.js'); +var ContainerStore = require('./ContainerStore.js'); var setupSteps = [ { diff --git a/app/boot2docker.js b/app/boot2docker.js index b788db41a2..f4ef4858a9 100644 --- a/app/boot2docker.js +++ b/app/boot2docker.js @@ -156,6 +156,11 @@ var Boot2Docker = { } }); }, + createScratchImage: function (callback) { + cmdExec([Boot2Docker.command(), 'ssh', 'tar cv --files-from /dev/null | docker import - scratch'], function (err, out) { + callback(err); + }); + }, stats: function (callback) { var self = this; self.state(function (err, state) { diff --git a/app/images/downloading-arrow.png b/app/images/downloading-arrow.png new file mode 100644 index 0000000000..dfca72b6ae Binary files /dev/null and b/app/images/downloading-arrow.png differ diff --git a/app/images/downloading-arrow@2x.png b/app/images/downloading-arrow@2x.png new file mode 100644 index 0000000000..7f589b2596 Binary files /dev/null and b/app/images/downloading-arrow@2x.png differ diff --git a/app/images/downloading.png b/app/images/downloading.png new file mode 100644 index 0000000000..2dd4acfa27 Binary files /dev/null and b/app/images/downloading.png differ diff --git a/app/images/downloading@2x.png b/app/images/downloading@2x.png new file mode 100644 index 0000000000..b3d8b3a16d Binary files /dev/null and b/app/images/downloading@2x.png differ diff --git a/app/images/error.png b/app/images/error.png new file mode 100644 index 0000000000..886c466340 Binary files /dev/null and b/app/images/error.png differ diff --git a/app/images/error@2x.png b/app/images/error@2x.png new file mode 100644 index 0000000000..e075bb3295 Binary files /dev/null and b/app/images/error@2x.png differ diff --git a/app/images/paused.png b/app/images/paused.png new file mode 100644 index 0000000000..221adf9f1b Binary files /dev/null and b/app/images/paused.png differ diff --git a/app/images/paused@2x.png b/app/images/paused@2x.png new file mode 100644 index 0000000000..dea41b1881 Binary files /dev/null and b/app/images/paused@2x.png differ diff --git a/app/images/stopped.png b/app/images/stopped.png new file mode 100644 index 0000000000..2b64d4a3cd Binary files /dev/null and b/app/images/stopped.png differ diff --git a/app/images/stopped@2x.png b/app/images/stopped@2x.png new file mode 100644 index 0000000000..776f0c8aea Binary files /dev/null and b/app/images/stopped@2x.png differ diff --git a/app/index.html b/app/index.html index b33a677446..5b3d4ba12d 100644 --- a/app/index.html +++ b/app/index.html @@ -2,6 +2,7 @@ + diff --git a/app/main.js b/app/main.js index 6f4173ba60..11d5f92eaf 100644 --- a/app/main.js +++ b/app/main.js @@ -21,16 +21,13 @@ var NoContainers = React.createClass({ render: function () { return (
    - + No Containers
    ); } }); var App = React.createClass({ - componentWillMount: function () { - ContainerStore.init(); - }, render: function () { return ( @@ -53,8 +50,14 @@ var routes = ( Router.run(routes, function (Handler) { boot2docker.ip(function (err, ip) { - docker.setHost(ip); - React.render(, document.body); + if (!err) { + docker.setHost(ip); + ContainerStore.init(function () { + React.render(, document.body); + }); + } else { + React.render(, document.body); + } }); }); diff --git a/app/styles/main.less b/app/styles/main.less index f5a4146218..d2e3fe941a 100644 --- a/app/styles/main.less +++ b/app/styles/main.less @@ -188,9 +188,9 @@ li { vertical-align: middle; - margin: 11px 24px 0px; + margin: 14px 24px 0px; border-bottom: 1px solid #efefef; - padding-bottom: 11px; + padding-bottom: 14px; display: flex; flex-direction: row; @@ -209,34 +209,65 @@ } .image { color: #999; - font-size: 12px; + font-size: 10px; font-weight: 400; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } } .state { margin-top: 8px; display: inline-block; + position: relative; + min-width: 20px; + height: 20px; + } + + .state-error { + .at2x('error.png', 20px, 20px); + } + + .state-stopped { + .at2x('stopped.png', 20px, 20px); + } + + .state-paused { + .at2x('paused.png', 20px, 20px); + } + + .state-downloading { + .at2x('downloading.png', 20px, 20px); + overflow: hidden; + + .downloading-arrow { + width: 20px; + height: 20px; + .at2x('downloading-arrow.png', 20px, 20px); + position: absolute; + // background-repeat: repeat; + -webkit-animation-name: translatedownload; + -webkit-animation-duration: 1.8s; + -webkit-animation-iteration-count: infinite; + -webkit-animation-timing-function: linear; + } } .state-running { - display: inline-block; - position: relative; - width: 20px; - height: 20px; .at2x('running.png', 20px, 20px); overflow: hidden; // -webkit-mask-image: -webkit-radial-gradient(circle, white, black); .runningwave { position: absolute; - width: 44px; + width: 40px; height: 20px; left: -20px; .at2x('runningwave.png', 20px, 20px); // background-repeat: repeat; - -webkit-animation-name: translate; - -webkit-animation-duration: 6.0s; + -webkit-animation-name: translatewave; + -webkit-animation-duration: 8.0s; -webkit-animation-iteration-count: infinite; -webkit-animation-timing-function: linear; } @@ -248,6 +279,7 @@ height: 20px; .at2x('restarting.png', 20px, 20px); background-repeat: repeat-x; + -webkit-animation-delay: -1s; -webkit-animation-name: rotate; -webkit-animation-duration: 3.0s; -webkit-animation-iteration-count: infinite; @@ -478,7 +510,7 @@ html, body { height: 100%; } -@-webkit-keyframes rotate { +@-webkit-keyframes translatedownload { from { -webkit-transform: rotate(0deg); } @@ -487,7 +519,7 @@ html, body { } } -@-webkit-keyframes translate { +@-webkit-keyframes translatewave { from { -webkit-transform: translateX(0px); } @@ -495,3 +527,22 @@ html, body { -webkit-transform: translateX(20px); } } + +@-webkit-keyframes translatedownload { + 0% { + -webkit-transform: translateY(6px); + opacity: 0; + } + 25% { + opacity: 1; + -webkit-transform: translateY(6px); + } + 50% { + opacity: 1; + -webkit-transform: translateY(20px); + } + 100% { + opacity: 1; + -webkit-transform: translateY(20px); + } +} diff --git a/browser/main.js b/browser/main.js index 049802f1ef..ba2c77ebb2 100644 --- a/browser/main.js +++ b/browser/main.js @@ -24,7 +24,6 @@ app.on('activate-with-no-open-windows', function () { }); app.on('ready', function() { - // Create the browser window. var windowOptions = { width: 1200, height: 800,