Api Keys
blocksecurityDeveloper API-key manager in a SectionCard. Self-contained stateful block with stub keys, reveal/copy/revoke row actions, an empty state, and a create-key dialog.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/api-keys.json$npx shadcn@latest add https://uipkge.dev/r/react/api-keys.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/api-keys.json$bunx shadcn@latest add https://uipkge.dev/r/react/api-keys.jsonNamed registry:
npx shadcn@latest add @uipkge-react/api-keysInstalls to:components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
initialKeysReplacement seed data. Pass [] to start from the empty state. | ApiKeyItem[] | — | optional |
initialCreateOpen | boolean | — | optional |
density | 'default' | 'compact' | — | optional |
className | string | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ApiKeyIteminterface ApiKeyItem {
id: string
name: string
environment: ApiKeyEnvironment
value: string
createdAt: Date
lastUsedAt: Date | null
status: ApiKeyStatus
}npm dependencies
Files installed (1)
components/blocks/ApiKeys.tsx11.8 kB
'use client' import * as React from 'react' import { Check, Copy, Eye, EyeOff, KeyRound, Plus } from 'lucide-react' import { cn } from '@/lib/utils' import { SectionCard } from '@/components/ui/section-card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' export type ApiKeyEnvironment = 'production' | 'staging' | 'development' export type ApiKeyStatus = 'active' | 'revoked' | 'expiring' export interface ApiKeyItem { id: string name: string environment: ApiKeyEnvironment value: string createdAt: Date lastUsedAt: Date | null status: ApiKeyStatus } export interface ApiKeysProps { /** Replacement seed data. Pass [] to start from the empty state. */ initialKeys?: ApiKeyItem[] initialCreateOpen?: boolean density?: 'default' | 'compact' className?: string } const statusMeta: Record<ApiKeyStatus, { label: string; variant: 'success' | 'warning' | 'destructive' }> = { active: { label: 'Active', variant: 'success' }, expiring: { label: 'Expires soon', variant: 'warning' }, revoked: { label: 'Revoked', variant: 'destructive' }, } const environmentPrefix: Record<ApiKeyEnvironment, string> = { production: 'uipkge_live_', staging: 'sk_stg_', development: 'sk_dev_', } function maskKeyValue(value: string): string { const parts = value.split('_') const prefix = parts.length > 2 ? `${parts[0]}_${parts[1]}_` : '' return `${prefix}••••••••${value.slice(-4)}` } function formatDate(d: Date): string { return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) } function createStubKeys(now: Date): ApiKeyItem[] { const daysAgo = (days: number) => new Date(now.getTime() - days * 24 * 60 * 60 * 1000) const hoursAgo = (hours: number) => new Date(now.getTime() - hours * 60 * 60 * 1000) return [ { id: 'key-prod-server', name: 'Production server', environment: 'production', value: 'uipkge_live_51HxQp9mZvKYlo2C4f2a', createdAt: daysAgo(94), lastUsedAt: hoursAgo(2), status: 'active', }, { id: 'key-ci-pipeline', name: 'CI pipeline (GitHub Actions)', environment: 'staging', value: 'sk_stg_6JdRw3nAwLZmp7xQb91e', createdAt: daysAgo(41), lastUsedAt: hoursAgo(26), status: 'active', }, { id: 'key-mobile-backend', name: 'Mobile app backend', environment: 'production', value: 'uipkge_live_8KfSx1oCyUZiqw9Ma2b7', createdAt: daysAgo(27), lastUsedAt: hoursAgo(5), status: 'expiring', }, { id: 'key-local-dev', name: 'Local development', environment: 'development', value: 'sk_dev_k2MnQ8tReFvBgy5Xc40d', createdAt: daysAgo(12), lastUsedAt: daysAgo(3), status: 'active', }, { id: 'key-legacy-webhooks', name: 'Legacy webhook integration', environment: 'production', value: 'uipkge_live_2PvBgR6nTxAokdJe5f8c', createdAt: daysAgo(210), lastUsedAt: daysAgo(64), status: 'revoked', }, ] } export function ApiKeys({ initialKeys, initialCreateOpen = false, density = 'default', className }: ApiKeysProps) { const [now] = React.useState(() => new Date()) const [keys, setKeys] = React.useState<ApiKeyItem[]>(() => initialKeys ? initialKeys.map((k) => ({ ...k })) : createStubKeys(now), ) const [revealed, setRevealed] = React.useState<Record<string, boolean>>({}) const [copiedId, setCopiedId] = React.useState<string | null>(null) const [confirmingId, setConfirmingId] = React.useState<string | null>(null) const [createOpen, setCreateOpen] = React.useState(initialCreateOpen) const [newName, setNewName] = React.useState('') const [newEnv, setNewEnv] = React.useState<ApiKeyEnvironment>('production') const copyTimer = React.useRef<number | undefined>(undefined) const confirmTimer = React.useRef<number | undefined>(undefined) const dense = density === 'compact' function lastUsedText(d: Date | null): string { if (!d) return 'Never' const minutes = Math.max(1, Math.round((now.getTime() - d.getTime()) / 60000)) if (minutes < 60) return `${minutes}m ago` const hours = Math.round(minutes / 60) if (hours < 24) return `${hours}h ago` return `${Math.round(hours / 24)}d ago` } function toggleRevealed(id: string) { setRevealed((r) => ({ ...r, [id]: !r[id] })) } async function copyKey(key: ApiKeyItem) { try { await navigator.clipboard.writeText(key.value) setCopiedId(key.id) window.clearTimeout(copyTimer.current) copyTimer.current = window.setTimeout(() => setCopiedId(null), 1600) } catch { // Clipboard unavailable (e.g. insecure context) — skip the feedback swap. } } function requestRevoke(id: string) { if (confirmingId === id) { revokeKey(id) return } setConfirmingId(id) window.clearTimeout(confirmTimer.current) confirmTimer.current = window.setTimeout(() => setConfirmingId(null), 3000) } function cancelRevoke(id: string) { if (confirmingId === id) { window.clearTimeout(confirmTimer.current) setConfirmingId(null) } } function revokeKey(id: string) { window.clearTimeout(confirmTimer.current) setConfirmingId(null) setKeys((ks) => ks.map((k) => (k.id === id ? { ...k, status: 'revoked' as const } : k))) } function openCreate() { setNewName('') setNewEnv('production') setCreateOpen(true) } function handleCreateOpenChange(open: boolean) { if (open) openCreate() else setCreateOpen(false) } function createKey() { const name = newName.trim() if (!name) return const random = Array.from({ length: 2 }, () => Math.random().toString(36).slice(2, 14)).join('') setKeys((ks) => [ { id: `key-${Date.now()}`, name, environment: newEnv, value: `${environmentPrefix[newEnv]}${random}`, createdAt: new Date(), lastUsedAt: null, status: 'active', }, ...ks, ]) setCreateOpen(false) } return ( <div data-slot="api-keys" className={cn('w-full', className)}> <SectionCard title="API Keys" description="Secret keys used to authenticate requests to your API." headerAction={ <Button size="sm" onClick={openCreate}> <Plus className="size-4" /> Create key </Button> } > {keys.length === 0 ? ( <div className="flex flex-col items-center justify-center px-6 py-14 text-center"> <div className="bg-muted text-muted-foreground mx-auto grid size-12 place-items-center rounded-full"> <KeyRound className="size-5" /> </div> <p className="mt-3 text-sm font-medium">No API keys yet</p> <p className="text-muted-foreground mt-0.5 max-w-xs text-xs"> Create a key to start authenticating requests against your API. </p> <Button size="sm" className="mt-4" onClick={openCreate}> Create your first key </Button> </div> ) : ( <ul className="-my-4 divide-y"> {keys.map((key) => ( <li key={key.id} className={cn('flex items-center gap-3 sm:gap-4', dense ? 'py-2.5' : 'py-4')}> <div className="min-w-0 flex-1"> <div className="flex items-center gap-2"> <p className="truncate text-sm font-medium">{key.name}</p> <Badge variant={statusMeta[key.status].variant}>{statusMeta[key.status].label}</Badge> </div> <p className="text-muted-foreground mt-0.5 truncate text-xs"> Created {formatDate(key.createdAt)} · Last used {lastUsedText(key.lastUsedAt)} </p> </div> <code className="bg-muted hidden shrink-0 rounded px-2 py-1 font-mono text-xs md:block"> {revealed[key.id] ? key.value : maskKeyValue(key.value)} </code> <div className="flex shrink-0 items-center gap-1"> <Button variant="ghost" size="icon-sm" aria-label={revealed[key.id] ? 'Hide key value' : 'Reveal key value'} onClick={() => toggleRevealed(key.id)} > {revealed[key.id] ? <EyeOff className="size-4" /> : <Eye className="size-4" />} </Button> <Button variant="ghost" size="icon-sm" aria-label={copiedId === key.id ? 'Copied' : 'Copy key value'} onClick={() => copyKey(key)} > {copiedId === key.id ? ( <Check className="text-success size-4" /> ) : ( <Copy className="text-muted-foreground size-4" /> )} </Button> {key.status !== 'revoked' && ( <Button size="sm" variant={confirmingId === key.id ? 'destructive' : 'ghost'} className={confirmingId === key.id ? undefined : 'text-muted-foreground hover:text-destructive'} onClick={() => requestRevoke(key.id)} onBlur={() => cancelRevoke(key.id)} > {confirmingId === key.id ? 'Confirm?' : 'Revoke'} </Button> )} </div> </li> ))} </ul> )} </SectionCard> <Dialog open={createOpen} onOpenChange={handleCreateOpenChange}> <DialogContent className="sm:max-w-md"> <DialogHeader> <DialogTitle>Create API key</DialogTitle> <DialogDescription> Generate a new secret key. The full value is only shown once — store it somewhere safe. </DialogDescription> </DialogHeader> <div className="grid gap-4 py-2"> <div className="grid gap-1.5"> <label htmlFor="api-key-name" className="text-sm leading-none font-medium"> Name </label> <Input id="api-key-name" value={newName} onChange={(e) => setNewName(e.target.value)} placeholder="e.g. Production server" onKeyDown={(e) => e.key === 'Enter' && createKey()} /> </div> <div className="grid gap-1.5"> <label htmlFor="api-key-environment" className="text-sm leading-none font-medium"> Environment </label> <Select value={newEnv} onValueChange={(v) => setNewEnv(v as ApiKeyEnvironment)}> <SelectTrigger id="api-key-environment" className="w-full"> <SelectValue placeholder="Choose an environment" /> </SelectTrigger> <SelectContent> <SelectItem value="production">Production</SelectItem> <SelectItem value="staging">Staging</SelectItem> <SelectItem value="development">Development</SelectItem> </SelectContent> </Select> </div> </div> <DialogFooter> <Button variant="outline" onClick={() => setCreateOpen(false)}> Cancel </Button> <Button disabled={!newName.trim()} onClick={createKey}> Create key </Button> </DialogFooter> </DialogContent> </Dialog> </div> ) }
Raw manifest:https://uipkge.dev/r/react/api-keys.json