mirror of https://github.com/docker/docs.git
Merge pull request #165 from kitematic/container-dropdowns
Added drop down settings for container ports and volumes.
This commit is contained in:
commit
e00454f1ba
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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));
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue