Merge branch 'master' into container-dropdowns

Conflicts:
app/ContainerDetails.react.js
app/ContainerModal.react.js
app/styles/main.less
This commit is contained in:
Jeffrey Morgan 2015-01-27 17:05:11 -05:00
parent a671c11f2f
commit 475654e42c
16 changed files with 359 additions and 90 deletions

View File

@ -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 (
<div className="details">
<div className="details-header">
@ -379,37 +376,31 @@ var ContainerDetails = React.createClass({
<div className="details-header-actions">
<div className="action btn-group">
<a className={buttonClass} onClick={this.handleView}><span className="icon icon-preview-2"></span><span className="content">View</span></a>
<OverlayTrigger trigger="click" placement="bottom" overlay={
<Popover className="popover-view">
<div className="port-labels">
<div className="label-docker">DOCKER PORT</div>
<div className="label-local">LOCAL PORT</div>
</div>
<div className="ports">
{ports}
</div>
</Popover>
}>
<a className={dropdownButtonClass}><span className="icon-dropdown icon icon-arrow-37"></span></a>
</OverlayTrigger>
<Popover className={popoverViewClasses}>
<div className="port-labels">
<div className="label-docker">DOCKER PORT</div>
<div className="label-local">LOCAL PORT</div>
</div>
<div className="ports">
{ports}
</div>
</Popover>
<a className={dropdownButtonClass}><span className="icon-dropdown icon icon-arrow-37"></span></a>
</div>
<div className="action">
<OverlayTrigger trigger="click" placement="bottom" overlay={
<Popover className="popover-volume">
<div className="port-labels">
<div className="label-docker">DOCKER FOLDER</div>
<div className="label-local">LOCAL FOLDER</div>
</div>
<div className="ports">
{volumes}
</div>
</Popover>
}>
<a className={dropdownButtonClass}><span className="icon icon-folder-1"></span> <span className="content">Volumes</span> <span className="icon-dropdown icon icon-arrow-37"></span></a>
</OverlayTrigger>
<Popover className={popoverVolumeClasses}>
<div className="port-labels">
<div className="label-docker">DOCKER FOLDER</div>
<div className="label-local">LOCAL FOLDER</div>
</div>
<div className="ports">
{volumes}
</div>
</Popover>
<a className={dropdownButtonClass}><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) {
@ -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;

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,10 @@ app.on('ready', function() {
}
});
ipc.on('vm', function (event, 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",