This commit is contained in:
Jeffrey Morgan
2015-05-20 20:09:14 -07:00
parent 0d52985606
commit d80983fb4e
21 changed files with 232 additions and 277 deletions

BIN
images/userdropdown@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

View File

@@ -2,7 +2,7 @@ import alt from '../alt';
import hub from '../utils/HubUtil';
class AccountActions {
login(username, password) {
login (username, password) {
this.dispatch({});
hub.login(username, password);
}
@@ -12,8 +12,14 @@ class AccountActions {
hub.signup(username, password, email, subscribe);
}
logout () {
this.dispatch({});
hub.logout();
}
skip () {
this.dispatch({});
hub.prompted(true);
}
}

View File

@@ -5,13 +5,16 @@ import router from '../router';
class AccountServerActions {
constructor () {
this.generateActions(
'loggedout',
'prompted',
'errors'
);
}
loggedin ({username, verified}) {
console.log(username, verified);
router.get().goBack();
if (router.get()) {
router.get().goBack();
}
this.dispatch({username, verified});
}

View File

@@ -3,6 +3,7 @@ import alt from '../alt';
class RepositoryServerActions {
constructor () {
this.generateActions(
'searched',
'fetched',
'error'
);

View File

@@ -9,6 +9,7 @@ var metrics = require('./utils/MetricsUtil');
var router = require('./router');
var template = require('./menutemplate');
var webUtil = require('./utils/WebUtil');
var hubUtil = require('./utils/HubUtil');
var urlUtil = require ('./utils/URLUtil');
var app = remote.require('app');
var request = require('request');
@@ -24,6 +25,8 @@ webUtil.addLiveReload();
webUtil.addBugReporting();
webUtil.disableGlobalBackspace();
hubUtil.init();
Menu.setApplicationMenu(Menu.buildFromTemplate(template()));
metrics.track('Started App');

View File

@@ -27,10 +27,10 @@ app.on('open-url', function (event, url) {
app.on('ready', function () {
var mainWindow = new BrowserWindow({
width: size.width || 1000,
height: size.height || 700,
'min-width': 1000,
'min-height': 700,
width: size.width || 800,
height: size.height || 600,
'min-width': 800,
'min-height': 600,
'standard-window': false,
resizable: true,
frame: false,

View File

@@ -13,6 +13,16 @@ module.exports = React.createClass({
return accountStore.getState();
},
componentDidMount: function () {
document.addEventListener('keyup', this.handleDocumentKeyUp, false);
accountStore.listen(this.update);
},
componentWillUnmount: function () {
document.removeEventListener('keyup', this.handleDocumentKeyUp, false);
accountStore.unlisten(this.update);
},
handleSkip: function () {
accountActions.skip();
this.transitionTo('search');
@@ -24,17 +34,13 @@ module.exports = React.createClass({
metrics.track('Closed Signup');
},
componentDidMount: function () {
accountStore.listen(this.update);
},
update: function () {
this.setState(accountStore.getState());
},
render: function () {
let close = this.state.prompted ?
<a className="btn btn-action btn-skip" onClick={this.handleClose}>Close</a> :
<a className="btn btn-action btn-close" onClick={this.handleClose}>Close</a> :
<a className="btn btn-action btn-skip" onClick={this.handleSkip}>Skip For Now</a>;
return (

View File

@@ -16,7 +16,9 @@ module.exports = React.createClass({
errors: {}
};
},
componentDidMount: function () {
React.findDOMNode(this.refs.usernameInput).focus();
},
componentWillReceiveProps: function (nextProps) {
this.setState({errors: nextProps.errors});
},
@@ -50,7 +52,7 @@ module.exports = React.createClass({
handleClickSignup: function () {
if (!this.props.loading) {
this.transitionTo('signup');
this.replaceWith('signup');
}
},
@@ -59,11 +61,10 @@ module.exports = React.createClass({
},
render: function () {
console.log(this.state.errors);
let loading = this.props.loading ? <div className="spinner la-ball-clip-rotate la-dark"><div></div></div> : null;
return (
<form className="form-connect">
<input ref="usernameInput"maxLength="30" name="username" placeholder="username" type="text" disabled={this.props.loading} valueLink={this.linkState('username')} onBlur={this.handleBlur}/>
<input ref="usernameInput"maxLength="30" name="username" placeholder="username" type="text" disabled={this.props.loading} valueLink={this.linkState('username')} onBlur={this.handleBlur}/>
<p className="error-message">{this.state.errors.username}</p>
<input ref="passwordInput" name="password" placeholder="password" type="password" disabled={this.props.loading} valueLink={this.linkState('password')} onBlur={this.handleBlur}/>
<p className="error-message">{this.state.errors.password}</p>

View File

@@ -18,6 +18,10 @@ module.exports = React.createClass({
};
},
componentDidMount: function () {
React.findDOMNode(this.refs.usernameInput).focus();
},
componentWillReceiveProps: function (nextProps) {
this.setState({errors: nextProps.errors});
},
@@ -54,7 +58,7 @@ module.exports = React.createClass({
handleClickLogin: function () {
if (!this.props.loading) {
this.transitionTo('login');
this.replaceWith('login');
}
},

View File

@@ -8,7 +8,9 @@ var metrics = require('../utils/MetricsUtil');
var Menu = remote.require('menu');
var MenuItem = remote.require('menu-item');
var accountStore = require('../stores/AccountStore');
var accountActions = require('../actions/AccountActions');
var Router = require('react-router');
var classNames = require('classNames');
var Header = React.createClass({
mixins: [Router.Navigation],
@@ -23,6 +25,8 @@ var Header = React.createClass({
componentDidMount: function () {
document.addEventListener('keyup', this.handleDocumentKeyUp, false);
accountStore.listen(this.update);
ipc.on('application:update-available', () => {
this.setState({
updateAvailable: true
@@ -32,6 +36,14 @@ var Header = React.createClass({
},
componentWillUnmount: function () {
document.removeEventListener('keyup', this.handleDocumentKeyUp, false);
accountStore.unlisten(this.update);
},
update: function () {
let accountState = accountStore.getState();
this.setState({
username: accountState.username,
verified: accountState.verified
});
},
handleDocumentKeyUp: function (e) {
if (e.keyCode === 27 && remote.getCurrentWindow().isFullScreen()) {
@@ -60,12 +72,15 @@ var Header = React.createClass({
},
handleUserClick: function (e) {
let menu = new Menu();
menu.append(new MenuItem({ label: 'Sign Out', click: function() { console.log('item 1 clicked'); } }));
menu.append(new MenuItem({ label: 'Sign Out', click: this.handleLogoutClick.bind(this)}));
menu.popup(remote.getCurrentWindow(), e.currentTarget.offsetLeft, e.currentTarget.offsetTop + e.currentTarget.clientHeight + 10);
},
handleLoginClick: function () {
this.transitionTo('login');
},
handleLogoutClick: function () {
accountActions.logout();
},
render: function () {
let updateWidget = this.state.updateAvailable ? <a className="btn btn-action small no-drag" onClick={this.handleAutoUpdateClick}>UPDATE NOW</a> : null;
let buttons;
@@ -91,7 +106,11 @@ var Header = React.createClass({
if (this.props.hideLogin) {
username = null;
} else if (this.state.username) {
username = <span>{this.state.username}</span>;
username = (
<span className="no-drag" onClick={this.handleUserClick}>
<RetinaImage src="user.png"/> {this.state.username} <RetinaImage src="userdropdown.png"/>
</span>
);
} else {
username = (
<span className="no-drag" onClick={this.handleLoginClick}>
@@ -100,8 +119,14 @@ var Header = React.createClass({
);
}
let headerClasses = classNames({
bordered: !this.props.hideLogin,
header: true,
'no-drag': true
});
return (
<div className="header no-drag">
<div className={headerClasses}>
{buttons}
<div className="updates">
{updateWidget}

View File

@@ -1,171 +0,0 @@
var _ = require('underscore');
var $ = require('jquery');
var React = require('react');
var RetinaImage = require('react-retina-image');
var Radial = require('./Radial.react');
var ImageCard = require('./ImageCard.react');
var Promise = require('bluebird');
var metrics = require('../utils/MetricsUtil');
var classNames = require('classnames');
var _recommended = [];
var _searchPromise = null;
var NewContainer = React.createClass({
getInitialState: function () {
return {
query: '',
loading: false,
results: _recommended
};
},
componentDidMount: function () {
this.refs.searchInput.getDOMNode().focus();
this.recommended();
},
componentWillUnmount: function () {
if (_searchPromise) {
_searchPromise.cancel();
}
},
search: function (query) {
if (_searchPromise) {
_searchPromise.cancel();
_searchPromise = null;
}
if (!query.length) {
this.setState({
query: query,
results: _recommended,
loading: false
});
return;
}
this.setState({
query: query,
loading: true
});
_searchPromise = Promise.delay(200).then(() => Promise.resolve($.get('https://registry.hub.docker.com/v1/search?q=' + query))).cancellable().then(data => {
metrics.track('Searched for Images');
this.setState({
results: data.results,
query: query,
loading: false
});
_searchPromise = null;
}).catch(Promise.CancellationError, () => {
});
},
recommended: function () {
if (_recommended.length) {
return;
}
Promise.resolve($.ajax({
url: 'https://kitematic.com/recommended.json',
cache: false,
dataType: 'json',
})).then(res => res.repos).map(repo => {
var query = repo.repo;
var vals = query.split('/');
if (vals.length === 1) {
query = 'library/' + vals[0];
}
return $.get('https://registry.hub.docker.com/v1/repositories_info/' + query).then(data => {
var res = _.extend(data, repo);
res.description = data.short_description;
res.is_official = data.namespace === 'library';
res.name = data.repo;
res.star_count = data.stars;
return res;
});
}).then(results => {
_recommended = results.filter(r => !!r);
if (!this.state.query.length && this.isMounted()) {
this.setState({
results: _recommended
});
}
}).catch(err => {
console.log(err);
});
},
handleChange: function (e) {
var query = e.target.value;
if (query === this.state.query) {
return;
}
this.search(query);
},
render: function () {
var title = this.state.query ? 'Results' : 'Recommended';
var data = this.state.results;
var results;
if (data.length) {
var items = data.map(function (image) {
return (
<ImageCard key={image.name} image={image} />
);
});
results = (
<div className="result-grid">
{items}
</div>
);
} else {
if (this.state.results.length === 0 && this.state.query === '') {
results = (
<div className="no-results">
<div className="loader">
<h2>Loading Images</h2>
<Radial spin="true" progress={90} thick={true} transparent={true} />
</div>
</div>
);
} else {
results = (
<div className="no-results">
<h1>Cannot find a matching image.</h1>
</div>
);
}
}
var loadingClasses = classNames({
hidden: !this.state.loading,
loading: true
});
var magnifierClasses = classNames({
hidden: this.state.loading,
icon: true,
'icon-magnifier': true,
'search-icon': true
});
return (
<div className="details">
<div className="new-container">
<div className="new-container-header">
<div className="text">
Select a Docker image to create a new container.
</div>
<div className="search">
<div className="search-bar">
<input type="search" ref="searchInput" className="form-control" placeholder="Search Docker Hub for an image" onChange={this.handleChange}/>
<div className={magnifierClasses}></div>
<RetinaImage className={loadingClasses} src="loading.png"/>
</div>
</div>
</div>
<div className="results">
<h4>{title}</h4>
{results}
</div>
</div>
</div>
);
}
});
module.exports = NewContainer;

View File

@@ -1,7 +1,4 @@
var _ = require('underscore');
var $ = require('jquery');
var React = require('react/addons');
var RetinaImage = require('react-retina-image');
var ImageCard = require('./ImageCard.react');
var Promise = require('bluebird');
var metrics = require('../utils/MetricsUtil');
@@ -15,12 +12,19 @@ module.exports = React.createClass({
return {
query: '',
loading: false,
results: _recommended
category: 'recommended',
recommendedrepos: [],
publicrepos: [],
userrepos: [],
results: [],
tab: 'all'
};
},
componentDidMount: function () {
// fetch recommended
// fetch public repos
// if logged in: my repos
this.refs.searchInput.getDOMNode().focus();
this.recommended();
},
componentWillUnmount: function () {
if (_searchPromise) {
@@ -47,49 +51,11 @@ module.exports = React.createClass({
loading: true
});
_searchPromise = Promise.delay(200).cancellable().then(() => Promise.resolve($.get('https://registry.hub.docker.com/v1/search?q=' + query))).then(data => {
_searchPromise = Promise.delay(200).cancellable().then(() => {
metrics.track('Searched for Images');
this.setState({
results: data.results,
query: query,
loading: false
});
_searchPromise = null;
}).catch(Promise.CancellationError, () => {
});
},
recommended: function () {
if (_recommended.length) {
return;
}
Promise.resolve($.ajax({
url: 'https://kitematic.com/recommended.json',
cache: false,
dataType: 'json',
})).then(res => res.repos).map(repo => {
var query = repo.repo;
var vals = query.split('/');
if (vals.length === 1) {
query = 'library/' + vals[0];
}
return $.get('https://registry.hub.docker.com/v1/repositories_info/' + query).then(data => {
var res = _.extend(data, repo);
res.description = data.short_description;
res.is_official = data.namespace === 'library';
res.name = data.repo;
res.star_count = data.stars;
return res;
});
}).then(results => {
_recommended = results.filter(r => !!r);
if (!this.state.query.length && this.isMounted()) {
this.setState({
results: _recommended
});
}
}).catch(err => {
console.log(err);
});
// TODO: call search action
}).catch(Promise.CancellationError, () => {});
},
handleChange: function (e) {
var query = e.target.value;
@@ -99,8 +65,7 @@ module.exports = React.createClass({
this.search(query);
},
render: function () {
var title = this.state.query ? 'Results' : 'Recommended';
var data = this.state.results;
var data = this.state.recommendedrepos;
var results;
if (data.length) {
var items = data.map(function (image) {
@@ -132,7 +97,8 @@ module.exports = React.createClass({
);
}
}
var loadingClasses = classNames({
let loadingClasses = classNames({
hidden: !this.state.loading,
spinner: true,
loading: true,
@@ -140,12 +106,18 @@ module.exports = React.createClass({
'la-dark': true,
'la-sm': true
});
var magnifierClasses = classNames({
let magnifierClasses = classNames({
hidden: this.state.loading,
icon: true,
'icon-magnifier': true,
'search-icon': true
});
let allTabClasses = classNames({
'results-filter':
});
return (
<div className="details">
<div className="new-container">
@@ -162,7 +134,12 @@ module.exports = React.createClass({
</div>
</div>
<div className="results">
<h4>{title}</h4>
<div className="results-filters">
<span className="results-filter results-filter-title">FILTER BY</span>
<span className="results-filter results-all tab">All</span>
<span className="results-filter results-recommended tab">Recommended</span>
<span className="results-filter results-userrepos tab">My Repositories</span>
</div>
{results}
</div>
</div>

View File

@@ -19,7 +19,6 @@ 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 () {
@@ -52,7 +51,6 @@ var routes = (
<Route name="preferences" path="/preferences" handler={Preferences}/>
</Route>
<DefaultRoute name="setup" handler={Setup}/>
<Redirect from="containers/details/:name" to="containerHome"/>
</Route>
);

View File

@@ -7,7 +7,7 @@ class AccountStore {
this.bindActions(accountServerActions);
this.bindActions(accountActions);
this.prompted = localStorage.getItem('account.prompted') || false;
this.prompted = false;
this.loading = false;
this.errors = {};
@@ -19,7 +19,6 @@ class AccountStore {
this.setState({
prompted: true
});
localStorage.setItem('account.prompted', true);
}
login () {
@@ -29,6 +28,15 @@ class AccountStore {
});
}
logout () {
this.setState({
loading: false,
errors: {},
username: null,
verified: false
});
}
signup () {
this.setState({
loading: true,
@@ -48,6 +56,10 @@ class AccountStore {
this.setState({verified});
}
prompted ({prompted}) {
this.setState({prompted});
}
errors ({errors}) {
this.setState({errors, loading: false});
}

View File

@@ -5,6 +5,8 @@ class RepositoryStore {
constructor () {
this.bindActions(repositoryServerActions);
this.repos = [];
this.recommended = [];
this.userrepos = [];
this.loading = false;
this.error = null;
}

View File

@@ -2,6 +2,16 @@ var request = require('request');
var accountServerActions = require('../actions/AccountServerActions');
module.exports = {
init: function () {
accountServerActions.prompted({prompted: localStorage.getItem('auth.prompted')});
if (this.jwt()) { // TODO: check for config too
let username = localStorage.getItem('auth.username');
let verified = localStorage.getItem('auth.verified');
accountServerActions.loggedin({username, verified});
}
},
// Returns the base64 encoded index token or null if no token exists
config: function () {
let config = localStorage.getItem('auth.config');
@@ -11,6 +21,11 @@ module.exports = {
return config;
},
prompted: function (prompted) {
localStorage.setItem('auth.prompted', true);
accountServerActions.prompted({prompted});
},
// Retrives the current jwt hub token or null if no token exists
jwt: function () {
let jwt = localStorage.getItem('auth.jwt');
@@ -20,8 +35,16 @@ module.exports = {
return jwt;
},
loggedin: function () {
return this.jwt() && this.config();
refresh: function () {
// TODO: implement me
},
logout: function () {
localStorage.removeItem('auth.jwt');
localStorage.removeItem('auth.username');
localStorage.removeItem('auth.verified');
localStorage.removeItem('auth.config');
accountServerActions.loggedout();
},
// Places a token under ~/.dockercfg and saves a jwt to localstore
@@ -30,8 +53,10 @@ module.exports = {
let data = JSON.parse(body);
if (response.statusCode === 200) {
// TODO: save username to localstorage
// TODO: handle case where token does not exist
if (data.token) {
localStorage.setItem('auth.jwt', data.token);
localStorage.setItem('auth.username', username);
}
accountServerActions.loggedin({username, verified: true});
} else if (response.statusCode === 401) {
@@ -56,7 +81,7 @@ module.exports = {
}, (err, response, body) => {
// TODO: save username to localstorage
if (response.statusCode === 204) {
accountServerActions.signedup({username});
accountServerActions.signedup({username, verified: false});
} else {
let data = JSON.parse(body);
let errors = {};

View File

@@ -3,6 +3,40 @@ var async = require('async');
var repositoryServerActions = require('../actions/RepositoryServerActions');
module.exports = {
search: function (query) {
if (!query) {
return;
}
request.get({
url: 'https://registry.hub.docker.com/v1/search?',
qs: {q: query}
}, (error, response, body) => {
if (error) {
// TODO: report search error
}
let data = JSON.parse(body);
if (response.statusCode === 200) {
repositoryServerActions.searched({});
}
});
},
recommended: function () {
request.get('https://kitematic.com/recommended.json', (error, response, body) => {
if (error) {
// TODO: report search error
}
let data = JSON.parse(body);
console.log(data);
if (response.statusCode === 200) {
repositoryServerActions.recommended({});
}
});
},
// Returns the base64 encoded index token or null if no token exists
repos: function (jwt) {
@@ -28,6 +62,7 @@ module.exports = {
}, (error, response, body) => {
if (error) {
repositoryServerActions.error({error});
return;
}
let data = JSON.parse(body);

View File

@@ -3,7 +3,9 @@
-webkit-app-region: drag;
-webkit-user-select: none;
border-bottom: 1px solid #E7E7E7;
&.bordered {
border-bottom: 1px solid #E7E7E7;
}
display: flex;
@@ -30,7 +32,7 @@
color: #88919C;
align-items: center;
justify-content: flex-end;
margin-right: 30px;
margin-right: 13px;
&:active {
img, span {

View File

@@ -36,6 +36,27 @@
display: flex;
flex-direction: column;
flex: 1 auto;
color: @gray-normal;
.results-filters {
display: flex;
flex-direction: row;
justify-content: flex-end;
font-size: 13px;
.results-filter {
text-align: center;
margin: 0 10px;
min-width: 40px;
}
.results-filter-title {
color: @gray-lighter;
font-weight: 500;
padding-top: 6px;
}
}
.no-results {
flex: 1 auto;
display: flex;
@@ -64,18 +85,17 @@
}
}
.new-container-header {
margin-bottom: 18px;
margin-bottom: 8px;
display: flex;
flex: 0 auto;
.text {
flex: 1 auto;
width: 50%;
font-size: 14px;
color: @gray-lighter;
color: @gray-normal;
}
.search {
flex: 1 auto;
margin-right: 30px;
.search-bar {
top: -7px;
position: relative;
@@ -105,7 +125,7 @@
border-color: @brand-primary;
}
&::-webkit-input-placeholder {
color: @gray-lightest;
color: @gray-lighter;
font-weight: 400;
}
}

View File

@@ -65,28 +65,6 @@
text-align: right;
margin-right: 3px;
margin-top: 3px;
.tab {
margin-left: 16px;
padding: 6px 10px;
font-weight: 400;
display: inline-block;
&:hover {
border-radius: 40px;
background-color: darken(@color-background, 2%);
}
&.active {
border-radius: 40px;
color: white;
.brand-gradient();
}
&.disabled {
opacity: 0.5;
&:hover {
border-radius: 40px;
background-color: transparent;
}
}
}
}
}
.details-header {
@@ -123,6 +101,26 @@
}
}
}
.tab {
margin-left: 16px;
padding: 6px 10px;
font-weight: 400;
display: inline-block;
&.active {
border-radius: 40px;
color: white;
.brand-gradient();
}
&.disabled {
opacity: 0.5;
&:hover {
border-radius: 40px;
background-color: transparent;
}
}
}
.details-progress {
margin: 20% auto 0;
text-align: center;

View File

@@ -164,6 +164,14 @@
font-size: 14px;
}
.btn-close {
-webkit-app-region: no-drag;
position: absolute;
top: 16px;
right: 16px;
font-size: 14px;
}
.desc {
flex: 1 auto;
display: flex;