140 lines
8.4 KiB
TypeScript
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>
|
|
);
|
|
}
|