diff --git a/app/ContainerDetails.react.js b/app/ContainerDetails.react.js index ab950bf739..120bab6451 100644 --- a/app/ContainerDetails.react.js +++ b/app/ContainerDetails.react.js @@ -3,6 +3,7 @@ var $ = require('jquery'); var React = require('react/addons'); var Router = require('react-router'); var ContainerStore = require('./ContainerStore'); +var ContainerUtil = require('./ContainerUtil'); var docker = require('./docker'); var exec = require('exec'); var boot2docker = require('./boot2docker'); @@ -22,22 +23,28 @@ var ContainerDetails = React.createClass({ getInitialState: function () { return { logs: [], - page: this.PAGE_LOGS + page: this.PAGE_LOGS, + env: {}, + pendingEnv: {} }; }, componentWillReceiveProps: function () { - this.setState({ - page: this.PAGE_LOGS - }); - ContainerStore.fetchLogs(this.getParams().name, function () { - this.updateLogs(); - }.bind(this)); + // active container changes + if (this.state.page === this.PAGE_SETTINGS) { + + } + console.log(this.props.container); + this.init(); + }, + componentWillMount: function () { + this.init(); }, componentDidMount: function () { ContainerStore.on(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress); ContainerStore.on(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs); }, componentWillUnmount: function () { + // app close ContainerStore.removeListener(ContainerStore.SERVER_PROGRESS_EVENT, this.updateProgress); ContainerStore.removeListener(ContainerStore.SERVER_LOGS_EVENT, this.updateLogs); }, @@ -52,6 +59,15 @@ var ContainerDetails = React.createClass({ } this._oldHeight = parent[0].scrollHeight - parent.height(); }, + init: function () { + this.setState({ + page: this.PAGE_LOGS, + env: ContainerUtil.env(ContainerStore.container(this.getParams().name)) + }); + ContainerStore.fetchLogs(this.getParams().name, function () { + this.updateLogs(); + }.bind(this)); + }, updateLogs: function (name) { if (name && name !== this.getParams().name) { return; @@ -78,7 +94,7 @@ var ContainerDetails = React.createClass({ page: this.PAGE_SETTINGS }); }, - handleClick: function (name) { + handleView: function () { var container = this.props.container; boot2docker.ip(function (err, ip) { var ports = _.map(container.NetworkSettings.Ports, function (value, key) { @@ -102,6 +118,46 @@ var ContainerDetails = React.createClass({ }); }); }, + handleSaveEnvVar: function () { + var $rows = $('.env-vars .keyval-row'); + var envVarList = []; + $rows.each(function () { + var key = $(this).find('.key').val(); + var val = $(this).find('.val').val(); + envVarList.push(key + '=' + val); + }); + console.log(envVarList); + }, + handleAddPendingEnvVar: function () { + var newKey = $('#new-env-key').val(); + var newVal = $('#new-env-val').val(); + var newEnv = {}; + newEnv[newKey] = newVal; + this.setState({ + pendingEnv: _.extend(this.state.pendingEnv, newEnv) + }); + $('#new-env-key').val(''); + $('#new-env-val').val(''); + }, + handleRemoveEnvVar: function (key) { + var newEnv = _.omit(this.state.env, key); + this.setState({ + env: newEnv + }); + }, + handleRemovePendingEnvVar: function (key) { + var newEnv = _.omit(this.state.pendingEnv, key); + this.setState({ + pendingEnv: newEnv + }); + }, + handleDeleteContainer: function () { + var container = this.props.container; + var name = container.Name.replace('/', ''); + ContainerStore.remove(name, function (err) { + console.error(err); + }); + }, render: function () { var self = this; @@ -135,6 +191,25 @@ var ContainerDetails = React.createClass({ button = View; } + var envVars = _.map(this.state.env, function (val, key) { + return ( +
+ + + +
+ ); + }); + var pendingEnvVars = _.map(this.state.pendingEnv, function (val, key) { + return ( +
+ + + +
+ ); + }); + var body; if (this.props.container.State.Downloading) { body = ( @@ -145,7 +220,7 @@ var ContainerDetails = React.createClass({ } else { if (this.state.page === this.PAGE_LOGS) { body = ( -
+
{logs}
@@ -153,16 +228,33 @@ var ContainerDetails = React.createClass({ ); } else { body = ( -
+
+

Container Name

+ +

Environment Variables

+
+
KEY
+
VALUE
+
+
+ {envVars} + {pendingEnvVars} +
+ + + +
+
+ Save +

Delete Container

+ Delete Container
); } } - var name = this.props.container.Name; - var image = this.props.container.Config.Image; var disabledClass = ''; if (!this.props.container.State.Running) { disabledClass = 'disabled'; @@ -201,20 +293,20 @@ var ContainerDetails = React.createClass({
-

{name}

{state}

Image

{image}

+

{this.props.container.Name}

{state}

Image

{this.props.container.Config.Image}

- View + View
diff --git a/app/ContainerDetailsSettings.react.js b/app/ContainerDetailsSettings.react.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/ContainerModal.react.js b/app/ContainerModal.react.js index 8fff6ddde7..a8f45c03b9 100644 --- a/app/ContainerModal.react.js +++ b/app/ContainerModal.react.js @@ -10,6 +10,8 @@ var MenuItem = require('react-bootstrap/MenuItem'); var RetinaImage = require('react-retina-image'); var ContainerStore = require('./ContainerStore'); +var OverlayTrigger = require('react-bootstrap/OverlayTrigger'); +var Popover = require('react-bootstrap/Popover'); var ContainerModal = React.createClass({ _searchRequest: null, @@ -152,8 +154,7 @@ var ContainerModal = React.createClass({
diff --git a/app/ContainerStore.js b/app/ContainerStore.js index c8701934cd..3a6fab989b 100644 --- a/app/ContainerStore.js +++ b/app/ContainerStore.js @@ -6,6 +6,7 @@ var Convert = require('ansi-to-html'); var convert = new Convert(); var docker = require('./docker'); var registry = require('./registry'); +var ContainerUtil = require('./ContainerUtil'); var $ = require('jquery'); var _ = require('underscore'); @@ -104,16 +105,12 @@ var ContainerStore = assign(EventEmitter.prototype, { div.appendChild(text); return div.innerHTML; }, - _createContainer: function (image, name, callback) { + _createContainer: function (name, data, callback) { var existing = docker.client().getContainer(name); var self = this; + data.name = name; existing.remove(function (err, data) { - docker.client().createContainer({ - Image: image, - Tty: false, - name: name, - User: 'root' - }, function (err, container) { + docker.client().createContainer(data, function (err, container) { if (err) { callback(err, null); return; @@ -130,6 +127,79 @@ var ContainerStore = assign(EventEmitter.prototype, { }); }); }, + updateContainer: function (name, data) { + var fullData = assign(this._containers[name], data); + this._createContainer(name, fullData, function (err) { + console.log(err); + }); + }, + rename: function (name, newName, callback) { + var existing = docker.client().getContainer(name); + var existingImage = existing.Image; + var self = this; + existing.remove(function (err, data) { + docker.client().createContainer({ + Image: existingImage, + Tty: false, + name: newName, + User: 'root' + }, function (err, container) { + if (err) { + callback(err, null); + return; + } + container.start({ + PublishAllPorts: true + }, function (err) { + if (err) { + callback(err); + return; + } + self.fetchContainer(newName, callback); + }); + }); + }); + }, + remove: function (name, callback) { + var self = this; + var existing = docker.client().getContainer(name); + if (_containers[name].State.Paused) { + existing.unpause(function (err) { + if (err) { + callback(err); + return; + } else { + existing.kill(function (err) { + if (err) { + callback(err); + return; + } else { + existing.remove(function (err) { + if (err) { + callback(err); + return; + } + }); + } + }); + } + }); + } else { + existing.kill(function (err) { + if (err) { + callback(err); + return; + } else { + existing.remove(function (err) { + if (err) { + callback(err); + return; + } + }); + } + }); + } + }, _createPlaceholderContainer: function (imageName, name, callback) { var self = this; this._pullScratchImage(function (err) { @@ -231,7 +301,7 @@ var ContainerStore = assign(EventEmitter.prototype, { container.Name = container.Name.replace('/', ''); // Add Downloading State (stored in environment variables) to containers for Kitematic - var env = _.object(container.Config.Env.map(function (e) { return e.split('='); })); + var env = ContainerUtil.env(container); container.State.Downloading = !!env.KITEMATIC_DOWNLOADING; container.KitematicDownloadingImage = env.KITEMATIC_DOWNLOADING_IMAGE; @@ -373,16 +443,7 @@ var ContainerStore = assign(EventEmitter.prototype, { }, sorted: function () { return _.values(_containers).sort(function (a, b) { - var active = function (container) { - return container.State.Running || container.State.Restarting || container.State.Downloading; - }; - if (active(a) && !active(b)) { - return -1; - } else if (!active(a) && active(b)) { - return 1; - } else { - return a.Name.localeCompare(b.Name); - } + return a.Name.localeCompare(b.Name); }); }, recommended: function () { diff --git a/app/ContainerUtil.js b/app/ContainerUtil.js new file mode 100644 index 0000000000..41b9a7cca3 --- /dev/null +++ b/app/ContainerUtil.js @@ -0,0 +1,16 @@ +var _ = require('underscore'); + +var ContainerUtil = { + env: function (container) { + if (!container || !container.Config || !container.Config.Env) { + return {}; + } + return _.object(container.Config.Env.map(function (env) { + var i = env.indexOf('='); + var splits = [env.slice(0, i), env.slice(i + 1)]; + return splits; + })); + } +}; + +module.exports = ContainerUtil; diff --git a/app/images/paused.png b/app/images/paused.png index 221adf9f1b..225cd93ec4 100644 Binary files a/app/images/paused.png and b/app/images/paused.png differ diff --git a/app/images/paused@2x.png b/app/images/paused@2x.png index dea41b1881..567492f747 100644 Binary files a/app/images/paused@2x.png and b/app/images/paused@2x.png differ diff --git a/app/images/runningwave.png b/app/images/runningwave.png index 9833ff0ac1..d65fc18e13 100644 Binary files a/app/images/runningwave.png and b/app/images/runningwave.png differ diff --git a/app/images/runningwave@2x.png b/app/images/runningwave@2x.png index cfda2c62a4..bb2a5ef49f 100644 Binary files a/app/images/runningwave@2x.png and b/app/images/runningwave@2x.png differ diff --git a/app/images/still-white.png b/app/images/still-white.png new file mode 100644 index 0000000000..ec950d5c24 Binary files /dev/null and b/app/images/still-white.png differ diff --git a/app/images/still-white@2x.png b/app/images/still-white@2x.png new file mode 100644 index 0000000000..64110715fe Binary files /dev/null and b/app/images/still-white@2x.png differ diff --git a/app/images/stopped.png b/app/images/stopped.png index 2b64d4a3cd..420b832ac7 100644 Binary files a/app/images/stopped.png and b/app/images/stopped.png differ diff --git a/app/images/stopped@2x.png b/app/images/stopped@2x.png index 776f0c8aea..0c19121c59 100644 Binary files a/app/images/stopped@2x.png and b/app/images/stopped@2x.png differ diff --git a/app/images/wavy-white.png b/app/images/wavy-white.png new file mode 100644 index 0000000000..066b750d35 Binary files /dev/null and b/app/images/wavy-white.png differ diff --git a/app/images/wavy-white@2x.png b/app/images/wavy-white@2x.png new file mode 100644 index 0000000000..3d9a4287c4 Binary files /dev/null and b/app/images/wavy-white@2x.png differ diff --git a/app/index.html b/app/index.html index 5b3d4ba12d..a97ba8b788 100644 --- a/app/index.html +++ b/app/index.html @@ -3,6 +3,7 @@ + Kitematic diff --git a/app/styles/containers.less b/app/styles/containers.less index a2719b89aa..ef62d65a91 100644 --- a/app/styles/containers.less +++ b/app/styles/containers.less @@ -41,7 +41,7 @@ .create { flex: 1 auto; text-align: right; - .btn { + /*.btn { margin-top: 4px; padding: 4px 7px; font-size: 16px; @@ -51,7 +51,7 @@ top: 3px; left: 1px; } - } + }*/ } } @@ -329,25 +329,55 @@ width: 300px; } - .details-logs { + .details-panel { flex: 1; overflow: auto; - h4 { - font-size: 14px; - margin-top: 16px; - margin-left: 40px; - } .logs { -webkit-user-select: text; font-family: Menlo; font-size: 12px; - padding: 18px 45px; + padding: 18px 35px; color: lighten(@gray-normal, 6%); white-space: pre-wrap; p { margin: 0 6px; } } + .settings { + padding: 18px 35px; + } + } + + .env-vars-labels { + width: 100%; + font-size: 12px; + color: @gray-lightest; + margin-left: 5px; + margin-bottom: 5px; + .label-key { + display: inline-block; + margin-right: 30px; + width: 20%; + } + .label-val { + display: inline-block; + width: 40%; + } + } + .env-vars { + margin-bottom: 20px; + .keyval-row { + margin-bottom: 5px; + } + input { + margin-right: 30px; + &.key { + width: 20%; + } + &.val { + width: 40%; + } + } } } } diff --git a/app/styles/main.less b/app/styles/main.less index edf89fd2e3..f93e70af99 100644 --- a/app/styles/main.less +++ b/app/styles/main.less @@ -3,7 +3,6 @@ @import "clearsans.less"; @import "theme.less"; @import "icons.less"; -@import "icons-filled.less"; @import "retina.less"; @import "setup.less"; @import "radial.less"; @@ -42,11 +41,14 @@ html, body { width: 7px; border-radius: 8px; background-color: rgba(0,0,0,0.2); + + &:hover { + background-color: rgba(0,0,0,0.25); + } } .popover { font-family: 'Clear Sans', sans-serif; - color: @gray-normal; font-weight: 400; font-size: 13px; diff --git a/app/styles/theme.less b/app/styles/theme.less index ac9d02e910..faa71737d8 100644 --- a/app/styles/theme.less +++ b/app/styles/theme.less @@ -5,6 +5,10 @@ @import "bootstrap/variables.less"; @import "bootstrap/mixins.less"; +h3 { + font-size: 14px; + color: @gray-darkest; +} h4 { font-size: 13px; @@ -12,6 +16,30 @@ h4 { font-weight: 400; } +.popover-content { + color: @gray-normal; + font-size: 13px; +} + +input[type="text"] { + &.line { + border: 0; + border-bottom: 1px solid @gray-lightest; + color: @gray-normal; + font-weight: 300; + padding: 5px; + transition: all 0.1s; + &:focus { + outline: 0; + border-bottom: 1px solid @brand-action; + } + &::-webkit-input-placeholder { + color: #ddd; + font-weight: 300; + } + } +} + // // Buttons // -------------------------------------------------- @@ -63,10 +91,17 @@ h4 { } .btn-group { + &.tabs { + .btn { + padding: 6px 14px 6px 14px; + } + } .btn { .icon-dropdown { &.icon:before { - top: 7px; + position: relative; + font-size: 10px; + top: -2px; margin-left: 0px; margin-right: 4px; } @@ -88,6 +123,13 @@ h4 { height: 32px; cursor: default; + &.small { + height: 22px; + .icon { + font-size: 10px; + } + } + .content { position: relative; top: -4px; @@ -95,6 +137,14 @@ h4 { margin-right: 5px; } + .icon-dropdown { + &.icon:before { + font-size: 10px; + position: relative; + top: -2px; + } + } + .icon { position: relative; font-size: 16px; @@ -112,12 +162,22 @@ h4 { box-shadow: none; outline: none !important; } + + &.only-icon { + padding: 6px 7px 6px 7px; + &.small { + padding: 2px 5px 3px 5px; + } + } } // Apply the mixin to the buttons .btn-action { .btn-styles(@brand-action); } +.btn-positive { + .btn-styles(@brand-positive); +} .btn-default { .btn-styles(@btn-default-bg); } .btn-primary { .btn-styles(@btn-primary-bg); } .btn-success { .btn-styles(@btn-success-bg); } diff --git a/package.json b/package.json index 52e6f7c3f8..757d5c0888 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "minimist": "^1.1.0", "moment": "2.8.1", "node-uuid": "1.4.1", + "object-assign": "^2.0.0", "open": "0.0.5", "react": "^0.12.2", "react-bootstrap": "^0.13.2",