diff --git a/app/Containers.react.js b/app/ContainerList.react.js
similarity index 66%
rename from app/Containers.react.js
rename to app/ContainerList.react.js
index c87968ad5c..f63d07b534 100644
--- a/app/Containers.react.js
+++ b/app/ContainerList.react.js
@@ -1,3 +1,6 @@
+var async = require('async');
+var _ = require('underscore');
+var $ = require('jquery');
var React = require('react/addons');
var Router = require('react-router');
var Modal = require('react-bootstrap/Modal');
@@ -6,10 +9,7 @@ var ModalTrigger = require('react-bootstrap/ModalTrigger');
var ContainerModal = require('./ContainerModal.react');
var ContainerStore = require('./ContainerStore');
var Header = require('./Header.react');
-var async = require('async');
-var _ = require('underscore');
var docker = require('./docker');
-var $ = require('jquery');
var Link = Router.Link;
var RouteHandler = Router.RouteHandler;
@@ -24,37 +24,34 @@ var ContainerList = React.createClass({
};
},
componentDidMount: function () {
- this.update();
- if (this.state.containers.length > 0) {
- var name = this.state.containers[0].Name.replace('/', '');
- active = name;
- ContainerStore.setActive(name);
- }
- ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.update);
- ContainerStore.addChangeListener(ContainerStore.ACTIVE, this.update);
+ this.updateContainers();
+ ContainerStore.addChangeListener(ContainerStore.ACTIVE, this.updateActive);
+ ContainerStore.addChangeListener(ContainerStore.CONTAINERS, this.updateContainers);
},
componentWillMount: function () {
this._start = Date.now();
},
componentWillUnmount: function () {
- ContainerStore.removeChangeListener(ContainerStore.CONTAINERS, this.update);
- ContainerStore.removeChangeListener(ContainerStore.ACTIVE, this.update);
+ ContainerStore.removeChangeListener(ContainerStore.CONTAINERS, this.updateContainers);
+ ContainerStore.removeChangeListener(ContainerStore.ACTIVE, updateActive.update);
},
- componentDidUpdate: function () {
-
+ updateActive: function () {
+ if (ContainerStore.active()) {
+ this.transitionTo('container', {name: ContainerStore.active()});
+ }
},
- update: function () {
+ updateContainers: function () {
+ // Sort by name
var containers = _.values(ContainerStore.containers()).sort(function (a, b) {
return a.Name.localeCompare(b.Name);
});
- this.setState({
- active: ContainerStore.active(),
- containers: containers
- });
+ this.setState({containers: containers});
- if (ContainerStore.active()) {
- this.transitionTo('container', {name: ContainerStore.active()});
+ // Transition to the active container or set one
+ var active = ContainerStore.active();
+ if (!ContainerStore.container(active) && containers.length > 0) {
+ ContainerStore.setActive(containers[0].Name.replace('/', ''));
}
},
handleClick: function (containerId) {
@@ -77,13 +74,12 @@ var ContainerList = React.createClass({
var imageName = downloadingImage || container.Config.Image;
- var state;
-
// Synchronize all animations
var style = {
WebkitAnimationDelay: (self._start - Date.now()) + 'ms'
};
+ var state;
if (downloading) {
state =
;
} else if (container.State.Running && !container.State.Paused) {
@@ -123,52 +119,4 @@ var ContainerList = React.createClass({
}
});
-var Containers = React.createClass({
- getInitialState: function () {
- return {
- sidebarOffset: 0
- };
- },
- handleScroll: function (e) {
- if (e.target.scrollTop > 0 && !this.state.sidebarOffset) {
- this.setState({
- sidebarOffset: e.target.scrollTop
- });
- } else if (e.target.scrollTop === 0 && this.state.sidebarOffset) {
- this.setState({
- sidebarOffset: 0
- });
- }
- },
- render: function () {
- var sidebarHeaderClass = 'sidebar-header';
- if (this.state.sidebarOffset) {
- sidebarHeaderClass += ' sep';
- }
- return (
-
- );
- }
-});
-
-module.exports = Containers;
+module.exports = ContainerList;
diff --git a/app/ContainerModal.react.js b/app/ContainerModal.react.js
index 8b5d74cdb0..a01de96227 100644
--- a/app/ContainerModal.react.js
+++ b/app/ContainerModal.react.js
@@ -8,6 +8,7 @@ var ContainerStore = require('./ContainerStore');
var Navigation = Router.Navigation;
var ContainerModal = React.createClass({
+ _searchRequest: null,
getInitialState: function () {
return {
query: '',
@@ -19,9 +20,11 @@ var ContainerModal = React.createClass({
},
search: function (query) {
var self = this;
- $.get('https://registry.hub.docker.com/v1/search?q=' + query, function (result) {
- self.setState(result);
- console.log(result);
+ this._searchRequest = $.get('https://registry.hub.docker.com/v1/search?q=' + query, function (result) {
+ if (self.isMounted()) {
+ self.setState(result);
+ console.log(result);
+ }
});
},
handleChange: function (e) {
@@ -30,6 +33,11 @@ var ContainerModal = React.createClass({
if (query === this.state.query) {
return;
}
+
+ if (this._searchRequest) {
+ this._searchRequest.abort();
+ this._searchRequest = null;
+ }
clearTimeout(this.timeout);
var self = this;
this.timeout = setTimeout(function () {
diff --git a/app/ContainerStore.js b/app/ContainerStore.js
index cb401dd744..2f75319cf2 100644
--- a/app/ContainerStore.js
+++ b/app/ContainerStore.js
@@ -1,89 +1,21 @@
var EventEmitter = require('events').EventEmitter;
var async = require('async');
var assign = require('react/lib/Object.assign');
-var docker = require('./docker.js');
+var docker = require('./docker');
+var registry = require('./registry');
var $ = require('jquery');
var _ = require('underscore');
// Merge our store with Node's Event Emitter
var ContainerStore = assign(EventEmitter.prototype, {
CONTAINERS: 'containers',
+ PROGRESS: 'progress',
+ LOGS: 'logs',
ACTIVE: 'active',
_containers: {},
+ _progress: {},
_logs: {},
_active: null,
- 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) {
- console.log(data);
-
- // TODO: Dont refresh on deleting placeholder containers
- self.update(function (err) {
- console.log('Updated container data.');
- });
- });
- });
- });
- },
- update: function (callback) {
- var self = this;
- docker.client().listContainers({all: true}, function (err, containers) {
- if (err) {
- callback(err);
- return;
- }
- async.map(containers, function(container, callback) {
- docker.client().getContainer(container.Id).inspect(function (err, data) {
- callback(err, data);
- });
- }, function (err, results) {
- if (err) {
- callback(err);
- return;
- }
- var containers = {};
- results.forEach(function (r) {
- containers[r.Name.replace('/', '')] = r;
- });
- self._containers = containers;
- _.keys(self._containers).forEach(function(c) {
- self.emit(c);
- });
- self.emit(self.CONTAINERS);
- callback(null);
- });
- });
- },
_pullScratchImage: function (callback) {
var image = docker.client().getImage('scratch:latest');
image.inspect(function (err, data) {
@@ -163,41 +95,156 @@ var ContainerStore = assign(EventEmitter.prototype, {
}
}
},
- // Returns all containers
- containers: function() {
- return this._containers;
+ 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 () {
+
+ });
+ });
+ });
+ });
+
+ docker.client().getEvents(function (err, stream) {
+ stream.setEncoding('utf8');
+ stream.on('data', function (data) {
+ console.log(data);
+
+ // TODO: Dont refresh on deleting placeholder containers
+ var deletingPlaceholder = data.status === 'destroy' && self.container(data.id) && self.container(data.id).Config.Env.indexOf('KITEMATIC_DOWNLOADING=true') !== -1;
+ console.log(deletingPlaceholder);
+ if (!deletingPlaceholder) {
+ self.update(function (err) {
+ console.log('Updated container data.');
+ });
+ }
+ });
+ });
+ });
+ },
+ update: function (callback) {
+ var self = this;
+ docker.client().listContainers({all: true}, function (err, containers) {
+ if (err) {
+ callback(err);
+ return;
+ }
+ async.map(containers, function(container, callback) {
+ docker.client().getContainer(container.Id).inspect(function (err, data) {
+ callback(err, data);
+ });
+ }, function (err, results) {
+ if (err) {
+ callback(err);
+ return;
+ }
+ var containers = {};
+ results.forEach(function (r) {
+ containers[r.Name.replace('/', '')] = r;
+ });
+ self._containers = containers;
+ self.emit(self.CONTAINERS);
+ callback(null);
+ });
+ });
},
create: function (repository, tag, callback) {
- var containerName = this._generateName(repository);
tag = tag || 'latest';
- var imageName = repository + ':' + tag;
- // Check if image is not local or already being downloaded
- console.log('Creating container.');
var self = this;
+ var imageName = repository + ':' + tag;
+ var containerName = this._generateName(repository);
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) {
- });*/
+ image.inspect(function (err, data) {
if (!data) {
// Pull image
self._createPlaceholderContainer(imageName, containerName, function (err, container) {
if (err) {
console.log(err);
}
- console.log('Placeholder container created.');
- docker.client().pull(imageName, function (err, stream) {
- console.log(containerName);
- callback(null, containerName);
- stream.setEncoding('utf8');
- stream.on('data', function (data) {
- // TODO: update progress
- //console.log(data);
- });
- stream.on('end', function () {
- self._createContainer(imageName, containerName, function () {
+ registry.layers(repository, tag, function (err, layerSizes) {
+ if (err) {
+ callback(err);
+ }
+
+ // TODO: Support v2 registry API
+ // TODO: clean this up- It's messy to work with pulls from both the v1 and v2 registry APIs
+ // Use the per-layer pull progress % to update the total progress.
+ docker.client().listImages({all: 1}, function(err, images) {
+ var existingIds = new Set(images.map(function (image) {
+ return image.Id.slice(0, 12);
+ }));
+ var layersToDownload = layerSizes.filter(function (layerSize) {
+ return !existingIds.has(layerSize.Id);
+ });
+
+ var totalBytes = layersToDownload.map(function (s) { return s.size; }).reduce(function (pv, sv) { return pv + sv; }, 0);
+ docker.client().pull(imageName, function (err, stream) {
+ callback(null, containerName);
+ stream.setEncoding('utf8');
+
+ var layerProgress = layersToDownload.reduce(function (r, layer) {
+ if (_.findWhere(images, {Id: layer.Id})) {
+ r[layer.Id] = 100;
+ } else {
+ r[layer.Id] = 0;
+ }
+ return r;
+ }, {});
+
+ self._progress[containerName] = 0;
+ self.emit(containerName);
+
+ stream.on('data', function (str) {
+ console.log(str);
+ var data = JSON.parse(str);
+
+ if (data.status === 'Already exists') {
+ layerProgress[data.id] = 1;
+ } else if (data.status === 'Downloading') {
+ var current = data.progressDetail.current;
+ var total = data.progressDetail.total;
+ var layerFraction = current / total;
+ layerProgress[data.id] = layerFraction;
+ }
+
+ var chunks = layersToDownload.map(function (s) {
+ return layerProgress[s.Id] * s.size;
+ });
+
+ var totalReceived = chunks.reduce(function (pv, sv) {
+ return pv + sv;
+ });
+
+ var totalProgress = totalReceived / totalBytes;
+ self._progress[containerName] = totalProgress;
+ self.emit(self.PROGRESS);
+ });
+ stream.on('end', function () {
+ self._createContainer(imageName, containerName, function () {
+ delete self._progress[containerName];
+ });
+ });
});
});
});
@@ -210,19 +257,27 @@ var ContainerStore = assign(EventEmitter.prototype, {
});
}
});
- // If so then create a container w/ kitematic-only 'downloading state'
- // Pull image
- // When image is done pulling then
},
- setActive: function (containerName) {
- this._active = containerName;
- this.emit(self.ACTIVE);
+ setActive: function (name) {
+ console.log('set active');
+ this._active = name;
+ this.emit(this.ACTIVE);
},
active: function () {
return this._active;
},
- logs: function (containerName) {
- return logs[containerId];
+ // Returns all containers
+ containers: function() {
+ return this._containers;
+ },
+ container: function (name) {
+ return this._containers[name];
+ },
+ progress: function (name) {
+ return this._progress[name];
+ },
+ logs: function (name) {
+ return logs[name];
},
addChangeListener: function(eventType, callback) {
this.on(eventType, callback);
diff --git a/app/main.js b/app/main.js
index 05e9e31515..aa765b88f4 100644
--- a/app/main.js
+++ b/app/main.js
@@ -8,8 +8,8 @@ var boot2docker = require('./boot2docker.js');
var Setup = require('./Setup.react');
var Containers = require('./Containers.react');
var ContainerDetails = require('./ContainerDetails.react');
-var ContainerStore = require('./ContainerStore.js');
-var Radial = require('./Radial.react.js');
+var ContainerStore = require('./ContainerStore');
+var Radial = require('./Radial.react');
var Route = Router.Route;
var NotFoundRoute = Router.NotFoundRoute;
@@ -17,6 +17,14 @@ var DefaultRoute = Router.DefaultRoute;
var Link = Router.Link;
var RouteHandler = Router.RouteHandler;
+var App = React.createClass({
+ render: function () {
+ return (
+
+ );
+ }
+});
+
var NoContainers = React.createClass({
render: function () {
return (
@@ -27,14 +35,6 @@ var NoContainers = React.createClass({
}
});
-var App = React.createClass({
- render: function () {
- return (
-
- );
- }
-});
-
var routes = (
diff --git a/app/registry.js b/app/registry.js
new file mode 100644
index 0000000000..2fc0dcf0be
--- /dev/null
+++ b/app/registry.js
@@ -0,0 +1,84 @@
+var async = require('async');
+var $ = require('jquery');
+
+var Registry = {
+ token: function(repository, callback) {
+ $.ajax({
+ url: 'https://registry.hub.docker.com/v1/repositories/' + repository + '/images',
+ headers: {
+ 'X-Docker-Token': true,
+ },
+ success: function (res, status, xhr) {
+ callback(null, xhr.getResponseHeader('X-Docker-Token'));
+ },
+ error: function (err) {
+ callback(err);
+ }
+ });
+ },
+ ancestry: function (imageId, token, callback) {
+ $.ajax({
+ url: 'https://registry-1.docker.io/v1/images/' + imageId + '/ancestry',
+ headers: {
+ Authorization: 'Token ' + token
+ },
+ success: function (layers, status, xhr) {
+ callback(null, layers);
+ },
+ error: function (err) {
+ callback(err);
+ }
+ });
+ },
+ imageId: function (repository, tag, token, callback) {
+ $.ajax({
+ url: 'https://registry-1.docker.io/v1/repositories/' + repository + '/tags/' + tag,
+ headers: {
+ Authorization: 'Token ' + token
+ },
+ success: function (res, status, xhr) {
+ callback(null, res);
+ },
+ error: function (err) {
+ callback(err);
+ }
+ });
+ },
+
+ // Returns an array [{Id: <12 character image ID, size: size of layer in bytes}]
+ layers: function (repository, tag, callback) {
+ var self = this;
+ this.token(repository, function (err, token) {
+ self.imageId(repository, tag, token, function (err, imageId) {
+ self.ancestry(imageId, token, function (err, layers) {
+ async.map(layers, function (layer, callback) {
+ $.ajax({
+ url: 'https://registry-1.docker.io/v1/images/' + layer + '/json',
+ headers: {
+ Authorization: 'Token ' + token
+ },
+ success: function (res, status, xhr) {
+ var size = xhr.getResponseHeader('X-Docker-Size');
+ callback(null, {
+ Id: layer.slice(0, 12),
+ size: parseInt(size, 10)
+ });
+ },
+ error: function (err) {
+ callback(err);
+ }
+ });
+ }, function (err, results) {
+ if (err) {
+ callback('Could not sum' + err);
+ return;
+ }
+ callback(null, results);
+ });
+ });
+ });
+ });
+ }
+};
+
+module.exports = Registry;