import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alertDialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; import { resetDurationLabels } from "@/lib/constants/governance"; import { getErrorMessage, useDeleteVirtualKeyMutation, useLazyGetVirtualKeysQuery } from "@/lib/store"; import { Customer, Team, VirtualKey } from "@/lib/types/governance"; import { cn } from "@/lib/utils"; import { RateLimitDisplay } from "@/components/rateLimitDisplay"; import { formatCurrency } from "@/lib/utils/governance"; import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib"; import { useVirtualKeyUsage } from "../hooks/useVirtualKeyUsage"; import { ArrowUpDown, ChevronLeft, ChevronRight, Copy, Download, Edit, Eye, EyeOff, Loader2, Plus, Search, ShieldCheck, Trash2, } from "lucide-react"; import { useMemo, useState } from "react"; import { toast } from "sonner"; import VirtualKeyDetailSheet from "./virtualKeyDetailsSheet"; import { VirtualKeysEmptyState } from "./virtualKeysEmptyState"; import VirtualKeySheet from "./virtualKeySheet"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; const formatResetDuration = (duration: string) => resetDurationLabels[duration] || duration; type ExportScope = "current_page" | "all"; function virtualKeysToCSV(vks: VirtualKey[]): string { const headers = ["Name", "Status", "Assigned To", "Budget Limit", "Budget Spent", "Budget Reset", "Description", "Created At"]; const rows = vks.map((vk) => { const isExhausted = vk.budgets?.some((b) => b.current_usage >= b.max_limit) || (vk.rate_limit?.token_current_usage && vk.rate_limit?.token_max_limit && vk.rate_limit.token_current_usage >= vk.rate_limit.token_max_limit) || (vk.rate_limit?.request_current_usage && vk.rate_limit?.request_max_limit && vk.rate_limit.request_current_usage >= vk.rate_limit.request_max_limit); const status = vk.is_active ? (isExhausted ? "Exhausted" : "Active") : "Inactive"; const assignedTo = vk.team ? `Team: ${vk.team.name}` : vk.customer ? `Customer: ${vk.customer.name}` : ""; const budgetLimit = vk.budgets?.length ? vk.budgets.map((b) => formatCurrency(b.max_limit)).join("; ") : ""; const budgetSpent = vk.budgets?.length ? vk.budgets.map((b) => formatCurrency(b.current_usage)).join("; ") : ""; const budgetReset = vk.budgets?.length ? vk.budgets.map((b) => formatResetDuration(b.reset_duration)).join("; ") : ""; return [vk.name, status, assignedTo, budgetLimit, budgetSpent, budgetReset, vk.description || "", vk.created_at]; }); return [headers, ...rows].map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")).join("\n"); } function downloadCSV(content: string) { const blob = new Blob([content], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `virtual-keys-${new Date().toISOString().split("T")[0]}.csv`; link.click(); URL.revokeObjectURL(url); } function VKBudgetCell({ vk }: { vk: VirtualKey }) { const { displayBudgets } = useVirtualKeyUsage(vk); if (!displayBudgets || displayBudgets.length === 0) { return -; } return (
{displayBudgets.map((b, idx) => (
= b.max_limit && "text-red-400")}> {formatCurrency(b.current_usage)} / {formatCurrency(b.max_limit)} Resets {formatResetDuration(b.reset_duration)} {vk.calendar_aligned && " (calendar)"}
))}
); } function VKRateLimitCell({ vk }: { vk: VirtualKey }) { const { displayRateLimit } = useVirtualKeyUsage(vk); return ; } // Status badge derives exhaustion from the same AP-backed source as the budget/rate-limit cells // so managed keys don't show "Active" next to an exhausted-looking bar. function VKStatusBadge({ vk }: { vk: VirtualKey }) { const { isExhausted } = useVirtualKeyUsage(vk); return ( {vk.is_active ? (isExhausted ? "Exhausted" : "Active") : "Inactive"} ); } // Per-row delete button. Calls useVirtualKeyUsage (same cached query as the budget/ // rate-limit cells — RTK dedupes) to detect managed-by-AP VKs and swap the normal // delete AlertDialog for a disabled button + tooltip so users aren't lured into a // confirm-then-403 loop. function VKDeleteButton({ vk, hasDeleteAccess, isDeleting, onDelete, }: { vk: VirtualKey; hasDeleteAccess: boolean; isDeleting: boolean; onDelete: (vkId: string) => void; }) { const { isManagedByProfile } = useVirtualKeyUsage(vk); if (isManagedByProfile) { return (

