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 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({
mixins: [Router.State],
_oldHeight: 0,
@ -35,14 +29,13 @@ var ContainerDetails = React.createClass({
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);
},
componentWillUnmount: function () {
// app close
ContainerStore.removeListener(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress);
ContainerStore.removeListener(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs);
},
@ -74,7 +67,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)
@ -115,6 +107,11 @@ var ContainerDetails = React.createClass({
});
});
},
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, '\\\\ ');
@ -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>
</div>
<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 className="action">
<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 () {
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) {

View File

@ -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) {
@ -403,21 +403,27 @@ var ContainerStore = assign(EventEmitter.prototype, {
_muted[name] = false;
}.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;
@ -428,12 +434,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;

View File

@ -72,6 +72,7 @@ var Containers = React.createClass({
sidebarHeaderClass += ' sep';
}
var container = this.getParams().name ? this.state.containers[this.getParams().name] : {};
return (
<div className="containers">
<Header/>
@ -89,7 +90,7 @@ var Containers = React.createClass({
<ContainerList containers={this.state.sorted}/>
</section>
</div>
<Router.RouteHandler container={this.state.containers[this.getParams().name]}/>
<Router.RouteHandler container={container}/>
</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) {
boot2docker.ip(function (err, ip) {
docker.setHost(ip);
self.transitionTo('containers');
ContainerStore.init(function () {
self.transitionTo('containers');
});
});
}
});

View File

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

View File

@ -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(<Handler/>, document.body);

View File

@ -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 = (
<Route name="app" path="/" handler={App}>
<Route name="containers" handler={Containers}>
<Route name="container" path=":name" handler={ContainerDetails}>
</Route>
<Route name="container" path="/containers/:name" handler={ContainerDetails}/>
<Route name="preferences" path="/preferences" handler={Preferences}/>
<DefaultRoute handler={NoContainers}/>
</Route>
<Route name="setup" handler={Setup}></Route>
<DefaultRoute handler={Setup}/>
<Route name="setup" handler={Setup}>
</Route>
</Route>
);

View File

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

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);
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,11 @@ app.on('ready', function() {
}
});
ipc.on('vm', function (event, arg) {
console.log('save vm', arg);
saveVMOnQuit = arg;
});
autoUpdater.checkForUpdates();
});
});

View File

@ -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 () {

Binary file not shown.

View File

@ -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",