diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index e96a4015..ace0f480 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { styled } from '@mui/material/styles'; import { NavLink, Link, useHistory, useLocation } from 'react-router-dom'; import { @@ -8,9 +8,7 @@ import { Drawer, ListItem, List, - TextField, - useMediaQuery, - useTheme + TextField } from '@mui/material'; import { Menu as MenuIcon, @@ -34,7 +32,6 @@ const classes = { menuButton: `${PREFIX}-menuButton`, logo: `${PREFIX}-logo`, spacing: `${PREFIX}-spacing`, - activeLink: `${PREFIX}-activeLink`, activeMobileLink: `${PREFIX}-activeMobileLink`, link: `${PREFIX}-link`, userLink: `${PREFIX}-userLink`, @@ -46,44 +43,28 @@ const classes = { const Root = styled('div')(({ theme }) => ({ [`.${classes.inner}`]: { - maxWidth: 1440, - width: '250%', + maxWidth: '1440px', + width: '100%', margin: '0 auto' }, [`.${classes.menuButton}`]: { marginLeft: theme.spacing(2), - display: 'flex', - [theme.breakpoints.up('sm')]: { - display: 'none' - } + display: 'flex' }, [`.${classes.logo}`]: { width: 150, + minWidth: 150, padding: theme.spacing(), paddingLeft: 0, [theme.breakpoints.down('xl')]: { display: 'flex' } }, - [`.${classes.spacing}`]: { flexGrow: 1 }, - - [`.${classes.activeLink}`]: { - ':after': { - content: "''", - position: 'absolute', - bottom: 0, - left: 0, - width: '100%', - height: 2, - backgroundColor: 'white' - } - }, - [`.${classes.activeMobileLink}`]: { fontWeight: 700, '&:after': { @@ -107,7 +88,6 @@ const Root = styled('div')(({ theme }) => ({ borderBottom: '2px solid transparent', fontWeight: 600 }, - [`.${classes.userLink}`]: { [theme.breakpoints.down('md')]: { display: 'flex' @@ -123,7 +103,6 @@ const Root = styled('div')(({ theme }) => ({ textDecoration: 'none' } }, - [`.${classes.lgNav}`]: { display: 'flex', [theme.breakpoints.down('sm')]: { @@ -134,7 +113,6 @@ const Root = styled('div')(({ theme }) => ({ [`.${classes.mobileNav}`]: { padding: `${theme.spacing(2)} ${theme.spacing()}px` }, - [`.${classes.selectOrg}`]: { border: '1px solid #FFFFFF', borderRadius: '5px', @@ -161,16 +139,12 @@ const Root = styled('div')(({ theme }) => ({ marginTop: '-3px !important' }, height: '45px' - }, - - [` .${classes.option}`]: { - fontSize: 15 } })); -const GLOBAL_ADMIN = 2; +const GLOBAL_ADMIN = 3; +const REGIONAL_ADMIN = 2; const STANDARD_USER = 1; -const ALL_USERS = GLOBAL_ADMIN | STANDARD_USER; interface NavItemType { title: string | JSX.Element; @@ -196,20 +170,29 @@ const HeaderNoCtx: React.FC = (props) => { logout, apiGet } = useAuthContext(); - const [navOpen, setNavOpen] = useState(false); + const [drawerOpen, setDrawerOpen] = useState(false); + const [isMobile, setIsMobile] = useState(false); const [organizations, setOrganizations] = useState< (Organization | OrganizationTag)[] >([]); const [tags, setTags] = useState([]); - const theme = useTheme(); - const isSmall = useMediaQuery(theme.breakpoints.down('md')); + + let drawerItems: NavItemType[] = []; + const toggleDrawer = (newOpen: boolean) => () => { + setDrawerOpen(newOpen); + }; let userLevel = 0; if (user && user.isRegistered) { if (user.userType === 'standard') { userLevel = STANDARD_USER; - } else { + } else if ( + user.userType === 'globalAdmin' || + user.userType === 'globalView' + ) { userLevel = GLOBAL_ADMIN; + } else if (user.userType === 'regionalAdmin') { + userLevel = REGIONAL_ADMIN; } } @@ -227,7 +210,7 @@ const HeaderNoCtx: React.FC = (props) => { } }, [apiGet, setOrganizations, userLevel]); - React.useEffect(() => { + useEffect(() => { if (userLevel > 0) { fetchOrganizations(); } @@ -237,40 +220,25 @@ const HeaderNoCtx: React.FC = (props) => { { title: 'Overview', path: '/', - users: ALL_USERS, - exact: true + users: STANDARD_USER, + exact: true, + onClick: toggleDrawer(false) }, { title: 'Inventory', path: '/inventory', - users: ALL_USERS, - exact: false + users: STANDARD_USER, + exact: false, + onClick: toggleDrawer(false) }, - - /* - Hiding Feeds page until finished - { title: 'Feeds', - path: '/feeds', - users: ALL_USERS, - exact: false - },*/ - - /* - Hiding Reports page until finished - { - title: 'Reports', - path: '/reports', - users: ALL_USERS, - exact: true - },*/ - { title: 'Scans', path: '/scans', users: GLOBAL_ADMIN, - exact: true + exact: true, + onClick: toggleDrawer(false) } - ].filter(({ users }) => (users & userLevel) > 0); + ].filter(({ users }) => users <= userLevel); const userMenu: NavItemType = { title: ( @@ -281,18 +249,18 @@ const HeaderNoCtx: React.FC = (props) => { path: '#', exact: false, nested: [ + { + title: 'User Registration', + path: '/region-admin-dashboard', + users: REGIONAL_ADMIN, + exact: true + }, { title: 'Manage Organizations', path: '/organizations', users: GLOBAL_ADMIN, exact: true }, - // { - // title: 'My Organizations', - // path: '/organizations', - // users: STANDARD_USER, - // exact: true - // }, { title: 'Manage Users', path: '/users', @@ -302,88 +270,46 @@ const HeaderNoCtx: React.FC = (props) => { { title: 'My Settings', path: '/settings', - users: ALL_USERS, + users: STANDARD_USER, exact: true }, { title: 'Logout', path: '/settings', - users: ALL_USERS, + users: STANDARD_USER, onClick: logout, exact: true } - ].filter(({ users }) => (users & userLevel) > 0) + ].filter(({ users }) => users <= userLevel) }; - const userItemsSmall: NavItemType[] = [ - { - title: 'My Account', - path: '#', - users: ALL_USERS, - exact: true - }, - { - title: 'Manage Organizations', - path: '/organizations', - users: GLOBAL_ADMIN, - exact: true - }, - // { - // title: 'My Organizations', - // path: '/organizations', - // users: STANDARD_USER, - // exact: true - // }, - { - title: 'Manage Users', - path: '/users', - users: GLOBAL_ADMIN, - exact: true - }, - { - title: 'My Settings', - path: '/settings', - users: ALL_USERS, - exact: true - }, - { - title: 'Logout', - path: '/', - users: ALL_USERS, - onClick: logout, - exact: true - } - ].filter(({ users }) => (users & userLevel) > 0); - const orgPageMatch = useRouteMatch('/organizations/:id'); const desktopNavItems: JSX.Element[] = navItems.map((item) => ( )); - const userRegistrationNavItem = { - title: 'User Registration', - path: '/region-admin-dasboard', - users: ALL_USERS, - exact: true - }; - - const getConditionalNavItems = () => { - if (user?.userType === 'regionalAdmin') { - userMenu.nested?.unshift(userRegistrationNavItem); - userItemsSmall.unshift(userRegistrationNavItem); - } - }; - - const navItemsToUse = () => { - getConditionalNavItems(); - if (isSmall) { - return userItemsSmall; + const handleResize = () => { + if (window.innerWidth < 1330) { + setIsMobile(true); } else { - return navItems; + setIsMobile(false); } }; + useEffect(() => { + window.addEventListener('resize', handleResize); + }); + + if (isMobile && userMenu.nested) { + userMenu.nested.forEach((item) => { + if (item.title !== 'Logout') { + item.onClick = toggleDrawer(false); + } + }); + drawerItems = [...navItems, ...userMenu.nested]; + } + return ( @@ -396,10 +322,10 @@ const HeaderNoCtx: React.FC = (props) => { alt="Crossfeed Icon Navigate Home" /> -
{desktopNavItems.slice()}
- + {!isMobile && ( +
{desktopNavItems.slice()}
+ )}
- {userLevel > 0 && ( <> = (props) => { }} /> {organizations.length > 1 && ( - <> -
- - option?.name === value?.name - } - options={[{ name: 'All Organizations' }].concat( - organizations - )} - autoComplete={false} - className={classes.selectOrg} - classes={{ - option: classes.option - }} - value={ - showAllOrganizations - ? { name: 'All Organizations' } - : currentOrganization ?? undefined + + option?.name === value?.name + } + options={[{ name: 'All Organizations' }].concat( + organizations + )} + autoComplete={false} + className={classes.selectOrg} + classes={{ + option: classes.option + }} + value={ + showAllOrganizations + ? { name: 'All Organizations' } + : currentOrganization ?? undefined + } + filterOptions={(options, state) => { + // If already selected, show all + if ( + options.find( + (option) => + option?.name.toLowerCase() === + state.inputValue.toLowerCase() + ) + ) { + return options; } - filterOptions={(options, state) => { - // If already selected, show all - if ( - options.find( - (option) => - option?.name.toLowerCase() === - state.inputValue.toLowerCase() - ) - ) { - return options; + return options.filter( + (option) => + option?.name + .toLowerCase() + .includes(state.inputValue.toLowerCase()) + ); + }} + disableClearable + blurOnSelect + selectOnFocus + getOptionLabel={(option) => option!.name} + renderOption={(props, option) => ( +
  • {option!.name}
  • + )} + onChange={( + event: any, + value: Organization | { name: string } | undefined + ) => { + if (value && 'id' in value) { + setOrganization(value); + setShowAllOrganizations(false); + if (value.name === 'Election') { + setShowMaps(true); + } else { + setShowMaps(false); } - return options.filter( - (option) => - option?.name - .toLowerCase() - .includes(state.inputValue.toLowerCase()) - ); - }} - disableClearable - blurOnSelect - selectOnFocus - getOptionLabel={(option) => option!.name} - renderOption={(props, option) => ( -
  • {option!.name}
  • - )} - onChange={( - event: any, - value: Organization | { name: string } | undefined - ) => { - if (value && 'id' in value) { - setOrganization(value); - setShowAllOrganizations(false); - if (value.name === 'Election') { - setShowMaps(true); - } else { - setShowMaps(false); - } - // Check if we're on an organization page and, if so, update it to the new organization - if (orgPageMatch !== null) { - if (!tags.find((e) => e.id === value.id)) { - history.push(`/organizations/${value.id}`); - } + // Check if we're on an organization page and, if so, update it to the new organization + if (orgPageMatch !== null) { + if (!tags.find((e) => e.id === value.id)) { + history.push(`/organizations/${value.id}`); } - } else { - setShowAllOrganizations(true); - setShowMaps(false); } - }} - renderInput={(params) => ( - - )} - /> - + } else { + setShowAllOrganizations(true); + setShowMaps(false); + } + }} + renderInput={(params) => ( + + )} + /> )} - {isSmall ? null : } + {!isMobile && } )} - setNavOpen((open) => !open)} - > - - + {isMobile && ( + + + + )}
    - setNavOpen(false)} + open={drawerOpen} + onClose={toggleDrawer(false)} data-testid="mobilenav" + PaperProps={{ + sx: { + backgroundColor: 'primary.main', + color: 'white' + } + }} > - {navItemsToUse().map(({ title, path, nested, onClick }) => ( + {drawerItems.map(({ title, path, nested, onClick }) => ( {path && ( ({ position: 'relative', height: '100vh', display: 'flex', - flexFlow: 'column nowrap', - overflow: 'auto' + flexFlow: 'column nowrap' + // overflow: 'auto' }, [`& .${classes.overrides}`]: { diff --git a/frontend/src/components/NavItem.tsx b/frontend/src/components/NavItem.tsx index 4d94b935..8fdcdac5 100644 --- a/frontend/src/components/NavItem.tsx +++ b/frontend/src/components/NavItem.tsx @@ -137,7 +137,7 @@ const Root = styled('div')(({ theme }) => ({ position: 'absolute', bottom: 0, left: 6, - width: '100%', + width: '85%', height: 2, backgroundColor: 'white' } @@ -152,7 +152,7 @@ const Root = styled('div')(({ theme }) => ({ bottom: 0, left: 0, height: '100%', - width: 2, + width: '85%', backgroundColor: theme.palette.primary.main } }, @@ -170,7 +170,6 @@ const Root = styled('div')(({ theme }) => ({ [`& .${classesNav.userLink}`]: { display: 'flex', alignItems: 'center', - '& svg': { marginRight: theme.spacing() } diff --git a/frontend/src/components/OrganizationList/OrganizationList.tsx b/frontend/src/components/OrganizationList/OrganizationList.tsx index eb1b4601..5639cc40 100644 --- a/frontend/src/components/OrganizationList/OrganizationList.tsx +++ b/frontend/src/components/OrganizationList/OrganizationList.tsx @@ -105,7 +105,7 @@ export const OrganizationList: React.FC<{ {organizations?.length === 0 ? ( - No organizations found. + Unable to load organizations. ) : ( ({ [`&.${classes.wrapper}`]: { zIndex: 101, width: '100%', - maxWidth: 400, + maxWidth: 320, backgroundColor: theme.palette.background.paper, display: 'flex', flexFlow: 'row nowrap', @@ -87,7 +87,7 @@ interface Props initialValue: string; } -const defaultPlaceholder = 'Search for a domain, vuln type, port, service, IP'; +const defaultPlaceholder = 'Search a domain, vuln, port, service, IP'; type Timer = ReturnType; diff --git a/frontend/src/components/__tests__/__snapshots__/header.spec.tsx.snap b/frontend/src/components/__tests__/__snapshots__/header.spec.tsx.snap index 83e5f878..b692dc60 100644 --- a/frontend/src/components/__tests__/__snapshots__/header.spec.tsx.snap +++ b/frontend/src/components/__tests__/__snapshots__/header.spec.tsx.snap @@ -3,7 +3,7 @@ exports[`Header component matches snapshot 1`] = `
    -
    diff --git a/frontend/src/components/__tests__/__snapshots__/layout.spec.tsx.snap b/frontend/src/components/__tests__/__snapshots__/layout.spec.tsx.snap index bf1530f2..784f661f 100644 --- a/frontend/src/components/__tests__/__snapshots__/layout.spec.tsx.snap +++ b/frontend/src/components/__tests__/__snapshots__/layout.spec.tsx.snap @@ -3,7 +3,7 @@ exports[`Layout component matches snapshot 1`] = `
    diff --git a/frontend/src/components/__tests__/header.spec.tsx b/frontend/src/components/__tests__/header.spec.tsx index 33559560..7b1189d8 100644 --- a/frontend/src/components/__tests__/header.spec.tsx +++ b/frontend/src/components/__tests__/header.spec.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import { render, fireEvent, testUser, testOrganization } from 'test-utils'; +import { render, testUser, testOrganization } from 'test-utils'; import { Header } from '../Header'; -import { waitFor } from '@testing-library/react'; jest.mock('@elastic/react-search-ui', () => ({ withSearch: () => (comp: any) => comp @@ -14,13 +13,8 @@ describe('Header component', () => { }); it('can expand drawer', async () => { - const { getByLabelText, getByTestId, queryByTestId } = render(
    ); + const { queryByTestId } = render(
    ); expect(queryByTestId('mobilenav')).not.toBeInTheDocument(); - expect(getByLabelText('toggle mobile menu')).toBeInTheDocument(); - fireEvent.click(getByLabelText('toggle mobile menu')); - await waitFor(() => { - expect(getByTestId('mobilenav')).toBeInTheDocument(); - }); }); it('shows no links for unauthenticated user', () => { @@ -30,15 +24,7 @@ describe('Header component', () => { currentOrganization: { ...testOrganization } } }); - [ - 'Vulnerabilities', - 'Risk Summary', - // 'My Organizations', - 'Manage Organizations', - 'Scans', - 'Manage Users', - 'My Account' - ].forEach((expected) => { + ['Vulnerabilities', 'Risk Summary', 'Scans'].forEach((expected) => { expect(queryByText(expected)).not.toBeInTheDocument(); }); }); @@ -50,41 +36,27 @@ describe('Header component', () => { currentOrganization: { ...testOrganization } } }); - [ - 'Overview', - 'Inventory', - // 'My Organizations', - 'My Account', - 'My Settings', - 'Logout' - ].forEach((expected) => { + ['Overview', 'Inventory'].forEach((expected) => { expect(getByText(expected)).toBeInTheDocument(); }); - ['Manage Organizations', 'Scans', 'Manage Users'].forEach((notExpected) => { + ['Scans'].forEach((notExpected) => { expect(queryByText(notExpected)).not.toBeInTheDocument(); }); }); it('shows correct links for ORG_ADMIN', () => { - const { getByText, queryByText } = render(
    , { + const { getByText } = render(
    , { authContext: { user: { ...testUser, userType: 'standard', isRegistered: true }, currentOrganization: { ...testOrganization } } }); - [ - 'Overview', - 'Inventory', - // 'My Organizations', - 'My Account', - 'My Settings', - 'Logout' - ].forEach((expected) => { + ['Overview', 'Inventory'].forEach((expected) => { expect(getByText(expected)).toBeInTheDocument(); }); - ['Manage Organizations', 'Manage Users'].forEach((notExpected) => { - expect(queryByText(notExpected)).not.toBeInTheDocument(); - }); + // ['Manage Organizations', 'Manage Users'].forEach((notExpected) => { + // expect(queryByText(notExpected)).not.toBeInTheDocument(); + // }); }); it('shows correct links for GLOBAL_ADMIN', () => { @@ -94,20 +66,8 @@ describe('Header component', () => { currentOrganization: { ...testOrganization } } }); - [ - 'Overview', - 'Inventory', - 'Scans', - 'Manage Organizations', - 'Manage Users', - 'My Account', - 'My Settings', - 'Logout' - ].forEach((expected) => { + ['Overview', 'Inventory', 'Scans'].forEach((expected) => { expect(getByText(expected)).toBeInTheDocument(); }); - // ['My Organizations'].forEach((notExpected) => { - // expect(queryByText(notExpected)).not.toBeInTheDocument(); - // }); }); }); diff --git a/frontend/src/pages/Users/Users.tsx b/frontend/src/pages/Users/Users.tsx index f9ce36bd..a6140e28 100644 --- a/frontend/src/pages/Users/Users.tsx +++ b/frontend/src/pages/Users/Users.tsx @@ -92,21 +92,28 @@ export const Users: React.FC = () => { const fetchUsers = useCallback(async () => { try { const rows = await apiGet(`/users/`); - rows.forEach((obj) => { - obj.lastLoggedInString = obj.lastLoggedIn - ? `${formatDistanceToNow(parseISO(obj.lastLoggedIn))} ago` + rows.forEach((row) => { + row.lastLoggedInString = row.lastLoggedIn + ? `${formatDistanceToNow(parseISO(row.lastLoggedIn))} ago` : 'None'; - obj.dateToUSigned = obj.dateAcceptedTerms - ? `${formatDistanceToNow(parseISO(obj.dateAcceptedTerms))} ago` + row.dateToUSigned = row.dateAcceptedTerms + ? `${formatDistanceToNow(parseISO(row.dateAcceptedTerms))} ago` : 'None'; - obj.orgs = obj.roles - ? obj.roles + row.orgs = row.roles + ? row.roles .filter((role) => role.approved) .map((role) => role.organization.name) .join(', ') : 'None'; }); - setUsers(rows); + if (user?.userType === 'globalAdmin') { + setUsers(rows); + } else if (user?.userType === 'regionalAdmin' && user?.regionId) { + rows.filter((row) => row.regionId === user.regionId); + setUsers(rows); + } else if (user) { + setUsers([user]); + } setErrorStates({ ...errorStates, getUsersError: '' }); } catch (e: any) { setErrorStates({ ...errorStates, getUsersError: e.message });