feat: implement reusable DataTable component and add AuditLogs pages for superadmin and tenant views

This commit is contained in:
sibarchannayak 2026-05-29 15:40:12 +05:30
parent 9415e9e9bf
commit 277600edf0
3 changed files with 205 additions and 99 deletions

View File

@ -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>

View File

@ -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 */}

View File

@ -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>