diff --git a/frontend/package.json b/frontend/package.json index 4e1d218930..6b01c7cf69 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,11 +6,13 @@ "@testing-library/jest-dom": "^4.2.4", "@trussworks/react-uswds": "^1.9.1", "http-proxy-middleware": "^1.0.5", + "lodash": "^4.17.20", "prop-types": "^15.7.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", + "react-router-prop-types": "^1.0.5", "react-scripts": "3.4.1", "uswds": "^2.8.1" }, @@ -85,9 +87,10 @@ ] }, "devDependencies": { - "@testing-library/dom": "^7.21.7", + "@sheerun/mutationobserver-shim": "^0.3.3", + "@testing-library/dom": "^7.24.2", "@testing-library/react": "^10.4.9", - "@testing-library/user-event": "^7.1.2", + "@testing-library/user-event": "^12.1.5", "cross-env": "^7.0.2", "eslint": "^6.8.0", "eslint-config-airbnb": "^18.2.0", @@ -97,6 +100,7 @@ "eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-react": "^7.20.5", "eslint-plugin-react-hooks": "^4.0.8", + "history": "^5.0.0", "jest-junit": "^11.1.0" } } diff --git a/frontend/src/App.js b/frontend/src/App.js index 3b5adcb624..0445a072e3 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -7,6 +7,7 @@ import { import { Alert } from '@trussworks/react-uswds'; import Header from './components/Header'; +import Admin from './pages/Admin'; function App() { return ( @@ -21,11 +22,7 @@ function App() { World! - -
- Hello second Page! -
-
+ ); diff --git a/frontend/src/Constants.js b/frontend/src/Constants.js new file mode 100644 index 0000000000..cb67e2cc6d --- /dev/null +++ b/frontend/src/Constants.js @@ -0,0 +1,89 @@ +export const REGIONAL_SCOPES = [ + { + name: 'READ_WRITE_REPORTS', + description: 'Can view and create/edit reports in the region', + }, + { + name: 'READ_REPORTS', + description: 'Can view reports in the region', + }, + { + name: 'THIRD_SCOPE', + description: 'A third scope used as an example', + }, + { + name: 'FORTH_SCOPE', + description: 'Another testing scope that will soon be deleted', + }, +]; + +export const GLOBAL_SCOPES = [ + { + name: 'SITE_ACCESS', + description: 'User can login and view the TTAHUB site', + }, + { + name: 'ADMIN', + description: 'User can view the admin panel and change user permissions (including their own)', + }, +]; + +export const JOB_TITLES = [ + 'Program Specialist', + 'Early Childhood Specialist', + 'Grantee Specialist', + 'Family Engagement Specialist', + 'Health Specialist', + 'Systems Specialist', +]; + +export const REGIONS = [ + { + number: 1, + name: 'Boston', + }, + { + number: 2, + name: 'New York City', + }, + { + number: 3, + name: 'Philadelphia', + }, + { + number: 4, + name: 'Atlanta', + }, + { + number: 5, + name: 'Chicago', + }, + { + number: 6, + name: 'Dallas', + }, + { + number: 7, + name: 'Kansas City', + }, + { + number: 8, + name: 'Denver', + }, + { + number: 9, + name: 'San Francisco', + }, + { + number: 10, + name: 'Seattle', + }, + { + number: 11, + name: 'AIAN', + }, + { + number: 12, + name: 'MSHS', + }, +]; diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js index ff584feb90..f5ad9e9632 100644 --- a/frontend/src/components/Header.js +++ b/frontend/src/components/Header.js @@ -13,8 +13,8 @@ function Header() { Home , - - Second Page + + Admin , ]; diff --git a/frontend/src/components/IndeterminateCheckbox.css b/frontend/src/components/IndeterminateCheckbox.css new file mode 100644 index 0000000000..1d3346c8df --- /dev/null +++ b/frontend/src/components/IndeterminateCheckbox.css @@ -0,0 +1,8 @@ +.usa-checkbox__input:indeterminate+.usa-checkbox__label::before { + background-image: url(../images/minus.svg); + background-repeat: no-repeat; + background-position: center center; + background-size: .75rem auto; + background-color: #949494; + box-shadow: 0 0 0 2px #949494; +} \ No newline at end of file diff --git a/frontend/src/components/IndeterminateCheckbox.js b/frontend/src/components/IndeterminateCheckbox.js new file mode 100644 index 0000000000..5ef4d530e9 --- /dev/null +++ b/frontend/src/components/IndeterminateCheckbox.js @@ -0,0 +1,51 @@ +import React, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import './IndeterminateCheckbox.css'; + +function Checkbox({ + id, name, label, checked, indeterminate, disabled, onChange, +}) { + const indeterminateRef = useRef(); + useEffect(() => { + indeterminateRef.current.indeterminate = indeterminate; + }); + + const onLocalChange = (e) => { + onChange(e, indeterminate); + }; + + return ( + <> + + + + ); +} + +Checkbox.propTypes = { + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + label: PropTypes.node.isRequired, + checked: PropTypes.bool.isRequired, + indeterminate: PropTypes.bool, + disabled: PropTypes.bool, + onChange: PropTypes.func.isRequired, +}; + +Checkbox.defaultProps = { + indeterminate: false, + disabled: false, +}; + +export default Checkbox; diff --git a/frontend/src/components/JobTitleDropdown.js b/frontend/src/components/JobTitleDropdown.js new file mode 100644 index 0000000000..f257fdeee7 --- /dev/null +++ b/frontend/src/components/JobTitleDropdown.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Label, Dropdown, +} from '@trussworks/react-uswds'; + +import { JOB_TITLES } from '../Constants'; + +function JobTitleDropdown({ + id, name, value, onChange, +}) { + return ( + <> + + + + {JOB_TITLES.map((jobTitle) => ( + + ))} + + + ); +} + +JobTitleDropdown.propTypes = { + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + value: PropTypes.string, + onChange: PropTypes.func.isRequired, +}; + +JobTitleDropdown.defaultProps = { + value: 'default', +}; + +export default JobTitleDropdown; diff --git a/frontend/src/components/RegionDropdown.js b/frontend/src/components/RegionDropdown.js new file mode 100644 index 0000000000..f7377d98cc --- /dev/null +++ b/frontend/src/components/RegionDropdown.js @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Label, Dropdown, +} from '@trussworks/react-uswds'; + +import { REGIONS } from '../Constants'; + +function RegionDropdown({ + id, name, value, onChange, includeCentralOffice, +}) { + return ( + <> + + + + {REGIONS.map(({ number, name: description }) => ( + + ))} + {includeCentralOffice + && } + + + ); +} + +RegionDropdown.propTypes = { + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + value: PropTypes.string, + onChange: PropTypes.func.isRequired, + includeCentralOffice: PropTypes.bool, +}; + +RegionDropdown.defaultProps = { + value: 'default', + includeCentralOffice: false, +}; + +export default RegionDropdown; diff --git a/frontend/src/components/__tests__/IndeterminateCheckbox.js b/frontend/src/components/__tests__/IndeterminateCheckbox.js new file mode 100644 index 0000000000..e4f7a77f67 --- /dev/null +++ b/frontend/src/components/__tests__/IndeterminateCheckbox.js @@ -0,0 +1,33 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; + +import IndeterminateCheckbox from '../IndeterminateCheckbox'; + +describe('IndeterminateCheckbox', () => { + test('indeterminate can be false', () => { + render( {}} />); + expect(screen.getByLabelText('false').indeterminate).toBeFalsy(); + }); + + test('indeterminate can be true', () => { + render( {}} />); + expect(screen.getByLabelText('true').indeterminate).toBeTruthy(); + }); + + test('can be disabled', () => { + render( {}} />); + expect(screen.getByLabelText('checkbox')).toBeDisabled(); + }); + + test('onChange includes indeterminate', () => { + let result = false; + const onChange = (e, indeterminate) => { + result = indeterminate; + }; + render(); + const checkbox = screen.getByLabelText('checkbox'); + fireEvent.click(checkbox); + expect(result).toBeTruthy(); + }); +}); diff --git a/frontend/src/components/__tests__/JobTitleDropdown.js b/frontend/src/components/__tests__/JobTitleDropdown.js new file mode 100644 index 0000000000..9f0fbef614 --- /dev/null +++ b/frontend/src/components/__tests__/JobTitleDropdown.js @@ -0,0 +1,18 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import JobTitleDropdown from '../JobTitleDropdown'; + +describe('JobTitleDropdown', () => { + test('shows "select a job title" (the default) when no value is selected', () => { + render( {}} />); + expect(screen.getByLabelText('Job Title').value).toBe('default'); + }); + + test('default option is not selectable', () => { + render( {}} />); + const item = screen.getByLabelText('Job Title').options.namedItem('default'); + expect(item.hidden).toBeTruthy(); + }); +}); diff --git a/frontend/src/components/__tests__/RegionDropdown.js b/frontend/src/components/__tests__/RegionDropdown.js new file mode 100644 index 0000000000..f086d1005a --- /dev/null +++ b/frontend/src/components/__tests__/RegionDropdown.js @@ -0,0 +1,28 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import RegionDropdown from '../RegionDropdown'; + +describe('RegionalDropdown', () => { + test('shows "select an option" (the default) when no value is selected', () => { + render( {}} />); + expect(screen.getByLabelText('Region').value).toBe('default'); + }); + + test('default option is not selectable', () => { + render( {}} />); + const item = screen.getByLabelText('Region').options.namedItem('default'); + expect(item.hidden).toBeTruthy(); + }); + + test('does not show central office when includeCentralOffice is not specified', () => { + render( {}} />); + expect(screen.queryByText('Central Office')).toBeNull(); + }); + + test('with prop includeCentralOffice has central office as an option', () => { + render( {}} />); + expect(screen.queryByText('Central Office')).toBeVisible(); + }); +}); diff --git a/frontend/src/images/minus.svg b/frontend/src/images/minus.svg new file mode 100644 index 0000000000..02fff80011 --- /dev/null +++ b/frontend/src/images/minus.svg @@ -0,0 +1 @@ +minus \ No newline at end of file diff --git a/frontend/src/pages/Admin/PermissionHelpers.js b/frontend/src/pages/Admin/PermissionHelpers.js new file mode 100644 index 0000000000..fe1fa96320 --- /dev/null +++ b/frontend/src/pages/Admin/PermissionHelpers.js @@ -0,0 +1,64 @@ +import { REGIONAL_SCOPES, GLOBAL_SCOPES, REGIONS } from '../../Constants'; + +/** + * Returns an object that has every regional scope as a key and a value of 'false' + * @returns {Object} An object with SCOPEs as keys and bool as values + */ +export function createScopeObject() { + return REGIONAL_SCOPES.reduce((acc, cur) => { + acc[cur.name] = false; + return acc; + }, {}); +} + +/** + * Return an object representing what permissions the user has per region. + * + * If a user has READ_REPORTS access on region 1 the resulting object will + * look like {"1": {"READ_REPORTS": true}} + * @param {*} - user object + * @returns {Object>}} + */ +export function userRegionalPermissions(user) { + const regionalPermissions = REGIONS.reduce((acc, cur) => { + acc[cur.number] = createScopeObject(); + return acc; + }, {}); + + if (!user.permissions) { + return regionalPermissions; + } + + user.permissions.filter((p) => ( + p.region !== 0 + )).forEach(({ region, scope }) => { + regionalPermissions[region][scope] = true; + }); + return regionalPermissions; +} + +/** + * This method returns an object representing the global permissions for the + * user. + * + * If a user has SITE_ACCESS resulting object will look like {"SITE_ACCESS": true} + * @param {*} - user object + * @returns {Object>} + */ +export function userGlobalPermissions(user) { + const globals = GLOBAL_SCOPES.reduce((acc, cur) => { + acc[cur.name] = false; + return acc; + }, {}); + + if (!user.permissions) { + return globals; + } + + user.permissions.filter((p) => ( + p.region === 0 + )).forEach(({ scope }) => { + globals[scope] = true; + }); + return globals; +} diff --git a/frontend/src/pages/Admin/UserInfo.js b/frontend/src/pages/Admin/UserInfo.js new file mode 100644 index 0000000000..568b637622 --- /dev/null +++ b/frontend/src/pages/Admin/UserInfo.js @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Label, TextInput, Grid, Fieldset, +} from '@trussworks/react-uswds'; + +import RegionDropdown from '../../components/RegionDropdown'; +import JobTitleDropdown from '../../components/JobTitleDropdown'; + +/** + * This component is the top half of the UserSection on the admin page. It displays and allows + * editing of basic user information. + */ +function UserInfo({ user, onUserChange }) { + return ( +
+ + + + + + + + + + + + + + + + + + +
+ ); +} + +UserInfo.propTypes = { + user: PropTypes.shape({ + email: PropTypes.string, + fullName: PropTypes.string, + region: PropTypes.string, + jobTitle: PropTypes.string, + }).isRequired, + onUserChange: PropTypes.func.isRequired, +}; + +export default UserInfo; diff --git a/frontend/src/pages/Admin/UserPermissions.js b/frontend/src/pages/Admin/UserPermissions.js new file mode 100644 index 0000000000..330056efe0 --- /dev/null +++ b/frontend/src/pages/Admin/UserPermissions.js @@ -0,0 +1,150 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import { + Checkbox, Grid, Fieldset, +} from '@trussworks/react-uswds'; + +import { GLOBAL_SCOPES, REGIONAL_SCOPES } from '../../Constants'; +import PermissionCheckboxLabel from './components/PermissionCheckboxLabel'; +import CurrentPermissions from './components/CurrentPermissions'; +import RegionDropdown from '../../components/RegionDropdown'; +import { createScopeObject } from './PermissionHelpers'; + +/** + * Display the current permissions for the selected user + * + * The permission object coming into this method is keyed off the region. We want to key + * the user's current permissions off the scope when displaying. This method creates a + * new permission object map with the schema . So a user with + * "READ_REPORTS" on region 1, 2 and 3 this object will be {"READ_REPORTS": ["1","2","3"]} + * @param {Object>}} permissions + */ +function renderUserPermissions(permissions) { + const currentPermissions = REGIONAL_SCOPES.reduce((acc, cur) => { + acc[cur.name] = []; + return acc; + }, {}); + + _.forEach(permissions, (scopes, region) => { + // Grab the scopes that are true. I.E. from {"READ_REPORTS": true, "READ_WRITE_REPORTS": true, + // "SCOPE": false} to {"READ_REPORTS": true, "READ_WRITE_REPORTS": true} + const trueScopes = _.pickBy(scopes); + // _.keys gives us an array of keys of the object, so ["READ_REPORTS", "READ_WRITE_REPORTS"] + _.keys(trueScopes).forEach((scope) => { + currentPermissions[scope].push(region); + }); + }); + + // regions.length being zero means the user does not have the scope in any region. Remove the + // scope to keep the UI less cluttered + const prunedPermissions = _.pickBy(currentPermissions, (regions) => ( + regions.length > 0 + )); + + return _.map(prunedPermissions, (regions, scope) => ( + + )); +} + +/** + * This component is the lower half of the UserSection. It is responsible for displaying permissions + * and passing any updates up to the UserSection component. The Admin can set permissions for a + * single region at a time. + */ +function UserPermissions({ + userId, + globalPermissions, + onGlobalPermissionChange, + regionalPermissions, + onRegionalPermissionChange, +}) { + // State of the region select dropdown + const [selectedRegion, updateSelectedRegion] = useState(); + // State of the regional permissions checkboxes, I.E. {"READ_REPORTS": true, ...} + const [permissionsForRegion, updatePermissionsForRegion] = useState(createScopeObject()); + const enablePermissions = selectedRegion !== undefined; + + useEffect(() => { + updateSelectedRegion(); + updatePermissionsForRegion(createScopeObject()); + }, [userId]); + + useEffect(() => { + updatePermissionsForRegion({ + ...createScopeObject(), + ...regionalPermissions[selectedRegion], + }); + }, [regionalPermissions, selectedRegion]); + + const onSelectedRegionChange = (e) => { + const { value } = e.target; + updatePermissionsForRegion({ ...permissionsForRegion, ...regionalPermissions[value] }); + updateSelectedRegion(value); + }; + + const onPermissionsForRegionChange = (e) => { + const newRegionPermissions = { ...permissionsForRegion, [e.target.name]: e.target.checked }; + onRegionalPermissionChange({ + ...regionalPermissions, + [selectedRegion]: { ...newRegionPermissions }, + }); + }; + + return ( + <> +
+ + {GLOBAL_SCOPES.map(({ name, description }) => ( + + )} + name={name} + disabled={false} + /> + + ))} + +
+
+

Current Permissions

+
    + {renderUserPermissions(regionalPermissions)} +
+ + + {REGIONAL_SCOPES.map(({ name, description }) => ( + + )} + /> + + ))} + +
+ + ); +} + +UserPermissions.propTypes = { + userId: PropTypes.number, + globalPermissions: PropTypes.objectOf(PropTypes.bool).isRequired, + onGlobalPermissionChange: PropTypes.func.isRequired, + regionalPermissions: PropTypes.objectOf(PropTypes.objectOf(PropTypes.bool)).isRequired, + onRegionalPermissionChange: PropTypes.func.isRequired, +}; + +UserPermissions.defaultProps = { + userId: null, +}; + +export default UserPermissions; diff --git a/frontend/src/pages/Admin/UserSection.js b/frontend/src/pages/Admin/UserSection.js new file mode 100644 index 0000000000..e18a979b42 --- /dev/null +++ b/frontend/src/pages/Admin/UserSection.js @@ -0,0 +1,90 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { + Form, Button, +} from '@trussworks/react-uswds'; + +import UserInfo from './UserInfo'; +import UserPermissions from './UserPermissions'; +import { userGlobalPermissions, userRegionalPermissions } from './PermissionHelpers'; + +/** + * The user section of the Admin UI. Creating new users (and editing existing) is done + * inside this component. This component holds all the state for the user that is currently + * being edited. + */ +function UserSection({ user }) { + const [formUser, updateUser] = useState(); + const [globalPermissions, updateGlobalPermissions] = useState({}); + const [regionalPermissions, updateRegionalPermissions] = useState(); + + useEffect(() => { + updateUser(user); + updateGlobalPermissions(userGlobalPermissions(user)); + updateRegionalPermissions(userRegionalPermissions(user)); + }, [user]); + + const onUserChange = (e) => { + updateUser({ + ...formUser, + [e.target.name]: e.target.value, + }); + }; + + const onGlobalPermissionChange = (e) => { + updateGlobalPermissions({ + ...globalPermissions, + [e.target.name]: e.target.checked, + }); + }; + + const onRegionalPermissionChange = (updatedRegionalPermissions) => { + updateRegionalPermissions({ + ...regionalPermissions, + ...updatedRegionalPermissions, + }); + }; + + if (!formUser) { + return ( +
+ Loading... +
+ ); + } + + return ( +
+ + + + + ); +} + +UserSection.propTypes = { + user: PropTypes.shape({ + id: PropTypes.number, + email: PropTypes.string, + fullName: PropTypes.string, + region: PropTypes.string, + jobTitle: PropTypes.string, + permissions: PropTypes.arrayOf(PropTypes.shape({ + region: PropTypes.number.isRequired, + scope: PropTypes.string.isRequired, + })), + }).isRequired, +}; + +export default UserSection; diff --git a/frontend/src/pages/Admin/__tests__/PermissionHelpers.js b/frontend/src/pages/Admin/__tests__/PermissionHelpers.js new file mode 100644 index 0000000000..9cf744b81f --- /dev/null +++ b/frontend/src/pages/Admin/__tests__/PermissionHelpers.js @@ -0,0 +1,86 @@ +import _ from 'lodash'; +import { createScopeObject, userRegionalPermissions, userGlobalPermissions } from '../PermissionHelpers'; +import { REGIONAL_SCOPES } from '../../../Constants'; + +describe('PermissionHelpers', () => { + describe('createScopeObject', () => { + it('creates an object with scopes as keys and false as values', () => { + const obj = createScopeObject(); + REGIONAL_SCOPES.forEach((scope) => { + expect(obj[scope]).toBeFalsy(); + }); + }); + }); + + describe('userRegionalPermissions', () => { + it('returns an all false object for a user with no permissions', () => { + const regionalPermissions = userRegionalPermissions({}); + + expect(Object.keys(regionalPermissions).length).toBe(12); + _.forEach(regionalPermissions, (scopes) => { + expect(_.every(scopes, (scope) => scope === false)).toBeTruthy(); + }); + }); + + describe('for a user with permissions', () => { + let regionalPermissions; + + beforeEach(() => { + const user = { + permissions: [ + { + scope: 'READ_REPORTS', + region: 1, + }, + ], + }; + + regionalPermissions = userRegionalPermissions(user); + }); + + it('flags regional permissions the user has as true', () => { + expect(regionalPermissions['1'].READ_REPORTS).toBeTruthy(); + }); + + it('flags regional permissions the user does not have as false', () => { + expect(regionalPermissions['1'].READ_WRITE_REPORTS).toBeFalsy(); + }); + + it('flags regional permissions for the correct region', () => { + expect(regionalPermissions['2'].READ_REPORTS).toBeFalsy(); + }); + }); + }); + + describe('userGlobalPermissions', () => { + it('returns an all false object for a user with no scopes', () => { + const globalPermissions = userGlobalPermissions({}); + expect(Object.keys(globalPermissions).length).not.toBe(0); + expect(_.every(globalPermissions, (p) => p === false)).toBeTruthy(); + }); + + describe('for a user with permissions', () => { + let globalPermissions; + + beforeEach(() => { + const user = { + permissions: [ + { + scope: 'ADMIN', + region: 0, + }, + ], + }; + globalPermissions = userGlobalPermissions(user); + }); + + it('flags global permissions the user has as true', () => { + expect(globalPermissions.ADMIN).toBeTruthy(); + }); + + it('flags global permissions the user does not have as false', () => { + expect(globalPermissions.SITE_ACCESS).toBeFalsy(); + }); + }); + }); +}); diff --git a/frontend/src/pages/Admin/__tests__/UserInfo.js b/frontend/src/pages/Admin/__tests__/UserInfo.js new file mode 100644 index 0000000000..d87f46ee87 --- /dev/null +++ b/frontend/src/pages/Admin/__tests__/UserInfo.js @@ -0,0 +1,58 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import UserInfo from '../UserInfo'; + +describe('UserInfo', () => { + describe('with an empty user object', () => { + beforeEach(() => { + render( {}} />); + }); + + test('has a blank email', async () => { + expect(screen.getByLabelText('Email')).toHaveValue(''); + }); + + test('has a blank fullName', () => { + expect(screen.getByLabelText('Full Name')).toHaveValue(''); + }); + + test('has the default region', () => { + expect(screen.getByLabelText('Region')).toHaveValue('default'); + }); + + test('has the default jobTitle', () => { + expect(screen.getByLabelText('Job Title')).toHaveValue('default'); + }); + }); + + describe('with a full user object', () => { + beforeEach(() => { + const user = { + email: 'email', + fullName: 'first last', + region: '1', + jobTitle: 'Grantee Specialist', + }; + + render( {}} />); + }); + + test('has correct email', async () => { + expect(screen.getByLabelText('Email')).toHaveValue('email'); + }); + + test('has correct fullName', () => { + expect(screen.getByLabelText('Full Name')).toHaveValue('first last'); + }); + + test('has correct region', () => { + expect(screen.getByLabelText('Region')).toHaveValue('1'); + }); + + test('has correct jobTitle', () => { + expect(screen.getByLabelText('Job Title')).toHaveValue('Grantee Specialist'); + }); + }); +}); diff --git a/frontend/src/pages/Admin/__tests__/UserPermissions.js b/frontend/src/pages/Admin/__tests__/UserPermissions.js new file mode 100644 index 0000000000..ada6ccc9ee --- /dev/null +++ b/frontend/src/pages/Admin/__tests__/UserPermissions.js @@ -0,0 +1,59 @@ +import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { render, screen, within } from '@testing-library/react'; + +import UserPermissions from '../UserPermissions'; +import { withText } from '../../../testHelpers'; + +describe('UserPermissions', () => { + describe('with no permissions', () => { + beforeEach(() => { + render( {}} + onGlobalPermissionChange={() => {}} + />); + }); + + it('has no checkboxes checked', () => { + screen.getAllByRole('checkbox').forEach((cb) => { + expect(cb).not.toBeChecked(); + }); + }); + }); + + describe('with permissions', () => { + beforeEach(() => { + render( {}} + onGlobalPermissionChange={() => {}} + />); + }); + + it('has correct global permissions checked', () => { + const checkbox = screen.getByRole('checkbox', { checked: true }); + expect(checkbox.name).toBe('SITE_ACCESS'); + }); + + it('displays the current regional permissions', () => { + expect(screen.getByText(withText('READ_REPORTS: Region 1'))).toBeVisible(); + }); + + describe('when a region is selected', () => { + it('the correct regional scopes are shown as checked', () => { + userEvent.selectOptions(screen.getByLabelText('Region'), '1'); + const fieldset = screen.getByRole('group', { name: 'Regional Permissions' }); + const checkbox = within(fieldset).getByRole('checkbox', { checked: true }); + expect(checkbox).toBeChecked(); + }); + }); + }); +}); diff --git a/frontend/src/pages/Admin/__tests__/UserSection.js b/frontend/src/pages/Admin/__tests__/UserSection.js new file mode 100644 index 0000000000..8afe7d5fc9 --- /dev/null +++ b/frontend/src/pages/Admin/__tests__/UserSection.js @@ -0,0 +1,53 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import UserSection from '../UserSection'; + +describe('UserSection', () => { + beforeEach(() => { + const user = { + id: 1, + email: 'email', + fullName: 'first last', + jobTitle: 'Grantee Specialist', + region: '1', + permissions: [ + { + region: 0, + scope: 'SITE_ACCESS', + }, + { + region: 1, + scope: 'READ_REPORTS', + }, + ], + }; + + render(); + }); + + it('properly controls user info', () => { + const inputBox = screen.getByLabelText('Full Name'); + expect(inputBox).toHaveValue('first last'); + userEvent.type(inputBox, '{selectall}{backspace}new name'); + expect(screen.getByLabelText('Full Name')).toHaveValue('new name'); + }); + + it('properly controls global permissions', () => { + const checkbox = screen.getByRole('checkbox', { checked: true }); + expect(checkbox).toBeChecked(); + userEvent.click(checkbox); + expect(checkbox).not.toBeChecked(); + }); + + it('properly controls regional permissions', () => { + const permissions = screen.getByRole('group', { name: 'Regional Permissions' }); + userEvent.selectOptions(within(permissions).getByLabelText('Region'), '1'); + const checkbox = within(permissions).getByRole('checkbox', { checked: true }); + expect(checkbox).toBeChecked(); + userEvent.click(checkbox); + expect(checkbox).not.toBeChecked(); + }); +}); diff --git a/frontend/src/pages/Admin/__tests__/index.js b/frontend/src/pages/Admin/__tests__/index.js new file mode 100644 index 0000000000..bc8aeec471 --- /dev/null +++ b/frontend/src/pages/Admin/__tests__/index.js @@ -0,0 +1,52 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { Router } from 'react-router'; +import { + render, screen, waitFor, within, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { createMemoryHistory } from 'history'; +import Admin from '../index'; + +describe('UserInfo', () => { + const history = createMemoryHistory(); + + describe('with no user selected', () => { + beforeEach(() => { + render(); + }); + + it('user list is filterable', async () => { + const filter = await waitFor(() => screen.getByLabelText('Filter Users')); + userEvent.type(filter, 'Harry'); + const sideNav = screen.getByTestId('sidenav'); + const links = within(sideNav).getAllByRole('link'); + expect(links.length).toBe(1); + expect(links[0]).toHaveTextContent('Harry Potter'); + }); + + it('new user button properly sets url', async () => { + const newUser = await waitFor(() => screen.getByText('Create New User')); + userEvent.click(newUser); + expect(history.location.pathname).toBe('/admin/new'); + }); + + it('allows a user to be selected', async () => { + const button = await waitFor(() => screen.getByText('Harry Potter')); + userEvent.click(button); + expect(history.location.pathname).toBe('/admin/3'); + }); + }); + + it('displays a new user', async () => { + render(); + const userInfo = await waitFor(() => screen.getByRole('group', { name: 'User Info' })); + expect(userInfo).toBeVisible(); + }); + + it('displays an existing user', async () => { + render(); + const userInfo = await waitFor(() => screen.getByRole('group', { name: 'User Info' })); + expect(userInfo).toBeVisible(); + }); +}); diff --git a/frontend/src/pages/Admin/components/CurrentPermissions.js b/frontend/src/pages/Admin/components/CurrentPermissions.js new file mode 100644 index 0000000000..4e062394e4 --- /dev/null +++ b/frontend/src/pages/Admin/components/CurrentPermissions.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +function CurrentPermissions({ regions, scope }) { + const regionsStr = regions.length === 1 ? 'Region' : 'Regions'; + const regionMsg = `${regionsStr} ${regions.join(', ')}`; + return ( +
  • + {scope} + {': '} + {regionMsg} +
  • + ); +} + +CurrentPermissions.propTypes = { + regions: PropTypes.arrayOf(PropTypes.string).isRequired, + scope: PropTypes.string.isRequired, +}; + +export default CurrentPermissions; diff --git a/frontend/src/pages/Admin/components/PermissionCheckboxLabel.js b/frontend/src/pages/Admin/components/PermissionCheckboxLabel.js new file mode 100644 index 0000000000..146866d08d --- /dev/null +++ b/frontend/src/pages/Admin/components/PermissionCheckboxLabel.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +function PermissionCheckboxLabel({ name, description }) { + return ( + <> + {name} + {': '} + {description} + + ); +} + +PermissionCheckboxLabel.propTypes = { + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, +}; + +export default PermissionCheckboxLabel; diff --git a/frontend/src/pages/Admin/components/__tests__/CurrentPermissions.js b/frontend/src/pages/Admin/components/__tests__/CurrentPermissions.js new file mode 100644 index 0000000000..ff6f1e5b4d --- /dev/null +++ b/frontend/src/pages/Admin/components/__tests__/CurrentPermissions.js @@ -0,0 +1,20 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import CurrentPermissions from '../CurrentPermissions'; +import { withText } from '../../../../testHelpers'; + +describe('CurrentPermissions', () => { + test('renders single region', () => { + render(); + expect(screen.getByText(withText('TEST_SCOPE: Region 1'))).toBeVisible(); + }); + + test('renders multiple regions', () => { + render(); + expect( + screen.getByText(withText('TEST_SCOPE: Regions 1, 2')), + ).toBeVisible(); + }); +}); diff --git a/frontend/src/pages/Admin/components/__tests__/PermissionCheckboxLabel.js b/frontend/src/pages/Admin/components/__tests__/PermissionCheckboxLabel.js new file mode 100644 index 0000000000..ca4b184aed --- /dev/null +++ b/frontend/src/pages/Admin/components/__tests__/PermissionCheckboxLabel.js @@ -0,0 +1,13 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import PermissionCheckboxLabel from '../PermissionCheckboxLabel'; +import { withText } from '../../../../testHelpers'; + +describe('PermissionCheckboxLabel', () => { + test('renders correct text', () => { + render(); + expect(screen.getByText(withText('TEST_SCOPE: test description'))).toBeVisible(); + }); +}); diff --git a/frontend/src/pages/Admin/index.js b/frontend/src/pages/Admin/index.js new file mode 100644 index 0000000000..12973b1bff --- /dev/null +++ b/frontend/src/pages/Admin/index.js @@ -0,0 +1,218 @@ +import React, { useState, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import ReactRouterPropTypes from 'react-router-prop-types'; +import _ from 'lodash'; +import { + GridContainer, Label, TextInput, Grid, SideNav, Button, +} from '@trussworks/react-uswds'; +import UserSection from './UserSection'; +import NavLink from '../../components/NavLink'; + +// Fake return from an API +const fetchedUsers = [ + { + id: 1, + email: 'dumbledore@hogwarts.com', + jobTitle: undefined, + fullName: undefined, + permissions: undefined, + region: undefined, + }, + { + id: 2, + email: 'hermionegranger@hogwarts.com', + jobTitle: 'Systems Specialist', + fullName: 'Hermione Granger', + permissions: [ + { + // Region 0 is used to flag permissions as being "global" (or not associated to a region) + // and will hopefully be changed in the future to something a little less magical. Future + // work will solidify the schema of both global and regional permissions which will require + // updates to any code that uses "region 0". + region: 0, + scope: 'SITE_ACCESS', + }, + { + region: 1, + scope: 'READ_WRITE_REPORTS', + }, + ], + region: 'co', + }, + { + id: 3, + email: 'harrypotter@hogwarts.com', + jobTitle: 'Grantee Specialist', + fullName: 'Harry Potter', + permissions: [ + { + region: 0, + scope: 'ADMIN', + }, + { + region: 0, + scope: 'SITE_ACCESS', + }, + { + region: 1, + scope: 'READ_REPORTS', + }, + { + region: 1, + scope: 'READ_WRITE_REPORTS', + }, + { + region: 2, + scope: 'READ_REPORTS', + }, + { + region: 3, + scope: 'READ_REPORTS', + }, + { + region: 4, + scope: 'READ_REPORTS', + }, + { + region: 5, + scope: 'READ_REPORTS', + }, + { + region: 6, + scope: 'READ_REPORTS', + }, + { + region: 7, + scope: 'READ_REPORTS', + }, + { + region: 8, + scope: 'READ_REPORTS', + }, + { + region: 9, + scope: 'READ_REPORTS', + }, + ], + region: '1', + }, + { + id: 4, + email: 'ronweasley@hogwarts.com', + jobTitle: 'Program Specialist', + fullName: 'Ron Weasley', + permissions: [ + { + region: 0, + scope: 'SITE_ACCESS', + }, + { + region: 5, + scope: 'READ_WRITE_REPORTS', + }, + ], + region: '5', + }, +]; + +/** + * Render the left hand user navigation in the Admin UI. Use the user's full name + * or email address if the user doesn't have a full name. + */ +function renderUserNav(users) { + return users.map((user) => { + const { + fullName, email, id, + } = user; + let display = email; + if (fullName) { + display = fullName; + } + return {display}; + }); +} + +/** + * Admin UI page component. It is split into two main sections, the user list and the + * user section. The user list can be filtered to make searching for users easier. The + * user section contains all info on the user that can be updated (full name, + * permissions, etc...). This component handles fetching of users from the API and will + * be responsible for sending updates/creates back to the API (not yet implemented). + */ +function Admin(props) { + const { match: { params: { userId } } } = props; + const [isLoaded, setIsLoaded] = useState(false); + const [users, updateUsers] = useState([]); + const [userSearch, updateUserSearch] = useState(''); + const history = useHistory(); + + useEffect(() => { + // Mock the API call. The setTimeout will be removed once we hit a real API + setTimeout(() => { + setIsLoaded(true); + updateUsers(fetchedUsers); + }, 400); + }, []); + + const onUserSearchChange = (e) => { + updateUserSearch(e.target.value); + }; + + if (!isLoaded) { + return ( +
    + Loading... +
    + ); + } + + let user; + if (userId === 'new') { + user = {}; + } else if (userId) { + user = users.find((u) => ( + u.id === parseInt(userId, 10) + )); + } + + const filteredUsers = _.filter(users, (u) => { + const { email, fullName } = u; + return `${email}${fullName}`.includes(userSearch); + }); + + return ( +
    + +

    User Administration

    + + + + + +
    + +
    +
    + + {!user + && ( +

    + Select a user... +

    + )} + {user + && ( + + )} +
    +
    +
    +
    + ); +} + +Admin.propTypes = { + match: ReactRouterPropTypes.match.isRequired, +}; + +export default Admin; diff --git a/frontend/src/setupTests.js b/frontend/src/setupTests.js index 74b1a275a0..03c4a3629c 100644 --- a/frontend/src/setupTests.js +++ b/frontend/src/setupTests.js @@ -1,5 +1,15 @@ +// This is a test file so ignore eslint error about packages +// being in dev dependencies instead of dependencies + +/* eslint-disable import/no-extraneous-dependencies */ + // jest-dom adds custom jest matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom/extend-expect'; +// See https://github.com/testing-library/dom-testing-library/releases/tag/v7.0.0 +// 'MutationObserver shim removed' +import MutationObserver from '@sheerun/mutationobserver-shim'; + +window.MutationObserver = MutationObserver; diff --git a/frontend/src/testHelpers.js b/frontend/src/testHelpers.js new file mode 100644 index 0000000000..a40bed82e7 --- /dev/null +++ b/frontend/src/testHelpers.js @@ -0,0 +1,13 @@ +// Disable eslint rule making this a default export. This file will, +// I'm sure, accumulate more helper functions + +/* eslint-disable import/prefer-default-export */ +export const withText = (text) => (content, node) => { + const hasText = (n) => n.textContent === text; + const nodeHasText = hasText(node); + const childrenDontHaveText = Array.from(node.children).every( + (child) => !hasText(child), + ); + + return nodeHasText && childrenDontHaveText; +}; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index a8ceb8f9af..cfaf67d711 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1118,7 +1118,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@>=7.0.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4": +"@babel/runtime@>=7.0.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== @@ -1373,6 +1373,17 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" +"@jest/types@^26.3.0": + version "26.3.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.3.0.tgz#97627bf4bdb72c55346eef98e3b3f7ddc4941f71" + integrity sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -1407,6 +1418,11 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" +"@sheerun/mutationobserver-shim@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz#5405ee8e444ed212db44e79351f0c70a582aae25" + integrity sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw== + "@svgr/babel-plugin-add-jsx-attribute@^4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz#dadcb6218503532d6884b210e7f3c502caaa44b1" @@ -1510,27 +1526,29 @@ "@svgr/plugin-svgo" "^4.3.1" loader-utils "^1.2.3" -"@testing-library/dom@^7.21.7": - version "7.21.7" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.21.7.tgz#23c57c361db5e961afa3e6f3f15bd57fbda01187" - integrity sha512-GVNrLAt0yq7Squz1HrW8IiDVKP5jeWSv9cpgQJsfmXYXLFPpaFoRxn+H/NcUitVXyb0J62PkpVWjMe5b0fvYrQ== +"@testing-library/dom@^7.22.3": + version "7.22.3" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.22.3.tgz#12c0b1b97115e7731da6a86b4574eae401cb9ac5" + integrity sha512-IK6/eL1Xza/0goDKrwnBvlM06L+5eL9b1o+hUhX7HslfUvMETh0TYgXEr2LVpsVkHiOhRmUbUyml95KV/VlRNw== dependencies: "@babel/runtime" "^7.10.3" "@types/aria-query" "^4.2.0" aria-query "^4.2.2" - dom-accessibility-api "^0.4.6" + dom-accessibility-api "^0.5.1" pretty-format "^25.5.0" -"@testing-library/dom@^7.22.3": - version "7.22.3" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.22.3.tgz#12c0b1b97115e7731da6a86b4574eae401cb9ac5" - integrity sha512-IK6/eL1Xza/0goDKrwnBvlM06L+5eL9b1o+hUhX7HslfUvMETh0TYgXEr2LVpsVkHiOhRmUbUyml95KV/VlRNw== +"@testing-library/dom@^7.24.2": + version "7.24.2" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.24.2.tgz#6d2b7dd21efbd5358b98c2777fc47c252f3ae55e" + integrity sha512-ERxcZSoHx0EcN4HfshySEWmEf5Kkmgi+J7O79yCJ3xggzVlBJ2w/QjJUC+EBkJJ2OeSw48i3IoePN4w8JlVUIA== dependencies: + "@babel/code-frame" "^7.10.4" "@babel/runtime" "^7.10.3" "@types/aria-query" "^4.2.0" aria-query "^4.2.2" + chalk "^4.1.0" dom-accessibility-api "^0.5.1" - pretty-format "^25.5.0" + pretty-format "^26.4.2" "@testing-library/jest-dom@^4.2.4": version "4.2.4" @@ -1555,10 +1573,12 @@ "@babel/runtime" "^7.10.3" "@testing-library/dom" "^7.22.3" -"@testing-library/user-event@^7.1.2": - version "7.2.1" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-7.2.1.tgz#2ad4e844175a3738cb9e7064be5ea070b8863a1c" - integrity sha512-oZ0Ib5I4Z2pUEcoo95cT1cr6slco9WY7yiPpG+RGNkj8YcYgJnM7pXmYmorNOReh8MIGcKSqXyeGjxnr8YiZbA== +"@testing-library/user-event@^12.1.5": + version "12.1.5" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-12.1.5.tgz#b2a94b8b3c1a908cc1c425623b11de63b20bb837" + integrity sha512-FzTnKvb0KC4T84G6uTx971ja8OOqLlsprzWbeRyd8f1MDwbcLIFgx0Hyr56izY9m9y2KwHGVKeVEkTPslw32lw== + dependencies: + "@babel/runtime" "^7.10.2" "@trussworks/react-uswds@^1.9.1": version "1.9.1" @@ -1655,6 +1675,13 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/istanbul-reports@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821" + integrity sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA== + dependencies: + "@types/istanbul-lib-report" "*" + "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.4": version "7.0.5" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" @@ -1680,6 +1707,11 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/prop-types@^15.7.3": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + "@types/q@^1.5.1": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" @@ -2550,9 +2582,9 @@ bindings@^1.5.0: file-uri-to-path "1.0.0" bl@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.2.tgz#52b71e9088515d0606d9dd9cc7aa48dc1f98e73a" - integrity sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ== + version "4.0.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" + integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== dependencies: buffer "^5.5.0" inherits "^2.0.4" @@ -2961,7 +2993,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== @@ -3979,11 +4011,6 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.4.6: - version "0.4.7" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.4.7.tgz#31d01c113af49f323409b3ed09e56967aba485a8" - integrity sha512-5+GzhTpCQYHz4NjL8loYTDVBnXIjNLBadWQBKxXk+osFEplLt3EsSYBu2YZcdZ8QqrvCHgW6TSMGMbmgfhrn2g== - dom-accessibility-api@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.1.tgz#152f5e88583d900977119223e3e76c2d93d23830" @@ -5430,6 +5457,13 @@ history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" +history@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/history/-/history-5.0.0.tgz#0cabbb6c4bbf835addb874f8259f6d25101efd08" + integrity sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg== + dependencies: + "@babel/runtime" "^7.7.6" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -7059,6 +7093,11 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== +lodash@^4.17.20: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + loglevel@^1.6.6: version "1.6.8" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" @@ -8917,6 +8956,16 @@ pretty-format@^25.5.0: ansi-styles "^4.0.0" react-is "^16.12.0" +pretty-format@^26.4.2: + version "26.4.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.4.2.tgz#d081d032b398e801e2012af2df1214ef75a81237" + integrity sha512-zK6Gd8zDsEiVydOCGLkoBoZuqv8VTiHyAbKznXe/gaph/DAeZOmit9yMfgIz5adIgAMMs5XfoYSwAX3jcCO1tA== + dependencies: + "@jest/types" "^26.3.0" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^16.12.0" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -9181,6 +9230,14 @@ react-router-dom@^5.2.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-router-prop-types@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/react-router-prop-types/-/react-router-prop-types-1.0.5.tgz#2e671d8412a793106bf70dc15c9ecc83ea4bc15b" + integrity sha512-q1xlFU2ol2U5zeVbA5hyBuxD3scHenqgMgCTuJQUanA2SyG8A3Fb1S6DleOo1cnGJB5Q05hnLge64kRj+xsuPA== + dependencies: + "@types/prop-types" "^15.7.3" + prop-types "^15.7.2" + react-router@5.2.0, react-router@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" diff --git a/src/index.js b/src/index.js index 57435cdbe3..b27be5f820 100644 --- a/src/index.js +++ b/src/index.js @@ -7,6 +7,7 @@ import memorystore from 'memorystore'; import unless from 'express-unless'; import _ from 'lodash'; import path from 'path'; +import logger from './logger'; import authMiddleware, { hsesAuth } from './middleware/authMiddleware'; @@ -52,8 +53,7 @@ router.get(oauth2CallbackPath, async (req, res) => { const { authorities } = data; req.session.userId = 1; // temporary req.session.role = _.get(authorities[0], 'authority'); - // TODO: replace with logging message - console.log(`role: ${req.session.role}`); + logger.info(`role: ${req.session.role}`); res.redirect(req.session.originalUrl); } catch (error) { // console.log(error);