Revert "Revert "Adding Custom URL Handler for running containers" for 1.6 release"

This commit is contained in:
Jeffrey Morgan 2015-04-16 14:54:40 -04:00
parent 9705699e0c
commit c02a037190
13 changed files with 210 additions and 80 deletions

View File

@ -27,5 +27,5 @@
"jest": true, "jest": true,
"pit": true "pit": true
}, },
"predef": [ "Promise" ] "predef": [ "-Promise" ]
} }

View File

@ -7,6 +7,7 @@ var util = require('./Util');
var metrics = require('./Metrics'); var metrics = require('./Metrics');
var Router = require('react-router'); var Router = require('react-router');
var ContainerStore = require('./ContainerStore'); var ContainerStore = require('./ContainerStore');
var dialog = require('remote').require('dialog');
var ContainerHomeFolder = React.createClass({ var ContainerHomeFolder = React.createClass({
mixins: [Router.State, Router.Navigation], mixins: [Router.State, Router.Navigation],
@ -16,6 +17,11 @@ var ContainerHomeFolder = React.createClass({
}); });
if (hostVolume.indexOf(process.env.HOME) === -1) { if (hostVolume.indexOf(process.env.HOME) === -1) {
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 volumes = _.clone(this.props.container.Volumes);
var newHostVolume = path.join(util.home(), 'Kitematic', this.props.container.Name, containerVolume); var newHostVolume = path.join(util.home(), 'Kitematic', this.props.container.Name, containerVolume);
volumes[containerVolume] = newHostVolume; volumes[containerVolume] = newHostVolume;
@ -24,13 +30,15 @@ var ContainerHomeFolder = React.createClass({
}); });
ContainerStore.updateContainer(this.props.container.Name, { ContainerStore.updateContainer(this.props.container.Name, {
Binds: binds Binds: binds
}, function (err) { }, (err) => {
if (err) { if (err) {
console.log(err); console.log(err);
return; return;
} }
shell.showItemInFolder(newHostVolume); shell.showItemInFolder(newHostVolume);
}); });
}
});
} else { } else {
shell.showItemInFolder(hostVolume); shell.showItemInFolder(hostVolume);
} }

View File

@ -19,15 +19,9 @@ var ContainerList = React.createClass({
<ContainerListItem key={containerId} container={container} start={self._start} /> <ContainerListItem key={containerId} container={container} start={self._start} />
); );
}); });
var newItem;
if (!this.props.downloading) {
newItem = <ContainerListNewItem key={'newcontainer'} containers={this.props.containers} />;
} else {
newItem = '';
}
return ( return (
<ul> <ul>
{newItem} <ContainerListNewItem key={'newcontainer'} containers={this.props.containers}/>
{containers} {containers}
</ul> </ul>
); );

View File

