refactor(keys): ConfirmDialog with type-to-confirm, EmptyState

- Replace window.confirm for key revoke with ConfirmDialog + confirmText prop
  requiring user to type the key label before confirmation
- Replace terse "No API keys" text with EmptyState component (Key, Plus, Shield icons)
- Add focus-visible ring on Revoke button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-05-13 11:16:10 +01:00
parent a605ba44eb
commit b118166693

View file

@ -9,8 +9,11 @@ import Button from '@/components/ui/Button.vue'
import Dialog from '@/components/ui/Dialog.vue'
import Input from '@/components/ui/Input.vue'
import Spinner from '@/components/ui/Spinner.vue'
import ConfirmDialog from '@/components/ui/ConfirmDialog.vue'
import EmptyState from '@/components/ui/EmptyState.vue'
import { toast } from 'vue-sonner'
import { formatDate } from '@/lib/utils'
import { Key, Plus, Shield } from 'lucide-vue-next'
import type { ApiKey } from '@/types'
const keys = ref<ApiKey[]>([])
@ -20,6 +23,10 @@ const newKeyLabel = ref('')
const creating = ref(false)
const generatedKey = ref<string | null>(null)
// Revoke confirm state
const showRevokeConfirm = ref(false)
const pendingRevokeKey = ref<ApiKey | null>(null)
onMounted(() => loadKeys())
async function loadKeys() {
@ -48,8 +55,15 @@ async function handleCreate() {
}
}
async function handleRevoke(key: ApiKey) {
if (!confirm(`Revoke key "${key.label}"? This cannot be undone.`)) return
function requestRevoke(key: ApiKey) {
pendingRevokeKey.value = key
showRevokeConfirm.value = true
}
async function executeRevoke() {
if (!pendingRevokeKey.value) return
const key = pendingRevokeKey.value
pendingRevokeKey.value = null
try {
await adminApi.revokeKey(key.id)
toast.success('Key revoked')
@ -58,13 +72,18 @@ async function handleRevoke(key: ApiKey) {
toast.error('Failed to revoke key')
}
}
function openCreateDialog() {
showCreate.value = true
generatedKey.value = null
}
</script>
<template>
<div class="p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-semibold text-foreground">API Keys</h2>
<Button size="sm" @click="showCreate = true; generatedKey = null">
<Button size="sm" @click="openCreateDialog">
<svg class="h-4 w-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
@ -77,8 +96,14 @@ async function handleRevoke(key: ApiKey) {
<div v-if="loading" class="flex items-center justify-center h-20">
<Spinner class="text-primary" />
</div>
<div v-else-if="keys.length === 0" class="text-center text-muted-foreground py-8 text-sm">
No API keys
<div v-else-if="keys.length === 0" class="p-6">
<EmptyState
title="No API keys yet"
description="Create a key to ingest Claude Code sessions."
:icons="[Key, Plus, Shield]"
action-label="Generate key"
@action="openCreateDialog"
/>
</div>
<table v-else class="w-full">
<thead>
@ -103,7 +128,12 @@ async function handleRevoke(key: ApiKey) {
{{ key.last_used ? formatDate(key.last_used) : 'Never' }}
</td>
<td class="px-4 py-3 text-right">
<Button variant="ghost" size="sm" class="text-destructive" @click="handleRevoke(key)">
<Button
variant="ghost"
size="sm"
class="text-destructive focus-visible:ring-destructive"
@click="requestRevoke(key)"
>
Revoke
</Button>
</td>
@ -137,5 +167,14 @@ async function handleRevoke(key: ApiKey) {
</Button>
</template>
</Dialog>
<!-- Revoke confirmation -->
<ConfirmDialog
v-model:open="showRevokeConfirm"
title="Revoke API key"
:description="`This will permanently revoke '${pendingRevokeKey?.label}'. Sessions using this key will stop ingesting.`"
:confirm-text="pendingRevokeKey?.label"
@confirm="executeRevoke"
/>
</div>
</template>