import { Fragment, useState } from "react"; import { Table, Text, Group, Skeleton, Anchor, Collapse, Box, } from "@mantine/core"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { IconChevronRight, IconChevronDown, IconArrowRight, } from "@tabler/icons-react"; import { IAuditLog } from "@/ee/audit/types/audit.types"; import { CustomAvatar } from "@/components/ui/custom-avatar"; import { getEventLabel } from "@/ee/audit/lib/audit-event-labels"; import { formattedDate } from "@/lib/time"; import NoTableResults from "@/components/common/no-table-results"; import classes from "./audit-logs.module.css"; type AuditLogsTableProps = { items?: IAuditLog[]; isLoading: boolean; }; function hasDetails(entry: IAuditLog): boolean { return !!(entry.changes?.before || entry.changes?.after || entry.metadata); } function getResourceUrl(entry: IAuditLog): string | null { if (!entry.resource) return null; switch (entry.resourceType) { case "group": return `/settings/groups/${entry.resource.id}`; case "space": case "space_member": return entry.resource.slug ? `/s/${entry.resource.slug}` : null; default: return null; } } function formatValue(value: unknown): string { if (value === null || value === undefined) return "—"; if (typeof value === "boolean") return value ? "true" : "false"; if (typeof value === "object") return JSON.stringify(value); return String(value); } function ChangesDiff({ changes }: { changes: IAuditLog["changes"] }) { const { t } = useTranslation(); if (!changes) return null; const { before, after } = changes; const allKeys = new Set([ ...Object.keys(before ?? {}), ...Object.keys(after ?? {}), ]); if (allKeys.size === 0) return null; return ( {t("Changes")} {[...allKeys].map((key) => { const hasBefore = before && key in before; const hasAfter = after && key in after; return ( {key}: {hasBefore && ( {formatValue(before[key])} )} {hasBefore && hasAfter && ( )} {hasAfter && ( {formatValue(after[key])} )} ); })} ); } function MetadataDisplay({ metadata }: { metadata: Record }) { const { t } = useTranslation(); const entries = Object.entries(metadata); if (entries.length === 0) return null; return ( {t("Metadata")} {entries.map(([key, value]) => ( {key}: {formatValue(value)} ))} ); } function TableSkeleton() { return ( <> {Array.from({ length: 8 }).map((_, i) => ( ))} > ); } function ResourceCell({ entry }: { entry: IAuditLog }) { if (!entry.resource?.name) { return ( — ); } const url = getResourceUrl(entry); if (url) { return ( {entry.resource.name} ); } return ( {entry.resource.name} ); } export default function AuditLogsTable({ items, isLoading, }: AuditLogsTableProps) { const { t } = useTranslation(); const [expanded, setExpanded] = useState>(new Set()); const toggleExpanded = (id: string) => { setExpanded((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }; return ( {t("Actor")} {t("Event")} {t("Resource")} {t("Date")} {isLoading ? ( ) : items && items.length > 0 ? ( items.map((entry) => { const expandable = hasDetails(entry); const isExpanded = expanded.has(entry.id); return ( toggleExpanded(entry.id) : undefined } style={{ cursor: expandable ? "pointer" : undefined }} > {expandable ? ( isExpanded ? ( ) : ( ) ) : ( )} {entry.actor ? ( {entry.actor.name} {entry.actor.email} ) : ( {entry.actorType === "system" ? t("System") : t("System")} )} {t(getEventLabel(entry.event))} {formattedDate(new Date(entry.createdAt))} {expandable && ( {entry.changes && ( )} {entry.metadata && ( )} )} ); }) ) : ( )} ); }