Merge branch 'master' into hub

This commit is contained in:
Jeffrey Morgan 2015-05-25 14:44:43 -07:00
commit 2400b17c5a
16 changed files with 249 additions and 143 deletions

View File

@ -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),

View File

@ -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.

View File

@ -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({
</div>
);
} else if (this.props.container && this.props.container.State.Downloading) {
if (this.props.container.Progress !== undefined) {
if (this.props.container.Progress > 0) {
body = (
<div className="details-progress">
<h2>Downloading Image</h2>
<Radial progress={Math.min(Math.round(this.props.container.Progress), 99)} thick={true} gray={true}/>
</div>
);
} else {
body = (
<div className="details-progress">
<h2>Downloading Image</h2>
<Radial spin="true" progress="90" thick={true} transparent={true}/>
</div>
);
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(<h2>{Math.round(sum*100)/100}%</h2>);
fields.push(<ContainerProgress pBar1={values[0]} pBar2={values[1]} pBar3={values[2]} pBar4={values[3]} />);
body = (
<div className="details-progress">
<h2>Downloading Image</h2>
{fields}
</div>
);
} else if (this.props.container.State.Waiting) {
body = (
<div className="details-progress">

View File

@ -64,7 +64,7 @@ module.exports = React.createClass({
},
render: function () {
var logs = this.state.logs.map(function (l, i) {
return <span key={i} dangerouslySetInnerHTML={{__html: l}}></span>;
return <span key={i} dangerouslySetInnerHTML={{__html: l}}></span>;
});
if (logs.length === 0) {
logs = "No logs for this container.";

View File

@ -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 (
<ul>
<ContainerListNewItem key={'newcontainer'} containers={this.props.containers}/>
{containers}
</ul>
);

View File

@ -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 = (
<div className="action">
<span className="icon icon-delete-3 btn-delete" onClick={this.handleDelete}></span>
</div>
);
}
return (
<Router.Link to="search">
<li className="new-container-item" onMouseEnter={this.handleItemMouseEnter} onMouseLeave={this.handleItemMouseLeave}>
<div className="state state-new"></div>
<div className="info">
<div className="name">
New Container
</div>
</div>
{action}
</li>
</Router.Link>
);
}
});
module.exports = ContainerListNewItem;

View File

@ -0,0 +1,41 @@
var React = require('react');
/*
Usage: <ContainerProgress pBar1={20} pBar2={70} pBar3={100} pBar4={20} />
*/
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 (
<div className="container-progress">
<div className="bar-1 bar-bg">
<div className="bar-1 bar-fg" style={pBar4Style}></div>
</div>
<div className="bar-2 bar-bg">
<div className="bar-2 bar-fg" style={pBar3Style}></div>
</div>
<div className="bar-3 bar-bg">
<div className="bar-3 bar-fg" style={pBar2Style}></div>
</div>
<div className="bar-4 bar-bg">
<div className="bar-4 bar-fg" style={pBar1Style}></div>
</div>
</div>
);
}
});
module.exports = ContainerProgress;

View File

@ -161,7 +161,9 @@ var Containers = React.createClass({
<section className={sidebarHeaderClass}>
<h4>Containers</h4>
<div className="create">
<a className="btn-new icon icon-add-3" onClick={this.handleNewContainer}></a>
<Router.Link to="new">
<span className="btn-new icon icon-add-3"></span>
</Router.Link>
</div>
</section>
<section className="sidebar-containers" onScroll={this.handleScroll}>

View File

@ -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({
<span className="text" onClick={self.handleTagOverlayClick.bind(self, this.props.image.name)} data-name={this.props.image.name}>{this.state.chosenTag}</span>
</div>
<div className="action">
<a className="btn btn-action" onClick={self.handleClick.bind(self, this.props.image.name)}>Create</a>
<a className="btn btn-action" onClick={self.handleClick}>Create</a>
</div>
</div>
</div>

View File

@ -45,7 +45,7 @@ var routes = (
</Route>
</Route>
<Route name="new" path="containers/new">
<Route name="search" path="containers/new/search" handler={NewContainerSearch}></Route>
<DefaultRoute name="search" handler={NewContainerSearch}/>
<Route name="pull" path="containers/new/pull" handler={NewContainerPull}></Route>
</Route>
<Route name="preferences" path="/preferences" handler={Preferences}/>

View File

@ -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});
}

View File

@ -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();
});
});
},

View File

@ -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;
}
}

View File

@ -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%);
}
}
}
}

View File

@ -17,6 +17,7 @@
@import "container-settings.less";
@import "spinner.less";
@import "animation.less";
@import "container-progress.less";
html, body {
height: 100%;

View File

@ -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;
}