Merge pull request #166 from kitematic/jmorgan-preferences

Adding preferences pane
This commit is contained in:
Jeffrey Morgan 2015-01-27 16:13:47 -05:00
commit 29c96bc0d5
16 changed files with 315 additions and 39 deletions

View File

@ -12,12 +12,6 @@ var docker = require('./docker');
var boot2docker = require('./boot2docker'); var boot2docker = require('./boot2docker');
var ProgressBar = require('react-bootstrap/ProgressBar'); var ProgressBar = require('react-bootstrap/ProgressBar');
var Route = Router.Route;
var NotFoundRoute = Router.NotFoundRoute;
var DefaultRoute = Router.DefaultRoute;
var Link = Router.Link;
var RouteHandler = Router.RouteHandler;
var ContainerDetails = React.createClass({ var ContainerDetails = React.createClass({
mixins: [Router.State], mixins: [Router.State],
_oldHeight: 0, _oldHeight: 0,
@ -35,14 +29,13 @@ var ContainerDetails = React.createClass({
this.init(); this.init();
}, },
componentWillMount: function () { componentWillMount: function () {
this.init();
}, },
componentDidMount: function () { componentDidMount: function () {
this.init();
ContainerStore.on(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress); ContainerStore.on(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress);
ContainerStore.on(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs); ContainerStore.on(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs);
}, },
componentWillUnmount: function () { componentWillUnmount: function () {
// app close
ContainerStore.removeListener(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress); ContainerStore.removeListener(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress);
ContainerStore.removeListener(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs); ContainerStore.removeListener(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs);
}, },
@ -74,7 +67,6 @@ var ContainerDetails = React.createClass({
}); });
}, },
updateProgress: function (name) { updateProgress: function (name) {
console.log('progress', name, ContainerStore.progress(name));
if (name === this.getParams().name) { if (name === this.getParams().name) {
this.setState({ this.setState({
progress: ContainerStore.progress(name) progress: ContainerStore.progress(name)
@ -115,6 +107,11 @@ var ContainerDetails = React.createClass({
}); });
}); });
}, },
handleRestart: function () {
ContainerStore.restart(this.props.container.Name, function (err) {
console.log(err);
});
},
handleTerminal: function () { handleTerminal: function () {
var container = this.props.container; var container = this.props.container;
var terminal = path.join(process.cwd(), 'resources', 'terminal').replace(/ /g, '\\\\ '); var terminal = path.join(process.cwd(), 'resources', 'terminal').replace(/ /g, '\\\\ ');
@ -320,7 +317,7 @@ var ContainerDetails = React.createClass({
<a className={dropdownButtonClass} onClick={this.handleView}><span className="icon icon-folder-1"></span> <span className="content">Volumes</span> <span className="icon-dropdown icon icon-arrow-37"></span></a> <a className={dropdownButtonClass} onClick={this.handleView}><span className="icon icon-folder-1"></span> <span className="content">Volumes</span> <span className="icon-dropdown icon icon-arrow-37"></span></a>
</div> </div>
<div className="action"> <div className="action">
<a className={buttonClass} onClick={this.handleView}><span className="icon icon-refresh"></span> <span className="content">Restart</span></a> <a className={buttonClass} onClick={this.handleRestart}><span className="icon icon-refresh"></span> <span className="content">Restart</span></a>
</div> </div>
<div className="action"> <div className="action">
<a className={buttonClass} onClick={this.handleTerminal}><span className="icon icon-window-code-3"></span> <span className="content">Terminal</span></a> <a className={buttonClass} onClick={this.handleTerminal}><span className="icon icon-window-code-3"></span> <span className="content">Terminal</span></a>

View File

@ -26,7 +26,7 @@ var ContainerModal = React.createClass({
}, },
componentDidMount: function () { componentDidMount: function () {
this.refs.searchInput.getDOMNode().focus(); this.refs.searchInput.getDOMNode().focus();
ContainerStore.on(ContainerStore.SERVER_RECOMMENDED_EVENT, this.update); ContainerStore.on(ContainerStore.CLIENT_RECOMMENDED_EVENT, this.update);
}, },
update: function () { update: function () {
if (!this.state.query.length) { if (!this.state.query.length) {

View File

@ -19,9 +19,9 @@ var _muted = {};
var ContainerStore = assign(EventEmitter.prototype, { var ContainerStore = assign(EventEmitter.prototype, {
CLIENT_CONTAINER_EVENT: 'client_container', CLIENT_CONTAINER_EVENT: 'client_container',
CLIENT_RECOMMENDED_EVENT: 'client_recommended_event',
SERVER_CONTAINER_EVENT: 'server_container', SERVER_CONTAINER_EVENT: 'server_container',
SERVER_PROGRESS_EVENT: 'server_progress', SERVER_PROGRESS_EVENT: 'server_progress',
SERVER_RECOMMENDED_EVENT: 'server_recommended_event',
SERVER_LOGS_EVENT: 'server_logs', SERVER_LOGS_EVENT: 'server_logs',
_pullScratchImage: function (callback) { _pullScratchImage: function (callback) {
var image = docker.client().getImage('scratch:latest'); var image = docker.client().getImage('scratch:latest');
@ -110,7 +110,7 @@ var ContainerStore = assign(EventEmitter.prototype, {
_createContainer: function (name, containerData, callback) { _createContainer: function (name, containerData, callback) {
var existing = docker.client().getContainer(name); var existing = docker.client().getContainer(name);
var self = this; var self = this;
containerData.name = name; if (!containerData.name) containerData.name = containerData.Name;
if (containerData.Config && containerData.Config.Image) { if (containerData.Config && containerData.Config.Image) {
containerData.Image = containerData.Config.Image; containerData.Image = containerData.Config.Image;
} }
@ -232,12 +232,12 @@ var ContainerStore = assign(EventEmitter.prototype, {
this.fetchAllContainers(function (err) { this.fetchAllContainers(function (err) {
callback(); callback();
this.emit(this.CLIENT_CONTAINER_EVENT); this.emit(this.CLIENT_CONTAINER_EVENT);
this.fetchRecommended(function (err) {
this.emit(this.SERVER_RECOMMENDED_EVENT);
}.bind(this));
this._resumePulling(); this._resumePulling();
this._startListeningToEvents(); this._startListeningToEvents();
}.bind(this)); }.bind(this));
this.fetchRecommended(function (err) {
this.emit(this.CLIENT_RECOMMENDED_EVENT);
}.bind(this));
}, },
fetchContainer: function (id, callback) { fetchContainer: function (id, callback) {
docker.client().getContainer(id).inspect(function (err, container) { docker.client().getContainer(id).inspect(function (err, container) {
@ -403,21 +403,27 @@ var ContainerStore = assign(EventEmitter.prototype, {
_muted[name] = false; _muted[name] = false;
}.bind(this)); }.bind(this));
}, },
restart: function (name, callback) {
var container = docker.client().getContainer(name);
container.restart(function (err) {
callback(err);
});
},
remove: function (name, callback) { remove: function (name, callback) {
var self = this; var self = this;
var existing = docker.client().getContainer(name); var container = docker.client().getContainer(name);
if (_containers[name].State.Paused) { if (_containers[name].State.Paused) {
existing.unpause(function (err) { container.unpause(function (err) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} else { } else {
existing.kill(function (err) { container.kill(function (err) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} else { } else {
existing.remove(function (err) { container.remove(function (err) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
@ -428,12 +434,12 @@ var ContainerStore = assign(EventEmitter.prototype, {
} }
}); });
} else { } else {
existing.kill(function (err) { container.kill(function (err) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} else { } else {
existing.remove(function (err) { container.remove(function (err) {
if (err) { if (err) {
callback(err); callback(err);
return; return;

View File

@ -72,6 +72,7 @@ var Containers = React.createClass({
sidebarHeaderClass += ' sep'; sidebarHeaderClass += ' sep';
} }
var container = this.getParams().name ? this.state.containers[this.getParams().name] : {};
return ( return (
<div className="containers"> <div className="containers">
<Header/> <Header/>
@ -89,7 +90,7 @@ var Containers = React.createClass({
<ContainerList containers={this.state.sorted}/> <ContainerList containers={this.state.sorted}/>
</section> </section>
</div> </div>
<Router.RouteHandler container={this.state.containers[this.getParams().name]}/> <Router.RouteHandler container={container}/>
</div> </div>
</div> </div>
); );

142
app/Menu.js Normal file
View File

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

64
app/Preferences.react.js Normal file
View File

@ -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 (
<div className="preferences">
<div className="preferences-content">
<div className="title">VM Settings</div>
<div className="option">
<div className="option-name">
Save Linux VM state on closing Kitematic
</div>
<div className="option-value">
<input type="checkbox" checked={this.state.save_vm_on_quit} onChange={this.handleChange.bind(this, 'save_vm_on_quit')}/>
</div>
</div>
<div className="title">App Settings</div>
<div className="option">
<div className="option-name">
Report anonymous usage analytics
</div>
<div className="option-value">
<input type="checkbox" checked={this.state.report_analytics} onChange={this.handleChange.bind(this, 'report_analytics')}/>
</div>
</div>
</div>
</div>
);
}
});
module.exports = Preferences;

View File

@ -173,7 +173,9 @@ var Setup = React.createClass({
if (!err) { if (!err) {
boot2docker.ip(function (err, ip) { boot2docker.ip(function (err, ip) {
docker.setHost(ip); docker.setHost(ip);
self.transitionTo('containers'); ContainerStore.init(function () {
self.transitionTo('containers');
});
}); });
} }
}); });

View File

@ -7,6 +7,5 @@
</head> </head>
<body> <body>
<script src="main.js"></script> <script src="main.js"></script>
<script src="http://localhost:35729/livereload.js"></script>
</body> </body>
</html> </html>

View File

@ -10,7 +10,11 @@ var docker = require('./docker');
var router = require('./router'); var router = require('./router');
var boot2docker = require('./boot2docker'); var boot2docker = require('./boot2docker');
var ContainerStore = require('./ContainerStore'); 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 Route = Router.Route;
var NotFoundRoute = Router.NotFoundRoute; var NotFoundRoute = Router.NotFoundRoute;
@ -24,6 +28,14 @@ Bugsnag.releaseStage = process.env.NODE_ENV === 'development' ? 'development' :
Bugsnag.notifyReleaseStages = []; Bugsnag.notifyReleaseStages = [];
Bugsnag.appVersion = app.getVersion(); 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 === '#/') { if (!window.location.hash.length || window.location.hash === '#/') {
router.run(function (Handler) { router.run(function (Handler) {
React.render(<Handler/>, document.body); React.render(<Handler/>, document.body);

View File

@ -2,6 +2,7 @@ var React = require('react/addons');
var Setup = require('./Setup.react'); var Setup = require('./Setup.react');
var Containers = require('./Containers.react'); var Containers = require('./Containers.react');
var ContainerDetails = require('./ContainerDetails.react'); var ContainerDetails = require('./ContainerDetails.react');
var Preferences = require('./Preferences.react');
var NoContainers = require('./NoContainers.react'); var NoContainers = require('./NoContainers.react');
var Router = require('react-router'); var Router = require('react-router');
@ -20,13 +21,12 @@ var App = React.createClass({
var routes = ( var routes = (
<Route name="app" path="/" handler={App}> <Route name="app" path="/" handler={App}>
<Route name="containers" handler={Containers}> <Route name="containers" handler={Containers}>
<Route name="container" path=":name" handler={ContainerDetails}> <Route name="container" path="/containers/:name" handler={ContainerDetails}/>
</Route> <Route name="preferences" path="/preferences" handler={Preferences}/>
<DefaultRoute handler={NoContainers}/> <DefaultRoute handler={NoContainers}/>
</Route> </Route>
<Route name="setup" handler={Setup}></Route>
<DefaultRoute handler={Setup}/> <DefaultRoute handler={Setup}/>
<Route name="setup" handler={Setup}>
</Route>
</Route> </Route>
); );

View File

@ -6,6 +6,7 @@
@import "retina.less"; @import "retina.less";
@import "setup.less"; @import "setup.less";
@import "radial.less"; @import "radial.less";
@import "preferences.less";
@import "header.less"; @import "header.less";
@import "containers.less"; @import "containers.less";
@import "container-modal.less"; @import "container-modal.less";

View File

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

View File

@ -46,10 +46,10 @@ app.on('ready', function() {
process.on('uncaughtException', app.quit); process.on('uncaughtException', app.quit);
var saveVMOnQuit = true; var saveVMOnQuit = false;
app.on('will-quit', function (e) { app.on('will-quit', function (e) {
if (saveVMOnQuit) { 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,11 @@ app.on('ready', function() {
} }
}); });
ipc.on('vm', function (event, arg) {
console.log('save vm', arg);
saveVMOnQuit = arg;
});
autoUpdater.checkForUpdates(); autoUpdater.checkForUpdates();
}); });
}); });

View File

@ -31,7 +31,6 @@ var options = {
test: process.argv.indexOf('test') !== -1, test: process.argv.indexOf('test') !== -1,
filename: 'Kitematic.app', filename: 'Kitematic.app',
name: 'Kitematic' name: 'Kitematic'
//signing_identity: fs.readFileSync('./identity')
}; };
gulp.task('js', function () { gulp.task('js', function () {
@ -157,11 +156,16 @@ gulp.task('dist', function (cb) {
}); });
gulp.task('sign', function () { gulp.task('sign', function () {
return gulp.src('').pipe(shell([ try {
'codesign --deep --force --verbose --sign "' + options.signing_identity + '" ' + options.filename var signing_identity = fs.readFileSync('./identity', 'utf8').trim();
], { return gulp.src('').pipe(shell([
cwd: './dist/osx/' '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 () { gulp.task('zip', function () {

Binary file not shown.

View File

@ -14,7 +14,7 @@
"start": "gulp", "start": "gulp",
"preinstall": "./deps", "preinstall": "./deps",
"test": "gulp test", "test": "gulp test",
"release": ". ./script/identity && gulp release" "release": "gulp release"
}, },
"licenses": [ "licenses": [
{ {
@ -68,6 +68,7 @@
"gulp-uglifyjs": "^0.5.0", "gulp-uglifyjs": "^0.5.0",
"gulp-util": "^3.0.0", "gulp-util": "^3.0.0",
"jasmine-tagged": "^1.1.2", "jasmine-tagged": "^1.1.2",
"livereload-js": "^2.2.1",
"reactify": "^0.15.2", "reactify": "^0.15.2",
"run-sequence": "^1.0.2", "run-sequence": "^1.0.2",
"vinyl-source-stream": "^0.1.1", "vinyl-source-stream": "^0.1.1",