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();