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 @@
+
\ 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 (
+ <>
+
+
+ >
+ );
+}
+
+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);