diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b49261e426..cf4dbb202e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -18,8 +18,7 @@ Before you fil an issue or a pull request, quickly read of the following tips on
### Prerequisites
Most of the time, you'll have installed Kitematic before contibuting, but for the
-sake of completeness, you can also install [Node.js](https://nodejs.org/) and then
-run from your Git clone.
+sake of completeness, you can also install [Node.js](https://nodejs.org/) and the latest Xcode from the Apple App Store and then run from your Git clone.
Running `npm start` will download and install the OS X Docker client,
[Docker machine](https://github.com/docker/machine),
diff --git a/README.md b/README.md
index 65e8ecef80..ff1fcca754 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,12 @@ Kitematic is a simple application for managing Docker containers on Mac OS X and
Kitematic's documentation and other information can be found at [http://kitematic.com/docs](http://kitematic.com/docs).
+## Security Disclosure
+
+Security is very important to us. If you have any issue regarding security,
+please disclose the information responsibly by sending an email to
+security@docker.com and not by creating a github issue.
+
## Bugs and Feature Requests
Have a bug or a feature request? Please first read the [Issue Guidelines](https://github.com/kitematic/kitematic/blob/master/CONTRIBUTING.md#using-the-issue-tracker) and search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/kitematic/kitematic/issues/new).
@@ -45,3 +51,4 @@ rm -rf ~/Library/Application\ Support/Kitematic
## Copyright and License
Code released under the [Apache license](LICENSE).
+Images are copyrighted by Docker, Inc.
diff --git a/src/components/ContainerHome.react.js b/src/components/ContainerHome.react.js
index 08016322d1..645ae14faf 100644
--- a/src/components/ContainerHome.react.js
+++ b/src/components/ContainerHome.react.js
@@ -2,6 +2,7 @@ var _ = require('underscore');
var $ = require('jquery');
var React = require('react/addons');
var Radial = require('./Radial.react');
+var ContainerProgress = require('./ContainerProgress.react');
var ContainerHomePreview = require('./ContainerHomePreview.react');
var ContainerHomeLogs = require('./ContainerHomeLogs.react');
var ContainerHomeFolders = require('./ContainerHomeFolders.react');
@@ -50,23 +51,29 @@ var ContainerHome = React.createClass({
);
} else if (this.props.container && this.props.container.State.Downloading) {
- if (this.props.container.Progress !== undefined) {
- if (this.props.container.Progress > 0) {
- body = (
-
-
Downloading Image
-
-
- );
- } else {
- body = (
-
-
Downloading Image
-
-
- );
+ if (this.props.container.Progress) {
+
+ let fields = [];
+ let values = [];
+ let sum = 0.0;
+
+ for (let i = 0; i < this.props.container.Progress.amount; i++) {
+ values.push(Math.round(this.props.container.Progress.progress[i].value));
+ sum += this.props.container.Progress.progress[i].value;
}
+ sum = sum / this.props.container.Progress.amount;
+
+ fields.push({Math.round(sum*100)/100}%
);
+ fields.push();
+
+ body = (
+
+
Downloading Image
+ {fields}
+
+ );
+
} else if (this.props.container.State.Waiting) {
body = (
diff --git a/src/components/ContainerHomeLogs.react.js b/src/components/ContainerHomeLogs.react.js
index f8d7ac4570..d31a4df92e 100644
--- a/src/components/ContainerHomeLogs.react.js
+++ b/src/components/ContainerHomeLogs.react.js
@@ -64,7 +64,7 @@ module.exports = React.createClass({
},
render: function () {
var logs = this.state.logs.map(function (l, i) {
- return
;
+ return
;
});
if (logs.length === 0) {
logs = "No logs for this container.";
diff --git a/src/components/ContainerList.react.js b/src/components/ContainerList.react.js
index 7ff8504b3f..4a042b4c81 100644
--- a/src/components/ContainerList.react.js
+++ b/src/components/ContainerList.react.js
@@ -1,6 +1,5 @@
var React = require('react/addons');
var ContainerListItem = require('./ContainerListItem.react');
-var ContainerListNewItem = require('./ContainerListNewItem.react');
var ContainerList = React.createClass({
componentWillMount: function () {
@@ -14,7 +13,6 @@ var ContainerList = React.createClass({
});
return (
);
diff --git a/src/components/ContainerListNewItem.react.js b/src/components/ContainerListNewItem.react.js
deleted file mode 100644
index 55cebf975e..0000000000
--- a/src/components/ContainerListNewItem.react.js
+++ /dev/null
@@ -1,54 +0,0 @@
-var $ = require('jquery');
-var React = require('react');
-var Router = require('react-router');
-var metrics = require('../utils/MetricsUtil');
-
-var ContainerListNewItem = React.createClass({
- mixins: [Router.Navigation, Router.State],
- handleItemMouseEnter: function () {
- var $action = $(this.getDOMNode()).find('.action');
- $action.show();
- },
- handleItemMouseLeave: function () {
- var $action = $(this.getDOMNode()).find('.action');
- $action.hide();
- },
- handleDelete: function (event) {
- metrics.track('Deleted Container', {
- from: 'list',
- type: 'new'
- });
-
- if (this.props.containers.length > 0 && this.getRoutes()[this.getRoutes().length - 2].name === 'new') {
- var name = this.props.containers[0].Name;
- this.transitionTo('containerHome', {name});
- }
- $(this.getDOMNode()).fadeOut(300);
- event.preventDefault();
- },
- render: function () {
- var action;
- if (this.props.containers.length > 0) {
- action = (
-
-
-
- );
- }
- return (
-
-
-
-
- {action}
-
-
- );
- }
-});
-
-module.exports = ContainerListNewItem;
diff --git a/src/components/ContainerProgress.react.js b/src/components/ContainerProgress.react.js
new file mode 100644
index 0000000000..722f98b9d7
--- /dev/null
+++ b/src/components/ContainerProgress.react.js
@@ -0,0 +1,41 @@
+var React = require('react');
+
+/*
+
+ Usage:
+
+*/
+var ContainerProgress = React.createClass({
+ render: function () {
+ var pBar1Style = {
+ height: this.props.pBar1 + '%'
+ };
+ var pBar2Style = {
+ height: this.props.pBar2 + '%'
+ };
+ var pBar3Style = {
+ height: this.props.pBar3 + '%'
+ };
+ var pBar4Style = {
+ height: this.props.pBar4 + '%'
+ };
+ return (
+
+ );
+ }
+});
+
+module.exports = ContainerProgress;
diff --git a/src/components/Containers.react.js b/src/components/Containers.react.js
index 3862259c23..b7ef82dcb5 100644
--- a/src/components/Containers.react.js
+++ b/src/components/Containers.react.js
@@ -161,7 +161,9 @@ var Containers = React.createClass({
diff --git a/src/components/ImageCard.react.js b/src/components/ImageCard.react.js
index 129b8ac1e1..8d3868fba1 100644
--- a/src/components/ImageCard.react.js
+++ b/src/components/ImageCard.react.js
@@ -44,12 +44,13 @@ var ImageCard = React.createClass({
$tagOverlay.fadeOut(300);
metrics.track('Selected Image Tag');
},
- handleClick: function (repository) {
+ handleClick: function () {
metrics.track('Created Container', {
from: 'search'
});
- let name = containerStore.generateName(repository);
- containerActions.run(name, repository, this.state.chosenTag);
+ let name = containerStore.generateName(this.props.image.name);
+ let repo = this.props.image.namespace === 'library' ? this.props.image.name : this.props.image.namespace + '/' + this.props.image.name;
+ containerActions.run(name, repo, this.state.chosenTag);
this.transitionTo('containerHome', {name});
},
handleTagOverlayClick: function () {
@@ -164,7 +165,7 @@ var ImageCard = React.createClass({
{this.state.chosenTag}
diff --git a/src/routes.js b/src/routes.js
index 8fcd2e3c8e..d946cdbbda 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -45,7 +45,7 @@ var routes = (
-
+
diff --git a/src/stores/ContainerStore.js b/src/stores/ContainerStore.js
index d3b63099a0..304fec7131 100644
--- a/src/stores/ContainerStore.js
+++ b/src/stores/ContainerStore.js
@@ -97,11 +97,15 @@ class ContainerStore {
this.setState({containers});
}
+ // Receives the name of the container and columns of progression
+ // A column represents progression for one or more layers
progress ({name, progress}) {
let containers = this.containers;
+
if (containers[name]) {
containers[name].Progress = progress;
}
+
this.setState({containers});
}
diff --git a/src/utils/DockerUtil.js b/src/utils/DockerUtil.js
index 66a1499471..2f67e3ebf2 100644
--- a/src/utils/DockerUtil.js
+++ b/src/utils/DockerUtil.js
@@ -177,9 +177,16 @@ export default {
delete this.placeholders[name];
localStorage.setItem('placeholders', JSON.stringify(this.placeholders));
this.createContainer(name, {Image: imageName});
- }, progress => {
+ },
+
+ // progress is actually the progression PER LAYER (combined in columns)
+ // not total because it's not accurate enough
+ progress => {
containerServerActions.progress({name, progress});
- }, () => {
+ },
+
+
+ () => {
containerServerActions.waiting({name, waiting: true});
});
},
@@ -309,7 +316,7 @@ export default {
stream.setEncoding('utf8');
stream.on('data', json => {
let data = JSON.parse(json);
- console.log(data);
+ // console.log(data);
if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete') {
return;
@@ -327,66 +334,106 @@ export default {
},
pullImage (auth, repository, tag, callback, progressCallback, blockedCallback) {
- // 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.
- this.client.listImages({all: 1}, (err, images) => {
- images = images || [];
+ this.client.pull(repository + ':' + tag, (err, stream) => {
+ if (err) {
+ callback(err);
+ return;
+ }
- this.client.pull(repository + ':' + tag, {authconfig: {key: auth}}, (err, stream) => {
- if (err) {
- callback(err);
+ stream.setEncoding('utf8');
+
+ // scheduled to inform about progression at given interval
+ let tick = null;
+ let layerProgress = {};
+
+ // Split the loading in a few columns for more feedback
+ let columns = {};
+ columns.amount = 4; // arbitrary
+ columns.toFill = 0; // the current column index, waiting for layer IDs to be displayed
+
+ // data is associated with one layer only (can be identified with id)
+ stream.on('data', str => {
+ var data = JSON.parse(str);
+
+ if (data.error) {
return;
}
- stream.setEncoding('utf8');
- let timeout = null;
- let layerProgress = {};
- stream.on('data', str => {
- var data = JSON.parse(str);
+ if (data.status && (data.status === 'Pulling dependent layers' || data.status.indexOf('already being pulled by another client') !== -1)) {
+ blockedCallback();
+ return;
+ }
- if (data.error) {
- return;
- }
+ if (data.status === 'Pulling fs layer') {
+ layerProgress[data.id] = {
+ current: 0,
+ total: 1
+ };
+ } else if (data.status === 'Downloading') {
+ if (!columns.progress) {
+ columns.progress = []; // layerIDs, nbLayers, maxLayers, progress value
+ let layersToLoad = _.keys(layerProgress).length;
- if (data.status && (data.status === 'Pulling dependent layers' || data.status.indexOf('already being pulled by another client') !== -1)) {
- blockedCallback();
- return;
- }
+ console.log(_.values(layerProgress));
- if (!layerProgress[data.id]) {
- layerProgress[data.id] = 0;
- }
+ console.log('layersToLoad: ', layersToLoad);
- if (data.status === 'Already exists') {
- layerProgress[data.id] = 1;
- } else if (data.status === 'Downloading') {
- let current = data.progressDetail.current;
- let total = data.progressDetail.total;
-
- if (total <= 0) {
- progressCallback(0);
- return;
- } else {
- layerProgress[data.id] = current / total;
- }
-
- let sum = _.values(layerProgress).reduce((pv, sv) => pv + sv, 0);
- let numlayers = _.keys(layerProgress).length;
-
- var totalProgress = sum / numlayers * 100;
-
- if (!timeout) {
- progressCallback(totalProgress);
- timeout = setTimeout(() => {
- timeout = null;
- }, 100);
+ for (let i = 0; i < columns.amount; i++) {
+ let layerAmount = Math.ceil(layersToLoad / (columns.amount - i));
+ console.log(i, layerAmount);
+ layersToLoad -= layerAmount;
+ columns.progress[i] = {layerIDs:[], nbLayers:0, maxLayers:layerAmount, value:0.0};
}
}
- });
- stream.on('end', function () {
- callback();
- });
+
+ layerProgress[data.id].current = data.progressDetail.current;
+ layerProgress[data.id].total = data.progressDetail.total;
+
+ // Assign to a column if not done yet
+ if (!layerProgress[data.id].column) {
+ // test if we can still add layers to that column
+ if (columns.progress[columns.toFill].nbLayers === columns.progress[columns.toFill].maxLayers) {
+ columns.toFill++;
+ }
+
+ layerProgress[data.id].column = columns.toFill;
+ columns.progress[columns.toFill].layerIDs.push(data.id);
+ columns.progress[columns.toFill].nbLayers++;
+ }
+
+ if (!tick) {
+ tick = setTimeout(() => {
+ clearInterval(tick);
+ tick = null;
+ for (let i = 0; i < columns.amount; i++) {
+ columns.progress[i].value = 0.0;
+
+ // Start only if the column has accurate values for all layers
+ if (columns.progress[i].nbLayers === columns.progress[i].maxLayers) {
+ let layer;
+ let totalSum = 0;
+ let currentSum = 0;
+
+ for (let j = 0; j < columns.progress[i].nbLayers; j++) {
+ layer = layerProgress[columns.progress[i].layerIDs[j]];
+ totalSum += layer.total;
+ currentSum += layer.current;
+ }
+
+ if (totalSum > 0) {
+ columns.progress[i].value = 100.0 * currentSum / totalSum;
+ } else {
+ columns.progress[i].value = 0.0;
+ }
+ }
+ }
+ progressCallback(columns);
+ }, 16);
+ }
+ }
+ });
+ stream.on('end', function () {
+ callback();
});
});
},
diff --git a/styles/container-progress.less b/styles/container-progress.less
new file mode 100644
index 0000000000..a8bf96613a
--- /dev/null
+++ b/styles/container-progress.less
@@ -0,0 +1,35 @@
+.container-progress {
+ width: 100px;
+ height: 100px;
+ border: 4px solid @brand-primary;
+ border-radius: 10px;
+ transform: rotate(180deg);
+ .bar-bg {
+ display: inline-block;
+ position: relative;
+ top: 22px;
+ background-color: @gray-lightest;
+ width: 4px;
+ height: 50px;
+ border-radius: 10px;
+ }
+ .bar-fg {
+ background-color: @brand-primary;
+ width: 4px;
+ height: 0px;
+ border-radius: 10px;
+ transition: 0.3 all;
+ }
+ .bar-1 {
+ left: 21px;
+ }
+ .bar-2 {
+ left: 32px;
+ }
+ .bar-3 {
+ left: 43px;
+ }
+ .bar-4 {
+ left: 54px;
+ }
+}
diff --git a/styles/left-panel.less b/styles/left-panel.less
index 81f1fafacf..cd0f3b17ab 100644
--- a/styles/left-panel.less
+++ b/styles/left-panel.less
@@ -29,16 +29,31 @@
position: relative;
}
.create {
+ display: flex;
flex: 1 auto;
- text-align: right;
+ justify-content: flex-end;
margin-right: 20px;
margin-top: 3px;
- .btn-new {
- font-size: 24px;
- color: @brand-action;
- transition: all 0.25s;
- &:hover {
- color: darken(@brand-action, 15%);
+ a {
+ display: block;
+ text-decoration: none;
+ cursor: default;
+ &.active {
+ .btn-new {
+ opacity: 0.3;
+ &:hover {
+ color: @brand-action;
+ }
+ }
+ }
+ .btn-new {
+ display: block;
+ font-size: 24px;
+ color: @brand-action;
+ transition: all 0.25s;
+ &:hover {
+ color: darken(@brand-action, 15%);
+ }
}
}
}
diff --git a/styles/main.less b/styles/main.less
index 9ec9503bcb..2494e833aa 100644
--- a/styles/main.less
+++ b/styles/main.less
@@ -17,6 +17,7 @@
@import "container-settings.less";
@import "spinner.less";
@import "animation.less";
+@import "container-progress.less";
html, body {
height: 100%;
diff --git a/styles/right-panel.less b/styles/right-panel.less
index faee2d737a..4a7ff4cc8e 100644
--- a/styles/right-panel.less
+++ b/styles/right-panel.less
@@ -122,9 +122,12 @@
}
.details-progress {
- margin: 20% auto 0;
- text-align: center;
- width: 400px;
+ flex: 1 auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ margin-top: -70px;
h2 {
margin-bottom: 20px;
}