import { useVirtualKeyUsage } from "@/app/workspace/virtual-keys/hooks/useVirtualKeyUsage"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alertDialog"; import { AsyncMultiSelect } from "@/components/ui/asyncMultiselect"; import { Button } from "@/components/ui/button"; import { ConfigSyncAlert } from "@/components/ui/configSyncAlert"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ModelMultiselect } from "@/components/ui/modelMultiselect"; import MultiBudgetLines from "@/components/ui/multibudgets"; import { MultiSelect } from "@/components/ui/multiSelect"; import NumberAndSelect from "@/components/ui/numberAndSelect"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { DottedSeparator } from "@/components/ui/separator"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { Switch } from "@/components/ui/switch"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Textarea } from "@/components/ui/textarea"; import Toggle from "@/components/ui/toggle"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/components/ui/utils"; import { ModelPlaceholders } from "@/lib/constants/config"; import { resetDurationOptions, supportsCalendarAlignment } from "@/lib/constants/governance"; import { ProviderIconType, RenderProviderIcon } from "@/lib/constants/icons"; import { ProviderLabels, ProviderName } from "@/lib/constants/logs"; import { getErrorMessage, useCreateVirtualKeyMutation, useGetAllKeysQuery, useGetMCPClientsQuery, useGetProvidersQuery, useUpdateVirtualKeyMutation, } from "@/lib/store"; import { KnownProvider } from "@/lib/types/config"; import { CreateVirtualKeyRequest, Customer, Team, UpdateVirtualKeyRequest, VirtualKey } from "@/lib/types/governance"; import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib"; import { zodResolver } from "@hookform/resolvers/zod"; import { useNavigate } from "@tanstack/react-router"; import { Building, Info, Lock, RotateCcw, Trash2, Users, X } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { components, MultiValueProps, OptionProps } from "react-select"; import { toast } from "sonner"; import { z } from "zod"; interface VirtualKeySheetProps { virtualKey?: VirtualKey | null; teams: Team[]; customers: Customer[]; // When set and not editing, the new VK is created owned by this team and the sheet locks // all fields except name/description (same treatment as access-profile-managed keys). defaultTeamId?: string; onSave: () => void; onCancel: () => void; } // Provider configuration schema const providerConfigSchema = z.object({ id: z.number().optional(), provider: z.string().min(1, "Provider is required"), weight: z.number().min(0, "Weight must be at least 0").max(1, "Weight must be at most 1").optional(), allowed_models: z.array(z.string()).optional(), key_ids: z.array(z.string()).optional(), // Keys associated with this provider config // Provider-level budget budgets: z .array( z.object({ max_limit: z.number().nonnegative().optional(), reset_duration: z.string().optional(), }), ) .optional(), // Provider-level rate limits rate_limit: z .object({ token_max_limit: z.number().int().nonnegative().optional(), token_reset_duration: z.string().optional(), request_max_limit: z.number().int().nonnegative().optional(), request_reset_duration: z.string().optional(), }) .optional(), }); const mcpConfigSchema = z.object({ id: z.number().optional(), mcp_client_name: z.string().min(1, "MCP client name is required"), tools_to_execute: z.array(z.string()).optional(), }); // Main form schema const formSchema = z .object({ name: z.string().min(1, "Virtual key name is required"), description: z.string().optional(), providerConfigs: z.array(providerConfigSchema).optional(), mcpConfigs: z.array(mcpConfigSchema).optional(), entityType: z.enum(["team", "customer", "none"]), teamId: z.string().optional(), customerId: z.string().optional(), isActive: z.boolean(), // Budget budgetCalendarAligned: z.boolean(), budgets: z .array( z.object({ max_limit: z.number().nonnegative().optional(), reset_duration: z.string(), }), ) .optional(), // Token limits tokenMaxLimit: z.number().int().nonnegative().optional(), tokenResetDuration: z.string().optional(), // Request limits requestMaxLimit: z.number().int().nonnegative().optional(), requestResetDuration: z.string().optional(), }) .refine( (data) => { // If entityType is "team", teamId must be provided and not empty if (data.entityType === "team") { return data.teamId && data.teamId.trim() !== ""; } // If entityType is "customer", customerId must be provided and not empty if (data.entityType === "customer") { return data.customerId && data.customerId.trim() !== ""; } return true; }, { message: "Please select a valid team or customer when assignment type is chosen", path: ["entityType"], // This will show the error on the entityType field }, ); type FormData = z.infer; type VirtualKeyType = { label: string; value: string; description: string; provider: string; }; export default function VirtualKeySheet({ virtualKey, teams, customers, defaultTeamId, onSave, onCancel }: VirtualKeySheetProps) { const [isOpen, setIsOpen] = useState(true); const navigate = useNavigate(); const isEditing = !!virtualKey; const hasCreateAccess = useRbac(RbacResource.VirtualKeys, RbacOperation.Create); const hasUpdateAccess = useRbac(RbacResource.VirtualKeys, RbacOperation.Update); const canSubmit = isEditing ? hasUpdateAccess : hasCreateAccess; // Detect AP-managed status via the managing profile's virtual_key_ids, not just by the presence // of assignees — directly-attached users don't imply an access-profile relation. const { assignedUsers, isManagedByProfile: isManagedByProfileHook } = useVirtualKeyUsage(virtualKey); const isManagedByProfile = isEditing && isManagedByProfileHook; // Team attachment: when a VK already belongs to a team (edit) or will be created for one // (create from team detail sheet via defaultTeamId), the team assignment is fixed — users // can still edit providers/budgets/rate limits/MCP, but not reparent the VK. const attachedTeamId = isEditing ? (virtualKey?.team_id || "") : (defaultTeamId || ""); const attachedTeam = attachedTeamId ? teams.find((t) => t.id === attachedTeamId) : undefined; const isTeamLocked = !!attachedTeamId; const handleClose = () => { setIsOpen(false); setTimeout(() => { onCancel(); }, 150); // Slightly longer than the 100ms animation duration }; // RTK Query hooks const { data: providersData, error: providersError } = useGetProvidersQuery(); const { data: keysData, error: keysError } = useGetAllKeysQuery(); const [createVirtualKey, { isLoading: isCreating }] = useCreateVirtualKeyMutation(); const [updateVirtualKey, { isLoading: isUpdating }] = useUpdateVirtualKeyMutation(); const { data: mcpClientsResponse, error: mcpClientsError } = useGetMCPClientsQuery(); const mcpClientsData = mcpClientsResponse?.clients || []; const isLoading = isCreating || isUpdating; const availableKeys = keysData || []; const availableProviders = providersData || []; // Form setup const form = useForm, unknown, FormData>({ resolver: zodResolver(formSchema), defaultValues: { name: virtualKey?.name || "", description: virtualKey?.description || "", providerConfigs: virtualKey?.provider_configs?.map((config) => ({ id: config.id, provider: config.provider, weight: config.weight ?? undefined, allowed_models: config.allowed_models, key_ids: config.allow_all_keys ? ["*"] : config.keys?.map((key) => key.key_id) || [], budgets: config.budgets?.map((b) => ({ max_limit: b.max_limit, reset_duration: b.reset_duration, })), rate_limit: config.rate_limit ? { token_max_limit: config.rate_limit.token_max_limit ?? undefined, token_reset_duration: config.rate_limit.token_reset_duration, request_max_limit: config.rate_limit.request_max_limit ?? undefined, request_reset_duration: config.rate_limit.request_reset_duration, } : undefined, })) || [], mcpConfigs: virtualKey?.mcp_configs?.map((config) => ({ id: config.id, mcp_client_name: config.mcp_client?.name || "", tools_to_execute: config.tools_to_execute || [], })) || [], entityType: virtualKey?.team_id ? "team" : virtualKey?.customer_id ? "customer" : !isEditing && defaultTeamId ? "team" : "none", teamId: virtualKey?.team_id || (!isEditing ? defaultTeamId || "" : ""), customerId: virtualKey?.customer_id || "", isActive: virtualKey?.is_active ?? true, budgets: virtualKey?.budgets && virtualKey.budgets.length > 0 ? virtualKey.budgets.map((b) => ({ max_limit: b.max_limit, reset_duration: b.reset_duration ?? "1M" })) : [], budgetCalendarAligned: virtualKey?.calendar_aligned ?? false, tokenMaxLimit: virtualKey?.rate_limit?.token_max_limit ?? undefined, tokenResetDuration: virtualKey?.rate_limit?.token_reset_duration || "1h", requestMaxLimit: virtualKey?.rate_limit?.request_max_limit ?? undefined, requestResetDuration: virtualKey?.rate_limit?.request_reset_duration || "1h", }, }); // Handle keys loading error useEffect(() => { if (keysError) { toast.error(`Failed to load available keys: ${getErrorMessage(keysError)}`); } }, [keysError]); // Handle providers loading error useEffect(() => { if (providersError) { toast.error(`Failed to load available providers: ${getErrorMessage(providersError)}`); } }, [providersError]); // Handle mcp clients loading error useEffect(() => { if (mcpClientsError) { toast.error(`Failed to load available MCP clients: ${getErrorMessage(mcpClientsError)}`); } }, [mcpClientsError]); // Clear team/customer IDs when entityType changes to "none" useEffect(() => { const entityType = form.watch("entityType"); if (entityType === "none") { form.setValue("teamId", "", { shouldDirty: true }); form.setValue("customerId", "", { shouldDirty: true }); } else if (entityType === "team") { form.setValue("customerId", "", { shouldDirty: true }); } else if (entityType === "customer") { form.setValue("teamId", "", { shouldDirty: true }); } }, [form.watch("entityType"), form]); // Provider configuration state const [selectedProvider, setSelectedProvider] = useState(""); // MCP client configuration state const [selectedMCPClient, setSelectedMCPClient] = useState(""); // Get current provider configs from form const providerConfigs = form.watch("providerConfigs") || []; // Get current MCP configs from form const mcpConfigs = form.watch("mcpConfigs") || []; // Watch budget/rate-limit fields for conditional rendering of reset buttons const watchedBudgets = form.watch("budgets"); const watchedTokenMaxLimit = form.watch("tokenMaxLimit"); const watchedRequestMaxLimit = form.watch("requestMaxLimit"); const watchedBudgetCalendarAligned = form.watch("budgetCalendarAligned"); // Calendar alignment is VK-wide: show toggle if any budget has a max_limit and supports alignment const hasAnyAlignableBudget = watchedBudgets && watchedBudgets.length > 0 && watchedBudgets.some((b) => b.max_limit !== undefined && b.max_limit !== null && supportsCalendarAlignment(b.reset_duration || "1M")); // Handle adding a new provider configuration const handleAddProvider = (provider: string) => { const existingConfig = providerConfigs.find((config) => config.provider === provider); if (existingConfig) { toast.error("This provider is already configured"); return; } const newConfig = { provider: provider, weight: undefined as number | undefined, // undefined = excluded from weighted routing until user sets a weight allowed_models: ["*"], key_ids: ["*"], }; form.setValue("providerConfigs", [...providerConfigs, newConfig], { shouldDirty: true }); }; // Handle removing a provider configuration const handleRemoveProvider = (index: number) => { const updatedConfigs = providerConfigs.filter((_, i) => i !== index); form.setValue("providerConfigs", updatedConfigs, { shouldDirty: true }); }; // Handle updating provider configuration const handleUpdateProviderConfig = (index: number, field: string, value: any) => { const updatedConfigs = [...providerConfigs]; updatedConfigs[index] = { ...updatedConfigs[index], [field]: value }; form.setValue("providerConfigs", updatedConfigs, { shouldDirty: true }); }; // Handle adding a new MCP client configuration const handleAddMCPClient = (mcpClientName: string) => { const existingConfig = mcpConfigs.find((config) => config.mcp_client_name === mcpClientName); if (existingConfig) { toast.error("This MCP client is already configured"); return; } const newConfig = { mcp_client_name: mcpClientName, tools_to_execute: ["*"], }; form.setValue("mcpConfigs", [...mcpConfigs, newConfig], { shouldDirty: true }); }; // Handle removing an MCP client configuration const handleRemoveMCPClient = (index: number) => { const updatedConfigs = mcpConfigs.filter((_, i) => i !== index); form.setValue("mcpConfigs", updatedConfigs, { shouldDirty: true }); }; // Handle updating MCP client configuration const handleUpdateMCPConfig = (index: number, field: keyof (typeof mcpConfigs)[0], value: any) => { const updatedConfigs = [...mcpConfigs]; updatedConfigs[index] = { ...updatedConfigs[index], [field]: value }; form.setValue("mcpConfigs", updatedConfigs, { shouldDirty: true }); }; const [showCalendarAlignWarning, setShowCalendarAlignWarning] = useState(false); const handleCalendarAlignedChange = (checked: boolean) => { if (checked && isEditing) { // Show warning when enabling on an existing VK setShowCalendarAlignWarning(true); } else { form.setValue("budgetCalendarAligned", checked, { shouldDirty: true }); } }; const clearVirtualKeyBudget = () => { form.setValue("budgets", [], { shouldDirty: true }); form.setValue("budgetCalendarAligned", false, { shouldDirty: true }); }; const clearVirtualKeyRateLimits = () => { form.setValue("tokenMaxLimit", undefined, { shouldDirty: true }); form.setValue("tokenResetDuration", "1h", { shouldDirty: true }); form.setValue("requestMaxLimit", undefined, { shouldDirty: true }); form.setValue("requestResetDuration", "1h", { shouldDirty: true }); }; const normalizeProviderConfigs = (configs: typeof providerConfigs, existingConfigs?: VirtualKey["provider_configs"]): any[] => { return configs.map((config) => ({ ...config, budgets: config.budgets?.filter((b): b is { max_limit: number; reset_duration: string } => b.max_limit !== undefined), weight: config.weight ?? null, rate_limit: (() => { const hasTokenMaxLimit = config.rate_limit?.token_max_limit !== undefined; const hasRequestMaxLimit = config.rate_limit?.request_max_limit !== undefined; if (hasTokenMaxLimit || hasRequestMaxLimit) { return { token_max_limit: config.rate_limit?.token_max_limit ?? null, token_reset_duration: hasTokenMaxLimit ? config.rate_limit?.token_reset_duration || "1h" : null, request_max_limit: config.rate_limit?.request_max_limit ?? null, request_reset_duration: hasRequestMaxLimit ? config.rate_limit?.request_reset_duration || "1h" : null, }; } const existingConfig = existingConfigs?.find((item) => (config.id ? item.id === config.id : item.provider === config.provider)); if (existingConfig?.rate_limit) { return {}; } return undefined; })(), })); }; // Handle form submission const onSubmit = async (data: FormData) => { if (!canSubmit) { toast.error("You don't have permission to perform this action"); return; } try { // Managed VKs only allow name + description updates; all other fields are owned by the access profile. if (isManagedByProfile && virtualKey) { await updateVirtualKey({ vkId: virtualKey.id, data: { name: data.name, description: data.description, }, }).unwrap(); toast.success("Virtual key updated"); onSave(); return; } // Normalize provider configs to ensure weights are numbers and handle budget/rate limits const normalizedProviderConfigs = data.providerConfigs ? normalizeProviderConfigs(data.providerConfigs, virtualKey?.provider_configs) : []; if (isEditing && virtualKey) { // Update existing virtual key const updateData: UpdateVirtualKeyRequest = { name: data.name, description: data.description, provider_configs: normalizedProviderConfigs, mcp_configs: data.mcpConfigs, team_id: data.entityType === "team" && data.teamId && data.teamId.trim() !== "" ? data.teamId : undefined, customer_id: data.entityType === "customer" && data.customerId && data.customerId.trim() !== "" ? data.customerId : undefined, is_active: data.isActive, }; // Add budgets if enabled const validBudgets = (data.budgets || []).filter( (b): b is { max_limit: number; reset_duration: string } => b.max_limit !== undefined, ); const hadBudget = virtualKey.budgets && virtualKey.budgets.length > 0; if (validBudgets.length > 0) { updateData.budgets = validBudgets; updateData.calendar_aligned = data.budgetCalendarAligned; } else if (hadBudget) { updateData.budgets = []; updateData.calendar_aligned = false; } // Add rate limit if enabled const hadRateLimit = !!virtualKey.rate_limit; const hasTokenMaxLimit = data.tokenMaxLimit !== undefined; const hasRequestMaxLimit = data.requestMaxLimit !== undefined; const hasRateLimit = hasTokenMaxLimit || hasRequestMaxLimit; if (hasRateLimit) { updateData.rate_limit = { token_max_limit: data.tokenMaxLimit ?? null, token_reset_duration: hasTokenMaxLimit ? data.tokenResetDuration || "1h" : null, request_max_limit: data.requestMaxLimit ?? null, request_reset_duration: hasRequestMaxLimit ? data.requestResetDuration || "1h" : null, }; } else if (hadRateLimit) { updateData.rate_limit = {}; } await updateVirtualKey({ vkId: virtualKey.id, data: updateData }).unwrap(); toast.success("Virtual key updated successfully"); } else { // Create new virtual key const createData: CreateVirtualKeyRequest = { name: data.name, description: data.description || undefined, provider_configs: normalizedProviderConfigs, mcp_configs: data.mcpConfigs, team_id: data.entityType === "team" && data.teamId && data.teamId.trim() !== "" ? data.teamId : undefined, customer_id: data.entityType === "customer" && data.customerId && data.customerId.trim() !== "" ? data.customerId : undefined, is_active: data.isActive, }; // Add budgets if enabled const validBudgets = (data.budgets || []).filter( (b): b is { max_limit: number; reset_duration: string } => b.max_limit !== undefined, ); if (validBudgets.length > 0) { createData.budgets = validBudgets; createData.calendar_aligned = data.budgetCalendarAligned; } // Add rate limit if enabled const hasTokenMaxLimit = data.tokenMaxLimit !== undefined; const hasRequestMaxLimit = data.requestMaxLimit !== undefined; if (hasTokenMaxLimit || hasRequestMaxLimit) { createData.rate_limit = { token_max_limit: data.tokenMaxLimit, token_reset_duration: hasTokenMaxLimit ? data.tokenResetDuration || "1h" : undefined, request_max_limit: data.requestMaxLimit, request_reset_duration: hasRequestMaxLimit ? data.requestResetDuration || "1h" : undefined, }; } await createVirtualKey(createData).unwrap(); toast.success("Virtual key created successfully"); } onSave(); } catch (error) { toast.error(getErrorMessage(error)); } }; return ( !open && handleClose()}> e.preventDefault()} onEscapeKeyDown={() => handleClose()} > {isEditing ? virtualKey?.name : "Create Virtual Key"} {isEditing ? "Update the virtual key configuration and permissions." : "Create a new virtual key with specific permissions, budgets, and rate limits."}
{isManagedByProfile && ( This virtual key is managed by an access profile. Only the name and description can be modified — providers, budgets, rate limits, and MCP access are controlled by the profile. )} {isTeamLocked && !isManagedByProfile && (

This virtual key is attached to team{" "} {attachedTeam?.name ?? virtualKey?.team?.name ?? attachedTeamId} . The team assignment can't be changed here — all other fields remain editable.

)} {/* Assigned User */} {assignedUsers.length > 0 && (
{assignedUsers.map((u) => u.name || u.email).join(", ")}
)} {/* Basic Information */}
( Name * )} /> ( Description