fiddy/apps/web/features/groups/components/group-settings-audit-card.tsx

140 lines
8.4 KiB
TypeScript

"use client";
import type { GroupSettingsViewModelState } from "@/features/groups/components/use-group-settings-view-model";
type GroupSettingsAuditCardProps = {
vm: GroupSettingsViewModelState;
};
export default function GroupSettingsAuditCard({ vm }: GroupSettingsAuditCardProps) {
if (!vm.canViewAudit) return null;
return (
<div className="panel panel-accent p-4 space-y-3">
<div className="card-header">
<div className="card-title">Audit log</div>
<button
type="button"
className="rounded-lg btn-outline-accent px-2 py-1 text-xs"
onClick={() => vm.setAuditOpen(prev => !prev)}
>
{vm.auditOpen ? "Collapse" : "Expand"}
</button>
</div>
{vm.auditOpen ? (
<>
<div className="flex flex-wrap items-center gap-2">
<div className="rounded-lg border border-accent-weak bg-panel px-3 py-2">
<div className="text-[11px] text-soft">Total logs</div>
<div className="text-base font-semibold">{vm.events.length}</div>
</div>
<div className="rounded-lg border border-accent-weak bg-panel px-3 py-2">
<div className="text-[11px] text-soft">Logs today</div>
<div className="text-base font-semibold">{vm.logsToday}</div>
</div>
<button
type="button"
className="rounded-lg border border-accent-weak bg-panel px-3 py-2 text-left disabled:opacity-60"
onClick={() => vm.mostActiveUser ? vm.setAuditQuery(vm.mostActiveUser.searchValue) : null}
disabled={!vm.mostActiveUser}
>
<div className="text-[11px] text-soft">Most Active User ({vm.mostActiveCount})</div>
<div className="text-base font-semibold">
{vm.mostActiveUser ? `${vm.mostActiveUser.name}` : "-"}
</div>
</button>
</div>
<div className="flex flex-wrap items-center gap-2">
{([
{ key: "entries", label: "Entries" },
{ key: "members", label: "Members" },
{ key: "tags", label: "Tags" },
{ key: "settings", label: "Settings" }
] as const).map(filter => (
<button
key={filter.key}
type="button"
className={`rounded-lg border px-2 py-1 text-xs font-semibold ${vm.auditFilters.includes(filter.key) ? "border-accent bg-accent-soft" : "border-accent-weak bg-panel"}`}
onClick={() => vm.setAuditFilters(prev => prev.includes(filter.key) ? prev.filter(item => item !== filter.key) : [...prev, filter.key])}
>
{filter.label}
</button>
))}
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="relative min-w-[220px] flex-1">
<svg className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-soft" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
<input
className="input-base w-full px-9 py-2 text-sm"
placeholder="Search by user, action, or request id"
value={vm.auditQuery}
onChange={event => vm.setAuditQuery(event.target.value)}
/>
</div>
<div className="flex min-w-[220px] flex-1 items-center gap-2 flex-nowrap">
<div className="relative flex-1">
<svg className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-soft" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
<input
type="date"
className="no-date-icon input-base date-input w-full pl-9 pr-3 py-2 text-sm"
value={vm.auditFrom}
onChange={event => vm.setAuditFrom(event.target.value)}
/>
</div>
<div className="relative flex-1">
<svg className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-soft" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
<input
type="date"
className="no-date-icon input-base date-input w-full pl-9 pr-3 py-2 text-sm"
value={vm.auditTo}
onChange={event => vm.setAuditTo(event.target.value)}
/>
</div>
</div>
</div>
{!vm.filteredEvents.length ? (
<div className="text-xs text-soft">No audit events match your filters.</div>
) : (
<div className="max-h-72 space-y-2 overflow-auto rounded-lg border border-accent-weak bg-panel p-2">
{vm.filteredEvents.slice(0, vm.auditLimit).map(event => (
<div key={event.id} className="rounded-lg border border-accent-weak bg-panel px-3 py-2 text-xs text-soft">
<div className="text-sm font-semibold text-[color:var(--color-text)]">{event.eventType}</div>
<div className="mt-1 flex flex-wrap gap-2 text-[11px] text-soft">
<span>Actor: {vm.getMemberLabel(event.actorUserId)}</span>
<span>Role: {event.actorRole ?? "-"}</span>
<span>{new Date(event.createdAt).toLocaleString()}</span>
</div>
</div>
))}
{vm.filteredEvents.length > vm.auditLimit ? (
<button
type="button"
className="w-full rounded-lg btn-outline-accent px-2 py-1 text-xs"
onClick={() => vm.setAuditLimit(prev => prev + 10)}
>
Load more
</button>
) : null}
</div>
)}
</>
) : (
<div className="text-xs text-soft">Audit log is collapsed.</div>
)}
</div>
);
}