Merge branch 'master' into fix-log-scroll

Conflicts:
	src/NewContainer.react.js
This commit is contained in:
Jeffrey Morgan 2015-02-16 17:49:25 -08:00
parent 75de3328a5
commit 1f368771f6
19 changed files with 266 additions and 101 deletions

View File

@ -1,4 +1,4 @@
[![Build Status](https://travis-ci.org/kitematic/kitematic.svg?branch=master)](https://travis-ci.org/kitematic/kitematic)
[![CircleCI](https://img.shields.io/circleci/project/kitematic/kitematic.svg)](https://circleci.com/gh/kitematic/kitematic/tree/master)
[![bitHound Score](https://app.bithound.io/kitematic/kitematic/badges/score.svg)](http://app.bithound.io/kitematic/kitematic)
![Kitematic Logo](https://cloud.githubusercontent.com/assets/251292/5269258/1b229c3c-7a2f-11e4-96f1-e7baf3c86d73.png)
@ -52,10 +52,6 @@ Keep track of development and community news.
- Follow [@kitematic on Twitter](https://twitter.com/kitematic).
- Read and subscribe to [The Kitematic Blog](http://blog.kitematic.com).
## Versioning
For transparency into our release cycle and in striving to maintain backward compatibility, Kitematic is maintained under the [Semantic Versioning Guidelines](http://semver.org/). We'll try very hard to adhere to those rules whenever possible.
## Copyright and License
Code released under the [AGPL license](LICENSE).

View File

@ -105,8 +105,6 @@ app.on('ready', function() {
autoUpdater.quitAndInstall();
}
});
autoUpdater.checkForUpdates();
}
ipc.on('vm', function (event, arg) {

BIN
images/loading-white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

BIN
images/loading-white@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -56,6 +56,7 @@
"exec": "0.1.2",
"jquery": "^2.1.3",
"minimist": "^1.1.0",
"node-uuid": "^1.4.2",
"object-assign": "^2.0.0",
"react": "^0.12.2",
"react-bootstrap": "^0.13.2",

View File

@ -33,15 +33,21 @@ var ContainerHomeFolder = React.createClass({
}
});
}
return (
<div className="folders wrapper">
<h4>Edit Files</h4>
<div className="widget">
{folders}
if (this.props.container && this.props.container.Volumes && _.keys(this.props.container.Volumes).length > 0 && this.props.container.State.Running) {
return (
<div className="folders wrapper">
<h4>Edit Files</h4>
<div className="widget">
{folders}
</div>
<div className="subtext" onClick={this.handleClickChangeFolders}>Change Folders</div>
</div>
<div className="subtext" onClick={this.handleClickChangeFolders}>Change Folders</div>
</div>
);
);
} else {
return (
<div></div>
);
}
}
});

View File

@ -1,4 +1,3 @@
var _ = require('underscore');
var $ = require('jquery');
var React = require('react/addons');
var Router = require('react-router');
@ -23,6 +22,10 @@ var ContainerListItem = React.createClass({
if (index === 0) {
ContainerStore.remove(this.props.container.Name, function (err) {
console.error(err);
var containers = ContainerStore.sorted();
if (containers.length === 1) {
$(document.body).find('.new-container-item').parent().fadeIn();
}
});
}
}.bind(this));

View File

@ -1,6 +1,7 @@
var $ = require('jquery');
var React = require('react/addons');
var Router = require('react-router');
var ContainerStore = require('./ContainerStore');
var ContainerListNewItem = React.createClass({
mixins: [Router.State, Router.Navigation],
@ -13,8 +14,14 @@ var ContainerListNewItem = React.createClass({
$action.hide();
},
handleDelete: function () {
$(this.getDOMNode()).fadeOut();
this.transitionTo('containers');
var self = this;
var containers = ContainerStore.sorted();
$(self.getDOMNode()).fadeOut(300, function () {
if (containers.length > 0) {
var name = containers[0].Name;
self.transitionTo('containerHome', {name: name});
}
});
},
render: function () {
var self = this;

View File

@ -330,10 +330,7 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
return;
} else {
container.remove(function (err) {
if (err) {
callback(err);
return;
}
callback(err);
});
}
});

View File

@ -4,6 +4,9 @@ var Router = require('react-router');
var ContainerStore = require('./ContainerStore');
var ContainerList = require('./ContainerList.react');
var Header = require('./Header.react');
var ipc = require('ipc');
var remote = require('remote');
var autoUpdater = remote.require('auto-updater');
var Containers = React.createClass({
mixins: [Router.Navigation, Router.State],
@ -11,7 +14,8 @@ var Containers = React.createClass({
return {
sidebarOffset: 0,
containers: ContainerStore.containers(),
sorted: ContainerStore.sorted()
sorted: ContainerStore.sorted(),
updateAvailable: false
};
},
componentDidMount: function () {
@ -22,6 +26,15 @@ var Containers = React.createClass({
if (this.state.sorted.length) {
this.transitionTo('containerHome', {name: this.state.sorted[0].Name});
}
autoUpdater.checkForUpdates();
ipc.on('notify', function (message) {
if (message === 'window:update-available') {
this.setState({
updateAvailable: true
});
}
});
},
componentDidUnmount: function () {
ContainerStore.removeListener(ContainerStore.SERVER_CONTAINER_EVENT, this.update);
@ -64,12 +77,25 @@ var Containers = React.createClass({
$(this.getDOMNode()).find('.new-container-item').parent().fadeIn();
this.transitionTo('new');
},
handleAutoUpdateClick: function () {
console.log('CLICKED UPDATE');
ipc.send('command', 'application:quit-install');
},
render: function () {
var sidebarHeaderClass = 'sidebar-header';
if (this.state.sidebarOffset) {
sidebarHeaderClass += ' sep';
}
var updateNotification;
var updatePadding;
if (this.state.updateAvailable) {
updateNotification = (
<div className="update-notification"><span className="text">Update Available</span><a className="btn btn-action small" onClick={this.handleAutoUpdateClick}>Update Now</a></div>
);
updatePadding = (
<div className="update-padding"></div>
);
}
var container = this.getParams().name ? this.state.containers[this.getParams().name] : {};
return (
<div className="containers">
@ -84,6 +110,8 @@ var Containers = React.createClass({
</section>
<section className="sidebar-containers" onScroll={this.handleScroll}>
<ContainerList containers={this.state.sorted} newContainer={this.state.newContainer} />
{updatePadding}
{updateNotification}
</section>
</div>
<Router.RouteHandler container={container}/>

113
src/ImageCard.react.js Normal file
View File

@ -0,0 +1,113 @@
var $ = require('jquery');
var React = require('react/addons');
var RetinaImage = require('react-retina-image');
var ContainerStore = require('./ContainerStore');
var ImageCard = React.createClass({
getInitialState: function () {
return {
tags: [],
chosenTag: 'latest'
};
},
handleTagClick: function (tag) {
this.setState({
chosenTag: tag
});
var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay');
$tagOverlay.fadeOut(300);
},
handleClick: function (name) {
ContainerStore.create(name, this.state.chosenTag, function (err) {
if (err) {
throw err;
}
$(document.body).find('.new-container-item').parent().fadeOut();
}.bind(this));
},
handleTagOverlayClick: function (name) {
var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay');
$tagOverlay.fadeIn(300);
$.get('https://registry.hub.docker.com/v1/repositories/' + name + '/tags', function (result) {
console.log(result);
this.setState({
tags: result
});
}.bind(this));
},
handleCloseTagOverlay: function () {
var $tagOverlay = $(this.getDOMNode()).find('.tag-overlay');
$tagOverlay.fadeOut(300);
},
render: function () {
var self = this;
var name;
if (this.props.image.is_official) {
name = <span><RetinaImage src="official.png"/>{this.props.image.name}</span>;
} else {
name = <span>{this.props.image.name}</span>;
}
var description;
if (this.props.image.description) {
description = this.props.image.description;
} else {
description = "No description.";
}
var logoStyle = {
backgroundImage: `linear-gradient(-180deg, ${this.props.image.gradient_start} 4%, ${this.props.image.gradient_end} 100%)`
};
var imgsrc;
if (this.props.image.img) {
imgsrc = `http://kitematic.com/recommended/${this.props.image.img}`;
} else {
imgsrc = 'http://kitematic.com/recommended/kitematic_html.png';
}
var tags;
if (this.state.tags.length > 0) {
var tagDisplay = this.state.tags.map(function (t) {
return <div className="tag" key={t.name} onClick={self.handleTagClick.bind(self, t.name)}>{t.name}</div>;
});
tags = (
<div className="tag-list">
{tagDisplay}
</div>
);
} else {
tags = <RetinaImage className="tags-loading" src="loading-white.png"/>;
}
return (
<div className="image-item">
<div className="tag-overlay" onClick={self.handleCloseTagOverlay}>
{tags}
</div>
<div className="logo" style={logoStyle}>
<RetinaImage src={imgsrc}/>
</div>
<div className="card">
<div className="name">
{name}
</div>
<div className="description">
{description}
</div>
<div className="actions">
<div className="stars">
<span className="icon icon-star-9"></span>
<span className="text">{this.props.image.star_count}</span>
</div>
<div className="tags">
<span className="icon icon-tag-1"></span>
<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>
</div>
</div>
</div>
</div>
);
}
});
module.exports = ImageCard;

View File

@ -9,8 +9,9 @@ var router = require('./router');
var boot2docker = require('./boot2docker');
var ContainerStore = require('./ContainerStore');
var SetupStore = require('./SetupStore');
var MenuTemplate = require('./MenuTemplate');
var Menu = remote.require('menu');
var settingsjson;
var Menu = require('./Menu');
try {
settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8'));
@ -33,6 +34,9 @@ bugsnag.releaseStage = process.env.NODE_ENV === 'development' ? 'development' :
bugsnag.notifyReleaseStages = ['production'];
bugsnag.appVersion = app.getVersion();
var menu = Menu.buildFromTemplate(MenuTemplate);
Menu.setApplicationMenu(menu);
router.run(Handler => React.render(<Handler/>, document.body));
SetupStore.run().then(boot2docker.ip).then(ip => {
console.log(ip);

View File

@ -2,13 +2,12 @@ var remote = require('remote');
var app = remote.require('app');
var path = require('path');
var docker = require('./Docker');
var Menu = remote.require('menu');
var BrowserWindow = remote.require('browser-window');
var router = require('./Router');
var util = require('./Util');
// main.js
var template = [
var MenuTemplate = [
{
label: 'Kitematic',
submenu: [
@ -160,7 +159,5 @@ var template = [
},
];
var menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
module.exports = menu;
module.exports = MenuTemplate;

View File

@ -2,10 +2,11 @@ var _ = require('underscore');
var $ = require('jquery');
var React = require('react/addons');
var RetinaImage = require('react-retina-image');
var ContainerStore = require('./ContainerStore');
var Radial = require('./Radial.react');
var assign = require('object-assign');
var ImageCard = require('./ImageCard.react');
var Promise = require('bluebird');
var assign = require('object-assign');
var ContainerStore = require('./ContainerStore');
var _recommended = [];
var _searchPromise = null;
@ -14,9 +15,9 @@ var NewContainer = React.createClass({
getInitialState: function () {
return {
query: '',
results: _recommended,
loading: false,
tags: {}
tags: {},
results: _recommended
};
},
componentDidMount: function () {
@ -107,68 +108,19 @@ var NewContainer = React.createClass({
$.get('https://registry.hub.docker.com/v1/repositories/' + name + '/tags', function (result) {
var res = {};
res[name] = result;
console.log(assign(this.state.tags, res));
this.setState({
tags: assign(this.state.tags, res)
});
}.bind(this));
},
render: function () {
var self = this;
var title = this.state.query ? 'Results' : 'Recommended';
var data = this.state.results;
var results;
if (data.length) {
var items = data.map(function (r) {
var name;
if (r.is_official) {
name = <span><RetinaImage src="official.png"/>{r.name}</span>;
} else {
name = <span>{r.name}</span>;
}
var description;
if (r.description) {
description = r.description;
} else {
description = "No description.";
}
var logoStyle = {
backgroundImage: `linear-gradient(-180deg, ${r.gradient_start} 4%, ${r.gradient_end} 100%)`
};
var imgsrc;
if (r.img) {
imgsrc = `http://kitematic.com/recommended/${r.img}`;
} else {
imgsrc = 'http://kitematic.com/recommended/kitematic_html.png';
}
var action = <a className="btn btn-action" onClick={self.handleClick.bind(self, r.name)}>Create</a>;
var items = data.map(function (image) {
return (
<div key={r.name} className="image-item">
<div className="logo" style={logoStyle}>
<RetinaImage src={imgsrc}/>
</div>
<div className="card">
<div className="name">
{name}
</div>
<div className="description">
{description}
</div>
<div className="actions">
<div className="stars">
<span className="icon icon-star-9"></span>
<span className="text">{r.star_count}</span>
</div>
<div className="tags">
<span className="icon icon-tag-1"></span>
<span className="text">latest</span>
</div>
<div className="action">
{action}
</div>
</div>
</div>
</div>
<ImageCard key={image.name} image={image} />
);
});

View File

@ -1,13 +0,0 @@
var React = require('react/addons');
var NoContainers = React.createClass({
render: function () {
return (
<div className="no-containers">
<h3>No Containers</h3>
</div>
);
}
});
module.exports = NoContainers;

View File

@ -43,7 +43,7 @@ var Setup = React.createClass({
},
renderContents: function () {
var img = 'virtualbox.png';
if (SetupStore.step().name.indexOf('start') !== -1 || SetupStore.step().name.indexOf('init') !== -1) {
if (SetupStore.step().name === 'init' || SetupStore.step().name === 'start') {
img = 'boot2docker.png';
}
return (
@ -107,7 +107,7 @@ var Setup = React.createClass({
<h4>Installation Error</h4>
<h1>We&#39;re Sorry!</h1>
<p>There seems to have been an unexpected error with Kitematic:</p>
<p className="error">{this.state.error.message}</p>
<p className="error">{this.state.error}<br />{this.state.error.message}</p>
</div>
</div>
</div>

View File

@ -183,6 +183,7 @@ var SetupStore = assign(Object.create(EventEmitter.prototype), {
} catch (err) {
if (err) {
console.log(err);
console.log(err.stack);
_error = err;
this.emit(this.ERROR_EVENT);
} else {

View File

@ -34,10 +34,54 @@
background-color: white;
margin-right: 20px;
margin-bottom: 20px;
.tag-overlay {
z-index: 999;
background-color: rgba(0,0,0,0.8);
border-radius: 4px;
width: 320px;
height: 166px;
position: absolute;
color: white;
font-size: 13px;
display: none;
padding: 10px;
.tag-list {
display: flex;
flex-direction: row;
align-items: flex-start;
align-content: flex-start;
flex-flow: row wrap;
height: 140px;
overflow: auto;
.tag {
display: inline-block;
flex: 0 auto;
margin-right: 2px;
padding: 3px 5px;
&:hover {
background-color: rgba(255,255,255,0.2);
border-radius: 20px;
}
}
}
.tags-loading {
position: relative;
left: 42%;
top: 20%;
text-align: center;
margin: 14px auto;
-webkit-animation-name: spin;
-webkit-animation-duration: 1.8s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-timing-function: linear;
}
}
.logo {
flex: 1 auto;
min-width: 90px;
background-color: @brand-action;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
justify-content: center;
text-align: center;
img {
@ -100,13 +144,20 @@
.icon {
position: relative;
font-size: 11px;
margin-right: 5px;
margin-right: 2px;
top: 2px;
color: @gray-darkest;
}
.text {
position: relative;
top: 0px;
padding: 3px 5px;
text-decoration: underline;
&:hover {
background-color: @brand-action;
color: white;
border-radius: 20px;
}
}
}
.action {

View File

@ -61,6 +61,30 @@ html, body {
border: 1px solid #ddd;
}
.update-notification {
background-color: white;
opacity: 0.9;
position: fixed;
bottom: 0;
width: 259px;
padding: 10px;
//border-top: 1px solid darken(#FCF8E3, 5%);
color: @gray-normal;
font-size: 12px;
.text {
position: relative;
top: 3px;
}
.btn {
position: relative;
float: right;
}
}
.update-padding {
position: relative;
height: 40px;
}
@-webkit-keyframes spin {
from {