diff --git a/.gitignore b/.gitignore index d35cfedc57..d9662ee9f5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ resources/boot2docker* # Cache cache + +resources/settings* diff --git a/browser/main.js b/browser/main.js index ee51d8d578..c4100e1cf2 100644 --- a/browser/main.js +++ b/browser/main.js @@ -11,7 +11,7 @@ var BrowserWindow = require('browser-window'); var ipc = require('ipc'); var argv = require('minimist')(process.argv); -var saveVMOnQuit = false; +var settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8')); process.env.NODE_PATH = __dirname + '/../node_modules'; process.env.RESOURCES_PATH = __dirname + '/../resources'; @@ -41,10 +41,12 @@ app.on('ready', function() { show: false }); + var saveVMOnQuit = false; + if (argv.test) { - mainWindow.loadUrl('file://' + __dirname + '/../tests/tests.html'); + mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, '..', 'tests/tests.html'))); } else { - mainWindow.loadUrl('file://' + __dirname + '/../build/index.html'); + mainWindow.loadUrl(path.normalize('file://' + path.join(__dirname, '..', 'build/index.html'))); app.on('will-quit', function (e) { if (saveVMOnQuit) { exec('VBoxManage controlvm boot2docker-vm savestate', function (stderr, stdout, code) {}); diff --git a/gulp b/gulp deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/gulpfile.js b/gulpfile.js index 546108ce42..b167e2b8fc 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -17,12 +17,15 @@ var packagejson = require('./package.json'); var dependencies = Object.keys(packagejson.dependencies); var devDependencies = Object.keys(packagejson.devDependencies); +var isBeta = process.argv.indexOf('--beta') !== -1; var options = { dev: process.argv.indexOf('release') === -1 && process.argv.indexOf('test') === -1, test: process.argv.indexOf('test') !== -1, integration: process.argv.indexOf('--integration') !== -1, - filename: 'Kitematic.app', - name: 'Kitematic' + beta: isBeta, + filename: isBeta ? 'Kitematic (Beta).app' : 'Kitematic.app', + name: isBeta ? 'Kitematic (Beta)' : 'Kitematic', + icon: isBeta ? 'kitematic-beta.icns' : 'kitematic.icns' }; gulp.task('js', function () { @@ -83,11 +86,13 @@ gulp.task('dist', function (cb) { 'cp -R ./cache/Atom.app ./dist/osx/<%= filename %>', 'mv ./dist/osx/<%= filename %>/Contents/MacOS/Atom ./dist/osx/<%= filename %>/Contents/MacOS/<%= name %>', 'mkdir -p ./dist/osx/<%= filename %>/Contents/Resources/app', + 'mkdir -p ./dist/osx/<%= filename %>/Contents/Resources/app/node_modules', 'cp -R browser dist/osx/<%= filename %>/Contents/Resources/app', 'cp package.json dist/osx/<%= filename %>/Contents/Resources/app/', + 'cp settings.json dist/osx/<%= filename %>/Contents/Resources/app/', 'mkdir -p dist/osx/<%= filename %>/Contents/Resources/app/resources', 'cp -v resources/* dist/osx/<%= filename %>/Contents/Resources/app/resources/ || :', - 'cp kitematic.icns dist/osx/<%= filename %>/Contents/Resources/atom.icns', + 'cp <%= icon %> dist/osx/<%= filename %>/Contents/Resources/atom.icns', '/usr/libexec/PlistBuddy -c "Set :CFBundleVersion <%= version %>" dist/osx/<%= filename %>/Contents/Info.plist', '/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName <%= name %>" dist/osx/<%= filename %>/Contents/Info.plist', '/usr/libexec/PlistBuddy -c "Set :CFBundleName <%= name %>" dist/osx/<%= filename %>/Contents/Info.plist', @@ -95,10 +100,11 @@ gulp.task('dist', function (cb) { '/usr/libexec/PlistBuddy -c "Set :CFBundleExecutable <%= name %>" dist/osx/<%= filename %>/Contents/Info.plist' ], { templateData: { - filename: options.filename, - name: options.name, + filename: options.filename.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)'), + name: options.name.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)'), version: packagejson.version, - bundle: 'com.kitematic.app' + bundle: 'com.kitematic.app', + icon: options.icon } })); @@ -107,7 +113,7 @@ gulp.task('dist', function (cb) { 'cp -R node_modules/' + d + ' dist/osx/<%= filename %>/Contents/Resources/app/node_modules/' ], { templateData: { - filename: options.filename + filename: options.filename.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)') } })); }); @@ -119,7 +125,7 @@ gulp.task('sign', function () { try { var signing_identity = fs.readFileSync('./identity', 'utf8').trim(); return gulp.src('').pipe(shell([ - 'codesign --deep --force --verbose --sign "' + signing_identity + '" ' + options.filename + 'codesign --deep --force --verbose --sign "' + signing_identity + '" ' + options.filename.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)') ], { cwd: './dist/osx/' })); @@ -130,7 +136,7 @@ gulp.task('sign', function () { gulp.task('zip', function () { return gulp.src('').pipe(shell([ - 'ditto -c -k --sequesterRsrc --keepParent ' + options.filename + ' ' + options.name + '-' + packagejson.version + '.zip' + 'ditto -c -k --sequesterRsrc --keepParent ' + options.filename.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)') + ' ' + options.name.replace(' ', '\\ ').replace('(','\\(').replace(')','\\)') + '-' + packagejson.version + '.zip' ], { cwd: './dist/osx/' })); diff --git a/images/boot2docker.png b/images/boot2docker.png new file mode 100644 index 0000000000..947896617d Binary files /dev/null and b/images/boot2docker.png differ diff --git a/images/boot2docker@2x.png b/images/boot2docker@2x.png new file mode 100644 index 0000000000..54d2f03695 Binary files /dev/null and b/images/boot2docker@2x.png differ diff --git a/images/virtualbox.png b/images/virtualbox.png new file mode 100644 index 0000000000..7a5f540ffd Binary files /dev/null and b/images/virtualbox.png differ diff --git a/images/virtualbox@2x.png b/images/virtualbox@2x.png new file mode 100644 index 0000000000..ad3436f04e Binary files /dev/null and b/images/virtualbox@2x.png differ diff --git a/kitematic-beta.icns b/kitematic-beta.icns new file mode 100644 index 0000000000..034f487955 Binary files /dev/null and b/kitematic-beta.icns differ diff --git a/package.json b/package.json index 9371f56922..371080b511 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "test": "gulp test --silent", "test:integration": "gulp test --silent --integration", "all-tests": "npm test && npm run integration-tests", - "release": "gulp run release", + "release": "gulp release", + "release:beta": "gulp release --beta", "preinstall": "./deps" }, "licenses": [ @@ -55,7 +56,8 @@ "request": "^2.51.0", "request-progress": "0.3.1", "retina.js": "^1.1.0", - "underscore": "^1.7.0" + "underscore": "^1.7.0", + "rimraf": "^2.2.8" }, "devDependencies": { "browserify": "^6.2.0", @@ -80,7 +82,6 @@ "gulp-uglifyjs": "^0.5.0", "gulp-util": "^3.0.0", "reactify": "^0.15.2", - "rimraf": "^2.2.8", "run-sequence": "^1.0.2", "time-require": "^0.1.2", "vinyl-source-stream": "^0.1.1", diff --git a/src/ContainerDetails.react.js b/src/ContainerDetails.react.js index 8ee442168b..05edc35b24 100644 --- a/src/ContainerDetails.react.js +++ b/src/ContainerDetails.react.js @@ -6,6 +6,8 @@ var exec = require('exec'); var path = require('path'); var assign = require('object-assign'); var remote = require('remote'); +var rimraf = require('rimraf'); +var fs = require('fs'); var dialog = remote.require('dialog'); var ContainerStore = require('./ContainerStore'); var ContainerUtil = require('./ContainerUtil'); @@ -27,7 +29,6 @@ var ContainerDetails = React.createClass({ pendingEnv: {}, ports: {}, defaultPort: null, - volumes: {}, popoverVolumeOpen: false, popoverViewOpen: false, }; @@ -71,19 +72,20 @@ var ContainerDetails = React.createClass({ this._oldHeight = parent[0].scrollHeight - parent.height(); } - var $viewDropdown = $(this.getDOMNode()).find('.dropdown-view'); + var $viewDropdown = $(this.getDOMNode()).find('.dropdown-view > .icon-dropdown'); var $volumeDropdown = $(this.getDOMNode()).find('.dropdown-volume'); var $viewPopover = $(this.getDOMNode()).find('.popover-view'); var $volumePopover = $(this.getDOMNode()).find('.popover-volume'); - if ($viewDropdown.offset() && $volumeDropdown.offset()) { + if ($viewDropdown.offset()) { $viewPopover.offset({ - top: $viewDropdown.offset().top + 32, - left: $viewDropdown.offset().left - ($viewPopover.outerWidth() / 2) + 14 + top: $viewDropdown.offset().top + 27, + left: $viewDropdown.offset().left - ($viewPopover.outerWidth() / 2) + 5 }); - + } + if ($volumeDropdown.offset()) { $volumePopover.offset({ - top: $volumeDropdown.offset().top + 32, + top: $volumeDropdown.offset().top + 33, left: $volumeDropdown.offset().left + $volumeDropdown.outerWidth() - $volumePopover.outerWidth() / 2 - 20 }); } @@ -96,17 +98,16 @@ var ContainerDetails = React.createClass({ this.setState({ progress: ContainerStore.progress(this.getParams().name), env: ContainerUtil.env(container), + page: this.PAGE_LOGS }); var ports = ContainerUtil.ports(container); var webPorts = ['80', '8000', '8080', '3000', '5000', '2368']; - console.log(ports); this.setState({ ports: ports, defaultPort: _.find(_.keys(ports), function (port) { return webPorts.indexOf(port) !== -1; }) }); - console.log(this.state); this.updateLogs(); }, updateLogs: function (name) { @@ -136,8 +137,6 @@ var ContainerDetails = React.createClass({ }, handleView: function () { if (this.state.defaultPort) { - console.log(this.state.defaultPort); - console.log(this.state.ports[this.state.defaultPort].url); exec(['open', this.state.ports[this.state.defaultPort].url], function (err) { if (err) { throw err; } }); @@ -148,14 +147,48 @@ var ContainerDetails = React.createClass({ if (err) { throw err; } }); }, + handleChangeDefaultPort: function (port) { + this.setState({ + defaultPort: port + }); + }, handleViewDropdown: function(e) { this.setState({ popoverViewOpen: !this.state.popoverViewOpen }); }, handleVolumeDropdown: function(e) { - this.setState({ - popoverVolumeOpen: !this.state.popoverVolumeOpen + var self = this; + if (_.keys(this.props.container.Volumes).length) { + exec(['open', path.join(process.env.HOME, 'Kitematic', self.props.container.Name)], function (err) { + if (err) { throw err; } + }); + } + }, + handleChooseVolumeClick: function (dockerVol) { + var self = this; + dialog.showOpenDialog({properties: ['openDirectory', 'createDirectory']}, function (filenames) { + if (!filenames) { + return; + } + var directory = filenames[0]; + if (directory) { + var volumes = _.clone(self.props.container.Volumes); + volumes[dockerVol] = directory; + var binds = _.pairs(volumes).map(function (pair) { + return pair[1] + ':' + pair[0]; + }); + ContainerStore.updateContainer(self.props.container.Name, { + Binds: binds + }, function (err) { + if (err) console.log(err); + }); + } + }); + }, + handleOpenVolumeClick: function (path) { + exec(['open', path], function (err) { + if (err) { throw err; } }); }, handleRestart: function () { @@ -165,16 +198,24 @@ 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']; + var terminal = path.join(process.cwd(), 'resources', 'terminal'); + var cmd = [terminal, boot2docker.command().replace(/ /g, '\\\\\\\\ ').replace(/\(/g, '\\\\\\\\(').replace(/\)/g, '\\\\\\\\)'), 'ssh', '-t', 'sudo', 'docker', 'exec', '-i', '-t', container.Name, 'sh']; exec(cmd, function (stderr, stdout, code) { + console.log(stderr); + console.log(stdout); if (code) { console.log(stderr); } }); }, handleSaveContainerName: function () { + if (newName === this.props.container.Name) { + return; + } var newName = $('#input-container-name').val(); + if (fs.existsSync(path.join(process.env.HOME, 'Kitematic', this.props.container.Name))) { + fs.renameSync(path.join(process.env.HOME, 'Kitematic', this.props.container.Name), path.join(process.env.HOME, 'Kitematic', newName)); + } ContainerStore.updateContainer(this.props.container.Name, { name: newName }, function (err) { @@ -238,6 +279,12 @@ var ContainerDetails = React.createClass({ message: 'Are you sure you want to delete this container?', buttons: ['Delete', 'Cancel'] }, function (index) { + var volumePath = path.join(process.env.HOME, 'Kitematic', this.props.container.Name); + if (fs.existsSync(volumePath)) { + rimraf(volumePath, function (err) { + console.log(err); + }); + } if (index === 0) { ContainerStore.remove(this.props.container.Name, function (err) { console.error(err); @@ -313,7 +360,7 @@ var ContainerDetails = React.createClass({ btn: true, 'btn-action': true, 'with-icon': true, - disabled: this.props.container.State.Restarting + disabled: this.props.container.State.Downloading || this.props.container.State.Restarting }); var viewButtonClass = React.addons.classSet({ @@ -323,6 +370,17 @@ var ContainerDetails = React.createClass({ disabled: !this.props.container.State.Running || !this.state.defaultPort }); + var kitematicVolumes = _.pairs(this.props.container.Volumes).filter(function (pair) { + return pair[1].indexOf(path.join(process.env.HOME, 'Kitematic')) !== -1; + }); + + var volumesButtonClass = React.addons.classSet({ + btn: true, + 'btn-action': true, + 'with-icon': true, + disabled: !kitematicVolumes.length + }); + var textButtonClasses = React.addons.classSet({ 'btn': true, 'btn-action': true, @@ -362,15 +420,19 @@ var ContainerDetails = React.createClass({ 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 = ( -
- -
- ); + if (this.state.progress) { + body = ( +
+

Downloading

+ +
+ ); + } else { + + } } else { if (this.state.page === this.PAGE_LOGS) { body = ( @@ -381,14 +443,19 @@ var ContainerDetails = React.createClass({ ); } else { + var rename = ( +
+

Container Name

+
+ +
+ Save +
+ ); body = (
-

Container Name

-
- -
- Save + {rename}

Environment Variables

KEY
@@ -419,22 +486,41 @@ var ContainerDetails = React.createClass({
{key} {val.display} +
); }); var volumes = _.map(self.props.container.Volumes, function (val, key) { if (!val || val.indexOf(process.env.HOME) === -1) { - val = 'No Host Folder'; + val = No folderChoose; + } else { + val = {val.replace(process.env.HOME, '~')} Choose; } return (
{key} - {val.replace(process.env.HOME, '~')} + {val}
); }); + var view; + if (this.state.defaultPort) { + view = ( +
+ View + +
+ ); + } else { + view = ( +
+ Ports +
+ ); + } + return (
@@ -442,12 +528,9 @@ var ContainerDetails = React.createClass({

{this.props.container.Name}

{state}

Image

{this.props.container.Config.Image}

-
- View - -
+ {view}
Restart @@ -472,7 +555,7 @@ var ContainerDetails = React.createClass({
-
DOCKER FOLDER
+
DOCKER VOLUME
MAC FOLDER
{volumes} diff --git a/src/ContainerModal.react.js b/src/ContainerModal.react.js index 64543d6f5f..27d853dfe2 100644 --- a/src/ContainerModal.react.js +++ b/src/ContainerModal.react.js @@ -82,17 +82,16 @@ var ContainerModal = React.createClass({ } }, handleClick: function (name, event) { + this.props.onRequestHide(); ContainerStore.create(name, 'latest', function (err, containerName) { if (err) { throw err; } - this.props.onRequestHide(); }.bind(this)); }, handleTagClick: function (tag, name, event) { - ContainerStore.create(name, tag, function (err, containerName) { - this.props.onRequestHide(); - }.bind(this)); + this.props.onRequestHide(); + ContainerStore.create(name, tag, function (err, containerName) {}); }, handleDropdownClick: function (name, event) { this.setState({ diff --git a/src/ContainerStore.js b/src/ContainerStore.js index 82f3eaa4e2..74b50a7ef2 100644 --- a/src/ContainerStore.js +++ b/src/ContainerStore.js @@ -20,11 +20,11 @@ var _streams = {}; var _muted = {}; var ContainerStore = assign(EventEmitter.prototype, { - CLIENT_CONTAINER_EVENT: 'client_container', + CLIENT_CONTAINER_EVENT: 'client_container_event', CLIENT_RECOMMENDED_EVENT: 'client_recommended_event', - SERVER_CONTAINER_EVENT: 'server_container', - SERVER_PROGRESS_EVENT: 'server_progress', - SERVER_LOGS_EVENT: 'server_logs', + SERVER_CONTAINER_EVENT: 'server_container_event', + SERVER_PROGRESS_EVENT: 'server_progress_event', + SERVER_LOGS_EVENT: 'server_logs_event', _pullScratchImage: function (callback) { var image = docker.client().getImage('scratch:latest'); image.inspect(function (err, data) { @@ -121,15 +121,24 @@ var ContainerStore = assign(EventEmitter.prototype, { containerData.Image = containerData.Config.Image; } existing.kill(function (err, data) { + if (err) { + console.log(err); + } existing.remove(function (err, data) { + if (err) { + console.log(err); + } docker.client().getImage(containerData.Image).inspect(function (err, data) { + if (err) { + callback(err); + return; + } 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); @@ -311,11 +320,13 @@ var ContainerStore = assign(EventEmitter.prototype, { var self = this; $.ajax({ url: 'https://kitematic.com/recommended.json', + cache: false, dataType: 'json', success: function (res, status) { var recommended = res.recommended; async.map(recommended, function (repository, callback) { $.get('https://registry.hub.docker.com/v1/search?q=' + repository, function (data) { + console.log(data); var results = data.results; callback(null, _.find(results, function (r) { return r.name === repository; @@ -379,38 +390,27 @@ var ContainerStore = assign(EventEmitter.prototype, { var imageName = repository + ':' + tag; var containerName = this._generateName(repository); var image = docker.client().getImage(imageName); - - image.inspect(function (err, data) { - if (!data) { - // Pull image - self._createPlaceholderContainer(imageName, containerName, function (err, container) { - if (err) { - callback(err); - return; - } - _containers[containerName] = container; - self.emit(self.CLIENT_CONTAINER_EVENT, containerName, 'create'); - _muted[containerName] = true; - _progress[containerName] = 0; - self._pullImage(repository, tag, function () { - 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; - self.emit(self.SERVER_PROGRESS_EVENT, containerName); - }); - callback(null, containerName); - }); - } else { - // If not then directly create the container - self._createContainer(containerName, {Image: imageName}, function (err, container) { - self.emit(ContainerStore.CLIENT_CONTAINER_EVENT, containerName, 'create'); - callback(null, containerName); - }); + // Pull image + self._createPlaceholderContainer(imageName, containerName, function (err, container) { + if (err) { + callback(err); + return; } + _containers[containerName] = container; + self.emit(self.CLIENT_CONTAINER_EVENT, containerName, 'create'); + _muted[containerName] = true; + _progress[containerName] = 0; + self._pullImage(repository, tag, function () { + 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; + self.emit(self.SERVER_PROGRESS_EVENT, containerName); + }); + callback(null, containerName); }); }, updateContainer: function (name, data, callback) { @@ -419,10 +419,10 @@ var ContainerStore = assign(EventEmitter.prototype, { data.name = data.Name; } var fullData = assign(_containers[name], data); - console.log(fullData); this._createContainer(name, fullData, function (err) { - callback(err); _muted[name] = false; + this.emit(this.CLIENT_CONTAINER_EVENT, name); + callback(err); }.bind(this)); }, restart: function (name, callback) { diff --git a/src/Main.js b/src/Main.js index e58c34486a..9c5c7dd205 100644 --- a/src/Main.js +++ b/src/Main.js @@ -1,22 +1,26 @@ +var module = require('module'); +require.main.paths.splice(0, 0, process.env.NODE_PATH); + +var remote = require('remote'); +var app = remote.require('app'); +var ipc = require('ipc'); var React = require('react'); var Router = require('react-router'); var RetinaImage = require('react-retina-image'); -var async = require('async'); +var fs = require('fs'); +var path = require('path'); var docker = require('./Docker'); var router = require('./router'); var boot2docker = require('./boot2docker'); var ContainerStore = require('./ContainerStore'); var SetupStore = require('./ContainerStore'); var Menu = require('./Menu'); -var remote = require('remote'); -var app = remote.require('app'); -var ipc = require('ipc'); - var Route = Router.Route; var NotFoundRoute = Router.NotFoundRoute; var DefaultRoute = Router.DefaultRoute; var Link = Router.Link; var RouteHandler = Router.RouteHandler; +var settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8')); if (process.env.NODE_ENV === 'development') { var script = document.createElement('script'); @@ -24,19 +28,26 @@ if (process.env.NODE_ENV === 'development') { script.src = 'http://localhost:35729/livereload.js'; var head = document.getElementsByTagName('head')[0]; head.appendChild(script); +} else { + var bugsnag = require('bugsnag-js'); + bugsnag.apiKey = settingsjson.bugsnag; + bugsnag.autoNotify = true; + bugsnag.releaseStage = process.env.NODE_ENV === 'development' ? 'development' : 'production'; + bugsnag.notifyReleaseStages = ['production']; + bugsnag.appVersion = app.getVersion(); } if (!window.location.hash.length || window.location.hash === '#/') { + router.run(function (Handler) { + React.render(, document.body); + }); SetupStore.run(function (err) { + router.transitionTo('setup'); boot2docker.ip(function (err, ip) { if (err) console.log(err); docker.setHost(ip); - router.transitionTo('containers'); ContainerStore.init(function (err) { - if (err) console.log(err); - router.run(function (Handler) { - React.render(, document.body); - }); + router.transitionTo('containers'); }); }); }); diff --git a/src/Setup.react.js b/src/Setup.react.js index c86f389ed4..8df59eb28e 100644 --- a/src/Setup.react.js +++ b/src/Setup.react.js @@ -6,24 +6,128 @@ var assign = require('object-assign'); var fs = require('fs'); var path = require('path'); var virtualbox = require('./Virtualbox'); -var util = require('./Util'); var SetupStore = require('./SetupStore'); +var RetinaImage = require('react-retina-image'); var Setup = React.createClass({ mixins: [ Router.Navigation ], getInitialState: function () { return { - message: '', - progress: 0 + progress: 0, + name: '' }; }, componentWillMount: function () { SetupStore.on(SetupStore.PROGRESS_EVENT, this.update); + SetupStore.on(SetupStore.STEP_EVENT, this.update); }, componentDidMount: function () { }, update: function () { - + this.setState({ + progress: SetupStore.stepProgress(), + step: SetupStore.stepName() + }); + }, + renderDownloadingVirtualboxStep: function () { + var message = 'Kitematic needs VirtualBox to run containers. VirtualBox is being downloaded from Oracle\'s website.'; + return ( +
+
+
+ +
+ +
+
+
+
+
+

Downloading VirtualBox

+

{message}

+
+
+
+ ); + }, + renderInstallingVirtualboxStep: function () { + var message = 'VirtualBox is being installed. Administrative privileges are required.'; + return ( +
+
+
+ +
+ +
+
+
+
+
+

Installing VirtualBox

+

{message}

+
+
+
+ ); + }, + renderInitBoot2DockerStep: function () { + var message = 'Containers run in a virtual machine provided by Boot2Docker. Kitematic is setting up that Linux VM.'; + return ( +
+
+
+ +
+ +
+
+
+
+
+

Setting up the Docker VM

+

{message}

+
+
+
+ ); + }, + renderStartBoot2DockerStep: function () { + var message = 'Kitematic is starting the Boot2Docker Linux VM.'; + return ( +
+
+
+ +
+ +
+
+
+
+
+

Starting the Docker VM

+

{message}

+
+
+
+ ); + }, + renderStep: function () { + switch(this.state.step) { + case 'downloading_virtualbox': + return this.renderDownloadingVirtualboxStep(); + case 'installing_virtualbox': + return this.renderInstallingVirtualboxStep(); + case 'cleanup_kitematic': + return this.renderInitBoot2DockerStep(); + case 'init_boot2docker': + return this.renderInitBoot2DockerStep(); + case 'start_boot2docker': + return this.renderStartBoot2DockerStep(); + default: + return false; + } }, render: function () { var radial; @@ -34,6 +138,9 @@ var Setup = React.createClass({ } else { radial = ; } + + var step = this.renderStep(); + if (this.state.error) { return (
@@ -42,12 +149,7 @@ var Setup = React.createClass({
); } else { - return ( -
- {radial} -

{this.state.message}

-
- ); + return step; } } }); diff --git a/src/SetupStore.js b/src/SetupStore.js index 3dcf5fbdb1..32b14dc50d 100644 --- a/src/SetupStore.js +++ b/src/SetupStore.js @@ -11,7 +11,7 @@ var packagejson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package var _currentStep = null; var _error = null; -var _progress = null; +var _progress = 0; var SetupStore = assign(EventEmitter.prototype, { PROGRESS_EVENT: 'setup_progress', @@ -48,26 +48,31 @@ var SetupStore = assign(EventEmitter.prototype, { } }, name: 'downloading_virtualbox', - message: 'Downloading Virtualbox', }, installVirtualboxStep: { _install: function (callback) { - exec(['hdiutil', 'attach', path.join(setupUtil.supportDir(), 'VirtualBox-4.3.20-96996-OSX.dmg')], function (stderr, stdout, code) { + console.log('attaching'); + exec(['hdiutil', 'attach', path.join(setupUtil.supportDir(), packagejson['virtualbox-filename'])], function (stderr, stdout, code) { if (code) { callback(stderr); return; } + console.log('Attached.'); var iconPath = path.join(setupUtil.resourceDir(), 'kitematic.icns'); setupUtil.isSudo(function (err, isSudo) { + console.log(isSudo); sudoCmd = isSudo ? ['sudo'] : [path.join(setupUtil.resourceDir(), 'cocoasudo'), '--icon=' + iconPath, '--prompt=Kitematic requires administrative privileges to install VirtualBox and copy itself to the Applications folder.']; sudoCmd.push.apply(sudoCmd, ['installer', '-pkg', '/Volumes/VirtualBox/VirtualBox.pkg', '-target', '/']); exec(sudoCmd, function (stderr, stdout, code) { + console.log(stdout); + console.log('Ran installer.'); if (code) { console.log(stderr); console.log(stdout); callback('Could not install virtualbox.'); } else { exec(['hdiutil', 'detach', '/Volumes/VirtualBox'], function(stderr, stdout, code) { + console.log('detaching'); if (code) { callback(stderr); } else { @@ -101,7 +106,6 @@ var SetupStore = assign(EventEmitter.prototype, { } }, name: 'installing_virtualbox', - message: 'Installing VirtualBox', }, cleanupKitematicStep: { run: function (callback) { @@ -113,7 +117,6 @@ var SetupStore = assign(EventEmitter.prototype, { }); }, name: 'cleanup_kitematic', - message: 'Cleaning up existing Kitematic install...' }, initBoot2DockerStep: { run: function (callback) { @@ -143,7 +146,6 @@ var SetupStore = assign(EventEmitter.prototype, { }); }, name: 'init_boot2docker', - message: 'Setting up the Docker VM...' }, startBoot2DockerStep: { run: function (callback) { @@ -161,31 +163,30 @@ var SetupStore = assign(EventEmitter.prototype, { }); }, name: 'start_boot2docker', - message: 'Starting the Docker VM...' }, - step: function () { - return _currentStep; + stepName: function () { + return _currentStep.name; }, - progress: function () { + stepProgress: function () { return _progress; }, run: function (callback) { var self = this; var steps = [this.downloadVirtualboxStep, this.installVirtualboxStep, this.cleanupKitematicStep, this.initBoot2DockerStep, this.startBoot2DockerStep]; async.eachSeries(steps, function (step, callback) { - console.log(step.name); _currentStep = step; - _progress = null; + _progress = 0; + self.emit(self.STEP_EVENT); + step.run(function (err) { if (err) { callback(err); } else { - self.emit(self.STEP_EVENT); callback(); } }, function (progress) { - self.emit(self.PROGRESS_EVENT, progress); _progress = progress; + self.emit(self.PROGRESS_EVENT, progress); }); }, function (err) { if (err) { diff --git a/src/Virtualbox.js b/src/Virtualbox.js index 446537c461..e224764bf5 100644 --- a/src/Virtualbox.js +++ b/src/Virtualbox.js @@ -2,7 +2,6 @@ var fs = require('fs'); var exec = require('exec'); var path = require('path'); var async = require('async'); -var util = require('./Util'); var VirtualBox = { command: function () { diff --git a/styles/containers.less b/styles/containers.less index 4e00a55783..9011f5aaf3 100644 --- a/styles/containers.less +++ b/styles/containers.less @@ -1,11 +1,11 @@ .popover { &.popover-view { - min-width: 290px; + min-width: 364px; } &.popover-volume { - min-width: 400px; + min-width: 480px; } .popover-content { @@ -53,9 +53,15 @@ flex: 0 auto; } .value-right { - flex: 1 auto; + position: relative; + flex: 0 auto; -webkit-user-select: text; width: 154px; + white-space: nowrap; + a { + overflow: hidden; + text-overflow: ellipsis; + } } } .table-new { @@ -92,6 +98,13 @@ } } + &.ports { + input { + margin-left: 6px; + margin-right: 5px; + } + } + &.volumes { .label-left { min-width: 120px; @@ -99,10 +112,22 @@ .value-left { min-width: 120px; } - .icon { + .icon-arrow-right { color: #aaa; margin: 2px 9px 0; } + .icon-folder-1 { + position: relative; + top: 4px; + font-size: 16px; + padding-right: 4px; + } + .btn-xs { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + height: 22px; + } } } @@ -195,15 +220,14 @@ color: inherit; flex-shrink: 0; cursor: default; - margin: 0px 3px 0px 8px; + margin: 0px 3px 0px 0; outline: none; - padding: 4px 5px; + padding: 4px 13px; &.active { + background: @brand-primary; li { border-bottom: none; - border-radius: 40px; - background: @brand-primary; .name { color: white; } @@ -388,6 +412,11 @@ border-bottom: 1px solid transparent; transition: border-bottom 0.25s; .action { + + a { + -webkit-transition: none; + } + transition: none; flex: 0 auto; margin-right: 24px; } @@ -442,6 +471,7 @@ .details-progress { margin: 26% auto 0; + text-align: center; width: 300px; } diff --git a/styles/radial.less b/styles/radial.less index d2db07aa20..4e989d16f8 100644 --- a/styles/radial.less +++ b/styles/radial.less @@ -12,16 +12,16 @@ .radial-progress { &.radial-spinner { - -webkit-animation: rotating 1.2s linear infinite; + -webkit-animation: rotating 2.4s linear infinite; } - @circle-size: 96px; - @circle-background: transparent; - @inset-size: 92px; + @circle-size: 140px; + @circle-background: #F2F2F2; + @inset-size: 136px; @inset-color: white; @transition-length: 1s; // @percentage-color: #3FD899; - @percentage-font-size: 14px; + @percentage-font-size: 24px; @percentage-text-width: 57px; margin: 0 auto; @@ -70,7 +70,7 @@ line-height: 1; text-align: center; - // color: @percentage-color; + color: @brand-primary; font-weight: 500; font-size: @percentage-font-size; } diff --git a/styles/setup.less b/styles/setup.less index 8537b00717..2d2ac1f5ce 100644 --- a/styles/setup.less +++ b/styles/setup.less @@ -1,6 +1,52 @@ .setup { - margin-top: 25%; - text-align: center; + display: flex; + height: 100%; + width: 100%; + flex-direction: row; + -webkit-app-region: drag; + + .image { + display: flex; + width: 50%; + height: 100%; + flex: 0 auto; + align-items: center; + justify-content: flex-end; + padding-right: 40px; + + .contents { + position: relative; + .detail { + position: absolute; + right: 0; + bottom: 0; + } + } + } + + .desc { + display: flex; + width: 50%; + height: 100%; + + align-items: center; + padding-left: 40px; + + .content { + max-width: 320px; + + h1 { + margin-top: -30px; + font-size: 24px; + } + + p { + font-size: 13px; + color: @gray-normal; + } + } + + } p { &.error { diff --git a/styles/theme.less b/styles/theme.less index faa71737d8..f40f8f5a5b 100644 --- a/styles/theme.less +++ b/styles/theme.less @@ -87,6 +87,13 @@ input[type="text"] { &:disabled, &[disabled] { opacity: 0.5; + background: none; + &.active { + background: none; + color: white; + box-shadow: none; + box-shadow: none; + } } }