MotorSignal-App/website/components/ops/operations-audit.tsx

255 lines
11 KiB
TypeScript

'use client';
import React from 'react';
import { SafeAuditEvent, OperationsRole } from './types';
interface OperationsAuditProps {
role: OperationsRole;
events: SafeAuditEvent[];
nextCursor?: string | null;
onNextPage?: () => Promise<void> | void;
isLoading?: boolean;
isLoadingMore?: boolean;
total?: number;
}
export function OperationsAudit({
role,
events,
nextCursor,
onNextPage,
isLoading = false,
isLoadingMore = false,
total,
}: OperationsAuditProps) {
const isOwner = role === 'owner';
// Section 7: Operator Denied Access UX
if (!isOwner) {
return (
<div className="p-8 rounded-xl bg-surface-container-low border border-outline-variant/30 text-center max-w-xl mx-auto my-12 space-y-4">
<div className="w-12 h-12 rounded-full bg-surface-container-high border border-outline-variant/40 flex items-center justify-center text-outline mx-auto">
<span className="material-symbols-outlined text-[24px]">lock</span>
</div>
<div>
<span className="text-[10px] font-mono text-outline font-semibold">
Access Restricted
</span>
<h2 className="text-lg font-bold text-on-surface mt-1">
Owner Authority Required
</h2>
<p className="text-xs text-on-surface-variant mt-2 leading-relaxed">
You do not have authority to view Audit. Audit log inspection is restricted to verified Operations Owners.
</p>
</div>
<div className="pt-3 border-t border-outline-variant/20 flex items-center justify-center gap-2 text-xs text-outline font-mono">
<span>Current Role:</span>
<span className="px-2 py-0.5 rounded bg-surface-container-highest text-on-surface font-semibold capitalize">
{role}
</span>
</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Header Row */}
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 pb-4 border-b border-outline-variant/30">
<div>
<span className="text-[11px] font-mono text-primary-fixed-dim block font-semibold">
Governance &amp; Compliance
</span>
<h1 className="font-headline-xl text-2xl sm:text-3xl lg:text-4xl font-extrabold text-on-surface tracking-tight mt-1">
Audit Activity
</h1>
<p className="text-xs sm:text-sm text-outline mt-1.5 max-w-2xl">
Immutable operational and security activity log across platform boundaries.
</p>
</div>
{total !== undefined && (
<div className="text-right shrink-0">
<span className="text-[11px] font-mono text-outline block">
Recorded Events
</span>
<span className="text-sm sm:text-base font-mono font-extrabold text-on-surface">
{total.toLocaleString()}
</span>
</div>
)}
</div>
{/* Audit Activity Table Container */}
<div className="rounded-2xl bg-surface-container-low border border-outline-variant/30 overflow-hidden shadow-sm">
<div className="overflow-x-auto">
<table
className="w-full text-left border-collapse min-w-[720px]"
aria-label="Operations Audit Events"
>
<thead>
<tr className="border-b border-outline-variant/30 bg-surface-container-lowest/60 text-[11px] font-mono text-outline">
<th scope="col" className="py-4 px-5 sm:px-6 font-semibold">
Time
</th>
<th scope="col" className="py-4 px-5 sm:px-6 font-semibold">
Action
</th>
<th scope="col" className="py-4 px-5 sm:px-6 font-semibold">
Result
</th>
<th scope="col" className="py-4 px-5 sm:px-6 font-semibold">
Actor
</th>
<th scope="col" className="py-4 px-5 sm:px-6 font-semibold">
Target
</th>
<th scope="col" className="py-4 px-5 sm:px-6 font-semibold">
Case
</th>
</tr>
</thead>
<tbody className="divide-y divide-outline-variant/20 text-xs">
{isLoading ? (
// Skeleton loading state
Array.from({ length: 5 }).map((_, index) => (
<tr key={`skeleton-${index}`} className="animate-pulse">
<td className="py-3.5 px-4">
<div className="h-3 w-28 bg-surface-container-high rounded"></div>
</td>
<td className="py-3.5 px-4">
<div className="h-4 w-32 bg-surface-container-high rounded"></div>
</td>
<td className="py-3.5 px-4">
<div className="h-4 w-16 bg-surface-container-high rounded"></div>
</td>
<td className="py-3.5 px-4">
<div className="h-3 w-36 bg-surface-container-high rounded"></div>
</td>
<td className="py-3.5 px-4">
<div className="h-3 w-20 bg-surface-container-high rounded"></div>
</td>
<td className="py-3.5 px-4">
<div className="h-3 w-16 bg-surface-container-high rounded"></div>
</td>
</tr>
))
) : events.length === 0 ? (
<tr>
<td
colSpan={6}
className="py-12 px-4 text-center text-on-surface-variant"
>
<span className="material-symbols-outlined text-[32px] text-outline block mb-2">
inbox
</span>
<p className="text-xs font-medium">No audit events recorded.</p>
</td>
</tr>
) : (
events.map((event, index) => {
const formattedTime = new Date(event.timestamp).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
const isSuccess =
event.result.toLowerCase() === 'success' ||
event.result.toLowerCase() === 'recorded';
const isFailure =
event.result.toLowerCase() === 'failure' ||
event.result.toLowerCase() === 'denied';
return (
<tr
key={`event-${index}`}
className="hover:bg-surface-container-high/40 transition-colors"
>
{/* TIME */}
<td className="py-4 px-5 sm:px-6 font-mono text-[11px] sm:text-xs text-on-surface-variant whitespace-nowrap">
<time dateTime={event.timestamp}>{formattedTime}</time>
</td>
{/* ACTION */}
<td className="py-4 px-5 sm:px-6 font-medium text-on-surface whitespace-nowrap">
<div className="flex flex-col">
<span className="font-mono text-xs sm:text-sm font-semibold">{event.action}</span>
{event.reason_code && (
<span className="text-[10px] text-outline font-mono mt-0.5">
reason: {event.reason_code}
</span>
)}
</div>
</td>
{/* RESULT */}
<td className="py-4 px-5 sm:px-6 whitespace-nowrap">
<span
className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-mono font-semibold capitalize border ${
isSuccess
? 'bg-emerald-950/60 text-emerald-400 border-emerald-800/60'
: isFailure
? 'bg-rose-950/60 text-rose-400 border-rose-800/60'
: 'bg-surface-container-highest text-on-surface-variant border-outline-variant/30'
}`}
>
{event.result}
</span>
</td>
{/* ACTOR (Current Operations Identity) */}
<td className="py-4 px-5 sm:px-6">
<div className="flex flex-col">
<span className="font-semibold text-on-surface truncate max-w-xs sm:max-w-sm text-xs sm:text-sm" title={event.actor_identity}>
{event.actor_identity}
</span>
{event.actor_role && (
<span className="text-[10px] text-outline font-mono mt-0.5 capitalize">
Role: {event.actor_role}
</span>
)}
</div>
</td>
{/* TARGET */}
<td className="py-4 px-5 sm:px-6 font-mono text-xs text-on-surface-variant">
{event.target_resource || '--'}
</td>
{/* CASE */}
<td className="py-4 px-5 sm:px-6 font-mono text-xs text-on-surface-variant">
{event.case_reference || '--'}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
{/* Server Cursor Bounded Pagination */}
{nextCursor && (
<div className="p-3 border-t border-outline-variant/30 bg-surface-container-lowest/40 flex items-center justify-between">
<span className="text-[11px] text-outline font-mono">
Displaying bounded result set
</span>
<button
type="button"
onClick={() => void onNextPage?.()}
disabled={isLoadingMore}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-surface-container-high hover:bg-surface-bright text-on-surface border border-outline-variant/40 text-xs font-semibold transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none disabled:opacity-50"
>
<span>{isLoadingMore ? 'Loading...' : 'Next Page'}</span>
<span className="material-symbols-outlined text-[14px]">arrow_forward</span>
</button>
</div>
)}
</div>
</div>
);
}