@ -14,6 +14,7 @@ var _progress = {};
var _muted = {}; var _muted = {};
var _blocked = {}; var _blocked = {};
var _error = null; var _error = null;
var _pending = null;
var ContainerStore = assign(Object.create(EventEmitter.prototype), { var ContainerStore = assign(Object.create(EventEmitter.prototype), {
CLIENT_CONTAINER_EVENT: 'client_container_event', CLIENT_CONTAINER_EVENT: 'client_container_event',
@ -201,6 +202,10 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
var data = JSON.parse(json); var data = JSON.parse(json);
console.log(data); console.log(data);
if (data.status === 'pull' || data.status === 'untag' || data.status === 'delete') {
return;
}
// If the event is delete, remove the container // If the event is delete, remove the container
if (data.status === 'destroy') { if (data.status === 'destroy') {
var container = _.findWhere(_.values(_containers), {Id: data.id}); var container = _.findWhere(_.values(_containers), {Id: data.id});
@ -226,7 +231,6 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
} }
}, },
init: function (callback) { init: function (callback) {
// TODO: Load cached data from db on loading
this.fetchAllContainers(err => { this.fetchAllContainers(err => {
if (err) { if (err) {
_error = err; _error = err;
@ -471,6 +475,20 @@ var ContainerStore = assign(Object.create(EventEmitter.prototype), {
}, },
downloading: function () { downloading: function () {
return !!_.keys(_placeholders).length; 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');
} }
}); });

View File

@ -10,8 +10,6 @@ var metrics = require('./Metrics');
var autoUpdater = remote.require('auto-updater'); var autoUpdater = remote.require('auto-updater');
var RetinaImage = require('react-retina-image'); var RetinaImage = require('react-retina-image');
var machine = require('./DockerMachine'); var machine = require('./DockerMachine');
var OverlayTrigger = require('react-bootstrap').OverlayTrigger;
var Tooltip = require('react-bootstrap').Tooltip;
var util = require('./Util'); var util = require('./Util');
var Containers = React.createClass({ var Containers = React.createClass({
@ -33,10 +31,6 @@ var Containers = React.createClass({
ContainerStore.on(ContainerStore.SERVER_CONTAINER_EVENT, this.update); ContainerStore.on(ContainerStore.SERVER_CONTAINER_EVENT, this.update);
ContainerStore.on(ContainerStore.CLIENT_CONTAINER_EVENT, this.updateFromClient); 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', () => { ipc.on('application:update-available', () => {
this.setState({ this.setState({
updateAvailable: true updateAvailable: true
@ -48,38 +42,33 @@ var Containers = React.createClass({
ContainerStore.removeListener(ContainerStore.SERVER_CONTAINER_EVENT, this.update); ContainerStore.removeListener(ContainerStore.SERVER_CONTAINER_EVENT, this.update);
ContainerStore.removeListener(ContainerStore.CLIENT_CONTAINER_EVENT, this.updateFromClient); 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) { updateError: function (err) {
this.setState({ this.setState({
error: err error: err
}); });
}, },
update: function (name, status) { update: function (name, status) {
var sorted = ContainerStore.sorted();
this.setState({ this.setState({
containers: ContainerStore.containers(), containers: ContainerStore.containers(),
sorted: ContainerStore.sorted(), sorted: sorted,
pending: ContainerStore.pending(),
downloading: ContainerStore.downloading() downloading: ContainerStore.downloading()
}); });
if (status === 'destroy') { if (status === 'destroy') {
this.onDestroy(); if (sorted.length) {
this.transitionTo('containerHome', {name: sorted[0].Name});
} else {
this.transitionTo('containers');
}
} }
}, },
updateFromClient: function (name, status) { updateFromClient: function (name, status) {
this.setState({ this.update(name, status);
containers: ContainerStore.containers(),
sorted: ContainerStore.sorted(),
downloading: ContainerStore.downloading()
});
if (status === 'create') { if (status === 'create') {
this.transitionTo('containerHome', {name: name}); this.transitionTo('containerHome', {name: name});
} else if (status === 'destroy') { } else if (status === 'pending' && ContainerStore.pending()) {
this.onDestroy(); this.transitionTo('pull');
} }
}, },
handleScroll: function (e) { handleScroll: function (e) {
@ -162,17 +151,6 @@ var Containers = React.createClass({
); );
} }
var button;
if (this.state.downloading) {
button = (
<OverlayTrigger placement="bottom" overlay={<Tooltip>Only one Docker image can be downloaded at a time.</Tooltip>}>
<a disabled={true} className="btn-new icon icon-add-3"></a>
</OverlayTrigger>
);
} else {
button = <a className="btn-new icon icon-add-3" onClick={this.handleNewContainer}></a>;
}
var container = this.getParams().name ? this.state.containers[this.getParams().name] : {}; var container = this.getParams().name ? this.state.containers[this.getParams().name] : {};
return ( return (
<div className="containers"> <div className="containers">
@ -182,7 +160,7 @@ var Containers = React.createClass({
<section className={sidebarHeaderClass}> <section className={sidebarHeaderClass}>
<h4>Containers</h4> <h4>Containers</h4>
<div className="create"> <div className="create">
{button} <a className="btn-new icon icon-add-3" onClick={this.handleNewContainer}></a>
</div> </div>
</section> </section>
<section className="sidebar-containers" onScroll={this.handleScroll}> <section className="sidebar-containers" onScroll={this.handleScroll}>
@ -197,7 +175,7 @@ var Containers = React.createClass({
<div className="sidebar-buttons-padding"></div> <div className="sidebar-buttons-padding"></div>
</section> </section>
</div> </div>
<Router.RouteHandler container={container} error={this.state.error}/> <Router.RouteHandler pending={this.state.pending} container={container} error={this.state.error}/>
</div> </div>
</div> </div>
); );

View File

@ -27,14 +27,48 @@ setInterval(function () {
router.run(Handler => React.render(<Handler/>, document.body)); router.run(Handler => React.render(<Handler/>, 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(() => { SetupStore.setup().then(() => {
if (ContainerStore.pending()) {
router.transitionTo('pull');
} else {
router.transitionTo('new');
}
Menu.setApplicationMenu(Menu.buildFromTemplate(template())); Menu.setApplicationMenu(Menu.buildFromTemplate(template()));
ContainerStore.on(ContainerStore.SERVER_ERROR_EVENT, (err) => { ContainerStore.on(ContainerStore.SERVER_ERROR_EVENT, (err) => {
bugsnag.notify(err); bugsnag.notify(err);
}); });
ContainerStore.init(function () { ContainerStore.init(function () {});
router.transitionTo('containers');
});
}).catch(err => { }).catch(err => {
metrics.track('Setup Failed', { metrics.track('Setup Failed', {
step: 'catch', step: 'catch',
@ -43,9 +77,3 @@ SetupStore.setup().then(() => {
console.log(err); console.log(err);
bugsnag.notify(err); bugsnag.notify(err);
}); });
ipc.on('application:quitting', opts => {
if (!opts.updating && localStorage.getItem('settings.closeVMOnQuit') === 'true') {
machine.stop();
}
});

View File

@ -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 (
<div className="details">
<div className="new-container-pull">
<div className="content">
<h1>You&#39;re about to download and run <a onClick={this.handleOpenClick}>{this.props.pending.repository}:{this.props.pending.tag}</a>.</h1>
<h1>Please confirm to create the container.</h1>
<div className="buttons">
<a className="btn btn-action" onClick={this.handleCancelClick}>Cancel</a> <a onClick={this.handleConfirmClick} className="btn btn-action">Confirm</a>
</div>
</div>
</div>
</div>
);
}
});

View File

@ -10,7 +10,7 @@ var metrics = require('./Metrics');
var _recommended = []; var _recommended = [];
var _searchPromise = null; var _searchPromise = null;
var NewContainer = React.createClass({ module.exports = React.createClass({
getInitialState: function () { getInitialState: function () {
return { return {
query: '', query: '',
@ -47,7 +47,7 @@ var NewContainer = React.createClass({
loading: true 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'); metrics.track('Searched for Images');
this.setState({ this.setState({
results: data.results, results: data.results,
@ -166,5 +166,3 @@ var NewContainer = React.createClass({
); );
} }
}); });
module.exports = NewContainer;

View File

@ -9,12 +9,14 @@ var ContainerSettingsGeneral = require('./ContainerSettingsGeneral.react');
var ContainerSettingsPorts = require('./ContainerSettingsPorts.react'); var ContainerSettingsPorts = require('./ContainerSettingsPorts.react');
var ContainerSettingsVolumes = require('./ContainerSettingsVolumes.react'); var ContainerSettingsVolumes = require('./ContainerSettingsVolumes.react');
var Preferences = require('./Preferences.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 Router = require('react-router');
var Route = Router.Route; var Route = Router.Route;
var DefaultRoute = Router.DefaultRoute; var DefaultRoute = Router.DefaultRoute;
var RouteHandler = Router.RouteHandler; var RouteHandler = Router.RouteHandler;
var Redirect = Router.Redirect;
var App = React.createClass({ var App = React.createClass({
render: function () { render: function () {
@ -27,17 +29,21 @@ var App = React.createClass({
var routes = ( var routes = (
<Route name="app" path="/" handler={App}> <Route name="app" path="/" handler={App}>
<Route name="containers" handler={Containers}> <Route name="containers" handler={Containers}>
<Route name="containerDetails" path="containers/:name" handler={ContainerDetails}> <Route name="containerDetails" path="containers/details/:name" handler={ContainerDetails}>
<Route name="containerHome" path="containers/:name/home" handler={ContainerHome} /> <Route name="containerHome" path="containers/details/:name/home" handler={ContainerHome} />
<Route name="containerLogs" path="containers/:name/logs" handler={ContainerLogs}/> <Route name="containerLogs" path="containers/details/:name/logs" handler={ContainerLogs}/>
<Route name="containerSettings" path="containers/:name/settings" handler={ContainerSettings}> <Route name="containerSettings" path="containers/details/:name/settings" handler={ContainerSettings}>
<Route name="containerSettingsGeneral" path="containers/:name/settings/general" handler={ContainerSettingsGeneral}/> <Route name="containerSettingsGeneral" path="containers/details/:name/settings/general" handler={ContainerSettingsGeneral}/>
<Route name="containerSettingsPorts" path="containers/:name/settings/ports" handler={ContainerSettingsPorts}/> <Route name="containerSettingsPorts" path="containers/details/:name/settings/ports" handler={ContainerSettingsPorts}/>
<Route name="containerSettingsVolumes" path="containers/:name/settings/volumes" handler={ContainerSettingsVolumes}/> <Route name="containerSettingsVolumes" path="containers/details/:name/settings/volumes" handler={ContainerSettingsVolumes}/>
</Route> </Route>
</Route> </Route>
<Route name="new" path="containers/new">
<DefaultRoute name="search" handler={NewContainerSearch}/>
<Route name="pull" path="containers/new/pull" handler={NewContainerPull}></Route>
</Route>
<Route name="preferences" path="/preferences" handler={Preferences}/> <Route name="preferences" path="/preferences" handler={Preferences}/>
<DefaultRoute name="new" handler={NewContainer}/> <Redirect to="new"/>
</Route> </Route>
<DefaultRoute name="setup" handler={Setup}/> <DefaultRoute name="setup" handler={Setup}/>
</Route> </Route>

View File

@ -18,6 +18,13 @@ try {
settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8')); settingsjson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings.json'), 'utf8'));
} catch (err) {} } catch (err) {}
var openURL = null;
app.on('open-url', function (event, url) {
event.preventDefault();
openURL = url;
});
app.on('ready', function () { app.on('ready', function () {
var mainWindow = new BrowserWindow({ var mainWindow = new BrowserWindow({
width: size.width || 1000, width: size.width || 1000,
@ -65,6 +72,18 @@ app.on('ready', function () {
mainWindow.show(); mainWindow.show();
mainWindow.focus(); 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') { if (process.env.NODE_ENV !== 'development') {
autoUpdater.setFeedUrl('https://updates.kitematic.com/releases/latest?version=' + app.getVersion() + '&beta=' + !!settingsjson.beta); autoUpdater.setFeedUrl('https://updates.kitematic.com/releases/latest?version=' + app.getVersion() + '&beta=' + !!settingsjson.beta);
} }

View File

@ -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 { .new-container {
display: flex; display: flex;
flex: 1 auto; flex: 1 auto;

View File

@ -134,7 +134,6 @@ input[type="text"] {
font-weight: 400; font-weight: 400;
text-shadow: none; text-shadow: none;
padding: 5px 14px 5px 14px; padding: 5px 14px 5px 14px;
height: 30px;
cursor: default; cursor: default;
&.small { &.small {

View File

@ -26,5 +26,16 @@
<string>AtomApplication</string> <string>AtomApplication</string>
<key>NSSupportsAutomaticGraphicsSwitching</key> <key>NSSupportsAutomaticGraphicsSwitching</key>
<true/> <true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>docker</string>
</array>
<key>CFBundleURLName</key>
<string>Docker App Protocol</string>
</dict>
</array>
</dict> </dict>
</plist> </plist>