diff --git a/app/ContainerDetails.react.js b/app/ContainerDetails.react.js index ab950bf739..31e3fcdcee 100644 --- a/app/ContainerDetails.react.js +++ b/app/ContainerDetails.react.js @@ -2,9 +2,13 @@ var _ = require('underscore'); var $ = require('jquery'); var React = require('react/addons'); var Router = require('react-router'); -var ContainerStore = require('./ContainerStore'); -var docker = require('./docker'); var exec = require('exec'); +var path = require('path'); +var remote = require('remote'); +var dialog = remote.require('dialog'); +var ContainerStore = require('./ContainerStore'); +var ContainerUtil = require('./ContainerUtil'); +var docker = require('./docker'); var boot2docker = require('./boot2docker'); var ProgressBar = require('react-bootstrap/ProgressBar'); @@ -22,22 +26,23 @@ 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)); + 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 +57,14 @@ var ContainerDetails = React.createClass({ } this._oldHeight = parent[0].scrollHeight - parent.height(); }, + init: function () { + this.setState({ + 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 +91,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 +115,66 @@ var ContainerDetails = React.createClass({ }); }); }, + handleTerminal: function () { + var container = this.props.container; + var terminal = path.join(process.cwd(), 'resources', 'terminal').replace(/ /g, '\\\\ '); + var cmd = [terminal, boot2docker.command().replace(/ /g, '\\\\ '), 'ssh', '-t', 'sudo', 'docker', 'exec', '-i', '-t', container.Name, 'bash']; + exec(cmd, function (stderr, stdout, code) { + if (code) { + console.log(stderr); + } + }); + }, + 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(); + if (!key.length || !val.length) { + return; + } + envVarList.push(key + '=' + val); + }); + ContainerStore.updateContainer(this.props.container.Name, { + Env: 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 () { + dialog.showMessageBox({ + message: 'Are you sure you want to delete this container?', + buttons: ['Delete', 'Cancel'] + }, function (index) { + if (index === 0) { + ContainerStore.remove(this.props.container.Name, function (err) { + console.error(err); + }); + } + }.bind(this)); + }, render: function () { var self = this; @@ -135,6 +208,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 +237,7 @@ var ContainerDetails = React.createClass({ } else { if (this.state.page === this.PAGE_LOGS) { body = ( -
+
{logs}
@@ -153,16 +245,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'; @@ -186,7 +295,7 @@ var ContainerDetails = React.createClass({ 'btn-action': true, 'only-icon': true, 'active': this.state.page === this.PAGE_LOGS, - disabled: !this.props.container.State.Running + disabled: this.props.container.State.Downloading }); var gearButtonClass = React.addons.classSet({ @@ -194,27 +303,27 @@ var ContainerDetails = React.createClass({ 'btn-action': true, 'only-icon': true, 'active': this.state.page === this.PAGE_SETTINGS, - disabled: !this.props.container.State.Running + disabled: this.props.container.State.Downloading }); return (
-

{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..504bd658d7 100644 --- a/app/ContainerStore.js +++ b/app/ContainerStore.js @@ -3,16 +3,19 @@ var async = require('async'); var assign = require('object-assign'); var Stream = require('stream'); 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'); +var convert = new Convert(); + var _recommended = []; var _containers = {}; var _progress = {}; var _logs = {}; +var _muted = {}; var ContainerStore = assign(EventEmitter.prototype, { CLIENT_CONTAINER_EVENT: 'client_container', @@ -104,28 +107,33 @@ var ContainerStore = assign(EventEmitter.prototype, { div.appendChild(text); return div.innerHTML; }, - _createContainer: function (image, name, callback) { + _createContainer: function (name, containerData, callback) { var existing = docker.client().getContainer(name); var self = this; - existing.remove(function (err, data) { - docker.client().createContainer({ - Image: image, - Tty: false, - name: name, - User: 'root' - }, function (err, container) { - if (err) { - callback(err, null); - return; - } - container.start({ - PublishAllPorts: true - }, function (err) { + 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); + callback(err, null); return; } - self.fetchContainer(name, callback); + 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); + }); + } }); }); }); @@ -181,7 +189,7 @@ var ContainerStore = assign(EventEmitter.prototype, { stream.setEncoding('utf8'); stream.on('data', function (data) {}); stream.on('end', function () { - self._createContainer(container.KitematicDownloadingImage, container.Name, function () {}); + self._createContainer(container.Name, {Image: container.KitematicDownloadingImage}, function () {}); }); }); }); @@ -201,11 +209,20 @@ 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]) { + return; + } delete _containers[container.Name]; this.emit(this.SERVER_CONTAINER_EVENT, container.Name, data.status); } else { this.fetchContainer(data.id, function (err) { + if (err) { + return; + } var container = _.findWhere(_.values(_containers), {Id: data.id}); + if (!container || _muted[container.Name]) { + return; + } this.emit(this.SERVER_CONTAINER_EVENT, container ? container.Name : null, data.status); }.bind(this)); } @@ -227,11 +244,15 @@ var ContainerStore = assign(EventEmitter.prototype, { if (err) { callback(err); } else { + if (container.Config.Image === container.Image.slice(0, 12) || container.Config.Image === container.Image) { + callback(); + return; + } // Fix leading slash in container names 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; @@ -269,7 +290,6 @@ var ContainerStore = assign(EventEmitter.prototype, { async.map(recommended, function (repository, callback) { $.get('https://registry.hub.docker.com/v1/search?q=' + repository, function (data) { var results = data.results; - console.log(repository, data); callback(null, _.find(results, function (r) { return r.name === repository; })); @@ -287,8 +307,9 @@ var ContainerStore = assign(EventEmitter.prototype, { fetchLogs: function (name, callback) { if (_logs[name]) { callback(); + } else { + _logs[name] = []; } - _logs[name] = []; var index = 0; var self = this; docker.client().getContainer(name).logs({ @@ -297,6 +318,9 @@ var ContainerStore = assign(EventEmitter.prototype, { stderr: true, timestamps: true }, function (err, stream) { + if (err) { + return; + } stream.setEncoding('utf8'); stream.on('data', function (buf) { // Every other message is a header @@ -304,11 +328,11 @@ var ContainerStore = assign(EventEmitter.prototype, { 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 (buf) { + self.emit(self.SERVER_LOGS_EVENT, name); callback(); docker.client().getContainer(name).logs({ follow: true, @@ -317,6 +341,9 @@ var ContainerStore = assign(EventEmitter.prototype, { timestamps: true, tail: 0 }, function (err, stream) { + if (err) { + return; + } stream.setEncoding('utf8'); stream.on('data', function (buf) { // Every other message is a header @@ -345,10 +372,13 @@ var ContainerStore = assign(EventEmitter.prototype, { self._createPlaceholderContainer(imageName, containerName, function (err, container) { _containers[containerName] = container; self.emit(self.CLIENT_CONTAINER_EVENT, containerName, 'create'); + _muted[containerName] = true; _progress[containerName] = 0; self._pullImage(repository, tag, function () { - self._createContainer(imageName, containerName, function (err, container) { + self._createContainer(containerName, {Image: imageName}, function (err, container) { delete _progress[containerName]; + _muted[containerName] = false; + self.emit(self.CLIENT_CONTAINER_EVENT, containerName); }); }, function (progress) { _progress[containerName] = progress; @@ -358,13 +388,61 @@ var ContainerStore = assign(EventEmitter.prototype, { }); } else { // If not then directly create the container - self._createContainer(imageName, containerName, function (err, container) { + self._createContainer(containerName, {Image: imageName}, function (err, container) { self.emit(ContainerStore.CLIENT_CONTAINER_EVENT, containerName, 'create'); callback(null, containerName); }); } }); }, + updateContainer: function (name, data) { + _muted[name] = true; + var fullData = assign(_containers[name], data); + this._createContainer(name, fullData, function (err) { + this.emit(this.CLIENT_CONTAINER_EVENT, name); + _muted[name] = false; + }.bind(this)); + }, + 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; + } + }); + } + }); + } + }, containers: function() { return _containers; }, @@ -373,16 +451,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/Containers.react.js b/app/Containers.react.js index 45ab3f9fd5..45789f8db0 100644 --- a/app/Containers.react.js +++ b/app/Containers.react.js @@ -52,7 +52,6 @@ var Containers = React.createClass({ sorted: ContainerStore.sorted() }); if (status === 'create') { - console.log('transition'); this.transitionTo('container', {name: name}); } }, diff --git a/app/Radial.react.js b/app/Radial.react.js index c91c898445..3eda034f15 100644 --- a/app/Radial.react.js +++ b/app/Radial.react.js @@ -12,7 +12,8 @@ var Radial = React.createClass({ } var classes = React.addons.classSet({ 'radial-progress': true, - 'radial-spinner': this.props.spin + 'radial-spinner': this.props.spin, + 'radial-negative': this.props.error }); return (
diff --git a/app/Setup.react.js b/app/Setup.react.js index 9f8661eb9e..16f8928b31 100644 --- a/app/Setup.react.js +++ b/app/Setup.react.js @@ -14,9 +14,10 @@ var ContainerStore = require('./ContainerStore.js'); var setupSteps = [ { run: function (callback, progressCallback) { + console.log(util.supportDir()); var installed = virtualbox.installed(); if (!installed) { - util.download('https://s3.amazonaws.com/kite-installer/' + virtualbox.INSTALLER_FILENAME, path.join(process.cwd(), 'resources', virtualbox.INSTALLER_FILENAME), virtualbox.INSTALLER_CHECKSUM, function (err) { + util.download('https://s3.amazonaws.com/kite-installer/' + virtualbox.INSTALLER_FILENAME, path.join(util.supportDir(), virtualbox.INSTALLER_FILENAME), virtualbox.INSTALLER_CHECKSUM, function (err) { if (err) {callback(err); return;} virtualbox.install(function (err) { if (!virtualbox.installed()) { @@ -144,15 +145,24 @@ var Setup = React.createClass({ var radial; if (this.state.progress) { radial = ; - } else { - radial = ; + } else if (this.state.error) { + radial = ; + } + if (this.state.error) { + return ( +
+ {radial} +

Error: {this.state.error}

+
+ ); + } else { + return ( +
+ {radial} +

{this.state.message}

+
+ ); } - return ( -
- {radial} -

{this.state.message}

-
- ); }, componentWillMount: function () { this.setState({}); @@ -160,10 +170,12 @@ var Setup = React.createClass({ componentDidMount: function () { var self = this; this.setup(function (err) { - boot2docker.ip(function (err, ip) { - docker.setHost(ip); - self.transitionTo('containers'); - }); + if (!err) { + boot2docker.ip(function (err, ip) { + docker.setHost(ip); + self.transitionTo('containers'); + }); + } }); }, setup: function (callback) { @@ -188,7 +200,7 @@ var Setup = React.createClass({ // if any of the steps fail console.log('Kitematic setup failed at step ' + currentStep); console.log(err); - self.setState({error: err}); + self.setState({error: err.message}); callback(err); } else { // Setup Finished diff --git a/app/boot2docker.js b/app/boot2docker.js index 1c90bfe105..8e64baedc4 100644 --- a/app/boot2docker.js +++ b/app/boot2docker.js @@ -54,7 +54,13 @@ var Boot2Docker = { return path.join(process.cwd(), 'resources', 'boot2docker-' + this.version()); }, exists: function (callback) { - cmdExec([Boot2Docker.command(), 'info'], callback); + cmdExec([Boot2Docker.command(), 'info'], function (err, out) { + if (err) { + callback(null, false); + } else { + callback(null, true); + } + }); }, status: function (callback) { cmdExec([Boot2Docker.command(), 'status'], function (err, out) { 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/main.js b/app/main.js index e508dc3261..52ec623ddc 100644 --- a/app/main.js +++ b/app/main.js @@ -24,7 +24,7 @@ Bugsnag.releaseStage = process.env.NODE_ENV === 'development' ? 'development' : Bugsnag.notifyReleaseStages = []; Bugsnag.appVersion = app.getVersion(); -if (window.location.hash === '#/') { +if (!window.location.hash.length || window.location.hash === '#/') { router.run(function (Handler) { React.render(, document.body); }); diff --git a/app/styles/container-modal.less b/app/styles/container-modal.less index 6fd190d0e7..77b5975319 100644 --- a/app/styles/container-modal.less +++ b/app/styles/container-modal.less @@ -10,7 +10,7 @@ //box-shadow: 0 3px 15px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.10); border: none; //1px solid #ccc; - height: 650px; + height: 610px; display: flex; } @@ -31,7 +31,7 @@ text-align: center; .popover-content { - max-height: 300px; + max-height: 160px; padding: 0; overflow: auto; } diff --git a/app/styles/containers.less b/app/styles/containers.less index a2719b89aa..6882fb166a 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; } - } + }*/ } } @@ -61,6 +61,7 @@ overflow-y: scroll; overflow-x: hidden; box-sizing: border-box; + max-width: 280px; &.sep { border-top: 1px solid #eee; @@ -329,25 +330,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/setup.less b/app/styles/setup.less index f1b5324c3b..8537b00717 100644 --- a/app/styles/setup.less +++ b/app/styles/setup.less @@ -3,6 +3,9 @@ text-align: center; p { + &.error { + color: @brand-danger; + } margin-top: 20px; } } 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/app/util.js b/app/util.js index 6df25646c6..78a4c61035 100644 --- a/app/util.js +++ b/app/util.js @@ -6,9 +6,23 @@ var progress = require('request-progress'); var exec = require('exec'); var Util = { + supportDir: function (callback) { + var dirs = ['Application\ Support', 'Kitematic']; + var acc = process.env.HOME; + dirs.forEach(function (d) { + acc = path.join(acc, d); + if (!fs.existsSync(acc)) { + fs.mkdirSync(acc); + } + }); + return acc; + }, download: function (url, filename, checksum, callback, progressCallback) { var doDownload = function () { - progress(request(url), { + progress(request({ + uri: url, + rejectUnauthorized: false + }), { throttle: 250 }).on('progress', function (state) { progressCallback(state.percent); diff --git a/app/virtualbox.js b/app/virtualbox.js index 5516643348..a7192793de 100644 --- a/app/virtualbox.js +++ b/app/virtualbox.js @@ -2,6 +2,7 @@ var fs = require('fs'); var exec = require('exec'); var path = require('path'); var async = require('async'); +var util = require('./util'); var VirtualBox = { REQUIRED_VERSION: '4.3.18', @@ -16,7 +17,7 @@ var VirtualBox = { }, install: function (callback) { // -W waits for the process to close before finishing. - exec('open -W ' + path.join(process.cwd(), 'resources', this.INSTALLER_FILENAME).replace(' ', '\\ '), function (stderr, stdout, code) { + exec('open -W ' + path.join(util.supportDir(), this.INSTALLER_FILENAME).replace(' ', '\\ '), function (stderr, stdout, code) { if (code) { callback(stderr); return; 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", diff --git a/resources/terminal b/resources/terminal index 592e3c9fa9..7ab0de25de 100755 --- a/resources/terminal +++ b/resources/terminal @@ -9,8 +9,6 @@ end try return doesExist EOF` -echo $ITERM_EXISTS - if [ $ITERM_EXISTS == "true" ]; then osascript > /dev/null <