-

API Key Card

PreviousNext

A management card for API keys, scopes, last-used timestamps, and revoke actions.

Installation

pnpm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/api-key-card.json
npm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/api-key-card.json
yarn
yarn dlx shadcn@latest add https://ui.zgi.ai/r/api-key-card.json

Component

components/ui/api-key-card.tsx
"use client"

import * as React from "react"
import {
  CalendarIcon,
  CopyIcon,
  KeyRoundIcon,
  MoreHorizontalIcon,
} from "lucide-react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export interface ApiKeyCardAction {
  label: React.ReactNode
  icon?: React.ReactNode
  onSelect?: () => void
}

export interface ApiKeyCardProps
  extends Omit<React.ComponentProps<typeof Card>, "title"> {
  name: React.ReactNode
  keyPrefix: string
  status?: "active" | "expired" | "revoked"
  scopes?: React.ReactNode[]
  createdAt?: React.ReactNode
  lastUsedAt?: React.ReactNode
  onCopy?: () => void
  actions?: ApiKeyCardAction[]
}

const statusClassName = {
  active: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
  expired: "bg-amber-500/10 text-amber-700 dark:text-amber-300",
  revoked: "bg-destructive/10 text-destructive",
}

export function ApiKeyCard({
  name,
  keyPrefix,
  status = "active",
  scopes = [],
  createdAt,
  lastUsedAt,
  onCopy,
  actions = [],
  className,
  ...props
}: ApiKeyCardProps) {
  return (
    <Card className={cn("rounded-lg py-0", className)} {...props}>
      <CardContent className="space-y-4 p-4">
        <div className="flex items-start gap-3">
          <div className="bg-muted text-muted-foreground flex size-10 shrink-0 items-center justify-center rounded-lg">
            <KeyRoundIcon className="size-5" />
          </div>
          <div className="min-w-0 flex-1">
            <div className="flex min-w-0 flex-wrap items-center gap-2">
              <h3 className="truncate text-sm font-medium">{name}</h3>
              <Badge
                variant="secondary"
                className={cn(
                  "h-6 rounded-md border-0",
                  statusClassName[status]
                )}
              >
                {status}
              </Badge>
            </div>
            <div className="mt-2 flex items-center gap-2">
              <code className="bg-muted rounded-md px-2 py-1 text-xs">
                {keyPrefix}••••••••
              </code>
              <Button
                type="button"
                variant="ghost"
                size="icon"
                className="size-7 rounded-md"
                onClick={onCopy}
                aria-label="Copy API key prefix"
              >
                <CopyIcon className="size-3.5" />
              </Button>
            </div>
          </div>
          {actions.length ? (
            <DropdownMenu>
              <DropdownMenuTrigger asChild>
                <Button
                  type="button"
                  variant="ghost"
                  size="icon"
                  className="size-8 shrink-0 rounded-md"
                  aria-label="Open API key actions"
                >
                  <MoreHorizontalIcon className="size-4" />
                </Button>
              </DropdownMenuTrigger>
              <DropdownMenuContent align="end">
                {actions.map((action, index) => (
                  <DropdownMenuItem key={index} onSelect={action.onSelect}>
                    {action.icon ? (
                      <span className="[&_svg]:size-4">{action.icon}</span>
                    ) : null}
                    {action.label}
                  </DropdownMenuItem>
                ))}
              </DropdownMenuContent>
            </DropdownMenu>
          ) : null}
        </div>

        {scopes.length ? (
          <div className="flex flex-wrap gap-1.5">
            {scopes.map((scope, index) => (
              <Badge
                key={index}
                variant="outline"
                className="h-6 rounded-md px-2 font-normal"
              >
                {scope}
              </Badge>
            ))}
          </div>
        ) : null}

        <div className="grid gap-2 border-t pt-3 text-xs sm:grid-cols-2">
          {createdAt ? (
            <span className="text-muted-foreground flex min-w-0 items-center gap-1.5">
              <CalendarIcon className="size-3.5 shrink-0" />
              <span className="truncate">Created {createdAt}</span>
            </span>
          ) : null}
          {lastUsedAt ? (
            <span className="text-muted-foreground truncate">
              Last used {lastUsedAt}
            </span>
          ) : null}
        </div>
      </CardContent>
    </Card>
  )
}

Usage

import { ApiKeyCard } from "@/components/ui/api-key-card"
 
export function Example() {
  return (
    <ApiKeyCard
      name="Production API"
      keyPrefix="zgi_live_8x42"
      scopes={["agents:run", "datasets:read"]}
      lastUsedAt="4 minutes ago"
    />
  )
}

Props

PropTypeDefault
nameReactNoderequired
keyPrefixstringrequired
status"active" | "expired" | "revoked""active"
scopesReactNode[][]
createdAtReactNode-
lastUsedAtReactNode-
onCopy() => void-
actionsApiKeyCardAction[][]