Merge pull request #68 from ansible/refactor-nav

replace navitem generator with expandable navgroup component
This commit is contained in:
Jake McDermott
2018-12-17 12:15:39 -05:00
committed by GitHub
3 changed files with 173 additions and 159 deletions

View File

@@ -0,0 +1,63 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { mount } from 'enzyme';
import { Nav } from '@patternfly/react-core';
import NavExpandableGroup from '../../src/components/NavExpandableGroup';
describe('NavExpandableGroup', () => {
test('initialization and render', () => {
const component = mount(
<MemoryRouter initialEntries={['/foo']}>
<Nav aria-label="Test Navigation">
<NavExpandableGroup
groupId="test"
title="Test"
routes={[
{ path: '/foo', title: 'Foo' },
{ path: '/bar', title: 'Bar' },
{ path: '/fiz', title: 'Fiz' },
]}
/>
</Nav>
</MemoryRouter>
).find('NavExpandableGroup').instance();
expect(component.navItemPaths).toEqual(['/foo', '/bar', '/fiz']);
expect(component.isActiveGroup()).toEqual(true);
});
describe('isActivePath', () => {
const params = [
['/fo', '/foo', false],
['/foo', '/foo', true],
['/foo/1/bar/fiz', '/foo', true],
['/foo/1/bar/fiz', 'foo', false],
['/foo/1/bar/fiz', 'foo/', false],
['/foo/1/bar/fiz', '/bar', false],
['/foo/1/bar/fiz', '/fiz', false],
];
params.forEach(([location, path, expected]) => {
test(`when location is ${location}', isActivePath('${path}') returns ${expected} `, () => {
const component = mount(
<MemoryRouter initialEntries={[location]}>
<Nav aria-label="Test Navigation">
<NavExpandableGroup
groupId="test"
title="Test"
routes={[
{ path: '/foo', title: 'Foo' },
{ path: '/bar', title: 'Bar' },
{ path: '/fiz', title: 'Fiz' },
]}
/>
</Nav>
</MemoryRouter>
).find('NavExpandableGroup').instance();
expect(component.isActivePath(path)).toEqual(expected);
});
});
});
});

View File

