studio/src/components/audit-log-table.tsx (175 lines of code) (raw):

import { docsBaseURL } from "@/lib/constants"; import { AuditLog } from "@wundergraph/cosmo-connect/dist/platform/v1/platform_pb"; import { EmptyState } from "./empty-state"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, TableWrapper, } from "./ui/table"; import { formatDateTime } from "@/lib/format-date"; import { capitalize } from "@/lib/utils"; import { pascalCase } from "change-case"; import { AiOutlineAudit } from "react-icons/ai"; import { PiKeyBold, PiRobotFill, PiUserBold } from "react-icons/pi"; import { Badge } from "./ui/badge"; export const Empty = (params: { unauthorized: boolean }) => { if (params.unauthorized) { return ( <EmptyState title="Unauthorized" description="You are not authorized to manage this organization." /> ); } return ( <EmptyState icon={<AiOutlineAudit />} title="No audit logs" description={ <div className="space-x-1"> <span>You can view activity within your organization here.</span> <a target="_blank" rel="noreferrer" href={docsBaseURL + "/studio/audit-log"} className="text-primary" > Learn more. </a> </div> } /> ); }; export const AuditLogTable = ({ logs }: { logs?: AuditLog[] }) => { return ( <TableWrapper> <Table> <TableHeader> <TableRow> <TableHead className="px-4">Actor</TableHead> <TableHead className="px-4">Action</TableHead> <TableHead className="px-4">Date</TableHead> </TableRow> </TableHeader> <TableBody> {logs?.map( ({ id, actorDisplayName, apiKeyName, actorType, auditAction, createdAt, action, auditableDisplayName, targetDisplayName, targetType, targetNamespaceDisplayName, }) => { let preParagraph = null; let postParagraph = null; if (auditAction === "organization_invitation.created") { postParagraph = "for"; } else if (auditAction === "member_role.updated") { preParagraph = "role for"; postParagraph = "to"; } else if (action === "moved") { postParagraph = `to ${targetNamespaceDisplayName} namespace,`; } else if (auditableDisplayName) { preParagraph = "in"; if (!!targetNamespaceDisplayName) { postParagraph = `in ${targetNamespaceDisplayName} namespace,`; } } let label = null; if (targetDisplayName) { label = ( <> {preParagraph && ( <span className="text-gray-500 dark:text-gray-400"> {preParagraph} </span> )} <span className="inline-block max-w-md truncate" title={pascalCase(targetType)} > <span className="text-purple whitespace-nowrap font-mono"> {targetDisplayName} </span> </span> </> ); } const actionView = ( <> <span className="text-gray-500 dark:text-gray-400"> {capitalize(action)} </span> {label} {auditableDisplayName && ( <> {postParagraph && ( <span className="text-gray-500 dark:text-gray-400"> {postParagraph} </span> )} <span className="inline-block max-w-md truncate text-primary"> {auditableDisplayName} </span> </> )} </> ); return ( <TableRow key={id} className="group py-1" > <TableCell className="align-top font-medium"> <span className="flex items-center space-x-2"> {actorType === "api_key" && ( <PiKeyBold className="h-4 w-4" title="API Key activity" /> )} {actorType === "user" && ( <PiUserBold className="h-4 w-4" title="User activity" /> )} {actorType === "system" && ( <PiRobotFill className="h-4 w-4" title="System activity" /> )} <span className="block font-medium"> {apiKeyName ? `${apiKeyName} (${actorDisplayName})` : `${actorDisplayName}`} </span> </span> </TableCell> <TableCell> <div className="space-y-2"> <div className="flex flex-wrap space-x-1.5"> {actionView} </div> <Badge className="font-mono" variant="outline"> {auditAction} </Badge> </div> </TableCell> <TableCell className="align-top"> {formatDateTime(new Date(createdAt))} </TableCell> </TableRow> ); }, )} </TableBody> </Table> </TableWrapper> ); };