diff --git a/api/src/paths/administrative-activities.ts b/api/src/paths/administrative-activities.ts index 81d8d9b58d..31b7c2fbd1 100644 --- a/api/src/paths/administrative-activities.ts +++ b/api/src/paths/administrative-activities.ts @@ -108,6 +108,16 @@ GET.apiDoc = { create_date: { type: 'string', description: 'ISO 8601 date string' + }, + updated_by: { + type: 'string', + description: 'Display name of the user who last updated the record', + nullable: true + }, + update_date: { + type: 'string', + description: 'Date when the record was last updated', + nullable: true } } } diff --git a/api/src/repositories/administrative-activity-repository.ts b/api/src/repositories/administrative-activity-repository.ts index 44f02a3a62..69a09b41b0 100644 --- a/api/src/repositories/administrative-activity-repository.ts +++ b/api/src/repositories/administrative-activity-repository.ts @@ -24,7 +24,9 @@ export const IAdministrativeActivity = z.object({ description: z.string().nullable(), data: shallowJsonSchema, notes: z.string().nullable(), - create_date: z.string() + create_date: z.string(), + updated_by: z.string().nullable(), + update_date: z.string().nullable() }); export type IAdministrativeActivity = z.infer; @@ -66,7 +68,9 @@ export class AdministrativeActivityRepository extends BaseRepository { aa.description, aa.data, aa.notes, - aa.create_date + aa.create_date, + su.display_name as updated_by, + aa.update_date FROM administrative_activity aa LEFT OUTER JOIN @@ -77,6 +81,10 @@ export class AdministrativeActivityRepository extends BaseRepository { administrative_activity_type aat ON aa.administrative_activity_type_id = aat.administrative_activity_type_id + LEFT OUTER JOIN + system_user su + ON + su.system_user_id = aa.update_user WHERE 1 = 1 `; @@ -115,7 +123,9 @@ export class AdministrativeActivityRepository extends BaseRepository { sqlStatement.append(SQL`)`); } - sqlStatement.append(`;`); + sqlStatement.append(` + ORDER BY create_date DESC; + `); const response = await this.connection.sql(sqlStatement, IAdministrativeActivity); return response.rows; diff --git a/api/src/services/administrative-activity-service.test.ts b/api/src/services/administrative-activity-service.test.ts index 0f570b87cf..74cc0e175f 100644 --- a/api/src/services/administrative-activity-service.test.ts +++ b/api/src/services/administrative-activity-service.test.ts @@ -42,7 +42,9 @@ describe('AdministrativeActivityService', () => { identitySource: 'BCEIDBASIC' }, notes: null, - create_date: '2023-05-02T02:04:10.751Z' + create_date: '2023-05-02T02:04:10.751Z', + update_date: '2023-05-02T02:04:10.751Z', + updated_by: 'Doe, John WLRS:EX' } ]); @@ -66,7 +68,9 @@ describe('AdministrativeActivityService', () => { identitySource: 'BCEIDBASIC' }, notes: null, - create_date: '2023-05-02T02:04:10.751Z' + create_date: '2023-05-02T02:04:10.751Z', + update_date: '2023-05-02T02:04:10.751Z', + updated_by: 'Doe, John WLRS:EX' } ]); }); diff --git a/app/src/constants/colours.ts b/app/src/constants/colours.ts index f74fb8b408..8d1b98cd2e 100644 --- a/app/src/constants/colours.ts +++ b/app/src/constants/colours.ts @@ -65,6 +65,16 @@ const NRM_REGION_COLOUR_MAP = { 'Skeena Natural Resource Region': { colour: red } }; +/** + * Colour map for access request chips. + * + */ +const ACCESS_REQUEST_STATUS_COLOUR_MAP = { + Pending: { colour: purple }, + Actioned: { colour: green }, + Rejected: { colour: red } +}; + /** * ColourMap key types * @@ -85,6 +95,12 @@ const generateColourMapGetter = (colourMap: T, fallbackColo return (lookup: keyof T) => colourMap[lookup]?.colour ?? fallbackColour; }; +/** + * Get access request status colour mapping. + * + */ +export const getAccessRequestStatusColour = generateColourMapGetter(ACCESS_REQUEST_STATUS_COLOUR_MAP); + /** * Get survey progress colour mapping. * diff --git a/app/src/features/admin/AdminUsersRouter.tsx b/app/src/features/admin/AdminUsersRouter.tsx index a9ae56b0d5..29daa60b0d 100644 --- a/app/src/features/admin/AdminUsersRouter.tsx +++ b/app/src/features/admin/AdminUsersRouter.tsx @@ -3,7 +3,7 @@ import { Redirect, Route, Switch } from 'react-router'; import RouteWithTitle from 'utils/RouteWithTitle'; import { getTitle } from 'utils/Utils'; import ManageUsersPage from './users/ManageUsersPage'; -import UsersDetailPage from './users/UsersDetailPage'; +import UsersDetailPage from './users/projects/UsersDetailPage'; /** * Router for all `/admin/users/*` pages. diff --git a/app/src/features/admin/users/AccessRequestList.test.tsx b/app/src/features/admin/users/AccessRequestList.test.tsx deleted file mode 100644 index 98af5b1f3e..0000000000 --- a/app/src/features/admin/users/AccessRequestList.test.tsx +++ /dev/null @@ -1,291 +0,0 @@ -import { SYSTEM_IDENTITY_SOURCE } from 'constants/auth'; -import { AdministrativeActivityStatusType } from 'constants/misc'; -import AccessRequestList from 'features/admin/users/AccessRequestList'; -import { useBiohubApi } from 'hooks/useBioHubApi'; -import { IAccessRequestDataObject, IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; -import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; -import { codes } from 'test-helpers/code-helpers'; -import { cleanup, fireEvent, render, waitFor } from 'test-helpers/test-utils'; - -jest.mock('../../../hooks/useBioHubApi'); -const mockBiohubApi = useBiohubApi as jest.Mock; - -const mockUseApi = { - admin: { - approveAccessRequest: jest.fn(), - denyAccessRequest: jest.fn() - } -}; - -// Mock the DataGrid component to disable virtualization -jest.mock('@mui/x-data-grid', () => { - const OriginalModule = jest.requireActual('@mui/x-data-grid'); - - // Wrap the DataGrid component to add disableVirtualization prop - const MockedDataGrid = (props: any) => ; - - // Return the original module with the mocked DataGrid - return { - ...OriginalModule, - DataGrid: MockedDataGrid - }; -}); - -const renderContainer = ( - accessRequests: IGetAccessRequestsListResponse[], - codes: IGetAllCodeSetsResponse, - refresh: () => void -) => { - return render(); -}; - -describe('AccessRequestList', () => { - beforeEach(() => { - mockBiohubApi.mockImplementation(() => mockUseApi); - mockUseApi.admin.approveAccessRequest.mockClear(); - mockUseApi.admin.denyAccessRequest.mockClear(); - }); - - afterEach(() => { - cleanup(); - }); - - it('shows `No Access Requests` when there are no access requests', async () => { - const { getByText } = renderContainer([], codes, () => {}); - - await waitFor(() => { - expect(getByText('No Access Requests')).toBeVisible(); - }); - }); - - it('shows a table row for a pending access request', async () => { - const { getByText } = await renderContainer( - [ - { - id: 1, - type: 1, - type_name: 'test type', - status: 1, - status_name: AdministrativeActivityStatusType.PENDING, - description: 'test description', - notes: 'test notes', - data: { - name: 'test user', - username: 'testusername', - userGuid: 'aaaa', - email: 'email@email.com', - identitySource: SYSTEM_IDENTITY_SOURCE.IDIR, - displayName: 'test user', - company: 'test company', - reason: 'my reason' - }, - create_date: '2020-04-20' - } - ], - codes, - () => {} - ); - - await waitFor(() => { - expect(getByText('testusername')).toBeVisible(); - expect(getByText('Apr 20, 2020')).toBeVisible(); - expect(getByText('Review Request')).toBeVisible(); - }); - }); - - it('shows a table row for a rejected access request', async () => { - const { getByText, queryByText } = renderContainer( - [ - { - id: 1, - type: 1, - type_name: 'test type', - status: 1, - status_name: AdministrativeActivityStatusType.REJECTED, - description: 'test description', - notes: 'test notes', - data: { - name: 'test user', - username: 'testusername', - userGuid: 'aaaa', - email: 'email@email.com', - identitySource: SYSTEM_IDENTITY_SOURCE.IDIR, - displayName: 'test user', - company: 'test company', - reason: 'my reason' - }, - create_date: '2020-04-20' - } - ], - codes, - () => {} - ); - - await waitFor(() => { - expect(getByText('testusername')).toBeVisible(); - expect(getByText('Apr 20, 2020')).toBeVisible(); - expect(getByText('Denied')).toBeVisible(); - expect(queryByText('Review Request')).not.toBeInTheDocument(); - }); - }); - - it('shows a table row for a actioned access request', async () => { - const { getByText, queryByText } = renderContainer( - [ - { - id: 1, - type: 1, - type_name: 'test type', - status: 1, - status_name: AdministrativeActivityStatusType.ACTIONED, - description: 'test description', - notes: 'test notes', - data: { - name: 'test user', - username: 'testusername', - userGuid: 'aaaa', - email: 'email@email.com', - identitySource: SYSTEM_IDENTITY_SOURCE.IDIR, - displayName: 'test user', - company: 'test company', - reason: 'my reason' - }, - create_date: '2020-04-20' - } - ], - codes, - () => {} - ); - - await waitFor(() => { - expect(getByText('testusername')).toBeVisible(); - expect(getByText('Apr 20, 2020')).toBeVisible(); - expect(getByText('Approved')).toBeVisible(); - expect(queryByText('Review Request')).not.toBeInTheDocument(); - }); - }); - - it('shows a table row when the data object is null', async () => { - const { getByText } = renderContainer( - [ - { - id: 1, - type: 1, - type_name: 'test type', - status: 1, - status_name: AdministrativeActivityStatusType.PENDING, - description: 'test description', - notes: 'test notes', - data: null as unknown as IAccessRequestDataObject, - create_date: '2020-04-20' - } - ], - codes, - () => {} - ); - - await waitFor(() => { - expect(getByText('Apr 20, 2020')).toBeVisible(); - expect(getByText('Review Request')).toBeVisible(); - }); - }); - - it('opens the review dialog and calls approveAccessRequest on approval', async () => { - const refresh = jest.fn(); - - const { getByText, getByTestId } = renderContainer( - [ - { - id: 1, - type: 1, - type_name: 'test type', - status: 1, - status_name: AdministrativeActivityStatusType.PENDING, - description: 'test description', - notes: 'test notes', - data: { - name: 'test user', - username: 'testusername', - userGuid: 'aaaa', - email: 'email@email.com', - identitySource: SYSTEM_IDENTITY_SOURCE.IDIR, - displayName: 'test user', - company: 'test company', - reason: 'my reason' - }, - create_date: '2020-04-20' - } - ], - codes, - refresh - ); - - const reviewButton = getByText('Review Request'); - fireEvent.click(reviewButton); - - await waitFor(() => { - // wait for dialog to open - expect(getByText('Review Access Request')).toBeVisible(); - fireEvent.click(getByTestId('request_approve_button')); - }); - - await waitFor(() => { - expect(refresh).toHaveBeenCalledTimes(1); - expect(mockUseApi.admin.approveAccessRequest).toHaveBeenCalledTimes(1); - expect(mockUseApi.admin.approveAccessRequest).toHaveBeenCalledWith(1, { - displayName: 'test user', - email: 'email@email.com', - identitySource: 'IDIR', - roleIds: [], - userGuid: 'aaaa', - userIdentifier: 'testusername' - }); - }); - }); - - it('opens the review dialog and calls denyAccessRequest on denial', async () => { - const refresh = jest.fn(); - - const { getByText, getByTestId } = renderContainer( - [ - { - id: 1, - type: 1, - type_name: 'test type', - status: 1, - status_name: AdministrativeActivityStatusType.PENDING, - description: 'test description', - notes: 'test notes', - data: { - name: 'test user', - username: 'testusername', - userGuid: 'aaaa', - email: 'email@email.com', - identitySource: SYSTEM_IDENTITY_SOURCE.IDIR, - displayName: 'test user', - company: 'test company', - reason: 'my reason' - }, - create_date: '2020-04-20' - } - ], - codes, - refresh - ); - - const reviewButton = getByText('Review Request'); - fireEvent.click(reviewButton); - - await waitFor(() => { - // wait for dialog to open - expect(getByText('Review Access Request')).toBeVisible(); - fireEvent.click(getByTestId('request_deny_button')); - }); - - await waitFor(() => { - expect(refresh).toHaveBeenCalledTimes(1); - expect(mockUseApi.admin.denyAccessRequest).toHaveBeenCalledTimes(1); - expect(mockUseApi.admin.denyAccessRequest).toHaveBeenCalledWith(1); - }); - }); -}); diff --git a/app/src/features/admin/users/ManageUsersPage.test.tsx b/app/src/features/admin/users/ManageUsersPage.test.tsx index c9d44764ba..ea17d4e119 100644 --- a/app/src/features/admin/users/ManageUsersPage.test.tsx +++ b/app/src/features/admin/users/ManageUsersPage.test.tsx @@ -86,7 +86,7 @@ describe('ManageUsersPage', () => { const { getByText } = renderContainer(); await waitFor(() => { - expect(getByText('No Access Requests')).toBeVisible(); + expect(getByText('No Pending Access Requests')).toBeVisible(); expect(getByText('No Active Users')).toBeVisible(); }); }); diff --git a/app/src/features/admin/users/ManageUsersPage.tsx b/app/src/features/admin/users/ManageUsersPage.tsx index 69afec4863..9c11f7e044 100644 --- a/app/src/features/admin/users/ManageUsersPage.tsx +++ b/app/src/features/admin/users/ManageUsersPage.tsx @@ -3,13 +3,13 @@ import CircularProgress from '@mui/material/CircularProgress'; import Container from '@mui/material/Container'; import PageHeader from 'components/layout/PageHeader'; import { AdministrativeActivityStatusType, AdministrativeActivityType } from 'constants/misc'; -import AccessRequestList from 'features/admin/users/AccessRequestList'; import { useBiohubApi } from 'hooks/useBioHubApi'; import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; import { ISystemUser } from 'interfaces/useUserApi.interface'; import React, { useEffect, useState } from 'react'; -import ActiveUsersList from './ActiveUsersList'; +import AccessRequestContainer from './access-requests/AccessRequestContainer'; +import ActiveUsersList from './active/ActiveUsersList'; /** * Page to display user management data/functionality. @@ -33,7 +33,11 @@ const ManageUsersPage: React.FC = () => { const refreshAccessRequests = async () => { const accessResponse = await biohubApi.admin.getAdministrativeActivities( [AdministrativeActivityType.SYSTEM_ACCESS], - [AdministrativeActivityStatusType.PENDING, AdministrativeActivityStatusType.REJECTED] + [ + AdministrativeActivityStatusType.PENDING, + AdministrativeActivityStatusType.REJECTED, + AdministrativeActivityStatusType.ACTIONED + ] ); setAccessRequests(accessResponse); @@ -43,7 +47,11 @@ const ManageUsersPage: React.FC = () => { const getAccessRequests = async () => { const accessResponse = await biohubApi.admin.getAdministrativeActivities( [AdministrativeActivityType.SYSTEM_ACCESS], - [AdministrativeActivityStatusType.PENDING, AdministrativeActivityStatusType.REJECTED] + [ + AdministrativeActivityStatusType.PENDING, + AdministrativeActivityStatusType.REJECTED, + AdministrativeActivityStatusType.ACTIONED + ] ); setAccessRequests(() => { @@ -120,7 +128,7 @@ const ManageUsersPage: React.FC = () => { <> - { diff --git a/app/src/features/admin/users/access-requests/AccessRequestContainer.tsx b/app/src/features/admin/users/access-requests/AccessRequestContainer.tsx new file mode 100644 index 0000000000..051a9616ce --- /dev/null +++ b/app/src/features/admin/users/access-requests/AccessRequestContainer.tsx @@ -0,0 +1,123 @@ +import { mdiCancel, mdiCheck, mdiExclamationThick } from '@mdi/js'; +import Icon from '@mdi/react'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Divider from '@mui/material/Divider'; +import Paper from '@mui/material/Paper'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup/ToggleButtonGroup'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; +import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; +import { useState } from 'react'; +import AccessRequestActionedList from './list/actioned/AccessRequestActionedList'; +import AccessRequestPendingList from './list/pending/AccessRequestPendingList'; +import AccessRequestRejectedList from './list/rejected/AccessRequestRejectedList'; + +export interface IAccessRequestContainerProps { + accessRequests: IGetAccessRequestsListResponse[]; + codes: IGetAllCodeSetsResponse; + refresh: () => void; +} + +enum AccessRequestViewEnum { + ACTIONED = 'ACTIONED', + PENDING = 'PENDING', + REJECTED = 'REJECTED' +} + +/** + * Container for displaying a list of user access requests. + * + */ +const AccessRequestContainer = (props: IAccessRequestContainerProps) => { + const { accessRequests, codes, refresh } = props; + const [activeView, setActiveView] = useState(AccessRequestViewEnum.PENDING); + + const views = [ + { value: AccessRequestViewEnum.PENDING, label: 'Pending', icon: mdiExclamationThick }, + { value: AccessRequestViewEnum.ACTIONED, label: 'Approved', icon: mdiCheck }, + { value: AccessRequestViewEnum.REJECTED, label: 'Rejected', icon: mdiCancel } + ]; + + const pendingRequests = accessRequests.filter((request) => request.status_name === 'Pending'); + const actionedRequests = accessRequests.filter((request) => request.status_name === 'Actioned'); + const rejectedRequests = accessRequests.filter((request) => request.status_name === 'Rejected'); + + return ( + + + + Access Requests + + + + + { + if (!view) { + // An active view must be selected at all times + return; + } + + setActiveView(view); + }} + exclusive + sx={{ + width: '100%', + gap: 1, + '& Button': { + py: 0.5, + px: 1.5, + border: 'none !important', + fontWeight: 700, + borderRadius: '4px !important', + fontSize: '0.875rem', + letterSpacing: '0.02rem' + } + }}> + {views.map((view) => { + const getCount = () => { + switch (view.value) { + case AccessRequestViewEnum.PENDING: + return pendingRequests.length; + case AccessRequestViewEnum.ACTIONED: + return actionedRequests.length; + case AccessRequestViewEnum.REJECTED: + return rejectedRequests.length; + default: + return 0; + } + }; + return ( + }> + {view.label} ({getCount()}) + + ); + })} + + + + + {activeView === AccessRequestViewEnum.PENDING && ( + + )} + {activeView === AccessRequestViewEnum.ACTIONED && ( + + )} + {activeView === AccessRequestViewEnum.REJECTED && ( + + )} + + + ); +}; + +export default AccessRequestContainer; diff --git a/app/src/features/admin/users/ReviewAccessRequestForm.test.tsx b/app/src/features/admin/users/access-requests/components/ReviewAccessRequestForm.test.tsx similarity index 95% rename from app/src/features/admin/users/ReviewAccessRequestForm.test.tsx rename to app/src/features/admin/users/access-requests/components/ReviewAccessRequestForm.test.tsx index 1c4c5a2c36..4041c0f84a 100644 --- a/app/src/features/admin/users/ReviewAccessRequestForm.test.tsx +++ b/app/src/features/admin/users/access-requests/components/ReviewAccessRequestForm.test.tsx @@ -1,12 +1,12 @@ import { SYSTEM_IDENTITY_SOURCE } from 'constants/auth'; -import ReviewAccessRequestForm, { - ReviewAccessRequestFormInitialValues, - ReviewAccessRequestFormYupSchema -} from 'features/admin/users/ReviewAccessRequestForm'; import { Formik } from 'formik'; import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; import { codes } from 'test-helpers/code-helpers'; import { render, waitFor } from 'test-helpers/test-utils'; +import ReviewAccessRequestForm, { + ReviewAccessRequestFormInitialValues, + ReviewAccessRequestFormYupSchema +} from './ReviewAccessRequestForm'; describe('ReviewAccessRequestForm', () => { describe('IDIR Request', () => { @@ -20,6 +20,8 @@ describe('ReviewAccessRequestForm', () => { description: 'test description', notes: 'test node', create_date: '2021-04-18', + updated_by: 'Doe, John WLRS:EX', + update_date: '2021-04-20', data: { name: 'test data name', username: 'test data username', @@ -54,7 +56,6 @@ describe('ReviewAccessRequestForm', () => { expect(getByText('test data name')).toBeVisible(); expect(getByText('IDIR/test data username')).toBeVisible(); expect(getByText('test data email')).toBeVisible(); - expect(getByText('04/18/2021')).toBeVisible(); }); }); }); @@ -70,6 +71,8 @@ describe('ReviewAccessRequestForm', () => { description: 'test description', notes: 'test node', create_date: '2021-04-18', + updated_by: 'Doe, John WLRS:EX', + update_date: '2021-04-20', data: { name: 'test data name', username: 'test data username', @@ -105,7 +108,6 @@ describe('ReviewAccessRequestForm', () => { expect(getByText('test data name')).toBeVisible(); expect(getByText('BCeID Basic/test data username')).toBeVisible(); expect(getByText('test data email')).toBeVisible(); - expect(getByText('04/18/2021')).toBeVisible(); expect(getByText('test company')).toBeVisible(); }); }); diff --git a/app/src/features/admin/users/ReviewAccessRequestForm.tsx b/app/src/features/admin/users/access-requests/components/ReviewAccessRequestForm.tsx similarity index 95% rename from app/src/features/admin/users/ReviewAccessRequestForm.tsx rename to app/src/features/admin/users/access-requests/components/ReviewAccessRequestForm.tsx index 51f4f2c7b9..5aee63c3f7 100644 --- a/app/src/features/admin/users/ReviewAccessRequestForm.tsx +++ b/app/src/features/admin/users/access-requests/components/ReviewAccessRequestForm.tsx @@ -4,10 +4,11 @@ import Typography from '@mui/material/Typography'; import AutocompleteField, { IAutocompleteFieldOption } from 'components/fields/AutocompleteField'; import { SYSTEM_IDENTITY_SOURCE } from 'constants/auth'; import { DATE_FORMAT } from 'constants/dateTimeFormats'; +import dayjs from 'dayjs'; import { useFormikContext } from 'formik'; import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; import React from 'react'; -import { getFormattedDate, getFormattedIdentitySource } from 'utils/Utils'; +import { getFormattedIdentitySource } from 'utils/Utils'; import yup from 'utils/YupSchema'; export interface IReviewAccessRequestForm { @@ -79,7 +80,7 @@ const ReviewAccessRequestForm: React.FC = (props) Date of Request - {getFormattedDate(DATE_FORMAT.ShortDateFormatMonthFirst, props.request.create_date)} + {dayjs(props.request.create_date).format(DATE_FORMAT.ShortMediumDateTimeFormat)} @@ -109,7 +110,7 @@ const ReviewAccessRequestForm: React.FC = (props) sx={{ marginBottom: '18px' }}> - Requested System Role + System Role
diff --git a/app/src/features/admin/users/access-requests/components/ViewAccessRequestForm.tsx b/app/src/features/admin/users/access-requests/components/ViewAccessRequestForm.tsx new file mode 100644 index 0000000000..49ee184853 --- /dev/null +++ b/app/src/features/admin/users/access-requests/components/ViewAccessRequestForm.tsx @@ -0,0 +1,93 @@ +import { mdiInformationOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import Box from '@mui/material/Box'; +import { blue } from '@mui/material/colors'; +import Grid from '@mui/material/Grid'; +import Typography from '@mui/material/Typography'; +import { SYSTEM_IDENTITY_SOURCE } from 'constants/auth'; +import { DATE_FORMAT } from 'constants/dateTimeFormats'; +import dayjs from 'dayjs'; +import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; +import React from 'react'; +import { getFormattedIdentitySource } from 'utils/Utils'; + +export interface IViewAccessReuqestFormProps { + request: IGetAccessRequestsListResponse; + bannerText: string; +} + +/** + * Component to view system access requests without the ability to edit the user's system role + * + * @return {*} + */ +export const ViewAccessRequestForm: React.FC = (props: IViewAccessReuqestFormProps) => { + const formattedUsername = [ + getFormattedIdentitySource(props.request.data.identitySource as SYSTEM_IDENTITY_SOURCE), + props.request.data.username + ] + .filter(Boolean) + .join('/'); + + return ( + <> + + + + + {props.bannerText} + + + + + + User Details + +
+ + + + Name + + {props.request.data.name} + + + + Username + + {formattedUsername} + + + + Email Address + + {props.request.data.email} + + + + Date of Request + + + {dayjs(props.request.create_date).format(DATE_FORMAT.ShortMediumDateTimeFormat)} + + + + + Company + + + {('company' in props.request.data && props.request.data.company) || 'Not Applicable'} + + + + + Reason for Request + + {props.request.data.reason} + + +
+
+ + ); +}; diff --git a/app/src/features/admin/users/access-requests/list/actioned/AccessRequestActionedList.tsx b/app/src/features/admin/users/access-requests/list/actioned/AccessRequestActionedList.tsx new file mode 100644 index 0000000000..c16d0bc61a --- /dev/null +++ b/app/src/features/admin/users/access-requests/list/actioned/AccessRequestActionedList.tsx @@ -0,0 +1,128 @@ +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import { GridColDef } from '@mui/x-data-grid'; +import ColouredRectangleChip from 'components/chips/ColouredRectangleChip'; +import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; +import { getAccessRequestStatusColour } from 'constants/colours'; +import { DATE_FORMAT } from 'constants/dateTimeFormats'; +import dayjs from 'dayjs'; +import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; +import { useState } from 'react'; +import { ViewAccessRequestForm } from '../../components/ViewAccessRequestForm'; + +interface IAccessRequestActionedListProps { + accessRequests: IGetAccessRequestsListResponse[]; +} + +/** + * Returns a data grid component displaying approved access requests + * + * @param props {IAccessRequestActionedListProps} + * @returns + */ +const AccessRequestActionedList = (props: IAccessRequestActionedListProps) => { + const { accessRequests } = props; + + const [showViewDialog, setShowViewDialog] = useState(false); + const [activeReview, setActiveReview] = useState(null); + + const accessRequestsColumnDefs: GridColDef[] = [ + { + field: 'display_name', + headerName: 'Display Name', + flex: 1, + disableColumnMenu: true, + valueGetter: (params) => { + return params.row.data?.displayName; + } + }, + { + field: 'username', + headerName: 'Username', + flex: 1, + disableColumnMenu: true, + valueGetter: (params) => { + return params.row.data?.username; + } + }, + { + field: 'create_date', + flex: 1, + headerName: 'Date of Request', + disableColumnMenu: true, + valueFormatter: (params) => { + return dayjs(params.value).format(DATE_FORMAT.ShortMediumDateTimeFormat); + } + }, + { + field: 'status_name', + width: 170, + headerName: 'Status', + disableColumnMenu: true, + renderCell: (params) => { + return ( + + ); + } + }, + { + field: 'actions', + headerName: '', + type: 'actions', + flex: 1, + sortable: false, + disableColumnMenu: true, + resizable: false, + align: 'right', + renderCell: (params) => ( + + ) + } + ]; + + return ( + <> + {activeReview && ( + setShowViewDialog(false)}> + Access Request + + + + + )} + + + ); +}; + +export default AccessRequestActionedList; diff --git a/app/src/features/admin/users/AccessRequestList.tsx b/app/src/features/admin/users/access-requests/list/pending/AccessRequestPendingList.tsx similarity index 59% rename from app/src/features/admin/users/AccessRequestList.tsx rename to app/src/features/admin/users/access-requests/list/pending/AccessRequestPendingList.tsx index fbd0ac607a..8144e3da8c 100644 --- a/app/src/features/admin/users/AccessRequestList.tsx +++ b/app/src/features/admin/users/access-requests/list/pending/AccessRequestPendingList.tsx @@ -1,88 +1,60 @@ -import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; -import Divider from '@mui/material/Divider'; -import Paper from '@mui/material/Paper'; -import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { GridColDef } from '@mui/x-data-grid'; -import { AccessStatusChip } from 'components/chips/AccessStatusChip'; +import ColouredRectangleChip from 'components/chips/ColouredRectangleChip'; import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; import RequestDialog from 'components/dialog/RequestDialog'; +import { getAccessRequestStatusColour } from 'constants/colours'; import { DATE_FORMAT } from 'constants/dateTimeFormats'; -import { AccessApprovalDispatchI18N, AccessDenialDispatchI18N, ReviewAccessRequestI18N } from 'constants/i18n'; -import { AdministrativeActivityStatusType } from 'constants/misc'; -import { DialogContext } from 'contexts/dialogContext'; +import { ReviewAccessRequestI18N } from 'constants/i18n'; +import { DialogContext, ISnackbarProps } from 'contexts/dialogContext'; +import dayjs from 'dayjs'; import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; import { useContext, useState } from 'react'; -import { getFormattedDate } from 'utils/Utils'; import ReviewAccessRequestForm, { IReviewAccessRequestForm, ReviewAccessRequestFormInitialValues, ReviewAccessRequestFormYupSchema -} from './ReviewAccessRequestForm'; +} from '../../components/ReviewAccessRequestForm'; -export interface IAccessRequestListProps { +interface IAccessRequestPendingListProps { accessRequests: IGetAccessRequestsListResponse[]; codes: IGetAllCodeSetsResponse; refresh: () => void; } -const pageSizeOptions = [10, 25, 50]; - /** - * Page to display a list of user access. + * Returns a data grid component displaying pending access requests * + * @param props {IAccessRequestPendingListProps} + * @returns */ -const AccessRequestList = (props: IAccessRequestListProps) => { +const AccessRequestPendingList = (props: IAccessRequestPendingListProps) => { const { accessRequests, codes, refresh } = props; const biohubApi = useBiohubApi(); + const dialogContext = useContext(DialogContext); const [showReviewDialog, setShowReviewDialog] = useState(false); const [activeReview, setActiveReview] = useState(null); - const dialogContext = useContext(DialogContext); - - const defaultErrorDialogProps = { - dialogTitle: ReviewAccessRequestI18N.reviewErrorTitle, - dialogText: ReviewAccessRequestI18N.reviewErrorText, - open: false, - onClose: () => { - dialogContext.setErrorDialog({ open: false }); - }, - onOk: () => { - dialogContext.setErrorDialog({ open: false }); - } - }; - - const dispatchApprovalErrorDialogProps = { - dialogTitle: AccessApprovalDispatchI18N.reviewErrorTitle, - dialogText: AccessApprovalDispatchI18N.reviewErrorText, - open: false, - onClose: () => { - dialogContext.setErrorDialog({ open: false }); - }, - onOk: () => { - dialogContext.setErrorDialog({ open: false }); - } - }; - - const dispatchDenialErrorDialogProps = { - dialogTitle: AccessDenialDispatchI18N.reviewErrorTitle, - dialogText: AccessDenialDispatchI18N.reviewErrorText, - open: false, - onClose: () => { - dialogContext.setErrorDialog({ open: false }); - }, - onOk: () => { - dialogContext.setErrorDialog({ open: false }); - } + const showSnackBar = (textDialogProps?: Partial) => { + dialogContext.setSnackbar({ ...textDialogProps, open: true }); }; const accessRequestsColumnDefs: GridColDef[] = [ + { + field: 'display_name', + headerName: 'Display Name', + flex: 1, + disableColumnMenu: true, + valueGetter: (params) => { + return params.row.data?.displayName; + } + }, { field: 'username', headerName: 'Username', @@ -98,7 +70,7 @@ const AccessRequestList = (props: IAccessRequestListProps) => { headerName: 'Date of Request', disableColumnMenu: true, valueFormatter: (params) => { - return getFormattedDate(DATE_FORMAT.ShortMediumDateFormat, params.value); + return dayjs(params.value).format(DATE_FORMAT.ShortMediumDateTimeFormat); } }, { @@ -107,7 +79,12 @@ const AccessRequestList = (props: IAccessRequestListProps) => { headerName: 'Status', disableColumnMenu: true, renderCell: (params) => { - return ; + return ( + + ); } }, { @@ -119,26 +96,32 @@ const AccessRequestList = (props: IAccessRequestListProps) => { disableColumnMenu: true, resizable: false, align: 'right', - renderCell: (params) => { - if (params.row.status_name !== AdministrativeActivityStatusType.PENDING) { - return <>; - } - - return ( - - ); - } + renderCell: (params) => ( + + ) } ]; + const defaultErrorDialogProps = { + dialogTitle: ReviewAccessRequestI18N.reviewErrorTitle, + dialogText: ReviewAccessRequestI18N.reviewErrorText, + open: false, + onClose: () => { + dialogContext.setErrorDialog({ open: false }); + }, + onOk: () => { + dialogContext.setErrorDialog({ open: false }); + } + }; + const handleReviewDialogApprove = async (values: IReviewAccessRequestForm) => { if (!activeReview) { return; @@ -156,6 +139,14 @@ const AccessRequestList = (props: IAccessRequestListProps) => { roleIds: (values.system_role && [values.system_role]) || [] }); + showSnackBar({ + snackbarMessage: ( + + Approved access request + + ) + }); + try { await biohubApi.admin.sendGCNotification( { @@ -166,16 +157,18 @@ const AccessRequestList = (props: IAccessRequestListProps) => { { subject: 'SIMS: Your request for access has been approved.', header: 'Your request for access to the Species Inventory Management System has been approved.', - main_body1: 'This is an automated message from the BioHub Species Inventory Management System', + main_body1: 'This is an automated message from the Species Inventory Management System', main_body2: '', footer: '' } ); } catch (error) { - dialogContext.setErrorDialog({ - ...dispatchApprovalErrorDialogProps, - open: true, - dialogErrorDetails: (error as APIError).errors + showSnackBar({ + snackbarMessage: ( + + Approved access request, but failed to send notification + + ) }); } finally { refresh(); @@ -199,6 +192,14 @@ const AccessRequestList = (props: IAccessRequestListProps) => { try { await biohubApi.admin.denyAccessRequest(activeReview.id); + showSnackBar({ + snackbarMessage: ( + + Approved access request + + ) + }); + try { await biohubApi.admin.sendGCNotification( { @@ -209,16 +210,18 @@ const AccessRequestList = (props: IAccessRequestListProps) => { { subject: 'SIMS: Your request for access has been denied.', header: 'Your request for access to the Species Inventory Management System has been denied.', - main_body1: 'This is an automated message from the BioHub Species Inventory Management System', + main_body1: 'This is an automated message from the Species Inventory Management System', main_body2: '', footer: '' } ); } catch (error) { - dialogContext.setErrorDialog({ - ...dispatchDenialErrorDialogProps, - open: true, - dialogErrorDetails: (error as APIError).errors + showSnackBar({ + snackbarMessage: ( + + Denied access request, but failed to send notification + + ) }); } finally { refresh(); @@ -246,51 +249,28 @@ const AccessRequestList = (props: IAccessRequestListProps) => { element: activeReview ? ( { - return { value: item.id, label: item.name }; - }) || [] - } + system_roles={codes?.system_roles?.map((item: any) => ({ value: item.id, label: item.name })) || []} /> ) : ( <> ) }} /> - - - - Access Requests{' '} - - ({Number(accessRequests?.length ?? 0).toLocaleString()}) - - - - - - - - + ); }; -export default AccessRequestList; +export default AccessRequestPendingList; diff --git a/app/src/features/admin/users/access-requests/list/rejected/AccessRequestRejectedList.tsx b/app/src/features/admin/users/access-requests/list/rejected/AccessRequestRejectedList.tsx new file mode 100644 index 0000000000..b22d0ddab8 --- /dev/null +++ b/app/src/features/admin/users/access-requests/list/rejected/AccessRequestRejectedList.tsx @@ -0,0 +1,128 @@ +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import { GridColDef } from '@mui/x-data-grid'; +import ColouredRectangleChip from 'components/chips/ColouredRectangleChip'; +import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; +import { getAccessRequestStatusColour } from 'constants/colours'; +import { DATE_FORMAT } from 'constants/dateTimeFormats'; +import dayjs from 'dayjs'; +import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; +import { useState } from 'react'; +import { ViewAccessRequestForm } from '../../components/ViewAccessRequestForm'; + +interface IAccessRequestRejectedListProps { + accessRequests: IGetAccessRequestsListResponse[]; +} + +/** + * Returns a data grid component displaying denied access requests + * + * @param props {IAccessRequestRejectedListProps} + * @returns + */ +const AccessRequestRejectedList = (props: IAccessRequestRejectedListProps) => { + const { accessRequests } = props; + + const [showReviewDialog, setShowReviewDialog] = useState(false); + const [activeReview, setActiveReview] = useState(null); + + const accessRequestsColumnDefs: GridColDef[] = [ + { + field: 'display_name', + headerName: 'Display Name', + flex: 1, + disableColumnMenu: true, + valueGetter: (params) => { + return params.row.data?.displayName; + } + }, + { + field: 'username', + headerName: 'Username', + flex: 1, + disableColumnMenu: true, + valueGetter: (params) => { + return params.row.data?.username; + } + }, + { + field: 'create_date', + flex: 1, + headerName: 'Date of Request', + disableColumnMenu: true, + valueFormatter: (params) => { + return dayjs(params.value).format(DATE_FORMAT.ShortMediumDateTimeFormat); + } + }, + { + field: 'status_name', + width: 170, + headerName: 'Status', + disableColumnMenu: true, + renderCell: (params) => { + return ( + + ); + } + }, + { + field: 'actions', + headerName: '', + type: 'actions', + flex: 1, + sortable: false, + disableColumnMenu: true, + resizable: false, + align: 'right', + renderCell: (params) => ( + + ) + } + ]; + + return ( + <> + {activeReview && ( + setShowReviewDialog(false)}> + Access Request + + + + + )} + + + ); +}; + +export default AccessRequestRejectedList; diff --git a/app/src/features/admin/users/ActiveUsersList.test.tsx b/app/src/features/admin/users/active/ActiveUsersList.test.tsx similarity index 61% rename from app/src/features/admin/users/ActiveUsersList.test.tsx rename to app/src/features/admin/users/active/ActiveUsersList.test.tsx index 767aadca26..272db2df25 100644 --- a/app/src/features/admin/users/ActiveUsersList.test.tsx +++ b/app/src/features/admin/users/active/ActiveUsersList.test.tsx @@ -11,7 +11,7 @@ import ActiveUsersList, { IActiveUsersListProps } from './ActiveUsersList'; const history = createMemoryHistory(); -jest.mock('../../../hooks/useBioHubApi'); +jest.mock('../../../../hooks/useBioHubApi'); const mockBiohubApi = useBiohubApi as jest.Mock; const mockUseApi = { @@ -69,57 +69,6 @@ describe('ActiveUsersList', () => { }); }); - it('shows a table row for an active user with all fields having values', async () => { - const { getByText } = renderContainer({ - activeUsers: [ - { - system_user_id: 1, - user_identifier: 'username', - user_guid: 'user-guid', - record_end_date: '2020-10-10', - role_names: ['role 1'], - identity_source: 'idir', - role_ids: [1], - email: '', - display_name: '', - agency: '' - } - ], - codes: codes, - refresh: () => {} - }); - - await waitFor(() => { - expect(getByText('username')).toBeVisible(); - expect(getByText('role 1')).toBeVisible(); - }); - }); - - it('shows a table row for an active user with fields not having values', async () => { - const { getByTestId } = renderContainer({ - activeUsers: [ - { - system_user_id: 1, - user_identifier: 'username', - user_guid: 'user-guid', - record_end_date: '2020-10-10', - role_names: [], - identity_source: 'idir', - role_ids: [], - email: '', - display_name: '', - agency: '' - } - ], - codes: codes, - refresh: () => {} - }); - - await waitFor(() => { - expect(getByTestId('custom-menu-button-NotApplicable')).toBeInTheDocument(); - }); - }); - it('renders the add new users button correctly', async () => { const { getByTestId } = renderContainer({ activeUsers: [], diff --git a/app/src/features/admin/users/ActiveUsersList.tsx b/app/src/features/admin/users/active/ActiveUsersList.tsx similarity index 93% rename from app/src/features/admin/users/ActiveUsersList.tsx rename to app/src/features/admin/users/active/ActiveUsersList.tsx index f8ef7845d7..f6380948e2 100644 --- a/app/src/features/admin/users/ActiveUsersList.tsx +++ b/app/src/features/admin/users/active/ActiveUsersList.tsx @@ -2,6 +2,7 @@ import { mdiAccountDetailsOutline, mdiChevronDown, mdiDotsVertical, mdiPlus, mdi import Icon from '@mdi/react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; +import grey from '@mui/material/colors/grey'; import Divider from '@mui/material/Divider'; import Link from '@mui/material/Link'; import Paper from '@mui/material/Paper'; @@ -27,7 +28,7 @@ import AddSystemUsersForm, { AddSystemUsersFormInitialValues, AddSystemUsersFormYupSchema, IAddSystemUsersForm -} from './AddSystemUsersForm'; +} from '../add/AddSystemUsersForm'; export interface IActiveUsersListProps { activeUsers: ISystemUser[]; @@ -60,8 +61,24 @@ const ActiveUsersList = (props: IActiveUsersListProps) => { const activeUsersColumnDefs: GridColDef[] = [ { - field: 'user_identifier', - headerName: 'Username', + field: 'system_user_id', + headerName: 'ID', + width: 70, + minWidth: 70, + renderHeader: () => ( + + ID + + ), + renderCell: (params) => ( + + {params.row.system_user_id} + + ) + }, + { + field: 'display_name', + headerName: 'Display Name', flex: 1, disableColumnMenu: true, renderCell: (params) => { @@ -71,11 +88,29 @@ const ActiveUsersList = (props: IActiveUsersListProps) => { underline="always" to={`/admin/users/${params.row.system_user_id}`} component={RouterLink}> - {params.row.user_identifier || 'No identifier'} + {params.row.display_name || 'No identifier'} ); } }, + { + field: 'identity_source', + headerName: 'Account Type', + flex: 1, + disableColumnMenu: true, + valueGetter: (params) => { + return params.row.identity_source; + } + }, + { + field: 'user_identifier', + headerName: 'Username', + flex: 1, + disableColumnMenu: true, + valueGetter: (params) => { + return params.row.user_identifier; + } + }, { field: 'role_names', flex: 1, @@ -351,7 +386,7 @@ const ActiveUsersList = (props: IActiveUsersListProps) => { - Active Users{' '} + Active Users  = (props) => { Manage Users - {userDetails.user_identifier} + {userDetails.display_name} } - title={userDetails.user_identifier} + title={userDetails.display_name} subTitleJSX={ {userDetails.role_names[0]} diff --git a/app/src/features/admin/users/UsersDetailPage.test.tsx b/app/src/features/admin/users/projects/UsersDetailPage.test.tsx similarity index 90% rename from app/src/features/admin/users/UsersDetailPage.test.tsx rename to app/src/features/admin/users/projects/UsersDetailPage.test.tsx index 1de69e46d3..e12cf2326b 100644 --- a/app/src/features/admin/users/UsersDetailPage.test.tsx +++ b/app/src/features/admin/users/projects/UsersDetailPage.test.tsx @@ -1,15 +1,15 @@ import { createMemoryHistory } from 'history'; +import { useBiohubApi } from 'hooks/useBioHubApi'; import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; +import { IGetUserProjectsListResponse } from 'interfaces/useProjectApi.interface'; +import { ISystemUser } from 'interfaces/useUserApi.interface'; import { Router } from 'react-router'; import { cleanup, render, waitFor } from 'test-helpers/test-utils'; -import { useBiohubApi } from '../../../hooks/useBioHubApi'; -import { IGetUserProjectsListResponse } from '../../../interfaces/useProjectApi.interface'; -import { ISystemUser } from '../../../interfaces/useUserApi.interface'; import UsersDetailPage from './UsersDetailPage'; const history = createMemoryHistory(); -jest.mock('../../../hooks/useBioHubApi'); +jest.mock('../../../../hooks/useBioHubApi'); const mockBiohubApi = useBiohubApi as jest.Mock; diff --git a/app/src/features/admin/users/UsersDetailPage.tsx b/app/src/features/admin/users/projects/UsersDetailPage.tsx similarity index 90% rename from app/src/features/admin/users/UsersDetailPage.tsx rename to app/src/features/admin/users/projects/UsersDetailPage.tsx index 5a0dd9b5e6..a9785a8da2 100644 --- a/app/src/features/admin/users/UsersDetailPage.tsx +++ b/app/src/features/admin/users/projects/UsersDetailPage.tsx @@ -1,9 +1,9 @@ import CircularProgress from '@mui/material/CircularProgress'; import Container from '@mui/material/Container'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import { ISystemUser } from 'interfaces/useUserApi.interface'; import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router'; -import { useBiohubApi } from '../../../hooks/useBioHubApi'; -import { ISystemUser } from '../../../interfaces/useUserApi.interface'; import UsersDetailHeader from './UsersDetailHeader'; import UsersDetailProjects from './UsersDetailProjects'; diff --git a/app/src/features/admin/users/UsersDetailProjects.test.tsx b/app/src/features/admin/users/projects/UsersDetailProjects.test.tsx similarity index 98% rename from app/src/features/admin/users/UsersDetailProjects.test.tsx rename to app/src/features/admin/users/projects/UsersDetailProjects.test.tsx index 4759fcb244..ebac9cf029 100644 --- a/app/src/features/admin/users/UsersDetailProjects.test.tsx +++ b/app/src/features/admin/users/projects/UsersDetailProjects.test.tsx @@ -1,16 +1,16 @@ import { DialogContextProvider } from 'contexts/dialogContext'; import { createMemoryHistory } from 'history'; +import { useBiohubApi } from 'hooks/useBioHubApi'; import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; +import { IGetUserProjectsListResponse } from 'interfaces/useProjectApi.interface'; import { ISystemUser } from 'interfaces/useUserApi.interface'; import { Router } from 'react-router'; import { cleanup, fireEvent, render, waitFor } from 'test-helpers/test-utils'; -import { useBiohubApi } from '../../../hooks/useBioHubApi'; -import { IGetUserProjectsListResponse } from '../../../interfaces/useProjectApi.interface'; import UsersDetailProjects from './UsersDetailProjects'; const history = createMemoryHistory(); -jest.mock('../../../hooks/useBioHubApi'); +jest.mock('../../../../hooks/useBioHubApi'); const mockBiohubApi = useBiohubApi as jest.Mock; diff --git a/app/src/features/admin/users/UsersDetailProjects.tsx b/app/src/features/admin/users/projects/UsersDetailProjects.tsx similarity index 93% rename from app/src/features/admin/users/UsersDetailProjects.tsx rename to app/src/features/admin/users/projects/UsersDetailProjects.tsx index 7000e425e0..47e7827515 100644 --- a/app/src/features/admin/users/UsersDetailProjects.tsx +++ b/app/src/features/admin/users/projects/UsersDetailProjects.tsx @@ -13,19 +13,19 @@ import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; +import { IErrorDialogProps } from 'components/dialog/ErrorDialog'; +import { IYesNoDialogProps } from 'components/dialog/YesNoDialog'; +import { CustomMenuButton } from 'components/toolbar/ActionToolbars'; +import { ProjectParticipantsI18N, SystemUserI18N } from 'constants/i18n'; +import { DialogContext } from 'contexts/dialogContext'; +import { APIError } from 'hooks/api/useAxios'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import { CodeSet, IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; +import { IGetUserProjectsListResponse } from 'interfaces/useProjectApi.interface'; +import { ISystemUser } from 'interfaces/useUserApi.interface'; import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { useParams } from 'react-router'; import { Link as RouterLink } from 'react-router-dom'; -import { IErrorDialogProps } from '../../../components/dialog/ErrorDialog'; -import { IYesNoDialogProps } from '../../../components/dialog/YesNoDialog'; -import { CustomMenuButton } from '../../../components/toolbar/ActionToolbars'; -import { ProjectParticipantsI18N, SystemUserI18N } from '../../../constants/i18n'; -import { DialogContext } from '../../../contexts/dialogContext'; -import { APIError } from '../../../hooks/api/useAxios'; -import { useBiohubApi } from '../../../hooks/useBioHubApi'; -import { CodeSet, IGetAllCodeSetsResponse } from '../../../interfaces/useCodesApi.interface'; -import { IGetUserProjectsListResponse } from '../../../interfaces/useProjectApi.interface'; -import { ISystemUser } from '../../../interfaces/useUserApi.interface'; export interface IProjectDetailsProps { userDetails: ISystemUser; diff --git a/app/src/interfaces/useAdminApi.interface.ts b/app/src/interfaces/useAdminApi.interface.ts index 2f2140928d..41bc926d27 100644 --- a/app/src/interfaces/useAdminApi.interface.ts +++ b/app/src/interfaces/useAdminApi.interface.ts @@ -29,6 +29,8 @@ export interface IGetAccessRequestsListResponse { description: string; notes: string; create_date: string; + update_date: string | null; + updated_by: string | null; data: IAccessRequestDataObject; } diff --git a/app/src/themes/appTheme.ts b/app/src/themes/appTheme.ts index 1afe273e4c..cd82c81de1 100644 --- a/app/src/themes/appTheme.ts +++ b/app/src/themes/appTheme.ts @@ -210,7 +210,8 @@ const appTheme = createTheme({ MuiDialogTitle: { styleOverrides: { root: { - paddingTop: '24px' + paddingTop: '24px', + paddingBottom: '8px' } } }, diff --git a/database/src/seeds/03_basic_project_survey_setup.ts b/database/src/seeds/03_basic_project_survey_setup.ts index 84f8dff30b..1ec5a7b19a 100644 --- a/database/src/seeds/03_basic_project_survey_setup.ts +++ b/database/src/seeds/03_basic_project_survey_setup.ts @@ -21,6 +21,8 @@ const focalTaxonIdOptions = [ const surveyRegionsA = ['Kootenay-Boundary Natural Resource Region', 'West Coast Natural Resource Region']; const surveyRegionsB = ['Cariboo Natural Resource Region', 'South Coast Natural Resource Region']; +const identitySources = ['IDIR', 'BCEIDBUSINESS', 'BCEIDBASIC']; + /** * Add spatial transform * @@ -44,6 +46,11 @@ export async function seed(knex: Knex): Promise { `); } + // Insert access requests + for (let i = 0; i < 8; i++) { + await knex.raw(`${insertAccessRequest()}`); + } + // Check if at least 1 project already exists const checkProjectsResponse = await knex.raw(checkAnyProjectExists()); @@ -729,3 +736,37 @@ const insertSurveyRegionData = (surveyId: string, region: string) => ` WHERE region_name = $$${region}$$; `; + +/** + * SQL to insert system access requests + * + */ +const insertAccessRequest = () => ` + INSERT INTO administrative_activity + ( + administrative_activity_status_type_id, + administrative_activity_type_id, + reported_system_user_id, + assigned_system_user_id, + description, + data, + notes + ) + VALUES ( + (SELECT administrative_activity_status_type_id FROM administrative_activity_status_type ORDER BY random() LIMIT 1), + (SELECT administrative_activity_type_id FROM administrative_activity_type WHERE name = 'System Access'), + (SELECT system_user_id FROM system_user ORDER BY random() LIMIT 1), + (SELECT system_user_id FROM system_user ORDER BY random() LIMIT 1), + $$${faker.lorem.sentences(2)}$$, + jsonb_build_object( + 'reason', '${faker.lorem.sentences(1)}', + 'userGuid', '${faker.string.uuid()}', + 'name', '${faker.lorem.words(2)}', + 'username', '${faker.lorem.words(1)}', + 'email', 'default', + 'identitySource', '${identitySources[faker.number.int({ min: 0, max: identitySources.length - 1 })]}', + 'displayName', '${faker.lorem.words(1)}' + ), + $$${faker.lorem.sentences(2)}$$ + ); + `;