diff --git a/app/ContainerDetails.react.js b/app/ContainerDetails.react.js
index 17c0b392c3..c1d48058ab 100644
--- a/app/ContainerDetails.react.js
+++ b/app/ContainerDetails.react.js
@@ -4,6 +4,7 @@ var React = require('react/addons');
var Router = require('react-router');
var exec = require('exec');
var path = require('path');
+var assign = require('object-assign');
var remote = require('remote');
var dialog = remote.require('dialog');
var ContainerStore = require('./ContainerStore');
@@ -13,7 +14,7 @@ var boot2docker = require('./boot2docker');
var ProgressBar = require('react-bootstrap/ProgressBar');
var ContainerDetails = React.createClass({
- mixins: [Router.State],
+ mixins: [Router.State, Router.Navigation],
_oldHeight: 0,
PAGE_LOGS: 'logs',
PAGE_SETTINGS: 'settings',
@@ -22,7 +23,12 @@ var ContainerDetails = React.createClass({
logs: [],
page: this.PAGE_LOGS,
env: {},
- pendingEnv: {}
+ pendingEnv: {},
+ ports: {},
+ defaultPort: null,
+ volumes: {},
+ popoverVolumeOpen: false,
+ popoverViewOpen: false,
};
},
componentWillReceiveProps: function () {
@@ -34,29 +40,72 @@ var ContainerDetails = React.createClass({
this.init();
ContainerStore.on(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress);
ContainerStore.on(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs);
+
+ // Make clicking anywhere close popovers
+ $('body').on('click', function (e) {
+ 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 () {
ContainerStore.removeListener(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress);
ContainerStore.removeListener(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs);
},
componentDidUpdate: function () {
+ // Scroll logs to bottom
var parent = $('.details-logs');
- if (!parent.length) {
- return;
+ if (parent.length) {
+ 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();
- parent.scrollTop(parent[0].scrollHeight - parent.height());
+
+ var $viewDropdown = $(this.getDOMNode()).find('.dropdown-view');
+ 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 () {
+ var container = ContainerStore.container(this.getParams().name);
+ if (!container) {
+ return;
+ }
this.setState({
- env: ContainerUtil.env(ContainerStore.container(this.getParams().name))
+ env: ContainerUtil.env(container),
});
- ContainerStore.fetchLogs(this.getParams().name, function () {
- this.updateLogs();
- }.bind(this));
+ var ports = ContainerUtil.ports(container);
+ var webPorts = ['80', '8000', '8080', '3000', '5000', '2368'];
+ this.setState({
+ ports: ports,
+ defaultPort: _.find(_.keys(ports), function (port) {
+ return webPorts.indexOf(port) !== -1;
+ })
+ });
+ this.updateLogs();
},
updateLogs: function (name) {
if (name && name !== this.getParams().name) {
@@ -84,27 +133,31 @@ var ContainerDetails = React.createClass({
});
},
handleView: function () {
- var container = this.props.container;
- boot2docker.ip(function (err, ip) {
- var ports = _.map(container.NetworkSettings.Ports, function (value, key) {
- var portProtocolPair = key.split('/');
- var res = {
- 'port': portProtocolPair[0],
- 'protocol': portProtocolPair[1]
- };
- if (value && value.length) {
- var port = value[0].HostPort;
- res.host = ip;
- res.port = port;
- res.url = 'http://' + ip + ':' + port;
- } else {
- return null;
- }
- return res;
- });
- exec(['open', ports[0].url], function (err) {
+ if (this.state.defaultPort) {
+ console.log(this.state.defaultPort);
+ exec(['open', this.state.ports[this.state.defaultPort].url], function (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 () {
@@ -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 () {
var $rows = $('.env-vars .keyval-row');
var envVarList = [];
@@ -133,8 +197,19 @@ var ContainerDetails = React.createClass({
}
envVarList.push(key + '=' + val);
});
- ContainerStore.updateContainer(this.props.container.Name, {
+ var self = this;
+ ContainerStore.updateContainer(self.props.container.Name, {
Env: envVarList
+ }, function (err) {
+ if (err) {
+ console.error(err);
+ } else {
+ self.setState({
+ pendingEnv: {}
+ });
+ $('#new-env-key').val('');
+ $('#new-env-val').val('');
+ }
});
},
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;
if (this.props.container.State.Downloading) {
body = (
@@ -245,7 +380,10 @@ var ContainerDetails = React.createClass({
Container Name
-
+
+
+
+
Save
Environment Variables
KEY
@@ -269,38 +407,27 @@ 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 dropdownButtonClass = React.addons.classSet({
- btn: true,
- 'btn-action': true,
- 'with-icon': true,
- 'dropdown-toggle': true,
- disabled: !this.props.container.State.Running
+ var ports = _.map(_.pairs(self.state.ports), function (pair, index, list) {
+ var key = pair[0];
+ var val = pair[1];
+ return (
+
+ );
});
- 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 volumes = _.map(self.props.container.Volumes, function (val, key) {
+ if (!val || val.indexOf(process.env.HOME) === -1) {
+ val = 'No Host Folder';
+ }
+ return (
+
+ );
});
return (
@@ -311,10 +438,11 @@ var ContainerDetails = React.createClass({
Restart
@@ -327,6 +455,24 @@ var ContainerDetails = React.createClass({
+
+
+
+
DOCKER PORT
+
MAC PORT
+
+ {ports}
+
+
+
+
+
+
DOCKER FOLDER
+
MAC FOLDER
+
+ {volumes}
+
+
{body}
diff --git a/app/ContainerModal.react.js b/app/ContainerModal.react.js
index 68558b0896..b9d862a7dd 100644
--- a/app/ContainerModal.react.js
+++ b/app/ContainerModal.react.js
@@ -41,6 +41,10 @@ var ContainerModal = React.createClass({
this._searchRequest = null;
}
+ if (!query.length) {
+ return;
+ }
+
this.setState({
loading: true
});
@@ -227,6 +231,7 @@ var ContainerModal = React.createClass({
+ {question}
{title}
{results}
diff --git a/app/ContainerStore.js b/app/ContainerStore.js
index 9099c31e36..ff975c4241 100644
--- a/app/ContainerStore.js
+++ b/app/ContainerStore.js
@@ -1,5 +1,6 @@
var EventEmitter = require('events').EventEmitter;
var async = require('async');
+var path = require('path');
var assign = require('object-assign');
var Stream = require('stream');
var Convert = require('ansi-to-html');
@@ -15,7 +16,9 @@ var _recommended = [];
var _containers = {};
var _progress = {};
var _logs = {};
+var _streams = {};
var _muted = {};
+var _config = {};
var ContainerStore = assign(EventEmitter.prototype, {
CLIENT_CONTAINER_EVENT: 'client_container',
@@ -110,30 +113,44 @@ var ContainerStore = assign(EventEmitter.prototype, {
_createContainer: function (name, containerData, callback) {
var existing = docker.client().getContainer(name);
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) {
containerData.Image = containerData.Config.Image;
}
existing.kill(function (err, data) {
existing.remove(function (err, data) {
- docker.client().createContainer(containerData, function (err, container) {
- if (err) {
- callback(err, null);
- return;
- }
- 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().getImage(containerData.Image).inspect(function (err, data) {
+ var binds = [];
+ if (data.Config.Volumes) {
+ _.each(data.Config.Volumes, function (value, key) {
+ binds.push(path.join(process.env.HOME, 'Kitematic', containerData.name, key)+ ':' + key);
});
}
+
+ 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 (data.status === 'destroy') {
var container = _.findWhere(_.values(_containers), {Id: data.id});
- if (!container || _muted[container.Name]) {
+ if (!container) {
return;
}
delete _containers[container.Name];
+ if (_muted[container.Name]) {
+ return;
+ }
this.emit(this.SERVER_CONTAINER_EVENT, container.Name, data.status);
} else {
this.fetchContainer(data.id, function (err) {
@@ -256,10 +276,13 @@ var ContainerStore = assign(EventEmitter.prototype, {
container.State.Downloading = !!env.KITEMATIC_DOWNLOADING;
container.KitematicDownloadingImage = env.KITEMATIC_DOWNLOADING_IMAGE;
+ this.fetchLogs(container.Name, function (err) {
+ }.bind(this));
+
_containers[container.Name] = container;
callback(null, container);
}
- });
+ }.bind(this));
},
fetchAllContainers: function (callback) {
var self = this;
@@ -305,57 +328,45 @@ var ContainerStore = assign(EventEmitter.prototype, {
});
},
fetchLogs: function (name, callback) {
- if (_logs[name]) {
- callback();
- } else {
- _logs[name] = [];
- }
var index = 0;
var self = this;
docker.client().getContainer(name).logs({
- follow: false,
+ follow: true,
stdout: true,
stderr: true,
timestamps: true
}, function (err, stream) {
+ callback(err);
+ if (_streams[name]) {
+ return;
+ }
+ _streams[name] = stream;
if (err) {
return;
}
+ _logs[name] = [];
stream.setEncoding('utf8');
+ var timeout;
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);
+ 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)));
}
index += 1;
});
- stream.on('end', function (buf) {
- self.emit(self.SERVER_LOGS_EVENT, name);
- callback();
- 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;
- });
- });
+ stream.on('end', function () {
+ delete _streams[name];
+ console.log('end', name);
});
});
},
@@ -395,11 +406,15 @@ var ContainerStore = assign(EventEmitter.prototype, {
}
});
},
- updateContainer: function (name, data) {
+ updateContainer: function (name, data, callback) {
_muted[name] = true;
+ if (!data.name) {
+ data.name = data.Name;
+ }
var fullData = assign(_containers[name], data);
+ console.log(fullData);
this._createContainer(name, fullData, function (err) {
- this.emit(this.CLIENT_CONTAINER_EVENT, name);
+ callback(err);
_muted[name] = false;
}.bind(this));
},
diff --git a/app/ContainerUtil.js b/app/ContainerUtil.js
index 41b9a7cca3..1b396b0298 100644
--- a/app/ContainerUtil.js
+++ b/app/ContainerUtil.js
@@ -1,4 +1,5 @@
var _ = require('underscore');
+var docker = require('./docker');
var ContainerUtil = {
env: function (container) {
@@ -10,6 +11,26 @@ var ContainerUtil = {
var splits = [env.slice(0, i), env.slice(i + 1)];
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;
}
};
diff --git a/app/Setup.react.js b/app/Setup.react.js
index 08561310e5..5c07232533 100644
--- a/app/Setup.react.js
+++ b/app/Setup.react.js
@@ -125,7 +125,7 @@ var setupSteps = [
boot2docker.ip(function (err, ip) {
if (err) {callback(err); return;}
console.log('Setting host IP to: ' + ip);
- // Docker.setHost(ip);
+ docker.setHost(ip);
callback(err);
});
},
@@ -147,6 +147,8 @@ var Setup = React.createClass({
radial =
;
} else if (this.state.error) {
radial = ;
+ } else {
+ radial = ;
}
if (this.state.error) {
return (
diff --git a/app/styles/container-modal.less b/app/styles/container-modal.less
index 77b5975319..295f9f25f7 100644
--- a/app/styles/container-modal.less
+++ b/app/styles/container-modal.less
@@ -1,7 +1,7 @@
.create-modal {
@modal-padding: 32px;
@search-width: 372px;
- @custom-width: 270px;
+ @custom-width: 0;
.modal-dialog {
margin-top: 80px;
width: calc(@modal-padding + @search-width + 2 * @modal-padding + @custom-width);
@@ -20,12 +20,6 @@
flex-direction: row;
padding: 32px 32px;
- aside.custom {
- flex: 0 auto;
- padding-left: 32px;
- min-width: 270px;
- }
-
.popover {
width: 180px;
text-align: center;
@@ -62,12 +56,8 @@
section.search {
min-width: 404px;
- padding-right: 32px;
- border-right: 1px solid #eee;
.question {
- color: @gray-lightest;
- font-size: 10px;
text-align: right;
}
diff --git a/app/styles/containers.less b/app/styles/containers.less
index 6882fb166a..4e00a55783 100644
--- a/app/styles/containers.less
+++ b/app/styles/containers.less
@@ -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 {
+ box-sizing: border-box;
height: 100%;
display: flex;
flex-direction: column;
@@ -349,6 +464,13 @@
}
}
+ .container-name {
+ margin-bottom: 20px;
+ input {
+ width: 20%;
+ }
+ }
+
.env-vars-labels {
width: 100%;
font-size: 12px;
diff --git a/app/styles/main.less b/app/styles/main.less
index e4a5514222..81fe584909 100644
--- a/app/styles/main.less
+++ b/app/styles/main.less
@@ -48,6 +48,11 @@ html, body {
}
}
+.question {
+ color: @gray-lightest;
+ font-size: 10px;
+}
+
.popover {
font-family: 'Clear Sans', sans-serif;
color: @gray-normal;
diff --git a/browser/main.js b/browser/main.js
index 9e7fc065a9..21bec8ecd9 100644
--- a/browser/main.js
+++ b/browser/main.js
@@ -17,6 +17,7 @@ if (argv.test) {
}
process.env.NODE_PATH = __dirname + '/../node_modules';
+process.chdir(path.join(__dirname, '..'));
app.on('activate-with-no-open-windows', function () {
if (mainWindow) {
@@ -98,7 +99,6 @@ app.on('ready', function() {
});
ipc.on('vm', function (event, arg) {
- console.log('save vm', arg);
saveVMOnQuit = arg;
});