diff --git a/application/admin-client/cypress.config.ts b/application/admin-client/cypress.config.ts
index 9394a07f..a2e7d54b 100644
--- a/application/admin-client/cypress.config.ts
+++ b/application/admin-client/cypress.config.ts
@@ -8,6 +8,7 @@ import {
wipeDB,
calculateHash,
readCommonFile,
+ seedAuditLogs,
} from 'common/testing/TestHelpers'
import { defineConfig } from 'cypress'
@@ -49,6 +50,10 @@ export default defineConfig({
readCommonFile(fileName: string) {
return readCommonFile(fileName)
},
+ async seedAuditLogs(count: number) {
+ await seedAuditLogs(count)
+ return null
+ },
})
},
},
diff --git a/application/admin-client/cypress/e2e/auditLogs.cy.js b/application/admin-client/cypress/e2e/auditLogs.cy.js
new file mode 100644
index 00000000..e580dcba
--- /dev/null
+++ b/application/admin-client/cypress/e2e/auditLogs.cy.js
@@ -0,0 +1,45 @@
+const { UserType } = require('../../../common/cypress/support/commands')
+
+before(() => {
+ cy.task('reset')
+ cy.task('seedAuditLogs', 55)
+})
+
+// Note: I've decided against testing the sorting and pagination as this comes from the library
+
+describe('Audit Logs', () => {
+ it('Organisation Admin can view Audit Log', () => {
+ cy.login(UserType.ORG_ADMIN)
+ cy.visit('/audit-logs')
+ cy.contains('Audit Logs').should('exist')
+ })
+
+ it('Study Amin can view Audit Log', () => {
+ cy.login(UserType.STUDY_ADMIN)
+ cy.visit('/audit-logs')
+ cy.contains('Audit Logs').should('exist')
+ })
+
+ it('should toggle the View Payload cell and display JSON', () => {
+ cy.login(UserType.STUDY_ADMIN)
+ cy.visit('/audit-logs')
+
+ // Toggle button to view payload
+ cy.get('[data-cy="toggle-payload-view"]').first().should('contain.text', 'View Payload').click()
+
+ // Assert text changes
+ cy.get('[data-cy="toggle-payload-view"]').first().should('contain.text', 'Hide Payload')
+
+ // Assert JSON appears
+ // Note: this works because the most recent action is the Study Admin
+ // logging in at the start of the test :)
+ cy.get('.MuiCollapse-root').should('be.visible').and('contain.text', UserType.STUDY_ADMIN)
+
+ // Toggle button to hide payload
+ cy.get('[data-cy="toggle-payload-view"]').first().should('contain.text', 'Hide Payload').click()
+
+ cy.get('[data-cy="toggle-payload-view"]').first().should('contain.text', 'View Payload')
+
+ cy.get('[data-cy="payload-viewer"]').should('not.exist')
+ })
+})
diff --git a/application/admin-client/src/App.tsx b/application/admin-client/src/App.tsx
index 4e1fd7d1..bc5bb89e 100644
--- a/application/admin-client/src/App.tsx
+++ b/application/admin-client/src/App.tsx
@@ -30,6 +30,7 @@ import { authProvider } from './providers/authProvider'
import { ParticipantList, ParticipantShow } from './pages/participants'
import { SurveyImport, IntegrationsHome, ParticipantImport } from './pages/integrations'
import { ResponsesView } from './pages/responses'
+import { AuditLogList } from './pages/audit-logs'
import {
ListAlt,
Person,
@@ -39,6 +40,7 @@ import {
AdminPanelSettings,
RestoreFromTrash,
LibraryBooks,
+ History,
} from '@mui/icons-material'
import { ParticipantEdit } from './pages/participants/edit'
import { SetupPage } from './pages/setup'
@@ -158,6 +160,16 @@ function App() {
list: '/studies',
meta: {
icon: ,
+ label: 'Manage Studies',
+ parent: 'admin',
+ },
+ },
+ {
+ name: 'audit-logs',
+ list: '/audit-logs',
+ meta: {
+ icon: ,
+ label: 'Audit Logs',
parent: 'admin',
},
},
@@ -245,6 +257,7 @@ function App() {
} />
} />
} />
+ } />
} />
diff --git a/application/admin-client/src/components/RedcapLogo.tsx b/application/admin-client/src/components/RedcapLogo.tsx
index f479eddf..913e4f9b 100644
--- a/application/admin-client/src/components/RedcapLogo.tsx
+++ b/application/admin-client/src/components/RedcapLogo.tsx
@@ -1,13 +1,12 @@
import { ColorModeContext } from '../contexts/color-mode'
import { useContext } from 'react'
-
export const RedcapLogo = () => {
- const { mode } = useContext(ColorModeContext)
+ const { mode } = useContext(ColorModeContext)
- return mode === 'dark' ? (
-
- ) : (
-
- )
-}
\ No newline at end of file
+ return mode === 'dark' ? (
+
+ ) : (
+
+ )
+}
diff --git a/application/admin-client/src/pages/audit-logs/index.ts b/application/admin-client/src/pages/audit-logs/index.ts
new file mode 100644
index 00000000..c1edf1e2
--- /dev/null
+++ b/application/admin-client/src/pages/audit-logs/index.ts
@@ -0,0 +1 @@
+export * from './list'
diff --git a/application/admin-client/src/pages/audit-logs/list.tsx b/application/admin-client/src/pages/audit-logs/list.tsx
new file mode 100644
index 00000000..c3b8bc2f
--- /dev/null
+++ b/application/admin-client/src/pages/audit-logs/list.tsx
@@ -0,0 +1,173 @@
+import React, { useState } from 'react'
+import { defaultAuditLogsPageSize } from '@common/src/config'
+import { Box, Button, Chip, Collapse, Typography } from '@mui/material'
+import { DataGrid, type GridColDef } from '@mui/x-data-grid'
+import { DateField, List, useDataGrid } from '@refinedev/mui'
+import { AUDIT_LOG_SORTABLE_FIELDS } from '@common/types/api/audit-logs/getAuditLogs'
+
+const ExpandableJsonCell = ({ value }: { value: any }) => {
+ const [expanded, setExpanded] = useState(false)
+
+ if (!value || Object.keys(value).length === 0) {
+ return -
+ }
+
+ return (
+
+
+
+
+
+ {JSON.stringify(value, null, 2)}
+
+
+
+ )
+}
+
+export const AuditLogList = () => {
+ const { dataGridProps } = useDataGrid({
+ syncWithLocation: false,
+ // Note: By default Refined uses 'server' mode for pagination, sorting and filtering
+ // pagination: { mode: 'off' },
+ // sorters: { mode: 'off' },
+ filters: { mode: 'off' },
+ resource: 'audit-logs',
+ sorters: {
+ initial: [{ field: 'timestamp', order: 'desc' }],
+ },
+ })
+
+ const columns = React.useMemo(() => {
+ const baseColumns: GridColDef[] = [
+ {
+ field: 'id',
+ headerName: 'ID',
+ width: 10,
+ },
+ {
+ field: 'resource',
+ flex: 1,
+ headerName: 'Resource',
+ minWidth: 150,
+ },
+ {
+ field: 'operation',
+ headerName: 'Operation',
+ width: 90,
+ },
+ {
+ field: 'success',
+ headerName: 'Success',
+ width: 80,
+ },
+ {
+ field: 'timestamp',
+ headerName: 'Timestamp',
+ width: 200,
+ type: 'date',
+ valueGetter: (value) => {
+ if (!value) return null
+ return new Date(value)
+ },
+ renderCell: function render({ value }) {
+ // ISO format for good sorting properties :)
+ return
+ },
+ },
+ {
+ field: 'userId',
+ headerName: 'userId',
+ width: 70,
+ },
+ {
+ field: 'meta',
+ headerName: 'Request Details',
+ flex: 1,
+ minWidth: 350,
+ renderCell: ({ value }) => {
+ if (!value || !value.method) return '-'
+
+ return (
+
+
+
+ {value.url}
+
+
+ )
+ },
+ },
+ {
+ field: 'requestBody',
+ headerName: 'RequestBody',
+ flex: 2,
+ minWidth: 300,
+ renderCell: ({ value }) => ,
+ },
+ ]
+ return baseColumns.map((col) => ({
+ ...col,
+ sortable: col.sortable ?? AUDIT_LOG_SORTABLE_FIELDS.includes(col.field as any),
+ }))
+ }, [])
+ return (
+
+
+ 'auto'}
+ getEstimatedRowHeight={() => 52}
+ pageSizeOptions={[10, defaultAuditLogsPageSize, 50]}
+ initialState={{
+ pagination: { paginationModel: { pageSize: defaultAuditLogsPageSize } },
+ }}
+ slotProps={{ root: { 'data-cy': 'audit-logs-list' } }}
+ disableColumnMenu
+ sx={{
+ '& .MuiDataGrid-columnHeaderTitleContainer': {
+ justifyContent: 'center',
+ },
+ '& .MuiDataGrid-cell': {
+ display: 'flex',
+ alignItems: 'center',
+ },
+ }}
+ />
+
+
+ )
+}
diff --git a/application/admin-client/src/pages/restore/index.tsx b/application/admin-client/src/pages/restore/index.tsx
index 4e4884c6..38f19613 100644
--- a/application/admin-client/src/pages/restore/index.tsx
+++ b/application/admin-client/src/pages/restore/index.tsx
@@ -229,7 +229,7 @@ const RestorePage = () => {
>
)}
-
+
)
}
diff --git a/application/admin-client/src/pages/settings/index.tsx b/application/admin-client/src/pages/settings/index.tsx
index 5be76b58..5b34b259 100644
--- a/application/admin-client/src/pages/settings/index.tsx
+++ b/application/admin-client/src/pages/settings/index.tsx
@@ -198,7 +198,6 @@ const SettingsPage = () => {
-
)
}
diff --git a/application/admin-client/src/pages/surveys/edit.tsx b/application/admin-client/src/pages/surveys/edit.tsx
index 0d43e8c5..15b9c665 100644
--- a/application/admin-client/src/pages/surveys/edit.tsx
+++ b/application/admin-client/src/pages/surveys/edit.tsx
@@ -139,7 +139,16 @@ export const SurveyEditor = () => {
return isLoading ? null : (
-
+
- {Object.keys(errors) && { }}
+ {Object.keys(errors) && {}}
{Object.values(ContactMethod).map((val, idx) => (
-