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 = ( -
{message}
+{message}
+{message}
+{message}
+{this.state.message}
-