diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ed751a..fc9baee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,3 +19,5 @@ A **Task** is something that is not complex enough to be defined as a **Feature* The title of a an issue that is expected to be treated as a **BUG** must be prefixed with **BUG:**, i.e., **BUG: Wrong new notifications count**. Submit your pull requests to the corresponding branch according to the branching model mentioned at the beginning of this section. + +Last but not least, [stop using `git pull`](https://adamcod.es/2014/12/10/git-pull-correct-workflow.html) diff --git a/README.md b/README.md index 72ec59a..f4d89be 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# phoenix-webpack-relay-react (pwr2) +# Phoenix, Webpack, React and Relay (pwr2) Docker image based on Ubuntu providing a base setup for a Phoenix+Webpack+Relay+React project, with some sugar and conventions to develop and build your own web applications. @@ -6,6 +6,22 @@ _We will improve docs and code including test coverage in the next releases_ **NOTICE:** The default branch for this repo is **develop**. Check the [README](https://github.com/iporaitech/pwr2-docker/blob/master/README.md) on master to see what's in the last release. +## What is this for? + +You can use this to start your own Elixir/Phoenix based web application with React+Relay - styled with Material Design, in the front-end all bundled with Webpack. + +So far we've implemented the following: + +* A GraphQL endpoint implemented in Elixir with Absinthe. +* Authentication using JWTs (JSON Web Tokens) via GraphQL (LoginMutation & LogoutMutation). +* Hardcoded Role based Authorization. +* StarWars GraphQL example. +* GraphiQL console. +* Some interesting React/Relay components in the client(browser), including a router. +* CSS Modules _integration_ with Material Design Lite. +* Testing framework for the backend (Elixir/Phoenix). + + ## Requirements To run this software you need to install [Docker](https://www.docker.com/) on your system. It is very simple and there are a lot of guides and tutorials out there on the Internet. @@ -97,7 +113,7 @@ Once the containers are up and running you can copy the source code of the base Once all setup and with the app running and assuming your `HTTP_PORT` is 4000, you can: -0. Login with credentials available in [priv/repo/seeds.exs](priv/repo/seeds.exs). Logout is also available **BUT DISPLAYING AN ERROR when trying lo Login with wrong credentials is not implemented yet**. +0. Login with credentials available in [priv/repo/seeds.exs](priv/repo/seeds.exs). Logout is also available. 1. Visit http://localhost:4000/admin/graphiql to access a [GraphiQL](https://github.com/graphql/graphiql) IDE. 2. Visit http://localhost:4000/admin/star-wars to experiment with our implementation of the [Relay Star Wars example](https://github.com/relayjs/relay-examples/tree/master/star-wars). The _[database](./web/graphql/star_wars_db.ex)_ for this example is implemented as an [Elixir.Agent](http://elixir-lang.org/docs/stable/elixir/Agent.html) 3. You can also use something like Google Chrome's Advanced Rest Client(ARC) or any other JSON API client and (with the corresponding Authorization header) send queries to http://localhost:4000/graphql like: diff --git a/mix.exs b/mix.exs index deea994..0658a03 100644 --- a/mix.exs +++ b/mix.exs @@ -3,7 +3,7 @@ defmodule Webapp.Mixfile do def project do [app: :webapp, - version: "0.3.0", + version: "0.3.2", elixir: "~> 1.3", elixirc_paths: elixirc_paths(Mix.env), compilers: [:phoenix, :gettext] ++ Mix.compilers, diff --git a/package.json b/package.json index 9b00ba7..35c521d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "pwr2-docker-ui", "description": "Javascript dependencies for pwr2-docker, a Docker image based on Ubuntu providing a base setup for a Phoenix+Webpack+Relay+React project, with some sugar and conventions to develop and build your own web applications.", "main": "webpack.config.js", - "version": "0.3.0", + "version": "0.3.2", "contributors": [ { "name": "Iporaitech and others", @@ -33,6 +33,7 @@ }, "dependencies": { "babel-runtime": "^6.11.6", + "classnames": "^2.2.5", "es6-promise": "^3.2.1", "graphiql": "^0.7.3", "graphql": "^0.6.2", @@ -48,5 +49,5 @@ "react-router-relay": "https://github.com/iporaitech/react-router-relay/tarball/build", "sync-request": "^3.0.1" }, - "license" : "MIT" + "license": "MIT" } diff --git a/web/graphql/types/auth_mutations.ex b/web/graphql/types/auth_mutations.ex index cf3c003..36f3179 100644 --- a/web/graphql/types/auth_mutations.ex +++ b/web/graphql/types/auth_mutations.ex @@ -3,8 +3,11 @@ defmodule Webapp.GraphQL.Types.AuthMutations do use Absinthe.Relay.Schema.Notation import Comeonin.Bcrypt, only: [checkpw: 2, dummy_checkpw: 0] - #TODO: Create resolve functions for these mutations in their own module and test them. - # It will be easier to test the resolvers in isolation than embedded in the muations. + #TODO: + # 1. Create resolve functions for these mutations in their own module and test them. It + # will be easier to test the resolvers in isolation than embedded in the muations. + # 2. Refactor to use Scalar types for email and password. + # See http://graphql.org/learn/schema/#scalar-types object :auth_mutations do payload field :login do diff --git a/web/static/js/layout/index.js b/web/static/js/layout/index.js index 40beeb7..7ceb94a 100644 --- a/web/static/js/layout/index.js +++ b/web/static/js/layout/index.js @@ -1,7 +1,7 @@ // file: layout/index.js import React from 'react'; import { Link } from 'react-router'; -import LogoutLink from 'lib/LogoutLink'; +import LogoutLink from 'shared/LogoutLink'; import mdlUpgrade from 'lib/mdlUpgrade'; import styles from 'material-design-lite/material.css'; diff --git a/web/static/js/login/index.js b/web/static/js/login/index.js index 51779fc..4cf1a8b 100644 --- a/web/static/js/login/index.js +++ b/web/static/js/login/index.js @@ -2,23 +2,29 @@ import React from 'react'; import Relay from 'react-relay'; import mdlUpgrade from 'lib/mdlUpgrade'; +import Loading from 'shared/loading'; import material from 'material-design-lite/material.css'; +import classNames from 'classnames/bind'; import styles from './styles.css'; import { withRouter } from 'react-router'; import LoginMutation from './mutation'; import Auth from 'lib/auth'; +const cx = classNames.bind(styles); + class Login extends React.Component { constructor(props) { super(props); this.state = { - error: false + hasError: false, + isLoading: false } } handleSubmit(event) { event.preventDefault(); + this.setState({isLoading: true}); this.props.relay.commitUpdate( new LoginMutation({ @@ -34,40 +40,55 @@ class Login extends React.Component { } else { router.replace('/') } + }, + onFailure: transaction => { + this.setState({hasError: true}); + this.setState({isLoading: false}); } } ); } render() { + // We use classNames for CSS that depends on MDL javascript + const inputClassName = cx( + "mdl-js-textfield", + 'mdl-textfield', + 'mdl-textfield--floating-label', + {"is-invalid": this.state.hasError} + ); + const { isLoading } = this.state; + return (
-
-
+
+

Login

-
-
- - +
+ { isLoading && ()} +
+ { this.state.hasError && ( + Incorrect email or password + )} +
+ + +
+
+ + +
-
- - +
+
-
- - {this.state.error && ( -

Bad login information

- )} -
@@ -80,7 +101,7 @@ class Login extends React.Component { // style.css contains customizations on some mdl classes export default Relay.createContainer( withRouter( - mdlUpgrade(Login, Object.assign({}, material, styles), {allowMultiple: true}) + mdlUpgrade(Login, Object.assign({}, material, styles)) ),{ fragments: {} } diff --git a/web/static/js/login/mutation.js b/web/static/js/login/mutation.js index c059763..f5ef44e 100644 --- a/web/static/js/login/mutation.js +++ b/web/static/js/login/mutation.js @@ -2,9 +2,7 @@ import Relay from 'react-relay'; export default class extends Relay.Mutation { getMutation() { - return Relay.QL`mutation { - login - }`; + return Relay.QL`mutation {login}`; } getVariables() { @@ -17,7 +15,7 @@ export default class extends Relay.Mutation { // TODO: Add field to LoginPayload to get errors getFatQuery() { return Relay.QL` - fragment on LoginPayload { + fragment on LoginPayload @relay(pattern: true) { accessToken } `; diff --git a/web/static/js/login/styles.css b/web/static/js/login/styles.css index 0162efe..1171a19 100644 --- a/web/static/js/login/styles.css +++ b/web/static/js/login/styles.css @@ -9,3 +9,19 @@ padding: 24px; flex: none; } + +.card { + composes: mdl-card mdl-shadow--6dp from "material-design-lite/material.css"; +} + +.card-title { + composes: mdl-card__title mdl-color--primary mdl-color-text--white + from "material-design-lite/material.css"; +} + +.login-error { + composes: mdl-typography--caption-color-contrast from "material-design-lite/material.css"; + color: red; + text-align: center; + font-size: 14px; +} diff --git a/web/static/js/lib/LogoutLink.js b/web/static/js/shared/LogoutLink.js similarity index 97% rename from web/static/js/lib/LogoutLink.js rename to web/static/js/shared/LogoutLink.js index a624d7e..a7023b3 100644 --- a/web/static/js/lib/LogoutLink.js +++ b/web/static/js/shared/LogoutLink.js @@ -1,4 +1,4 @@ -// file: layout/LogoutLink.js +// file: shared/LogoutLink.js import React from 'react'; import Relay from 'react-relay'; import Auth from 'lib/auth'; diff --git a/web/static/js/shared/loading/index.js b/web/static/js/shared/loading/index.js new file mode 100644 index 0000000..6faac69 --- /dev/null +++ b/web/static/js/shared/loading/index.js @@ -0,0 +1,34 @@ +// file: shared/LoadingOverlay.js +import React from 'react'; +import mdlUpgrade from 'lib/mdlUpgrade'; +import material from 'material-design-lite/material.css'; +import classNames from 'classnames/bind'; +import styles from './styles.css'; + +const cx = classNames.bind(styles); + +class Loading extends React.Component { + static propTypes = { + overlay: React.PropTypes.bool + } + static defaultProps = { + overlay: true + } + + render(){ + const className = cx( + 'mdl-spinner', + 'mdl-js-spinner', + "is-active" + ); + return ( + this.props.overlay ? +
+
+
+ :
+ ) + } +} + +export default mdlUpgrade(Loading, Object.assign({}, material, styles)); diff --git a/web/static/js/shared/loading/styles.css b/web/static/js/shared/loading/styles.css new file mode 100644 index 0000000..40679b5 --- /dev/null +++ b/web/static/js/shared/loading/styles.css @@ -0,0 +1,14 @@ + +.overlay { + background-color: rgba(255, 255, 255, 0.7); + width: 100%; + height: 80%; + position: absolute; + z-index: 10; + display: flex; + justify-content: center; +} + +.overlay div { + align-self: center; +}