diff --git a/.jshintrc b/.jshintrc index daeaf45072..9240a3ae9f 100644 --- a/.jshintrc +++ b/.jshintrc @@ -27,5 +27,5 @@ "jest": true, "pit": true }, - "predef": [ "Promise" ] + "predef": [ "-Promise" ] } diff --git a/src/ContainerHomeFolders.react.js b/src/ContainerHomeFolders.react.js index 1ace6d4e90..2e3e796cc3 100644 --- a/src/ContainerHomeFolders.react.js +++ b/src/ContainerHomeFolders.react.js @@ -7,6 +7,7 @@ var util = require('./Util'); var metrics = require('./Metrics'); var Router = require('react-router'); var ContainerStore = require('./ContainerStore'); +var dialog = require('remote').require('dialog'); var ContainerHomeFolder = React.createClass({ mixins: [Router.State, Router.Navigation], @@ -16,20 +17,27 @@ var ContainerHomeFolder = React.createClass({ }); if (hostVolume.indexOf(process.env.HOME) === -1) { - var volumes = _.clone(this.props.container.Volumes); - var newHostVolume = path.join(util.home(), 'Kitematic', this.props.container.Name, containerVolume); - volumes[containerVolume] = newHostVolume; - var binds = _.pairs(volumes).map(function (pair) { - return pair[1] + ':' + pair[0]; - }); - ContainerStore.updateContainer(this.props.container.Name, { - Binds: binds - }, function (err) { - if (err) { - console.log(err); - return; + dialog.showMessageBox({ + message: 'Enable all volumes to edit files via Finder? This may not work with all database containers.', + buttons: ['Enable Volumes', 'Cancel'] + }, (index) => { + if (index === 0) { + var volumes = _.clone(this.props.container.Volumes); + var newHostVolume = path.join(util.home(), 'Kitematic', this.props.container.Name, containerVolume); + volumes[containerVolume] = newHostVolume; + var binds = _.pairs(volumes).map(function (pair) { + return pair[1] + ':' + pair[0]; + }); + ContainerStore.updateContainer(this.props.container.Name, { + Binds: binds + }, (err) => { + if (err) { + console.log(err); + return; + } + shell.showItemInFolder(newHostVolume); + }); } - shell.showItemInFolder(newHostVolume); }); } else { shell.showItemInFolder(hostVolume); diff --git a/src/ContainerList.react.js b/src/ContainerList.react.js index d2954dc603..1e6f358d6d 100644 --- a/src/ContainerList.react.js +++ b/src/ContainerList.react.js @@ -19,15 +19,9 @@ var ContainerList = React.createClass({ ); }); - var newItem; - if (!this.props.downloading) { - newItem = ; - } else { - newItem = ''; - } return ( ); diff --git a/src/ContainerStore.js b/src/ContainerStore.js index 109a022d19..262ddc6a57 100644 --- a/src/ContainerStore.js +++ b/src/ContainerStore.js @@ -14,6 +14,7 @@ var _progress = {}; var _muted = {}; var _blocked = {}; var _error = null; +var _pending = null; var ContainerStore = assign(Object.create(EventEmitter.prototype), { CLIENT_CONTAINER_EVENT: 'client_container_event', @@ -201,6 +202,10 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { var data = JSON.parse(json); console.log(data); + if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete') { + return; + } + // If the event is delete, remove the container if (data.status === 'destroy') { var container = _.findWhere(_.values(_containers), {Id: data.id}); @@ -226,7 +231,6 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { } }, init: function (callback) { - // TODO: Load cached data from db on loading this.fetchAllContainers(err => { if (err) { _error = err; @@ -471,6 +475,20 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), { }, downloading: function () { return !!_.keys(_placeholders).length; + }, + pending: function () { + return _pending; + }, + setPending: function (repository, tag) { + _pending = { + repository: repository, + tag: tag + }; + this.emit(this.CLIENT_CONTAINER_EVENT, null, 'pending'); + }, + clearPending: function () { + _pending = null; + this.emit(this.CLIENT_CONTAINER_EVENT, null, 'pending'); } }); diff --git a/src/Containers.react.js b/src/Containers.react.js index af151dabca..ab613bb61a 100644 --- a/src/Containers.react.js +++ b/src/Containers.react.js @@ -10,8 +10,6 @@ var metrics = require('./Metrics'); var autoUpdater = remote.require('auto-updater'); var RetinaImage = require('react-retina-image'); var machine = require('./DockerMachine'); -var OverlayTrigger = require('react-bootstrap').OverlayTrigger; -var Tooltip = require('react-bootstrap').Tooltip; var util = require('./Util'); var Containers = React.createClass({ @@ -33,10 +31,6 @@ var Containers = React.createClass({ ContainerStore.on(ContainerStore.SERVER_CONTAINER_EVENT, this.update); ContainerStore.on(ContainerStore.CLIENT_CONTAINER_EVENT, this.updateFromClient); - if (this.state.sorted.length) { - this.transitionTo('containerHome', {name: this.state.sorted[0].Name}); - } - ipc.on('application:update-available', () => { this.setState({ updateAvailable: true @@ -48,38 +42,33 @@ var Containers = React.createClass({ ContainerStore.removeListener(ContainerStore.SERVER_CONTAINER_EVENT, this.update); ContainerStore.removeListener(ContainerStore.CLIENT_CONTAINER_EVENT, this.updateFromClient); }, - onDestroy: function () { - if (this.state.sorted.length) { - this.transitionTo('containerHome', {name: this.state.sorted[0].Name}); - } else { - this.transitionTo('containers'); - } - }, updateError: function (err) { this.setState({ error: err }); }, update: function (name, status) { + var sorted = ContainerStore.sorted(); this.setState({ containers: ContainerStore.containers(), - sorted: ContainerStore.sorted(), + sorted: sorted, + pending: ContainerStore.pending(), downloading: ContainerStore.downloading() }); if (status === 'destroy') { - this.onDestroy(); + if (sorted.length) { + this.transitionTo('containerHome', {name: sorted[0].Name}); + } else { + this.transitionTo('containers'); + } } }, updateFromClient: function (name, status) { - this.setState({ - containers: ContainerStore.containers(), - sorted: ContainerStore.sorted(), - downloading: ContainerStore.downloading() - }); + this.update(name, status); if (status === 'create') { this.transitionTo('containerHome', {name: name}); - } else if (status === 'destroy') { - this.onDestroy(); + } else if (status === 'pending' && ContainerStore.pending()) { + this.transitionTo('pull'); } }, handleScroll: function (e) { @@ -162,17 +151,6 @@ var Containers = React.createClass({ ); } - var button; - if (this.state.downloading) { - button = ( - Only one Docker image can be downloaded at a time.}> - - - ); - } else { - button = ; - } - var container = this.getParams().name ? this.state.containers[this.getParams().name] : {}; return (
@@ -182,7 +160,7 @@ var Containers = React.createClass({

Containers

- {button} +
@@ -197,7 +175,7 @@ var Containers = React.createClass({
- + ); diff --git a/src/Main.js b/src/Main.js index 9f582f3588..44235be0bc 100644 --- a/src/Main.js +++ b/src/Main.js @@ -27,14 +27,48 @@ setInterval(function () { router.run(Handler => React.render(, document.body)); +ipc.on('application:quitting', opts => { + if (!opts.updating && localStorage.getItem('settings.closeVMOnQuit') === 'true') { + machine.stop(); + } +}); + +ipc.on('application:open-url', opts => { + var repoRegexp = /[a-z0-9]+(?:[._-][a-z0-9]+)*/; + var parser = document.createElement('a'); + parser.href = opts.url; + + if (parser.protocol !== 'docker:') { + return; + } + + var pathname = parser.pathname.replace('//', ''); + var tokens = pathname.split('/'); + var type = tokens[0]; + var method = tokens[1]; + var repo = tokens.slice(2).join('/'); + + // Only accept official repos for now + if (repo.indexOf('/') !== -1 || !repoRegexp.test(repo)) { + return; + } + + if (type === 'repository' && method === 'run') { + ContainerStore.setPending(repo, 'latest'); + } +}); + SetupStore.setup().then(() => { + if (ContainerStore.pending()) { + router.transitionTo('pull'); + } else { + router.transitionTo('new'); + } Menu.setApplicationMenu(Menu.buildFromTemplate(template())); ContainerStore.on(ContainerStore.SERVER_ERROR_EVENT, (err) => { bugsnag.notify(err); }); - ContainerStore.init(function () { - router.transitionTo('containers'); - }); + ContainerStore.init(function () {}); }).catch(err => { metrics.track('Setup Failed', { step: 'catch', @@ -43,9 +77,3 @@ SetupStore.setup().then(() => { console.log(err); bugsnag.notify(err); }); - -ipc.on('application:quitting', opts => { - if (!opts.updating && localStorage.getItem('settings.closeVMOnQuit') === 'true') { - machine.stop(); - } -}); diff --git a/src/NewContainerPull.react.js b/src/NewContainerPull.react.js new file mode 100644 index 0000000000..bc915b4dda --- /dev/null +++ b/src/NewContainerPull.react.js @@ -0,0 +1,42 @@ +var React = require('react/addons'); +var Router = require('react-router'); +var shell = require('shell'); +var ContainerStore = require('./ContainerStore'); + +module.exports = React.createClass({ + mixins: [Router.Navigation], + handleOpenClick: function () { + var repo = this.props.pending.repository; + if (repo.indexOf('/') === -1) { + shell.openExternal(`https://registry.hub.docker.com/_/${this.props.pending.repository}`); + } else { + shell.openExternal(`https://registry.hub.docker.com/u/${this.props.pending.repository}`); + } + }, + handleCancelClick: function () { + ContainerStore.clearPending(); + this.transitionTo('new'); + }, + handleConfirmClick: function () { + ContainerStore.clearPending(); + ContainerStore.create(this.props.pending.repository, this.props.pending.tag, function () {}); + }, + render: function () { + if (!this.props.pending) { + return false; + } + return ( +
+
+
+

You're about to download and run {this.props.pending.repository}:{this.props.pending.tag}.

+

Please confirm to create the container.

+ +
+
+
+ ); + } +}); diff --git a/src/NewContainer.react.js b/src/NewContainerSearch.react.js similarity index 95% rename from src/NewContainer.react.js rename to src/NewContainerSearch.react.js index 05e772cedd..3ac67f2882 100644 --- a/src/NewContainer.react.js +++ b/src/NewContainerSearch.react.js @@ -10,7 +10,7 @@ var metrics = require('./Metrics'); var _recommended = []; var _searchPromise = null; -var NewContainer = React.createClass({ +module.exports = React.createClass({ getInitialState: function () { return { query: '', @@ -47,7 +47,7 @@ var NewContainer = React.createClass({ loading: true }); - _searchPromise = Promise.delay(200).then(() => Promise.resolve($.get('https://registry.hub.docker.com/v1/search?q=' + query))).cancellable().then(data => { + _searchPromise = Promise.delay(200).cancellable().then(() => Promise.resolve($.get('https://registry.hub.docker.com/v1/search?q=' + query))).then(data => { metrics.track('Searched for Images'); this.setState({ results: data.results, @@ -166,5 +166,3 @@ var NewContainer = React.createClass({ ); } }); - -module.exports = NewContainer; diff --git a/src/Routes.js b/src/Routes.js index eab468fa1c..cfdade8b51 100644 --- a/src/Routes.js +++ b/src/Routes.js @@ -9,12 +9,14 @@ var ContainerSettingsGeneral = require('./ContainerSettingsGeneral.react'); var ContainerSettingsPorts = require('./ContainerSettingsPorts.react'); var ContainerSettingsVolumes = require('./ContainerSettingsVolumes.react'); var Preferences = require('./Preferences.react'); -var NewContainer = require('./NewContainer.react'); +var NewContainerSearch = require('./NewContainerSearch.react'); +var NewContainerPull = require('./NewContainerPull.react'); var Router = require('react-router'); var Route = Router.Route; var DefaultRoute = Router.DefaultRoute; var RouteHandler = Router.RouteHandler; +var Redirect = Router.Redirect; var App = React.createClass({ render: function () { @@ -27,17 +29,21 @@ var App = React.createClass({ var routes = ( - - - - - - - + + + + + + + + + + + - + diff --git a/src/browser.js b/src/browser.js index 8722673dcb..ee7810450a 100644 --- a/src/browser.js +++ b/src/browser.js @@ -18,6 +18,13 @@ try { settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8')); } catch (err) {} + +var openURL = null; +app.on('open-url', function (event, url) { + event.preventDefault(); + openURL = url; +}); + app.on('ready', function () { var mainWindow = new BrowserWindow({ width: size.width || 1000, @@ -65,6 +72,18 @@ app.on('ready', function () { mainWindow.show(); mainWindow.focus(); + if (openURL) { + mainWindow.webContents.send('application:open-url', { + url: openURL + }); + } + app.on('open-url', function (event, url) { + event.preventDefault(); + mainWindow.webContents.send('application:open-url', { + url: url + }); + }); + if (process.env.NODE_ENV !== 'development') { autoUpdater.setFeedUrl('https://updates.kitematic.com/releases/latest?version=' + app.getVersion() + '&beta=' + !!settingsjson.beta); } diff --git a/styles/new-container.less b/styles/new-container.less index a00f9c3eb3..86b60d8903 100644 --- a/styles/new-container.less +++ b/styles/new-container.less @@ -1,3 +1,32 @@ +.new-container-pull { + display: flex; + flex: 1 auto; + align-items: center; + justify-content: center; + .content { + text-align: center; + + .buttons { + margin-top: 30px; + .btn { + margin-left: 10px; + margin-right: 10px; + padding: 8px 18px; + font-size: 14px; + background: white; + font-weight: 300; + } + } + } + h1 { + font-size: 20px; + color: @gray-normal; + font-weight: 400; + text-align: center; + margin-top: 10px; + } +} + .new-container { display: flex; flex: 1 auto; diff --git a/styles/theme.less b/styles/theme.less index 07e8cedd22..d20daccc76 100644 --- a/styles/theme.less +++ b/styles/theme.less @@ -134,7 +134,6 @@ input[type="text"] { font-weight: 400; text-shadow: none; padding: 5px 14px 5px 14px; - height: 30px; cursor: default; &.small { diff --git a/util/Info.plist b/util/Info.plist index 396be69183..5efd29e633 100644 --- a/util/Info.plist +++ b/util/Info.plist @@ -26,5 +26,16 @@ AtomApplication NSSupportsAutomaticGraphicsSwitching + CFBundleURLTypes + + + CFBundleURLSchemes + + docker + + CFBundleURLName + Docker App Protocol + +