feat: summarise key posture above the ledger
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
useToast,
|
||||
} from "@/components/ui";
|
||||
import { KeyLedger, LedgerSkeleton } from "./KeyLedger";
|
||||
import { KeyPosture } from "./KeyPosture";
|
||||
import { CreateKeyDialog, EXPIRY_OPTIONS } from "./CreateKeyDialog";
|
||||
|
||||
const ROLES: Role[] = ["owner", "admin", "member"];
|
||||
@@ -137,12 +138,7 @@ export function ApiKeysPanel() {
|
||||
return (
|
||||
<div className="p-4 sm:p-6 lg:p-8">
|
||||
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-text-primary">API Keys</h1>
|
||||
<p className="mt-1 text-sm text-text-secondary">
|
||||
{count} key{count !== 1 ? "s" : ""} · {showAll ? "instance-wide" : "yours"}
|
||||
</p>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-text-primary">API Keys</h1>
|
||||
<Button variant="primary" onClick={() => setCreateOpen(true)}>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
@@ -176,6 +172,8 @@ export function ApiKeysPanel() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{count > 0 && <KeyPosture tokens={tokens ?? []} capDays={capDays} />}
|
||||
|
||||
<Card padding={false}>
|
||||
<AsyncBoundary
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { ApiToken } from "@/lib/api";
|
||||
import { keyLifetime } from "@/lib/keyLifetime";
|
||||
|
||||
/*
|
||||
* Four counts above the list, answering "is anything wrong here" before a
|
||||
* single row is read. All four are derived from the tokens already in hand —
|
||||
* no second request, and no endpoint that could disagree with the list.
|
||||
*
|
||||
* The counts describe the list as filtered, so this sits below the My keys /
|
||||
* All keys toggle and moves with it.
|
||||
*/
|
||||
export function KeyPosture({ tokens, capDays }: { tokens: ApiToken[]; capDays: number }) {
|
||||
const now = Date.now();
|
||||
const lifetimes = tokens.map((t) => keyLifetime(t, capDays, now));
|
||||
|
||||
const cells: { n: number; label: string; tone: string }[] = [
|
||||
{ n: tokens.length, label: `key${tokens.length === 1 ? "" : "s"} listed`, tone: "text-text-primary" },
|
||||
{ n: lifetimes.filter((l) => l.state === "soon").length, label: "expire within 7 days", tone: "text-warning" },
|
||||
{ n: lifetimes.filter((l) => l.state === "eternal").length, label: "never expire", tone: "text-danger" },
|
||||
// Muted, not coloured: an unused key is a cleanup candidate, not an
|
||||
// incident.
|
||||
{ n: tokens.filter((t) => !t.last_used_at).length, label: "unused since issue", tone: "text-text-secondary" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-6 grid gap-px overflow-hidden rounded-lg border border-border bg-border-soft sm:grid-cols-2 lg:grid-cols-4">
|
||||
{cells.map((c) => (
|
||||
<div key={c.label} className="flex items-baseline gap-2.5 bg-surface px-3.5 py-3 lg:flex-col lg:gap-0.5">
|
||||
<span className={`font-mono text-xl font-semibold tabular-nums tracking-tight ${c.tone}`}>{c.n}</span>
|
||||
<span className="text-xs text-text-tertiary">{c.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user