Nav and login updates

This commit is contained in:
Jake McDermott
2018-10-15 12:18:30 -04:00
parent ff53a9c8ea
commit a54fb0e27d
4 changed files with 338 additions and 156 deletions

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { Fragment } from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import { import {
HashRouter as Router, HashRouter as Router,
@@ -7,20 +7,29 @@ import {
Redirect, Redirect,
Switch, Switch,
} from 'react-router-dom'; } from 'react-router-dom';
import { import {
BackgroundImage,
BackgroundImageSrc,
Brand, Brand,
Button,
ButtonVariant,
Nav, Nav,
NavList,
NavGroup, NavGroup,
NavItem, NavItem,
Page, Page,
PageHeader, PageHeader,
PageSection,
PageSectionVariants,
PageSidebar, PageSidebar,
Title, TextContent,
Text,
Toolbar, Toolbar,
ToolbarGroup, ToolbarGroup,
ToolbarItem, ToolbarItem
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { global_breakpoint_md as breakpointMd } from '@patternfly/react-tokens';
import { css } from '@patternfly/react-styles';
import api from './api'; import api from './api';
@@ -47,7 +56,6 @@ import Teams from './pages/Teams';
import Templates from './pages/Templates'; import Templates from './pages/Templates';
import Users from './pages/Users'; import Users from './pages/Users';
const AuthenticatedRoute = ({ component: Component, ...rest }) => ( const AuthenticatedRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => ( <Route {...rest} render={props => (
api.isAuthenticated() ? ( api.isAuthenticated() ? (
@@ -61,11 +69,28 @@ const AuthenticatedRoute = ({ component: Component, ...rest }) => (
)}/> )}/>
); );
const UnauthenticatedRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
!api.isAuthenticated() ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/',
state: { from: props.location }
}}/>
)
)}/>
);
class App extends React.Component { class App extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { activeItem: 'dashboard', isNavOpen: true }; this.state = {
activeItem: 'dashboard',
isNavOpen: (typeof window !== 'undefined' &&
window.innerWidth >= parseInt(breakpointMd.value, 10)),
};
} }
onNavToggle = () => { onNavToggle = () => {
@@ -78,30 +103,43 @@ class App extends React.Component {
this.setState({ activeItem: itemId }); this.setState({ activeItem: itemId });
}; };
onLogoClick = () => {
this.setState({ activeItem: "dashboard" });
}
onDevLogout = () => {
api.logout()
.then(() => {
this.setState({ activeItem: "dashboard" });
})
}
render() { render() {
const { activeItem, isNavOpen } = this.state; const { activeItem, isNavOpen } = this.state;
const { logo, loginInfo } = this.props;
return ( return (
<Router> <Router>
<Fragment>
<BackgroundImage src={{
[BackgroundImageSrc.lg]: '/assets/images/pfbg_1200.jpg',
[BackgroundImageSrc.md]: '/assets/images/pfbg_992.jpg',
[BackgroundImageSrc.md2x]: '/assets/images/pfbg_992@2x.jpg',
[BackgroundImageSrc.sm]: '/assets/images/pfbg_768.jpg',
[BackgroundImageSrc.sm2x]: '/assets/images/pfbg_768@2x.jpg',
[BackgroundImageSrc.xl]: '/assets/images/pfbg_2000.jpg',
[BackgroundImageSrc.xs]: '/assets/images/pfbg_576.jpg',
[BackgroundImageSrc.xs2x]: '/assets/images/pfbg_576@2x.jpg',
[BackgroundImageSrc.filter]: '/assets/images/background-filter.svg'
}} />
<Switch> <Switch>
<Route path="/login" component={Login} /> <UnauthenticatedRoute path="/login" component={() => <Login logo={logo} loginInfo={loginInfo} />} />
<AuthenticatedRoute component={() => ( <AuthenticatedRoute component={() => (
<Page <Page
header={( header={(
<PageHeader <PageHeader
logo={<TowerLogo />} logo={<TowerLogo onClick={this.onLogoClick} />}
toolbar={( avatar={<i className="fas fa-user" onClick={this.onDevLogout}></i>}
<Toolbar>
<ToolbarGroup>
<ToolbarItem>Item 1</ToolbarItem>
</ToolbarGroup>
<ToolbarGroup>
<ToolbarItem>Item 2</ToolbarItem>
<ToolbarItem>Item 3</ToolbarItem>
</ToolbarGroup>
</Toolbar>
)}
avatar="| avatar"
showNavToggle showNavToggle
onNavToggle={this.onNavToggle} onNavToggle={this.onNavToggle}
/> />
@@ -165,6 +203,7 @@ class App extends React.Component {
</Page> </Page>
)} /> )} />
</Switch> </Switch>
</Fragment>
</Router> </Router>
); );
} }
@@ -172,4 +211,9 @@ class App extends React.Component {
const el = document.getElementById('app'); const el = document.getElementById('app');
render(<App />, el); api.getRoot()
.then(({ data }) => {
const { custom_logo, custom_login_info } = data;
render(<App logo={custom_logo} loginInfo={custom_login_info} />, el);
});

View File

@@ -2,6 +2,7 @@ import axios from 'axios';
const API_ROOT = '/api/'; const API_ROOT = '/api/';
const API_LOGIN = `${API_ROOT}login/`; const API_LOGIN = `${API_ROOT}login/`;
const API_LOGOUT = `${API_ROOT}logout/`;
const API_V2 = `${API_ROOT}v2/`; const API_V2 = `${API_ROOT}v2/`;
const API_CONFIG = `${API_V2}config/`; const API_CONFIG = `${API_V2}config/`;
const API_PROJECTS = `${API_V2}projects/`; const API_PROJECTS = `${API_V2}projects/`;
@@ -12,7 +13,6 @@ const CSRF_HEADER_NAME = 'X-CSRFToken';
class APIClient { class APIClient {
constructor () { constructor () {
this.authenticated = false; // temporary
this.http = axios.create({ this.http = axios.create({
xsrfCookieName: CSRF_COOKIE_NAME, xsrfCookieName: CSRF_COOKIE_NAME,
xsrfHeaderName: CSRF_HEADER_NAME, xsrfHeaderName: CSRF_HEADER_NAME,
@@ -20,7 +20,15 @@ class APIClient {
} }
isAuthenticated () { isAuthenticated () {
return this.authenticated; let authenticated = false;
const parsed = (`; ${document.cookie}`).split('; userLoggedIn=');
if (parsed.length === 2) {
authenticated = parsed.pop().split(';').shift() === 'true';
}
return authenticated;
} }
login (username, password, redirect = API_CONFIG) { login (username, password, redirect = API_CONFIG) {
@@ -32,16 +40,15 @@ class APIClient {
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
return this.http.get(API_LOGIN, { headers }) return this.http.get(API_LOGIN, { headers })
.then(() => this.http.post(API_LOGIN, data, { headers })) .then(() => this.http.post(API_LOGIN, data, { headers }));
.then(res => {
this.authenticated = true; // temporary
return res;
});
} }
logout () { logout () {
return this.http.delete(API_LOGIN); return this.http.get(API_LOGOUT);
}
getConfig () {
return this.http.get(API_CONFIG);
} }
getProjects () { getProjects () {
@@ -51,6 +58,10 @@ class APIClient {
getOrganizations () { getOrganizations () {
return this.http.get(API_ORGANIZATIONS); return this.http.get(API_ORGANIZATIONS);
} }
getRoot () {
return this.http.get(API_ROOT);
}
} }
export default new APIClient(); export default new APIClient();

View File

@@ -1,34 +1,95 @@
//
// Header
//
.pf-l-page__header { .pf-l-page__header {
--pf-l-page__header--MinHeight: 0px; --pf-l-page__header--MinHeight: 0px;
display: flex; display: flex;
align-items: center; align-items: center;
height: 60px; height: 60px;
background-color: #030303;
} }
.pf-l-page__header-brand { .pf-l-page__header-brand {
--pf-l-page__header-brand--PaddingBottom: 0px; align-self: center;
height: 60px;
max-width: 190px;
padding: 0px;
margin: 0px;
} }
.pf-l-page__header-tools {
align-self: center;
height: 60px;
padding-left: 190px;
.fa-user:hover {
// temporary dev logout
cursor: pointer;
}
}
.pf-l-toolbar {
align-self: center;
height: 60px;
}
.pf-l-page__header-brand-link {
align-self: center;
}
.pf-l-page__header-brand-link img {
transform: scale(1.1, 1.1);
position: relative;
top: 6px;
}
.pf-l-page__header-brand-toggle {
align-self: center;
position: relative;
right: 14px;
--pf-l-page__header-brand-link--MarginLeft: 0px;
--pf-l-page__header-brand-link--MarginLeft: 0px;
button {
--pf-l-page__header-sidebar-toggle--FontSize: 18px;
}
}
//
// Side Navigation
//
.pf-c-nav {
overflow-y: auto;
}
.pf-c-nav__section {
--pf-c-nav__section--MarginTop: 8px;
}
.pf-l-page__sidebar{
--pf-l-page__sidebar--Width--lg: 190px;
}
.pf-c-nav__section + .pf-c-nav__section {
--pf-c-nav__section--MarginTop: 8px;
}
.pf-c-nav__simple-list .pf-c-nav__link {
--pf-c-nav__simple-list-link--PaddingLeft: 24px;
--pf-c-nav__simple-list-link--PaddingBottom: 6px;
--pf-c-nav__simple-list-link--PaddingTop: 6px;
}
.pf-c-nav__section-title {
--pf-c-nav__section-title--PaddingLeft: 24px;
}
//
// Page
//
.pf-l-page__main-section { .pf-l-page__main-section {
--pf-l-page__main-section--PaddingTop: 11px; --pf-l-page__main-section--PaddingTop: 11px;
--pf-l-page__main-section--PaddingLeft: 11px; --pf-l-page__main-section--PaddingLeft: 11px;
} }
.pf-c-nav__section + .pf-c-nav__section {
--pf-c-nav__section--MarginTop: 16px;
}
.pf-l-page__header-brand-toggle {
padding-bottom: 4px;
padding-right: 0px;
}
.pf-l-page__header-brand-link {
transform: scale(0.75, 0.75);
}
.pf-l-page__sidebar{
--pf-l-page__sidebar--Width--lg: 200px;
}

View File

@@ -1,69 +1,135 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import { import {
Bullseye, Brand,
Button, Button,
TextInput Level,
LevelItem,
Login,
LoginBox,
LoginBoxHeader,
LoginBoxBody,
LoginFooter,
LoginHeaderBrand,
TextInput,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import TowerLogo from '../components/TowerLogo';
import api from '../api'; import api from '../api';
class Login extends Component { const LOGIN_ERROR_MESSAGE = 'Invalid username or password. Please try again.';
state = {
class LoginPage extends Component {
constructor (props) {
super(props);
this.state = {
username: '', username: '',
password: '', password: '',
redirect: false, redirect: false,
loading: false,
}; };
}
handleUsernameChange = value => this.setState({ username: value }); componentWillUnmount () {
this.unmounting = true; // todo: state management
}
handlePasswordChange = value => this.setState({ password: value }); safeSetState = obj => !this.unmounting && this.setState(obj);
handleUsernameChange = value => this.safeSetState({ username: value, error: '' });
handlePasswordChange = value => this.safeSetState({ password: value, error: '' });
handleSubmit = event => { handleSubmit = event => {
const { username, password } = this.state; const { username, password } = this.state;
event.preventDefault(); event.preventDefault();
this.safeSetState({ loading: true });
api.login(username, password) api.login(username, password)
.then(() => this.setState({ redirect: true })); .then(() => this.safeSetState({ redirect: true }))
.catch(error => {
if (error.response.status === 401) {
this.safeSetState({ error: LOGIN_ERROR_MESSAGE });
}
})
.finally(() => {
this.safeSetState({ loading: false });
});
} }
render () { render () {
const { username, password, redirect } = this.state; const { username, password, redirect, loading, error } = this.state;
const { logo, loginInfo } = this.props;
if (redirect) { if (redirect) {
return (<Redirect to="/" />); return (<Redirect to="/" />);
} }
return ( return (
<Bullseye> <Login
<form onSubmit={this.handleSubmit}> header={(
<div> <LoginHeaderBrand>
{logo ? <Brand src={`data:image/jpeg;${logo}`} alt="logo brand" /> : <TowerLogo />}
</LoginHeaderBrand>
)}
footer={<LoginFooter>{ loginInfo }</LoginFooter>}
>
<LoginBox>
<LoginBoxHeader>
Welcome to Ansible Tower! Please Sign In.
</LoginBoxHeader>
<LoginBoxBody>
<form className="pf-c-form" onSubmit={this.handleSubmit}>
<div className="pf-c-form__group" id="username">
<label className="pf-c-form__label" htmlFor="username">
Username
<span className="pf-c-form__label__required" aria-hidden="true">&#42;</span>
</label>
<TextInput <TextInput
autoComplete="off"
aria-label="Username" aria-label="Username"
name="username" name="username"
type="text" type="text"
onChange={this.handleUsernameChange} isDisabled={loading}
value={username} value={username}
onChange={this.handleUsernameChange}
/> />
</div> </div>
<div> <div className="pf-c-form__group">
<label className="pf-c-form__label" htmlFor="pw">
Password
<span className="pf-c-form__label__required" aria-hidden="true">&#42;</span>
</label>
<TextInput <TextInput
aria-label="Password" aria-label="Password"
name="password" name="password"
type="password" type="password"
onChange={this.handlePasswordChange} isDisabled={loading}
value={password} value={password}
onChange={this.handlePasswordChange}
/> />
</div> </div>
<Button type="submit"> <Level>
Login <LevelItem>
<p className="pf-c-form__helper-text pf-m-error" aria-live="polite">
{ error }
</p>
</LevelItem>
<LevelItem>
<Button type="submit" isDisabled={loading}>
Sign In
</Button> </Button>
</LevelItem>
</Level>
</form> </form>
</Bullseye> </LoginBoxBody>
</LoginBox>
</Login>
); );
} }
} }
export default Login; export default LoginPage;