Merge pull request #165 from kitematic/container-dropdowns

Added drop down settings for container ports and volumes.
This commit is contained in:
Jeffrey Morgan 2015-01-29 16:54:44 -05:00
commit e00454f1ba
9 changed files with 436 additions and 130 deletions

View File

@ -4,6 +4,7 @@ var React = require('react/addons');
var Router = require('react-router'); var Router = require('react-router');
var exec = require('exec'); var exec = require('exec');
var path = require('path'); var path = require('path');
var assign = require('object-assign');
var remote = require('remote'); var remote = require('remote');
var dialog = remote.require('dialog'); var dialog = remote.require('dialog');
var ContainerStore = require('./ContainerStore'); var ContainerStore = require('./ContainerStore');
@ -13,7 +14,7 @@ var boot2docker = require('./boot2docker');
var ProgressBar = require('react-bootstrap/ProgressBar'); var ProgressBar = require('react-bootstrap/ProgressBar');
var ContainerDetails = React.createClass({ var ContainerDetails = React.createClass({
mixins: [Router.State], mixins: [Router.State, Router.Navigation],
_oldHeight: 0, _oldHeight: 0,
PAGE_LOGS: 'logs', PAGE_LOGS: 'logs',
PAGE_SETTINGS: 'settings', PAGE_SETTINGS: 'settings',
@ -22,7 +23,12 @@ var ContainerDetails = React.createClass({
logs: [], logs: [],
page: this.PAGE_LOGS, page: this.PAGE_LOGS,
env: {}, env: {},
pendingEnv: {} pendingEnv: {},
ports: {},
defaultPort: null,
volumes: {},
popoverVolumeOpen: false,
popoverViewOpen: false,
}; };
}, },
componentWillReceiveProps: function () { componentWillReceiveProps: function () {
@ -34,29 +40,72 @@ var ContainerDetails = React.createClass({
this.init(); 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);
// Make clicking anywhere close popovers
$('body').on('click', function (e) {
var popoverViewIsTarget = $('.popover-view').is(e.target) || $('.popover-view').has(e.target).length !== 0 || $('.dropdown-view').is(e.target) || $('.dropdown-view').has(e.target).length !== 0;
var popoverVolumeIsTarget = $('.popover-volume').is(e.target) || $('.popover-volume').has(e.target).length !== 0 || $('.dropdown-volume').is(e.target) || $('.dropdown-volume').has(e.target).length !== 0;
var state = {};
if (!popoverViewIsTarget) {
state.popoverViewOpen = false;
}
if (!popoverVolumeIsTarget) {
state.popoverVolumeOpen = false;
}
if (this.state.popoverViewOpen || this.state.popoverVolumeOpen) {
this.setState(state);
}
}.bind(this));
}, },
componentWillUnmount: function () { componentWillUnmount: function () {
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);
}, },
componentDidUpdate: function () { componentDidUpdate: function () {
// Scroll logs to bottom
var parent = $('.details-logs'); var parent = $('.details-logs');
if (!parent.length) { if (parent.length) {
return; if (parent.scrollTop() >= this._oldHeight) {
parent.stop();
parent.scrollTop(parent[0].scrollHeight - parent.height());
}
this._oldHeight = parent[0].scrollHeight - parent.height();
} }
if (parent.scrollTop() >= this._oldHeight) {
parent.stop(); var $viewDropdown = $(this.getDOMNode()).find('.dropdown-view');
parent.scrollTop(parent[0].scrollHeight - parent.height()); var $volumeDropdown = $(this.getDOMNode()).find('.dropdown-volume');
var $viewPopover = $(this.getDOMNode()).find('.popover-view');
var $volumePopover = $(this.getDOMNode()).find('.popover-volume');
if ($viewDropdown && $volumeDropdown && $viewPopover && $volumePopover) {
$viewPopover.offset({
top: $viewDropdown.offset().top + 32,
left: $viewDropdown.offset().left - ($viewPopover.outerWidth() / 2) + 14
});
$volumePopover.offset({
top: $volumeDropdown.offset().top + 32,
left: $volumeDropdown.offset().left + $volumeDropdown.outerWidth() - $volumePopover.outerWidth() / 2 - 20
});
} }
this._oldHeight = parent[0].scrollHeight - parent.height();
}, },
init: function () { init: function () {
var container = ContainerStore.container(this.getParams().name);
if (!container) {
return;
}
this.setState({ this.setState({
env: ContainerUtil.env(ContainerStore.container(this.getParams().name)) env: ContainerUtil.env(container),
}); });
ContainerStore.fetchLogs(this.getParams().name, function () { var ports = ContainerUtil.ports(container);
this.updateLogs(); var webPorts = ['80', '8000', '8080', '3000', '5000', '2368'];
}.bind(this)); this.setState({
ports: ports,
defaultPort: _.find(_.keys(ports), function (port) {
return webPorts.indexOf(port) !== -1;
})
});
this.updateLogs();
}, },
updateLogs: function (name) { updateLogs: function (name) {
if (name && name !== this.getParams().name) { if (name && name !== this.getParams().name) {
@ -84,27 +133,31 @@ var ContainerDetails = React.createClass({
}); });
}, },
handleView: function () { handleView: function () {
var container = this.props.container; if (this.state.defaultPort) {
boot2docker.ip(function (err, ip) { console.log(this.state.defaultPort);
var ports = _.map(container.NetworkSettings.Ports, function (value, key) { exec(['open', this.state.ports[this.state.defaultPort].url], function (err) {
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; } if (err) { throw err; }
}); });
}
},
handleViewLink: function (url) {
exec(['open', url], function (err) {
if (err) { throw err; }
});
},
handleViewDropdown: function(e) {
this.setState({
popoverViewOpen: !this.state.popoverViewOpen
});
},
handleVolumeDropdown: function(e) {
this.setState({
popoverVolumeOpen: !this.state.popoverVolumeOpen
});
},
handleRestart: function () {
ContainerStore.restart(this.props.container.Name, function (err) {
console.log(err);
}); });
}, },
handleRestart: function () { handleRestart: function () {
@ -122,6 +175,17 @@ var ContainerDetails = React.createClass({
} }
}); });
}, },
handleSaveContainerName: function () {
var newName = $('#input-container-name').val();
ContainerStore.updateContainer(this.props.container.Name, {
name: newName
}, function (err) {
this.transitionTo('container', {name: newName});
if (err) {
console.error(err);
}
}.bind(this));
},
handleSaveEnvVar: function () { handleSaveEnvVar: function () {
var $rows = $('.env-vars .keyval-row'); var $rows = $('.env-vars .keyval-row');
var envVarList = []; var envVarList = [];
@ -133,8 +197,19 @@ var ContainerDetails = React.createClass({
} }
envVarList.push(key + '=' + val); envVarList.push(key + '=' + val);
}); });
ContainerStore.updateContainer(this.props.container.Name, { var self = this;
ContainerStore.updateContainer(self.props.container.Name, {
Env: envVarList Env: envVarList
}, function (err) {
if (err) {
console.error(err);
} else {
self.setState({
pendingEnv: {}
});
$('#new-env-key').val('');
$('#new-env-val').val('');
}
}); });
}, },
handleAddPendingEnvVar: function () { handleAddPendingEnvVar: function () {
@ -224,6 +299,66 @@ var ContainerDetails = React.createClass({
); );
}); });
var disabledClass = '';
if (!this.props.container.State.Running) {
disabledClass = 'disabled';
}
var buttonClass = React.addons.classSet({
btn: true,
'btn-action': true,
'with-icon': true,
disabled: !this.props.container.State.Running
});
var viewButtonClass = React.addons.classSet({
btn: true,
'btn-action': true,
'with-icon': true,
disabled: !this.props.container.State.Running || !this.state.defaultPort
});
var textButtonClasses = React.addons.classSet({
'btn': true,
'btn-action': true,
'only-icon': true,
'active': this.state.page === this.PAGE_LOGS,
disabled: this.props.container.State.Downloading
});
var gearButtonClass = React.addons.classSet({
'btn': true,
'btn-action': true,
'only-icon': true,
'active': this.state.page === this.PAGE_SETTINGS,
disabled: this.props.container.State.Downloading
});
var viewPopoverClasses = React.addons.classSet({
popover: true,
hidden: false
});
var popoverVolumeClasses = React.addons.classSet({
'popover-volume': true,
hidden: !this.state.popoverVolumeOpen
});
var popoverViewClasses = React.addons.classSet({
'popover-view': true,
hidden: !this.state.popoverViewOpen
});
var dropdownClasses = {
btn: true,
'btn-action': true,
'with-icon': true,
'dropdown-toggle': true,
disabled: !this.props.container.State.Running
};
var dropdownViewButtonClass = React.addons.classSet(assign({'dropdown-view': true}, dropdownClasses));
var dropdownVolumeButtonClass = React.addons.classSet(assign({'dropdown-volume': true}, dropdownClasses));
var body; var body;
if (this.props.container.State.Downloading) { if (this.props.container.State.Downloading) {
body = ( body = (
@ -245,7 +380,10 @@ var ContainerDetails = React.createClass({
<div className="details-panel"> <div className="details-panel">
<div className="settings"> <div className="settings">
<h3>Container Name</h3> <h3>Container Name</h3>
<input id="input-container-name" type="text" className="line" placeholder="Container Name" defaultValue={this.props.container.Name}></input> <div className="container-name">
<input id="input-container-name" type="text" className="line" placeholder="Container Name" defaultValue={this.props.container.Name}></input>
</div>
<a className="btn btn-action" onClick={this.handleSaveContainerName}>Save</a>
<h3>Environment Variables</h3> <h3>Environment Variables</h3>
<div className="env-vars-labels"> <div className="env-vars-labels">
<div className="label-key">KEY</div> <div className="label-key">KEY</div>
@ -269,38 +407,27 @@ var ContainerDetails = React.createClass({
} }
} }
var disabledClass = ''; var ports = _.map(_.pairs(self.state.ports), function (pair, index, list) {
if (!this.props.container.State.Running) { var key = pair[0];
disabledClass = 'disabled'; var val = pair[1];
} return (
<div key={key} className="table-values">
var buttonClass = React.addons.classSet({ <span className="value-left">{key}</span><span className="icon icon-arrow-right"></span>
btn: true, 'btn-action': true, <a className="value-right" onClick={self.handleViewLink.bind(self, val.url)}>{val.display}</a>
'with-icon': true, </div>
disabled: !this.props.container.State.Running );
});
var dropdownButtonClass = React.addons.classSet({
btn: true,
'btn-action': true,
'with-icon': true,
'dropdown-toggle': true,
disabled: !this.props.container.State.Running
}); });
var textButtonClasses = React.addons.classSet({ var volumes = _.map(self.props.container.Volumes, function (val, key) {
'btn': true, if (!val || val.indexOf(process.env.HOME) === -1) {
'btn-action': true, val = 'No Host Folder';
'only-icon': true, }
'active': this.state.page === this.PAGE_LOGS, return (
disabled: this.props.container.State.Downloading <div key={key} className="table-values">
}); <span className="value-left">{key}</span><span className="icon icon-arrow-right"></span>
<a className="value-right">{val.replace(process.env.HOME, '~')}</a>
var gearButtonClass = React.addons.classSet({ </div>
'btn': true, );
'btn-action': true,
'only-icon': true,
'active': this.state.page === this.PAGE_SETTINGS,
disabled: this.props.container.State.Downloading
}); });
return ( return (
@ -311,10 +438,11 @@ var ContainerDetails = React.createClass({
</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.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> <a className={viewButtonClass} onClick={this.handleView}><span className="icon icon-preview-2"></span><span className="content">View</span></a>
<a className={dropdownViewButtonClass} onClick={this.handleViewDropdown}><span className="icon-dropdown icon icon-arrow-37"></span></a>
</div> </div>
<div className="action"> <div className="action">
<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={dropdownVolumeButtonClass} onClick={this.handleVolumeDropdown}><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.handleRestart}><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>
@ -327,6 +455,24 @@ var ContainerDetails = React.createClass({
<a className={gearButtonClass} onClick={this.showSettings}><span className="icon icon-setting-gear"></span></a> <a className={gearButtonClass} onClick={this.showSettings}><span className="icon icon-setting-gear"></span></a>
</div> </div>
</div> </div>
<Popover className={popoverViewClasses} placement="bottom">
<div className="table ports">
<div className="table-labels">
<div className="label-left">DOCKER PORT</div>
<div className="label-right">MAC PORT</div>
</div>
{ports}
</div>
</Popover>
<Popover className={popoverVolumeClasses} placement="bottom">
<div className="table volumes">
<div className="table-labels">
<div className="label-left">DOCKER FOLDER</div>
<div className="label-right">MAC FOLDER</div>
</div>
{volumes}
</div>
</Popover>
</div> </div>
{body} {body}
</div> </div>

View File

@ -41,6 +41,10 @@ var ContainerModal = React.createClass({
this._searchRequest = null; this._searchRequest = null;
} }
if (!query.length) {
return;
}
this.setState({ this.setState({
loading: true loading: true
}); });
@ -227,6 +231,7 @@ var ContainerModal = React.createClass({
<div className={magnifierClasses}></div> <div className={magnifierClasses}></div>
<RetinaImage className={loadingClasses} src="loading.png"/> <RetinaImage className={loadingClasses} src="loading.png"/>
</div> </div>
{question}
<div className="results"> <div className="results">
<div className="title">{title}</div> <div className="title">{title}</div>
{results} {results}

View File

@ -1,5 +1,6 @@
var EventEmitter = require('events').EventEmitter; var EventEmitter = require('events').EventEmitter;
var async = require('async'); var async = require('async');
var path = require('path');
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');
@ -15,7 +16,9 @@ var _recommended = [];
var _containers = {}; var _containers = {};
var _progress = {}; var _progress = {};
var _logs = {}; var _logs = {};
var _streams = {};
var _muted = {}; var _muted = {};
var _config = {};
var ContainerStore = assign(EventEmitter.prototype, { var ContainerStore = assign(EventEmitter.prototype, {
CLIENT_CONTAINER_EVENT: 'client_container', CLIENT_CONTAINER_EVENT: 'client_container',
@ -110,30 +113,44 @@ 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;
if (!containerData.name) containerData.name = containerData.Name; if (!containerData.name && containerData.Name) {
containerData.name = containerData.Name;
} else if (!containerData.name) {
containerData.name = name;
}
if (containerData.Config && containerData.Config.Image) { if (containerData.Config && containerData.Config.Image) {
containerData.Image = containerData.Config.Image; containerData.Image = containerData.Config.Image;
} }
existing.kill(function (err, data) { existing.kill(function (err, data) {
existing.remove(function (err, data) { existing.remove(function (err, data) {
docker.client().createContainer(containerData, function (err, container) { docker.client().getImage(containerData.Image).inspect(function (err, data) {
if (err) { var binds = [];
callback(err, null); if (data.Config.Volumes) {
return; _.each(data.Config.Volumes, function (value, key) {
} binds.push(path.join(process.env.HOME, 'Kitematic', containerData.name, key)+ ':' + key);
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);
}); });
} }
docker.client().createContainer(containerData, function (err, container) {
if (err) {
callback(err, null);
return;
}
if (containerData.State && !containerData.State.Running) {
self.fetchContainer(containerData.name, callback);
} else {
container.start({
PublishAllPorts: true,
Binds: binds
}, function (err) {
if (err) {
callback(err);
return;
}
self.fetchContainer(containerData.name, callback);
});
}
});
}); });
}); });
}); });
@ -209,10 +226,13 @@ 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]) { if (!container) {
return; return;
} }
delete _containers[container.Name]; delete _containers[container.Name];
if (_muted[container.Name]) {
return;
}
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) {
@ -256,10 +276,13 @@ var ContainerStore = assign(EventEmitter.prototype, {
container.State.Downloading = !!env.KITEMATIC_DOWNLOADING; container.State.Downloading = !!env.KITEMATIC_DOWNLOADING;
container.KitematicDownloadingImage = env.KITEMATIC_DOWNLOADING_IMAGE; container.KitematicDownloadingImage = env.KITEMATIC_DOWNLOADING_IMAGE;
this.fetchLogs(container.Name, function (err) {
}.bind(this));
_containers[container.Name] = container; _containers[container.Name] = container;
callback(null, container); callback(null, container);
} }
}); }.bind(this));
}, },
fetchAllContainers: function (callback) { fetchAllContainers: function (callback) {
var self = this; var self = this;
@ -305,57 +328,45 @@ var ContainerStore = assign(EventEmitter.prototype, {
}); });
}, },
fetchLogs: function (name, callback) { fetchLogs: function (name, callback) {
if (_logs[name]) {
callback();
} else {
_logs[name] = [];
}
var index = 0; var index = 0;
var self = this; var self = this;
docker.client().getContainer(name).logs({ docker.client().getContainer(name).logs({
follow: false, follow: true,
stdout: true, stdout: true,
stderr: true, stderr: true,
timestamps: true timestamps: true
}, function (err, stream) { }, function (err, stream) {
callback(err);
if (_streams[name]) {
return;
}
_streams[name] = stream;
if (err) { if (err) {
return; return;
} }
_logs[name] = [];
stream.setEncoding('utf8'); stream.setEncoding('utf8');
var timeout;
stream.on('data', function (buf) { stream.on('data', function (buf) {
// Every other message is a header // Every other message is a header
if (index % 2 === 1) { if (index % 2 === 1) {
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);
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
timeout = setTimeout(function () {
timeout = null;
self.emit(self.SERVER_LOGS_EVENT, name);
}, 100);
_logs[name].push(convert.toHtml(self._escapeHTML(msg))); _logs[name].push(convert.toHtml(self._escapeHTML(msg)));
} }
index += 1; index += 1;
}); });
stream.on('end', function (buf) { stream.on('end', function () {
self.emit(self.SERVER_LOGS_EVENT, name); delete _streams[name];
callback(); console.log('end', name);
docker.client().getContainer(name).logs({
follow: true,
stdout: true,
stderr: true,
timestamps: true,
tail: 0
}, function (err, stream) {
if (err) {
return;
}
stream.setEncoding('utf8');
stream.on('data', function (buf) {
// Every other message is a header
if (index % 2 === 1) {
var time = buf.substr(0,buf.indexOf(' '));
var msg = buf.substr(buf.indexOf(' ')+1);
_logs[name].push(convert.toHtml(self._escapeHTML(msg)));
self.emit(self.SERVER_LOGS_EVENT, name);
}
index += 1;
});
});
}); });
}); });
}, },
@ -395,11 +406,15 @@ var ContainerStore = assign(EventEmitter.prototype, {
} }
}); });
}, },
updateContainer: function (name, data) { updateContainer: function (name, data, callback) {
_muted[name] = true; _muted[name] = true;
if (!data.name) {
data.name = data.Name;
}
var fullData = assign(_containers[name], data); var fullData = assign(_containers[name], data);
console.log(fullData);
this._createContainer(name, fullData, function (err) { this._createContainer(name, fullData, function (err) {
this.emit(this.CLIENT_CONTAINER_EVENT, name); callback(err);
_muted[name] = false; _muted[name] = false;
}.bind(this)); }.bind(this));
}, },

