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 uipkge-ng@latest add api-keys$npx uipkge-ng@latest add api-keys$yarn dlx uipkge-ng@latest add api-keys$bunx uipkge-ng@latest add api-keysManifest:
https://uipkge.dev/r/angular/api-keys.jsonInstalls to:src/app/components/blocks/api-keys/Variants
Loading interactive previews…
Loading interactive previews…
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 (2)
src/app/components/blocks/api-keys/api-keys.component.ts14.1 kB
// Developer 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. // Port of the React block 1:1. import { ChangeDetectionStrategy, Component, DestroyRef, Input, inject, signal } from '@angular/core' import { Check, Copy, Eye, EyeOff, KeyRound, LucideAngularModule, Plus } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, } from '@/ui/dialog/dialog.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSectionCardComponent } from '@/ui/section-card/section-card.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' export type ApiKeyEnvironment = 'production' | 'staging' | 'development' export type ApiKeyStatus = 'active' | 'revoked' | 'expiring' export type ApiKeysDensity = 'default' | 'compact' export interface ApiKeyItem { id: string name: string environment: ApiKeyEnvironment value: string createdAt: Date lastUsedAt: Date | null status: ApiKeyStatus } 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', }, ] } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-api-keys, [ui-api-keys]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, UiInputComponent, UiSectionCardComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, ], template: ` <div data-slot="api-keys" [class]="rootClass"> <div ui-section-card title="API Keys" description="Secret keys used to authenticate requests to your API."> <button ui-button size="sm" slot="header-action" (click)="openCreate()"> <lucide-icon [img]="Plus" class="size-4" /> Create key </button> @if (keys().length === 0) { <div class="flex flex-col items-center justify-center px-6 py-14 text-center"> <div class="bg-muted text-muted-foreground mx-auto grid size-12 place-items-center rounded-full"> <lucide-icon [img]="KeyRound" class="size-5" /> </div> <p class="mt-3 text-sm font-medium">No API keys yet</p> <p class="text-muted-foreground mt-0.5 max-w-xs text-xs"> Create a key to start authenticating requests against your API. </p> <button ui-button size="sm" class="mt-4" (click)="openCreate()">Create your first key</button> </div> } @else { <ul class="-my-4 divide-y"> @for (key of keys(); track key.id) { <li [class]="rowClass"> <div class="min-w-0 flex-1"> <div class="flex items-center gap-2"> <p class="truncate text-sm font-medium">{{ key.name }}</p> <span ui-badge [variant]="statusVariant(key.status)">{{ statusLabel(key.status) }}</span> </div> <p class="text-muted-foreground mt-0.5 truncate text-xs"> Created {{ formatDate(key.createdAt) }} · Last used {{ lastUsedText(key.lastUsedAt) }} </p> </div> <code class="bg-muted hidden shrink-0 rounded px-2 py-1 font-mono text-xs md:block"> {{ isRevealed(key.id) ? key.value : mask(key.value) }} </code> <div class="flex shrink-0 items-center gap-1"> <button ui-button variant="ghost" size="icon-sm" [attr.aria-label]="isRevealed(key.id) ? 'Hide key value' : 'Reveal key value'" (click)="toggleRevealed(key.id)" > @if (isRevealed(key.id)) { <lucide-icon [img]="EyeOff" class="size-4" /> } @else { <lucide-icon [img]="Eye" class="size-4" /> } </button> <button ui-button variant="ghost" size="icon-sm" [attr.aria-label]="copiedId() === key.id ? 'Copied' : 'Copy key value'" (click)="copyKey(key)" > @if (copiedId() === key.id) { <lucide-icon [img]="Check" class="text-success size-4" /> } @else { <lucide-icon [img]="Copy" class="text-muted-foreground size-4" /> } </button> @if (key.status !== 'revoked') { <button ui-button size="sm" [variant]="confirmingId() === key.id ? 'destructive' : 'ghost'" [class]="confirmingId() === key.id ? undefined : 'text-muted-foreground hover:text-destructive'" (click)="requestRevoke(key.id)" (blur)="cancelRevoke(key.id)" > {{ confirmingId() === key.id ? 'Confirm?' : 'Revoke' }} </button> } </div> </li> } </ul> } </div> <ui-dialog [open]="createOpen()" (openChange)="handleCreateOpenChange($event)"> <ui-dialog-content class="sm:max-w-md"> <div ui-dialog-header> <h2 ui-dialog-title>Create API key</h2> <p ui-dialog-description> Generate a new secret key. The full value is only shown once — store it somewhere safe. </p> </div> <div class="grid gap-4 py-2"> <div class="grid gap-1.5"> <label for="angular-api-key-name" class="text-sm leading-none font-medium">Name</label> <ui-input id="angular-api-key-name" [value]="newName()" (valueChange)="newName.set($event)" placeholder="e.g. Production server" (keydown.enter)="createKey()" /> </div> <div class="grid gap-1.5"> <label for="angular-api-key-environment" class="text-sm leading-none font-medium">Environment</label> <ui-select [value]="newEnv()" (valueChange)="setNewEnv($event)"> <button ui-select-trigger id="angular-api-key-environment" class="w-full"> <ui-select-value placeholder="Choose an environment" /> </button> <ui-select-content> <ui-select-item value="production">Production</ui-select-item> <ui-select-item value="staging">Staging</ui-select-item> <ui-select-item value="development">Development</ui-select-item> </ui-select-content> </ui-select> </div> </div> <div ui-dialog-footer> <button ui-button variant="outline" (click)="createOpen.set(false)">Cancel</button> <button ui-button [disabled]="!newName().trim()" (click)="createKey()">Create key</button> </div> </ui-dialog-content> </ui-dialog> </div> `, }) export class UiApiKeysComponent { protected readonly Check = Check protected readonly Copy = Copy protected readonly Eye = Eye protected readonly EyeOff = EyeOff protected readonly KeyRound = KeyRound protected readonly Plus = Plus private readonly now = new Date() @Input() set initialKeys(v: ApiKeyItem[] | undefined) { this.keys.set(v ? v.map((k) => ({ ...k })) : createStubKeys(this.now)) } @Input() set initialCreateOpen(v: boolean) { this.createOpen.set(!!v) } @Input() set density(v: ApiKeysDensity) { this.densityValue.set(v ?? 'default') } @Input('class') className?: string readonly keys = signal<ApiKeyItem[]>(createStubKeys(this.now)) readonly revealed = signal<Record<string, boolean>>({}) readonly copiedId = signal<string | null>(null) readonly confirmingId = signal<string | null>(null) readonly createOpen = signal(false) readonly newName = signal('') readonly newEnv = signal<ApiKeyEnvironment>('production') readonly densityValue = signal<ApiKeysDensity>('default') private copyTimer: ReturnType<typeof setTimeout> | undefined private confirmTimer: ReturnType<typeof setTimeout> | undefined constructor() { inject(DestroyRef).onDestroy(() => { clearTimeout(this.copyTimer) clearTimeout(this.confirmTimer) }) } get rootClass(): string { return cn('w-full', this.className) } get rowClass(): string { return cn('flex items-center gap-3 sm:gap-4', this.densityValue() === 'compact' ? 'py-2.5' : 'py-4') } statusLabel(status: ApiKeyStatus): string { return statusMeta[status].label } statusVariant(status: ApiKeyStatus): 'success' | 'warning' | 'destructive' { return statusMeta[status].variant } mask(value: string): string { return maskKeyValue(value) } formatDate(d: Date): string { return formatDate(d) } isRevealed(id: string): boolean { return Boolean(this.revealed()[id]) } lastUsedText(d: Date | null): string { if (!d) return 'Never' const minutes = Math.max(1, Math.round((this.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` } setNewEnv(v: string): void { this.newEnv.set(v as ApiKeyEnvironment) } toggleRevealed(id: string): void { this.revealed.update((r) => ({ ...r, [id]: !r[id] })) } async copyKey(key: ApiKeyItem): Promise<void> { try { await navigator.clipboard.writeText(key.value) this.copiedId.set(key.id) clearTimeout(this.copyTimer) this.copyTimer = setTimeout(() => this.copiedId.set(null), 1600) } catch { // Clipboard unavailable (e.g. insecure context) — skip the feedback swap. } } requestRevoke(id: string): void { if (this.confirmingId() === id) { this.revokeKey(id) return } this.confirmingId.set(id) clearTimeout(this.confirmTimer) this.confirmTimer = setTimeout(() => this.confirmingId.set(null), 3000) } cancelRevoke(id: string): void { if (this.confirmingId() === id) { clearTimeout(this.confirmTimer) this.confirmingId.set(null) } } revokeKey(id: string): void { clearTimeout(this.confirmTimer) this.confirmingId.set(null) this.keys.update((ks) => ks.map((k) => (k.id === id ? { ...k, status: 'revoked' as const } : k))) } openCreate(): void { this.newName.set('') this.newEnv.set('production') this.createOpen.set(true) } handleCreateOpenChange(open: boolean): void { if (open) this.openCreate() else this.createOpen.set(false) } createKey(): void { const name = this.newName().trim() if (!name) return const random = Array.from({ length: 2 }, () => Math.random().toString(36).slice(2, 14)).join('') const env = this.newEnv() this.keys.update((ks) => [ { id: `key-${Date.now()}`, name, environment: env, value: `${environmentPrefix[env]}${random}`, createdAt: new Date(), lastUsedAt: null, status: 'active', }, ...ks, ]) this.createOpen.set(false) } }src/app/components/blocks/api-keys/index.ts0.1 kB
export { UiApiKeysComponent, type ApiKeyEnvironment, type ApiKeyItem, type ApiKeysDensity, type ApiKeyStatus, } from './api-keys.component'
Raw manifest:https://uipkge.dev/r/angular/api-keys.json