mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
moved virtual-key-refactor to its own branch
This commit is contained in:
parent
2b0df22dc5
commit
825a040524
32 changed files with 37 additions and 3164 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Team[] | null>([]);
|
||||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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<CreateKeyProps> = ({ 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<string | null>(null);
|
||||
const [possibleUIRoles, setPossibleUIRoles] = useState<Record<string, Record<string, string>>>({});
|
||||
|
||||
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 (
|
||||
<div>
|
||||
{userRole && rolesWithWriteAccess.includes(userRole) && (
|
||||
<Button className="mx-auto" onClick={() => setIsModalVisible(true)}>
|
||||
+ Create New Key
|
||||
</Button>
|
||||
)}
|
||||
<CreateKeyModal
|
||||
isModalVisible={isModalVisible}
|
||||
form={form}
|
||||
handleUserSelect={handleUserSelect}
|
||||
setIsCreateUserModalVisible={setIsCreateUserModalVisible}
|
||||
team={team}
|
||||
setIsModalVisible={setIsModalVisible}
|
||||
data={data}
|
||||
addKey={addKey}
|
||||
/>
|
||||
|
||||
{isCreateUserModalVisible && (
|
||||
<Modal
|
||||
title="Create New User"
|
||||
open={isCreateUserModalVisible}
|
||||
onCancel={() => setIsCreateUserModalVisible(false)}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<CreateUserModal possibleUIRoles={possibleUIRoles} onUserCreated={handleUserCreated} isEmbedded={true} />
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateKey;
|
||||
|
|
@ -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 (
|
||||
<div className="mb-8">
|
||||
<Title className="mb-4">Key Details</Title>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
{keyOwner === "you" || keyOwner === "another_user" ? "Key Name" : "Service Account ID"}{" "}
|
||||
<Tooltip
|
||||
title={
|
||||
keyOwner === "you" || keyOwner === "another_user"
|
||||
? "A descriptive name to identify this key"
|
||||
: "Unique identifier for this service account"
|
||||
}
|
||||
>
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="key_alias"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `Please input a ${keyOwner === "you" ? "key name" : "service account ID"}`,
|
||||
},
|
||||
]}
|
||||
help="required"
|
||||
>
|
||||
<TextInput placeholder="" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Models{" "}
|
||||
<Tooltip title="Select which models this key can access. Choose 'All Team Models' to grant access to all models available to the team">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
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"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="Select models"
|
||||
style={{ width: "100%" }}
|
||||
disabled={keyType === "management" || keyType === "read_only"}
|
||||
onChange={(values) => {
|
||||
if (values.includes("all-team-models")) {
|
||||
form.setFieldsValue({ models: ["all-team-models"] });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Option key="all-team-models" value="all-team-models">
|
||||
All Team Models
|
||||
</Option>
|
||||
{modelsToPick.map((model: string) => (
|
||||
<Option key={model} value={model}>
|
||||
{getModelDisplayName(model)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Key Type{" "}
|
||||
<Tooltip title="Select the type of key to determine what routes and operations this key can access">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="key_type"
|
||||
initialValue="default"
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue="default"
|
||||
placeholder="Select key type"
|
||||
style={{ width: "100%" }}
|
||||
optionLabelProp="label"
|
||||
onChange={(value) => {
|
||||
setKeyType(value);
|
||||
// Clear models field and disable if management or read_only
|
||||
if (value === "management" || value === "read_only") {
|
||||
form.setFieldsValue({ models: [] });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Option value="default" label="Default">
|
||||
<div style={{ padding: "4px 0" }}>
|
||||
<div style={{ fontWeight: 500 }}>Default</div>
|
||||
<div style={{ fontSize: "11px", color: "#6b7280", marginTop: "2px" }}>
|
||||
Can call LLM API + Management routes
|
||||
</div>
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="llm_api" label="LLM API">
|
||||
<div style={{ padding: "4px 0" }}>
|
||||
<div style={{ fontWeight: 500 }}>LLM API</div>
|
||||
<div style={{ fontSize: "11px", color: "#6b7280", marginTop: "2px" }}>
|
||||
Can call only LLM API routes (chat/completions, embeddings, etc.)
|
||||
</div>
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="management" label="Management">
|
||||
<div style={{ padding: "4px 0" }}>
|
||||
<div style={{ fontWeight: 500 }}>Management</div>
|
||||
<div style={{ fontSize: "11px", color: "#6b7280", marginTop: "2px" }}>
|
||||
Can call only management routes (user/team/key management)
|
||||
</div>
|
||||
</div>
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeyDetailsSection;
|
||||
|
|
@ -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 (
|
||||
<div className="mb-8">
|
||||
<Accordion className="mt-4 mb-4">
|
||||
<AccordionHeader>
|
||||
<Title className="m-0">Optional Settings</Title>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<Form.Item
|
||||
className="mt-4"
|
||||
label={
|
||||
<span>
|
||||
Max Budget (USD){" "}
|
||||
<Tooltip title="Maximum amount in USD this key can spend. When reached, the key will be blocked from making further requests">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
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)}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<NumericalInput step={0.01} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="mt-4"
|
||||
label={
|
||||
<span>
|
||||
Reset Budget{" "}
|
||||
<Tooltip title="How often the budget should reset. For example, setting 'daily' will reset the budget every 24 hours">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="budget_duration"
|
||||
help={`Team Reset Budget: ${team?.budget_duration !== null && team?.budget_duration !== undefined ? team?.budget_duration : "None"}`}
|
||||
>
|
||||
<BudgetDurationDropdown onChange={(value) => form.setFieldValue("budget_duration", value)} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="mt-4"
|
||||
label={
|
||||
<span>
|
||||
Tokens per minute Limit (TPM){" "}
|
||||
<Tooltip title="Maximum number of tokens this key can process per minute. Helps control usage and costs">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<NumericalInput step={1} width={400} />
|
||||
</Form.Item>
|
||||
<RateLimitTypeFormItem
|
||||
type="tpm"
|
||||
name="tpm_limit_type"
|
||||
className="mt-4"
|
||||
initialValue={null}
|
||||
form={form}
|
||||
showDetailedDescriptions={true}
|
||||
/>
|
||||
<Form.Item
|
||||
className="mt-4"
|
||||
label={
|
||||
<span>
|
||||
Requests per minute Limit (RPM){" "}
|
||||
<Tooltip title="Maximum number of API requests this key can make per minute. Helps prevent abuse and manage load">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<NumericalInput step={1} width={400} />
|
||||
</Form.Item>
|
||||
<RateLimitTypeFormItem
|
||||
type="rpm"
|
||||
name="rpm_limit_type"
|
||||
className="mt-4"
|
||||
initialValue={null}
|
||||
form={form}
|
||||
showDetailedDescriptions={true}
|
||||
/>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Guardrails{" "}
|
||||
<Tooltip title="Apply safety guardrails to this key to filter content or enforce policies">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()} // Prevent accordion from collapsing when clicking link
|
||||
>
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="guardrails"
|
||||
className="mt-4"
|
||||
help={
|
||||
premiumUser
|
||||
? "Select existing guardrails or enter new ones"
|
||||
: "Premium feature - Upgrade to set guardrails by key"
|
||||
}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
disabled={!premiumUser}
|
||||
placeholder={
|
||||
!premiumUser ? "Premium feature - Upgrade to set guardrails by key" : "Select or enter guardrails"
|
||||
}
|
||||
options={guardrails.map((name) => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Prompts{" "}
|
||||
<Tooltip title="Allow this key to use specific prompt templates">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/prompt_management"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()} // Prevent accordion from collapsing when clicking link
|
||||
>
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="prompts"
|
||||
className="mt-4"
|
||||
help={
|
||||
premiumUser
|
||||
? "Select existing prompts or enter new ones"
|
||||
: "Premium feature - Upgrade to set prompts by key"
|
||||
}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
disabled={!premiumUser}
|
||||
placeholder={!premiumUser ? "Premium feature - Upgrade to set prompts by key" : "Select or enter prompts"}
|
||||
options={prompts.map((name) => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Allowed Vector Stores{" "}
|
||||
<Tooltip title="Select which vector stores this key can access. If none selected, the key will have access to all available vector stores">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="allowed_vector_store_ids"
|
||||
className="mt-4"
|
||||
help="Select vector stores this key can access. Leave empty for access to all vector stores"
|
||||
>
|
||||
<VectorStoreSelector
|
||||
onChange={(values: string[]) => form.setFieldValue("allowed_vector_store_ids", values)}
|
||||
value={form.getFieldValue("allowed_vector_store_ids")}
|
||||
accessToken={accessToken}
|
||||
placeholder="Select vector stores (optional)"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Allowed MCP Servers{" "}
|
||||
<Tooltip title="Select which MCP servers or access groups this key can access. ">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="allowed_mcp_servers_and_groups"
|
||||
className="mt-4"
|
||||
help="Select MCP servers or access groups this key can access. "
|
||||
>
|
||||
<MCPServerSelector
|
||||
onChange={(val: any) => 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)"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Metadata{" "}
|
||||
<Tooltip title="JSON object with additional information about this key. Used for tracking or custom logic">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="metadata"
|
||||
className="mt-4"
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="Enter metadata as JSON" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Tags{" "}
|
||||
<Tooltip title="Tags for tracking spend and/or doing tag-based routing. Used for analytics and filtering">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="tags"
|
||||
className="mt-4"
|
||||
help={`Tags for tracking spend and/or doing tag-based routing.`}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Enter tags"
|
||||
tokenSeparators={[","]}
|
||||
options={predefinedTags}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{premiumUser ? (
|
||||
<Accordion className="mt-4 mb-4">
|
||||
<AccordionHeader>
|
||||
<b>Logging Settings</b>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<div className="mt-4">
|
||||
<PremiumLoggingSettings
|
||||
value={loggingSettings}
|
||||
onChange={setLoggingSettings}
|
||||
premiumUser={true}
|
||||
disabledCallbacks={disabledCallbacks}
|
||||
onDisabledCallbacksChange={setDisabledCallbacks}
|
||||
/>
|
||||
</div>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Key-level logging settings is an enterprise feature, get in touch -
|
||||
<a href="https://www.litellm.ai/enterprise" target="_blank">
|
||||
https://www.litellm.ai/enterprise
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<div style={{ position: "relative" }}>
|
||||
<div style={{ opacity: 0.5 }}>
|
||||
<Accordion className="mt-4 mb-4">
|
||||
<AccordionHeader>
|
||||
<b>Logging Settings</b>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<div className="mt-4">
|
||||
<PremiumLoggingSettings
|
||||
value={loggingSettings}
|
||||
onChange={setLoggingSettings}
|
||||
premiumUser={false}
|
||||
disabledCallbacks={disabledCallbacks}
|
||||
onDisabledCallbacksChange={setDisabledCallbacks}
|
||||
/>
|
||||
</div>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
</div>
|
||||
<div style={{ position: "absolute", inset: 0, cursor: "not-allowed" }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Accordion className="mt-4 mb-4">
|
||||
<AccordionHeader>
|
||||
<b>Model Aliases</b>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<div className="mt-4">
|
||||
<Text className="text-sm text-gray-600 mb-4">
|
||||
Create custom aliases for models that can be used in API calls. This allows you to create shortcuts
|
||||
for specific models.
|
||||
</Text>
|
||||
<ModelAliasManager
|
||||
accessToken={accessToken}
|
||||
initialModelAliases={modelAliases}
|
||||
onAliasUpdate={setModelAliases}
|
||||
showExampleConfig={false}
|
||||
/>
|
||||
</div>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
|
||||
<Accordion className="mt-4 mb-4">
|
||||
<AccordionHeader>
|
||||
<b>Key Lifecycle</b>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<div className="mt-4">
|
||||
<KeyLifecycleSettings
|
||||
form={form}
|
||||
autoRotationEnabled={autoRotationEnabled}
|
||||
onAutoRotationChange={setAutoRotationEnabled}
|
||||
rotationInterval={rotationInterval}
|
||||
onRotationIntervalChange={setRotationInterval}
|
||||
/>
|
||||
</div>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
<Accordion className="mt-4 mb-4">
|
||||
<AccordionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<b>Advanced Settings</b>
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Learn more about advanced settings in our{" "}
|
||||
<a
|
||||
href={
|
||||
proxyBaseUrl
|
||||
? `${proxyBaseUrl}/#/key%20management/generate_key_fn_key_generate_post`
|
||||
: `/#/key%20management/generate_key_fn_key_generate_post`
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
documentation
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoCircleOutlined className="text-gray-400 hover:text-gray-300 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<SchemaFormFields
|
||||
schemaComponent="GenerateKeyRequest"
|
||||
form={form}
|
||||
excludedFields={[
|
||||
"key_alias",
|
||||
"team_id",
|
||||
"models",
|
||||
"duration",
|
||||
"metadata",
|
||||
"tags",
|
||||
"guardrails",
|
||||
"max_budget",
|
||||
"budget_duration",
|
||||
"tpm_limit",
|
||||
"rpm_limit",
|
||||
]}
|
||||
/>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OptionalSettingsSection;
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { Button as Button2, Form, Radio, Select, Tooltip } from "antd";
|
||||
import { Title } from "@tremor/react";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import TeamDropdown from "@/components/common_components/team_dropdown";
|
||||
import React from "react";
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import { UserOption } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types";
|
||||
|
||||
interface OwnershipSectionProps {
|
||||
team: Team | null;
|
||||
teams: Team[] | null;
|
||||
userRole: string | null;
|
||||
userOptions: UserOption[];
|
||||
keyOwner: string;
|
||||
setKeyOwner: React.Dispatch<React.SetStateAction<string>>;
|
||||
handleUserSearch: (q: string) => void;
|
||||
userSearchLoading: boolean;
|
||||
handleUserSelect: (q: string, option: UserOption) => void;
|
||||
setSelectedCreateKeyTeam: React.Dispatch<React.SetStateAction<Team | null>>;
|
||||
setIsCreateUserModalVisible: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
const OwnershipSection = ({
|
||||
team,
|
||||
teams,
|
||||
userRole,
|
||||
userOptions,
|
||||
keyOwner,
|
||||
setKeyOwner,
|
||||
handleUserSearch,
|
||||
userSearchLoading,
|
||||
handleUserSelect,
|
||||
setSelectedCreateKeyTeam,
|
||||
setIsCreateUserModalVisible,
|
||||
}: OwnershipSectionProps) => {
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<Title className="mb-4">Key Ownership</Title>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Owned By{" "}
|
||||
<Tooltip title="Select who will own this API key">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
className="mb-4"
|
||||
>
|
||||
<Radio.Group onChange={(e) => setKeyOwner(e.target.value)} value={keyOwner}>
|
||||
<Radio value="you">You</Radio>
|
||||
<Radio value="service_account">Service Account</Radio>
|
||||
{userRole === "Admin" && <Radio value="another_user">Another User</Radio>}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{keyOwner === "another_user" && (
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
User ID{" "}
|
||||
<Tooltip title="The user who will own this key and be responsible for its usage">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="user_id"
|
||||
className="mt-4"
|
||||
rules={[
|
||||
{
|
||||
required: keyOwner === "another_user",
|
||||
message: `Please input the user ID of the user you are assigning the key to`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div>
|
||||
<div style={{ display: "flex", marginBottom: "8px" }}>
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="Type email to search for users"
|
||||
filterOption={false}
|
||||
onSearch={handleUserSearch}
|
||||
onSelect={(value, option) => handleUserSelect(value, option as UserOption)}
|
||||
options={userOptions}
|
||||
loading={userSearchLoading}
|
||||
allowClear
|
||||
style={{ width: "100%" }}
|
||||
notFoundContent={userSearchLoading ? "Searching..." : "No users found"}
|
||||
/>
|
||||
<Button2 onClick={() => setIsCreateUserModalVisible(true)} style={{ marginLeft: "8px" }}>
|
||||
Create User
|
||||
</Button2>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">Search by email to find users</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Team{" "}
|
||||
<Tooltip title="The team this key belongs to, which determines available models and budget limits">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
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" : ""}
|
||||
>
|
||||
<TeamDropdown
|
||||
teams={teams}
|
||||
onChange={(teamId) => {
|
||||
const selectedTeam = teams?.find((t) => t.team_id === teamId) || null;
|
||||
setSelectedCreateKeyTeam(selectedTeam);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OwnershipSection;
|
||||
|
|
@ -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<string | null>(null);
|
||||
const [keyOwner, setKeyOwner] = useState("you");
|
||||
const [keyType, setKeyType] = useState<string>("default");
|
||||
|
||||
const [selectedCreateKeyTeam, setSelectedCreateKeyTeam] = useState<Team | null>(team);
|
||||
|
||||
const [softBudget, setSoftBudget] = useState<string | null>(null);
|
||||
const [loggingSettings, setLoggingSettings] = useState<any[]>([]);
|
||||
const [disabledCallbacks, setDisabledCallbacks] = useState<string[]>([]);
|
||||
const [autoRotationEnabled, setAutoRotationEnabled] = useState<boolean>(false);
|
||||
const [rotationInterval, setRotationInterval] = useState<string>("30d");
|
||||
const [modelAliases, setModelAliases] = useState<ModelAliases>({});
|
||||
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<string, any>) => {
|
||||
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 (
|
||||
<div>
|
||||
<Modal open={isModalVisible} width={1000} footer={null} onOk={handleOk} onCancel={handleCancel}>
|
||||
<Form form={form} onFinish={handleCreate} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} labelAlign="left">
|
||||
<OwnershipSection
|
||||
team={team}
|
||||
teams={teams}
|
||||
userRole={userRole}
|
||||
userOptions={userOptions}
|
||||
keyOwner={keyOwner}
|
||||
setKeyOwner={setKeyOwner}
|
||||
handleUserSearch={handleUserSearch}
|
||||
userSearchLoading={userSearchLoading}
|
||||
handleUserSelect={handleUserSelect}
|
||||
setSelectedCreateKeyTeam={setSelectedCreateKeyTeam}
|
||||
setIsCreateUserModalVisible={setIsCreateUserModalVisible}
|
||||
/>
|
||||
|
||||
{/* Show message when team selection is required */}
|
||||
{isFormDisabled && (
|
||||
<div className="mb-8 p-4 bg-blue-50 border border-blue-200 rounded-md">
|
||||
<Text className="text-blue-800 text-sm">
|
||||
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.
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section 2: Key Details */}
|
||||
{!isFormDisabled && (
|
||||
<KeyDetailsSection
|
||||
form={form}
|
||||
keyOwner={keyOwner}
|
||||
modelsToPick={modelsToPick}
|
||||
keyType={keyType}
|
||||
setKeyType={setKeyType}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Section 3: Optional Settings */}
|
||||
{!isFormDisabled && (
|
||||
<OptionalSettingsSection
|
||||
form={form}
|
||||
team={team}
|
||||
premiumUser={premiumUser}
|
||||
guardrails={guardrails}
|
||||
prompts={prompts}
|
||||
accessToken={accessToken}
|
||||
predefinedTags={predefinedTags}
|
||||
loggingSettings={loggingSettings}
|
||||
setLoggingSettings={setLoggingSettings}
|
||||
disabledCallbacks={disabledCallbacks}
|
||||
setDisabledCallbacks={setDisabledCallbacks}
|
||||
modelAliases={modelAliases}
|
||||
setModelAliases={setModelAliases}
|
||||
autoRotationEnabled={autoRotationEnabled}
|
||||
setAutoRotationEnabled={setAutoRotationEnabled}
|
||||
rotationInterval={rotationInterval}
|
||||
setRotationInterval={setRotationInterval}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button2 htmlType="submit" disabled={isFormDisabled} style={{ opacity: isFormDisabled ? 0.5 : 1 }}>
|
||||
Create Key
|
||||
</Button2>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
{apiKey && (
|
||||
<SaveKeyModal apiKey={apiKey} isModalVisible={isModalVisible} handleOk={handleOk} handleCancel={handleCancel} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateKeyModal;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
export * from "./useGuardrailsAndPrompts";
|
||||
export * from "./useMcpAccessGroups";
|
||||
export * from "./useUserModels";
|
||||
export * from "./useTeamModels";
|
||||
export * from "./useUserSearch";
|
||||
|
|
@ -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<string[]>([]);
|
||||
const [prompts, setPrompts] = useState<string[]>([]);
|
||||
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 };
|
||||
};
|
||||
|
|
@ -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<string[]>([]);
|
||||
const { accessToken } = useAuthorized();
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken) {
|
||||
setMcpAccessGroups([]);
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
const groups = await getMCPAccessGroups(accessToken);
|
||||
setMcpAccessGroups(groups || []);
|
||||
})();
|
||||
}, [accessToken]);
|
||||
|
||||
return mcpAccessGroups;
|
||||
};
|
||||
|
|
@ -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<string[]>([]);
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
|
@ -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<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userID || !userRole || !accessToken) {
|
||||
setUserModels([]);
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
const modelNames = await getUserModelNames(userID, userRole, accessToken);
|
||||
setUserModels(modelNames || []);
|
||||
})();
|
||||
}, [userID, userRole, accessToken]);
|
||||
|
||||
return userModels;
|
||||
};
|
||||
|
|
@ -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<UserOption[]>([]);
|
||||
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 };
|
||||
};
|
||||
|
|
@ -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<UserOption[]> => {
|
||||
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<string[]> => {
|
||||
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<string[]> => {
|
||||
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<string[]> => {
|
||||
try {
|
||||
if (accessToken == null) {
|
||||
return [];
|
||||
}
|
||||
return await fetchMCPAccessGroups(accessToken);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch MCP access groups:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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<string, any>;
|
||||
|
||||
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<PrepareOptions, "keyOwner" | "userID">,
|
||||
): 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<PrepareOptions, "keyOwner" | "loggingSettings" | "disabledCallbacks">,
|
||||
): 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<PrepareOptions, "autoRotationEnabled" | "rotationInterval">,
|
||||
): 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;
|
||||
}
|
||||
|
|
@ -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<KeyResponse>) => 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<Record<string, boolean>>({});
|
||||
|
||||
// Add local state to maintain key data and track regeneration
|
||||
const [currentKeyData, setCurrentKeyData] = useState<KeyResponse | undefined>(keyData);
|
||||
const [lastRegeneratedAt, setLastRegeneratedAt] = useState<Date | null>(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 (
|
||||
<div className="p-4">
|
||||
<Button icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
|
||||
{backButtonText}
|
||||
</Button>
|
||||
<Text>Key not found</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleKeyUpdate = async (formValues: Record<string, any>) => {
|
||||
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<string, string> = {
|
||||
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<KeyResponse>) => {
|
||||
// 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 (
|
||||
<div className="w-full h-screen p-4">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<Button icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
|
||||
{backButtonText}
|
||||
</Button>
|
||||
<Title>{currentKeyData.key_alias || "API Key"}</Title>
|
||||
|
||||
<div className="flex items-center cursor-pointer mb-2 space-y-6">
|
||||
<div>
|
||||
<Text className="text-xs text-gray-400 uppercase tracking-wide mt-2">Key ID</Text>
|
||||
<Text className="text-gray-500 font-mono text-sm">{currentKeyData.token_id || currentKeyData.token}</Text>
|
||||
</div>
|
||||
<AntdButton
|
||||
type="text"
|
||||
size="small"
|
||||
icon={copiedStates["key-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
|
||||
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"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Add timestamp and regeneration indicator */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Text className="text-sm text-gray-500">
|
||||
{currentKeyData.updated_at && currentKeyData.updated_at !== currentKeyData.created_at
|
||||
? `Updated: ${formatTimestamp(currentKeyData.updated_at)}`
|
||||
: `Created: ${formatTimestamp(currentKeyData.created_at)}`}
|
||||
</Text>
|
||||
|
||||
{isRecentlyRegenerated && (
|
||||
<Badge color="green" size="xs" className="animate-pulse">
|
||||
Recently Regenerated
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{lastRegeneratedAt && (
|
||||
<Badge color="blue" size="xs">
|
||||
Regenerated
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{userRole && rolesWithWriteAccess.includes(userRole) && (
|
||||
<div className="flex gap-2">
|
||||
<Tooltip
|
||||
title={!premiumUser ? "This is a LiteLLM Enterprise feature, and requires a valid key to use." : ""}
|
||||
>
|
||||
<span className="inline-block">
|
||||
<Button
|
||||
icon={RefreshIcon}
|
||||
variant="secondary"
|
||||
onClick={() => setIsRegenerateModalOpen(true)}
|
||||
className="flex items-center"
|
||||
disabled={!premiumUser}
|
||||
>
|
||||
Regenerate Key
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Button
|
||||
icon={TrashIcon}
|
||||
variant="secondary"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
className="flex items-center"
|
||||
>
|
||||
Delete Key
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add RegenerateKeyModal */}
|
||||
<RegenerateKeyModal
|
||||
selectedToken={currentKeyData}
|
||||
visible={isRegenerateModalOpen}
|
||||
onClose={() => 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 (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl min-h-[380px] py-6 overflow-hidden transform transition-all flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Delete Key</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setDeleteConfirmInput("");
|
||||
}}
|
||||
className="text-gray-400 hover:text-gray-500 focus:outline-none"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<div className="flex items-start gap-3 p-4 bg-red-50 border border-red-100 rounded-md mb-5">
|
||||
<div className="text-red-500 mt-0.5">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.082 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-base font-medium text-red-600">
|
||||
Warning: You are about to delete this API key.
|
||||
</p>
|
||||
<p className="text-base text-red-600 mt-2">
|
||||
This action is irreversible and will immediately revoke access for any applications using this
|
||||
key.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-base text-gray-600 mb-5">Are you sure you want to delete this API key?</p>
|
||||
<div className="mb-5">
|
||||
<label className="block text-base font-medium text-gray-700 mb-2">
|
||||
{`Type `}
|
||||
<span className="underline">{keyName}</span>
|
||||
{` to confirm deletion:`}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={deleteConfirmInput}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-4 bg-gray-50 flex justify-end gap-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setDeleteConfirmInput("");
|
||||
}}
|
||||
className="px-5 py-3 bg-white border border-gray-300 rounded-md text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={!isValid}
|
||||
className={`px-5 py-3 rounded-md text-base font-medium text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 ${isValid ? "bg-red-600 hover:bg-red-700" : "bg-red-300 cursor-not-allowed"}`}
|
||||
>
|
||||
Delete Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<TabGroup>
|
||||
<TabList className="mb-4">
|
||||
<Tab>Overview</Tab>
|
||||
<Tab>Settings</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
{/* Overview Panel */}
|
||||
<TabPanel>
|
||||
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
|
||||
<Card>
|
||||
<Text>Spend</Text>
|
||||
<div className="mt-2">
|
||||
<Title>${formatNumberWithCommas(currentKeyData.spend, 4)}</Title>
|
||||
<Text>
|
||||
of{" "}
|
||||
{currentKeyData.max_budget !== null
|
||||
? `$${formatNumberWithCommas(currentKeyData.max_budget)}`
|
||||
: "Unlimited"}
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Rate Limits</Text>
|
||||
<div className="mt-2">
|
||||
<Text>TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"}</Text>
|
||||
<Text>RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.rpm_limit : "Unlimited"}</Text>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Models</Text>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{currentKeyData.models && currentKeyData.models.length > 0 ? (
|
||||
currentKeyData.models.map((model, index) => (
|
||||
<Badge key={index} color="red">
|
||||
{model}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<Text>No models specified</Text>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<ObjectPermissionsView
|
||||
objectPermission={currentKeyData.object_permission}
|
||||
variant="inline"
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<LoggingSettingsView
|
||||
loggingConfigs={extractLoggingSettings(currentKeyData.metadata)}
|
||||
disabledCallbacks={
|
||||
Array.isArray(currentKeyData.metadata?.litellm_disabled_callbacks)
|
||||
? mapInternalToDisplayNames(currentKeyData.metadata.litellm_disabled_callbacks)
|
||||
: []
|
||||
}
|
||||
variant="card"
|
||||
/>
|
||||
|
||||
<AutoRotationView
|
||||
autoRotate={currentKeyData.auto_rotate}
|
||||
rotationInterval={currentKeyData.rotation_interval}
|
||||
lastRotationAt={currentKeyData.last_rotation_at}
|
||||
keyRotationAt={currentKeyData.key_rotation_at}
|
||||
nextRotationAt={currentKeyData.next_rotation_at}
|
||||
variant="card"
|
||||
/>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
|
||||
{/* Settings Panel */}
|
||||
<TabPanel>
|
||||
<Card className="overflow-y-auto max-h-[65vh]">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Key Settings</Title>
|
||||
{!isEditing && userRole && rolesWithWriteAccess.includes(userRole) && (
|
||||
<Button variant="light" onClick={() => setIsEditing(true)}>
|
||||
Edit Settings
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<KeyEditView
|
||||
keyData={currentKeyData}
|
||||
onCancel={() => setIsEditing(false)}
|
||||
onSubmit={handleKeyUpdate}
|
||||
teams={teams}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-medium">Key ID</Text>
|
||||
<Text className="font-mono">{currentKeyData.token_id || currentKeyData.token}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Key Alias</Text>
|
||||
<Text>{currentKeyData.key_alias || "Not Set"}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Secret Key</Text>
|
||||
<Text className="font-mono">{currentKeyData.key_name}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Team ID</Text>
|
||||
<Text>{currentKeyData.team_id || "Not Set"}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Organization</Text>
|
||||
<Text>{currentKeyData.organization_id || "Not Set"}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Created</Text>
|
||||
<Text>{formatTimestamp(currentKeyData.created_at)}</Text>
|
||||
</div>
|
||||
|
||||
{lastRegeneratedAt && (
|
||||
<div>
|
||||
<Text className="font-medium">Last Regenerated</Text>
|
||||
<div className="flex items-center gap-2">
|
||||
<Text>{formatTimestamp(lastRegeneratedAt)}</Text>
|
||||
<Badge color="green" size="xs">
|
||||
Recent
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Expires</Text>
|
||||
<Text>{currentKeyData.expires ? formatTimestamp(currentKeyData.expires) : "Never"}</Text>
|
||||
</div>
|
||||
|
||||
<AutoRotationView
|
||||
autoRotate={currentKeyData.auto_rotate}
|
||||
rotationInterval={currentKeyData.rotation_interval}
|
||||
lastRotationAt={currentKeyData.last_rotation_at}
|
||||
keyRotationAt={currentKeyData.key_rotation_at}
|
||||
nextRotationAt={currentKeyData.next_rotation_at}
|
||||
variant="inline"
|
||||
className="pt-4 border-t border-gray-200"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Spend</Text>
|
||||
<Text>${formatNumberWithCommas(currentKeyData.spend, 4)} USD</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Budget</Text>
|
||||
<Text>
|
||||
{currentKeyData.max_budget !== null
|
||||
? `$${formatNumberWithCommas(currentKeyData.max_budget, 2)}`
|
||||
: "Unlimited"}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Prompts</Text>
|
||||
<Text>
|
||||
{Array.isArray(currentKeyData.metadata?.prompts) && currentKeyData.metadata.prompts.length > 0
|
||||
? currentKeyData.metadata.prompts.map((prompt, index) => (
|
||||
<span key={index} className="px-2 mr-2 py-1 bg-blue-100 rounded text-xs">
|
||||
{prompt}
|
||||
</span>
|
||||
))
|
||||
: "No prompts specified"}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Models</Text>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{currentKeyData.models && currentKeyData.models.length > 0 ? (
|
||||
currentKeyData.models.map((model, index) => (
|
||||
<span key={index} className="px-2 py-1 bg-blue-100 rounded text-xs">
|
||||
{model}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<Text>No models specified</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Rate Limits</Text>
|
||||
<Text>TPM: {currentKeyData.tpm_limit !== null ? currentKeyData.tpm_limit : "Unlimited"}</Text>
|
||||
<Text>RPM: {currentKeyData.rpm_limit !== null ? currentKeyData.rpm_limit : "Unlimited"}</Text>
|
||||
<Text>
|
||||
Max Parallel Requests:{" "}
|
||||
{currentKeyData.max_parallel_requests !== null
|
||||
? currentKeyData.max_parallel_requests
|
||||
: "Unlimited"}
|
||||
</Text>
|
||||
<Text>
|
||||
Model TPM Limits:{" "}
|
||||
{currentKeyData.metadata?.model_tpm_limit
|
||||
? JSON.stringify(currentKeyData.metadata.model_tpm_limit)
|
||||
: "Unlimited"}
|
||||
</Text>
|
||||
<Text>
|
||||
Model RPM Limits:{" "}
|
||||
{currentKeyData.metadata?.model_rpm_limit
|
||||
? JSON.stringify(currentKeyData.metadata.model_rpm_limit)
|
||||
: "Unlimited"}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Metadata</Text>
|
||||
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
|
||||
{formatMetadataForDisplay(currentKeyData.metadata)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<ObjectPermissionsView
|
||||
objectPermission={currentKeyData.object_permission}
|
||||
variant="inline"
|
||||
className="pt-4 border-t border-gray-200"
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
|
||||
<LoggingSettingsView
|
||||
loggingConfigs={extractLoggingSettings(currentKeyData.metadata)}
|
||||
disabledCallbacks={
|
||||
Array.isArray(currentKeyData.metadata?.litellm_disabled_callbacks)
|
||||
? mapInternalToDisplayNames(currentKeyData.metadata.litellm_disabled_callbacks)
|
||||
: []
|
||||
}
|
||||
variant="inline"
|
||||
className="pt-4 border-t border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default KeyInfoView;
|
||||
|
|
@ -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 (
|
||||
<Modal open={isModalVisible} onOk={handleOk} onCancel={handleCancel} footer={null}>
|
||||
<Grid numItems={1} className="gap-2 w-full">
|
||||
<Title>Save your Key</Title>
|
||||
<Col numColSpan={1}>
|
||||
<p>
|
||||
Please save this secret key somewhere safe and accessible. For security reasons,{" "}
|
||||
<b>you will not be able to view it again</b> through your LiteLLM account. If you lose this secret key, you
|
||||
will need to generate a new one.
|
||||
</p>
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
{apiKey != null ? (
|
||||
<div>
|
||||
<Text className="mt-3">API Key:</Text>
|
||||
<div
|
||||
style={{
|
||||
background: "#f8f8f8",
|
||||
padding: "10px",
|
||||
borderRadius: "5px",
|
||||
marginBottom: "10px",
|
||||
}}
|
||||
>
|
||||
<pre style={{ wordWrap: "break-word", whiteSpace: "normal" }}>{apiKey}</pre>
|
||||
</div>
|
||||
|
||||
<CopyToClipboard text={apiKey} onCopy={handleCopy}>
|
||||
<Button className="mt-3">Copy API Key</Button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
) : (
|
||||
<Text>Key being created, this might take 30s</Text>
|
||||
)}
|
||||
</Col>
|
||||
</Grid>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaveKeyModal;
|
||||
|
|
@ -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<string | null>(null);
|
||||
const [userList, setUserList] = useState<UserResponse[]>([]);
|
||||
const [sorting, setSorting] = React.useState<SortingState>(() => {
|
||||
if (currentSort) {
|
||||
return [
|
||||
{
|
||||
id: currentSort.sortBy,
|
||||
desc: currentSort.sortOrder === "desc",
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: "created_at",
|
||||
desc: true,
|
||||
},
|
||||
];
|
||||
});
|
||||
const [expandedAccordions, setExpandedAccordions] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 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<KeyResponse>[] = [
|
||||
{
|
||||
id: "expander",
|
||||
header: () => null,
|
||||
cell: ({ row }) =>
|
||||
row.getCanExpand() ? (
|
||||
<button onClick={row.getToggleExpandedHandler()} style={{ cursor: "pointer" }}>
|
||||
{row.getIsExpanded() ? "▼" : "▶"}
|
||||
</button>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
id: "token",
|
||||
accessorKey: "token",
|
||||
header: "Key ID",
|
||||
cell: (info) => (
|
||||
<div className="overflow-hidden">
|
||||
<Tooltip title={info.getValue() as string}>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
|
||||
onClick={() => setSelectedKeyId(info.getValue() as string)}
|
||||
>
|
||||
{info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "-"}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "key_alias",
|
||||
accessorKey: "key_alias",
|
||||
header: "Key Alias",
|
||||
cell: (info) => {
|
||||
const value = info.getValue() as string;
|
||||
return (
|
||||
<Tooltip title={value}>{value ? (value.length > 20 ? `${value.slice(0, 20)}...` : value) : "-"}</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "key_name",
|
||||
accessorKey: "key_name",
|
||||
header: "Secret Key",
|
||||
cell: (info) => <span className="font-mono text-xs">{info.getValue() as string}</span>,
|
||||
},
|
||||
{
|
||||
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) => (
|
||||
<Tooltip title={info.getValue() as string}>
|
||||
{info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "-"}
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
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 ? (
|
||||
<Tooltip title={user?.user_email}>
|
||||
<span>{user?.user_email.slice(0, 20)}...</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
"-"
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "user_id",
|
||||
accessorKey: "user_id",
|
||||
header: "User ID",
|
||||
cell: (info) => {
|
||||
const userId = info.getValue() as string | null;
|
||||
if (userId && userId.length > 15) {
|
||||
return (
|
||||
<Tooltip title={userId}>
|
||||
<span>{userId.slice(0, 7)}...</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<Tooltip title={value}>
|
||||
<span>{value.slice(0, 7)}...</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<div className="flex flex-col py-2">
|
||||
{Array.isArray(models) ? (
|
||||
<div className="flex flex-col">
|
||||
{models.length === 0 ? (
|
||||
<Badge size={"xs"} className="mb-1" color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
</Badge>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-start">
|
||||
{models.length > 3 && (
|
||||
<div>
|
||||
<Icon
|
||||
icon={expandedAccordions[info.row.id] ? ChevronDownIcon : ChevronRightIcon}
|
||||
className="cursor-pointer"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setExpandedAccordions((prev) => ({
|
||||
...prev,
|
||||
[info.row.id]: !prev[info.row.id],
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{models.slice(0, 3).map((model, index) =>
|
||||
model === "all-proxy-models" ? (
|
||||
<Badge key={index} size={"xs"} color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge key={index} size={"xs"} color="blue">
|
||||
<Text>
|
||||
{model.length > 30
|
||||
? `${getModelDisplayName(model).slice(0, 30)}...`
|
||||
: getModelDisplayName(model)}
|
||||
</Text>
|
||||
</Badge>
|
||||
),
|
||||
)}
|
||||
{models.length > 3 && !expandedAccordions[info.row.id] && (
|
||||
<Badge size={"xs"} color="gray" className="cursor-pointer">
|
||||
<Text>
|
||||
+{models.length - 3} {models.length - 3 === 1 ? "more model" : "more models"}
|
||||
</Text>
|
||||
</Badge>
|
||||
)}
|
||||
{expandedAccordions[info.row.id] && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{models.slice(3).map((model, index) =>
|
||||
model === "all-proxy-models" ? (
|
||||
<Badge key={index + 3} size={"xs"} color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge key={index + 3} size={"xs"} color="blue">
|
||||
<Text>
|
||||
{model.length > 30
|
||||
? `${getModelDisplayName(model).slice(0, 30)}...`
|
||||
: getModelDisplayName(model)}
|
||||
</Text>
|
||||
</Badge>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "rate_limits",
|
||||
header: "Rate Limits",
|
||||
cell: ({ row }) => {
|
||||
const key = row.original;
|
||||
return (
|
||||
<div>
|
||||
<div>TPM: {key.tpm_limit !== null ? key.tpm_limit : "Unlimited"}</div>
|
||||
<div>RPM: {key.rpm_limit !== null ? key.rpm_limit : "Unlimited"}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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 (
|
||||
<div className="w-full h-full overflow-hidden">
|
||||
{selectedKeyId ? (
|
||||
<KeyInfoView
|
||||
keyId={selectedKeyId}
|
||||
onClose={() => 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}
|
||||
/>
|
||||
) : (
|
||||
<div className="border-b py-4 flex-1 overflow-hidden">
|
||||
<div className="w-full mb-6">
|
||||
<FilterComponent
|
||||
options={filterOptions}
|
||||
onApplyFilters={handleFilterChange}
|
||||
initialValues={filters}
|
||||
onResetFilters={handleFilterReset}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between w-full mb-4">
|
||||
<span className="inline-flex text-sm text-gray-700">
|
||||
Showing{" "}
|
||||
{isLoading
|
||||
? "..."
|
||||
: `${(pagination.currentPage - 1) * pageSize + 1} - ${Math.min(pagination.currentPage * pageSize, pagination.totalCount)}`}{" "}
|
||||
of {isLoading ? "..." : pagination.totalCount} results
|
||||
</span>
|
||||
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<span className="text-sm text-gray-700">
|
||||
Page {isLoading ? "..." : pagination.currentPage} of {isLoading ? "..." : pagination.totalPages}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => onPageChange(pagination.currentPage - 1)}
|
||||
disabled={isLoading || pagination.currentPage === 1}
|
||||
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onPageChange(pagination.currentPage + 1)}
|
||||
disabled={isLoading || pagination.currentPage === pagination.totalPages}
|
||||
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[75vh] overflow-auto">
|
||||
<div className="rounded-lg custom-border relative">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="[&_td]:py-0.5 [&_th]:py-1">
|
||||
<TableHead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHeaderCell
|
||||
key={header.id}
|
||||
className={`py-1 h-8 ${
|
||||
header.id === "actions"
|
||||
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
|
||||
: ""
|
||||
}`}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</div>
|
||||
{header.id !== "actions" && (
|
||||
<div className="w-4">
|
||||
{header.column.getIsSorted() ? (
|
||||
{
|
||||
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
|
||||
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
|
||||
}[header.column.getIsSorted() as string]
|
||||
) : (
|
||||
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableHeaderCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>🚅 Loading keys...</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : filteredKeys.length > 0 ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} className="h-8">
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{
|
||||
maxWidth: "8-x",
|
||||
whiteSpace: "pre-wrap",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${cell.column.id === "models" && (cell.getValue() as string[]).length > 3 ? "px-0" : ""}`}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>No keys found</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VirtualKeysTable;
|
||||
|
|
@ -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<FilterState>(defaultFilters);
|
||||
const [allTeams, setAllTeams] = useState<Team[]>(teams || []);
|
||||
const [allOrganizations, setAllOrganizations] = useState<Organization[]>(organizations || []);
|
||||
const [filteredKeys, setFilteredKeys] = useState<KeyResponse[]>(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<string, string>) => {
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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<boolean>(false);
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
|
|
@ -28,20 +30,21 @@ const VirtualKeysPage = () => {
|
|||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className="w-full mx-4 h-[75vh]">
|
||||
<Grid numItems={1} className="gap-2 p-8 w-full mt-2">
|
||||
<Col numColSpan={1} className="flex flex-col gap-2">
|
||||
<CreateKey team={null} userRole={userRole} data={null} addKey={addKey} />
|
||||
<VirtualKeysTable
|
||||
keys={keys}
|
||||
setKeys={setKeys}
|
||||
pagination={pagination}
|
||||
onPageChange={() => {}}
|
||||
organizations={null}
|
||||
/>
|
||||
</Col>
|
||||
</Grid>
|
||||
</div>
|
||||
<UserDashboard
|
||||
userID={userId}
|
||||
userRole={userRole}
|
||||
userEmail={userEmail}
|
||||
teams={teams}
|
||||
keys={keys}
|
||||
setUserRole={() => {}}
|
||||
setUserEmail={() => {}}
|
||||
setTeams={setTeams}
|
||||
setKeys={setKeys}
|
||||
premiumUser={premiumUser}
|
||||
organizations={organizations}
|
||||
addKey={addKey}
|
||||
createClicked={createClicked}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ interface ViewKeyTableProps {
|
|||
selectedTeam: Team | null;
|
||||
setSelectedTeam: React.Dispatch<React.SetStateAction<any | null>>;
|
||||
data: KeyResponse[] | null;
|
||||
setData: React.Dispatch<React.SetStateAction<any[] | null>>;
|
||||
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<ViewKeyTableProps> = ({
|
||||
userID,
|
||||
userRole,
|
||||
|
|
|
|||
|
|
@ -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<React.SetStateAction<string>>;
|
||||
setUserEmail: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
setTeams: React.Dispatch<React.SetStateAction<Team[] | null>>;
|
||||
setKeys: React.Dispatch<React.SetStateAction<Object[] | null>>;
|
||||
setKeys: (keys: KeyResponse[]) => void;
|
||||
premiumUser: boolean;
|
||||
organizations: Organization[] | null;
|
||||
addKey: (data: any) => void;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue