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 ( - -
  • -
    -
    -
    - New Container -
    -
    - {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({

    Containers

    - + + +
    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}
    - Create + Create
    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; }