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/README.md b/README.md index 854c01e358..70be3f0a25 100755 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ Kitematic's documentation and other information can be found at [http://kitemati ### Development -- `sudo npm install -g less` - `npm install` To run the app in development: diff --git a/browser/main.js b/browser/main.js index 04c8d5f54b..3ebc40d665 100644 --- a/browser/main.js +++ b/browser/main.js @@ -1,13 +1,13 @@ var path = require('path'); var exec = require('exec'); - +var fs = require('fs'); var autoUpdater = require('auto-updater'); var app = require('app'); 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'; @@ -39,10 +39,11 @@ app.on('activate-with-no-open-windows', function () { app.on('ready', function() { var mainWindow = new BrowserWindow(windowOptions); + 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 () { if (saveVMOnQuit) { exec('VBoxManage controlvm boot2docker-vm savestate', function () {}); @@ -63,7 +64,7 @@ app.on('ready', function() { // Auto Updates if (process.env.NODE_ENV !== 'development' && !argv.test) { - autoUpdater.setFeedUrl('https://updates.kitematic.com/releases/latest?version=' + app.getVersion()); + autoUpdater.setFeedUrl('https://updates.kitematic.com/releases/latest?version=' + app.getVersion() + '&beta=' + settingsjson.beta); autoUpdater.on('checking-for-update', function () { console.log('Checking for update...'); 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 e91347baf7..09fb1e3eaf 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,9 @@ "test:integration": "gulp test --silent --integration", "all-tests": "npm test && npm run integration-tests", "release": "gulp run release", + "release:beta": "gulp release --beta", "preinstall": "./deps", - "lint": "jsxhint src/**/*" + "lint": "jsxhint src/" }, "licenses": [ { @@ -33,7 +34,7 @@ ] }, "boot2docker-version": "1.4.1", - "atom-shell-version": "0.21.0", + "atom-shell-version": "0.20.0", "virtualbox-version": "4.3.20", "virtualbox-filename": "VirtualBox-4.3.20-96996-OSX.dmg", "virtualbox-required-version": "4.3.18", @@ -56,7 +57,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", @@ -81,7 +83,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/settings.json b/settings.json new file mode 100644 index 0000000000..ef3ae13771 --- /dev/null +++ b/settings.json @@ -0,0 +1,5 @@ +{ + "bugsnag": "fc51aab02ce9dd1bb6ebc9fe2f4d43d7", + "mixpanel": "114caa6b42de655c4671c4b1e69f1b81", + "beta": true +} diff --git a/src/ContainerDetails.react.js b/src/ContainerDetails.react.js index d7ed9124cb..182a96cff2 100644 --- a/src/ContainerDetails.react.js +++ b/src/ContainerDetails.react.js @@ -5,6 +5,8 @@ var Router = require('react-router'); var exec = require('exec'); var path = require('path'); 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'); @@ -63,17 +65,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) { @@ -121,8 +122,6 @@ var ContainerDetails = React.createClass({ console.log(this.state.ports); console.log(this.state.defaultPort); 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; } }); @@ -133,6 +132,43 @@ var ContainerDetails = React.createClass({ if (err) { throw err; } }); }, + handleChangeDefaultPort: function (port, e) { + if (e.target.checked) { + this.setState({ + defaultPort: null + }); + } else { + this.setState({ + defaultPort: port + }); + } + }, + 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 () { ContainerStore.restart(this.props.container.Name, function (err) { console.log(err); @@ -140,9 +176,11 @@ 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); } @@ -150,6 +188,12 @@ var ContainerDetails = React.createClass({ }, handleSaveContainerName: function () { var newName = $('#input-container-name').val(); + if (newName === this.props.container.Name) { + return; + } + 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) { @@ -213,6 +257,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); @@ -277,7 +327,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({ @@ -287,6 +337,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, @@ -328,11 +389,14 @@ var ContainerDetails = React.createClass({ var body; if (this.props.container.State.Downloading) { - body = ( -
- -
- ); + if (this.state.progress) { + body = ( +
+

Downloading

+ +
+ ); + } } else { if (this.state.page === this.PAGE_HOME) { body = ( @@ -377,16 +441,19 @@ var ContainerDetails = React.createClass({ ); } else { + var rename = ( +
+

Container Name

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

Container Name

-
- -
- Save -
+ {rename}

Environment Variables

@@ -426,24 +493,54 @@ var ContainerDetails = React.createClass({ disabled: this.props.container.State.Downloading }); - /*var tabPortsClasses = React.addons.classSet({ - 'tab': true, - 'active': this.state.page === this.PAGE_PORTS, - disabled: this.props.container.State.Downloading - }); - - var tabVolumesClasses = React.addons.classSet({ - 'tab': true, - 'active': this.state.page === this.PAGE_VOLUMES, - disabled: this.props.container.State.Downloading - });*/ - var tabSettingsClasses = React.addons.classSet({ 'tab': true, 'active': this.state.page === this.PAGE_SETTINGS, disabled: this.props.container.State.Downloading }); + /*var ports = _.map(_.pairs(self.state.ports), function (pair, index, list) { + var key = pair[0]; + var val = pair[1]; + return ( +
+ {key} + {val.display} + +
+ ); + }); + + var volumes = _.map(self.props.container.Volumes, function (val, key) { + if (!val || val.indexOf(process.env.HOME) === -1) { + val = No folderChoose; + } else { + val = {val.replace(process.env.HOME, '~')} Choose; + } + return ( +
+ {key} + {val} +
+ ); + });*/ + + /* var view; + if (this.state.defaultPort) { + view = ( +
+ View + +
+ ); + } else { + view = ( +
+ Ports +
+ ); + }*/ + return (
diff --git a/src/ContainerModal.react.js b/src/ContainerModal.react.js index ba35c6b425..0f6a13bda4 100644 --- a/src/ContainerModal.react.js +++ b/src/ContainerModal.react.js @@ -77,18 +77,17 @@ var ContainerModal = React.createClass({ }, 200); } }, - handleClick: function (name) { - ContainerStore.create(name, 'latest', function (err) { + 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) { - ContainerStore.create(name, tag, function () { - this.props.onRequestHide(); - }.bind(this)); + handleTagClick: function (tag, name, event) { + this.props.onRequestHide(); + ContainerStore.create(name, tag, function (err, containerName) {}); }, handleDropdownClick: function (name) { this.setState({ diff --git a/src/ContainerStore.js b/src/ContainerStore.js index f8bb709d4b..32773181f4 100644 --- a/src/ContainerStore.js +++ b/src/ContainerStore.js @@ -18,11 +18,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) { @@ -118,16 +118,25 @@ var ContainerStore = assign(EventEmitter.prototype, { if (containerData.Config && containerData.Config.Image) { containerData.Image = containerData.Config.Image; } - existing.kill(function () { - existing.remove(function () { + 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); @@ -308,11 +317,13 @@ var ContainerStore = assign(EventEmitter.prototype, { } $.ajax({ url: 'https://kitematic.com/recommended.json', + cache: false, dataType: 'json', success: function (res) { 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; @@ -376,38 +387,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 () { - 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 () { - 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) { @@ -416,10 +416,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/Docker.js b/src/Docker.js index 6c8768bf2e..83c9786c12 100644 --- a/src/Docker.js +++ b/src/Docker.js @@ -12,7 +12,7 @@ var Docker = { return; } this._client = new dockerode({ - host: '192.168.59.103', + host: host, port: 2376, ca: fs.readFileSync(path.join(certDir, 'ca.pem')), cert: fs.readFileSync(path.join(certDir, 'cert.pem')), diff --git a/src/Main.js b/src/Main.js index cbc6c57068..b9124e1634 100644 --- a/src/Main.js +++ b/src/Main.js @@ -1,9 +1,16 @@ +require.main.paths.splice(0, 0, process.env.NODE_PATH); + +var remote = require('remote'); +var app = remote.require('app'); var React = require('react'); +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 settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8')); if (process.env.NODE_ENV === 'development') { var script = document.createElement('script'); @@ -11,13 +18,21 @@ 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 () { + SetupStore.run(function (err) { + if (err) { console.log(err); } boot2docker.ip(function (err, ip) { if (err) { console.log(err); } docker.setHost(ip); diff --git a/src/Setup.react.js b/src/Setup.react.js index 7baa722006..8df59eb28e 100644 --- a/src/Setup.react.js +++ b/src/Setup.react.js @@ -1,23 +1,133 @@ var React = require('react/addons'); var Router = require('react-router'); var Radial = require('./Radial.react.js'); +var async = require('async'); +var assign = require('object-assign'); +var fs = require('fs'); +var path = require('path'); +var virtualbox = require('./Virtualbox'); 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; @@ -28,6 +138,9 @@ var Setup = React.createClass({ } else { radial = ; } + + var step = this.renderStep(); + if (this.state.error) { return (
@@ -36,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 f1a56bebd6..ca799f0cea 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,30 @@ 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) { var 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 +105,6 @@ var SetupStore = assign(EventEmitter.prototype, { } }, name: 'installing_virtualbox', - message: 'Installing VirtualBox', }, cleanupKitematicStep: { run: function (callback) { @@ -113,7 +116,6 @@ var SetupStore = assign(EventEmitter.prototype, { }); }, name: 'cleanup_kitematic', - message: 'Cleaning up existing Kitematic install...' }, initBoot2DockerStep: { run: function (callback) { @@ -143,15 +145,16 @@ var SetupStore = assign(EventEmitter.prototype, { }); }, name: 'init_boot2docker', - message: 'Setting up the Docker VM...' }, startBoot2DockerStep: { run: function (callback) { boot2docker.waitWhileStatus('saving', function () { boot2docker.status(function (err, status) { if (err) {callback(err); return;} + console.log(status); if (status !== 'running') { boot2docker.start(function (err) { + console.log('stated'); callback(err); }); } else { @@ -161,37 +164,37 @@ 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) { self.emit(self.ERROR_EVENT, _error); callback(err); } else { + console.log('done'); callback(); } }); diff --git a/styles/containers.less b/styles/containers.less index 6651ced1bd..1eb03f3787 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 { @@ -165,8 +165,8 @@ flex-shrink: 0; cursor: default; outline: none; - &.active { + background: @brand-primary; li { border-bottom: none; background-image: linear-gradient(-180deg, #24B8EB 4%, #24A3EB 100%); @@ -488,6 +488,11 @@ border-bottom: 1px solid transparent; transition: border-bottom 0.25s; .action { + + a { + -webkit-transition: none; + } + transition: none; flex: 0 auto; margin-right: 24px; } @@ -505,6 +510,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 6857c86555..f8346046a1 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; + } } }