"use client"; import { ExtendedColumnDef } from "@app/components/ui/data-table"; import { VirtualApiKeysDataTable } from "@app/components/VirtualApiKeysDataTable"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; import { Button } from "@app/components/ui/button"; import { Badge } from "@app/components/ui/badge"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; import { ArrowRight, ArrowUpDown, ArrowUpRight, Funnel, MoreHorizontal } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { formatAxiosError, createApiClient } from "@app/lib/api"; import { toast } from "@app/hooks/useToast"; import { useEnvContext } from "@app/hooks/useEnvContext"; import moment from "moment"; import CreateVirtualApiKeyForm, { type CreatedVirtualApiKey } from "@app/components/CreateVirtualApiKeyForm"; import EditVirtualApiKeyForm from "@app/components/EditVirtualApiKeyForm"; import ViewVirtualApiKeySecret from "@app/components/ViewVirtualApiKeySecret"; import CopyToClipboard from "@app/components/CopyToClipboard"; import { useTranslations } from "next-intl"; import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { UserSelector, type SelectedUser } from "@app/components/user-selector"; import { ResourceSelector, type SelectedResource } from "@app/components/resource-selector"; import { cn } from "@app/lib/cn"; import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilterPopover"; import type { GetVirtualApiKeyResponse } from "@server/routers/virtualApiKey/types"; import { formatVirtualApiKeyCredential, formatVirtualApiKeyPreview } from "@app/lib/virtualApiKeyFormat"; import { AxiosResponse } from "axios"; export type VirtualApiKeyRow = CreatedVirtualApiKey; type VirtualApiKeysTableProps = { virtualApiKeys: VirtualApiKeyRow[]; orgId: string; }; export default function VirtualApiKeysTable({ virtualApiKeys, orgId }: VirtualApiKeysTableProps) { const router = useRouter(); const t = useTranslations(); const api = createApiClient(useEnvContext()); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isViewSecretOpen, setIsViewSecretOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [selectedKey, setSelectedKey] = useState( null ); const [rows, setRows] = useState(virtualApiKeys); const [isRefreshing, setIsRefreshing] = useState(false); const [userFilterOpen, setUserFilterOpen] = useState(false); const [resourceFilterOpen, setResourceFilterOpen] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const [selectedResource, setSelectedResource] = useState(null); const [unassignedOnly, setUnassignedOnly] = useState(false); useEffect(() => { setRows(virtualApiKeys); }, [virtualApiKeys]); const filteredRows = useMemo(() => { return rows.filter((row) => { if (unassignedOnly && row.userId) { return false; } if (selectedUser && row.userId !== selectedUser.id) { return false; } if (selectedResource) { if ( !row.allResources && !row.resourceIds.includes(selectedResource.resourceId) ) { return false; } } return true; }); }, [rows, selectedUser, selectedResource, unassignedOnly]); const refreshData = async () => { setIsRefreshing(true); try { await new Promise((resolve) => setTimeout(resolve, 200)); router.refresh(); } catch { toast({ title: t("error"), description: t("refreshError"), variant: "destructive" }); } finally { setIsRefreshing(false); } }; async function deleteKey(id: string) { await api.delete(`/virtual-api-key/${id}`).catch((e) => { toast({ title: t("virtualApiKeysErrorDelete"), description: formatAxiosError( e, t("virtualApiKeysErrorDeleteMessage") ) }); throw e; }); setRows((prev) => prev.filter((r) => r.virtualApiKeyId !== id)); toast({ title: t("virtualApiKeysDeleted"), description: t("virtualApiKeysDeletedDescription") }); } const clearUserFilter = () => { setSelectedUser(null); setUnassignedOnly(false); setUserFilterOpen(false); }; const clearResourceFilter = () => { setSelectedResource(null); setResourceFilterOpen(false); }; const columns: ExtendedColumnDef[] = [ { accessorKey: "name", enableHiding: false, friendlyName: t("virtualApiKeysName"), header: ({ column }) => { return ( ); }, cell: ({ row }) => row.original.name || "-" }, { id: "resources", accessorFn: (row) => row.resourceNames, friendlyName: t("resource"), header: () => ( { setSelectedResource(resource); setResourceFilterOpen(false); }} /> ), cell: ({ row }) => { const r = row.original; if (r.allResources) { return t("virtualApiKeysAllResources"); } if (r.resources.length === 0) { return {t("virtualApiKeysNoResources")}; } if (r.resources.length === 1) { const resource = r.resources[0]; if (!resource.niceId) { return resource.name; } return ( ); } return r.resourceNames; } }, { accessorKey: "userId", friendlyName: t("user"), header: () => ( { setSelectedUser(null); setUnassignedOnly(true); setUserFilterOpen(false); } }} onSelectUser={(user) => { setSelectedUser(user); setUnassignedOnly(false); setUserFilterOpen(false); }} /> ), cell: ({ row }) => { const r = row.original; if (!r.userId) { return -; } return ( ); } }, { accessorKey: "lastChars", friendlyName: t("virtualApiKeysSecret"), header: ({ column }) => { return ( ); }, cell: ({ row }) => ( ) }, { accessorKey: "createdAt", friendlyName: t("created"), header: ({ column }) => { return ( ); }, cell: ({ row }) => moment(row.original.createdAt).format("lll") }, { id: "actions", enableHiding: false, header: () => , cell: ({ row }) => { const keyRow = row.original; return (
{ setSelectedKey(keyRow); setIsViewSecretOpen(true); }} > {t("virtualApiKeysViewSecret")} { setSelectedKey(keyRow); setIsDeleteModalOpen(true); }} > {t("delete")}
); } } ]; return ( <> {selectedKey && ( { setIsDeleteModalOpen(val); if (!val) setSelectedKey(null); }} dialog={

{t("virtualApiKeysQuestionRemove")}

{t("virtualApiKeysMessageRemove")}

} buttonText={t("virtualApiKeysDeleteConfirm")} onConfirm={async () => deleteKey(selectedKey.virtualApiKeyId) } string={selectedKey.name || selectedKey.virtualApiKeyId} title={t("virtualApiKeysDelete")} /> )} { setIsViewSecretOpen(val); if (!val) setSelectedKey(null); }} virtualApiKeyId={selectedKey?.virtualApiKeyId ?? null} name={selectedKey?.name} /> { setRows([val, ...rows]); }} /> { setIsEditModalOpen(val); if (!val) setSelectedKey(null); }} virtualApiKey={selectedKey} onUpdated={(val) => { setRows((prev) => prev.map((row) => row.virtualApiKeyId === val.virtualApiKeyId ? val : row ) ); }} /> { setIsCreateModalOpen(true); }} onRefresh={refreshData} isRefreshing={isRefreshing} /> ); } function VirtualApiKeySecretCell({ virtualApiKeyId, lastChars }: { virtualApiKeyId: string; lastChars: string; }) { const t = useTranslations(); const api = createApiClient(useEnvContext()); const preview = formatVirtualApiKeyPreview(virtualApiKeyId, lastChars); const [credential, setCredential] = useState(null); useEffect(() => { let cancelled = false; api.get>( `/virtual-api-key/${virtualApiKeyId}` ) .then((res) => { if (cancelled) { return; } const secret = res.data.data.virtualApiKey.secret; if (secret) { setCredential( formatVirtualApiKeyCredential(virtualApiKeyId, secret) ); } }) .catch((e) => { if (cancelled) { return; } toast({ variant: "destructive", title: t("virtualApiKeysErrorFetchSecret"), description: formatAxiosError( e, t("virtualApiKeysErrorFetchSecretDescription") ) }); }); return () => { cancelled = true; }; }, [virtualApiKeyId]); return ( ); }