diff --git a/app/ContainerDetails.react.js b/app/ContainerDetails.react.js index 7b14765f3b..c86b716609 100644 --- a/app/ContainerDetails.react.js +++ b/app/ContainerDetails.react.js @@ -14,12 +14,6 @@ var ProgressBar = require('react-bootstrap/ProgressBar'); var Popover = require('react-bootstrap/Popover'); var OverlayTrigger = require('react-bootstrap/OverlayTrigger'); -var Route = Router.Route; -var NotFoundRoute = Router.NotFoundRoute; -var DefaultRoute = Router.DefaultRoute; -var Link = Router.Link; -var RouteHandler = Router.RouteHandler; - var ContainerDetails = React.createClass({ mixins: [Router.State], _oldHeight: 0, @@ -32,21 +26,34 @@ var ContainerDetails = React.createClass({ env: {}, pendingEnv: {}, ports: {}, - volumes: {} + volumes: {}, + popoverVolumeOpen: false, + popoverPortsOpen: false, }; }, componentWillReceiveProps: function () { this.init(); }, componentWillMount: function () { - this.init(); }, componentDidMount: function () { + this.init(); ContainerStore.on(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress); ContainerStore.on(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs); + + // Make clicking anywhere close popovers + $('body').on('click', function (e) { + console.log(e.target); + console.log($('.popover-volume')); + var volumeOpen = $('.popover-volume').is(e.target); + var viewOpen = $('.popover-view').is(e.target); + this.setState({ + popoverViewOpen: viewOpen, + popoverVolumeOpen: volumeOpen + }); + }.bind(this)); }, componentWillUnmount: function () { - // app close ContainerStore.removeListener(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress); ContainerStore.removeListener(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs); }, @@ -84,7 +91,6 @@ var ContainerDetails = React.createClass({ }); }, updateProgress: function (name) { - console.log('progress', name, ContainerStore.progress(name)); if (name === this.getParams().name) { this.setState({ progress: ContainerStore.progress(name) @@ -101,35 +107,16 @@ var ContainerDetails = React.createClass({ page: this.PAGE_SETTINGS }); }, - handleView: function () { - var container = this.props.container; - boot2docker.ip(function (err, ip) { - var ports = _.map(container.NetworkSettings.Ports, function (value, key) { - var portProtocolPair = key.split('/'); - var res = { - 'port': portProtocolPair[0], - 'protocol': portProtocolPair[1] - }; - if (value && value.length) { - var port = value[0].HostPort; - res.host = ip; - res.port = port; - res.url = 'http://' + ip + ':' + port; - } else { - return null; - } - return res; - }); - exec(['open', ports[0].url], function (err) { - if (err) { throw err; } - }); - }); - }, handleViewLink: function (url) { exec(['open', url], function (err) { if (err) { throw err; } }); }, + handleRestart: function () { + ContainerStore.restart(this.props.container.Name, function (err) { + console.log(err); + }); + }, handleTerminal: function () { var container = this.props.container; var terminal = path.join(process.cwd(), 'resources', 'terminal').replace(/ /g, '\\\\ '); @@ -370,6 +357,16 @@ var ContainerDetails = React.createClass({ ); }); + var popoverVolumeClasses = React.addons.classSet({ + 'popover-volume': true, + hidden: !this.state.popoverVolumeOpen + }); + + var popoverViewClasses = React.addons.classSet({ + 'popover-volume': true, + hidden: !this.state.popoverViewOpen + }); + return (
@@ -379,37 +376,31 @@ var ContainerDetails = React.createClass({
View - -
-
DOCKER PORT
-
LOCAL PORT
-
-
- {ports} -
- - }> - -
+ +
+
DOCKER PORT
+
LOCAL PORT
+
+
+ {ports} +
+
+
- -
-
DOCKER FOLDER
-
LOCAL FOLDER
-
-
- {volumes} -
- - }> - Volumes -
+ +
+
DOCKER FOLDER
+
LOCAL FOLDER
+
+
+ {volumes} +
+
+ Volumes
- Restart + Restart
Terminal diff --git a/app/ContainerModal.react.js b/app/ContainerModal.react.js index 74098a8320..629e3c699b 100644 --- a/app/ContainerModal.react.js +++ b/app/ContainerModal.react.js @@ -26,7 +26,7 @@ var ContainerModal = React.createClass({ }, componentDidMount: function () { this.refs.searchInput.getDOMNode().focus(); - ContainerStore.on(ContainerStore.SERVER_RECOMMENDED_EVENT, this.update); + ContainerStore.on(ContainerStore.CLIENT_RECOMMENDED_EVENT, this.update); }, update: function () { if (!this.state.query.length) { diff --git a/app/ContainerStore.js b/app/ContainerStore.js index f3896c7894..95966bd70f 100644 --- a/app/ContainerStore.js +++ b/app/ContainerStore.js @@ -19,9 +19,9 @@ var _muted = {}; var ContainerStore = assign(EventEmitter.prototype, { CLIENT_CONTAINER_EVENT: 'client_container', + CLIENT_RECOMMENDED_EVENT: 'client_recommended_event', SERVER_CONTAINER_EVENT: 'server_container', SERVER_PROGRESS_EVENT: 'server_progress', - SERVER_RECOMMENDED_EVENT: 'server_recommended_event', SERVER_LOGS_EVENT: 'server_logs', _pullScratchImage: function (callback) { var image = docker.client().getImage('scratch:latest'); @@ -110,7 +110,7 @@ var ContainerStore = assign(EventEmitter.prototype, { _createContainer: function (name, containerData, callback) { var existing = docker.client().getContainer(name); var self = this; - containerData.name = name; + if (!containerData.name) containerData.name = containerData.Name; if (containerData.Config && containerData.Config.Image) { containerData.Image = containerData.Config.Image; } @@ -232,12 +232,12 @@ var ContainerStore = assign(EventEmitter.prototype, { this.fetchAllContainers(function (err) { callback(); this.emit(this.CLIENT_CONTAINER_EVENT); - this.fetchRecommended(function (err) { - this.emit(this.SERVER_RECOMMENDED_EVENT); - }.bind(this)); this._resumePulling(); this._startListeningToEvents(); }.bind(this)); + this.fetchRecommended(function (err) { + this.emit(this.CLIENT_RECOMMENDED_EVENT); + }.bind(this)); }, fetchContainer: function (id, callback) { docker.client().getContainer(id).inspect(function (err, container) { @@ -407,21 +407,27 @@ var ContainerStore = assign(EventEmitter.prototype, { callback(err); }.bind(this)); }, + restart: function (name, callback) { + var container = docker.client().getContainer(name); + container.restart(function (err) { + callback(err); + }); + }, remove: function (name, callback) { var self = this; - var existing = docker.client().getContainer(name); + var container = docker.client().getContainer(name); if (_containers[name].State.Paused) { - existing.unpause(function (err) { + container.unpause(function (err) { if (err) { callback(err); return; } else { - existing.kill(function (err) { + container.kill(function (err) { if (err) { callback(err); return; } else { - existing.remove(function (err) { + container.remove(function (err) { if (err) { callback(err); return; @@ -432,12 +438,12 @@ var ContainerStore = assign(EventEmitter.prototype, { } }); } else { - existing.kill(function (err) { + container.kill(function (err) { if (err) { callback(err); return; } else { - existing.remove(function (err) { + container.remove(function (err) { if (err) { callback(err); return; diff --git a/app/Containers.react.js b/app/Containers.react.js index 45789f8db0..0372475515 100644 --- a/app/Containers.react.js +++ b/app/Containers.react.js @@ -72,6 +72,7 @@ var Containers = React.createClass({ sidebarHeaderClass += ' sep'; } + var container = this.getParams().name ? this.state.containers[this.getParams().name] : {}; return (
@@ -89,7 +90,7 @@ var Containers = React.createClass({
- +
); diff --git a/app/Menu.js b/app/Menu.js new file mode 100644 index 0000000000..68bcd9cf2a --- /dev/null +++ b/app/Menu.js @@ -0,0 +1,142 @@ +var remote = require('remote'); +var app = remote.require('app'); +var Menu = remote.require('menu'); +var MenuItem = remote.require('menu-item'); +var BrowserWindow = remote.require('browser-window'); +var router = require('./router'); + +// main.js +var template = [ +{ + label: 'Kitematic', + submenu: [ + { + label: 'About Kitematic', + selector: 'orderFrontStandardAboutPanel:' + }, + { + type: 'separator' + }, + { + label: 'Preferences', + accelerator: 'Command+,', + click: function () { + router.transitionTo('preferences'); + } + }, + { + type: 'separator' + }, + { + label: 'Services', + submenu: [] + }, + { + type: 'separator' + }, + { + label: 'Hide Kitematic', + accelerator: 'Command+H', + selector: 'hide:' + }, + { + label: 'Hide Others', + accelerator: 'Command+Shift+H', + selector: 'hideOtherApplications:' + }, + { + label: 'Show All', + selector: 'unhideAllApplications:' + }, + { + type: 'separator' + }, + { + label: 'Quit', + accelerator: 'Command+Q', + click: function() { + app.quit(); + } + }, + ] +}, +{ + label: 'Edit', + submenu: [ + { + label: 'Undo', + accelerator: 'Command+Z', + selector: 'undo:' + }, + { + label: 'Redo', + accelerator: 'Shift+Command+Z', + selector: 'redo:' + }, + { + type: 'separator' + }, + { + label: 'Cut', + accelerator: 'Command+X', + selector: 'cut:' + }, + { + label: 'Copy', + accelerator: 'Command+C', + selector: 'copy:' + }, + { + label: 'Paste', + accelerator: 'Command+V', + selector: 'paste:' + }, + { + label: 'Select All', + accelerator: 'Command+A', + selector: 'selectAll:' + }, + ] +}, +{ + label: 'View', + submenu: [ + { + label: 'Toggle DevTools', + accelerator: 'Alt+Command+I', + click: function() { BrowserWindow.getFocusedWindow().toggleDevTools(); } + }, + ] +}, +{ + label: 'Window', + submenu: [ + { + label: 'Minimize', + accelerator: 'Command+M', + selector: 'performMiniaturize:' + }, + { + label: 'Close', + accelerator: 'Command+W', + selector: 'performClose:' + }, + { + type: 'separator' + }, + { + label: 'Bring All to Front', + selector: 'arrangeInFront:' + }, + ] +}, +{ + label: 'Help', + submenu: [] +}, +]; + +menu = Menu.buildFromTemplate(template); +Menu.setApplicationMenu(menu); + +module.exports = menu; diff --git a/app/Preferences.react.js b/app/Preferences.react.js new file mode 100644 index 0000000000..c18ddc782c --- /dev/null +++ b/app/Preferences.react.js @@ -0,0 +1,64 @@ +var React = require('react/addons'); +var assign = require('object-assign'); +var ipc = require('ipc'); + +// TODO: move this somewhere else +if (localStorage.getItem('options')) { + ipc.send('vm', JSON.parse(localStorage.getItem('options')).save_vm_on_quit); +} + +var Preferences = React.createClass({ + getInitialState: function () { + var data = JSON.parse(localStorage.getItem('options')); + return assign({ + save_vm_on_quit: true, + report_analytics: true + }, data || {}); + }, + handleChange: function (key, e) { + var change = {}; + change[key] = !this.state[key]; + console.log(change); + this.setState(change); + }, + saveState: function () { + ipc.send('vm', this.state.save_vm_on_quit); + localStorage.setItem('options', JSON.stringify(this.state)); + }, + componentDidMount: function () { + this.saveState(); + }, + componentDidUpdate: function () { + this.saveState(); + }, + render: function () { + console.log('render'); + return ( +
+
+
VM Settings
+
+
+ Save Linux VM state on closing Kitematic +
+
+ +
+
+
App Settings
+
+
+ Report anonymous usage analytics +
+
+ +
+
+ +
+
+ ); + } +}); + +module.exports = Preferences; diff --git a/app/Setup.react.js b/app/Setup.react.js index 16f8928b31..08561310e5 100644 --- a/app/Setup.react.js +++ b/app/Setup.react.js @@ -173,7 +173,9 @@ var Setup = React.createClass({ if (!err) { boot2docker.ip(function (err, ip) { docker.setHost(ip); - self.transitionTo('containers'); + ContainerStore.init(function () { + self.transitionTo('containers'); + }); }); } }); diff --git a/app/index.html b/app/index.html index a97ba8b788..699c7186f0 100644 --- a/app/index.html +++ b/app/index.html @@ -7,6 +7,5 @@ - diff --git a/app/main.js b/app/main.js index 52ec623ddc..5970a0a0b1 100644 --- a/app/main.js +++ b/app/main.js @@ -10,7 +10,11 @@ var docker = require('./docker'); var router = require('./router'); var boot2docker = require('./boot2docker'); var ContainerStore = require('./ContainerStore'); -var app = require('remote').require('app'); +var Menu = require('./Menu'); +var remote = require('remote'); +var app = remote.require('app'); +var ipc = require('ipc'); + var Route = Router.Route; var NotFoundRoute = Router.NotFoundRoute; @@ -24,6 +28,14 @@ Bugsnag.releaseStage = process.env.NODE_ENV === 'development' ? 'development' : Bugsnag.notifyReleaseStages = []; Bugsnag.appVersion = app.getVersion(); +if (process.env.NODE_ENV === 'development') { + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = 'http://localhost:35729/livereload.js'; + var head = document.getElementsByTagName('head')[0]; + head.appendChild(script); +} + if (!window.location.hash.length || window.location.hash === '#/') { router.run(function (Handler) { React.render(, document.body); diff --git a/app/routes.js b/app/routes.js index 88dcf91bcf..92b9df7d3a 100644 --- a/app/routes.js +++ b/app/routes.js @@ -2,6 +2,7 @@ var React = require('react/addons'); var Setup = require('./Setup.react'); var Containers = require('./Containers.react'); var ContainerDetails = require('./ContainerDetails.react'); +var Preferences = require('./Preferences.react'); var NoContainers = require('./NoContainers.react'); var Router = require('react-router'); @@ -20,13 +21,12 @@ var App = React.createClass({ var routes = ( - - + + + - - ); diff --git a/app/styles/main.less b/app/styles/main.less index f93e70af99..e4a5514222 100644 --- a/app/styles/main.less +++ b/app/styles/main.less @@ -6,6 +6,7 @@ @import "retina.less"; @import "setup.less"; @import "radial.less"; +@import "preferences.less"; @import "header.less"; @import "containers.less"; @import "container-modal.less"; diff --git a/app/styles/preferences.less b/app/styles/preferences.less new file mode 100644 index 0000000000..bc30b0494f --- /dev/null +++ b/app/styles/preferences.less @@ -0,0 +1,42 @@ +@import "variables.less"; + +.preferences { + flex: 1 auto; + display: flex; + align-items: flex-start; + justify-content: center; + + + .preferences-content { + flex: 1 auto; + margin-top: 20px; + padding: 50px; + max-width: 640px; + display: flex; + flex-direction: column; + + .title { + margin-top: 40px; + border-bottom: 1px solid #eee; + text-align: left; + font-size: 18px; + font-weight: 400; + color: @gray-darker; + } + + .option { + display: flex; + flex-direction: row; + margin-top: 14px; + + .option-name { + flex: 0 auto; + color: @gray-light; + } + .option-value { + flex: 1 auto; + text-align: right; + } + } + } +} diff --git a/browser/main.js b/browser/main.js index 92627b2b7b..89672d9e58 100644 --- a/browser/main.js +++ b/browser/main.js @@ -46,10 +46,10 @@ app.on('ready', function() { process.on('uncaughtException', app.quit); - var saveVMOnQuit = true; + var saveVMOnQuit = false; app.on('will-quit', function (e) { if (saveVMOnQuit) { - // exec('VBoxManage controlvm boot2docker-vm savestate', function (stderr, stdout, code) {}); + exec('VBoxManage controlvm boot2docker-vm savestate', function (stderr, stdout, code) {}); } }); @@ -97,6 +97,10 @@ app.on('ready', function() { } }); + ipc.on('vm', function (event, arg) { + saveVMOnQuit = arg; + }); + autoUpdater.checkForUpdates(); }); }); diff --git a/gulpfile.js b/gulpfile.js index 0f7dc7748c..7468cada7b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -31,7 +31,6 @@ var options = { test: process.argv.indexOf('test') !== -1, filename: 'Kitematic.app', name: 'Kitematic' - //signing_identity: fs.readFileSync('./identity') }; gulp.task('js', function () { @@ -157,11 +156,16 @@ gulp.task('dist', function (cb) { }); gulp.task('sign', function () { - return gulp.src('').pipe(shell([ - 'codesign --deep --force --verbose --sign "' + options.signing_identity + '" ' + options.filename - ], { - cwd: './dist/osx/' - })); + try { + var signing_identity = fs.readFileSync('./identity', 'utf8').trim(); + return gulp.src('').pipe(shell([ + 'codesign --deep --force --verbose --sign "' + signing_identity + '" ' + options.filename + ], { + cwd: './dist/osx/' + })); + } catch (error) { + gutil.log(gutil.colors.red('Error: ' + error.message)); + } }); gulp.task('zip', function () { diff --git a/kitematic.icns b/kitematic.icns index efc8397618..e8aea4b99f 100644 Binary files a/kitematic.icns and b/kitematic.icns differ diff --git a/package.json b/package.json index 757d5c0888..e11677079d 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "start": "gulp", "preinstall": "./deps", "test": "gulp test", - "release": ". ./script/identity && gulp release" + "release": "gulp release" }, "licenses": [ { @@ -68,6 +68,7 @@ "gulp-uglifyjs": "^0.5.0", "gulp-util": "^3.0.0", "jasmine-tagged": "^1.1.2", + "livereload-js": "^2.2.1", "reactify": "^0.15.2", "run-sequence": "^1.0.2", "vinyl-source-stream": "^0.1.1",