This virtual key is managed by an access profile and can't be deleted here. Detach the profile from the user or delete it from the access profile settings.

); } return ( Delete Virtual Key Are you sure you want to delete "{vk.name.length > 20 ? `${vk.name.slice(0, 20)}...` : vk.name}"? This action cannot be undone. Cancel onDelete(vk.id)} disabled={isDeleting} className="bg-destructive hover:bg-destructive/90" data-testid={`vk-delete-confirm-${vk.name}`} > {isDeleting ? "Deleting..." : "Delete"} ); } interface VirtualKeysTableProps { virtualKeys: VirtualKey[]; totalCount: number; teams: Team[]; customers: Customer[]; search: string; debouncedSearch: string; onSearchChange: (value: string) => void; customerFilter: string; onCustomerFilterChange: (value: string) => void; teamFilter: string; onTeamFilterChange: (value: string) => void; offset: number; limit: number; onOffsetChange: (offset: number) => void; sortBy?: string; order?: string; onSortChange: (sortBy: string, order: string) => void; } export default function VirtualKeysTable({ virtualKeys, totalCount, teams, customers, search, debouncedSearch, onSearchChange, customerFilter, onCustomerFilterChange, teamFilter, onTeamFilterChange, offset, limit, onOffsetChange, sortBy, order, onSortChange, }: VirtualKeysTableProps) { const [showVirtualKeySheet, setShowVirtualKeySheet] = useState(false); const [editingVirtualKeyId, setEditingVirtualKeyId] = useState(null); const [revealedKeys, setRevealedKeys] = useState>(new Set()); const [selectedVirtualKeyId, setSelectedVirtualKeyId] = useState(null); const [showDetailSheet, setShowDetailSheet] = useState(false); const [showExportDialog, setShowExportDialog] = useState(false); const [exportScope, setExportScope] = useState("current_page"); const [exportMaxLimit, setExportMaxLimit] = useState(""); const [fetchVirtualKeys, { isFetching: isExporting }] = useLazyGetVirtualKeysQuery(); // Derive objects from props so they stay in sync with RTK cache updates const editingVirtualKey = useMemo( () => (editingVirtualKeyId ? (virtualKeys.find((vk) => vk.id === editingVirtualKeyId) ?? null) : null), [editingVirtualKeyId, virtualKeys], ); const selectedVirtualKey = useMemo( () => (selectedVirtualKeyId ? (virtualKeys.find((vk) => vk.id === selectedVirtualKeyId) ?? null) : null), [selectedVirtualKeyId, virtualKeys], ); const hasCreateAccess = useRbac(RbacResource.VirtualKeys, RbacOperation.Create); const hasUpdateAccess = useRbac(RbacResource.VirtualKeys, RbacOperation.Update); const hasDeleteAccess = useRbac(RbacResource.VirtualKeys, RbacOperation.Delete); const [deleteVirtualKey, { isLoading: isDeleting }] = useDeleteVirtualKeyMutation(); const handleDelete = async (vkId: string) => { try { await deleteVirtualKey(vkId).unwrap(); toast.success("Virtual key deleted successfully"); } catch (error) { toast.error(getErrorMessage(error)); } }; const handleAddVirtualKey = () => { setEditingVirtualKeyId(null); setShowVirtualKeySheet(true); }; const handleEditVirtualKey = (vk: VirtualKey, e: React.MouseEvent) => { e.stopPropagation(); // Prevent row click setEditingVirtualKeyId(vk.id); setShowVirtualKeySheet(true); }; const handleVirtualKeySaved = () => { setShowVirtualKeySheet(false); setEditingVirtualKeyId(null); }; const handleRowClick = (vk: VirtualKey) => { setSelectedVirtualKeyId(vk.id); setShowDetailSheet(true); }; const handleDetailSheetClose = () => { setShowDetailSheet(false); setSelectedVirtualKeyId(null); }; const toggleKeyVisibility = (vkId: string) => { const newRevealed = new Set(revealedKeys); if (newRevealed.has(vkId)) { newRevealed.delete(vkId); } else { newRevealed.add(vkId); } setRevealedKeys(newRevealed); }; const maskKey = (key: string, revealed: boolean) => { if (revealed) return key; return key.substring(0, 8) + "•".repeat(Math.max(0, key.length - 8)); }; const { copy: copyToClipboard } = useCopyToClipboard(); const hasActiveFilters = debouncedSearch || customerFilter || teamFilter; const toggleSort = (column: string) => { if (sortBy === column) { if (order === "asc") { onSortChange(column, "desc"); } else { // Clicking again clears sort onSortChange("", ""); } } else { onSortChange(column, "asc"); } }; const handleExportCSV = async () => { if (exportScope === "current_page") { downloadCSV(virtualKeysToCSV(virtualKeys)); toast.success(`Exported ${virtualKeys.length} virtual keys`); setShowExportDialog(false); return; } // Fetch all with same filters/sort applied const maxLimit = exportMaxLimit ? parseInt(exportMaxLimit, 10) : undefined; const fetchLimit = maxLimit && maxLimit > 0 ? maxLimit : 10000; try { const result = await fetchVirtualKeys({ limit: fetchLimit, offset: 0, search: debouncedSearch || undefined, customer_id: customerFilter || undefined, team_id: teamFilter || undefined, sort_by: (sortBy as "name" | "budget_spent" | "created_at" | "status") || undefined, order: (order as "asc" | "desc") || undefined, export: true, }).unwrap(); downloadCSV(virtualKeysToCSV(result.virtual_keys)); toast.success(`Exported ${result.virtual_keys.length} virtual keys`); setShowExportDialog(false); } catch (error) { toast.error(`Export failed: ${getErrorMessage(error)}`); } }; const openExportDialog = () => { setExportScope("current_page"); setExportMaxLimit(""); setShowExportDialog(true); }; const SortableHeader = ({ column, label }: { column: string; label: string }) => ( ); // True empty state: no VKs at all (not just filtered to zero) if (totalCount === 0 && !hasActiveFilters) { return ( <> {showVirtualKeySheet && ( setShowVirtualKeySheet(false)} /> )} ); } return ( <> {showVirtualKeySheet && ( setShowVirtualKeySheet(false)} /> )} {showDetailSheet && selectedVirtualKey && } {/* Export Dialog */} Export Virtual Keys Download as CSV with current filters and sorting applied.
{exportScope === "all" && (
setExportMaxLimit(e.target.value)} data-testid="vk-export-max-limit" />
)} {hasActiveFilters && (

Filters applied:{" "} {[debouncedSearch && `search "${debouncedSearch}"`, customerFilter && "customer filter", teamFilter && "team filter"] .filter(Boolean) .join(", ")}

)}

API tokens are excluded from the export.

Virtual Keys

Manage virtual keys, their permissions, budgets, and rate limits.

{/* Toolbar: Search + Filters */}
onSearchChange(e.target.value)} className="pl-9" data-testid="vk-search-input" />
{customerFilter && teamFilter && or}
Assigned To Key Rate Limits {virtualKeys.length === 0 ? ( No matching virtual keys found. ) : ( virtualKeys.map((vk) => { const isRevealed = revealedKeys.has(vk.id); return ( handleRowClick(vk)} >
{vk.name}
{vk.team ? ( Team: {vk.team.name} ) : vk.customer ? ( Customer: {vk.customer.name} ) : ( - )} e.stopPropagation()}>
{maskKey(vk.value, isRevealed)}
e.stopPropagation()}>
); }) )}
{/* Pagination */} {totalCount > 0 && (

Showing {offset + 1}-{Math.min(offset + limit, totalCount)} of {totalCount}

)}
); }