Skip to content
Draft
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d08025a
Adding audit trail to dropdown under Welcome, user! under "manage acc…
Jun 10, 2025
4ae5c54
New audit trails page made
Jun 11, 2025
8d47648
Returning JSON for most recent session with username as input woring
Jun 16, 2025
6857f50
Working UI for recent session, good starting point
Jul 2, 2025
8c3e670
Added pop-up modal functionality for data column (now named Details),…
Jul 2, 2025
e6458ca
Merge remote-tracking branch 'origin/main' into task/WP-930
Jul 9, 2025
530138a
Added antd modal feature, data at bottom of modal, another good start…
Jul 9, 2025
8077e97
Merge main into task/WP-930
Jul 9, 2025
2287234
Initial push
Jul 15, 2025
e06ce9e
Removed comments
Jul 15, 2025
31f2134
Delete unnecessary add-ons, linting error fixes
Jul 15, 2025
4d1876b
Added extra lines to files, added hooks, minor changes to audittrail.…
Jul 22, 2025
7ddcc49
Update designsafe/apps/accounts/templates/designsafe/apps/accounts/ba…
erikriv16 Jul 22, 2025
12ce212
Merge branch 'main' into task/WP-930
fnets Jul 28, 2025
76f98e3
Fixed react/server side linting errors
Jul 28, 2025
fd31273
Added seperate file for table and added unit tests
Aug 6, 2025
5cb1a85
Good saving point, file portal search somewhat working, good exmaples…
Aug 13, 2025
3779d92
Another good saving point, going to try to rework search, have submit…
Aug 18, 2025
8776e11
Added functioning file search feature
Sep 8, 2025
2cf8247
Removed tests.py, fixed linting errors
Sep 9, 2025
0459011
Fixed linting errors
Sep 9, 2025
d497c04
last lint check
Sep 9, 2025
915d9af
Merge branch 'main' into task/WP-930
rstijerina Sep 11, 2025
c955edb
Merge branch 'main' into task/WP-930
fnets Sep 12, 2025
ee165cf
Added host to middle column dropdown
Nov 5, 2025
1268a9e
New API Response for frontend, stil need modifications to frontend
Nov 17, 2025
2437698
Refactor of UI for updated API return, added utils folder, WORKING
Nov 17, 2025
2c9b4de
Username filter on file search added
Nov 20, 2025
b3ad9bf
Submit Job action added
Nov 26, 2025
11d9812
File Search w/ Tapis Events
Dec 23, 2025
4aa4672
Merge branch 'main' into task/WP-930
erikriv16 Dec 23, 2025
73391b2
Format Checks
Dec 23, 2025
416ca52
Update main.tsx after rebase
Dec 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
357 changes: 357 additions & 0 deletions client/src/audit/AuditTrail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,357 @@
import React, { useEffect, useState, useRef } from 'react';
import styles from './AuditTrails.module.css';
import { Modal } from 'antd';

type PortalAuditApiResponse = {
data: PortalAuditEntry[];
};

type PortalAuditEntry = {
session_id: string;
timestamp: string;
portal: string;
username: string;
action: string;
tracking_id: string;
data: any;

Check failure on line 16 in client/src/audit/AuditTrail.tsx

View workflow job for this annotation

GitHub Actions / React_NX_linting

Unexpected any. Specify a different type
};

/* Not being used */
type TapisFilesAuditApiResponse = {

Check failure on line 20 in client/src/audit/AuditTrail.tsx

View workflow job for this annotation

GitHub Actions / React_NX_linting

'TapisFilesAuditApiResponse' is defined but never used
data: TapisFilesAuditEntry[];
};

/* Will change depending on requirements for tapis file audit UI / not being used */
type TapisFilesAuditEntry = {
writer_logtime: string;
action: string;
jwt_tenant: string;
jwt_user: string;
target_system_id: string;
target_path: string;
source_path: string;
tracking_id: string;
parent_tracking_id: string;
data: string;
};

