perf: optimize Activity Log — lazy diff, 30-day default, getById
- List query: exclude changes JSONB from select (only metadata) - Default to last 30 days when no date filter (avoids full table scan) - New getById query: fetches full changes JSONB on demand - ExpandedDiff component: fetches diff only when user expands an entry - 5-minute staleTime on expanded diffs (cacheable, rarely changes) Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -122,6 +122,28 @@ function DiffView({ changes }: { changes: Changes }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ExpandedDiff({ entryId }: { entryId: string }) {
|
||||||
|
const { data, isLoading } = trpc.auditLog.getById.useQuery(
|
||||||
|
{ id: entryId },
|
||||||
|
{ staleTime: 300_000 },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="border-t border-gray-100 px-4 py-3 dark:border-slate-700">
|
||||||
|
<div className="h-4 w-48 shimmer-skeleton rounded" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const changes = parseChanges((data as any)?.changes);
|
||||||
|
return (
|
||||||
|
<div className="border-t border-gray-100 px-4 py-3 dark:border-slate-700">
|
||||||
|
<DiffView changes={changes} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SummaryCards({ summary }: { summary: { byEntityType: Record<string, number>; total: number } }) {
|
function SummaryCards({ summary }: { summary: { byEntityType: Record<string, number>; total: number } }) {
|
||||||
const sorted = useMemo(() => {
|
const sorted = useMemo(() => {
|
||||||
return Object.entries(summary.byEntityType)
|
return Object.entries(summary.byEntityType)
|
||||||
@@ -355,7 +377,6 @@ export function ActivityLogClient() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{allEntries.map((entry) => {
|
{allEntries.map((entry) => {
|
||||||
const changes = parseChanges(entry.changes);
|
|
||||||
const isExpanded = expandedId === entry.id;
|
const isExpanded = expandedId === entry.id;
|
||||||
const entityLink = ENTITY_LINKS[entry.entityType]?.(entry.entityId);
|
const entityLink = ENTITY_LINKS[entry.entityType]?.(entry.entityId);
|
||||||
|
|
||||||
@@ -431,12 +452,8 @@ export function ActivityLogClient() {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Expanded Diff */}
|
{/* Expanded Diff — fetched on demand */}
|
||||||
{isExpanded && (
|
{isExpanded && <ExpandedDiff entryId={entry.id} />}
|
||||||
<div className="border-t border-gray-100 px-4 py-3 dark:border-slate-700">
|
|
||||||
<DiffView changes={changes} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -49,10 +49,27 @@ export const auditLogRouter = createTRPCRouter({
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default to last 30 days if no date filter to avoid full table scan
|
||||||
|
if (!startDate && !endDate && !entityId) {
|
||||||
|
const thirtyDaysAgo = new Date();
|
||||||
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||||
|
where.createdAt = { ...(where.createdAt as Record<string, Date> ?? {}), gte: thirtyDaysAgo };
|
||||||
|
}
|
||||||
|
|
||||||
const items = await ctx.db.auditLog.findMany({
|
const items = await ctx.db.auditLog.findMany({
|
||||||
where,
|
where,
|
||||||
include: {
|
select: {
|
||||||
|
id: true,
|
||||||
|
entityType: true,
|
||||||
|
entityId: true,
|
||||||
|
entityName: true,
|
||||||
|
action: true,
|
||||||
|
userId: true,
|
||||||
|
source: true,
|
||||||
|
summary: true,
|
||||||
|
createdAt: true,
|
||||||
user: { select: { id: true, name: true, email: true } },
|
user: { select: { id: true, name: true, email: true } },
|
||||||
|
// Exclude 'changes' from list query — fetch on demand when expanding
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
take: limit + 1,
|
take: limit + 1,
|
||||||
@@ -68,6 +85,18 @@ export const auditLogRouter = createTRPCRouter({
|
|||||||
return { items, nextCursor };
|
return { items, nextCursor };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single audit entry with full changes JSONB (for expand/detail view).
|
||||||
|
*/
|
||||||
|
getById: controllerProcedure
|
||||||
|
.input(z.object({ id: z.string() }))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
return ctx.db.auditLog.findUniqueOrThrow({
|
||||||
|
where: { id: input.id },
|
||||||
|
include: { user: { select: { id: true, name: true, email: true } } },
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all audit entries for a specific entity (e.g. a project or resource).
|
* Get all audit entries for a specific entity (e.g. a project or resource).
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user