feat: implement reusable DataTable component and add AuditLogs pages for superadmin and tenant views
This commit is contained in:
parent
9415e9e9bf
commit
277600edf0
@ -228,7 +228,7 @@ export const DataTable = <T,>({
|
||||
{canExpand && expanded && (
|
||||
<tr className="border-t border-[#D1D5DB] bg-[#F9F9F9]">
|
||||
<td colSpan={desktopColSpan}>
|
||||
<div className="flex flex-col items-start w-full bg-[#FFF] border border-gray-300 rounded-md p-4 text-xs text-gray-700 m-4">
|
||||
<div className="flex flex-col items-start bg-[#FFF] border border-gray-300 rounded-md p-4 text-xs text-gray-700 m-4">
|
||||
{renderExpandedRow(item)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@ -83,6 +83,7 @@ const getStatusColor = (status: number | null): string => {
|
||||
|
||||
const AuditLogs = (): ReactElement => {
|
||||
const { primaryColor } = useAppTheme();
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
|
||||
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||
const [modules, setModules] = useState<Array<{ id: string; name: string }>>(
|
||||
@ -192,6 +193,7 @@ const AuditLogs = (): ReactElement => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setExpandedId(null);
|
||||
const response = await auditLogService.getAll(
|
||||
currentPage,
|
||||
limit,
|
||||
@ -287,6 +289,78 @@ const AuditLogs = (): ReactElement => {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const toggleExpand = (id: string): void => {
|
||||
setExpandedId((prev) => (prev === id ? null : id));
|
||||
};
|
||||
|
||||
const renderExpanded = (row: AuditLog): ReactElement => {
|
||||
return (
|
||||
<div className="w-full text-xs text-gray-700 py-2">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Column 1: Request Details */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-gray-900 border-b border-gray-100 pb-1">Request Info</h4>
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="text-[#9aa6b2]">HTTP Method:</span>{" "}
|
||||
<StatusBadge variant={getMethodVariant(row.request_method)}>
|
||||
{row.request_method || "N/A"}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[#9aa6b2]">Request Path:</span>{" "}
|
||||
<code className="bg-gray-100 px-1.5 py-0.5 rounded break-all font-mono text-[11px] text-[#0f1724]">
|
||||
{row.request_path || "N/A"}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[#9aa6b2]">IP Address:</span>{" "}
|
||||
<span className="font-mono text-[#0f1724]">{row.ip_address || "N/A"}</span>
|
||||
</div>
|
||||
<div className="text-[11px] text-gray-500">
|
||||
<span className="text-[#9aa6b2] text-xs">User Agent:</span>{" "}
|
||||
<span className="break-all">{row.user_agent || "N/A"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Column 2: Audit Identifiers */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-gray-900 border-b border-gray-100 pb-1">Audit Details</h4>
|
||||
<div>
|
||||
<span className="text-[#9aa6b2]">Log ID:</span>{" "}
|
||||
<code className="bg-gray-100 px-1.5 py-0.5 rounded font-mono text-[11px] text-[#0f1724]">
|
||||
{row.id}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[#9aa6b2]">Resource ID:</span>{" "}
|
||||
<code className="bg-gray-100 px-1.5 py-0.5 rounded font-mono text-[11px] text-[#0f1724]">
|
||||
{row.resource_id || "N/A"}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[#9aa6b2]">Correlation ID:</span>{" "}
|
||||
<code className="bg-gray-100 px-1.5 py-0.5 rounded font-mono text-[11px] text-[#0f1724]">
|
||||
{row.correlation_id || "N/A"}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Column 3: Metadata */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-gray-900 border-b border-gray-100 pb-1">Metadata</h4>
|
||||
{row.metadata && Object.keys(row.metadata).length > 0 ? (
|
||||
<pre className="bg-gray-50 border border-gray-200 rounded p-2 overflow-auto max-h-36 font-mono text-[11px] leading-4 text-gray-600">
|
||||
{JSON.stringify(row.metadata, null, 2)}
|
||||
</pre>
|
||||
) : (
|
||||
<span className="text-gray-400 italic">No additional metadata</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Define table columns
|
||||
const columns: Column<AuditLog>[] = [
|
||||
{
|
||||
@ -302,17 +376,20 @@ const AuditLogs = (): ReactElement => {
|
||||
{
|
||||
key: "tenant",
|
||||
label: "Tenant",
|
||||
render: (log) => (
|
||||
<span className="text-sm font-normal text-[#0f1724]">
|
||||
{log.tenant ? log.tenant.name : "N/A"}
|
||||
</span>
|
||||
),
|
||||
render: (log) => {
|
||||
const tenantName = log.tenant ? log.tenant.name : "N/A";
|
||||
return (
|
||||
<span className="text-sm font-normal text-[#0f1724] block max-w-[150px] truncate" title={tenantName}>
|
||||
{tenantName}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "resource_type",
|
||||
label: "Resource Type",
|
||||
render: (log) => (
|
||||
<span className="text-sm font-normal text-[#0f1724]">
|
||||
<span className="text-sm font-normal text-[#0f1724] block max-w-[150px] truncate" title={log.resource_type}>
|
||||
{log.resource_type}
|
||||
</span>
|
||||
),
|
||||
@ -338,22 +415,16 @@ const AuditLogs = (): ReactElement => {
|
||||
{
|
||||
key: "user",
|
||||
label: "User",
|
||||
render: (log) => (
|
||||
<span className="text-sm font-normal text-[#0f1724]">
|
||||
{log.user
|
||||
? `${log.user.first_name} ${log.user.last_name}`
|
||||
: log.user_email || "N/A"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "request_method",
|
||||
label: "Method",
|
||||
render: (log) => (
|
||||
<StatusBadge variant={getMethodVariant(log.request_method)}>
|
||||
{log.request_method ? log.request_method.toUpperCase() : "N/A"}
|
||||
</StatusBadge>
|
||||
),
|
||||
render: (log) => {
|
||||
const userName = log.user
|
||||
? `${log.user.first_name} ${log.user.last_name}`
|
||||
: log.user_email || "N/A";
|
||||
return (
|
||||
<span className="text-sm font-normal text-[#0f1724] block max-w-[150px] truncate" title={userName}>
|
||||
{userName}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "response_status",
|
||||
@ -677,6 +748,11 @@ const AuditLogs = (): ReactElement => {
|
||||
error={error}
|
||||
mobileCardRenderer={mobileCardRenderer}
|
||||
emptyMessage="No platform-wide audit logs found"
|
||||
expandableRows
|
||||
isRowExpanded={(row) => expandedId === row.id}
|
||||
onRowExpandToggle={(row) => toggleExpand(row.id)}
|
||||
renderExpandedRow={renderExpanded}
|
||||
onRowClick={(row) => toggleExpand(row.id)}
|
||||
/>
|
||||
|
||||
{/* Pagination */}
|
||||
|
||||
@ -300,10 +300,68 @@ const AuditLogs = ({
|
||||
|
||||
const renderExpanded = (row: AuditLog): ReactElement => {
|
||||
return (
|
||||
<div className="w-full overflow-auto font-mono leading-6">
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(row.metadata, null, 2)}
|
||||
</pre>
|
||||
<div className="w-full text-xs text-gray-700 py-2">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Column 1: Request Details */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-gray-900 border-b border-gray-100 pb-1">Request Info</h4>
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="text-[#9aa6b2]">HTTP Method:</span>{" "}
|
||||
<StatusBadge variant={getMethodVariant(row.request_method)}>
|
||||
{row.request_method || "N/A"}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[#9aa6b2]">Request Path:</span>{" "}
|
||||
<code className="bg-gray-100 px-1.5 py-0.5 rounded break-all font-mono text-[11px] text-[#0f1724]">
|
||||
{row.request_path || "N/A"}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[#9aa6b2]">IP Address:</span>{" "}
|
||||
<span className="font-mono text-[#0f1724]">{row.ip_address || "N/A"}</span>
|
||||
</div>
|
||||
<div className="text-[11px] text-gray-500">
|
||||
<span className="text-[#9aa6b2] text-xs">User Agent:</span>{" "}
|
||||
<span className="break-all">{row.user_agent || "N/A"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Column 2: Audit Identifiers */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-gray-900 border-b border-gray-100 pb-1">Audit Details</h4>
|
||||
<div>
|
||||
<span className="text-[#9aa6b2]">Log ID:</span>{" "}
|
||||
<code className="bg-gray-100 px-1.5 py-0.5 rounded font-mono text-[11px] text-[#0f1724]">
|
||||
{row.id}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[#9aa6b2]">Resource ID:</span>{" "}
|
||||
<code className="bg-gray-100 px-1.5 py-0.5 rounded font-mono text-[11px] text-[#0f1724]">
|
||||
{row.resource_id || "N/A"}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[#9aa6b2]">Correlation ID:</span>{" "}
|
||||
<code className="bg-gray-100 px-1.5 py-0.5 rounded font-mono text-[11px] text-[#0f1724]">
|
||||
{row.correlation_id || "N/A"}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Column 3: Metadata */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-gray-900 border-b border-gray-100 pb-1">Metadata</h4>
|
||||
{row.metadata && Object.keys(row.metadata).length > 0 ? (
|
||||
<pre className="bg-gray-50 border border-gray-200 rounded p-2 overflow-auto max-h-36 font-mono text-[11px] leading-4 text-gray-600">
|
||||
{JSON.stringify(row.metadata, null, 2)}
|
||||
</pre>
|
||||
) : (
|
||||
<span className="text-gray-400 italic">No additional metadata</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -324,7 +382,7 @@ const AuditLogs = ({
|
||||
key: "resource_type",
|
||||
label: "Resource Type",
|
||||
render: (log) => (
|
||||
<span className="text-sm font-normal text-[#0f1724]">
|
||||
<span className="text-sm font-normal text-[#0f1724] block max-w-[160px] truncate" title={log.resource_type}>
|
||||
{log.resource_type}
|
||||
</span>
|
||||
),
|
||||
@ -349,28 +407,22 @@ const AuditLogs = ({
|
||||
},
|
||||
...(isTenantAdmin
|
||||
? [
|
||||
{
|
||||
key: "user",
|
||||
label: "User",
|
||||
render: (log: AuditLog) => (
|
||||
<span className="text-sm font-normal text-[#0f1724]">
|
||||
{log.user
|
||||
? `${log.user.first_name} ${log.user.last_name}`
|
||||
: log.user_email || "N/A"}
|
||||
{
|
||||
key: "user",
|
||||
label: "User",
|
||||
render: (log: AuditLog) => {
|
||||
const userName = log.user
|
||||
? `${log.user.first_name} ${log.user.last_name}`
|
||||
: log.user_email || "N/A";
|
||||
return (
|
||||
<span className="text-sm font-normal text-[#0f1724] block max-w-[160px] truncate" title={userName}>
|
||||
{userName}
|
||||
</span>
|
||||
),
|
||||
);
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "request_method",
|
||||
label: "Method",
|
||||
render: (log) => (
|
||||
<StatusBadge variant={getMethodVariant(log.request_method)}>
|
||||
{log.request_method ? log.request_method.toUpperCase() : "N/A"}
|
||||
</StatusBadge>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "response_status",
|
||||
label: "Status",
|
||||
@ -382,15 +434,6 @@ const AuditLogs = ({
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "ip_address",
|
||||
label: "IP Address",
|
||||
render: (log) => (
|
||||
<span className="text-sm font-normal text-[#0f1724] font-mono">
|
||||
{log.ip_address || "N/A"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
label: "Actions",
|
||||
@ -505,19 +548,6 @@ const AuditLogs = ({
|
||||
{ value: "SUBMIT", label: "SUBMIT" },
|
||||
{ value: "APPROVE", label: "APPROVE" },
|
||||
{ value: "REJECT", label: "REJECT" },
|
||||
// {value: 'PUBLISH', label: 'PUBLISH'},
|
||||
// {value: 'ARCHIVE', label: 'ARCHIVE'},
|
||||
// {value: 'CHECKOUT', label: 'CHECKOUT'},
|
||||
// {value: 'CHECKIN', label: 'CHECKIN'},
|
||||
// {value: 'TRANSITION', label: 'TRANSITION'},
|
||||
// {value: 'CANCEL', label: 'CANCEL'},
|
||||
// {value: 'COMPLETE', label: 'COMPLETE'},
|
||||
// {value: 'ACTIVATE', label: 'ACTIVATE'},
|
||||
// {value: 'REVOKE', label: 'REVOKE'},
|
||||
// {value: 'STATUS', label: 'STATUS'},
|
||||
// {value: 'VOID', label: 'VOID'},
|
||||
// {value: 'SEND', label: 'SEND'},
|
||||
// {value: 'API_CALL', label: 'API_CALL'},
|
||||
]}
|
||||
value={actionFilter}
|
||||
onChange={(value) => {
|
||||
@ -625,23 +655,23 @@ const AuditLogs = ({
|
||||
methodFilter ||
|
||||
moduleIdFilter ||
|
||||
search) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setStartDate("");
|
||||
setEndDate("");
|
||||
setActionFilter(null);
|
||||
setResourceTypeFilter(null);
|
||||
setMethodFilter(null);
|
||||
setModuleIdFilter(null);
|
||||
setSearch("");
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="text-xs hover:underline decoration-offset-2"
|
||||
style={{ color: primaryColor }}
|
||||
>
|
||||
Clear all filters
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setStartDate("");
|
||||
setEndDate("");
|
||||
setActionFilter(null);
|
||||
setResourceTypeFilter(null);
|
||||
setMethodFilter(null);
|
||||
setModuleIdFilter(null);
|
||||
setSearch("");
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="text-xs hover:underline decoration-offset-2"
|
||||
style={{ color: primaryColor }}
|
||||
>
|
||||
Clear all filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -667,19 +697,19 @@ const AuditLogs = ({
|
||||
|
||||
{/* Pagination */}
|
||||
{pagination.total > 0 && (
|
||||
<div className="border-t border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3">
|
||||
<Pagination
|
||||
currentPage={pagination.page}
|
||||
totalPages={pagination.totalPages}
|
||||
totalItems={pagination.total}
|
||||
limit={limit}
|
||||
onPageChange={(page: number) => setCurrentPage(page)}
|
||||
onLimitChange={(newLimit: number) => {
|
||||
setLimit(newLimit);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
// <div className="border-t border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3">
|
||||
<Pagination
|
||||
currentPage={pagination.page}
|
||||
totalPages={pagination.totalPages}
|
||||
totalItems={pagination.total}
|
||||
limit={limit}
|
||||
onPageChange={(page: number) => setCurrentPage(page)}
|
||||
onLimitChange={(newLimit: number) => {
|
||||
setLimit(newLimit);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
/>
|
||||
// </div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user