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 ( +
+ {key} + {val.display} +
+ ); }); - 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 ( +
+ {key} + {val.replace(process.env.HOME, '~')} +
+ ); }); return ( @@ -311,10 +438,11 @@ var ContainerDetails = React.createClass({
- View + View +
- Volumes + Volumes
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; });