const AuditTrail: React.FC = () => {
const [username, setUsername] = useState('');
const [source, setSource] = useState('portal');
const [data, setData] = useState<PortalAuditApiResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [allUsernames, setAllUsernames] = useState<string[]>([]);
const [filteredUsernames, setFilteredUsernames] = useState<string[]>([]);
const [showDropdown, setShowDropdown] = useState(false);
const containerRef = useRef<HTMLDivElement>(null); //dropdown closing on exit click
const [modalOpen, setModalOpen] = useState(false);
const [modalContent, setModalContent] = useState<string>('');
const [footerEntry, setFooterEntry] = useState<PortalAuditEntry | null>(null);

useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
setShowDropdown(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);

useEffect(() => {
fetch('/audit/api/usernames/portal')
.then((res) => res.json())
.then((data) => setAllUsernames(data.usernames || []));
}, []);

useEffect(() => {
if (username.length > 0) {
setFilteredUsernames(
allUsernames
.filter((name) => name.toLowerCase().includes(username.toLowerCase()))
.slice(0, 20)
);
if (
allUsernames.some(
(name) => name.toLowerCase() === username.toLowerCase()
)
) {
setShowDropdown(false);
}
} else {
setFilteredUsernames([]);
setShowDropdown(false);
}
}, [username]);

Check warning on line 91 in client/src/audit/AuditTrail.tsx

View workflow job for this annotation

GitHub Actions / React_NX_linting

React Hook useEffect has a missing dependency: 'allUsernames'. Either include it or remove the dependency array. You can also replace multiple useState variables with useReducer if 'setFilteredUsernames' needs the current value of 'allUsernames'

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setData(null);
setLoading(true);
try {
const endpoint = source === 'portal' ? 'portal' : 'tapis';
const res = await fetch(`/audit/api/user/${username}/${endpoint}/`);
console.log('username:', username);
console.log('source:', endpoint);

if (!res.ok) {
const errText = await res.text();
throw new Error(`API error: ${res.status} ${errText}`);
}
const result = await res.json();
console.log('APO RESPOSNE:', result);
setData(result);
} catch (err: any) {

Check failure on line 111 in client/src/audit/AuditTrail.tsx

View workflow job for this annotation

GitHub Actions / React_NX_linting

Unexpected any. Specify a different type
setError(err.message || 'Unknown error');
}
setLoading(false);
console.log(data);
};

function truncate(str: string, n: number) {
return str.length > n ? str.slice(0, n) + '…' : str;
}

const extractActionData = (entry: PortalAuditEntry): string => {
if (!entry.data) return '-';

try {
const action = entry.action?.toLowerCase();
const parsedData =
typeof entry.data == 'string' ? JSON.parse(entry.data) : entry.data;
switch (action) {
case 'submitjob':
return extractDataField(parsedData, 'body.job.name') || '-';

case 'getapp':
return extractDataField(parsedData, 'query.appId') || '-';

case 'trash':
return extractDataField(parsedData, 'path') || '-';

case 'upload':
return extractDataField(parsedData, 'path') || '-';

case 'download':
return extractDataField(parsedData, 'filePath') || '-';
}
} catch {
return '-';
}
return '-';
};

const extractDataField = (data: any, path: string): string => {

Check failure on line 151 in client/src/audit/AuditTrail.tsx

View workflow job for this annotation

GitHub Actions / React_NX_linting

Unexpected any. Specify a different type
if (!data) return '-';
const fields = path.split('.');
let value = data;
for (let i = 0; i < fields.length; i++) {
if (value && typeof value === 'object' && fields[i] in value) {
value = value[fields[i]];
} else {
return '-';
}
}
if (value === undefined || value == null || value === '') {
return '-';
}
return String(value);
};

