Better loading indicators, removed some unused code

Signed-off-by: Jeffrey Morgan <jmorganca@gmail.com>
This commit is contained in:
Jeffrey Morgan
2015-10-06 21:43:04 -04:00
18 changed files with 106 additions and 132 deletions

View File

@@ -315,7 +315,7 @@ module.exports = function (grunt) {
grunt.registerTask('default', ['download-binary', 'newer:babel', 'less', 'newer:copy:dev', 'shell:electron', 'watchChokidar']);
if (process.platform === 'win32') {
grunt.registerTask('release', ['clean:release', 'download-binary', 'babel', 'less', 'copy:dev', 'electron:windows', 'copy:windows', 'rcedit:exes', 'compress', 'create-windows-installer', 'rename:installer']);
grunt.registerTask('release', ['clean:release', 'download-binary', 'babel', 'less', 'copy:dev', 'electron:windows', 'copy:windows', 'rcedit:exes', 'compress']);
} else {
grunt.registerTask('release', ['clean:release', 'download-binary', 'babel', 'less', 'copy:dev', 'electron:osx', 'copy:osx', 'shell:sign', 'shell:zip']);
}

View File

@@ -55,7 +55,6 @@ describe('SetupStore', function () {
machine.stop.mockReturnValue(Promise.resolve());
machine.start.mockReturnValue(Promise.resolve());
machine.upgrade.mockReturnValue(Promise.resolve());
util.packagejson.mockReturnValue({'docker-version':'1.8.0'});
util.compareVersions.mockReturnValue(-1);
machine.create.mockClear();
machine.upgrade.mockClear();

View File

@@ -1,6 +1,6 @@
{
"name": "Kitematic",
"version": "0.8.5",
"version": "0.8.6",
"author": "Kitematic",
"description": "Simple Docker Container management for Mac OS X.",
"homepage": "https://kitematic.com/",

View File

@@ -14,7 +14,6 @@ import request from 'request';
import docker from './utils/DockerUtil';
import hub from './utils/HubUtil';
import Router from 'react-router';
import createHashHistory from 'history/lib/createHashHistory'
import routes from './routes';
import routerContainer from './router';
import repositoryActions from './actions/RepositoryActions';
@@ -41,16 +40,19 @@ setInterval(function () {
metrics.track('app heartbeat');
}, 14400000);
let history = createHashHistory()
React.render(<Router history={history}>{routes}</Router>, document.body)
var router = Router.create({
routes: routes
});
router.run(Handler => React.render(<Handler/>, document.body));
routerContainer.set(router);
setupUtil.setup().then(() => {
Menu.setApplicationMenu(Menu.buildFromTemplate(template()));
docker.init();
if (!hub.prompted() && !hub.loggedin()) {
history.replaceState(null, '/account/login');
router.transitionTo('login');
} else {
history.replaceState(null, '/containers/new');
router.transitionTo('search');
}
}).catch(err => {
metrics.track('Setup Failed', {

View File

@@ -74,6 +74,7 @@ app.on('ready', function () {
mainWindow.webContents.send('application:quitting');
return true;
});
app.on('window-all-closed', function() {
app.quit();
});

View File

@@ -39,7 +39,6 @@ var Preferences = React.createClass({
<div className="item">
<RetinaImage src="cartoon-docker.png"/>
<h4>Docker Engine</h4>
<p>{packages["docker-version"]}</p>
</div>
<div className="item">
<RetinaImage src="cartoon-docker-machine.png"/>

View File

@@ -1,7 +1,6 @@
import _ from 'underscore';
import $ from 'jquery';
import React from 'react/addons';
import Radial from './Radial.react';
import ContainerProgress from './ContainerProgress.react';
import ContainerHomePreview from './ContainerHomePreview.react';
import ContainerHomeLogs from './ContainerHomeLogs.react';
@@ -88,14 +87,14 @@ var ContainerHome = React.createClass({
body = (
<div className="details-progress">
<h2>Waiting For Another Download</h2>
<Radial spin="true" progress="90" thick={true} transparent={true}/>
<div className="spinner la-ball-clip-rotate la-lg la-dark"><div></div></div>
</div>
);
} else {
body = (
<div className="details-progress">
<h2>Connecting to Docker Hub</h2>
<Radial spin="true" progress="90" thick={true} transparent={true}/>
<div className="spinner la-ball-clip-rotate la-lg la-dark"><div></div></div>
</div>
);
}

View File

@@ -10,6 +10,10 @@ import shell from 'shell';
import machine from '../utils/DockerMachineUtil';
var Containers = React.createClass({
contextTypes: {
router: React.PropTypes.func
},
getInitialState: function () {
return {
sidebarOffset: 0,
@@ -48,9 +52,7 @@ var Containers = React.createClass({
let containers = containerStore.getState().containers;
let sorted = this.sorted(containerStore.getState().containers);
console.log(this.props);
let name = this.props.params.name;
let name = this.context.router.getCurrentParams().name;
if (containerStore.getState().pending) {
this.context.router.transitionTo('pull');
} else if (name && !containers[name]) {
@@ -82,7 +84,7 @@ var Containers = React.createClass({
handleNewContainer: function () {
$(this.getDOMNode()).find('.new-container-item').parent().fadeIn();
this.context.router.transitionTo('new');
this.context.router.transitionTo('search');
metrics.track('Pressed New Container');
},
@@ -149,7 +151,7 @@ var Containers = React.createClass({
sidebarHeaderClass += ' sep';
}
var container = this.props.params ? this.state.containers[ this.props.params] : {};
var container = this.context.router.getCurrentParams().name ? this.state.containers[this.context.router.getCurrentParams().name] : {};
return (
<div className="containers">
<Header />
@@ -158,7 +160,7 @@ var Containers = React.createClass({
<section className={sidebarHeaderClass}>
<h4>Containers</h4>
<div className="create">
<Router.Link to="new">
<Router.Link to="search">
<span className="btn btn-new btn-action has-icon btn-hollow"><span className="icon icon-add"></span>New</span>
</Router.Link>
</div>

View File

@@ -0,0 +1,15 @@
import React from 'react/addons';
import Header from './Header.react';
module.exports = React.createClass({
render: function () {
return (
<div className="loading">
<Header hideLogin={true}/>
<div className="loading-content">
<div className="spinner la-ball-clip-rotate la-lg la-dark"><div></div></div>
</div>
</div>
);
}
});

View File

@@ -1,50 +0,0 @@
import React from 'react/addons';
import Router from 'react-router';
import shell from 'shell';
import containerActions from '../actions/ContainerActions';
import containerStore from '../stores/ContainerStore';
import metrics from '../utils/MetricsUtil';
module.exports = React.createClass({
mixins: [Router.Navigation],
handleOpenClick: function () {
var repo = this.props.pending.repo;
if (repo.indexOf('/') === -1) {
shell.openExternal(`https://registry.hub.docker.com/_/${this.props.pending.repo}`);
} else {
shell.openExternal(`https://registry.hub.docker.com/u/${this.props.pending.repo}`);
}
},
handleCancelClick: function () {
metrics.track('Canceled Click-To-Pull');
containerActions.clearPending();
this.transitionTo('new');
},
handleConfirmClick: function () {
metrics.track('Created Container', {
from: 'click-to-pull'
});
containerActions.clearPending();
let name = containerStore.generateName(this.props.pending.repo);
containerActions.run(name, this.props.pending.repo, this.props.pending.tag);
this.transitionTo('containerHome', {name});
},
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.repo}:{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

@@ -31,7 +31,7 @@ var Setup = React.createClass({
<div className="contents">
<RetinaImage src="boot2docker.png" checkIfRetinaImgExists={false}/>
<div className="detail">
<Radial progress={this.state.progress} thick={true} gray={true}/>
<Radial progress={Math.round(this.state.progress)} thick={true} gray={true}/>
</div>
</div>
);
@@ -47,29 +47,8 @@ var Setup = React.createClass({
</div>
<div className="desc">
<div className="content">
<h1>{this.state.title}</h1>
<p>{this.state.message}</p>
</div>
</div>
</div>
</div>
);
},
renderCancelled: function () {
return (
<div className="setup">
<Header hideLogin={true}/>
<div className="setup-content">
<div className="image">
{this.renderContents()}
</div>
<div className="desc">
<div className="content">
<h4>Setup Cancelled</h4>
<h1>Couldn&#39;t Install Requirements</h1>
<p>Kitematic didn&#39;t receive the administrative privileges required to install or upgrade VirtualBox &amp; Docker.</p>
<p>Please click retry. If VirtualBox is not installed, you can download &amp; install it manually from the <a onClick={this.handleOpenWebsite}>official Oracle website</a>.</p>
<p><button className="btn btn-action" onClick={this.handleCancelRetry}>Retry</button></p>
<h1>Starting Docker VM</h1>
<p>To run Docker containers on your computer, Kitematic is starting a Linux virtual machine. This may take a minute...</p>
</div>
</div>
</div>

View File

@@ -6,9 +6,8 @@ var util = require('./utils/Util');
var setupUtil = require('./utils/SetupUtil');
var metrics = require('./utils/MetricsUtil');
var machine = require('./utils/DockerMachineUtil');
var docker = require('./utils/DockerUtil');
var dialog = remote.require('dialog');
import docker from './utils/DockerUtil';
import Router from 'react-router';
// main.js
var MenuTemplate = function () {
@@ -22,7 +21,7 @@ var MenuTemplate = function () {
metrics.track('Opened About', {
from: 'menu'
});
Router.transitionTo('about');
router.get().transitionTo('about');
}
},
{
@@ -36,7 +35,7 @@ var MenuTemplate = function () {
metrics.track('Opened Preferences', {
from: 'menu'
});
Router.transitionTo('preferences');
router.get().transitionTo('preferences');
}
},
{

View File

@@ -14,44 +14,45 @@ import ContainerSettingsVolumes from './components/ContainerSettingsVolumes.reac
import ContainerSettingsAdvanced from './components/ContainerSettingsAdvanced.react';
import Preferences from './components/Preferences.react';
import About from './components/About.react';
import Loading from './components/Loading.react';
import NewContainerSearch from './components/NewContainerSearch.react';
import NewContainerPull from './components/NewContainerPull.react';
import {Router, IndexRoute, Route, Link} from 'react-router'
import Router from 'react-router';
var Route = Router.Route;
var DefaultRoute = Router.DefaultRoute;
var RouteHandler = Router.RouteHandler;
var App = React.createClass({
render: function () {
return (
<div>
{this.props.children}
</div>
<RouteHandler/>
);
}
});
var routes = (
<Route path="/" component={App}>
<IndexRoute component={Setup}/>
<Route path="account" component={Account}>
<Route path="signup" component={AccountSignup}/>
<Route path="login" component={AccountLogin}/>
<Route name="app" path="/" handler={App}>
<Route name="account" path="account" handler={Account}>
<Route name="signup" path="signup" handler={AccountSignup}/>
<Route name="login" path="login" handler={AccountLogin}/>
</Route>
<Route path="containers" component={Containers}>
<Route path="details/:name" component={ContainerDetails}>
<IndexRoute component={ContainerHome} />
<Route path="logs" component={ContainerLogs}/>
<Route path="settings" component={ContainerSettings}>
<Route path="general" component={ContainerSettingsGeneral}/>
<Route path="ports" component={ContainerSettingsPorts}/>
<Route path="volumes" component={ContainerSettingsVolumes}/>
<Route path="advanced" component={ContainerSettingsAdvanced}/>
<Route name="containers" path="containers" handler={Containers}>
<Route name="container" path="details/:name" handler={ContainerDetails}>
<DefaultRoute name="containerHome" handler={ContainerHome} />
<Route name="containerLogs" path="logs" handler={ContainerLogs}/>
<Route name="containerSettings" path="settings" handler={ContainerSettings}>
<Route name="containerSettingsGeneral" path="general" handler={ContainerSettingsGeneral}/>
<Route name="containerSettingsPorts" path="ports" handler={ContainerSettingsPorts}/>
<Route name="containerSettingsVolumes" path="volumes" handler={ContainerSettingsVolumes}/>
<Route name="containerSettingsAdvanced" path="advanced" handler={ContainerSettingsAdvanced}/>
</Route>
</Route>
<Route name="new" path="new">
<IndexRoute component={NewContainerSearch}/>
</Route>
<Route path="preferences" component={Preferences}/>
<Route path="about" component={About}/>
<Route name="search" handler={NewContainerSearch}/>
<Route name="preferences" path="preferences" handler={Preferences}/>
<Route name="about" path="about" handler={About}/>
</Route>
<DefaultRoute name="loading" handler={Loading}/>
<Route name="setup" handler={Setup}/>
</Route>
);

View File

@@ -9,16 +9,17 @@ import setupActions from '../actions/SetupActions';
import metrics from './MetricsUtil';
import machine from './DockerMachineUtil';
import docker from './DockerUtil';
import router from '../router';
let _retryPromise = null;
export default {
simulateProgress (estimateSeconds, progress) {
simulateProgress (estimateSeconds) {
var times = _.range(0, estimateSeconds * 1000, 200);
var timers = [];
_.each(times, time => {
var timer = setTimeout(() => {
setupActions.progress(100 * time / (estimateSeconds * 1000));
setupActions.progress({progress: 100 * time / (estimateSeconds * 1000)});
}, time);
timers.push(timer);
});
@@ -27,7 +28,6 @@ export default {
retry (removeVM) {
if (removeVM) {
machine.rm().finally(() => {
console.log('machine removed');
_retryPromise.resolve();
});
} else {
@@ -50,17 +50,23 @@ export default {
let exists = await virtualBox.vmExists('default');
if (!exists) {
this.simulateProgress(60, progress => setupActions.progress(progress));
await machine.rm();
router.get().transitionTo('setup');
this.simulateProgress(60);
try {
await machine.rm();
} catch (err) {}
await machine.create();
} else {
let state = await machine.state();
if (state === 'Saved') {
this.simulateProgress(10, progress => setupActions.progress(progress));
} else {
this.simulateProgress(25, progress => setupActions.progress(progress));
if (state !== 'Running') {
router.get().transitionTo('setup');
if (state === 'Saved') {
this.simulateProgress(10);
} else if (state === 'Stopped') {
this.simulateProgress(25)
}
await machine.start();
}
await machine.start();
}
let ip = await machine.ip();
@@ -76,6 +82,7 @@ export default {
await this.pause();
}
}
console.log('Setup finished.');
metrics.track('Setup Finished');
}
};

View File

@@ -1,18 +1,24 @@
import fs from 'fs';
import path from 'path';
import util from './Util';
import Promise from 'bluebird';
var VirtualBox = {
command: function () {
if(util.isWindows()) {
return 'C:\\Program Files\\Oracle\\VirtualBox\\VBoxManage.exe';
if (process.env.VBOX_MSI_INSTALL_PATH) {
return path.join(process.env.VBOX_MSI_INSTALL_PATH, 'VBoxManage.exe');
} else {
return path.join(process.env.VBOX_INSTALL_PATH, 'VBoxManage.exe');
}
} else {
return '/Applications/VirtualBox.app/Contents/MacOS/VBoxManage';
}
},
installed: function () {
if(util.isWindows()) {
return fs.existsSync('C:\\Program Files\\Oracle\\VirtualBox\\VBoxManage.exe') && fs.existsSync('C:\\Program Files\\Oracle\\VirtualBox\\VirtualBox.exe');
return (process.env.VBOX_MSI_INSTALL_PATH && fs.existsSync(path.join(process.env.VBOX_MSI_INSTALL_PATH, 'VBoxManage.exe'))) ||
(process.env.VBOX_INSTALL_PATH && fs.existsSync(path.join(process.env.VBOX_INSTALL_PATH, 'VBoxManage.exe')));
} else {
return fs.existsSync('/Applications/VirtualBox.app') && fs.existsSync('/Applications/VirtualBox.app/Contents/MacOS/VBoxManage');
}

13
styles/loading.less Normal file
View File

@@ -0,0 +1,13 @@
.loading {
display: flex;
width: 100%;
height: 100%;
flex-direction: column;
.loading-content {
display: flex;
flex: 1 1;
align-items: center;
justify-content: center;
}
}

View File

@@ -18,6 +18,7 @@
@import "spinner.less";
@import "animation.less";
@import "container-progress.less";
@import "loading.less";
html, body {
height: 100%;

View File

@@ -292,6 +292,7 @@
}
}
.card {
flex: 1 0;
position: relative;
border: 1px solid darken(@gray-lightest, 0%);
border-left: 0;