@@ -11,9 +11,7 @@ import {
BackgroundImage, BackgroundImage,
BackgroundImageSrc, BackgroundImageSrc,
Nav, Nav,
NavExpandable,
NavList, NavList,
NavItem,
Page, Page,
PageHeader, PageHeader,
PageSidebar, PageSidebar,
@@ -30,6 +28,7 @@ import HelpDropdown from './components/HelpDropdown';
import LogoutButton from './components/LogoutButton'; import LogoutButton from './components/LogoutButton';
import TowerLogo from './components/TowerLogo'; import TowerLogo from './components/TowerLogo';
import ConditionalRedirect from './components/ConditionalRedirect'; import ConditionalRedirect from './components/ConditionalRedirect';
import NavExpandableGroup from './components/NavExpandableGroup';
import Applications from './pages/Applications'; import Applications from './pages/Applications';
import Credentials from './pages/Credentials'; import Credentials from './pages/Credentials';
@@ -67,41 +66,6 @@ const language = (navigator.languages && navigator.languages[0])
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0]; const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
const SideNavItems = ({ items, history }) => {
const currentPath = history.location.pathname.split('/')[1];
let activeGroup;
if (currentPath !== '') {
[{ groupName: activeGroup }] = items
.map(({ groupName, routes }) => ({
groupName,
paths: routes.map(({ path }) => path)
}))
.filter(({ paths }) => paths.indexOf(currentPath) > -1);
} else {
activeGroup = 'views';
}
return (items.map(({ title, groupName, routes }) => (
<NavExpandable
key={groupName}
title={title}
groupId={`${groupName}_group`}
isActive={`${activeGroup}_group` === `${groupName}_group`}
isExpanded={`${activeGroup}_group` === `${groupName}_group`}
>
{routes.map(({ path, title: itemTitle }) => (
<NavItem
key={path}
to={`#/${path}`}
groupId={`${groupName}_group`}
isActive={currentPath === path}
>
{itemTitle}
</NavItem>
))}
</NavExpandable>
)));
};
class App extends React.Component { class App extends React.Component {
constructor (props) { constructor (props) {
@@ -160,7 +124,12 @@ class App extends React.Component {
}} }}
/> />
<Switch> <Switch>
<ConditionalRedirect shouldRedirect={() => api.isAuthenticated()} redirectPath="/" path="/login" component={() => <Login logo={logo} loginInfo={loginInfo} />} /> <ConditionalRedirect
shouldRedirect={() => api.isAuthenticated()}
redirectPath="/"
path="/login"
component={() => <Login logo={logo} loginInfo={loginInfo} />}
/>
<Fragment> <Fragment>
<Page <Page
header={( header={(
@@ -179,127 +148,56 @@ class App extends React.Component {
{({ i18n }) => ( {({ i18n }) => (
<Nav aria-label={i18n._(t`Primary Navigation`)}> <Nav aria-label={i18n._(t`Primary Navigation`)}>
<NavList> <NavList>
<SideNavItems <NavExpandableGroup
history={history} groupId="views_group"
items={[ title={i18n._("Views")}
{ routes={[
groupName: 'views', { path: '/home', title: i18n._('Dashboard') },
title: i18n._('Views'), { path: '/jobs', title: i18n._('Jobs') },
routes: [ { path: '/schedules', title: i18n._('Schedules') },
{ { path: '/portal', title: i18n._('Portal Mode') },
path: 'home', ]}
title: i18n._('Dashboard') />
}, <NavExpandableGroup
{ groupId="resources_group"
path: 'jobs', title={i18n._("Resources")}
title: i18n._('Jobs') routes={[
}, { path: '/templates', title: i18n._('Templates') },
{ { path: '/credentials', title: i18n._('Credentials') },
path: 'schedules', { path: '/projects', title: i18n._('Projects') },
title: i18n._('Schedules') { path: '/inventories', title: i18n._('Inventories') },
}, { path: '/inventory_scripts', title: i18n._('Inventory Scripts') }
{ ]}
path: 'portal', />
title: i18n._('Portal Mode') <NavExpandableGroup
}, groupId="access_group"
] title={i18n._("Access")}
}, routes={[
{ { path: '/organizations', title: i18n._('Organizations') },
groupName: 'resources', { path: '/users', title: i18n._('Users') },
title: i18n._('Resources'), { path: '/teams', title: i18n._('Teams') }
routes: [ ]}
{ />
path: 'templates', <NavExpandableGroup
title: i18n._('Templates') groupId="administration_group"
}, title={i18n._("Administration")}
{ routes={[
path: 'credentials', { path: '/credential_types', title: i18n._('Credential Types') },
title: i18n._('Credentials') { path: '/notification_templates', title: i18n._('Notifications') },
}, { path: '/management_jobs', title: i18n._('Management Jobs') },
{ { path: '/instance_groups', title: i18n._('Instance Groups') },
path: 'projects', { path: '/applications', title: i18n._('Integrations') }
title: i18n._('Projects') ]}
}, />
{ <NavExpandableGroup
path: 'inventories', groupId="settings_group"
title: i18n._('Inventories') title={i18n._("Settings")}
}, routes={[
{ { path: '/auth_settings', title: i18n._('Authentication') },
path: 'inventory_scripts', { path: '/jobs_settings', title: i18n._('Jobs') },
title: i18n._('Inventory Scripts') { path: '/system_settings', title: i18n._('System') },
} { path: '/ui_settings', title: i18n._('User Interface') },
] { path: '/license', title: i18n._('License') }
},
{
groupName: 'access',
title: i18n._('Access'),
routes: [
{
path: 'organizations',
title: i18n._('Organizations')
},
{
path: 'users',
title: i18n._('Users')
},
{
path: 'teams',
title: i18n._('Teams')
}
]
},
{
groupName: 'administration',
title: i18n._('Administration'),
routes: [
{
path: 'credential_types',
title: i18n._('Credential Types'),
},
{
path: 'notification_templates',
title: i18n._('Notifications')
},
{
path: 'management_jobs',
title: i18n._('Management Jobs')
},
{
path: 'instance_groups',
title: i18n._('Instance Groups')
},
{
path: 'applications',
title: i18n._('Integrations')
}
]
},
{
groupName: 'settings',
title: i18n._('Settings'),
routes: [
{
path: 'auth_settings',
title: i18n._('Authentication'),
},
{
path: 'jobs_settings',
title: i18n._('Jobs')
},
{
path: 'system_settings',
title: i18n._('System')
},
{
path: 'ui_settings',
title: i18n._('User Interface')
},
{
path: 'license',
title: i18n._('License')
}
]
}
]} ]}
/> />
</NavList> </NavList>

View File

@@ -0,0 +1,53 @@
import React, { Component } from 'react';
import {
withRouter
} from 'react-router-dom';
import {
NavExpandable,
NavItem,
} from '@patternfly/react-core';
class NavExpandableGroup extends Component {
constructor (props) {
super(props);
const { routes } = this.props;
// Extract a list of paths from the route params and store them for later. This creates
// an array of url paths associated with any NavItem component rendered by this component.
this.navItemPaths = routes.map(({ path }) => path);
}
isActiveGroup = () => this.navItemPaths.some(this.isActivePath);
isActivePath = (path) => {
const { history } = this.props;
return history.location.pathname.startsWith(path);
};
render () {
const { routes, groupId, staticContext, ...rest } = this.props;
const isActive = this.isActiveGroup();
return (
<NavExpandable
isActive={isActive}
isExpanded={isActive}
groupId={groupId}
{...rest}
>
{routes.map(({ path, title }) => (
<NavItem
groupId={groupId}
isActive={this.isActivePath(path)}
key={path}
to={`/#${path}`}
>
{title}
</NavItem>
))}
</NavExpandable>
);
}
}
export default withRouter(NavExpandableGroup);