diff --git a/gui/__mocks__/fileMock.js b/gui/__mocks__/fileMock.js new file mode 100644 index 00000000000..0e56c5b5f76 --- /dev/null +++ b/gui/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub' diff --git a/gui/__tests__/containers/SignIn.test.js b/gui/__tests__/containers/SignIn.test.js index 13ef34c020f..8fe696dc21d 100644 --- a/gui/__tests__/containers/SignIn.test.js +++ b/gui/__tests__/containers/SignIn.test.js @@ -65,12 +65,6 @@ describe('containers/SignIn', () => { currentUrl: '/signin' }) expect(newState.authentication.allowed).toEqual(false) - expect(newState.authentication.errors).toEqual([]) - }) - - it('cannot submit an empty form', async () => { - const store = createStore() - const wrapper = mountSignIn(store) - expect(wrapper.find('form button').getDOMNode().disabled).toEqual(true) + expect(wrapper.text()).toContain('Your email or password is incorrect. Please try again') }) }) diff --git a/gui/src/App.js b/gui/src/App.js index bdb43811df8..2abbc873980 100644 --- a/gui/src/App.js +++ b/gui/src/App.js @@ -1,8 +1,9 @@ import React, { PureComponent } from 'react' -import Layout from 'Layout' -import createStore from 'connectors/redux' import { Provider } from 'react-redux' import { hot } from 'react-hot-loader' +import createStore from 'connectors/redux' +import Layout from 'Layout' +import './index.css' class App extends PureComponent { // Remove the server-side injected CSS. diff --git a/gui/src/Layout.js b/gui/src/Layout.js index e44c43d2902..18a3d62d301 100644 --- a/gui/src/Layout.js +++ b/gui/src/Layout.js @@ -1,115 +1,49 @@ -import React from 'react' -import Routes from 'react-static-routes' +import React, { PureComponent } from 'react' import CssBaseline from '@material-ui/core/CssBaseline' -import Grid from '@material-ui/core/Grid' -import PrivateRoute from './PrivateRoute' -import Header from 'containers/Header' -import Loading from 'components/Loading' -import Notifications from 'containers/Notifications' -import universal from 'react-universal-component' -import { Redirect } from 'react-router' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' import { Router, Route, Switch } from 'react-static' +import { Redirect } from 'react-router' +import Routes from 'react-static-routes' import { hot } from 'react-hot-loader' -import { withStyles } from '@material-ui/core/styles' -import { connect } from 'react-redux' -import { bindActionCreators } from 'redux' -import { useHooks, useState } from 'use-react-hooks' +import universal from 'react-universal-component' +import Loading from 'components/Loading' +import Private from './Private' +import PrivateRoute from './PrivateRoute' -// Asynchronously load routes that are chunked via code-splitting -// 'import' as a function must take a string. It can't take a variable. const uniOpts = { loading: Loading } -const DashboardsIndex = universal(import('./containers/Dashboards/Index'), uniOpts) -const JobsIndex = universal(import('./containers/Jobs/Index'), uniOpts) -const JobsShow = universal(import('./containers/Jobs/Show'), uniOpts) -const JobsDefinition = universal(import('./containers/Jobs/Definition'), uniOpts) -const JobsNew = universal(import('./containers/Jobs/New'), uniOpts) -const BridgesIndex = universal(import('./containers/Bridges/Index'), uniOpts) -const BridgesNew = universal(import('./containers/Bridges/New'), uniOpts) -const BridgesShow = universal(import('./containers/Bridges/Show'), uniOpts) -const BridgesEdit = universal(import('./containers/Bridges/Edit'), uniOpts) -const JobRunsIndex = universal(import('./containers/JobRuns/Index'), uniOpts) -const JobRunsShow = universal(import('./containers/JobRuns/Show'), uniOpts) -const JobRunsShowJson = universal(import('./containers/JobRuns/ShowJson'), uniOpts) -const Configuration = universal(import('./containers/Configuration'), uniOpts) const SignIn = universal(import('./containers/SignIn'), uniOpts) const SignOut = universal(import('./containers/SignOut'), uniOpts) -const styles = theme => { - return { - content: { - marginTop: 0, - marginBottom: theme.spacing.unit * 5 +class Layout extends PureComponent { + // Remove the server-side injected CSS. + componentDidMount () { + const jssStyles = document.getElementById('jss-server-side') + if (jssStyles && jssStyles.parentNode) { + jssStyles.parentNode.removeChild(jssStyles) } } -} - -const Layout = useHooks(props => { - const [headerHeight, resizeHeaderHeight] = useState(0) - const onHeaderResize = (_width, height) => { - resizeHeaderHeight(height) + render () { + const { redirectTo } = this.props + + return ( + + + + + + + + {redirectTo && } + + + + + + ) } - - const { classes, redirectTo } = props - - return ( - - - -
- -
{ props.drawerContainer = ref }} - style={{ paddingTop: headerHeight }} - > - - -
- - - - {redirectTo && } - ( - - )} - /> - - - - } - /> - - - - - - - - - - - - - -
-
- - - ) -}) +} const mapStateToProps = state => ({ redirectTo: state.redirect.to @@ -125,4 +59,4 @@ export const ConnectedLayout = connect( mapDispatchToProps )(Layout) -export default hot(module)(withStyles(styles)(ConnectedLayout)) +export default hot(module)(ConnectedLayout) diff --git a/gui/src/Private.js b/gui/src/Private.js new file mode 100644 index 00000000000..40d3160ad7f --- /dev/null +++ b/gui/src/Private.js @@ -0,0 +1,104 @@ +import React from 'react' +import Routes from 'react-static-routes' +import Grid from '@material-ui/core/Grid' +import universal from 'react-universal-component' +import { Switch } from 'react-static' +import { hot } from 'react-hot-loader' +import { withStyles } from '@material-ui/core/styles' +import { useHooks, useState } from 'use-react-hooks' +import Header from 'containers/Header' +import Loading from 'components/Loading' +import Notifications from 'containers/Notifications' +import PrivateRoute from './PrivateRoute' + +// Asynchronously load routes that are chunked via code-splitting +// 'import' as a function must take a string. It can't take a variable. +const uniOpts = { loading: Loading } +const DashboardsIndex = universal(import('./containers/Dashboards/Index'), uniOpts) +const JobsIndex = universal(import('./containers/Jobs/Index'), uniOpts) +const JobsShow = universal(import('./containers/Jobs/Show'), uniOpts) +const JobsDefinition = universal(import('./containers/Jobs/Definition'), uniOpts) +const JobsNew = universal(import('./containers/Jobs/New'), uniOpts) +const BridgesIndex = universal(import('./containers/Bridges/Index'), uniOpts) +const BridgesNew = universal(import('./containers/Bridges/New'), uniOpts) +const BridgesShow = universal(import('./containers/Bridges/Show'), uniOpts) +const BridgesEdit = universal(import('./containers/Bridges/Edit'), uniOpts) +const JobRunsIndex = universal(import('./containers/JobRuns/Index'), uniOpts) +const JobRunsShow = universal(import('./containers/JobRuns/Show'), uniOpts) +const JobRunsShowJson = universal(import('./containers/JobRuns/ShowJson'), uniOpts) +const Configuration = universal(import('./containers/Configuration'), uniOpts) + +const styles = theme => { + return { + content: { + marginTop: 0, + marginBottom: theme.spacing.unit * 5 + } + } +} + +const Private = useHooks(props => { + const [headerHeight, resizeHeaderHeight] = useState(0) + + const onHeaderResize = (_width, height) => { + resizeHeaderHeight(height) + } + + const { classes } = props + + return ( + + +
+ +
{ props.drawerContainer = ref }} + style={{ paddingTop: headerHeight }} + > + + +
+ + ( + + )} + /> + + + + } + /> + + + + + + + + + + + + + +
+
+ + + ) +}) + +export default hot(module)(withStyles(styles)(Private)) diff --git a/gui/src/components/Logo.js b/gui/src/components/Logo.js index 013b3945045..c32740d6ad2 100644 --- a/gui/src/components/Logo.js +++ b/gui/src/components/Logo.js @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' +import pick from 'lodash/pick' import Image from './Image' -import logo from '../images/chainlink-operator-logo.svg' import { withStyles } from '@material-ui/core/styles' const styles = theme => { @@ -16,21 +16,16 @@ const styles = theme => { } } -const Logo = ({ width, height }) => { - const size = { width, height } - - return ( - Chainlink Operator - ) +const Logo = props => { + const imageProps = pick(props, ['src', 'width', 'height', 'alt']) + return } Logo.propTypes = { + src: PropTypes.string.isRequired, width: PropTypes.number, - height: PropTypes.number + height: PropTypes.number, + alt: PropTypes.string } export default withStyles(styles)(Logo) diff --git a/gui/src/components/Logos/Hexagon.js b/gui/src/components/Logos/Hexagon.js new file mode 100644 index 00000000000..d7307afe8e9 --- /dev/null +++ b/gui/src/components/Logos/Hexagon.js @@ -0,0 +1,21 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Logo from '../Logo' +import src from '../../images/icon-logo-blue.svg' + +const Hexagon = props => { + return ( + + ) +} + +Hexagon.propTypes = { + width: PropTypes.number, + height: PropTypes.number +} + +export default Hexagon diff --git a/gui/src/components/Logos/Main.js b/gui/src/components/Logos/Main.js new file mode 100644 index 00000000000..eb494a5bc7a --- /dev/null +++ b/gui/src/components/Logos/Main.js @@ -0,0 +1,21 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Logo from '../Logo' +import src from '../../images/chainlink-operator-logo.svg' + +const Main = props => { + return ( + + ) +} + +Main.propTypes = { + width: PropTypes.number, + height: PropTypes.number +} + +export default Main diff --git a/gui/src/containers/Header.js b/gui/src/containers/Header.js index 87e8ee11c81..748855e9feb 100644 --- a/gui/src/containers/Header.js +++ b/gui/src/containers/Header.js @@ -19,7 +19,7 @@ import IconButton from '@material-ui/core/IconButton' import MenuIcon from '@material-ui/icons/Menu' import Portal from '@material-ui/core/Portal' import LoadingBar from 'components/LoadingBar' -import Logo from 'components/Logo' +import MainLogo from 'components/Logos/Main' import AvatarMenu from 'components/AvatarMenu' import { submitSignOut } from 'actions' import fetchCountSelector from 'selectors/fetchCount' @@ -140,7 +140,7 @@ const Header = useHooks(props => { - + diff --git a/gui/src/containers/SignIn.js b/gui/src/containers/SignIn.js index 2fa25866f6e..c740e5ffca6 100644 --- a/gui/src/containers/SignIn.js +++ b/gui/src/containers/SignIn.js @@ -3,20 +3,38 @@ import { connect } from 'react-redux' import { Redirect } from 'react-router' import { withStyles } from '@material-ui/core/styles' import Button from '@material-ui/core/Button' +import Card from '@material-ui/core/Card' +import CardContent from '@material-ui/core/CardContent' import Typography from '@material-ui/core/Typography' import TextField from '@material-ui/core/TextField' import { Grid } from '@material-ui/core' +import { useHooks, useState } from 'use-react-hooks' +import { hot } from 'react-hot-loader' import { submitSignIn } from 'actions' -import Title from 'components/Title' +import HexagonLogo from 'components/Logos/Hexagon' import matchRouteAndMapDispatchToProps from 'utils/matchRouteAndMapDispatchToProps' -import { useHooks, useState } from 'use-react-hooks' const styles = theme => ({ - button: { - margin: theme.spacing.unit * 5 + container: { + height: '100%' + }, + cardContent: { + paddingTop: theme.spacing.unit * 6, + paddingLeft: theme.spacing.unit * 4, + paddingRight: theme.spacing.unit * 4, + '&:last-child': { + paddingBottom: theme.spacing.unit * 6 + } + }, + headerRow: { + textAlign: 'center' }, - title: { - marginTop: theme.spacing.unit * 5 + error: { + backgroundColor: theme.palette.error.light, + marginTop: theme.spacing.unit * 2 + }, + errorText: { + color: theme.palette.error.main } }) @@ -31,55 +49,96 @@ export const SignIn = useHooks((props) => { e.preventDefault() props.submitSignIn({ email, password }) } - const { classes, fetching, authenticated } = props - const enabled = email.length > 0 && password.length > 0 + const { classes, fetching, authenticated, errors } = props if (authenticated) return return ( -
- - Sign In to Chainlink - - - - {fetching && ( - - Signing in... - - )} + + + + + + + + + + + + + + Operator + + + + + + {errors.length > 0 && errors.map(({ detail }, idx) => { + return ( + + + + + {detail} + + + + + ) + })} + + + 0} + variant='outlined' + fullWidth + /> + + + 0} + variant='outlined' + fullWidth + /> + + + + + + + + + {fetching && ( + + Signing in... + + )} + + + + - + ) -} -) +}) const mapStateToProps = state => ({ fetching: state.authentication.fetching, - authenticated: state.authentication.allowed + authenticated: state.authentication.allowed, + errors: state.notifications.errors }) export const ConnectedSignIn = connect( @@ -87,4 +146,4 @@ export const ConnectedSignIn = connect( matchRouteAndMapDispatchToProps({ submitSignIn }) )(SignIn) -export default withStyles(styles)(ConnectedSignIn) +export default hot(module)(withStyles(styles)(ConnectedSignIn)) diff --git a/gui/src/images/icon-logo-blue.svg b/gui/src/images/icon-logo-blue.svg new file mode 100644 index 00000000000..e35f1df284c --- /dev/null +++ b/gui/src/images/icon-logo-blue.svg @@ -0,0 +1 @@ + diff --git a/gui/src/index.css b/gui/src/index.css new file mode 100644 index 00000000000..29d0d4c55ee --- /dev/null +++ b/gui/src/index.css @@ -0,0 +1,9 @@ +body { + height: 100vh; + min-height: 100vh; +} + +#root { + height: 100%; + min-height: 100%; +} diff --git a/jest.config.js b/jest.config.js index 1dac83b3067..95c0f589817 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,6 +11,7 @@ module.exports = { '/node_modules/' ], moduleNameMapper: { + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/gui/__mocks__/fileMock.js', '\\.(css|less|sass|scss)$': '/gui/__mocks__/styleMock.js' } }