"use client"; /** * ApiKeyTable (TASK-0128) — the key list: `prefix`, `name`, scope badges, * last used (relative, "never"), expiry (red when < 7 d, grey "expired" * when passed), status (Active / Revoked / Expired). Actions: Revoke * (inline confirm — "Requests will fail immediately with 401."), copy * prefix, expandable curl example (masked prefix, `Bearer ` * placeholder — the raw key never renders here). * * States: loading (skeleton rows), empty ("No API keys yet — create one * to connect integrations" + CTA), error (toast at the mutation level). */ import { Fragment, useState } from "react"; import { Check, Copy, ChevronDown, ChevronRight, KeyRound, Loader2, Trash2 } from "lucide-react"; import { ScopesBadge } from "./ScopesBadge"; import { cn } from "@/lib/utils"; import { curlExample, expiryState, keyStatus, relativeTime, shortDate } from "@/lib/api-keys"; export interface ApiKeyRow { id: string; name: string; prefix: string; scopes: string[]; rateLimit: number | null; lastUsedAt: string | null; expiresAt: string | null; revokedAt: string | null; createdAt: string; } interface ApiKeyTableProps { rows: ApiKeyRow[]; isLoading: boolean; baseApiUrl: string; // e.g. http://localhost:4000/api/v1 (curl examples) workspaceId: string; onRevoke: (row: ApiKeyRow) => void; revokingId: string | null; onCreate: () => void; } const NOW = Date.now(); // module load — the relative labels are per-render; fine for a settings page export function ApiKeyTable({ rows, isLoading, baseApiUrl, workspaceId, onRevoke, revokingId, onCreate, }: ApiKeyTableProps) { const [expandedId, setExpandedId] = useState(null); const [confirmId, setConfirmId] = useState(null); const [copiedId, setCopiedId] = useState(null); const copyPrefix = async (row: ApiKeyRow) => { try { await navigator.clipboard.writeText(row.prefix); setCopiedId(row.id); setTimeout(() => setCopiedId((c) => (c === row.id ? null : c)), 2000); } catch { /* clipboard unavailable */ } }; const curl = (row: ApiKeyRow) => curlExample( row.prefix, "POST", `${baseApiUrl}/workspaces/${workspaceId}/pages`, JSON.stringify({ title: "New page" }), ); return (
{isLoading ? (
{[0, 1, 2].map((i) => (
))}
) : rows.length === 0 ? (

No API keys yet — create one to connect integrations.

) : (
{rows.map((row) => { const status = keyStatus(row, NOW); const exp = expiryState(row.expiresAt, NOW); const expanded = expandedId === row.id; return ( <> {/* Inline revoke confirm. */} {confirmId === row.id && status === "Active" && ( )} {/* Expandable curl example. */} {expanded && ( )} ); })}
API keys for this workspace
Key Scopes Last used Expiry Status Actions
{row.rateLimit != null ? ( {row.rateLimit}/min ) : null} {relativeTime(row.lastUsedAt, NOW)} {exp === "never" ? ( never ) : exp === "expired" ? ( {shortDate(row.expiresAt!)} (expired) ) : exp === "soon" ? ( {shortDate(row.expiresAt!)} ) : ( {shortDate(row.expiresAt!)} )} {status} {status === "Active" && confirmId !== row.id && ( )}
Revoke this key? Requests will fail immediately with 401. {" "} {" "}

Example request (replace the bearer placeholder with the full key):

                            {curl(row)}
                          
)}
); }