Files
DeskcommCRM/components/admin/platform-admins/PlatformAdminsTable.tsx
T
Rafael MelgaçoandClaude Opus 4.7 09379256a4 feat(EPIC-11): platform-admins read-only [wave 14]
GET /api/v1/admin/platform-admins: requirePlatformAdmin, service-role
list of platform_admins with auth.users emails resolved (granted_by,
revoked_by, target user). POST/PATCH/DELETE return 405 explicitly with
message pointing to Spec 01 §3.4 T-04 (DBA-only mutation).

UI:
- /admin/platform-admins: prominent DBAOnlyNotice (blue alert with
  T-04 reference + runbook link) + PlatformAdminsTable (user, granted_at
  relative, granted_by, scope, mfa_required, status active/revoked,
  reason tooltip)
- ZERO mutation buttons by design

Closes EPIC-11 — Super-Admin Platform fully wired (14/14 waves).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:45:12 -03:00

220 lines
7.0 KiB
TypeScript

"use client";
import { formatDistanceToNow } from "date-fns";
import { ptBR } from "date-fns/locale";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { PlatformAdminEntry } from "@/hooks/useAdminPlatformAdmins";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function relativeDate(iso: string): string {
try {
return formatDistanceToNow(new Date(iso), { addSuffix: true, locale: ptBR });
} catch {
return iso;
}
}
function shortEmail(email: string | null): string {
if (!email) return "—";
const [local, domain] = email.split("@");
if (!domain) return email;
const shortDomain = domain.split(".")[0];
return `${local}@${shortDomain}`;
}
// ---------------------------------------------------------------------------
// Skeleton
// ---------------------------------------------------------------------------
export function PlatformAdminsTableSkeleton() {
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
{["Usuário", "Concedido em", "Concedido por", "Scope", "MFA", "Status", "Motivo"].map(
(h) => (
<TableHead key={h}>{h}</TableHead>
),
)}
</TableRow>
</TableHeader>
<TableBody>
{Array.from({ length: 4 }).map((_, i) => (
<TableRow key={i}>
{Array.from({ length: 7 }).map((__, j) => (
<TableCell key={j}>
<Skeleton className="h-4 w-full" />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
// ---------------------------------------------------------------------------
// Empty state
// ---------------------------------------------------------------------------
function EmptyState() {
return (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-16 text-center">
<p className="text-sm font-medium text-muted-foreground">
Nenhum platform admin encontrado
</p>
<p className="mt-1 text-xs text-muted-foreground">
Platform admins são configurados exclusivamente via DBA.
</p>
</div>
);
}
// ---------------------------------------------------------------------------
// Reason cell with tooltip for long text
// ---------------------------------------------------------------------------
function ReasonCell({ reason }: { reason: string | null }) {
if (!reason) return <span className="text-muted-foreground">—</span>;
const MAX = 40;
if (reason.length <= MAX) {
return <span className="text-xs">{reason}</span>;
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-help truncate text-xs underline decoration-dotted">
{reason.slice(0, MAX)}…
</span>
</TooltipTrigger>
<TooltipContent className="max-w-xs break-words">{reason}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
// ---------------------------------------------------------------------------
// Table
// ---------------------------------------------------------------------------
interface PlatformAdminsTableProps {
data: PlatformAdminEntry[];
}
export function PlatformAdminsTable({ data }: PlatformAdminsTableProps) {
if (data.length === 0) return <EmptyState />;
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[200px]">Usuário</TableHead>
<TableHead className="w-[140px]">Concedido em</TableHead>
<TableHead className="w-[160px]">Concedido por</TableHead>
<TableHead className="w-[120px]">Scope</TableHead>
<TableHead className="w-[60px]">MFA</TableHead>
<TableHead className="w-[90px]">Status</TableHead>
<TableHead>Motivo</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((row) => {
const isRevoked = !!row.revoked_at;
return (
<TableRow key={row.id} className={isRevoked ? "opacity-60" : undefined}>
{/* User */}
<TableCell>
<div className="flex flex-col gap-0.5">
<span className="text-sm font-medium">
{row.user_email ?? (
<span className="font-mono text-xs text-muted-foreground">
{row.user_id.slice(0, 8)}
</span>
)}
</span>
{row.user_name && (
<span className="text-xs text-muted-foreground">{row.user_name}</span>
)}
</div>
</TableCell>
{/* Granted At */}
<TableCell className="text-xs text-muted-foreground whitespace-nowrap">
{relativeDate(row.granted_at)}
</TableCell>
{/* Granted By */}
<TableCell className="text-xs text-muted-foreground">
{shortEmail(row.granted_by_email)}
</TableCell>
{/* Scope */}
<TableCell>
<Badge variant="outline" className="text-[10px] font-mono">
{row.scope ?? "platform"}
</Badge>
</TableCell>
{/* MFA Required */}
<TableCell>
{row.mfa_required ? (
<Badge variant="default" className="text-[10px]">
Sim
</Badge>
) : (
<Badge variant="secondary" className="text-[10px]">
Não
</Badge>
)}
</TableCell>
{/* Status */}
<TableCell>
{isRevoked ? (
<Badge variant="destructive" className="text-[10px]">
Revogado
</Badge>
) : (
<Badge
variant="outline"
className="border-green-500 text-[10px] text-green-700"
>
Ativo
</Badge>
)}
</TableCell>
{/* Reason */}
<TableCell className="max-w-[200px]">
<ReasonCell reason={row.reason} />
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
}