Merge pull request #163 from kitematic/jmorgan-container-merge

Update Existing containers
This commit is contained in:
Sean Li 2015-01-27 09:24:50 -08:00
commit ca69727d42
31 changed files with 421 additions and 97 deletions

View File

@ -2,9 +2,13 @@ var _ = require('underscore');
var $ = require('jquery'); var $ = require('jquery');
var React = require('react/addons'); var React = require('react/addons');
var Router = require('react-router'); var Router = require('react-router');
var ContainerStore = require('./ContainerStore');
var docker = require('./docker');
var exec = require('exec'); var exec = require('exec');
var path = require('path');
var remote = require('remote');
var dialog = remote.require('dialog');
var ContainerStore = require('./ContainerStore');
var ContainerUtil = require('./ContainerUtil');
var docker = require('./docker');
var boot2docker = require('./boot2docker'); var boot2docker = require('./boot2docker');
var ProgressBar = require('react-bootstrap/ProgressBar'); var ProgressBar = require('react-bootstrap/ProgressBar');
@ -22,22 +26,23 @@ var ContainerDetails = React.createClass({
getInitialState: function () { getInitialState: function () {
return { return {
logs: [], logs: [],
page: this.PAGE_LOGS page: this.PAGE_LOGS,
env: {},
pendingEnv: {}
}; };
}, },
componentWillReceiveProps: function () { componentWillReceiveProps: function () {
this.setState({ this.init();
page: this.PAGE_LOGS },
}); componentWillMount: function () {
ContainerStore.fetchLogs(this.getParams().name, function () { this.init();
this.updateLogs();
}.bind(this));
}, },
componentDidMount: function () { componentDidMount: function () {
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);
}, },
@ -52,6 +57,14 @@ var ContainerDetails = React.createClass({
} }
this._oldHeight = parent[0].scrollHeight - parent.height(); this._oldHeight = parent[0].scrollHeight - parent.height();
}, },
init: function () {
this.setState({
env: ContainerUtil.env(ContainerStore.container(this.getParams().name))
});
ContainerStore.fetchLogs(this.getParams().name, function () {
this.updateLogs();
}.bind(this));
},
updateLogs: function (name) { updateLogs: function (name) {
if (name && name !== this.getParams().name) { if (name && name !== this.getParams().name) {
return; return;
@ -78,7 +91,7 @@ var ContainerDetails = React.createClass({
page: this.PAGE_SETTINGS page: this.PAGE_SETTINGS
}); });
}, },
handleClick: function (name) { handleView: function () {
var container = this.props.container; var container = this.props.container;
boot2docker.ip(function (err, ip) { boot2docker.ip(function (err, ip) {
var ports = _.map(container.NetworkSettings.Ports, function (value, key) { var ports = _.map(container.NetworkSettings.Ports, function (value, key) {
@ -102,6 +115,66 @@ var ContainerDetails = React.createClass({
}); });
}); });
}, },
handleTerminal: function () {
var container = this.props.container;
var terminal = path.join(process.cwd(), 'resources', 'terminal').replace(/ /g, '\\\\ ');
var cmd = [terminal, boot2docker.command().replace(/ /g, '\\\\ '), 'ssh', '-t', 'sudo', 'docker', 'exec', '-i', '-t', container.Name, 'bash'];
exec(cmd, function (stderr, stdout, code) {
if (code) {
console.log(stderr);
}
});
},
handleSaveEnvVar: function () {
var $rows = $('.env-vars .keyval-row');
var envVarList = [];
$rows.each(function () {
var key = $(this).find('.key').val();
var val = $(this).find('.val').val();
if (!key.length || !val.length) {
return;
}
envVarList.push(key + '=' + val);
});
ContainerStore.updateContainer(this.props.container.Name, {
Env: envVarList
});
},
handleAddPendingEnvVar: function () {
var newKey = $('#new-env-key').val();
var newVal = $('#new-env-val').val();
var newEnv = {};
newEnv[newKey] = newVal;
this.setState({
pendingEnv: _.extend(this.state.pendingEnv, newEnv)
});
$('#new-env-key').val('');
$('#new-env-val').val('');
},
handleRemoveEnvVar: function (key) {
var newEnv = _.omit(this.state.env, key);
this.setState({
env: newEnv
});
},
handleRemovePendingEnvVar: function (key) {
var newEnv = _.omit(this.state.pendingEnv, key);
this.setState({
pendingEnv: newEnv
});
},
handleDeleteContainer: function () {
dialog.showMessageBox({
message: 'Are you sure you want to delete this container?',
buttons: ['Delete', 'Cancel']
}, function (index) {
if (index === 0) {
ContainerStore.remove(this.props.container.Name, function (err) {
console.error(err);
});
}
}.bind(this));
},
render: function () { render: function () {
var self = this; var self = this;
@ -135,6 +208,25 @@ var ContainerDetails = React.createClass({
button = <a className="btn btn-primary disabled" onClick={this.handleClick}>View</a>; button = <a className="btn btn-primary disabled" onClick={this.handleClick}>View</a>;
} }
var envVars = _.map(this.state.env, function (val, key) {
return (
<div key={key} className="keyval-row">
<input type="text" className="key line" defaultValue={key}></input>
<input type="text" className="val line" defaultValue={val}></input>
<a onClick={self.handleRemoveEnvVar.bind(self, key)} className="only-icon btn btn-action small"><span className="icon icon-cross"></span></a>
</div>
);
});
var pendingEnvVars = _.map(this.state.pendingEnv, function (val, key) {
return (
<div key={key} className="keyval-row">
<input type="text" className="key line" defaultValue={key}></input>
<input type="text" className="val line" defaultValue={val}></input>
<a onClick={self.handleRemovePendingEnvVar.bind(self, key)} className="only-icon btn btn-action small"><span className="icon icon-arrow-undo"></span></a>
</div>
);
});
var body; var body;
if (this.props.container.State.Downloading) { if (this.props.container.State.Downloading) {
body = ( body = (
@ -145,7 +237,7 @@ var ContainerDetails = React.createClass({
} else { } else {
if (this.state.page === this.PAGE_LOGS) { if (this.state.page === this.PAGE_LOGS) {
body = ( body = (
<div className="details-logs"> <div className="details-panel details-logs">
<div className="logs"> <div className="logs">
{logs} {logs}
</div> </div>
@ -153,16 +245,33 @@ var ContainerDetails = React.createClass({
); );
} else { } else {
body = ( body = (
<div className="details-logs"> <div className="details-panel">
<div className="settings"> <div className="settings">
<h3>Container Name</h3>
<input id="input-container-name" type="text" className="line" placeholder="Container Name" defaultValue={this.props.container.Name}></input>
<h3>Environment Variables</h3>
<div className="env-vars-labels">
<div className="label-key">KEY</div>
<div className="label-val">VALUE</div>
</div>
<div className="env-vars">
{envVars}
{pendingEnvVars}
<div className="keyval-row">
<input id="new-env-key" type="text" className="key line"></input>
<input id="new-env-val" type="text" className="val line"></input>
<a onClick={this.handleAddPendingEnvVar} className="only-icon btn btn-positive small"><span className="icon icon-add-1"></span></a>
</div>
</div>
<a className="btn btn-action" onClick={this.handleSaveEnvVar}>Save</a>
<h3>Delete Container</h3>
<a className="btn btn-action" onClick={this.handleDeleteContainer}>Delete Container</a>
</div> </div>
</div> </div>
); );
} }
} }
var name = this.props.container.Name;
var image = this.props.container.Config.Image;
var disabledClass = ''; var disabledClass = '';
if (!this.props.container.State.Running) { if (!this.props.container.State.Running) {
disabledClass = 'disabled'; disabledClass = 'disabled';
@ -186,7 +295,7 @@ var ContainerDetails = React.createClass({
'btn-action': true, 'btn-action': true,
'only-icon': true, 'only-icon': true,
'active': this.state.page === this.PAGE_LOGS, 'active': this.state.page === this.PAGE_LOGS,
disabled: !this.props.container.State.Running disabled: this.props.container.State.Downloading
}); });
var gearButtonClass = React.addons.classSet({ var gearButtonClass = React.addons.classSet({
@ -194,27 +303,27 @@ var ContainerDetails = React.createClass({
'btn-action': true, 'btn-action': true,
'only-icon': true, 'only-icon': true,
'active': this.state.page === this.PAGE_SETTINGS, 'active': this.state.page === this.PAGE_SETTINGS,
disabled: !this.props.container.State.Running disabled: this.props.container.State.Downloading
}); });
return ( return (
<div className="details"> <div className="details">
<div className="details-header"> <div className="details-header">
<div className="details-header-info"> <div className="details-header-info">
<h1>{name}</h1>{state}<h2 className="image-label">Image</h2><h2 className="image">{image}</h2> <h1>{this.props.container.Name}</h1>{state}<h2 className="image-label">Image</h2><h2 className="image">{this.props.container.Config.Image}</h2>
</div> </div>
<div className="details-header-actions"> <div className="details-header-actions">
<div className="action btn-group"> <div className="action btn-group">
<a className={buttonClass} onClick={this.handleClick}><span className="icon icon-preview-2"></span><span className="content">View</span></a><a className={dropdownButtonClass}><span className="icon-dropdown icon icon-arrow-37"></span></a> <a className={buttonClass} onClick={this.handleView}><span className="icon icon-preview-2"></span><span className="content">View</span></a><a className={dropdownButtonClass}><span className="icon-dropdown icon icon-arrow-37"></span></a>
</div> </div>
<div className="action"> <div className="action">
<a className={dropdownButtonClass} onClick={this.handleClick}><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.handleClick}><span className="icon icon-refresh"></span> <span className="content">Restart</span></a> <a className={buttonClass} onClick={this.handleView}><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.handleClick}><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>
</div> </div>
<div className="details-header-actions-rhs tabs btn-group"> <div className="details-header-actions-rhs tabs btn-group">
<a className={textButtonClasses} onClick={this.showLogs}><span className="icon icon-text-wrapping-2"></span></a> <a className={textButtonClasses} onClick={this.showLogs}><span className="icon icon-text-wrapping-2"></span></a>

View File

View File

@ -10,6 +10,8 @@ var MenuItem = require('react-bootstrap/MenuItem');
var RetinaImage = require('react-retina-image'); var RetinaImage = require('react-retina-image');
var ContainerStore = require('./ContainerStore'); var ContainerStore = require('./ContainerStore');
var OverlayTrigger = require('react-bootstrap/OverlayTrigger');
var Popover = require('react-bootstrap/Popover');
var ContainerModal = React.createClass({ var ContainerModal = React.createClass({
_searchRequest: null, _searchRequest: null,
@ -152,8 +154,7 @@ var ContainerModal = React.createClass({
<div className="btn-group"> <div className="btn-group">
<button type="button" className="btn btn-primary" onClick={self.handleClick.bind(self, r.name)}>Create</button> <button type="button" className="btn btn-primary" onClick={self.handleClick.bind(self, r.name)}>Create</button>
<button type="button" className="btn btn-primary dropdown-toggle" onClick={self.handleDropdownClick.bind(self, r.name)} data-name={r.name}> <button type="button" className="btn btn-primary dropdown-toggle" onClick={self.handleDropdownClick.bind(self, r.name)} data-name={r.name}>
<span className="caret"></span> <span className="icon-dropdown icon icon-arrow-37"></span>
<span className="sr-only">Toggle Dropdown</span>
</button> </button>
</div> </div>
</div> </div>

View File

@ -3,16 +3,19 @@ var async = require('async');
var assign = require('object-assign'); var assign = require('object-assign');
var Stream = require('stream'); var Stream = require('stream');
var Convert = require('ansi-to-html'); var Convert = require('ansi-to-html');
var convert = new Convert();
var docker = require('./docker'); var docker = require('./docker');
var registry = require('./registry'); var registry = require('./registry');
var ContainerUtil = require('./ContainerUtil');
var $ = require('jquery'); var $ = require('jquery');
var _ = require('underscore'); var _ = require('underscore');
var convert = new Convert();
var _recommended = []; var _recommended = [];
var _containers = {}; var _containers = {};
var _progress = {}; var _progress = {};
var _logs = {}; var _logs = {};
var _muted = {};
var ContainerStore = assign(EventEmitter.prototype, { var ContainerStore = assign(EventEmitter.prototype, {
CLIENT_CONTAINER_EVENT: 'client_container', CLIENT_CONTAINER_EVENT: 'client_container',
@ -104,28 +107,33 @@ var ContainerStore = assign(EventEmitter.prototype, {
div.appendChild(text); div.appendChild(text);
return div.innerHTML; return div.innerHTML;
}, },
_createContainer: function (image, name, callback) { _createContainer: function (name, containerData, callback) {
var existing = docker.client().getContainer(name); var existing = docker.client().getContainer(name);
var self = this; var self = this;
existing.remove(function (err, data) { containerData.name = name;
docker.client().createContainer({ if (containerData.Config && containerData.Config.Image) {
Image: image, containerData.Image = containerData.Config.Image;
Tty: false, }
name: name, existing.kill(function (err, data) {
User: 'root' existing.remove(function (err, data) {
}, function (err, container) { docker.client().createContainer(containerData, function (err, container) {
if (err) {
callback(err, null);
return;
}
container.start({
PublishAllPorts: true
}, function (err) {
if (err) { if (err) {
callback(err); callback(err, null);
return; return;
} }
self.fetchContainer(name, callback); if (containerData.State && !containerData.State.Running) {
self.fetchContainer(name, callback);
} else {
container.start({
PublishAllPorts: true
}, function (err) {
if (err) {
callback(err);
return;
}
self.fetchContainer(name, callback);
});
}
}); });
}); });
}); });
@ -181,7 +189,7 @@ var ContainerStore = assign(EventEmitter.prototype, {
stream.setEncoding('utf8'); stream.setEncoding('utf8');
stream.on('data', function (data) {}); stream.on('data', function (data) {});
stream.on('end', function () { stream.on('end', function () {
self._createContainer(container.KitematicDownloadingImage, container.Name, function () {}); self._createContainer(container.Name, {Image: container.KitematicDownloadingImage}, function () {});
}); });
}); });
}); });
@ -201,11 +209,20 @@ var ContainerStore = assign(EventEmitter.prototype, {
// If the event is delete, remove the container // If the event is delete, remove the container
if (data.status === 'destroy') { if (data.status === 'destroy') {
var container = _.findWhere(_.values(_containers), {Id: data.id}); var container = _.findWhere(_.values(_containers), {Id: data.id});
if (!container || _muted[container.Name]) {
return;
}
delete _containers[container.Name]; delete _containers[container.Name];
this.emit(this.SERVER_CONTAINER_EVENT, container.Name, data.status); this.emit(this.SERVER_CONTAINER_EVENT, container.Name, data.status);
} else { } else {
this.fetchContainer(data.id, function (err) { this.fetchContainer(data.id, function (err) {
if (err) {
return;
}
var container = _.findWhere(_.values(_containers), {Id: data.id}); var container = _.findWhere(_.values(_containers), {Id: data.id});
if (!container || _muted[container.Name]) {
return;
}
this.emit(this.SERVER_CONTAINER_EVENT, container ? container.Name : null, data.status); this.emit(this.SERVER_CONTAINER_EVENT, container ? container.Name : null, data.status);
}.bind(this)); }.bind(this));
} }
@ -227,11 +244,15 @@ var ContainerStore = assign(EventEmitter.prototype, {
if (err) { if (err) {
callback(err); callback(err);
} else { } else {
if (container.Config.Image === container.Image.slice(0, 12) || container.Config.Image === container.Image) {
callback();
return;
}
// Fix leading slash in container names // Fix leading slash in container names
container.Name = container.Name.replace('/', ''); container.Name = container.Name.replace('/', '');
// Add Downloading State (stored in environment variables) to containers for Kitematic // Add Downloading State (stored in environment variables) to containers for Kitematic
var env = _.object(container.Config.Env.map(function (e) { return e.split('='); })); var env = ContainerUtil.env(container);
container.State.Downloading = !!env.KITEMATIC_DOWNLOADING; container.State.Downloading = !!env.KITEMATIC_DOWNLOADING;
container.KitematicDownloadingImage = env.KITEMATIC_DOWNLOADING_IMAGE; container.KitematicDownloadingImage = env.KITEMATIC_DOWNLOADING_IMAGE;
@ -269,7 +290,6 @@ var ContainerStore = assign(EventEmitter.prototype, {
async.map(recommended, function (repository, callback) { async.map(recommended, function (repository, callback) {
$.get('https://registry.hub.docker.com/v1/search?q=' + repository, function (data) { $.get('https://registry.hub.docker.com/v1/search?q=' + repository, function (data) {
var results = data.results; var results = data.results;
console.log(repository, data);
callback(null, _.find(results, function (r) { callback(null, _.find(results, function (r) {
return r.name === repository; return r.name === repository;
})); }));
@ -287,8 +307,9 @@ var ContainerStore = assign(EventEmitter.prototype, {
fetchLogs: function (name, callback) { fetchLogs: function (name, callback) {
if (_logs[name]) { if (_logs[name]) {
callback(); callback();
} else {
_logs[name] = [];
} }
_logs[name] = [];
var index = 0; var index = 0;
var self = this; var self = this;
docker.client().getContainer(name).logs({ docker.client().getContainer(name).logs({
@ -297,6 +318,9 @@ var ContainerStore = assign(EventEmitter.prototype, {
stderr: true, stderr: true,
timestamps: true timestamps: true
}, function (err, stream) { }, function (err, stream) {
if (err) {
return;
}
stream.setEncoding('utf8'); stream.setEncoding('utf8');
stream.on('data', function (buf) { stream.on('data', function (buf) {
// Every other message is a header // Every other message is a header
@ -304,11 +328,11 @@ var ContainerStore = assign(EventEmitter.prototype, {
var time = buf.substr(0,buf.indexOf(' ')); var time = buf.substr(0,buf.indexOf(' '));
var msg = buf.substr(buf.indexOf(' ')+1); var msg = buf.substr(buf.indexOf(' ')+1);
_logs[name].push(convert.toHtml(self._escapeHTML(msg))); _logs[name].push(convert.toHtml(self._escapeHTML(msg)));
self.emit(self.SERVER_LOGS_EVENT, name);
} }
index += 1; index += 1;
}); });
stream.on('end', function (buf) { stream.on('end', function (buf) {
self.emit(self.SERVER_LOGS_EVENT, name);
callback(); callback();
docker.client().getContainer(name).logs({ docker.client().getContainer(name).logs({
follow: true, follow: true,
@ -317,6 +341,9 @@ var ContainerStore = assign(EventEmitter.prototype, {
timestamps: true, timestamps: true,
tail: 0 tail: 0
}, function (err, stream) { }, function (err, stream) {
if (err) {
return;
}
stream.setEncoding('utf8'); stream.setEncoding('utf8');
stream.on('data', function (buf) { stream.on('data', function (buf) {
// Every other message is a header // Every other message is a header
@ -345,10 +372,13 @@ var ContainerStore = assign(EventEmitter.prototype, {
self._createPlaceholderContainer(imageName, containerName, function (err, container) { self._createPlaceholderContainer(imageName, containerName, function (err, container) {
_containers[containerName] = container; _containers[containerName] = container;
self.emit(self.CLIENT_CONTAINER_EVENT, containerName, 'create'); self.emit(self.CLIENT_CONTAINER_EVENT, containerName, 'create');
_muted[containerName] = true;
_progress[containerName] = 0; _progress[containerName] = 0;
self._pullImage(repository, tag, function () { self._pullImage(repository, tag, function () {
self._createContainer(imageName, containerName, function (err, container) { self._createContainer(containerName, {Image: imageName}, function (err, container) {
delete _progress[containerName]; delete _progress[containerName];
_muted[containerName] = false;
self.emit(self.CLIENT_CONTAINER_EVENT, containerName);
}); });
}, function (progress) { }, function (progress) {
_progress[containerName] = progress; _progress[containerName] = progress;
@ -358,13 +388,61 @@ var ContainerStore = assign(EventEmitter.prototype, {
}); });
} else { } else {
// If not then directly create the container // If not then directly create the container
self._createContainer(imageName, containerName, function (err, container) { self._createContainer(containerName, {Image: imageName}, function (err, container) {
self.emit(ContainerStore.CLIENT_CONTAINER_EVENT, containerName, 'create'); self.emit(ContainerStore.CLIENT_CONTAINER_EVENT, containerName, 'create');
callback(null, containerName); callback(null, containerName);
}); });
} }
}); });
}, },
updateContainer: function (name, data) {
_muted[name] = true;
var fullData = assign(_containers[name], data);
this._createContainer(name, fullData, function (err) {
this.emit(this.CLIENT_CONTAINER_EVENT, name);
_muted[name] = false;
}.bind(this));
},
remove: function (name, callback) {
var self = this;
var existing = docker.client().getContainer(name);
if (_containers[name].State.Paused) {
existing.unpause(function (err) {
if (err) {
callback(err);
return;
} else {
existing.kill(function (err) {
if (err) {
callback(err);
return;
} else {
existing.remove(function (err) {
if (err) {
callback(err);
return;
}
});
}
});
}
});
} else {
existing.kill(function (err) {
if (err) {
callback(err);
return;
} else {
existing.remove(function (err) {
if (err) {
callback(err);
return;
}
});
}
});
}
},
containers: function() { containers: function() {
return _containers; return _containers;
}, },
@ -373,16 +451,7 @@ var ContainerStore = assign(EventEmitter.prototype, {
}, },
sorted: function () { sorted: function () {
return _.values(_containers).sort(function (a, b) { return _.values(_containers).sort(function (a, b) {
var active = function (container) { return a.Name.localeCompare(b.Name);
return container.State.Running || container.State.Restarting || container.State.Downloading;
};
if (active(a) && !active(b)) {
return -1;
} else if (!active(a) && active(b)) {
return 1;
} else {
return a.Name.localeCompare(b.Name);
}
}); });
}, },
recommended: function () { recommended: function () {

16
app/ContainerUtil.js Normal file
View File

@ -0,0 +1,16 @@
var _ = require('underscore');
var ContainerUtil = {
env: function (container) {
if (!container || !container.Config || !container.Config.Env) {
return {};
}
return _.object(container.Config.Env.map(function (env) {
var i = env.indexOf('=');
var splits = [env.slice(0, i), env.slice(i + 1)];
return splits;
}));
}
};
module.exports = ContainerUtil;

View File

@ -52,7 +52,6 @@ var Containers = React.createClass({
sorted: ContainerStore.sorted() sorted: ContainerStore.sorted()
}); });
if (status === 'create') { if (status === 'create') {
console.log('transition');
this.transitionTo('container', {name: name}); this.transitionTo('container', {name: name});
} }
}, },

View File

@ -12,7 +12,8 @@ var Radial = React.createClass({
} }
var classes = React.addons.classSet({ var classes = React.addons.classSet({
'radial-progress': true, 'radial-progress': true,
'radial-spinner': this.props.spin 'radial-spinner': this.props.spin,
'radial-negative': this.props.error
}); });
return ( return (
<div className={classes} data-progress={this.props.progress}> <div className={classes} data-progress={this.props.progress}>

View File

@ -14,9 +14,10 @@ var ContainerStore = require('./ContainerStore.js');
var setupSteps = [ var setupSteps = [
{ {
run: function (callback, progressCallback) { run: function (callback, progressCallback) {
console.log(util.supportDir());
var installed = virtualbox.installed(); var installed = virtualbox.installed();
if (!installed) { if (!installed) {
util.download('https://s3.amazonaws.com/kite-installer/' + virtualbox.INSTALLER_FILENAME, path.join(process.cwd(), 'resources', virtualbox.INSTALLER_FILENAME), virtualbox.INSTALLER_CHECKSUM, function (err) { util.download('https://s3.amazonaws.com/kite-installer/' + virtualbox.INSTALLER_FILENAME, path.join(util.supportDir(), virtualbox.INSTALLER_FILENAME), virtualbox.INSTALLER_CHECKSUM, function (err) {
if (err) {callback(err); return;} if (err) {callback(err); return;}
virtualbox.install(function (err) { virtualbox.install(function (err) {
if (!virtualbox.installed()) { if (!virtualbox.installed()) {
@ -144,15 +145,24 @@ var Setup = React.createClass({
var radial; var radial;
if (this.state.progress) { if (this.state.progress) {
radial = <Radial progress={this.state.progress}/>; radial = <Radial progress={this.state.progress}/>;
} else { } else if (this.state.error) {
radial = <Radial spin="true" progress="92"/>; radial = <Radial error={true} spin="true" progress="100"/>;
}
if (this.state.error) {
return (
<div className="setup">
{radial}
<p className="error">Error: {this.state.error}</p>
</div>
);
} else {
return (
<div className="setup">
{radial}
<p>{this.state.message}</p>
</div>
);
} }
return (
<div className="setup">
{radial}
<p>{this.state.message}</p>
</div>
);
}, },
componentWillMount: function () { componentWillMount: function () {
this.setState({}); this.setState({});
@ -160,10 +170,12 @@ var Setup = React.createClass({
componentDidMount: function () { componentDidMount: function () {
var self = this; var self = this;
this.setup(function (err) { this.setup(function (err) {
boot2docker.ip(function (err, ip) { if (!err) {
docker.setHost(ip); boot2docker.ip(function (err, ip) {
self.transitionTo('containers'); docker.setHost(ip);
}); self.transitionTo('containers');
});
}
}); });
}, },
setup: function (callback) { setup: function (callback) {
@ -188,7 +200,7 @@ var Setup = React.createClass({
// if any of the steps fail // if any of the steps fail
console.log('Kitematic setup failed at step ' + currentStep); console.log('Kitematic setup failed at step ' + currentStep);
console.log(err); console.log(err);
self.setState({error: err}); self.setState({error: err.message});
callback(err); callback(err);
} else { } else {
// Setup Finished // Setup Finished

View File

@ -54,7 +54,13 @@ var Boot2Docker = {
return path.join(process.cwd(), 'resources', 'boot2docker-' + this.version()); return path.join(process.cwd(), 'resources', 'boot2docker-' + this.version());
}, },
exists: function (callback) { exists: function (callback) {
cmdExec([Boot2Docker.command(), 'info'], callback); cmdExec([Boot2Docker.command(), 'info'], function (err, out) {
if (err) {
callback(null, false);
} else {
callback(null, true);
}
});
}, },
status: function (callback) { status: function (callback) {
cmdExec([Boot2Docker.command(), 'status'], function (err, out) { cmdExec([Boot2Docker.command(), 'status'], function (err, out) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 B

After

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 B

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 626 B

After

Width:  |  Height:  |  Size: 638 B

BIN
app/images/still-white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 B

After

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
app/images/wavy-white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -3,6 +3,7 @@
<head> <head>
<link rel="stylesheet" href="main.css"/> <link rel="stylesheet" href="main.css"/>
<meta http-equiv="Content-Security-Policy" content="default-src *; script-src 'self' http://localhost:35729; style-src 'self' 'unsafe-inline';"> <meta http-equiv="Content-Security-Policy" content="default-src *; script-src 'self' http://localhost:35729; style-src 'self' 'unsafe-inline';">
<title>Kitematic</title>
</head> </head>
<body> <body>
<script src="main.js"></script> <script src="main.js"></script>

View File

@ -24,7 +24,7 @@ Bugsnag.releaseStage = process.env.NODE_ENV === 'development' ? 'development' :
Bugsnag.notifyReleaseStages = []; Bugsnag.notifyReleaseStages = [];
Bugsnag.appVersion = app.getVersion(); Bugsnag.appVersion = app.getVersion();
if (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

@ -10,7 +10,7 @@
//box-shadow: 0 3px 15px rgba(0, 0, 0, 0.2); //box-shadow: 0 3px 15px rgba(0, 0, 0, 0.2);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.10); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.10);
border: none; //1px solid #ccc; border: none; //1px solid #ccc;
height: 650px; height: 610px;
display: flex; display: flex;
} }
@ -31,7 +31,7 @@
text-align: center; text-align: center;
.popover-content { .popover-content {
max-height: 300px; max-height: 160px;
padding: 0; padding: 0;
overflow: auto; overflow: auto;
} }

View File

@ -41,7 +41,7 @@
.create { .create {
flex: 1 auto; flex: 1 auto;
text-align: right; text-align: right;
.btn { /*.btn {
margin-top: 4px; margin-top: 4px;
padding: 4px 7px; padding: 4px 7px;
font-size: 16px; font-size: 16px;
@ -51,7 +51,7 @@
top: 3px; top: 3px;
left: 1px; left: 1px;
} }
} }*/
} }
} }
@ -61,6 +61,7 @@
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
box-sizing: border-box; box-sizing: border-box;
max-width: 280px;
&.sep { &.sep {
border-top: 1px solid #eee; border-top: 1px solid #eee;
@ -329,25 +330,55 @@
width: 300px; width: 300px;
} }
.details-logs { .details-panel {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
h4 {
font-size: 14px;
margin-top: 16px;
margin-left: 40px;
}
.logs { .logs {
-webkit-user-select: text; -webkit-user-select: text;
font-family: Menlo; font-family: Menlo;
font-size: 12px; font-size: 12px;
padding: 18px 45px; padding: 18px 35px;
color: lighten(@gray-normal, 6%); color: lighten(@gray-normal, 6%);
white-space: pre-wrap; white-space: pre-wrap;
p { p {
margin: 0 6px; margin: 0 6px;
} }
} }
.settings {
padding: 18px 35px;
}
}
.env-vars-labels {
width: 100%;
font-size: 12px;
color: @gray-lightest;
margin-left: 5px;
margin-bottom: 5px;
.label-key {
display: inline-block;
margin-right: 30px;
width: 20%;
}
.label-val {
display: inline-block;
width: 40%;
}
}
.env-vars {
margin-bottom: 20px;
.keyval-row {
margin-bottom: 5px;
}
input {
margin-right: 30px;
&.key {
width: 20%;
}
&.val {
width: 40%;
}
}
} }
} }
} }

View File

@ -3,7 +3,6 @@
@import "clearsans.less"; @import "clearsans.less";
@import "theme.less"; @import "theme.less";
@import "icons.less"; @import "icons.less";
@import "icons-filled.less";
@import "retina.less"; @import "retina.less";
@import "setup.less"; @import "setup.less";
@import "radial.less"; @import "radial.less";
@ -42,11 +41,14 @@ html, body {
width: 7px; width: 7px;
border-radius: 8px; border-radius: 8px;
background-color: rgba(0,0,0,0.2); background-color: rgba(0,0,0,0.2);
&:hover {
background-color: rgba(0,0,0,0.25);
}
} }
.popover { .popover {
font-family: 'Clear Sans', sans-serif; font-family: 'Clear Sans', sans-serif;
color: @gray-normal; color: @gray-normal;
font-weight: 400; font-weight: 400;
font-size: 13px; font-size: 13px;

View File

@ -3,6 +3,9 @@
text-align: center; text-align: center;
p { p {
&.error {
color: @brand-danger;
}
margin-top: 20px; margin-top: 20px;
} }
} }

View File

@ -5,6 +5,10 @@
@import "bootstrap/variables.less"; @import "bootstrap/variables.less";
@import "bootstrap/mixins.less"; @import "bootstrap/mixins.less";
h3 {
font-size: 14px;
color: @gray-darkest;
}
h4 { h4 {
font-size: 13px; font-size: 13px;
@ -12,6 +16,30 @@ h4 {
font-weight: 400; font-weight: 400;
} }
.popover-content {
color: @gray-normal;
font-size: 13px;
}
input[type="text"] {
&.line {
border: 0;
border-bottom: 1px solid @gray-lightest;
color: @gray-normal;
font-weight: 300;
padding: 5px;
transition: all 0.1s;
&:focus {
outline: 0;
border-bottom: 1px solid @brand-action;
}
&::-webkit-input-placeholder {
color: #ddd;
font-weight: 300;
}
}
}
// //
// Buttons // Buttons
// -------------------------------------------------- // --------------------------------------------------
@ -63,10 +91,17 @@ h4 {
} }
.btn-group { .btn-group {
&.tabs {
.btn {
padding: 6px 14px 6px 14px;
}
}
.btn { .btn {
.icon-dropdown { .icon-dropdown {
&.icon:before { &.icon:before {
top: 7px; position: relative;
font-size: 10px;
top: -2px;
margin-left: 0px; margin-left: 0px;
margin-right: 4px; margin-right: 4px;
} }
@ -88,6 +123,13 @@ h4 {
height: 32px; height: 32px;
cursor: default; cursor: default;
&.small {
height: 22px;
.icon {
font-size: 10px;
}
}
.content { .content {
position: relative; position: relative;
top: -4px; top: -4px;
@ -95,6 +137,14 @@ h4 {
margin-right: 5px; margin-right: 5px;
} }
.icon-dropdown {
&.icon:before {
font-size: 10px;
position: relative;
top: -2px;
}
}
.icon { .icon {
position: relative; position: relative;
font-size: 16px; font-size: 16px;
@ -112,12 +162,22 @@ h4 {
box-shadow: none; box-shadow: none;
outline: none !important; outline: none !important;
} }
&.only-icon {
padding: 6px 7px 6px 7px;
&.small {
padding: 2px 5px 3px 5px;
}
}
} }
// Apply the mixin to the buttons // Apply the mixin to the buttons
.btn-action { .btn-action {
.btn-styles(@brand-action); .btn-styles(@brand-action);
} }
.btn-positive {
.btn-styles(@brand-positive);
}
.btn-default { .btn-styles(@btn-default-bg); } .btn-default { .btn-styles(@btn-default-bg); }
.btn-primary { .btn-styles(@btn-primary-bg); } .btn-primary { .btn-styles(@btn-primary-bg); }
.btn-success { .btn-styles(@btn-success-bg); } .btn-success { .btn-styles(@btn-success-bg); }

View File

@ -6,9 +6,23 @@ var progress = require('request-progress');
var exec = require('exec'); var exec = require('exec');
var Util = { var Util = {
supportDir: function (callback) {
var dirs = ['Application\ Support', 'Kitematic'];
var acc = process.env.HOME;
dirs.forEach(function (d) {
acc = path.join(acc, d);
if (!fs.existsSync(acc)) {
fs.mkdirSync(acc);
}
});
return acc;
},
download: function (url, filename, checksum, callback, progressCallback) { download: function (url, filename, checksum, callback, progressCallback) {
var doDownload = function () { var doDownload = function () {
progress(request(url), { progress(request({
uri: url,
rejectUnauthorized: false
}), {
throttle: 250 throttle: 250
}).on('progress', function (state) { }).on('progress', function (state) {
progressCallback(state.percent); progressCallback(state.percent);

View File

@ -2,6 +2,7 @@ var fs = require('fs');
var exec = require('exec'); var exec = require('exec');
var path = require('path'); var path = require('path');
var async = require('async'); var async = require('async');
var util = require('./util');
var VirtualBox = { var VirtualBox = {
REQUIRED_VERSION: '4.3.18', REQUIRED_VERSION: '4.3.18',
@ -16,7 +17,7 @@ var VirtualBox = {
}, },
install: function (callback) { install: function (callback) {
// -W waits for the process to close before finishing. // -W waits for the process to close before finishing.
exec('open -W ' + path.join(process.cwd(), 'resources', this.INSTALLER_FILENAME).replace(' ', '\\ '), function (stderr, stdout, code) { exec('open -W ' + path.join(util.supportDir(), this.INSTALLER_FILENAME).replace(' ', '\\ '), function (stderr, stdout, code) {
if (code) { if (code) {
callback(stderr); callback(stderr);
return; return;

View File

@ -35,6 +35,7 @@
"minimist": "^1.1.0", "minimist": "^1.1.0",
"moment": "2.8.1", "moment": "2.8.1",
"node-uuid": "1.4.1", "node-uuid": "1.4.1",
"object-assign": "^2.0.0",
"open": "0.0.5", "open": "0.0.5",
"react": "^0.12.2", "react": "^0.12.2",
"react-bootstrap": "^0.13.2", "react-bootstrap": "^0.13.2",

View File

@ -9,8 +9,6 @@ end try
return doesExist return doesExist
EOF` EOF`
echo $ITERM_EXISTS
if [ $ITERM_EXISTS == "true" ]; then if [ $ITERM_EXISTS == "true" ]; then
osascript > /dev/null <<EOF osascript > /dev/null <<EOF
tell application "iTerm" tell application "iTerm"
@ -19,14 +17,14 @@ tell application "iTerm"
tell the first terminal tell the first terminal
launch session "Default Session" launch session "Default Session"
tell the last session tell the last session
write text "clear && $*" write text "bash -c \"clear && $*\""
end tell end tell
end tell end tell
on error on error
tell (make new terminal) tell (make new terminal)
launch session "Default Session" launch session "Default Session"
tell the last session tell the last session
write text "clear && $*" write text "bash -c \"clear && $*\""
end tell end tell
end tell end tell
end try end try
@ -39,7 +37,7 @@ tell application "Terminal" to activate
delay 0.4 delay 0.4
tell application "System Events" to keystroke "t" using command down tell application "System Events" to keystroke "t" using command down
tell application "Terminal" tell application "Terminal"
do script "clear && $*" in window 1 do script "bash -c \"clear && $*\"" in window 1
end tell end tell
EOF EOF

View File