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 (
- {newItem}
+
{containers}
);
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({
@@ -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 (
+
+
+
+
+
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
+
+