return (
<div>
<Modal
title="Details"
open={modalOpen}
onCancel={() => setModalOpen(false)}
footer={
footerEntry && (
<div
style={{
marginTop: '-30px',
marginBottom: '10px',
textAlign: 'center',
}}
>
{footerEntry.username} | {footerEntry.timestamp} |{' '}
{footerEntry.portal} | {footerEntry.action}
</div>
)
}
width={550}
style={{
maxHeight: '70vh',
overflow: 'auto',
top: '200px',
}}
>
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
{modalContent}
</pre>
</Modal>
{/*<h2>Audit Trail Test</h2>*/}
<form onSubmit={handleSubmit} style={{ marginBottom: 16 }}>
<div style={{ display: 'inline-flex', alignItems: 'center' }}>
<select
value={source}
onChange={(e) => setSource(e.target.value)}
style={{ marginRight: 8 }}
>
<option value="portal">Most Recent User Session Data</option>
<option value="tapis">File Search Data</option>
</select>
<div
ref={containerRef}
style={{ position: 'relative', display: 'inline-block' }}
>
<input
value={username}
onChange={(e) => {
setUsername(e.target.value);
setShowDropdown(source === 'portal');
}}
onFocus={() => {
setShowDropdown(source === 'portal');
}}
placeholder="Username/File Name:"
style={{ marginRight: 8, width: '100%' }}
/>
{showDropdown &&
source === 'portal' &&
filteredUsernames.length > 0 && (
<ul className={styles.dropdownList}>
{filteredUsernames.map((name) => (
<li
key={name}
onClick={() => {
setUsername(name);
setShowDropdown(false);
}}
style={{
padding: '8px',
cursor: 'pointer',
borderBottom: '1px solid',
}}
>
{name}
</li>
))}
</ul>
)}
</div>
<button
type="submit"
disabled={loading || !username}
style={{ marginLeft: '8px' }}
>
{loading ? 'Loading…' : 'Submit'}
</button>
</div>
</form>

{error && <div style={{ color: 'red' }}>Error: {error}</div>}

{data?.data && data.data.length === 0 && (
<div>No audit records found.</div>
)}

{data?.data && data.data.length > 0 && (
<table
style={{
width: '100%',
borderCollapse: 'collapse',
tableLayout: 'fixed',
}}
>
<thead>
<tr>
<th className={styles.headerCell} style={{ width: '50px' }}>
User
</th>
<th className={styles.headerCell} style={{ width: '50px' }}>
Date
</th>
<th className={styles.headerCell} style={{ width: '50px' }}>
Time
</th>
<th className={styles.headerCell} style={{ width: '100px' }}>
Portal
</th>
<th className={styles.headerCell} style={{ width: '200px' }}>
Action
</th>
<th className={styles.headerCell} style={{ width: '200px' }}>
Tracking ID
</th>
<th className={styles.headerCell} style={{ width: '100px' }}>
Details
</th>
</tr>
</thead>
<tbody>
{data.data.map((entry, idx) => {
let dateStr = '-';
let timeStr = '-';
if (entry.timestamp) {
const date = new Date(entry.timestamp);
dateStr = date.toLocaleDateString();
timeStr = date.toLocaleTimeString();
}
const actionDetails = extractActionData(entry);

return (
<tr key={idx}>
<td className={styles.cell}>{entry.username || '-'}</td>
<td className={styles.cell}>{dateStr}</td>
<td className={styles.cell}>{timeStr}</td>
<td className={styles.cell}>{entry.portal || '-'}</td>
<td className={styles.cell}>
{entry.action || '-'}
{actionDetails !== '-' &&
`: ${truncate(actionDetails, 50)}`}
</td>
<td className={styles.cell}>{entry.tracking_id || '-'}</td>
<td
className={styles.cell}
style={{
wordBreak: 'break-all',
cursor: 'pointer',
textDecoration: 'underline',
}}
onClick={() => {
let content = '';
if (entry.data) {
try {
const obj =
typeof entry.data === 'string'
? JSON.parse(entry.data)
: entry.data;
content = JSON.stringify(obj, null, 2);
} catch {
content = entry.data;
}
}
setModalContent(content);
setFooterEntry(entry);
setModalOpen(true);
}}
>
View Logs
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
);
};
export default AuditTrail;
24 changes: 24 additions & 0 deletions client/src/audit/AuditTrails.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.dropdownList {
position: absolute;
background: white;
border: 1px solid #ccc;
width: 100%;
max-height: 185px;
overflow-y: auto;
padding: 0;
list-style: none;
z-index: 10;
}

.headerCell {
border: 1px solid var(--global-color-primary--dark);
padding: 10px;
text-align: center;
background: var(--global-color-primary--normal);
}

.cell {
border: 1px solid #ccc;
padding: 8px;
overflow: hidden;
}
Loading
Loading