From eae2c37388fa5e3391e4686552baf277e6748c02 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 22 Oct 2025 18:21:54 -0700 Subject: [PATCH] Add expandable columns --- .../routers/auditLogs/queryActionAuditLog.ts | 2 + .../routers/auditLogs/exportRequstAuditLog.ts | 10 +- .../routers/auditLogs/queryRequstAuditLog.ts | 106 +++++---- server/routers/auditLogs/types.ts | 11 +- src/app/[orgId]/settings/logs/access/page.tsx | 18 ++ src/app/[orgId]/settings/logs/action/page.tsx | 18 ++ .../[orgId]/settings/logs/request/page.tsx | 53 +++++ src/components/LogDataTable.tsx | 204 +++++++++++++----- 8 files changed, 316 insertions(+), 106 deletions(-) diff --git a/server/private/routers/auditLogs/queryActionAuditLog.ts b/server/private/routers/auditLogs/queryActionAuditLog.ts index 77f3bbd8..9e357e8d 100644 --- a/server/private/routers/auditLogs/queryActionAuditLog.ts +++ b/server/private/routers/auditLogs/queryActionAuditLog.ts @@ -24,6 +24,7 @@ import { fromError } from "zod-validation-error"; import { QueryActionAuditLogResponse } from "@server/routers/auditLogs/types"; import response from "@server/lib/response"; import logger from "@server/logger"; +import { metadata } from "@app/app/[orgId]/settings/layout"; export const queryActionAuditLogsQuery = z.object({ // iso string just validate its a parseable date @@ -65,6 +66,7 @@ export function queryAction(timeStart: number, timeEnd: number, orgId: string) { orgId: actionAuditLog.orgId, action: actionAuditLog.action, actorType: actionAuditLog.actorType, + metadata: actionAuditLog.metadata, actorId: actionAuditLog.actorId, timestamp: actionAuditLog.timestamp, actor: actionAuditLog.actor diff --git a/server/routers/auditLogs/exportRequstAuditLog.ts b/server/routers/auditLogs/exportRequstAuditLog.ts index 759475c2..c1fb2872 100644 --- a/server/routers/auditLogs/exportRequstAuditLog.ts +++ b/server/routers/auditLogs/exportRequstAuditLog.ts @@ -41,7 +41,6 @@ export async function exportRequestAuditLogs( ) ); } - const { timeStart, timeEnd, limit, offset } = parsedQuery.data; const parsedParams = queryRequestAuditLogsParams.safeParse(req.params); if (!parsedParams.success) { @@ -52,16 +51,17 @@ export async function exportRequestAuditLogs( ) ); } - const { orgId } = parsedParams.data; - const baseQuery = queryRequest(timeStart, timeEnd, orgId); + const data = { ...parsedQuery.data, ...parsedParams.data }; - const log = await baseQuery.limit(limit).offset(offset); + const baseQuery = queryRequest(data); + + const log = await baseQuery.limit(data.limit).offset(data.offset); const csvData = generateCSV(log); res.setHeader('Content-Type', 'text/csv'); - res.setHeader('Content-Disposition', `attachment; filename="request-audit-logs-${orgId}-${Date.now()}.csv"`); + res.setHeader('Content-Disposition', `attachment; filename="request-audit-logs-${data.orgId}-${Date.now()}.csv"`); return res.send(csvData); } catch (error) { diff --git a/server/routers/auditLogs/queryRequstAuditLog.ts b/server/routers/auditLogs/queryRequstAuditLog.ts index b5af0e14..f4e24649 100644 --- a/server/routers/auditLogs/queryRequstAuditLog.ts +++ b/server/routers/auditLogs/queryRequstAuditLog.ts @@ -28,6 +28,11 @@ export const queryAccessAuditLogsQuery = z.object({ .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .optional() .default(new Date().toISOString()), + action: z.boolean().optional(), + method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional(), + reason: z.number().optional(), + resourceId: z.number().optional(), + actor: z.string().optional(), limit: z .string() .optional() @@ -46,55 +51,64 @@ export const queryRequestAuditLogsParams = z.object({ orgId: z.string() }); -export function queryRequest(timeStart: number, timeEnd: number, orgId: string) { +export const queryRequestAuditLogsCombined = + queryAccessAuditLogsQuery.merge(queryRequestAuditLogsParams); +type Q = z.infer; + +function getWhere(data: Q) { + return and( + gt(requestAuditLog.timestamp, data.timeStart), + lt(requestAuditLog.timestamp, data.timeEnd), + eq(requestAuditLog.orgId, data.orgId), + data.resourceId + ? eq(requestAuditLog.resourceId, data.resourceId) + : undefined, + data.actor ? eq(requestAuditLog.actor, data.actor) : undefined, + data.method ? eq(requestAuditLog.method, data.method) : undefined, + data.reason ? eq(requestAuditLog.reason, data.reason) : undefined + ); +} + +export function queryRequest(data: Q) { return db .select({ - timestamp: requestAuditLog.timestamp, - orgId: requestAuditLog.orgId, - action: requestAuditLog.action, - reason: requestAuditLog.reason, - actorType: requestAuditLog.actorType, - actor: requestAuditLog.actor, - actorId: requestAuditLog.actorId, - resourceId: requestAuditLog.resourceId, - ip: requestAuditLog.ip, - location: requestAuditLog.location, - userAgent: requestAuditLog.userAgent, - metadata: requestAuditLog.metadata, - headers: requestAuditLog.headers, - query: requestAuditLog.query, - originalRequestURL: requestAuditLog.originalRequestURL, - scheme: requestAuditLog.scheme, - host: requestAuditLog.host, - path: requestAuditLog.path, - method: requestAuditLog.method, + timestamp: requestAuditLog.timestamp, + orgId: requestAuditLog.orgId, + action: requestAuditLog.action, + reason: requestAuditLog.reason, + actorType: requestAuditLog.actorType, + actor: requestAuditLog.actor, + actorId: requestAuditLog.actorId, + resourceId: requestAuditLog.resourceId, + ip: requestAuditLog.ip, + location: requestAuditLog.location, + userAgent: requestAuditLog.userAgent, + metadata: requestAuditLog.metadata, + headers: requestAuditLog.headers, + query: requestAuditLog.query, + originalRequestURL: requestAuditLog.originalRequestURL, + scheme: requestAuditLog.scheme, + host: requestAuditLog.host, + path: requestAuditLog.path, + method: requestAuditLog.method, tls: requestAuditLog.tls, resourceName: resources.name, resourceNiceId: resources.niceId }) .from(requestAuditLog) - .leftJoin(resources, eq(requestAuditLog.resourceId, resources.resourceId)) // TODO: Is this efficient? - .where( - and( - gt(requestAuditLog.timestamp, timeStart), - lt(requestAuditLog.timestamp, timeEnd), - eq(requestAuditLog.orgId, orgId) - ) - ) + .leftJoin( + resources, + eq(requestAuditLog.resourceId, resources.resourceId) + ) // TODO: Is this efficient? + .where(getWhere(data)) .orderBy(requestAuditLog.timestamp); } -export function countRequestQuery(timeStart: number, timeEnd: number, orgId: string) { - const countQuery = db - .select({ count: count() }) - .from(requestAuditLog) - .where( - and( - gt(requestAuditLog.timestamp, timeStart), - lt(requestAuditLog.timestamp, timeEnd), - eq(requestAuditLog.orgId, orgId) - ) - ); +export function countRequestQuery(data: Q) { + const countQuery = db + .select({ count: count() }) + .from(requestAuditLog) + .where(getWhere(data)); return countQuery; } @@ -125,7 +139,6 @@ export async function queryRequestAuditLogs( ) ); } - const { timeStart, timeEnd, limit, offset } = parsedQuery.data; const parsedParams = queryRequestAuditLogsParams.safeParse(req.params); if (!parsedParams.success) { @@ -136,13 +149,14 @@ export async function queryRequestAuditLogs( ) ); } - const { orgId } = parsedParams.data; - const baseQuery = queryRequest(timeStart, timeEnd, orgId); + const data = { ...parsedQuery.data, ...parsedParams.data }; - const log = await baseQuery.limit(limit).offset(offset); + const baseQuery = queryRequest(data); - const totalCountResult = await countRequestQuery(timeStart, timeEnd, orgId); + const log = await baseQuery.limit(data.limit).offset(data.offset); + + const totalCountResult = await countRequestQuery(data); const totalCount = totalCountResult[0].count; return response(res, { @@ -150,8 +164,8 @@ export async function queryRequestAuditLogs( log: log, pagination: { total: totalCount, - limit, - offset + limit: data.limit, + offset: data.offset } }, success: true, diff --git a/server/routers/auditLogs/types.ts b/server/routers/auditLogs/types.ts index 893db5ef..b35c2d9e 100644 --- a/server/routers/auditLogs/types.ts +++ b/server/routers/auditLogs/types.ts @@ -4,6 +4,7 @@ export type QueryActionAuditLogResponse = { action: string; actorType: string; actorId: string; + metadata: string | null; timestamp: number; actor: string; }[]; @@ -49,8 +50,9 @@ export type QueryRequestAuditLogResponse = { export type QueryAccessAuditLogResponse = { log: { orgId: string; - action: string; - type: string; + action: boolean; + actorType: string | null; + actorId: string | null; resourceId: number | null; resourceName: string | null; resourceNiceId: string | null; @@ -58,10 +60,9 @@ export type QueryAccessAuditLogResponse = { location: string | null; userAgent: string | null; metadata: string | null; - actorType: string; - actorId: string; + type: string; timestamp: number; - actor: string; + actor: string | null; }[]; pagination: { total: number; diff --git a/src/app/[orgId]/settings/logs/access/page.tsx b/src/app/[orgId]/settings/logs/access/page.tsx index af0bc8fc..e2b68ae0 100644 --- a/src/app/[orgId]/settings/logs/access/page.tsx +++ b/src/app/[orgId]/settings/logs/access/page.tsx @@ -340,6 +340,21 @@ export default function GeneralPage() { } ]; + const renderExpandedRow = (row: any) => { + return ( +
+
+
+ Metadata: +
+                            {row.metadata ? JSON.stringify(JSON.parse(row.metadata), null, 2) : "N/A"}
+                        
+
+
+
+ ); + }; + return ( <> ); diff --git a/src/app/[orgId]/settings/logs/action/page.tsx b/src/app/[orgId]/settings/logs/action/page.tsx index 3b693515..a332f0cb 100644 --- a/src/app/[orgId]/settings/logs/action/page.tsx +++ b/src/app/[orgId]/settings/logs/action/page.tsx @@ -269,6 +269,21 @@ export default function GeneralPage() { } ]; + const renderExpandedRow = (row: any) => { + return ( +
+
+
+ Metadata: +
+                            {row.metadata ? JSON.stringify(JSON.parse(row.metadata), null, 2) : "N/A"}
+                        
+
+
+
+ ); + }; + return ( <> ); diff --git a/src/app/[orgId]/settings/logs/request/page.tsx b/src/app/[orgId]/settings/logs/request/page.tsx index fe4bb9f2..9c7f6bd4 100644 --- a/src/app/[orgId]/settings/logs/request/page.tsx +++ b/src/app/[orgId]/settings/logs/request/page.tsx @@ -313,6 +313,7 @@ export default function GeneralPage() { return ( e.stopPropagation()} > + ); + }, + size: 40 + }; + + return [expansionColumn, ...columns]; + }, [columns, expandable, expandedRows, toggleRowExpansion]); + const table = useReactTable({ data: filteredData, - columns, + columns: enhancedColumns, getCoreRowModel: getCoreRowModel(), // Only use client-side pagination if totalCount is not provided - ...(isServerPagination ? {} : { getPaginationRowModel: getPaginationRowModel() }), + ...(isServerPagination + ? {} + : { getPaginationRowModel: getPaginationRowModel() }), onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(), onColumnFiltersChange: setColumnFilters, getFilteredRowModel: getFilteredRowModel(), onGlobalFilterChange: setGlobalFilter, // Configure pagination state - ...(isServerPagination ? { - manualPagination: true, - pageCount: totalCount ? Math.ceil(totalCount / pageSize) : 0, - } : {}), + ...(isServerPagination + ? { + manualPagination: true, + pageCount: totalCount ? Math.ceil(totalCount / pageSize) : 0 + } + : {}), initialState: { pagination: { pageSize: pageSize, @@ -321,10 +388,7 @@ export function LogDataTable({ )} {onExport && ( -