mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
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:
parent
b48c435206
commit
6826bd5b6a
6 changed files with 106 additions and 170 deletions
|
|
@ -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}
|
||||
>
|
||||
×
|
||||
</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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
};
|
||||
|
|
@ -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: [],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue