255 lines
11 KiB
TypeScript
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 & 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>
|
|
);
|
|
}
|