"use client"; import { Button } from "@app/components/ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import { toast } from "@app/hooks/useToast"; import { zodResolver } from "@hookform/resolvers/zod"; import { AxiosResponse } from "axios"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import CopyTextBox from "@app/components/CopyTextBox"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { formatAxiosError, createApiClient } from "@app/lib/api"; import { cn } from "@app/lib/cn"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; import { CaretSortIcon } from "@radix-ui/react-icons"; import { Checkbox } from "@app/components/ui/checkbox"; import { useTranslations } from "next-intl"; import { UserSelector, type SelectedUser } from "@app/components/user-selector"; import type { CreateOrEditVirtualApiKeyResponse } from "@server/routers/virtualApiKey/types"; import { formatVirtualApiKeyCredential } from "@app/lib/virtualApiKeyFormat"; import { MultiResourcesSelector, formatMultiResourcesSelectorLabel } from "@app/components/multi-resource-selector"; import type { SelectedResource } from "@app/components/resource-selector"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; import { BudgetRowsFields, getBudgetRowsErrors, type BudgetRow } from "@app/components/BudgetsEditor"; export type CreatedVirtualApiKey = { virtualApiKeyId: string; orgId: string; kind: "manual" | "user"; userId: string | null; name: string | null; description: string | null; lastChars: string; allResources: boolean; expiresAt: number | null; lastUsedAt: number | null; createdAt: number; createdByUserId: string | null; resourceIds: number[]; userName?: string | null; username?: string | null; userEmail?: string | null; resourceNames: string; resources: { resourceId: number; name: string; niceId: string }[]; }; type FormProps = { open: boolean; setOpen: (open: boolean) => void; onCreated?: (result: CreatedVirtualApiKey) => void; }; export default function CreateVirtualApiKeyForm({ open, setOpen, onCreated }: FormProps) { const { org } = useOrgContext(); const { env } = useEnvContext(); const api = createApiClient({ env }); const t = useTranslations(); const [credential, setCredential] = useState(null); const [loading, setLoading] = useState(false); const [allResources, setAllResources] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const [selectedResources, setSelectedResources] = useState< SelectedResource[] >([]); const [pendingBudgetRows, setPendingBudgetRows] = useState([]); const [attemptedBudgetsSave, setAttemptedBudgetsSave] = useState(false); const formSchema = z.object({ name: z.string().min(1), description: z.string().optional() }); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { name: "", description: "" } }); function resetLocalState() { setCredential(null); setLoading(false); setAllResources(false); setSelectedUser(null); setSelectedResources([]); setPendingBudgetRows([]); setAttemptedBudgetsSave(false); form.reset(); } function handleFormSubmit(values: z.infer) { const { conflictingKeys, invalidAmountKeys } = getBudgetRowsErrors(pendingBudgetRows); if (conflictingKeys.size > 0 || invalidAmountKeys.size > 0) { setAttemptedBudgetsSave(true); toast({ variant: "destructive", title: t("aiBudgetErrorSave"), description: conflictingKeys.size ? t("aiBudgetConflictError") : t("aiBudgetInvalidAmountError") }); return; } return onSubmit(values); } async function onSubmit(values: z.infer) { setLoading(true); const res = await api .put>( `/org/${org.org.orgId}/virtual-api-key`, { name: values.name, description: values.description || null, userId: selectedUser?.id ?? null, allResources, resourceIds: allResources ? [] : selectedResources.map((r) => r.resourceId) } ) .catch((e) => { console.error(e); toast({ variant: "destructive", title: t("virtualApiKeysErrorCreate"), description: formatAxiosError( e, t("virtualApiKeysErrorCreateDescription") ) }); }); if (res?.data.data.virtualApiKey) { const key = res.data.data.virtualApiKey; if (key.secret) { setCredential( formatVirtualApiKeyCredential( key.virtualApiKeyId, key.secret ) ); } const pendingBudgets = pendingBudgetRows.filter( (budget) => budget.amount.trim() !== "" ); if (pendingBudgets.length > 0) { try { await Promise.all( pendingBudgets.map((budget) => api.put(`/org/${org.org.orgId}/ai-budget`, { virtualApiKeyId: key.virtualApiKeyId, amount: Number(budget.amount), unit: budget.unit, period: budget.period }) ) ); } catch (e) { toast({ variant: "destructive", title: t("aiBudgetErrorSave"), description: formatAxiosError(e, t("aiBudgetErrorSave")) }); } } const resourceLookup = new Map( selectedResources.map((r) => [ r.resourceId, { name: r.name, niceId: r.niceId } ]) ); const resourceNames = key.allResources ? t("virtualApiKeysAllResources") : key.resourceIds .map((id) => resourceLookup.get(id)?.name) .filter(Boolean) .join(", ") || t("virtualApiKeysNoResources"); onCreated?.({ virtualApiKeyId: key.virtualApiKeyId, orgId: key.orgId, kind: key.kind, userId: key.userId, name: key.name, description: key.description, lastChars: key.lastChars, allResources: key.allResources, expiresAt: key.expiresAt, lastUsedAt: key.lastUsedAt, createdAt: key.createdAt, createdByUserId: key.createdByUserId, resourceIds: key.resourceIds, userName: selectedUser?.text ?? null, username: null, userEmail: null, resourceNames, resources: key.resourceIds.map((id) => ({ resourceId: id, name: resourceLookup.get(id)?.name ?? String(id), niceId: resourceLookup.get(id)?.niceId ?? "" })) }); } setLoading(false); } return ( { setOpen(val); if (!val) { resetLocalState(); } }} > {t("virtualApiKeysCreate")} {t("virtualApiKeysCreateDescription")}
{!credential && (
( {t( "virtualApiKeysName" )} )} /> ( {t( "virtualApiKeysDescriptionOptional" )} )} />
{t( "virtualApiKeysAssociateUserOptional" )}

{t( "virtualApiKeysAssociateUserDescription" )}

{ setAllResources( val as boolean ); if (val) { setSelectedResources( [] ); } }} className="mt-0.5" />

{t( "virtualApiKeysAllResourcesDescription" )}

{!allResources && (
{t( "virtualApiKeysSelectResources" )} 0 } onClear={() => setSelectedResources( [] ) } /> {t( "virtualApiKeysSelectResourcesDescription" )}
)}

{t( "virtualApiKeysInferenceBudgetDescription" )}

)} {credential && (

{t("virtualApiKeysCopyKey")}

)}
); }