diff --git a/__tests__/App.test.jsx b/__tests__/App.test.jsx index ae7366a74e..d69dab8929 100644 --- a/__tests__/App.test.jsx +++ b/__tests__/App.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; +import { HashRouter as Router } from 'react-router-dom'; import { shallow, mount } from 'enzyme'; import App from '../src/App'; import api from '../src/api'; @@ -7,6 +7,16 @@ import { API_LOGOUT, API_CONFIG } from '../src/endpoints'; import Dashboard from '../src/pages/Dashboard'; import Login from '../src/pages/Login'; +import { asyncFlush } from '../jest.setup'; + +const DEFAULT_ACTIVE_GROUP = 'views_group'; +const DEFAULT_ACTIVE_ITEM = 'views_group_dashboard'; + +const routeGroups = [{ + groupId: DEFAULT_ACTIVE_GROUP, + title: 'test', + routes: [{ path: '/home', title: 'Dashboard', component: Dashboard }], +}]; describe('', () => { test('renders without crashing', () => { @@ -18,7 +28,7 @@ describe('', () => { api.isAuthenticated = jest.fn(); api.isAuthenticated.mockReturnValue(false); - const appWrapper = mount(); + const appWrapper = mount(); const login = appWrapper.find(Login); expect(login.length).toBe(1); @@ -30,7 +40,7 @@ describe('', () => { api.isAuthenticated = jest.fn(); api.isAuthenticated.mockReturnValue(true); - const appWrapper = mount(); + const appWrapper = mount(); const dashboard = appWrapper.find(Dashboard); expect(dashboard.length).toBe(1); @@ -39,21 +49,30 @@ describe('', () => { }); test('onNavToggle sets state.isNavOpen to opposite', () => { - const appWrapper = shallow(); + const appWrapper = shallow(); expect(appWrapper.state().isNavOpen).toBe(true); appWrapper.instance().onNavToggle(); expect(appWrapper.state().isNavOpen).toBe(false); }); + test('onLogoClick sets selected nav back to defaults', () => { + const appWrapper = shallow(); + appWrapper.setState({ activeGroup: 'foo', activeItem: 'bar' }); + expect(appWrapper.state().activeItem).toBe('bar'); + expect(appWrapper.state().activeGroup).toBe('foo'); + appWrapper.instance().onLogoClick(); + expect(appWrapper.state().activeGroup).toBe(DEFAULT_ACTIVE_GROUP); + }); + test('api.logout called from logout button', async () => { - const logOutButtonSelector = 'button[aria-label="Logout"]'; api.get = jest.fn().mockImplementation(() => Promise.resolve({})); - const appWrapper = mount(); - const logOutButton = appWrapper.find(logOutButtonSelector); - expect(logOutButton.length).toBe(1); - logOutButton.simulate('click'); + const appWrapper = shallow(); + appWrapper.instance().onDevLogout(); appWrapper.setState({ activeGroup: 'foo', activeItem: 'bar' }); expect(api.get).toHaveBeenCalledWith(API_LOGOUT); + await asyncFlush(); + expect(appWrapper.state().activeItem).toBe(DEFAULT_ACTIVE_ITEM); + expect(appWrapper.state().activeGroup).toBe(DEFAULT_ACTIVE_GROUP); }); test('Componenet makes REST call to API_CONFIG endpoint when mounted', () => { diff --git a/__tests__/index.test.jsx b/__tests__/index.test.jsx index 654bc5a6d6..f2b79ba7da 100644 --- a/__tests__/index.test.jsx +++ b/__tests__/index.test.jsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'; import api from '../src/api'; -import indexToRender from '../src/index'; +import { main } from '../src/index'; const custom_logo = (
logo
); const custom_login_info = 'custom login info'; @@ -15,7 +15,7 @@ describe('index.jsx', () => { api.getRoot = jest.fn().mockImplementation(() => Promise .resolve({ data: { custom_logo, custom_login_info } })); - await indexToRender(); + await main(); expect(ReactDOM.render).toHaveBeenCalled(); }); diff --git a/src/App.jsx b/src/App.jsx index d08764acd7..0fd573d650 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,14 +1,12 @@ import React, { Fragment } from 'react'; import { ConfigContext } from './context'; -import { I18nProvider, I18n } from '@lingui/react'; -import { t } from '@lingui/macro'; import { + HashRouter as Router, Redirect, Switch, withRouter } from 'react-router-dom'; - import { BackgroundImage, BackgroundImageSrc, @@ -22,64 +20,56 @@ import { ToolbarItem } from '@patternfly/react-core'; import { global_breakpoint_md as breakpointMd } from '@patternfly/react-tokens'; +import { I18nProvider, I18n } from '@lingui/react'; +import { t } from '@lingui/macro'; import api from './api'; import { API_LOGOUT, API_CONFIG } from './endpoints'; +import ja from '../build/locales/ja/messages'; +import en from '../build/locales/en/messages'; + +import Login from './pages/Login'; import HelpDropdown from './components/HelpDropdown'; import LogoutButton from './components/LogoutButton'; import TowerLogo from './components/TowerLogo'; import ConditionalRedirect from './components/ConditionalRedirect'; import NavExpandableGroup from './components/NavExpandableGroup'; -import Applications from './pages/Applications'; -import Credentials from './pages/Credentials'; -import CredentialTypes from './pages/CredentialTypes'; -import Dashboard from './pages/Dashboard'; -import InstanceGroups from './pages/InstanceGroups'; -import Inventories from './pages/Inventories'; -import InventoryScripts from './pages/InventoryScripts'; -import Jobs from './pages/Jobs'; -import Login from './pages/Login'; -import ManagementJobs from './pages/ManagementJobs'; -import NotificationTemplates from './pages/NotificationTemplates'; -import Organizations from './pages/Organizations'; -import Portal from './pages/Portal'; -import Projects from './pages/Projects'; -import Schedules from './pages/Schedules'; -import AuthSettings from './pages/AuthSettings'; -import JobsSettings from './pages/JobsSettings'; -import SystemSettings from './pages/SystemSettings'; -import UISettings from './pages/UISettings'; -import License from './pages/License'; -import Teams from './pages/Teams'; -import Templates from './pages/Templates'; -import Users from './pages/Users'; - -import ja from '../build/locales/ja/messages'; -import en from '../build/locales/en/messages'; - const catalogs = { en, ja }; - -// This spits out the language and the region. Example: es-US +// Derive the language and the region from global user agent data. Example: es-US +// https://developer.mozilla.org/en-US/docs/Web/API/Navigator const language = (navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage; - const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0]; +// define background src image config +const backgroundConfig = { + [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' +}; class App extends React.Component { constructor(props) { super(props); - const isNavOpen = typeof window !== 'undefined' && window.innerWidth >= parseInt(breakpointMd.value, 10); + const isNavOpen = typeof window !== 'undefined' + && window.innerWidth >= parseInt(breakpointMd.value, 10); + this.state = { isNavOpen, config: {}, error: false, }; - } + }; onNavToggle = () => { this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen })); @@ -104,158 +94,102 @@ class App extends React.Component { } } - render() { - const { isNavOpen, config } = this.state; - const { logo, loginInfo, history } = this.props; - - const PageToolbar = ( - - - - - - - this.onDevLogout()} /> - - - - ); + render () { + const { isNavOpen } = this.state; + const { logo, loginInfo, history, routeConfig = [] } = this.props; + // extract a flattened array of all routes from the provided route config + const allRoutes = routeConfig.reduce((flattened, { routes }) => flattened.concat(routes), []); return ( - - - - - - api.isAuthenticated()} - redirectPath="/" - path="/login" - component={() => } - /> + + + + {({ i18n }) => ( - } - toolbar={PageToolbar} - showNavToggle - onNavToggle={this.onNavToggle} + + + + api.isAuthenticated()} + redirectPath="/" + path="/login" + component={() => } /> - )} - sidebar={( - - {({ i18n }) => ( - - )} - - )} - /> - )} - useCondensed - > - !api.isAuthenticated()} redirectPath="/login" exact path="/" component={() => ()} /> - !api.isAuthenticated()} redirectPath="/login" path="/home" component={Dashboard} /> - !api.isAuthenticated()} redirectPath="/login" path="/jobs" component={Jobs} /> - !api.isAuthenticated()} redirectPath="/login" path="/schedules" component={Schedules} /> - !api.isAuthenticated()} redirectPath="/login" path="/portal" component={Portal} /> - !api.isAuthenticated()} redirectPath="/login" path="/templates" component={Templates} /> - !api.isAuthenticated()} redirectPath="/login" path="/credentials" component={Credentials} /> - !api.isAuthenticated()} redirectPath="/login" path="/projects" component={Projects} /> - !api.isAuthenticated()} redirectPath="/login" path="/inventories" component={Inventories} /> - !api.isAuthenticated()} redirectPath="/login" path="/inventory_scripts" component={InventoryScripts} /> - !api.isAuthenticated()} redirectPath="/login" path="/organizations" component={Organizations} /> - !api.isAuthenticated()} redirectPath="/login" path="/users" component={Users} /> - !api.isAuthenticated()} redirectPath="/login" path="/teams" component={Teams} /> - !api.isAuthenticated()} redirectPath="/login" path="/credential_types" component={CredentialTypes} /> - !api.isAuthenticated()} redirectPath="/login" path="/notification_templates" component={NotificationTemplates} /> - !api.isAuthenticated()} redirectPath="/login" path="/management_jobs" component={ManagementJobs} /> - !api.isAuthenticated()} redirectPath="/login" path="/instance_groups" component={InstanceGroups} /> - !api.isAuthenticated()} redirectPath="/login" path="/applications" component={Applications} /> - !api.isAuthenticated()} redirectPath="/login" path="/auth_settings" component={AuthSettings} /> - !api.isAuthenticated()} redirectPath="/login" path="/jobs_settings" component={JobsSettings} /> - !api.isAuthenticated()} redirectPath="/login" path="/system_settings" component={SystemSettings} /> - !api.isAuthenticated()} redirectPath="/login" path="/ui_settings" component={UISettings} /> - !api.isAuthenticated()} redirectPath="/login" path="/license" component={License} /> - - + + } + toolbar={( + + + + + + + this.onDevLogout()} /> + + + + )} + showNavToggle + onNavToggle={this.onNavToggle} + /> + )} + sidebar={( + + + { routeConfig.map(({ groupId, routes, title }) => ( + ({ + path: route.path, + component: route.component, + title: i18n._(route.title) + }))} + /> + ))} + + + )} + /> + )} + > + !api.isAuthenticated()} + redirectPath="/login" + exact path="/" + component={() => ()} + /> + { allRoutes.map(({ component, path }) => ( + !api.isAuthenticated()} + component={component} + /> + ))} + + + + - - - - + )} + + + ); } } -export default withRouter(App); +export default App; diff --git a/src/api.js b/src/api.js index 62649fa0e5..9e07c3bac5 100644 --- a/src/api.js +++ b/src/api.js @@ -1,6 +1,10 @@ import axios from 'axios'; -import * as endpoints from './endpoints'; +import { + API_CONFIG, + API_LOGIN, + API_ROOT, +} from './endpoints'; const CSRF_COOKIE_NAME = 'csrftoken'; const CSRF_HEADER_NAME = 'X-CSRFToken'; @@ -32,7 +36,11 @@ class APIClient { return authenticated; } - async login (username, password, redirect = endpoints.API_CONFIG) { + getRoot () { + return this.http.get(API_ROOT); + } + + async login (username, password, redirect = API_CONFIG) { const un = encodeURIComponent(username); const pw = encodeURIComponent(password); const next = encodeURIComponent(redirect); @@ -40,8 +48,8 @@ class APIClient { const data = `username=${un}&password=${pw}&next=${next}`; const headers = { 'Content-Type': LOGIN_CONTENT_TYPE }; - await this.http.get(endpoints.API_LOGIN, { headers }); - await this.http.post(endpoints.API_LOGIN, data, { headers }); + await this.http.get(API_LOGIN, { headers }); + await this.http.post(API_LOGIN, data, { headers }); } get = (endpoint, params = {}) => this.http.get(endpoint, { params }); diff --git a/src/index.jsx b/src/index.jsx index 24a5eb8576..03a4264183 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,12 +1,8 @@ import React from 'react'; import { render } from 'react-dom'; -import { - HashRouter as Router -} from 'react-router-dom'; import App from './App'; import api from './api'; -import { API_ROOT } from './endpoints'; import '@patternfly/react-core/dist/styles/base.css'; import '@patternfly/patternfly-next/patternfly.css'; @@ -15,13 +11,186 @@ import './app.scss'; import './components/Pagination/styles.scss'; import './components/DataListToolbar/styles.scss'; -const el = document.getElementById('app'); +import Applications from './pages/Applications'; +import Credentials from './pages/Credentials'; +import CredentialTypes from './pages/CredentialTypes'; +import Dashboard from './pages/Dashboard'; +import InstanceGroups from './pages/InstanceGroups'; +import Inventories from './pages/Inventories'; +import InventoryScripts from './pages/InventoryScripts'; +import Jobs from './pages/Jobs'; +import Login from './pages/Login'; +import ManagementJobs from './pages/ManagementJobs'; +import NotificationTemplates from './pages/NotificationTemplates'; +import Organizations from './pages/Organizations'; +import Portal from './pages/Portal'; +import Projects from './pages/Projects'; +import Schedules from './pages/Schedules'; +import AuthSettings from './pages/AuthSettings'; +import JobsSettings from './pages/JobsSettings'; +import SystemSettings from './pages/SystemSettings'; +import UISettings from './pages/UISettings'; +import License from './pages/License'; +import Teams from './pages/Teams'; +import Templates from './pages/Templates'; +import Users from './pages/Users'; -const main = async () => { - const { custom_logo, custom_login_info } = await api.get(API_ROOT); - render(, el); +const routeGroups = [ + { + groupId: 'views_group', + title: 'Views', + routes: [ + { + path: '/home', + title: 'Dashboard', + component: Dashboard + }, + { + path: '/jobs', + title: 'Jobs', + component: Jobs + }, + { + path: '/schedules', + title: 'Schedules', + component: Schedules + }, + { + path: '/portal', + title: 'Portal Mode', + component: Portal + }, + ], + }, + { + groupId: 'resources_group', + title: "Resources", + routes: [ + { + path: '/templates', + title: 'Templates', + component: Templates + }, + { + path: '/credentials', + title: 'Credentials', + component: Credentials + }, + { + path: '/projects', + title: 'Projects', + component: Projects + }, + { + path: '/inventories', + title: 'Inventories', + component: Inventories + }, + { + path: '/inventory_scripts', + title: 'Inventory Scripts', + component: InventoryScripts + }, + ], + }, + { + groupId: 'access_group', + title: 'Access', + routes: [ + { + path: '/organizations', + title: 'Organizations', + component: Organizations + }, + { + path: '/users', + title: 'Users', + component: Users + }, + { + path: '/teams', + title: 'Teams', + component: Teams + }, + ], + }, + { + groupId: 'administration_group', + title: 'Administration', + routes: [ + { + path: '/credential_types', + title: 'Credential Types', + component: CredentialTypes + }, + { + path: '/notification_templates', + title: 'Notifications', + component: NotificationTemplates + }, + { + path: '/management_jobs', + title: 'Management Jobs', + component: ManagementJobs + }, + { + path: '/instance_groups', + title: 'Instance Groups', + component: InstanceGroups + }, + { + path: '/applications', + title: 'Integrations', + component: Applications + }, + ], + }, + { + groupId: 'settings_group', + title: 'Settings', + routes: [ + { + path: '/auth_settings', + title: 'Authentication', + component: AuthSettings + }, + { + path: '/jobs_settings', + title: 'Jobs', + component: JobsSettings + }, + { + path: '/system_settings', + title: 'System', + component: SystemSettings + }, + { + path: '/ui_settings', + title: 'User Interface', + component: UISettings + }, + { + path: '/license', + title: 'License', + component: License + }, + ], + }, +]; + + +export async function main () { + const el = document.getElementById('app'); + // fetch additional config from server + const { data } = await api.getRoot(); + const { custom_logo, custom_login_info } = data; + + render( + , el); }; -main(); - -export default main; +export default main();