diff --git a/README.md b/README.md
index d59d58a7be..91712f1bc0 100755
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[](https://travis-ci.org/kitematic/kitematic)
+[](https://circleci.com/gh/kitematic/kitematic/tree/master)
[](http://app.bithound.io/kitematic/kitematic)

@@ -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).
diff --git a/browser/main.js b/browser/main.js
index 04354c9407..b92ca41030 100644
--- a/browser/main.js
+++ b/browser/main.js
@@ -105,8 +105,6 @@ app.on('ready', function() {
autoUpdater.quitAndInstall();
}
});
-
- autoUpdater.checkForUpdates();
}
ipc.on('vm', function (event, arg) {
diff --git a/images/loading-white.png b/images/loading-white.png
new file mode 100644
index 0000000000..17096a362c
Binary files /dev/null and b/images/loading-white.png differ
diff --git a/images/loading-white@2x.png b/images/loading-white@2x.png
new file mode 100644
index 0000000000..75051f37e5
Binary files /dev/null and b/images/loading-white@2x.png differ
diff --git a/package.json b/package.json
index db0fce0964..65940d1797 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/ContainerHomeFolders.react.js b/src/ContainerHomeFolders.react.js
index 3f4d45f5ba..46b1bba7a4 100644
--- a/src/ContainerHomeFolders.react.js
+++ b/src/ContainerHomeFolders.react.js
@@ -33,15 +33,21 @@ var ContainerHomeFolder = React.createClass({
}
});
}
- return (
-
-
Edit Files
-
- {folders}
+ if (this.props.container && this.props.container.Volumes && _.keys(this.props.container.Volumes).length > 0 && this.props.container.State.Running) {
+ return (
+
+
Edit Files
+
+ {folders}
+
+
Change Folders
-
Change Folders
-
- );
+ );
+ } else {
+ return (
+
+ );
+ }
}
});
diff --git a/src/ContainerListItem.react.js b/src/ContainerListItem.react.js
index 30b7becf04..44b510615c 100644
--- a/src/ContainerListItem.react.js
+++ b/src/ContainerListItem.react.js
@@ -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));
diff --git a/src/ContainerListNewItem.react.js b/src/ContainerListNewItem.react.js
index 0fb41752de..9e88d2e514 100644
--- a/src/ContainerListNewItem.react.js
+++ b/src/ContainerListNewItem.react.js
@@ -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;
diff --git a/src/ContainerStore.js b/src/ContainerStore.js
index 34b60d7b0e..04a27b50eb 100644
--- a/src/ContainerStore.js
+++ b/src/ContainerStore.js
@@ -330,10 +330,7 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
return;
} else {
container.remove(function (err) {
- if (err) {
- callback(err);
- return;
- }
+ callback(err);
});
}
});
diff --git a/src/Containers.react.js b/src/Containers.react.js
index 489456641b..59c657cc47 100644
--- a/src/Containers.react.js
+++ b/src/Containers.react.js
@@ -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 = (
+
+ );
+ updatePadding = (
+
+ );
+ }
var container = this.getParams().name ? this.state.containers[this.getParams().name] : {};
return (
@@ -84,6 +110,8 @@ var Containers = React.createClass({
+ {updatePadding}
+ {updateNotification}
diff --git a/src/ImageCard.react.js b/src/ImageCard.react.js
new file mode 100644
index 0000000000..2c01f67017
--- /dev/null
+++ b/src/ImageCard.react.js
@@ -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 = {this.props.image.name};
+ } else {
+ name = {this.props.image.name};
+ }
+ 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 {t.name}
;
+ });
+ tags = (
+
+ {tagDisplay}
+
+ );
+ } else {
+ tags = ;
+ }
+ return (
+
+
+ {tags}
+
+
+
+
+
+
+ {name}
+
+
+ {description}
+
+
+
+
+ {this.props.image.star_count}
+
+
+
+ {this.state.chosenTag}
+
+
+
+
+
+ );
+ }
+});
+
+module.exports = ImageCard;
diff --git a/src/Main.js b/src/Main.js
index 1e490973b7..5cfd4c0e46 100644
--- a/src/Main.js
+++ b/src/Main.js
@@ -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(, document.body));
SetupStore.run().then(boot2docker.ip).then(ip => {
console.log(ip);
diff --git a/src/Menu.js b/src/MenuTemplate.js
similarity index 94%
rename from src/Menu.js
rename to src/MenuTemplate.js
index 2249087873..c5a0095663 100644
--- a/src/Menu.js
+++ b/src/MenuTemplate.js
@@ -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;
diff --git a/src/NewContainer.react.js b/src/NewContainer.react.js
index d0cd7a4908..f7f93fcf5d 100644
--- a/src/NewContainer.react.js
+++ b/src/NewContainer.react.js
@@ -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 = {r.name};
- } else {
- name = {r.name};
- }
- 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 = Create;
+ var items = data.map(function (image) {
return (
-
-
-
-
-
-
- {name}
-
-
- {description}
-
-
-
-
- {r.star_count}
-
-
-
- latest
-
-
- {action}
-
-
-
-
+
);
});
diff --git a/src/NoContainers.react.js b/src/NoContainers.react.js
deleted file mode 100644
index 2be1c9d0eb..0000000000
--- a/src/NoContainers.react.js
+++ /dev/null
@@ -1,13 +0,0 @@
-var React = require('react/addons');
-
-var NoContainers = React.createClass({
- render: function () {
- return (
-
-
No Containers
-
- );
- }
-});
-
-module.exports = NoContainers;
diff --git a/src/Setup.react.js b/src/Setup.react.js
index ea48750a43..2e19bc9677 100644
--- a/src/Setup.react.js
+++ b/src/Setup.react.js
@@ -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({
Installation Error
We're Sorry!
There seems to have been an unexpected error with Kitematic:
- {this.state.error.message}
+ {this.state.error}
{this.state.error.message}
diff --git a/src/SetupStore.js b/src/SetupStore.js
index 152d76dc36..e7f8dcbc6c 100644
--- a/src/SetupStore.js
+++ b/src/SetupStore.js
@@ -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 {
diff --git a/styles/containers.less b/styles/containers.less
index 750a29aef8..5674056e54 100644
--- a/styles/containers.less
+++ b/styles/containers.less
@@ -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 {
diff --git a/styles/main.less b/styles/main.less
index 55c669b146..3c793c6a60 100644
--- a/styles/main.less
+++ b/styles/main.less
@@ -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 {