View File

@ -1,4 +1,5 @@
var _ = require('underscore'); var _ = require('underscore');
var docker = require('./docker');
var ContainerUtil = { var ContainerUtil = {
env: function (container) { env: function (container) {
@ -10,6 +11,26 @@ var ContainerUtil = {
var splits = [env.slice(0, i), env.slice(i + 1)]; var splits = [env.slice(0, i), env.slice(i + 1)];
return splits; return splits;
})); }));
},
ports: function (container, callback) {
var res = {};
var ip = docker.host;
console.log(container);
_.each(container.NetworkSettings.Ports, function (value, key) {
var dockerPort = key;
var localUrl = null;
var localUrlDisplay = null;
if (value && value.length) {
var port = value[0].HostPort;
localUrl = 'http://' + ip + ':' + port;
localUrlDisplay = ip + ': ' + port;
}
res[dockerPort] = {
url: localUrl,
display: localUrlDisplay
};
});
return res;
} }
}; };

View File

@ -125,7 +125,7 @@ var setupSteps = [
boot2docker.ip(function (err, ip) { boot2docker.ip(function (err, ip) {
if (err) {callback(err); return;} if (err) {callback(err); return;}
console.log('Setting host IP to: ' + ip); console.log('Setting host IP to: ' + ip);
// Docker.setHost(ip); docker.setHost(ip);
callback(err); callback(err);
}); });
}, },
@ -147,6 +147,8 @@ var Setup = React.createClass({
radial = <Radial progress={this.state.progress}/>; radial = <Radial progress={this.state.progress}/>;
} else if (this.state.error) { } else if (this.state.error) {
radial = <Radial error={true} spin="true" progress="100"/>; radial = <Radial error={true} spin="true" progress="100"/>;
} else {
radial = <Radial spin="true" progress="100"/>;
} }
if (this.state.error) { if (this.state.error) {
return ( return (

View File

@ -1,7 +1,7 @@
.create-modal { .create-modal {
@modal-padding: 32px; @modal-padding: 32px;
@search-width: 372px; @search-width: 372px;
@custom-width: 270px; @custom-width: 0;
.modal-dialog { .modal-dialog {
margin-top: 80px; margin-top: 80px;
width: calc(@modal-padding + @search-width + 2 * @modal-padding + @custom-width); width: calc(@modal-padding + @search-width + 2 * @modal-padding + @custom-width);
@ -20,12 +20,6 @@
flex-direction: row; flex-direction: row;
padding: 32px 32px; padding: 32px 32px;
aside.custom {
flex: 0 auto;
padding-left: 32px;
min-width: 270px;
}
.popover { .popover {
width: 180px; width: 180px;
text-align: center; text-align: center;
@ -62,12 +56,8 @@
section.search { section.search {
min-width: 404px; min-width: 404px;
padding-right: 32px;
border-right: 1px solid #eee;
.question { .question {
color: @gray-lightest;
font-size: 10px;
text-align: right; text-align: right;
} }

View File

@ -1,4 +1,119 @@
.popover {
&.popover-view {
min-width: 290px;
}
&.popover-volume {
min-width: 400px;
}
.popover-content {
display: flex;
flex-direction: column;
padding: 14px 14px 20px;
.table {
margin-bottom: 0;
.icon-arrow-right {
color: #aaa;
margin: 2px 9px 0;
flex: 0 auto;
min-width: 13px;
}
.btn {
min-width: 22px;
margin-left: 10px;
}
.table-labels {
flex: 1 auto;
display: flex;
font-size: 12px;
color: @gray-lightest;
.label-left {
flex: 0 auto;
min-width: 80px;
margin-right: 30px;
text-align: right;
}
.label-right {
flex: 1 auto;
display: inline-block;
width: 40%;
}
}
.table-values {
flex: 1 auto;
display: flex;
flex-direction: row;
margin: 8px 0;
.value-left {
text-align: right;
min-width: 80px;
flex: 0 auto;
}
.value-right {
flex: 1 auto;
-webkit-user-select: text;
width: 154px;
}
}
.table-new {
margin-top: 10px;
flex: 1 auto;
display: flex;
input {
padding: 0;
font-weight: 400;
}
input.new-left {
flex: 0 auto;
text-align: right;
min-width: 80px;
max-width: 80px;
}
.new-right-wrapper {
position: relative;
display: flex;
flex: 1 auto;
.new-right-placeholder {
position: absolute;
top: 3px;
left: 0;
font-weight: 400;
}
input.new-right {
flex: 1 auto;
height: 24px;
position :relative;
padding-left: 107px;
}
}
}
&.volumes {
.label-left {
min-width: 120px;
}
.value-left {
min-width: 120px;
}
.icon {
color: #aaa;
margin: 2px 9px 0;
}
}
}
.question {
margin: 12px 6px 6px;
}
}
}
.containers { .containers {
box-sizing: border-box;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -349,6 +464,13 @@
} }
} }
.container-name {
margin-bottom: 20px;
input {
width: 20%;
}
}
.env-vars-labels { .env-vars-labels {
width: 100%; width: 100%;
font-size: 12px; font-size: 12px;

View File

@ -48,6 +48,11 @@ html, body {
} }
} }
.question {
color: @gray-lightest;
font-size: 10px;
}
.popover { .popover {
font-family: 'Clear Sans', sans-serif; font-family: 'Clear Sans', sans-serif;
color: @gray-normal; color: @gray-normal;

View File

@ -17,6 +17,7 @@ if (argv.test) {
} }
process.env.NODE_PATH = __dirname + '/../node_modules'; process.env.NODE_PATH = __dirname + '/../node_modules';
process.chdir(path.join(__dirname, '..'));
app.on('activate-with-no-open-windows', function () { app.on('activate-with-no-open-windows', function () {
if (mainWindow) { if (mainWindow) {
@ -98,7 +99,6 @@ app.on('ready', function() {
}); });
ipc.on('vm', function (event, arg) { ipc.on('vm', function (event, arg) {
console.log('save vm', arg);
saveVMOnQuit = arg; saveVMOnQuit = arg;
}); });