Components

API Key Manager

An API key list with a create dialog, masked keys, copy-to-clipboard, and revoke actions.

data

Installation

npx cano-ui add api-key-manager

Installs badge, button, dialog, input, label, tooltip, lucide-react automatically if missing.

Source

"use client"

import * as React from "react"
import { Check, Copy, KeyRound, Plus, Trash2 } from "lucide-react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"

export interface ApiKey {
  id: string
  name: string
  /** Masked key shown in the list, e.g. "sk_live_••••4f2a". */
  maskedKey: string
  createdAt: string
  lastUsed?: string
  revoked?: boolean
}

export interface ApiKeyManagerProps extends React.ComponentProps<"div"> {
  keys: ApiKey[]
  onCreate?: (name: string) => void
  onRevoke?: (key: ApiKey) => void
  /** Returns the value to copy for a key; omit to hide the copy button. */
  getCopyValue?: (key: ApiKey) => string
  emptyMessage?: string
}

function CopyKeyButton({ value, name }: { value: string; name: string }) {
  const [copied, setCopied] = React.useState(false)

  return (
    <Tooltip>
      <TooltipTrigger asChild>
        <Button
          variant="ghost"
          size="icon"
          className="size-8"
          aria-label={`Copy ${name}`}
          onClick={() => {
            void navigator.clipboard?.writeText(value)
            setCopied(true)
            setTimeout(() => setCopied(false), 1500)
          }}
        >
          {copied ? (
            <Check className="text-emerald-600" aria-hidden="true" />
          ) : (
            <Copy aria-hidden="true" />
          )}
        </Button>
      </TooltipTrigger>
      <TooltipContent>{copied ? "Copied" : "Copy key"}</TooltipContent>
    </Tooltip>
  )
}

function CreateKeyDialog({ onCreate }: { onCreate: (name: string) => void }) {
  const [name, setName] = React.useState("")
  const [open, setOpen] = React.useState(false)

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <Button size="sm">
          <Plus aria-hidden="true" />
          Create key
        </Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-sm">
        <DialogHeader>
          <DialogTitle>Create API key</DialogTitle>
          <DialogDescription>
            Give the key a descriptive name so you can recognize it later.
          </DialogDescription>
        </DialogHeader>
        <div className="flex flex-col gap-2">
          <Label htmlFor="api-key-name">Key name</Label>
          <Input
            id="api-key-name"
            placeholder="e.g. Production server"
            value={name}
            onChange={(e) => setName(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === "Enter" && name.trim()) {
                onCreate(name.trim())
                setName("")
                setOpen(false)
              }
            }}
          />
        </div>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="outline">Cancel</Button>
          </DialogClose>
          <Button
            disabled={!name.trim()}
            onClick={() => {
              onCreate(name.trim())
              setName("")
              setOpen(false)
            }}
          >
            Create key
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

export function ApiKeyManager({
  keys,
  onCreate,
  onRevoke,
  getCopyValue,
  emptyMessage = "No API keys yet. Create one to get started.",
  className,
  ...props
}: ApiKeyManagerProps) {
  return (
    <div
      data-slot="api-key-manager"
      className={cn("w-full rounded-lg border", className)}
      {...props}
    >
      <div className="flex items-center justify-between gap-2 border-b px-4 py-3">
        <div className="flex flex-col">
          <h3 className="text-sm font-semibold">API keys</h3>
          <p className="text-xs text-muted-foreground">
            Keys grant full access to your account. Keep them secret.
          </p>
        </div>
        {onCreate ? <CreateKeyDialog onCreate={onCreate} /> : null}
      </div>
      {keys.length === 0 ? (
        <p className="px-4 py-10 text-center text-sm text-muted-foreground">
          {emptyMessage}
        </p>
      ) : (
        <ul className="divide-y">
          {keys.map((key) => (
            <li
              key={key.id}
              className={cn(
                "flex items-center gap-3 px-4 py-3",
                key.revoked && "opacity-60"
              )}
            >
              <span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted">
                <KeyRound
                  className="size-4 text-muted-foreground"
                  aria-hidden="true"
                />
              </span>
              <div className="flex min-w-0 flex-1 flex-col">
                <span className="flex items-center gap-2 text-sm font-medium">
                  <span className="truncate">{key.name}</span>
                  {key.revoked ? (
                    <Badge variant="outline" className="text-muted-foreground">
                      Revoked
                    </Badge>
                  ) : null}
                </span>
                <span className="flex gap-2 text-xs text-muted-foreground">
                  <code className="font-mono">{key.maskedKey}</code>
                  <span aria-hidden="true">·</span>
                  <span>
                    {key.lastUsed
                      ? `Last used ${key.lastUsed}`
                      : `Created ${key.createdAt}`}
                  </span>
                </span>
              </div>
              {!key.revoked && getCopyValue ? (
                <CopyKeyButton value={getCopyValue(key)} name={key.name} />
              ) : null}
              {!key.revoked && onRevoke ? (
                <Tooltip>
                  <TooltipTrigger asChild>
                    <Button
                      variant="ghost"
                      size="icon"
                      className="size-8 text-muted-foreground hover:text-destructive"
                      aria-label={`Revoke ${key.name}`}
                      onClick={() => onRevoke(key)}
                    >
                      <Trash2 aria-hidden="true" />
                    </Button>
                  </TooltipTrigger>
                  <TooltipContent>Revoke key</TooltipContent>
                </Tooltip>
              ) : null}
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}

This exact file lands in your project at components/ui/api-key-manager.tsx. It is yours to edit.