Recent activity card
A compact dashboard widget listing the org's most recent audit-log entries; labels billing events when billing is present.
A compact "Recent activity" dashboard widget over the org's audit log — a smaller sibling of the full audit-log viewer. It shows the latest few entries; billing-related events are labelled when the billing feature is present (the label logic strips out at install time in a billing-less app). The list query is admin-gated server-side, so a plain member sees the error state.
Requires: audit-log
Preview
"use client";import { Card, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card";import { trpc } from "@/lib/trpc";import { isBillingEvent } from "./is-billing-event";// The list query's inferred output type is very deep; declaring the row shape// inline and casting `query.data` avoids TS2589 ("type instantiation excessively// deep") — the same trick the full AuditLogViewer uses.type AuditEntry = { id: string; action: string; createdAt: string | Date };type AuditPage = { items: AuditEntry[]; nextCursor: string | null };/** How many recent entries the compact card shows. */const RECENT_LIMIT = 8;/** * A compact "Recent activity" dashboard widget over the org's audit log — a * smaller sibling of the full AuditLogViewer. Shows the latest few entries; * billing-related events are labelled when the billing feature is present (the * label logic strips out at install time in a billing-less app). The list query * is admin-gated server-side, so a plain member sees the error state. */export function ActivityCard() { const query = trpc.auditLog.list.useQuery({ limit: RECENT_LIMIT }); const page = query.data as unknown as AuditPage | undefined; const entries = page?.items ?? []; return ( <Card> <CardHeader> <CardTitle>Recent activity</CardTitle> <CardDescription> The latest changes in your organization. </CardDescription> </CardHeader> <CardContent className="flex flex-col gap-2 text-sm"> {query.isPending && ( <p className="text-muted-foreground">Loading activity…</p> )} {query.isError && ( <p className="text-destructive"> Could not load activity: {query.error.message} </p> )} {!query.isPending && !query.isError && entries.length === 0 && ( <p className="text-muted-foreground">No activity yet.</p> )} {entries.map((entry) => ( <div key={entry.id} className="flex items-center justify-between gap-3" > <span className="truncate"> {entry.action} {isBillingEvent(entry.action) && ( <span className="ml-2 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground"> billing </span> )} </span> <time className="shrink-0 text-muted-foreground"> {new Date(entry.createdAt).toLocaleDateString()} </time> </div> ))} </CardContent> </Card> );}Offline preview
The docs site has no backend, so the preview seeds a few representative entries
(including a billing one, to show the label). In an app the data comes from
the auditLog router.
Install
npx create-saas-starter add audit-log-cardRun it from your app's root. add only writes new files — wiring is up to you:
import { ActivityCard } from "@/components/blocks/audit-log-card/activity-card";