From 825a040524dfca7b343b7427d30df5a7a0ff5f30 Mon Sep 17 00:00:00 2001 From: Achintya Rajan Date: Wed, 8 Oct 2025 17:38:42 -0700 Subject: [PATCH] moved virtual-key-refactor to its own branch --- .../components/modals/CreateUserModal.tsx | 3 +- .../app/(dashboard)/hooks/useAuthorized.ts | 1 + .../{virtual-keys => }/hooks/useTeams.tsx | 2 +- .../src/app/(dashboard)/logs/page.tsx | 2 +- .../(dashboard)/models-and-endpoints/page.tsx | 2 +- .../{virtual-keys => }/networking.ts | 0 .../settings/admin-settings/page.tsx | 2 +- .../src/app/(dashboard)/teams/page.tsx | 2 +- .../src/app/(dashboard)/usage/page.tsx | 2 +- .../src/app/(dashboard)/users/page.tsx | 2 +- .../virtual-keys/components/CreateKey.tsx | 108 --- .../CreateKeyForm/KeyDetailsSection.tsx | 150 ---- .../CreateKeyForm/OptionalSettingsSection.tsx | 460 ------------ .../CreateKeyForm/OwnershipSection.tsx | 132 ---- .../CreateKeyModal/CreateKeyModal.tsx | 235 ------ .../components/CreateKeyModal/hooks/index.ts | 5 - .../hooks/useGuardrailsAndPrompts.ts | 24 - .../hooks/useMcpAccessGroups.ts | 21 - .../CreateKeyModal/hooks/useTeamModels.ts | 26 - .../CreateKeyModal/hooks/useUserModels.ts | 21 - .../CreateKeyModal/hooks/useUserSearch.ts | 41 - .../components/CreateKeyModal/networking.ts | 84 --- .../components/CreateKeyModal/types.ts | 17 - .../components/CreateKeyModal/utils.ts | 187 ----- .../virtual-keys/components/KeyInfoView.tsx | 699 ------------------ .../virtual-keys/components/SaveKeyModal.tsx | 60 -- .../VirtualKeysTable/VirtualKeysTable.tsx | 667 ----------------- .../VirtualKeysTable/hooks/useFilterLogic.ts | 189 ----- .../src/app/(dashboard)/virtual-keys/page.tsx | 41 +- .../components/templates/view_key_table.tsx | 9 +- .../src/components/user_dashboard.tsx | 5 +- .../src/components/view_users.tsx | 2 + 32 files changed, 37 insertions(+), 3164 deletions(-) rename ui/litellm-dashboard/src/app/(dashboard)/{virtual-keys => }/hooks/useTeams.tsx (88%) rename ui/litellm-dashboard/src/app/(dashboard)/{virtual-keys => }/networking.ts (100%) delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKey.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/index.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/types.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/utils.ts delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/KeyInfoView.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/SaveKeyModal.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx index 9eb4191be1d..be3ea48bf2a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/modals/CreateUserModal.tsx @@ -29,8 +29,7 @@ import { } from "@/components/networking"; import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; import BulkCreateUsersButton from "@/components/bulk_create_users_button"; -import { fetchTeams } from "@/app/(dashboard)/virtual-keys/networking"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; // Helper function to generate UUID compatible across all environments diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts index ebb0c345926..435000f9c88 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts @@ -34,6 +34,7 @@ const useAuthorized = () => { token: token, accessToken: decoded?.key ?? null, userId: decoded?.user_id ?? null, + userEmail: decoded?.user_email ?? null, userRole: decoded?.user_role ?? null, premiumUser: decoded?.premium_user ?? null, disabledPersonalKeyCreation: decoded?.disabled_non_admin_personal_key_creation ?? null, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/hooks/useTeams.tsx b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useTeams.tsx similarity index 88% rename from ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/hooks/useTeams.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/hooks/useTeams.tsx index 7ba7a2fea53..64cbf624f9c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/hooks/useTeams.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useTeams.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { fetchTeams } from "@/app/(dashboard)/virtual-keys/networking"; import { Team } from "@/components/key_team_helpers/key_list"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { fetchTeams } from "@/app/(dashboard)/networking"; const useTeams = () => { const [teams, setTeams] = useState([]); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx index 4c6e7acc199..b04a12e2306 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx @@ -2,7 +2,7 @@ import SpendLogsTable from "@/components/view_logs"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const LogsPage = () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx index 2c523ea4039..b661e4de3e7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx @@ -2,7 +2,7 @@ import ModelDashboard from "@/components/templates/model_dashboard"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; import { useState } from "react"; const ModelsAndEndpointsPage = () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/networking.ts b/ui/litellm-dashboard/src/app/(dashboard)/networking.ts similarity index 100% rename from ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/networking.ts rename to ui/litellm-dashboard/src/app/(dashboard)/networking.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/settings/admin-settings/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/settings/admin-settings/page.tsx index a851ce766aa..c99242f0c3c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/settings/admin-settings/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/settings/admin-settings/page.tsx @@ -4,7 +4,7 @@ import AdminPanel from "@/components/admins"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { useState } from "react"; import { Team } from "@/components/key_team_helpers/key_list"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; const AdminSettings = () => { const { teams, setTeams } = useTeams(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx index f1f0460d539..42e6d250ee4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx @@ -2,7 +2,7 @@ import Teams from "@/components/teams"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; import { useEffect, useState } from "react"; import { Organization } from "@/components/networking"; import { fetchOrganizations } from "@/components/organizations"; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx index c20b13c67d9..e4b44e5a450 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx @@ -2,7 +2,7 @@ import NewUsagePage from "@/components/new_usage"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; const UsagePage = () => { const { accessToken, userRole, userId, premiumUser } = useAuthorized(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx index dd531665741..7cf401873df 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx @@ -2,7 +2,7 @@ import ViewUserDashboard from "@/components/view_users"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; import { useState } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKey.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKey.tsx deleted file mode 100644 index 82be28e4951..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKey.tsx +++ /dev/null @@ -1,108 +0,0 @@ -"use client"; - -import React, { useState, useEffect } from "react"; -import { Button } from "@tremor/react"; -import { Modal, Form } from "antd"; -import { getPossibleUserRoles, Organization } from "@/components/networking"; -import { rolesWithWriteAccess } from "@/utils/roles"; -import { Team } from "@/components/key_team_helpers/key_list"; -import CreateKeyModal from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyModal"; -import CreateUserModal from "@/app/(dashboard)/components/modals/CreateUserModal"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -interface CreateKeyProps { - team: Team | null; - userRole: string | null; - data: any[] | null; - addKey: (data: any) => void; -} - -interface User { - user_id: string; - user_email: string; - role?: string; -} - -interface UserOption { - label: string; - value: string; - user: User; -} -const CreateKey: React.FC = ({ team, userRole, data, addKey }) => { - const { accessToken } = useAuthorized(); - const [form] = Form.useForm(); - const [isModalVisible, setIsModalVisible] = useState(false); - const [isCreateUserModalVisible, setIsCreateUserModalVisible] = useState(false); - const [newlyCreatedUserId, setNewlyCreatedUserId] = useState(null); - const [possibleUIRoles, setPossibleUIRoles] = useState>>({}); - - useEffect(() => { - const fetchPossibleRoles = async () => { - try { - if (accessToken) { - // Check if roles are cached in session storage - const cachedRoles = sessionStorage.getItem("possibleUserRoles"); - if (cachedRoles) { - setPossibleUIRoles(JSON.parse(cachedRoles)); - } else { - const availableUserRoles = await getPossibleUserRoles(accessToken); - sessionStorage.setItem("possibleUserRoles", JSON.stringify(availableUserRoles)); - setPossibleUIRoles(availableUserRoles); - } - } - } catch (error) { - console.error("Error fetching possible user roles:", error); - } - }; - - fetchPossibleRoles(); - }, [accessToken]); - - // Add a callback function to handle user creation - const handleUserCreated = (userId: string) => { - setNewlyCreatedUserId(userId); - form.setFieldsValue({ user_id: userId }); - setIsCreateUserModalVisible(false); - }; - - const handleUserSelect = (_value: string, option: UserOption): void => { - const selectedUser = option.user; - form.setFieldsValue({ - user_id: selectedUser.user_id, - }); - }; - - return ( -
- {userRole && rolesWithWriteAccess.includes(userRole) && ( - - )} - - - {isCreateUserModalVisible && ( - setIsCreateUserModalVisible(false)} - footer={null} - width={800} - > - - - )} -
- ); -}; - -export default CreateKey; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx deleted file mode 100644 index 33a8ae06cf9..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection.tsx +++ /dev/null @@ -1,150 +0,0 @@ -"use client"; - -import { TextInput, Title } from "@tremor/react"; -import { Form, FormInstance, Select, Tooltip } from "antd"; -import { InfoCircleOutlined } from "@ant-design/icons"; -import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; -import React from "react"; - -export interface KeyDetailsSectionProps { - form: FormInstance; - keyOwner: string; - modelsToPick: string[]; - keyType: string; - setKeyType: (keyType: string) => void; -} - -const { Option } = Select; - -const KeyDetailsSection = ({ form, keyOwner, keyType, modelsToPick, setKeyType }: KeyDetailsSectionProps) => { - return ( -
- Key Details - - {keyOwner === "you" || keyOwner === "another_user" ? "Key Name" : "Service Account ID"}{" "} - - - - - } - name="key_alias" - rules={[ - { - required: true, - message: `Please input a ${keyOwner === "you" ? "key name" : "service account ID"}`, - }, - ]} - help="required" - > - - - - - Models{" "} - - - - - } - name="models" - rules={ - keyType === "management" || keyType === "read_only" - ? [] - : [{ required: true, message: "Please select a model" }] - } - help={ - keyType === "management" || keyType === "read_only" - ? "Models field is disabled for this key type" - : "required" - } - className="mt-4" - > - - - - - Key Type{" "} - - - - - } - name="key_type" - initialValue="default" - className="mt-4" - > - - -
- ); -}; - -export default KeyDetailsSection; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx deleted file mode 100644 index b88f8919448..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection.tsx +++ /dev/null @@ -1,460 +0,0 @@ -"use client"; - -import { Accordion, AccordionBody, AccordionHeader, Text, Title } from "@tremor/react"; -import { Form, FormInstance, Input, Select, Tooltip } from "antd"; -import { InfoCircleOutlined } from "@ant-design/icons"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; -import NumericalInput from "@/components/shared/numerical_input"; -import BudgetDurationDropdown from "@/components/common_components/budget_duration_dropdown"; -import RateLimitTypeFormItem from "@/components/common_components/RateLimitTypeFormItem"; -import VectorStoreSelector from "@/components/vector_store_management/VectorStoreSelector"; -import MCPServerSelector from "@/components/mcp_server_management/MCPServerSelector"; -import PremiumLoggingSettings from "@/components/common_components/PremiumLoggingSettings"; -import ModelAliasManager from "@/components/common_components/ModelAliasManager"; -import KeyLifecycleSettings from "@/components/common_components/KeyLifecycleSettings"; -import { proxyBaseUrl } from "@/components/networking"; -import SchemaFormFields from "@/components/common_components/check_openapi_schema"; -import { Team } from "@/components/key_team_helpers/key_list"; -import React from "react"; -import { DefaultOptionType } from "rc-select/lib/Select"; -import { ModelAliases } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types"; - -export interface OptionalSettingsSectionProps { - form: FormInstance; - team: Team | null; - premiumUser: boolean; - guardrails: string[]; - prompts: string[]; - accessToken: string; - predefinedTags: DefaultOptionType[]; - loggingSettings: any; - setLoggingSettings: (settings: any) => void; - disabledCallbacks: string[]; - setDisabledCallbacks: (disabledCallbacks: string[]) => void; - modelAliases: ModelAliases; - setModelAliases: (aliases: ModelAliases) => void; - autoRotationEnabled: boolean; - setAutoRotationEnabled: (enabled: boolean) => void; - rotationInterval: string; - setRotationInterval: (interval: string) => void; -} - -// TODO: break apart this component into smaller sections -const OptionalSettingsSection = ({ - form, - team, - premiumUser, - guardrails, - prompts, - accessToken, - predefinedTags, - loggingSettings, - setLoggingSettings, - disabledCallbacks, - setDisabledCallbacks, - modelAliases, - setModelAliases, - autoRotationEnabled, - setAutoRotationEnabled, - rotationInterval, - setRotationInterval, -}: OptionalSettingsSectionProps) => { - return ( -
- - - Optional Settings - - - - Max Budget (USD){" "} - - - - - } - name="max_budget" - help={`Budget cannot exceed team max budget: $${team?.max_budget !== null && team?.max_budget !== undefined ? team?.max_budget : "unlimited"}`} - rules={[ - { - validator: async (_, value) => { - if (value && team && team.max_budget !== null && value > team.max_budget) { - throw new Error( - `Budget cannot exceed team max budget: $${formatNumberWithCommas(team.max_budget, 4)}`, - ); - } - }, - }, - ]} - > - - - - Reset Budget{" "} - - - - - } - name="budget_duration" - help={`Team Reset Budget: ${team?.budget_duration !== null && team?.budget_duration !== undefined ? team?.budget_duration : "None"}`} - > - form.setFieldValue("budget_duration", value)} /> - - - Tokens per minute Limit (TPM){" "} - - - - - } - name="tpm_limit" - help={`TPM cannot exceed team TPM limit: ${team?.tpm_limit !== null && team?.tpm_limit !== undefined ? team?.tpm_limit : "unlimited"}`} - rules={[ - { - validator: async (_, value) => { - if (value && team && team.tpm_limit !== null && value > team.tpm_limit) { - throw new Error(`TPM limit cannot exceed team TPM limit: ${team.tpm_limit}`); - } - }, - }, - ]} - > - - - - - Requests per minute Limit (RPM){" "} - - - - - } - name="rpm_limit" - help={`RPM cannot exceed team RPM limit: ${team?.rpm_limit !== null && team?.rpm_limit !== undefined ? team?.rpm_limit : "unlimited"}`} - rules={[ - { - validator: async (_, value) => { - if (value && team && team.rpm_limit !== null && value > team.rpm_limit) { - throw new Error(`RPM limit cannot exceed team RPM limit: ${team.rpm_limit}`); - } - }, - }, - ]} - > - - - - - Guardrails{" "} - - e.stopPropagation()} // Prevent accordion from collapsing when clicking link - > - - - - - } - name="guardrails" - className="mt-4" - help={ - premiumUser - ? "Select existing guardrails or enter new ones" - : "Premium feature - Upgrade to set guardrails by key" - } - > - ({ value: name, label: name }))} - /> - - - Allowed Vector Stores{" "} - - - - - } - name="allowed_vector_store_ids" - className="mt-4" - help="Select vector stores this key can access. Leave empty for access to all vector stores" - > - form.setFieldValue("allowed_vector_store_ids", values)} - value={form.getFieldValue("allowed_vector_store_ids")} - accessToken={accessToken} - placeholder="Select vector stores (optional)" - /> - - - - Allowed MCP Servers{" "} - - - - - } - name="allowed_mcp_servers_and_groups" - className="mt-4" - help="Select MCP servers or access groups this key can access. " - > - form.setFieldValue("allowed_mcp_servers_and_groups", val)} - value={form.getFieldValue("allowed_mcp_servers_and_groups")} - accessToken={accessToken} - placeholder="Select MCP servers or access groups (optional)" - /> - - - - Metadata{" "} - - - - - } - name="metadata" - className="mt-4" - > - - - - Tags{" "} - - - - - } - name="tags" - className="mt-4" - help={`Tags for tracking spend and/or doing tag-based routing.`} - > - handleUserSelect(value, option as UserOption)} - options={userOptions} - loading={userSearchLoading} - allowClear - style={{ width: "100%" }} - notFoundContent={userSearchLoading ? "Searching..." : "No users found"} - /> - setIsCreateUserModalVisible(true)} style={{ marginLeft: "8px" }}> - Create User - -
-
Search by email to find users
- - - )} - - Team{" "} - - - - - } - name="team_id" - initialValue={team ? team.team_id : null} - className="mt-4" - rules={[ - { - required: keyOwner === "service_account", - message: "Please select a team for the service account", - }, - ]} - help={keyOwner === "service_account" ? "required" : ""} - > - { - const selectedTeam = teams?.find((t) => t.team_id === teamId) || null; - setSelectedCreateKeyTeam(selectedTeam); - }} - /> - - - ); -}; - -export default OwnershipSection; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx deleted file mode 100644 index cbe3db4e8ff..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyModal.tsx +++ /dev/null @@ -1,235 +0,0 @@ -"use client"; - -import { Button as Button2, Form, FormInstance, Modal } from "antd"; -import { Text } from "@tremor/react"; -import { keyCreateCall, keyCreateServiceAccountCall } from "@/components/networking"; -import React, { useEffect, useState } from "react"; -import { Team } from "@/components/key_team_helpers/key_list"; -import SaveKeyModal from "@/app/(dashboard)/virtual-keys/components/SaveKeyModal"; -import NotificationsManager from "@/components/molecules/notifications_manager"; -import { - useGuardrailsAndPrompts, - useMcpAccessGroups, - useTeamModels, - useUserModels, - useUserSearch, -} from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks"; -import OwnershipSection from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection"; -import KeyDetailsSection from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection"; -import OptionalSettingsSection from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection"; -import { ModelAliases } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types"; -import { getPredefinedTags, prepareFormValues } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/utils"; -import { fetchTeams } from "@/app/(dashboard)/virtual-keys/networking"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -export interface CreateKeyModalProps { - isModalVisible: boolean; - setIsModalVisible: (isModalVisible: boolean) => void; - form: FormInstance; - handleUserSelect: (_value: string, option: UserOption) => void; - setIsCreateUserModalVisible: (visible: boolean) => void; - team: Team | null; - data: any[] | null; - addKey: (data: any) => void; -} - -interface User { - user_id: string; - user_email: string; - role?: string; -} - -interface UserOption { - label: string; - value: string; - user: User; -} - -const CreateKeyModal = ({ - isModalVisible, - setIsModalVisible, - form, - handleUserSelect, - setIsCreateUserModalVisible, - team, - data, - addKey, -}: CreateKeyModalProps) => { - const { userId: userID, userRole, accessToken, premiumUser } = useAuthorized(); - const [apiKey, setApiKey] = useState(null); - const [keyOwner, setKeyOwner] = useState("you"); - const [keyType, setKeyType] = useState("default"); - - const [selectedCreateKeyTeam, setSelectedCreateKeyTeam] = useState(team); - - const [softBudget, setSoftBudget] = useState(null); - const [loggingSettings, setLoggingSettings] = useState([]); - const [disabledCallbacks, setDisabledCallbacks] = useState([]); - const [autoRotationEnabled, setAutoRotationEnabled] = useState(false); - const [rotationInterval, setRotationInterval] = useState("30d"); - const [modelAliases, setModelAliases] = useState({}); - const predefinedTags = getPredefinedTags(data); - - const { options: userOptions, loading: userSearchLoading, onSearch: handleUserSearch } = useUserSearch(); - const { guardrails, prompts } = useGuardrailsAndPrompts(); - const teams = useTeams(); - const mcpAccessGroups = useMcpAccessGroups(); - const userModels = useUserModels(); - const modelsToPick = useTeamModels(selectedCreateKeyTeam); - - const isTeamSelectionRequired = modelsToPick.includes("no-default-models"); - const isFormDisabled = isTeamSelectionRequired && !selectedCreateKeyTeam; - - const handleCancel = () => { - setIsModalVisible(false); - setApiKey(null); - setSelectedCreateKeyTeam(null); - form.resetFields(); - setLoggingSettings([]); - setDisabledCallbacks([]); - setKeyType("default"); - setModelAliases({}); - setAutoRotationEnabled(false); - setRotationInterval("30d"); - }; - - const handleOk = () => { - setIsModalVisible(false); - form.resetFields(); - setLoggingSettings([]); - setDisabledCallbacks([]); - setKeyType("default"); - setModelAliases({}); - setAutoRotationEnabled(false); - setRotationInterval("30d"); - }; - - const handleCreate = async (formValues: Record) => { - try { - const newKeyAlias = formValues?.key_alias ?? ""; - const newKeyTeamId = formValues?.team_id ?? null; - - const existingKeyAliases = data?.filter((k) => k.team_id === newKeyTeamId).map((k) => k.key_alias) ?? []; - - if (existingKeyAliases.includes(newKeyAlias)) { - throw new Error( - `Key alias ${newKeyAlias} already exists for team with ID ${newKeyTeamId}, please provide another key alias`, - ); - } - - NotificationsManager.info("Making API Call"); - setIsModalVisible(true); - - const prepared = prepareFormValues(formValues, { - keyOwner, - userID, - loggingSettings, - disabledCallbacks, - autoRotationEnabled, - rotationInterval, - modelAliases, - }); - - let response; - if (keyOwner === "service_account") { - response = await keyCreateServiceAccountCall(accessToken, prepared); - } else { - response = await keyCreateCall(accessToken, userID, prepared); - } - - // TODO: change logic to trigger API call in parent component - addKey(response); - - setApiKey(response["key"]); - setSoftBudget(response["soft_budget"]); - NotificationsManager.success("API Key Created"); - form.resetFields(); - localStorage.removeItem("userData" + userID); - } catch (error) { - console.log("error in create key:", error); - NotificationsManager.fromBackend(`Error creating the key: ${error}`); - } - }; - - useEffect(() => { - form.setFieldValue("models", []); - }, [selectedCreateKeyTeam, accessToken, userID, userRole, form]); - - return ( -
- -
- - - {/* Show message when team selection is required */} - {isFormDisabled && ( -
- - Please select a team to continue configuring your API key. If you do not see any teams, please contact - your Proxy Admin to either provide you with access to models or to add you to a team. - -
- )} - - {/* Section 2: Key Details */} - {!isFormDisabled && ( - - )} - - {/* Section 3: Optional Settings */} - {!isFormDisabled && ( - - )} - -
- - Create Key - -
- -
- {apiKey && ( - - )} -
- ); -}; - -export default CreateKeyModal; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/index.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/index.ts deleted file mode 100644 index ca3c4a663b9..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./useGuardrailsAndPrompts"; -export * from "./useMcpAccessGroups"; -export * from "./useUserModels"; -export * from "./useTeamModels"; -export * from "./useUserSearch"; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts deleted file mode 100644 index f4718e8ba05..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useGuardrailsAndPrompts.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useEffect, useState } from "react"; -import { fetchGuardrails, fetchPrompts } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -export const useGuardrailsAndPrompts = () => { - const [guardrails, setGuardrails] = useState([]); - const [prompts, setPrompts] = useState([]); - const { accessToken } = useAuthorized(); - - useEffect(() => { - if (!accessToken) { - setGuardrails([]); - setPrompts([]); - return; - } - (async () => { - const [g, p] = await Promise.all([fetchGuardrails(accessToken), fetchPrompts(accessToken)]); - setGuardrails(g || []); - setPrompts(p || []); - })(); - }, [accessToken]); - - return { guardrails, prompts }; -}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts deleted file mode 100644 index 998b392f774..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useMcpAccessGroups.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useEffect, useState } from "react"; -import { getMCPAccessGroups } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -export const useMcpAccessGroups = () => { - const [mcpAccessGroups, setMcpAccessGroups] = useState([]); - const { accessToken } = useAuthorized(); - - useEffect(() => { - if (!accessToken) { - setMcpAccessGroups([]); - return; - } - (async () => { - const groups = await getMCPAccessGroups(accessToken); - setMcpAccessGroups(groups || []); - })(); - }, [accessToken]); - - return mcpAccessGroups; -}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts deleted file mode 100644 index dd99de0615b..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useTeamModels.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import type { Team } from "@/components/key_team_helpers/key_list"; -import { fetchTeamModels } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -export const useTeamModels = (selectedTeam: Team | null) => { - const { userId: userID, userRole, accessToken } = useAuthorized(); - const [modelsToPick, setModelsToPick] = useState([]); - - // turn array into a stable dep so it only re-runs when contents change - const selectedTeamModelsKey = useMemo(() => (selectedTeam?.models ?? []).join("|"), [selectedTeam?.models]); - - useEffect(() => { - if (!userID || !userRole || !accessToken) { - setModelsToPick([]); - return; - } - (async () => { - const fetched = await fetchTeamModels(userID, userRole, accessToken, selectedTeam?.team_id ?? null); - const union = Array.from(new Set([...(selectedTeam?.models ?? []), ...(fetched || [])])); - setModelsToPick(union); - })(); - }, [userID, userRole, accessToken, selectedTeam?.team_id, selectedTeamModelsKey, selectedTeam?.models]); - - return modelsToPick; -}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts deleted file mode 100644 index 66b5520f067..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserModels.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useEffect, useState } from "react"; -import { getUserModelNames } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -export const useUserModels = () => { - const { userId: userID, userRole, accessToken } = useAuthorized(); - const [userModels, setUserModels] = useState([]); - - useEffect(() => { - if (!userID || !userRole || !accessToken) { - setUserModels([]); - return; - } - (async () => { - const modelNames = await getUserModelNames(userID, userRole, accessToken); - setUserModels(modelNames || []); - })(); - }, [userID, userRole, accessToken]); - - return userModels; -}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts deleted file mode 100644 index d266a51667c..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks/useUserSearch.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { debounce } from "lodash"; -import { searchUserOptionsByEmail } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -type User = { user_id: string; user_email: string; role?: string }; -export interface UserOption { - label: string; - value: string; - user: User; -} - -export const useUserSearch = (delay = 300) => { - const { accessToken } = useAuthorized(); - const [options, setOptions] = useState([]); - const [loading, setLoading] = useState(false); - - const doSearch = async (raw: string) => { - const q = raw?.trim(); - if (!q || !accessToken) { - setOptions([]); - setLoading(false); - return; - } - setLoading(true); - try { - const res = await searchUserOptionsByEmail(accessToken, q); - setOptions(res || []); - } finally { - setLoading(false); - } - }; - - const onSearch = useMemo(() => debounce(doSearch, delay), [accessToken, delay]); - - useEffect(() => { - return () => onSearch.cancel(); - }, [onSearch]); - - return { options, loading, onSearch }; -}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking.ts deleted file mode 100644 index 377f6d31139..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - fetchMCPAccessGroups, - getGuardrailsList, - getPromptsList, - modelAvailableCall, - User, - userFilterUICall, -} from "@/components/networking"; -import { ModelAvailableResponse, UserOption } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types"; - -export const fetchGuardrails = async (accessToken: string) => { - try { - const response = await getGuardrailsList(accessToken); - return response.guardrails.map((g: { guardrail_name: string }) => g.guardrail_name); - } catch (error) { - console.error("Failed to fetch guardrails:", error); - return []; - } -}; - -export const fetchPrompts = async (accessToken: string) => { - try { - const response = await getPromptsList(accessToken); - return response.prompts.map((prompt) => prompt.prompt_id); - } catch (error) { - console.error("Failed to fetch prompts:", error); - return []; - } -}; - -export const searchUserOptionsByEmail = async (accessToken: string, emailQuery: string): Promise => { - const params = new URLSearchParams(); - params.append("user_email", emailQuery); - - const response = await userFilterUICall(accessToken, params); - const users: User[] = response; - - return users.map((user) => ({ - label: `${user.user_email} (${user.user_id})`, - value: user.user_id, - user, - })); -}; - -export const getUserModelNames = async (userID: string, userRole: string, accessToken: string): Promise => { - const res: ModelAvailableResponse = await modelAvailableCall(accessToken, userID, userRole); - return (res?.data ?? []).map((m) => m.id); -}; - -export const fetchTeamModels = async ( - userID: string, - userRole: string, - accessToken: string, - teamID: string | null, -): Promise => { - try { - if (userID === null || userRole === null) { - return []; - } - - if (accessToken !== null) { - const model_available = await modelAvailableCall(accessToken, userID, userRole, true, teamID, true); - let available_model_names = model_available["data"].map((element: { id: string }) => element.id); - console.log("available_model_names:", available_model_names); - return available_model_names; - } - return []; - } catch (error) { - console.error("Error fetching user models:", error); - return []; - } -}; - -export const getMCPAccessGroups = async (accessToken: string): Promise => { - try { - if (accessToken == null) { - return []; - } - return await fetchMCPAccessGroups(accessToken); - } catch (error) { - console.error("Failed to fetch MCP access groups:", error); - return []; - } -}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/types.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/types.ts deleted file mode 100644 index 4d48df82d44..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface User { - user_id: string; - user_email: string; - role?: string; -} - -export interface UserOption { - label: string; - value: string; - user: User; -} - -export type Model = { id: string }; - -export type ModelAvailableResponse = { data: Model[] }; - -export type ModelAliases = { [key: string]: string }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/utils.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/utils.ts deleted file mode 100644 index 1fb40b87895..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/CreateKeyModal/utils.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { mapDisplayToInternalNames } from "@/components/callback_info_helpers"; -import { ModelAliases } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types"; - -export const getPredefinedTags = (data: any[] | null) => { - let allTags = []; - - console.log("data:", JSON.stringify(data)); - - if (data) { - for (let key of data) { - if (key["metadata"] && key["metadata"]["tags"]) { - allTags.push(...key["metadata"]["tags"]); - } - } - } - - const uniqueTags = Array.from(new Set(allTags)).map((tag) => ({ - value: tag, - label: tag, - })); - - console.log("uniqueTags:", uniqueTags); - return uniqueTags; -}; - -type AnyObj = Record; - -type PrepareOptions = { - keyOwner: string; - userID: string; - loggingSettings: any[]; - disabledCallbacks: string[]; - autoRotationEnabled: boolean; - rotationInterval: string; - modelAliases: ModelAliases; -}; - -export function safeParseMetadata(metadataStr: string | undefined): AnyObj { - try { - return JSON.parse(metadataStr || "{}"); - } catch (error) { - console.error("Error parsing metadata:", error); - return {}; - } -} - -export function withOwnerAssignments( - values: AnyObj, - { keyOwner, userID }: Pick, -): AnyObj { - // If owned by "you", set user_id - const base = keyOwner === "you" ? { ...values, user_id: userID } : { ...values }; - return base; -} - -export function enrichMetadata( - metadata: AnyObj, - values: AnyObj, - { - keyOwner, - loggingSettings, - disabledCallbacks, - }: Pick, -): AnyObj { - let next = { ...metadata }; - - // If it's a service account, add the service_account_id to the metadata - if (keyOwner === "service_account") { - next = { ...next, service_account_id: values.key_alias }; - } - - // Add logging settings to the metadata - if (Array.isArray(loggingSettings) && loggingSettings.length > 0) { - next = { ...next, logging: loggingSettings.filter((config: any) => config?.callback_name) }; - } - - // Add disabled callbacks to the metadata - if (Array.isArray(disabledCallbacks) && disabledCallbacks.length > 0) { - const mappedDisabledCallbacks = mapDisplayToInternalNames(disabledCallbacks); - next = { ...next, litellm_disabled_callbacks: mappedDisabledCallbacks }; - } - - return next; -} - -export function withAutoRotation( - values: AnyObj, - { autoRotationEnabled, rotationInterval }: Pick, -): AnyObj { - if (!autoRotationEnabled) return { ...values }; - return { ...values, auto_rotate: true, rotation_interval: rotationInterval }; -} - -export function withDurationNoop(values: AnyObj): AnyObj { - // Preserve the original no-op logic: if duration exists, set it to itself - if (values.duration) { - return { ...values, duration: values.duration }; - } - return { ...values }; -} - -export function mergeObjectPermission(values: AnyObj, patch: AnyObj): AnyObj { - const object_permission = { ...(values.object_permission || {}), ...patch }; - return { ...values, object_permission }; -} - -export function withVectorStores(values: AnyObj): AnyObj { - const { allowed_vector_store_ids, ...rest } = values; - if (Array.isArray(allowed_vector_store_ids) && allowed_vector_store_ids.length > 0) { - const patched = mergeObjectPermission(rest, { vector_stores: allowed_vector_store_ids }); - return patched; // original field removed by destructuring - } - return { ...values }; -} - -export function withMcpServersAndGroups(values: AnyObj): AnyObj { - const { allowed_mcp_servers_and_groups, ...rest } = values; - const servers = allowed_mcp_servers_and_groups?.servers; - const accessGroups = allowed_mcp_servers_and_groups?.accessGroups; - - const hasServers = Array.isArray(servers) && servers.length > 0; - const hasAccessGroups = Array.isArray(accessGroups) && accessGroups.length > 0; - - if (!hasServers && !hasAccessGroups) return { ...values }; - - let patched = { ...rest }; - if (hasServers) patched = mergeObjectPermission(patched, { mcp_servers: servers }); - if (hasAccessGroups) patched = mergeObjectPermission(patched, { mcp_access_groups: accessGroups }); - - // original field removed by destructuring - return patched; -} - -export function withMcpAccessGroups(values: AnyObj): AnyObj { - const { allowed_mcp_access_groups, ...rest } = values; - if (Array.isArray(allowed_mcp_access_groups) && allowed_mcp_access_groups.length > 0) { - const patched = mergeObjectPermission(rest, { mcp_access_groups: allowed_mcp_access_groups }); - return patched; // original field removed by destructuring - } - return { ...values }; -} - -function withAliases(values: AnyObj, modelAliases: ModelAliases): AnyObj { - if (modelAliases && Object.keys(modelAliases).length > 0) { - return { ...values, aliases: JSON.stringify(modelAliases) }; - } - return { ...values }; -} - -function withMetadataString(values: AnyObj, metadata: AnyObj): AnyObj { - return { ...values, metadata: JSON.stringify(metadata) }; -} - -export function prepareFormValues(initialValues: AnyObj, opts: PrepareOptions): AnyObj { - // Step 1: assign user id if needed - let values = withOwnerAssignments(initialValues, { keyOwner: opts.keyOwner, userID: opts.userID }); - - // Step 2: metadata parse & enrichment - const parsedMetadata = safeParseMetadata(values.metadata); - const enrichedMetadata = enrichMetadata(parsedMetadata, values, { - keyOwner: opts.keyOwner, - loggingSettings: opts.loggingSettings, - disabledCallbacks: opts.disabledCallbacks, - }); - - // Step 3: auto-rotation - values = withAutoRotation(values, { - autoRotationEnabled: opts.autoRotationEnabled, - rotationInterval: opts.rotationInterval, - }); - - // Step 4: duration (no-op preservation) - values = withDurationNoop(values); - - // Step 5: commit metadata string - values = withMetadataString(values, enrichedMetadata); - - // Step 6: object_permission transformations - values = withVectorStores(values); - values = withMcpServersAndGroups(values); - values = withMcpAccessGroups(values); - - // Step 7: aliases - values = withAliases(values, opts.modelAliases); - - return values; -} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/KeyInfoView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/KeyInfoView.tsx deleted file mode 100644 index 9f9a47fbe10..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/KeyInfoView.tsx +++ /dev/null @@ -1,699 +0,0 @@ -"use client"; - -import React, { useEffect, useState } from "react"; -import { - Card, - Text, - Button, - Grid, - Tab, - TabList, - TabGroup, - TabPanel, - TabPanels, - Title, - Badge, -} from "@tremor/react"; -import { ArrowLeftIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline"; -import { Form, Tooltip, Button as AntdButton } from "antd"; -import { copyToClipboard as utilCopyToClipboard, formatNumberWithCommas } from "@/utils/dataUtils"; -import { CopyIcon, CheckIcon } from "lucide-react"; -import { KeyResponse } from "@/components/key_team_helpers/key_list" -import { mapDisplayToInternalNames, mapInternalToDisplayNames } from "@/components/callback_info_helpers" -import NotificationManager from "@/components/molecules/notifications_manager" -import { keyDeleteCall, keyUpdateCall } from "@/components/networking" -import { parseErrorMessage } from "@/components/shared/errorUtils" -import { rolesWithWriteAccess } from "@/utils/roles" -import { RegenerateKeyModal } from "@/components/organisms/regenerate_key_modal" -import ObjectPermissionsView from "@/components/object_permissions_view" -import LoggingSettingsView from "@/components/logging_settings_view" -import { extractLoggingSettings, formatMetadataForDisplay } from "@/components/key_info_utils" -import AutoRotationView from "@/components/common_components/AutoRotationView" -import { KeyEditView } from "@/components/templates/key_edit_view" - - -interface KeyInfoViewProps { - keyId: string; - onClose: () => void; - keyData: KeyResponse | undefined; - onKeyDataUpdate?: (data: Partial) => void; - onDelete?: () => void; - accessToken: string | null; - userID: string | null; - userRole: string | null; - teams: any[] | null; - premiumUser: boolean; - setAccessToken?: (token: string) => void; - backButtonText?: string; -} - -const KeyInfoView = ({ - keyId, - onClose, - keyData, - accessToken, - userID, - userRole, - teams, - onKeyDataUpdate, - onDelete, - premiumUser, - setAccessToken, - backButtonText = "Back to Keys", - }: KeyInfoViewProps) => { - const [isEditing, setIsEditing] = useState(false); - const [form] = Form.useForm(); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [deleteConfirmInput, setDeleteConfirmInput] = useState(""); - const [isRegenerateModalOpen, setIsRegenerateModalOpen] = useState(false); - const [copiedStates, setCopiedStates] = useState>({}); - - // Add local state to maintain key data and track regeneration - const [currentKeyData, setCurrentKeyData] = useState(keyData); - const [lastRegeneratedAt, setLastRegeneratedAt] = useState(null); - const [isRecentlyRegenerated, setIsRecentlyRegenerated] = useState(false); - - // Update local state when keyData prop changes (but don't reset to undefined) - useEffect(() => { - if (keyData) { - setCurrentKeyData(keyData); - } - }, [keyData]); - - // Reset recent regeneration indicator after 5 seconds - useEffect(() => { - if (isRecentlyRegenerated) { - const timer = setTimeout(() => { - setIsRecentlyRegenerated(false); - }, 5000); - return () => clearTimeout(timer); - } - }, [isRecentlyRegenerated]); - - // Use currentKeyData instead of keyData throughout the component - if (!currentKeyData) { - return ( -
- - Key not found -
- ); - } - - const handleKeyUpdate = async (formValues: Record) => { - try { - if (!accessToken) return; - - const currentKey = formValues.token; - formValues.key = currentKey; - - // Guard premium features - if (!premiumUser) { - delete formValues.guardrails; - delete formValues.prompts; - } - - // Handle object_permission updates - if (formValues.vector_stores !== undefined) { - formValues.object_permission = { - ...currentKeyData.object_permission, - vector_stores: formValues.vector_stores || [], - }; - // Remove vector_stores from the top level as it should be in object_permission - delete formValues.vector_stores; - } - - if (formValues.mcp_servers_and_groups !== undefined) { - const { servers, accessGroups } = formValues.mcp_servers_and_groups || { servers: [], accessGroups: [] }; - formValues.object_permission = { - ...currentKeyData.object_permission, - mcp_servers: servers || [], - mcp_access_groups: accessGroups || [], - }; - // Remove mcp_servers_and_groups from the top level as it should be in object_permission - delete formValues.mcp_servers_and_groups; - } - - // Convert metadata back to an object if it exists and is a string - if (formValues.metadata && typeof formValues.metadata === "string") { - try { - const parsedMetadata = JSON.parse(formValues.metadata); - formValues.metadata = { - ...parsedMetadata, - ...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}), - ...(formValues.logging_settings ? { logging: formValues.logging_settings } : {}), - ...(formValues.disabled_callbacks?.length > 0 - ? { - litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks), - } - : {}), - }; - } catch (error) { - console.error("Error parsing metadata JSON:", error); - NotificationManager.error("Invalid metadata JSON"); - return; - } - } else { - formValues.metadata = { - ...(formValues.metadata || {}), - ...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}), - ...(formValues.logging_settings ? { logging: formValues.logging_settings } : {}), - ...(formValues.disabled_callbacks?.length > 0 - ? { - litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks), - } - : {}), - }; - } - - delete formValues.logging_settings; - - // Convert budget_duration to API format - if (formValues.budget_duration) { - const durationMap: Record = { - daily: "24h", - weekly: "7d", - monthly: "30d", - }; - formValues.budget_duration = durationMap[formValues.budget_duration]; - } - - const newKeyValues = await keyUpdateCall(accessToken, formValues); - - // Update local state - setCurrentKeyData((prevData) => (prevData ? { ...prevData, ...newKeyValues } : undefined)); - - if (onKeyDataUpdate) { - onKeyDataUpdate(newKeyValues); - } - NotificationManager.success("Key updated successfully"); - setIsEditing(false); - // Refresh key data here if needed - } catch (error) { - NotificationManager.fromBackend(parseErrorMessage(error)); - console.error("Error updating key:", error); - } - }; - - const handleDelete = async () => { - try { - if (!accessToken) return; - await keyDeleteCall(accessToken as string, currentKeyData.token || currentKeyData.token_id); - NotificationManager.success("Key deleted successfully"); - if (onDelete) { - onDelete(); - } - onClose(); - } catch (error) { - console.error("Error deleting the key:", error); - NotificationManager.fromBackend(error); - } - // Reset the confirmation input - setDeleteConfirmInput(""); - }; - - const copyToClipboard = async (text: string, key: string) => { - const success = await utilCopyToClipboard(text); - if (success) { - setCopiedStates((prev) => ({ ...prev, [key]: true })); - setTimeout(() => { - setCopiedStates((prev) => ({ ...prev, [key]: false })); - }, 2000); - } - }; - - const handleRegenerateKeyUpdate = (updatedKeyData: Partial) => { - // Update local state immediately with ALL the new data - setCurrentKeyData((prevData) => { - if (!prevData) return undefined; - const newData = { - ...prevData, - ...updatedKeyData, // This should include the new token (key-id) - // Update the created_at to show when it was regenerated - created_at: new Date().toLocaleString(), - }; - return newData; - }); - - // Track regeneration timestamp - setLastRegeneratedAt(new Date()); - setIsRecentlyRegenerated(true); - - if (onKeyDataUpdate) { - onKeyDataUpdate({ - ...updatedKeyData, - created_at: new Date().toLocaleString(), - }); - } - }; - - // Update the formatTimestamp function to use the desired date format - const formatTimestamp = (timestamp: string | Date) => { - const date = new Date(timestamp); - const dateStr = date.toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); - const timeStr = date.toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - hour12: true, - }); - return `${dateStr} at ${timeStr}`; - }; - - return ( -
-
-
- - {currentKeyData.key_alias || "API Key"} - -
-
- Key ID - {currentKeyData.token_id || currentKeyData.token} -
- : } - onClick={() => copyToClipboard(currentKeyData.token_id || currentKeyData.token, "key-id")} - className={`ml-2 transition-all duration-200${ - copiedStates["key-id"] - ? "text-green-600 bg-green-50 border-green-200" - : "text-gray-500 hover:text-gray-700 hover:bg-gray-100" - }`} - /> -
- - {/* Add timestamp and regeneration indicator */} -
- - {currentKeyData.updated_at && currentKeyData.updated_at !== currentKeyData.created_at - ? `Updated: ${formatTimestamp(currentKeyData.updated_at)}` - : `Created: ${formatTimestamp(currentKeyData.created_at)}`} - - - {isRecentlyRegenerated && ( - - Recently Regenerated - - )} - - {lastRegeneratedAt && ( - - Regenerated - - )} -
-
- {userRole && rolesWithWriteAccess.includes(userRole) && ( -
- - - - - - -
- )} -
- - {/* Add RegenerateKeyModal */} - setIsRegenerateModalOpen(false)} - accessToken={accessToken} - premiumUser={premiumUser} - setAccessToken={setAccessToken} - onKeyUpdate={handleRegenerateKeyUpdate} - /> - - {/* Delete Confirmation Modal */} - {isDeleteModalOpen && - (() => { - const keyName = currentKeyData?.key_alias || currentKeyData?.token_id || "API Key"; - const isValid = deleteConfirmInput === keyName; - return ( -
-
-
-
-

Delete Key

- -
-
-
-
- - - -
-
-

- Warning: You are about to delete this API key. -

-

- This action is irreversible and will immediately revoke access for any applications using this - key. -

-
-
-

Are you sure you want to delete this API key?

-
- - setDeleteConfirmInput(e.target.value)} - placeholder="Enter key name exactly" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-base" - autoFocus - /> -
-
-
-
- - -
-
-
- ); - })()} - - - - Overview - Settings - - - - {/* Overview Panel */} - - - - Spend -
- ${formatNumberWithCommas(currentKeyData.spend, 4)} - - of{" "} - {currentKeyData.max_budget !== null - ? `$${formatNumberWithCommas(currentKeyData.max_budget)}` - : "Unlimited"} - -
-
- - - Rate Limits -
- TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"} - RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.rpm_limit : "Unlimited"} -
-
- - - Models -
- {currentKeyData.models && currentKeyData.models.length > 0 ? ( - currentKeyData.models.map((model, index) => ( - - {model} - - )) - ) : ( - No models specified - )} -
-
- - - - - - - - -
-
- - {/* Settings Panel */} - - -
- Key Settings - {!isEditing && userRole && rolesWithWriteAccess.includes(userRole) && ( - - )} -
- - {isEditing ? ( - setIsEditing(false)} - onSubmit={handleKeyUpdate} - teams={teams} - accessToken={accessToken} - userID={userID} - userRole={userRole} - premiumUser={premiumUser} - /> - ) : ( -
-
- Key ID - {currentKeyData.token_id || currentKeyData.token} -
- -
- Key Alias - {currentKeyData.key_alias || "Not Set"} -
- -
- Secret Key - {currentKeyData.key_name} -
- -
- Team ID - {currentKeyData.team_id || "Not Set"} -
- -
- Organization - {currentKeyData.organization_id || "Not Set"} -
- -
- Created - {formatTimestamp(currentKeyData.created_at)} -
- - {lastRegeneratedAt && ( -
- Last Regenerated -
- {formatTimestamp(lastRegeneratedAt)} - - Recent - -
-
- )} - -
- Expires - {currentKeyData.expires ? formatTimestamp(currentKeyData.expires) : "Never"} -
- - - -
- Spend - ${formatNumberWithCommas(currentKeyData.spend, 4)} USD -
- -
- Budget - - {currentKeyData.max_budget !== null - ? `$${formatNumberWithCommas(currentKeyData.max_budget, 2)}` - : "Unlimited"} - -
- -
- Prompts - - {Array.isArray(currentKeyData.metadata?.prompts) && currentKeyData.metadata.prompts.length > 0 - ? currentKeyData.metadata.prompts.map((prompt, index) => ( - - {prompt} - - )) - : "No prompts specified"} - -
- -
- Models -
- {currentKeyData.models && currentKeyData.models.length > 0 ? ( - currentKeyData.models.map((model, index) => ( - - {model} - - )) - ) : ( - No models specified - )} -
-
- -
- Rate Limits - TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"} - RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.rpm_limit : "Unlimited"} - - Max Parallel Requests:{" "} - {currentKeyData.max_parallel_requests !== null - ? currentKeyData.max_parallel_requests - : "Unlimited"} - - - Model TPM Limits:{" "} - {currentKeyData.metadata?.model_tpm_limit - ? JSON.stringify(currentKeyData.metadata.model_tpm_limit) - : "Unlimited"} - - - Model RPM Limits:{" "} - {currentKeyData.metadata?.model_rpm_limit - ? JSON.stringify(currentKeyData.metadata.model_rpm_limit) - : "Unlimited"} - -
- -
- Metadata -
-                      {formatMetadataForDisplay(currentKeyData.metadata)}
-                    
-
- - - - -
- )} -
-
-
-
-
- ); -} - -export default KeyInfoView; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/SaveKeyModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/SaveKeyModal.tsx deleted file mode 100644 index fed678aadca..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/SaveKeyModal.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import { Button, Col, Grid, Text, Title } from "@tremor/react"; -import { CopyToClipboard } from "react-copy-to-clipboard"; -import { Modal } from "antd"; -import React from "react"; -import NotificationsManager from "@/components/molecules/notifications_manager"; - -export interface SaveKeyModalProps { - apiKey: string; - isModalVisible: boolean; - handleOk: () => void; - handleCancel: () => void; -} - -const SaveKeyModal = ({ apiKey, isModalVisible, handleOk, handleCancel }: SaveKeyModalProps) => { - const handleCopy = () => { - NotificationsManager.success("API Key copied to clipboard"); - }; - - return ( - - - Save your Key - -

- Please save this secret key somewhere safe and accessible. For security reasons,{" "} - you will not be able to view it again through your LiteLLM account. If you lose this secret key, you - will need to generate a new one. -

- - - {apiKey != null ? ( -
- API Key: -
-
{apiKey}
-
- - - - -
- ) : ( - Key being created, this might take 30s - )} - -
-
- ); -}; - -export default SaveKeyModal; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx deleted file mode 100644 index 180d3336f1c..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable.tsx +++ /dev/null @@ -1,667 +0,0 @@ -"use client"; -// TODO: refactor - -import React, { useEffect, useState } from "react"; -import { ColumnDef } from "@tanstack/react-table"; -import { Button } from "@tremor/react"; -import { Tooltip } from "antd"; -import { updateExistingKeys } from "@/utils/dataUtils"; -import { flexRender, getCoreRowModel, getSortedRowModel, SortingState, useReactTable } from "@tanstack/react-table"; -import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell, Icon } from "@tremor/react"; -import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline"; -import { Badge, Text } from "@tremor/react"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; -import { KeyResponse, Team } from "@/components/key_team_helpers/key_list"; -import { Organization, userListCall } from "@/components/networking"; -import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; -import FilterComponent, { FilterOption } from "@/components/molecules/filter"; -import { useFilterLogic } from "@/components/key_team_helpers/filter_logic"; -import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import KeyInfoView from "@/app/(dashboard)/virtual-keys/components/KeyInfoView"; - -interface AllKeysTableProps { - keys: KeyResponse[]; - setKeys: (keys: KeyResponse[] | ((prev: KeyResponse[]) => KeyResponse[])) => void; - isLoading?: boolean; - pagination: { - currentPage: number; - totalPages: number; - totalCount: number; - }; - onPageChange: (page: number) => void; - pageSize?: number; - organizations: Organization[] | null; - refresh?: () => void; - onSortChange?: (sortBy: string, sortOrder: "asc" | "desc") => void; - currentSort?: { - sortBy: string; - sortOrder: "asc" | "desc"; - }; - setAccessToken?: (token: string) => void; -} - -interface UserResponse { - user_id: string; - user_email: string; - user_role: string; -} - -const VirtualKeysTable = ({ - keys, - setKeys, - isLoading = false, - pagination, - onPageChange, - pageSize = 50, - organizations, - refresh, - onSortChange, - currentSort, - setAccessToken, -}: AllKeysTableProps) => { - const { userId: userID, userRole, accessToken, premiumUser } = useAuthorized(); - const { teams } = useTeams(); - const [selectedKeyId, setSelectedKeyId] = useState(null); - const [userList, setUserList] = useState([]); - const [sorting, setSorting] = React.useState(() => { - if (currentSort) { - return [ - { - id: currentSort.sortBy, - desc: currentSort.sortOrder === "desc", - }, - ]; - } - return [ - { - id: "created_at", - desc: true, - }, - ]; - }); - const [expandedAccordions, setExpandedAccordions] = useState>({}); - - // Use the filter logic hook - - const { filters, filteredKeys, allKeyAliases, allTeams, allOrganizations, handleFilterChange, handleFilterReset } = - useFilterLogic({ - keys, - teams, - organizations, - accessToken, - }); - - useEffect(() => { - if (accessToken) { - const user_IDs = keys.map((key) => key.user_id).filter((id) => id !== null); - const fetchUserList = async () => { - const userListData = await userListCall(accessToken, user_IDs, 1, 100); - setUserList(userListData.users); - }; - fetchUserList(); - } - }, [accessToken, keys]); - - // Add a useEffect to call refresh when a key is created - useEffect(() => { - if (!refresh || typeof window === "undefined") { - return; - } - const handleStorageChange = () => { - refresh(); - }; - - // Listen for storage events that might indicate a key was created - window.addEventListener("storage", handleStorageChange); - - return () => { - window.removeEventListener("storage", handleStorageChange); - }; - }, [refresh]); - - const columns: ColumnDef[] = [ - { - id: "expander", - header: () => null, - cell: ({ row }) => - row.getCanExpand() ? ( - - ) : null, - }, - { - id: "token", - accessorKey: "token", - header: "Key ID", - cell: (info) => ( -
- - - -
- ), - }, - { - id: "key_alias", - accessorKey: "key_alias", - header: "Key Alias", - cell: (info) => { - const value = info.getValue() as string; - return ( - {value ? (value.length > 20 ? `${value.slice(0, 20)}...` : value) : "-"} - ); - }, - }, - { - id: "key_name", - accessorKey: "key_name", - header: "Secret Key", - cell: (info) => {info.getValue() as string}, - }, - { - id: "team_alias", - accessorKey: "team_id", - header: "Team Alias", - cell: ({ row, getValue }) => { - const teamId = getValue() as string; - const team = teams?.find((t) => t.team_id === teamId); - return team?.team_alias || "Unknown"; - }, - }, - { - id: "team_id", - accessorKey: "team_id", - header: "Team ID", - cell: (info) => ( - - {info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "-"} - - ), - }, - { - id: "organization_id", - accessorKey: "organization_id", - header: "Organization ID", - cell: (info) => (info.getValue() ? info.renderValue() : "-"), - }, - { - id: "user_email", - accessorKey: "user_id", - header: "User Email", - cell: (info) => { - const userId = info.getValue() as string; - const user = userList.find((u) => u.user_id === userId); - return user?.user_email ? ( - - {user?.user_email.slice(0, 20)}... - - ) : ( - "-" - ); - }, - }, - { - id: "user_id", - accessorKey: "user_id", - header: "User ID", - cell: (info) => { - const userId = info.getValue() as string | null; - if (userId && userId.length > 15) { - return ( - - {userId.slice(0, 7)}... - - ); - } - return userId ? userId : "-"; - }, - }, - { - id: "created_at", - accessorKey: "created_at", - header: "Created At", - cell: (info) => { - const value = info.getValue(); - return value ? new Date(value as string).toLocaleDateString() : "-"; - }, - }, - { - id: "created_by", - accessorKey: "created_by", - header: "Created By", - cell: (info) => { - const value = info.getValue() as string | null; - if (value && value.length > 15) { - return ( - - {value.slice(0, 7)}... - - ); - } - return value; - }, - }, - { - id: "updated_at", - accessorKey: "updated_at", - header: "Updated At", - cell: (info) => { - const value = info.getValue(); - return value ? new Date(value as string).toLocaleDateString() : "Never"; - }, - }, - { - id: "expires", - accessorKey: "expires", - header: "Expires", - cell: (info) => { - const value = info.getValue(); - return value ? new Date(value as string).toLocaleDateString() : "Never"; - }, - }, - { - id: "spend", - accessorKey: "spend", - header: "Spend (USD)", - cell: (info) => formatNumberWithCommas(info.getValue() as number, 4), - }, - { - id: "max_budget", - accessorKey: "max_budget", - header: "Budget (USD)", - cell: (info) => { - const maxBudget = info.getValue() as number | null; - if (maxBudget === null) { - return "Unlimited"; - } - return `$${formatNumberWithCommas(maxBudget)}`; - }, - }, - { - id: "budget_reset_at", - accessorKey: "budget_reset_at", - header: "Budget Reset", - cell: (info) => { - const value = info.getValue(); - return value ? new Date(value as string).toLocaleString() : "Never"; - }, - }, - { - id: "models", - accessorKey: "models", - header: "Models", - cell: (info) => { - const models = info.getValue() as string[]; - return ( -
- {Array.isArray(models) ? ( -
- {models.length === 0 ? ( - - All Proxy Models - - ) : ( - <> -
- {models.length > 3 && ( -
- { - setExpandedAccordions((prev) => ({ - ...prev, - [info.row.id]: !prev[info.row.id], - })); - }} - /> -
- )} -
- {models.slice(0, 3).map((model, index) => - model === "all-proxy-models" ? ( - - All Proxy Models - - ) : ( - - - {model.length > 30 - ? `${getModelDisplayName(model).slice(0, 30)}...` - : getModelDisplayName(model)} - - - ), - )} - {models.length > 3 && !expandedAccordions[info.row.id] && ( - - - +{models.length - 3} {models.length - 3 === 1 ? "more model" : "more models"} - - - )} - {expandedAccordions[info.row.id] && ( -
- {models.slice(3).map((model, index) => - model === "all-proxy-models" ? ( - - All Proxy Models - - ) : ( - - - {model.length > 30 - ? `${getModelDisplayName(model).slice(0, 30)}...` - : getModelDisplayName(model)} - - - ), - )} -
- )} -
-
- - )} -
- ) : null} -
- ); - }, - }, - { - id: "rate_limits", - header: "Rate Limits", - cell: ({ row }) => { - const key = row.original; - return ( -
-
TPM: {key.tpm_limit !== null ? key.tpm_limit : "Unlimited"}
-
RPM: {key.rpm_limit !== null ? key.rpm_limit : "Unlimited"}
-
- ); - }, - }, - ]; - - const filterOptions: FilterOption[] = [ - { - name: "Team ID", - label: "Team ID", - isSearchable: true, - searchFn: async (searchText: string) => { - if (!allTeams || allTeams.length === 0) return []; - - const filteredTeams = allTeams.filter( - (team) => - team.team_id.toLowerCase().includes(searchText.toLowerCase()) || - (team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase())), - ); - - return filteredTeams.map((team) => ({ - label: `${team.team_alias || team.team_id} (${team.team_id})`, - value: team.team_id, - })); - }, - }, - { - name: "Organization ID", - label: "Organization ID", - isSearchable: true, - searchFn: async (searchText: string) => { - if (!allOrganizations || allOrganizations.length === 0) return []; - - const filteredOrgs = allOrganizations.filter( - (org) => org.organization_id?.toLowerCase().includes(searchText.toLowerCase()) ?? false, - ); - - return filteredOrgs - .filter((org) => org.organization_id !== null && org.organization_id !== undefined) - .map((org) => ({ - label: `${org.organization_id || "Unknown"} (${org.organization_id})`, - value: org.organization_id as string, - })); - }, - }, - { - name: "Key Alias", - label: "Key Alias", - isSearchable: true, - searchFn: async (searchText) => { - const filteredKeyAliases = allKeyAliases.filter((key) => { - return key.toLowerCase().includes(searchText.toLowerCase()); - }); - - return filteredKeyAliases.map((key) => { - return { - label: key, - value: key, - }; - }); - }, - }, - { - name: "User ID", - label: "User ID", - isSearchable: false, - }, - { - name: "Key Hash", - label: "Key Hash", - isSearchable: false, - }, - ]; - - console.log(`keys: ${JSON.stringify(keys)}`); - - const table = useReactTable({ - data: filteredKeys, - columns: columns.filter((col) => col.id !== "expander"), - state: { - sorting, - }, - onSortingChange: (updaterOrValue) => { - const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue; - console.log(`newSorting: ${JSON.stringify(newSorting)}`); - setSorting(newSorting); - if (newSorting && newSorting.length > 0) { - const sortState = newSorting[0]; - const sortBy = sortState.id; - const sortOrder = sortState.desc ? "desc" : "asc"; - console.log(`sortBy: ${sortBy}, sortOrder: ${sortOrder}`); - handleFilterChange({ - ...filters, - "Sort By": sortBy, - "Sort Order": sortOrder, - }); - onSortChange?.(sortBy, sortOrder); - } - }, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - enableSorting: true, - manualSorting: false, - }); - - // Update local sorting state when currentSort prop changes - React.useEffect(() => { - if (currentSort) { - setSorting([ - { - id: currentSort.sortBy, - desc: currentSort.sortOrder === "desc", - }, - ]); - } - }, [currentSort]); - - return ( -
- {selectedKeyId ? ( - setSelectedKeyId(null)} - keyData={filteredKeys.find((k) => k.token === selectedKeyId)} - onKeyDataUpdate={(updatedKeyData) => { - setKeys((keys) => - keys.map((key) => { - if (key.token === updatedKeyData.token) { - return updateExistingKeys(key, updatedKeyData); - } - return key; - }), - ); - if (refresh) refresh(); // Minimal fix: refresh the full key list after an update - }} - onDelete={() => { - setKeys((keys) => keys.filter((key) => key.token !== selectedKeyId)); - if (refresh) refresh(); // Minimal fix: refresh the full key list after a delete - }} - accessToken={accessToken} - userID={userID} - userRole={userRole} - teams={allTeams} - premiumUser={premiumUser} - setAccessToken={setAccessToken} - /> - ) : ( -
-
- -
- -
- - Showing{" "} - {isLoading - ? "..." - : `${(pagination.currentPage - 1) * pageSize + 1} - ${Math.min(pagination.currentPage * pageSize, pagination.totalCount)}`}{" "} - of {isLoading ? "..." : pagination.totalCount} results - - -
- - Page {isLoading ? "..." : pagination.currentPage} of {isLoading ? "..." : pagination.totalPages} - - - - - -
-
-
-
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - -
-
- {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} -
- {header.id !== "actions" && ( -
- {header.column.getIsSorted() ? ( - { - asc: , - desc: , - }[header.column.getIsSorted() as string] - ) : ( - - )} -
- )} -
-
- ))} -
- ))} -
- - {isLoading ? ( - - -
-

🚅 Loading keys...

-
-
-
- ) : filteredKeys.length > 0 ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - 3 ? "px-0" : ""}`} - > - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - -
-

No keys found

-
-
-
- )} -
-
-
-
-
-
- )} -
- ); -}; - -export default VirtualKeysTable; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts deleted file mode 100644 index ac174a554e8..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/hooks/useFilterLogic.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { useCallback, useEffect, useState, useRef } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { debounce } from "lodash"; -import { KeyResponse, Team } from "@/components/key_team_helpers/key_list"; -import { keyListCall, Organization } from "@/components/networking"; -import { defaultPageSize } from "@/components/constants"; -import { fetchAllKeyAliases, fetchAllOrganizations, fetchAllTeams } from "@/components/key_team_helpers/filter_helpers"; - -export interface FilterState { - "Team ID": string; - "Organization ID": string; - "Key Alias": string; - [key: string]: string; - "User ID": string; - "Sort By": string; - "Sort Order": string; -} - -export function useFilterLogic({ - keys, - teams, - organizations, - accessToken, -}: { - keys: KeyResponse[]; - teams: Team[] | null; - organizations: Organization[] | null; - accessToken: string | null; -}) { - const defaultFilters: FilterState = { - "Team ID": "", - "Organization ID": "", - "Key Alias": "", - "User ID": "", - "Sort By": "created_at", - "Sort Order": "desc", - }; - const [filters, setFilters] = useState(defaultFilters); - const [allTeams, setAllTeams] = useState(teams || []); - const [allOrganizations, setAllOrganizations] = useState(organizations || []); - const [filteredKeys, setFilteredKeys] = useState(keys); - const lastSearchTimestamp = useRef(0); - const debouncedSearch = useCallback( - debounce(async (filters: FilterState) => { - if (!accessToken) { - return; - } - - const currentTimestamp = Date.now(); - lastSearchTimestamp.current = currentTimestamp; - - try { - // Make the API call using userListCall with all filter parameters - const data = await keyListCall( - accessToken, - filters["Organization ID"] || null, - filters["Team ID"] || null, - filters["Key Alias"] || null, - filters["User ID"] || null, - filters["Key Hash"] || null, - 1, // Reset to first page when searching - defaultPageSize, - filters["Sort By"] || null, - filters["Sort Order"] || null, - ); - - // Only update state if this is the most recent search - if (currentTimestamp === lastSearchTimestamp.current) { - if (data) { - setFilteredKeys(data.keys); - console.log("called from debouncedSearch filters:", JSON.stringify(filters)); - console.log("called from debouncedSearch data:", JSON.stringify(data)); - } - } - } catch (error) { - console.error("Error searching users:", error); - } - }, 300), - [accessToken], - ); - // Apply filters to keys whenever keys or filters change - useEffect(() => { - if (!keys) { - setFilteredKeys([]); - return; - } - - let result = [...keys]; - - // Apply Team ID filter - if (filters["Team ID"]) { - result = result.filter((key) => key.team_id === filters["Team ID"]); - } - - // Apply Organization ID filter - if (filters["Organization ID"]) { - result = result.filter((key) => key.organization_id === filters["Organization ID"]); - } - - setFilteredKeys(result); - }, [keys, filters]); - - // Fetch all data for filters when component mounts - useEffect(() => { - const loadAllFilterData = async () => { - // Load all teams - no organization filter needed here - const teamsData = await fetchAllTeams(accessToken); - if (teamsData.length > 0) { - setAllTeams(teamsData); - } - - // Load all organizations - const orgsData = await fetchAllOrganizations(accessToken); - if (orgsData.length > 0) { - setAllOrganizations(orgsData); - } - }; - - if (accessToken) { - loadAllFilterData(); - } - }, [accessToken]); - - const queryAllKeysQuery = useQuery({ - queryKey: ["allKeys"], - queryFn: async () => { - if (!accessToken) throw new Error("Access token required"); - return await fetchAllKeyAliases(accessToken); - }, - enabled: !!accessToken, - }); - const allKeyAliases = queryAllKeysQuery.data || []; - - // Update teams and organizations when props change - useEffect(() => { - if (teams && teams.length > 0) { - setAllTeams((prevTeams) => { - // Only update if we don't already have a larger set of teams - return prevTeams.length < teams.length ? teams : prevTeams; - }); - } - }, [teams]); - - useEffect(() => { - if (organizations && organizations.length > 0) { - setAllOrganizations((prevOrgs) => { - // Only update if we don't already have a larger set of organizations - return prevOrgs.length < organizations.length ? organizations : prevOrgs; - }); - } - }, [organizations]); - - const handleFilterChange = (newFilters: Record) => { - // Update filters state - setFilters({ - "Team ID": newFilters["Team ID"] || "", - "Organization ID": newFilters["Organization ID"] || "", - "Key Alias": newFilters["Key Alias"] || "", - "User ID": newFilters["User ID"] || "", - "Sort By": newFilters["Sort By"] || "created_at", - "Sort Order": newFilters["Sort Order"] || "desc", - }); - - // Fetch keys based on new filters - const updatedFilters = { - ...filters, - ...newFilters, - }; - debouncedSearch(updatedFilters); - }; - - const handleFilterReset = () => { - // Reset filters state - setFilters(defaultFilters); - - // Reset selections - debouncedSearch(defaultFilters); - }; - - return { - filters, - filteredKeys, - allKeyAliases, - allTeams, - allOrganizations, - handleFilterChange, - handleFilterReset, - }; -} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/page.tsx index 611d92f1a32..56f780b2962 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/virtual-keys/page.tsx @@ -1,16 +1,18 @@ "use client"; -import VirtualKeysTable from "@/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable"; import { useState } from "react"; -import useKeyList from "@/components/key_team_helpers/key_list"; +import useKeyList, { KeyResponse } from "@/components/key_team_helpers/key_list"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { Col, Grid } from "@tremor/react"; -import CreateKey from "@/app/(dashboard)/virtual-keys/components/CreateKey"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import UserDashboard from "@/components/user_dashboard"; +import useTeams from "@/app/(dashboard)/hooks/useTeams"; +import { Organization } from "@/components/networking"; const VirtualKeysPage = () => { - const { accessToken, userRole } = useAuthorized(); + const { accessToken, userRole, userId, premiumUser, userEmail } = useAuthorized(); + const { teams, setTeams } = useTeams(); const [createClicked, setCreateClicked] = useState(false); + const [organizations, setOrganizations] = useState([]); const queryClient = new QueryClient(); @@ -28,20 +30,21 @@ const VirtualKeysPage = () => { return ( -
- - - - {}} - organizations={null} - /> - - -
+ {}} + setUserEmail={() => {}} + setTeams={setTeams} + setKeys={setKeys} + premiumUser={premiumUser} + organizations={organizations} + addKey={addKey} + createClicked={createClicked} + />
); }; diff --git a/ui/litellm-dashboard/src/components/templates/view_key_table.tsx b/ui/litellm-dashboard/src/components/templates/view_key_table.tsx index d146d5ba79e..86903d55530 100644 --- a/ui/litellm-dashboard/src/components/templates/view_key_table.tsx +++ b/ui/litellm-dashboard/src/components/templates/view_key_table.tsx @@ -90,7 +90,7 @@ interface ViewKeyTableProps { selectedTeam: Team | null; setSelectedTeam: React.Dispatch>; data: KeyResponse[] | null; - setData: React.Dispatch>; + setData: (keys: KeyResponse[]) => void; teams: Team[] | null; premiumUser: boolean; currentOrg: Organization | null; @@ -135,13 +135,6 @@ interface CombinedLimits { [key: string]: CombinedLimit; // Index signature allowing string keys } -/** - * ───────────────────────────────────────────────────────────────────────── - * @deprecated - * This component is being DEPRECATED in favor of src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/ - * Please contribute to the new refactor. - * ───────────────────────────────────────────────────────────────────────── - */ const ViewKeyTable: React.FC = ({ userID, userRole, diff --git a/ui/litellm-dashboard/src/components/user_dashboard.tsx b/ui/litellm-dashboard/src/components/user_dashboard.tsx index b6470e49521..4d98b37a8f8 100644 --- a/ui/litellm-dashboard/src/components/user_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/user_dashboard.tsx @@ -20,11 +20,12 @@ import ViewUserTeam from "./view_user_team"; import DashboardTeam from "./dashboard_default_team"; import Onboarding from "../app/onboarding/page"; import { useSearchParams, useRouter } from "next/navigation"; -import { Team } from "./key_team_helpers/key_list"; +import { KeyResponse, Team } from "./key_team_helpers/key_list"; import { jwtDecode } from "jwt-decode"; import { Typography } from "antd"; import { clearTokenCookies } from "@/utils/cookieUtils"; import { clearMCPAuthTokens } from "./mcp_tools/mcp_auth_storage"; +import { Setter } from "@/types"; export interface ProxySettings { PROXY_BASE_URL: string | null; @@ -56,7 +57,7 @@ interface UserDashboardProps { setUserRole: React.Dispatch>; setUserEmail: React.Dispatch>; setTeams: React.Dispatch>; - setKeys: React.Dispatch>; + setKeys: (keys: KeyResponse[]) => void; premiumUser: boolean; organizations: Organization[] | null; addKey: (data: any) => void; diff --git a/ui/litellm-dashboard/src/components/view_users.tsx b/ui/litellm-dashboard/src/components/view_users.tsx index 19479ce21a9..7fcb7f54b82 100644 --- a/ui/litellm-dashboard/src/components/view_users.tsx +++ b/ui/litellm-dashboard/src/components/view_users.tsx @@ -30,6 +30,8 @@ import { updateExistingKeys } from "@/utils/dataUtils"; import { useDebouncedState } from "@tanstack/react-pacer/debouncer"; import { isAdminRole } from "@/utils/roles"; import NotificationsManager from "./molecules/notifications_manager"; +import { Setter } from "@/types"; +import { KeyResponse } from "@/components/key_team_helpers/key_list"; interface ViewUserDashboardProps { accessToken: string | null;