diff --git a/images/userdropdown@2x.png b/images/userdropdown@2x.png new file mode 100644 index 0000000000..dab8b11562 Binary files /dev/null and b/images/userdropdown@2x.png differ diff --git a/src/actions/AccountActions.js b/src/actions/AccountActions.js index ff2807de34..059f956a17 100644 --- a/src/actions/AccountActions.js +++ b/src/actions/AccountActions.js @@ -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); } } diff --git a/src/actions/AccountServerActions.js b/src/actions/AccountServerActions.js index 24b9d514d6..758a6adaae 100644 --- a/src/actions/AccountServerActions.js +++ b/src/actions/AccountServerActions.js @@ -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}); } diff --git a/src/actions/RepositoryServerActions.js b/src/actions/RepositoryServerActions.js index 0fa5cb03d1..ffb2e2166c 100644 --- a/src/actions/RepositoryServerActions.js +++ b/src/actions/RepositoryServerActions.js @@ -3,6 +3,7 @@ import alt from '../alt'; class RepositoryServerActions { constructor () { this.generateActions( + 'searched', 'fetched', 'error' ); diff --git a/src/app.js b/src/app.js index f7e370b651..243070d656 100644 --- a/src/app.js +++ b/src/app.js @@ -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'); diff --git a/src/browser.js b/src/browser.js index 0299b8afbf..e033f86f75 100644 --- a/src/browser.js +++ b/src/browser.js @@ -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, diff --git a/src/components/Account.react.js b/src/components/Account.react.js index 341762be8e..1118d03948 100644 --- a/src/components/Account.react.js +++ b/src/components/Account.react.js @@ -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 ? - Close : + Close : Skip For Now; return ( diff --git a/src/components/AccountLogin.react.js b/src/components/AccountLogin.react.js index 9757664937..6070f32292 100644 --- a/src/components/AccountLogin.react.js +++ b/src/components/AccountLogin.react.js @@ -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 ?
: null; return (
- +

{this.state.errors.username}

{this.state.errors.password}

diff --git a/src/components/AccountSignup.react.js b/src/components/AccountSignup.react.js index 162050b549..53e40bfc3a 100644 --- a/src/components/AccountSignup.react.js +++ b/src/components/AccountSignup.react.js @@ -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'); } }, diff --git a/src/components/Header.react.js b/src/components/Header.react.js index 1aa9cf7997..d6b043d88d 100644 --- a/src/components/Header.react.js +++ b/src/components/Header.react.js @@ -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 ? UPDATE NOW : null; let buttons; @@ -91,7 +106,11 @@ var Header = React.createClass({ if (this.props.hideLogin) { username = null; } else if (this.state.username) { - username = {this.state.username}; + username = ( + + {this.state.username} + + ); } else { username = ( @@ -100,8 +119,14 @@ var Header = React.createClass({ ); } + let headerClasses = classNames({ + bordered: !this.props.hideLogin, + header: true, + 'no-drag': true + }); + return ( -
+
{buttons}
{updateWidget} diff --git a/src/components/NewContainer.react.js b/src/components/NewContainer.react.js deleted file mode 100644 index cc6d7520c1..0000000000 --- a/src/components/NewContainer.react.js +++ /dev/null @@ -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 ( - - ); - }); - - results = ( -
- {items} -
- ); - } else { - if (this.state.results.length === 0 && this.state.query === '') { - results = ( -
-
-

Loading Images

- -
-
- ); - } else { - results = ( -
-

Cannot find a matching image.

-
- ); - } - } - 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 ( -
-
-
-
- Select a Docker image to create a new container. -
-
-
- -
- -
-
-
-
-

{title}

- {results} -
-
-
- ); - } -}); - -module.exports = NewContainer; diff --git a/src/components/NewContainerSearch.react.js b/src/components/NewContainerSearch.react.js index 8d961c8fbf..ab81643b28 100644 --- a/src/components/NewContainerSearch.react.js +++ b/src/components/NewContainerSearch.react.js @@ -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 (
@@ -162,7 +134,12 @@ module.exports = React.createClass({
-

{title}

+
+ FILTER BY + All + Recommended + My Repositories +
{results}
diff --git a/src/routes.js b/src/routes.js index 782a58ef16..d946cdbbda 100644 --- a/src/routes.js +++ b/src/routes.js @@ -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 = ( - ); diff --git a/src/stores/AccountStore.js b/src/stores/AccountStore.js index 3968dde3d9..fc2ce6c89d 100644 --- a/src/stores/AccountStore.js +++ b/src/stores/AccountStore.js @@ -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}); } diff --git a/src/stores/RepositoryStore.js b/src/stores/RepositoryStore.js index 53208eede3..4263b02671 100644 --- a/src/stores/RepositoryStore.js +++ b/src/stores/RepositoryStore.js @@ -5,6 +5,8 @@ class RepositoryStore { constructor () { this.bindActions(repositoryServerActions); this.repos = []; + this.recommended = []; + this.userrepos = []; this.loading = false; this.error = null; } diff --git a/src/utils/HubUtil.js b/src/utils/HubUtil.js index d95142c45c..99092da4f9 100644 --- a/src/utils/HubUtil.js +++ b/src/utils/HubUtil.js @@ -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 = {}; diff --git a/src/utils/RegHubUtil.js b/src/utils/RegHubUtil.js index b1c461764d..72aacf99dc 100644 --- a/src/utils/RegHubUtil.js +++ b/src/utils/RegHubUtil.js @@ -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); diff --git a/styles/header.less b/styles/header.less index 1d98ce64c7..5a4da6bfc3 100644 --- a/styles/header.less +++ b/styles/header.less @@ -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 { diff --git a/styles/new-container.less b/styles/new-container.less index 4ad11c53de..e9f7407858 100644 --- a/styles/new-container.less +++ b/styles/new-container.less @@ -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; } } diff --git a/styles/right-panel.less b/styles/right-panel.less index 85480a6f65..faee2d737a 100644 --- a/styles/right-panel.less +++ b/styles/right-panel.less @@ -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; diff --git a/styles/setup.less b/styles/setup.less index 2a705ed059..9f056ea272 100644 --- a/styles/setup.less +++ b/styles/setup.less @@ -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;