diff --git a/README.md b/README.md index d59d58a7be..91712f1bc0 100755 --- a/README.md +++ b/README.md @@ -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). 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 = ( +
Update AvailableUpdate Now
+ ); + 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} +
+
+ Create +
+
+
+
+ ); + } +}); + +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 {