diff --git a/src/App.jsx b/src/App.jsx index 7ff47fd2a3..b1d738f912 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import { render } from 'react-dom'; import { HashRouter as Router, @@ -7,20 +7,29 @@ import { Redirect, Switch, } from 'react-router-dom'; + import { + BackgroundImage, + BackgroundImageSrc, Brand, + Button, + ButtonVariant, Nav, - NavList, NavGroup, NavItem, Page, PageHeader, + PageSection, + PageSectionVariants, PageSidebar, - Title, + TextContent, + Text, Toolbar, ToolbarGroup, - ToolbarItem, + ToolbarItem } from '@patternfly/react-core'; +import { global_breakpoint_md as breakpointMd } from '@patternfly/react-tokens'; +import { css } from '@patternfly/react-styles'; import api from './api'; @@ -47,7 +56,6 @@ import Teams from './pages/Teams'; import Templates from './pages/Templates'; import Users from './pages/Users'; - const AuthenticatedRoute = ({ component: Component, ...rest }) => ( ( api.isAuthenticated() ? ( @@ -61,11 +69,28 @@ const AuthenticatedRoute = ({ component: Component, ...rest }) => ( )}/> ); +const UnauthenticatedRoute = ({ component: Component, ...rest }) => ( + ( + !api.isAuthenticated() ? ( + + ) : ( + + ) + )}/> +); + class App extends React.Component { constructor(props) { super(props); - this.state = { activeItem: 'dashboard', isNavOpen: true }; + this.state = { + activeItem: 'dashboard', + isNavOpen: (typeof window !== 'undefined' && + window.innerWidth >= parseInt(breakpointMd.value, 10)), + }; } onNavToggle = () => { @@ -78,93 +103,107 @@ class App extends React.Component { this.setState({ activeItem: itemId }); }; + onLogoClick = () => { + this.setState({ activeItem: "dashboard" }); + } + + onDevLogout = () => { + api.logout() + .then(() => { + this.setState({ activeItem: "dashboard" }); + }) + } + render() { const { activeItem, isNavOpen } = this.state; + const { logo, loginInfo } = this.props; return ( - - - ( - } - toolbar={( - - - Item 1 - - - Item 2 - Item 3 - - + + + + } /> + ( + } + avatar={} + showNavToggle + onNavToggle={this.onNavToggle} + /> )} - avatar="| avatar" - showNavToggle - onNavToggle={this.onNavToggle} - /> - )} - sidebar={( - - - Dashboard - Jobs - Schedules - My View - - - Templates - Credentials - Projects - Inventories - Inventory Scripts - - - Organizations - Users - Teams - - - Credential Types - Notifications - Management Jobs - Instance Groups - Applications - Settings - - - )} - /> - )}> - - ()} /> - - - - - - - - - - - - - - - - - - - - - )} /> - + sidebar={( + + + Dashboard + Jobs + Schedules + My View + + + Templates + Credentials + Projects + Inventories + Inventory Scripts + + + Organizations + Users + Teams + + + Credential Types + Notifications + Management Jobs + Instance Groups + Applications + Settings + + + )} + /> + )}> + + ()} /> + + + + + + + + + + + + + + + + + + + + + )} /> + + ); } @@ -172,4 +211,9 @@ class App extends React.Component { const el = document.getElementById('app'); -render(, el); +api.getRoot() + .then(({ data }) => { + const { custom_logo, custom_login_info } = data; + + render(, el); + }); diff --git a/src/api.js b/src/api.js index bd3ad42d2d..f442a8b765 100644 --- a/src/api.js +++ b/src/api.js @@ -2,6 +2,7 @@ import axios from 'axios'; const API_ROOT = '/api/'; const API_LOGIN = `${API_ROOT}login/`; +const API_LOGOUT = `${API_ROOT}logout/`; const API_V2 = `${API_ROOT}v2/`; const API_CONFIG = `${API_V2}config/`; const API_PROJECTS = `${API_V2}projects/`; @@ -12,7 +13,6 @@ const CSRF_HEADER_NAME = 'X-CSRFToken'; class APIClient { constructor () { - this.authenticated = false; // temporary this.http = axios.create({ xsrfCookieName: CSRF_COOKIE_NAME, xsrfHeaderName: CSRF_HEADER_NAME, @@ -20,7 +20,15 @@ class APIClient { } 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) { @@ -32,16 +40,15 @@ class APIClient { const headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; return this.http.get(API_LOGIN, { headers }) - .then(() => this.http.post(API_LOGIN, data, { headers })) - .then(res => { - this.authenticated = true; // temporary - - return res; - }); + .then(() => this.http.post(API_LOGIN, data, { headers })); } logout () { - return this.http.delete(API_LOGIN); + return this.http.get(API_LOGOUT); + } + + getConfig () { + return this.http.get(API_CONFIG); } getProjects () { @@ -51,6 +58,10 @@ class APIClient { getOrganizations () { return this.http.get(API_ORGANIZATIONS); } + + getRoot () { + return this.http.get(API_ROOT); + } } export default new APIClient(); diff --git a/src/app.scss b/src/app.scss index a70ea7fefa..1f6936f527 100644 --- a/src/app.scss +++ b/src/app.scss @@ -1,34 +1,95 @@ +// +// Header +// + .pf-l-page__header { --pf-l-page__header--MinHeight: 0px; display: flex; align-items: center; height: 60px; - background-color: #030303; - } .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--PaddingTop: 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; -} diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index 78e272ab4f..77fb3902a7 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -1,69 +1,135 @@ import React, { Component } from 'react'; import { Redirect } from 'react-router-dom'; - import { - Bullseye, + Brand, Button, - TextInput + Level, + LevelItem, + Login, + LoginBox, + LoginBoxHeader, + LoginBoxBody, + LoginFooter, + LoginHeaderBrand, + TextInput, } from '@patternfly/react-core'; +import TowerLogo from '../components/TowerLogo'; import api from '../api'; -class Login extends Component { - state = { - username: '', - password: '', - redirect: false, - }; +const LOGIN_ERROR_MESSAGE = 'Invalid username or password. Please try again.'; - handleUsernameChange = value => this.setState({ username: value }); +class LoginPage extends Component { + constructor (props) { + super(props); - handlePasswordChange = value => this.setState({ password: value }); + this.state = { + username: '', + password: '', + redirect: false, + loading: false, + }; + } + + componentWillUnmount () { + this.unmounting = true; // todo: state management + } + + safeSetState = obj => !this.unmounting && this.setState(obj); + + handleUsernameChange = value => this.safeSetState({ username: value, error: '' }); + + handlePasswordChange = value => this.safeSetState({ password: value, error: '' }); handleSubmit = event => { const { username, password } = this.state; event.preventDefault(); + this.safeSetState({ loading: true }); + 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 () { - const { username, password, redirect } = this.state; + const { username, password, redirect, loading, error } = this.state; + const { logo, loginInfo } = this.props; if (redirect) { return (); } return ( - -
-
- -
-
- -
- -
-
+ + {logo ? : } + + )} + footer={{ loginInfo }} + > + + + Welcome to Ansible Tower! Please Sign In. + + +
+
+ + +
+
+ + +
+ + +

+ { error } +

+
+ + + +
+
+
+
+
); } } -export default Login; +export default LoginPage;