Nav bar fixes (#569)

* Main nav item click brings user to default sub nav item

* Converts SiteNavHeader to a dumb component

* Stop passing dispatch to the FlashMessage component
This commit is contained in:
Mike Stone 2016-12-05 12:55:30 -05:00 committed by GitHub
parent b48c435206
commit 6826bd5b6a
6 changed files with 106 additions and 170 deletions

View file

@ -1,24 +1,12 @@
import React, { PropTypes } from 'react';
import { hideFlash } from '../../redux/nodes/notifications/actions';
import notificationInterface from '../../interfaces/notification';
const baseClass = 'flash-message';
const FlashMessage = ({ notification, dispatch }) => {
const FlashMessage = ({ notification, onRemoveFlash, onUndoActionClick }) => {
const { alertType, isVisible, message, undoAction } = notification;
const submitUndoAction = () => {
dispatch(undoAction);
dispatch(hideFlash);
return false;
};
const removeFlashMessage = () => {
dispatch(hideFlash);
return false;
};
if (!isVisible) {
return false;
}
@ -31,13 +19,13 @@ const FlashMessage = ({ notification, dispatch }) => {
<div className={`${baseClass}__action`}>
<button
className={`${baseClass}__undo button button--unstyled`}
onClick={submitUndoAction}
onClick={onUndoActionClick(undoAction)}
>
{undoAction && 'undo'}
</button>
<button
className={`${baseClass}__remove ${baseClass}__remove--${alertType} button button--unstyled`}
onClick={removeFlashMessage}
onClick={onRemoveFlash}
>
&times;
</button>
@ -47,8 +35,9 @@ const FlashMessage = ({ notification, dispatch }) => {
};
FlashMessage.propTypes = {
dispatch: PropTypes.func,
notification: notificationInterface,
onRemoveFlash: PropTypes.func,
onUndoActionClick: PropTypes.func,
};
export default FlashMessage;

View file

@ -1,19 +1,17 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import classnames from 'classnames';
import configInterface from 'interfaces/config';
import { logoutUser } from 'redux/nodes/auth/actions';
import userInterface from 'interfaces/user';
import Icon from 'components/Icon';
import kolideLogo from '../../../../assets/images/kolide-logo.svg';
import UserMenu from './UserMenu';
class SiteNavSidePanel extends Component {
class SiteNavHeader extends Component {
static propTypes = {
config: configInterface,
dispatch: PropTypes.func,
onLogoutUser: PropTypes.func,
user: userInterface,
};
@ -31,16 +29,6 @@ class SiteNavSidePanel extends Component {
document.addEventListener('mousedown', closeUserMenu, false);
}
onLogout = (evt) => {
evt.preventDefault();
const { dispatch } = this.props;
dispatch(logoutUser());
return false;
}
closeUserMenu = (evt) => {
const { headerNav } = this;
@ -61,11 +49,12 @@ class SiteNavSidePanel extends Component {
config: {
org_name: orgName,
},
onLogoutUser,
user,
} = this.props;
const { userMenuOpened } = this.state;
const { onLogout, toggleUserMenu } = this;
const { toggleUserMenu } = this;
const { enabled, username } = user;
const headerBaseClass = 'site-nav-header';
@ -99,7 +88,7 @@ class SiteNavSidePanel extends Component {
<UserMenu
isOpened={userMenuOpened}
onLogout={onLogout}
onLogout={onLogoutUser}
user={user}
/>
</button>
@ -108,5 +97,4 @@ class SiteNavSidePanel extends Component {
}
}
const ConnectedComponent = connect()(SiteNavSidePanel);
export default ConnectedComponent;
export default SiteNavHeader;

View file

@ -1,19 +1,14 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { isEqual } from 'lodash';
import { push } from 'react-router-redux';
import classnames from 'classnames';
import { logoutUser } from 'redux/nodes/auth/actions';
import userInterface from 'interfaces/user';
import Icon from 'components/Icon';
import { activeTabFromPathname, activeSubTabFromPathname } from './helpers';
import navItems from './navItems';
class SiteNavSidePanel extends Component {
static propTypes = {
dispatch: PropTypes.func,
onNavItemClick: PropTypes.func,
pathname: PropTypes.string,
user: userInterface,
};
@ -21,78 +16,24 @@ class SiteNavSidePanel extends Component {
constructor (props) {
super(props);
const { pathname, user: { admin } } = this.props;
const { user: { admin } } = this.props;
this.userNavItems = navItems(admin);
const activeTab = activeTabFromPathname(this.userNavItems, pathname);
const activeSubItem = activeSubTabFromPathname(activeTab, pathname);
this.state = {
activeTab,
activeSubItem,
showSubItems: false,
userMenuOpened: false,
};
}
componentWillReceiveProps (nextProps) {
if (isEqual(nextProps, this.props)) return false;
const { pathname } = nextProps;
const activeTab = activeTabFromPathname(this.userNavItems, pathname);
const activeSubItem = activeSubTabFromPathname(activeTab, pathname);
this.setState({
activeTab,
activeSubItem,
});
return false;
}
onLogout = (evt) => {
evt.preventDefault();
const { dispatch } = this.props;
dispatch(logoutUser());
return false;
}
setActiveSubItem = (activeSubItem) => {
onNavItemClick = (navItem) => {
return (evt) => {
evt.preventDefault();
if (activeSubItem) {
const { dispatch } = this.props;
const { path: { location: tabLocation } } = activeSubItem;
const { onNavItemClick: handleNavItemClick } = this.props;
const { pathname: navItemPathname } = navItem.location;
if (!tabLocation) return false;
dispatch(push(tabLocation));
}
return false;
};
}
setActiveTab = (activeTab) => {
return (evt) => {
evt.preventDefault();
const { pathname, dispatch } = this.props;
const activeSubItem = activeSubTabFromPathname(activeTab, pathname);
this.setState({ activeTab, activeSubItem });
const tabLocation = activeSubItem ? activeSubItem.path.location : activeTab.path.location;
dispatch(push(tabLocation));
return false;
return handleNavItemClick(navItemPathname);
};
}
@ -117,10 +58,10 @@ class SiteNavSidePanel extends Component {
}
renderNavItem = (navItem) => {
const { activeTab = {} } = this.state;
const { icon, name, subItems } = navItem;
const active = activeTab.name === name;
const { renderSubItems, setActiveTab } = this;
const { pathname } = this.props;
const { onNavItemClick, renderSubItems } = this;
const active = navItem.location.regex.test(pathname);
const navItemBaseClass = 'site-nav-item';
@ -133,7 +74,7 @@ class SiteNavSidePanel extends Component {
<li className={navItemClasses} key={`nav-item-${name}`}>
<button
className={`${navItemBaseClass}__button button button--unstyled`}
onClick={setActiveTab(navItem)}
onClick={onNavItemClick(navItem)}
style={{ width: '100%' }}
>
<Icon name={icon} className={`${navItemBaseClass}__icon`} />
@ -159,10 +100,10 @@ class SiteNavSidePanel extends Component {
}
renderSubItem = (subItem) => {
const { activeSubItem } = this.state;
const { icon, name, path } = subItem;
const active = activeSubItem === subItem;
const { setActiveSubItem } = this;
const { icon, name } = subItem;
const { pathname } = this.props;
const { onNavItemClick } = this;
const active = subItem.location.regex.test(pathname);
const baseSubItemClass = 'site-sub-item';
@ -177,10 +118,9 @@ class SiteNavSidePanel extends Component {
className={baseSubItemItemClass}
>
<button
key={`sub-item-${name}`}
onClick={setActiveSubItem(subItem)}
className={`${baseSubItemClass}__button button button--unstyled`}
to={path.location}
key={`sub-item-${name}`}
onClick={onNavItemClick(subItem)}
>
<span className={`${baseSubItemClass}__name`}>{name}</span>
<span className={`${baseSubItemClass}__icon`}><Icon name={icon} /></span>
@ -222,5 +162,4 @@ class SiteNavSidePanel extends Component {
}
}
const ConnectedComponent = connect()(SiteNavSidePanel);
export default ConnectedComponent;
export default SiteNavSidePanel;

View file

@ -1,23 +0,0 @@
import { find } from 'lodash';
export const activeTabFromPathname = (navItems, pathname) => {
return find(navItems, (item) => {
const { path: { regex } } = item;
return regex.test(pathname);
});
};
export const activeSubTabFromPathname = (activeTab, pathname) => {
if (!activeTab) return undefined;
const { subItems } = activeTab;
if (!subItems.length) {
return undefined;
}
return find(subItems, (subItem) => {
const { path: { regex } } = subItem;
return regex.test(pathname);
}) || subItems[0];
};

View file

@ -3,9 +3,9 @@ export default (admin) => {
{
icon: 'admin',
name: 'Admin',
path: {
regex: /^\/admin/,
location: '/admin/users',
location: {
regex: /^\/admin\/users/,
pathname: '/admin/users',
},
subItems: [],
},
@ -13,109 +13,105 @@ export default (admin) => {
const userNavItems = [
{
defaultPathname: '/hosts/manage',
icon: 'hosts',
name: 'Hosts',
path: {
location: {
regex: /^\/hosts/,
location: '/hosts/manage',
pathname: '/hosts/manage',
},
subItems: [
{
icon: 'hosts',
name: 'Manage Hosts',
path: {
location: {
regex: /\/hosts\/manage/,
location: '/hosts/manage',
pathname: '/hosts/manage',
},
},
{
icon: 'add-plus',
name: 'Add Hosts',
path: {
location: {
regex: /\/hosts\/new/,
location: '/hosts/new',
pathname: '/hosts/new',
},
},
],
},
{
defaultPathname: '/queries/results',
icon: 'query',
name: 'Query',
path: {
location: {
regex: /^\/queries/,
location: '/queries/results',
pathname: '/queries',
},
subItems: [
{
icon: 'query',
name: 'Manage Queries',
path: {
regex: /\/queries\/results/,
location: '/queries/results',
location: {
regex: /\/queries$/,
pathname: '/queries',
},
},
{
icon: 'pencil',
name: 'New Query',
path: {
location: {
regex: /\/queries\/new/,
location: '/queries/new',
pathname: '/queries/new',
},
},
],
},
{
defaultPathname: '/packs/all',
icon: 'packs',
name: 'Packs',
path: {
location: {
regex: /^\/packs/,
location: '/packs/all',
pathname: '/packs/all',
},
subItems: [
{
icon: 'packs',
name: 'Manage Packs',
path: {
location: {
regex: /\/packs\/all/,
location: '/packs/all',
pathname: '/packs/all',
},
},
{
icon: 'pencil',
name: 'New Pack',
path: {
location: {
regex: /\/packs\/new/,
location: '/packs/new',
pathname: '/packs/new',
},
},
],
},
{
defaultPathname: '/config/options',
icon: 'config',
name: 'Config',
path: {
location: {
regex: /^\/config/,
location: '/config/options',
pathname: '/config/options',
},
subItems: [
{
icon: 'config',
name: 'Osquery Options',
path: {
location: {
regex: /\/config\/options/,
location: '/config/options',
pathname: '/config/options',
},
},
{
icon: 'import',
name: 'Import Config',
path: {
location: {
regex: /\/config\/import/,
location: '/config/import',
pathname: '/config/import',
},
},
],
@ -123,7 +119,7 @@ export default (admin) => {
{
icon: 'help',
name: 'Help',
path: {
location: {
regex: /^\/help/,
},
subItems: [],

View file

@ -1,9 +1,12 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import classnames from 'classnames';
import { logoutUser } from 'redux/nodes/auth/actions';
import { push } from 'react-router-redux';
import configInterface from 'interfaces/config';
import FlashMessage from 'components/FlashMessage';
import { hideFlash } from 'redux/nodes/notifications/actions';
import SiteNavHeader from 'components/side_panels/SiteNavHeader';
import SiteNavSidePanel from 'components/side_panels/SiteNavSidePanel';
import notificationInterface from 'interfaces/notification';
@ -19,8 +22,45 @@ export class CoreLayout extends Component {
user: userInterface,
};
onLogoutUser = () => {
const { dispatch } = this.props;
dispatch(logoutUser());
return false;
}
onNavItemClick = (path) => {
const { dispatch } = this.props;
dispatch(push(path));
return false;
}
onRemoveFlash = () => {
const { dispatch } = this.props;
dispatch(hideFlash);
return false;
}
onUndoActionClick = (undoAction) => {
return (evt) => {
evt.preventDefault();
const { dispatch } = this.props;
const { onRemoveFlash } = this;
dispatch(undoAction);
return onRemoveFlash();
};
}
render () {
const { children, config, dispatch, notifications, showRightSidePanel, user } = this.props;
const { children, config, notifications, showRightSidePanel, user } = this.props;
const wrapperClass = classnames(
'core-wrapper',
{ 'core-wrapper--show-panel': showRightSidePanel }
@ -28,6 +68,7 @@ export class CoreLayout extends Component {
if (!user) return false;
const { onLogoutUser, onNavItemClick, onRemoveFlash, onUndoActionClick } = this;
const { pathname } = global.window.location;
return (
@ -35,16 +76,22 @@ export class CoreLayout extends Component {
<nav className="site-nav">
<SiteNavHeader
config={config}
onLogoutUser={onLogoutUser}
user={user}
/>
<SiteNavSidePanel
config={config}
onNavItemClick={onNavItemClick}
pathname={pathname}
user={user}
/>
</nav>
<div className={wrapperClass}>
<FlashMessage notification={notifications} dispatch={dispatch} />
<FlashMessage
notification={notifications}
onRemoveFlash={onRemoveFlash}
onUndoActionClick={onUndoActionClick}
/>
{children}
</div>
</div>