Merge pull request #20369 from BerriAI/litellm_ui_key_settings_routes

[Feature] UI - Keys: Allowed Routes to Key Info and Edit Pages
This commit is contained in:
yuneng-jiang
2026-02-03 16:10:30 -08:00
committed by GitHub
4 changed files with 575 additions and 84 deletions
@@ -1,24 +1,61 @@
import { fireEvent, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../../tests/test-utils";
import { KeyResponse } from "../key_team_helpers/key_list";
import { KeyEditView } from "./key_edit_view";
// Mock window.matchMedia
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
vi.mock("../networking", async () => {
const actual = await vi.importActual("../networking");
return {
...actual,
getPromptsList: vi.fn().mockResolvedValue({
prompts: [{ prompt_id: "prompt-1" }, { prompt_id: "prompt-2" }],
}),
modelAvailableCall: vi.fn().mockResolvedValue({
data: [{ id: "gpt-4" }, { id: "gpt-3.5-turbo" }],
}),
tagListCall: vi.fn().mockResolvedValue({
tag1: { name: "tag1", description: "Test tag 1" },
tag2: { name: "tag2", description: "Test tag 2" },
}),
getGuardrailsList: vi.fn().mockResolvedValue({
guardrails: [{ guardrail_name: "guardrail-1" }],
}),
getPoliciesList: vi.fn().mockResolvedValue({
policies: [{ policy_name: "policy-1" }],
}),
getPassThroughEndpointsCall: vi.fn().mockResolvedValue({
endpoints: [],
}),
vectorStoreListCall: vi.fn().mockResolvedValue({
data: [],
}),
mcpToolsCall: vi.fn().mockResolvedValue({
data: [],
}),
agentListCall: vi.fn().mockResolvedValue({
data: [],
}),
fetchMCPServers: vi.fn().mockResolvedValue([]),
fetchMCPAccessGroups: vi.fn().mockResolvedValue([]),
listMCPTools: vi.fn().mockResolvedValue({
tools: [],
error: null,
message: null,
stack_trace: null,
}),
getAgentsList: vi.fn().mockResolvedValue({
agents: [],
}),
getAgentAccessGroups: vi.fn().mockResolvedValue([]),
};
});
vi.mock("../organisms/create_key_button", () => ({
fetchTeamModels: vi.fn().mockResolvedValue(["team-model-1", "team-model-2"]),
}));
describe("KeyEditView", () => {
const MOCK_KEY_DATA: KeyResponse = {
token: "test-token-123",
@@ -93,8 +130,8 @@ describe("KeyEditView", () => {
const { getByText } = renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => {}}
onSubmit={async () => {}}
onCancel={() => { }}
onSubmit={async () => { }}
accessToken={""}
userID={""}
userRole={""}
@@ -111,8 +148,8 @@ describe("KeyEditView", () => {
const { getByText } = renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => {}}
onSubmit={async () => {}}
onCancel={() => { }}
onSubmit={async () => { }}
accessToken={""}
userID={""}
userRole={""}
@@ -129,8 +166,8 @@ describe("KeyEditView", () => {
const { getByLabelText } = renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => {}}
onSubmit={async () => {}}
onCancel={() => { }}
onSubmit={async () => { }}
accessToken={""}
userID={""}
userRole={""}
@@ -144,13 +181,17 @@ describe("KeyEditView", () => {
});
});
beforeEach(() => {
vi.clearAllMocks();
});
it("should call onCancel when cancel button is clicked", async () => {
const onCancelMock = vi.fn();
const { getByText } = renderWithProviders(
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={onCancelMock}
onSubmit={async () => {}}
onSubmit={async () => { }}
accessToken={""}
userID={""}
userRole={""}
@@ -159,12 +200,272 @@ describe("KeyEditView", () => {
);
await waitFor(() => {
expect(getByText("Cancel")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /cancel/i })).toBeInTheDocument();
});
const cancelButton = getByText("Cancel");
fireEvent.click(cancelButton);
const cancelButton = screen.getByRole("button", { name: /cancel/i });
await userEvent.click(cancelButton);
expect(onCancelMock).toHaveBeenCalledTimes(1);
});
it("should display key alias input field", async () => {
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => { }}
onSubmit={async () => { }}
accessToken={""}
userID={""}
userRole={""}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByLabelText("Key Alias")).toBeInTheDocument();
});
});
it("should display models select field", async () => {
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => { }}
onSubmit={async () => { }}
accessToken={""}
userID={""}
userRole={""}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByText("Models")).toBeInTheDocument();
});
});
it("should display max budget input field", async () => {
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => { }}
onSubmit={async () => { }}
accessToken={""}
userID={""}
userRole={""}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByLabelText("Max Budget (USD)")).toBeInTheDocument();
});
});
it("should display allowed routes input field", async () => {
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => { }}
onSubmit={async () => { }}
accessToken={""}
userID={""}
userRole={""}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByLabelText(/allowed routes/i)).toBeInTheDocument();
});
});
it("should call onSubmit with form values when form is submitted", async () => {
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => { }}
onSubmit={onSubmitMock}
accessToken={"test-token"}
userID={"test-user"}
userRole={"admin"}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByRole("button", { name: /save changes/i })).toBeInTheDocument();
});
const submitButton = screen.getByRole("button", { name: /save changes/i });
await userEvent.click(submitButton);
await waitFor(() => {
expect(onSubmitMock).toHaveBeenCalled();
});
});
it("should disable models field when management routes are selected", async () => {
const keyDataWithManagementRoutes = {
...MOCK_KEY_DATA,
allowed_routes: ["management_routes"],
};
renderWithProviders(
<KeyEditView
keyData={keyDataWithManagementRoutes}
onCancel={() => { }}
onSubmit={async () => { }}
accessToken={""}
userID={""}
userRole={""}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByText("Models field is disabled for this key type")).toBeInTheDocument();
});
});
it("should disable models field when info routes are selected", async () => {
const keyDataWithInfoRoutes = {
...MOCK_KEY_DATA,
allowed_routes: ["info_routes"],
};
renderWithProviders(
<KeyEditView
keyData={keyDataWithInfoRoutes}
onCancel={() => { }}
onSubmit={async () => { }}
accessToken={""}
userID={""}
userRole={""}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByText("Models field is disabled for this key type")).toBeInTheDocument();
});
});
it("should disable guardrails selector when user is not premium", async () => {
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => { }}
onSubmit={async () => { }}
accessToken={"test-token"}
userID={""}
userRole={""}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByText("Guardrails")).toBeInTheDocument();
});
});
it("should parse comma-separated allowed routes on submit", async () => {
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => { }}
onSubmit={onSubmitMock}
accessToken={"test-token"}
userID={"test-user"}
userRole={"admin"}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByLabelText(/allowed routes/i)).toBeInTheDocument();
});
const allowedRoutesInput = screen.getByLabelText(/allowed routes/i);
await userEvent.clear(allowedRoutesInput);
await userEvent.type(allowedRoutesInput, "route1, route2, route3");
const submitButton = screen.getByRole("button", { name: /save changes/i });
await userEvent.click(submitButton);
await waitFor(() => {
expect(onSubmitMock).toHaveBeenCalled();
const callArgs = onSubmitMock.mock.calls[0][0];
expect(Array.isArray(callArgs.allowed_routes)).toBe(true);
expect(callArgs.allowed_routes).toEqual(["route1", "route2", "route3"]);
});
});
it("should handle empty allowed routes string on submit", async () => {
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => { }}
onSubmit={onSubmitMock}
accessToken={"test-token"}
userID={"test-user"}
userRole={"admin"}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByLabelText(/allowed routes/i)).toBeInTheDocument();
});
const allowedRoutesInput = screen.getByLabelText(/allowed routes/i);
await userEvent.clear(allowedRoutesInput);
const submitButton = screen.getByRole("button", { name: /save changes/i });
await userEvent.click(submitButton);
await waitFor(() => {
expect(onSubmitMock).toHaveBeenCalled();
const callArgs = onSubmitMock.mock.calls[0][0];
expect(callArgs.allowed_routes).toEqual([]);
});
});
it("should disable cancel button during submission", async () => {
const onSubmitMock = vi.fn(
() =>
new Promise<void>((resolve) => {
setTimeout(resolve, 100);
}),
);
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
onCancel={() => { }}
onSubmit={onSubmitMock}
accessToken={"test-token"}
userID={"test-user"}
userRole={"admin"}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getByRole("button", { name: /cancel/i })).toBeInTheDocument();
});
const submitButton = screen.getByRole("button", { name: /save changes/i });
await userEvent.click(submitButton);
await waitFor(() => {
const cancelButton = screen.getByRole("button", { name: /cancel/i });
expect(cancelButton).toBeDisabled();
});
});
});
@@ -35,7 +35,6 @@ interface KeyEditViewProps {
// Add this helper function
const getAvailableModelsForKey = (keyData: KeyResponse, teams: any[] | null): string[] => {
// If no teams data is available, return empty array
console.log("getAvailableModelsForKey:", teams);
if (!teams || !keyData.team_id) {
return [];
}
@@ -172,7 +171,9 @@ export function KeyEditView({
: [],
auto_rotate: keyData.auto_rotate || false,
...(keyData.rotation_interval && { rotation_interval: keyData.rotation_interval }),
allowed_routes: keyData.allowed_routes,
allowed_routes: Array.isArray(keyData.allowed_routes) && keyData.allowed_routes.length > 0
? keyData.allowed_routes.join(", ")
: "",
};
useEffect(() => {
@@ -197,7 +198,9 @@ export function KeyEditView({
: [],
auto_rotate: keyData.auto_rotate || false,
...(keyData.rotation_interval && { rotation_interval: keyData.rotation_interval }),
allowed_routes: keyData.allowed_routes,
allowed_routes: Array.isArray(keyData.allowed_routes) && keyData.allowed_routes.length > 0
? keyData.allowed_routes.join(", ")
: "",
});
}, [keyData, form]);
@@ -226,11 +229,24 @@ export function KeyEditView({
fetchTags();
}, [accessToken]);
console.log("premiumUser:", premiumUser);
const handleSubmit = async (values: any) => {
try {
setIsKeySaving(true);
// Parse allowed_routes from comma-separated string to array
if (typeof values.allowed_routes === "string") {
const trimmedInput = values.allowed_routes.trim();
if (trimmedInput === "") {
values.allowed_routes = [];
} else {
values.allowed_routes = trimmedInput
.split(",")
.map((route: string) => route.trim())
.filter((route: string) => route.length > 0);
}
}
// If it's already an array (shouldn't happen, but handle it), keep as is
await onSubmit(values);
} finally {
setIsKeySaving(false);
@@ -251,7 +267,11 @@ export function KeyEditView({
}
>
{({ getFieldValue, setFieldValue }) => {
const allowedRoutes = getFieldValue("allowed_routes") || [];
const allowedRoutesValue = getFieldValue("allowed_routes") || "";
// Convert string to array for checking
const allowedRoutes = typeof allowedRoutesValue === "string" && allowedRoutesValue.trim() !== ""
? allowedRoutesValue.split(",").map((r: string) => r.trim()).filter((r: string) => r.length > 0)
: [];
const isDisabled = allowedRoutes.includes("management_routes") || allowedRoutes.includes("info_routes");
const models = getFieldValue("models") || [];
@@ -290,7 +310,11 @@ export function KeyEditView({
shouldUpdate={(prevValues, currentValues) => prevValues.allowed_routes !== currentValues.allowed_routes}
>
{({ getFieldValue, setFieldValue }) => {
const allowedRoutes = getFieldValue("allowed_routes");
const allowedRoutesValue = getFieldValue("allowed_routes") || "";
// Convert string to array for getKeyTypeFromRoutes
const allowedRoutes = typeof allowedRoutesValue === "string" && allowedRoutesValue.trim() !== ""
? allowedRoutesValue.split(",").map((r: string) => r.trim()).filter((r: string) => r.length > 0)
: [];
const keyTypeValue = getKeyTypeFromRoutes(allowedRoutes);
return (
@@ -302,13 +326,13 @@ export function KeyEditView({
onChange={(value) => {
switch (value) {
case "default":
setFieldValue("allowed_routes", []);
setFieldValue("allowed_routes", "");
break;
case "llm_api":
setFieldValue("allowed_routes", ["llm_api_routes"]);
setFieldValue("allowed_routes", "llm_api_routes");
break;
case "management":
setFieldValue("allowed_routes", ["management_routes"]);
setFieldValue("allowed_routes", "management_routes");
setFieldValue("models", []);
break;
}
@@ -344,6 +368,22 @@ export function KeyEditView({
</Form.Item>
</Form.Item>
<Form.Item
label={
<span>
Allowed Routes{" "}
<Tooltip title="List of allowed routes for the key (comma-separated). Can be specific routes (e.g., '/chat/completions') or route patterns (e.g., 'llm_api_routes', 'management_routes', '/keys/*'). Leave empty to allow all routes.">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="allowed_routes"
>
<Input
placeholder="Enter allowed routes (comma-separated). Special values: llm_api_routes, management_routes. Examples: llm_api_routes, /chat/completions, /keys/*. Leave empty to allow all routes"
/>
</Form.Item>
<Form.Item label="Max Budget (USD)" name="max_budget">
<NumericalInput step={0.01} style={{ width: "100%" }} placeholder="Enter a numerical value" />
</Form.Item>
@@ -473,7 +513,7 @@ export function KeyEditView({
!premiumUser
? "Premium feature - Upgrade to set allowed pass through routes by key"
: Array.isArray(keyData.metadata?.allowed_passthrough_routes) &&
keyData.metadata.allowed_passthrough_routes.length > 0
keyData.metadata.allowed_passthrough_routes.length > 0
? `Current: ${keyData.metadata.allowed_passthrough_routes.join(", ")}`
: "Select or enter allowed pass through routes"
}
@@ -590,11 +630,6 @@ export function KeyEditView({
<Input />
</Form.Item>
{/* Hidden form field for allowed_routes */}
<Form.Item name="allowed_routes" hidden>
<Input />
</Form.Item>
{/* Hidden form field for disabled callbacks */}
<Form.Item name="disabled_callbacks" hidden>
<Input />
@@ -1,6 +1,7 @@
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { KeyResponse, Team } from "../key_team_helpers/key_list";
import KeyInfoView from "./key_info_view";
@@ -13,6 +14,21 @@ vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: vi.fn(),
}));
vi.mock("../networking", () => ({
keyDeleteCall: vi.fn().mockResolvedValue({}),
keyUpdateCall: vi.fn().mockResolvedValue({}),
getPolicyInfoWithGuardrails: vi.fn().mockResolvedValue({
resolved_guardrails: ["guardrail-1", "guardrail-2"],
}),
}));
vi.mock("@/utils/dataUtils", () => ({
copyToClipboard: vi.fn().mockResolvedValue(true),
formatNumberWithCommas: vi.fn((value: number, decimals?: number) => {
return value.toFixed(decimals ?? 2);
}),
}));
describe("KeyInfoView", () => {
beforeEach(() => {
vi.mocked(useTeams).mockReturnValue({
@@ -105,34 +121,34 @@ describe("KeyInfoView", () => {
it("should render tags", async () => {
vi.mocked(useAuthorized).mockReturnValue(baseUseAuthorizedMock);
const { getByText } = render(
render(
<KeyInfoView
keyData={MOCK_KEY_DATA}
onClose={() => {}}
onClose={() => { }}
keyId={"test-key-id"}
onKeyDataUpdate={() => {}}
onKeyDataUpdate={() => { }}
teams={[]}
/>,
);
await waitFor(() => {
expect(getByText("test-tag")).toBeInTheDocument();
expect(screen.getByText("test-tag")).toBeInTheDocument();
});
});
it("should not render tags in metadata textarea", async () => {
vi.mocked(useAuthorized).mockReturnValue(baseUseAuthorizedMock);
const { container, getByText } = render(
const { container } = render(
<KeyInfoView
keyData={MOCK_KEY_DATA}
onClose={() => {}}
onClose={() => { }}
keyId={"test-key-id"}
onKeyDataUpdate={() => {}}
onKeyDataUpdate={() => { }}
teams={[]}
/>,
);
await waitFor(() => {
expect(getByText("Metadata")).toBeInTheDocument();
expect(screen.getByText("Metadata")).toBeInTheDocument();
const metadataBlock = container.querySelector("pre");
expect(metadataBlock).toBeInTheDocument();
expect(metadataBlock?.textContent?.trim()).toBe("{}");
@@ -153,7 +169,7 @@ describe("KeyInfoView", () => {
const keyData = { ...MOCK_KEY_DATA, user_id: "other-user-id" };
render(
<KeyInfoView keyData={keyData} onClose={() => {}} keyId={"test-key-id"} onKeyDataUpdate={() => {}} teams={[]} />,
<KeyInfoView keyData={keyData} onClose={() => { }} keyId={"test-key-id"} onKeyDataUpdate={() => { }} teams={[]} />,
);
await waitFor(() => {
@@ -182,6 +198,7 @@ describe("KeyInfoView", () => {
role: "admin",
},
],
spend: 0,
};
vi.mocked(useTeams).mockReturnValue({
@@ -197,7 +214,7 @@ describe("KeyInfoView", () => {
const keyData = { ...MOCK_KEY_DATA, team_id: teamId, user_id: "other-user-id" };
render(
<KeyInfoView keyData={keyData} onClose={() => {}} keyId={"test-key-id"} onKeyDataUpdate={() => {}} teams={[]} />,
<KeyInfoView keyData={keyData} onClose={() => { }} keyId={"test-key-id"} onKeyDataUpdate={() => { }} teams={[]} />,
);
await waitFor(() => {
@@ -221,7 +238,7 @@ describe("KeyInfoView", () => {
const ownerUserId = "owner-user-id";
const keyData = { ...MOCK_KEY_DATA, user_id: ownerUserId };
render(
<KeyInfoView keyData={keyData} onClose={() => {}} keyId={"test-key-id"} onKeyDataUpdate={() => {}} teams={[]} />,
<KeyInfoView keyData={keyData} onClose={() => { }} keyId={"test-key-id"} onKeyDataUpdate={() => { }} teams={[]} />,
);
await waitFor(() => {
@@ -244,7 +261,7 @@ describe("KeyInfoView", () => {
const keyData = { ...MOCK_KEY_DATA, user_id: "owner-user-id" };
render(
<KeyInfoView keyData={keyData} onClose={() => {}} keyId={"test-key-id"} onKeyDataUpdate={() => {}} teams={[]} />,
<KeyInfoView keyData={keyData} onClose={() => { }} keyId={"test-key-id"} onKeyDataUpdate={() => { }} teams={[]} />,
);
await waitFor(() => {
@@ -268,7 +285,7 @@ describe("KeyInfoView", () => {
const ownerUserId = "internal-viewer-user-id";
const keyData = { ...MOCK_KEY_DATA, user_id: ownerUserId };
render(
<KeyInfoView keyData={keyData} onClose={() => {}} keyId={"test-key-id"} onKeyDataUpdate={() => {}} teams={[]} />,
<KeyInfoView keyData={keyData} onClose={() => { }} keyId={"test-key-id"} onKeyDataUpdate={() => { }} teams={[]} />,
);
await waitFor(() => {
@@ -296,6 +313,7 @@ describe("KeyInfoView", () => {
role: "admin",
},
],
spend: 0,
};
vi.mocked(useTeams).mockReturnValue({
@@ -309,10 +327,9 @@ describe("KeyInfoView", () => {
userRole: "user",
});
// Key has a different team_id that doesn't match any team in teamsData
const keyData = { ...MOCK_KEY_DATA, team_id: "non-matching-team-id", user_id: "other-user-id" };
render(
<KeyInfoView keyData={keyData} onClose={() => {}} keyId={"test-key-id"} onKeyDataUpdate={() => {}} teams={[]} />,
<KeyInfoView keyData={keyData} onClose={() => { }} keyId={"test-key-id"} onKeyDataUpdate={() => { }} teams={[]} />,
);
await waitFor(() => {
@@ -320,4 +337,129 @@ describe("KeyInfoView", () => {
expect(screen.queryByText("Delete Key")).not.toBeInTheDocument();
});
});
it("should call onClose when back button is clicked", async () => {
vi.mocked(useAuthorized).mockReturnValue(baseUseAuthorizedMock);
const onCloseMock = vi.fn();
render(
<KeyInfoView
keyData={MOCK_KEY_DATA}
onClose={onCloseMock}
keyId={"test-key-id"}
onKeyDataUpdate={() => { }}
teams={[]}
/>,
);
await waitFor(() => {
expect(screen.getByRole("button", { name: /back to keys/i })).toBeInTheDocument();
});
const backButton = screen.getByRole("button", { name: /back to keys/i });
await userEvent.click(backButton);
expect(onCloseMock).toHaveBeenCalledTimes(1);
});
it("should show edit button in settings tab when user has write access", async () => {
vi.mocked(useAuthorized).mockReturnValue({
...baseUseAuthorizedMock,
userRole: "Admin",
});
render(
<KeyInfoView
keyData={MOCK_KEY_DATA}
onClose={() => { }}
keyId={"test-key-id"}
onKeyDataUpdate={() => { }}
teams={[]}
/>,
);
await waitFor(() => {
const settingsTab = screen.getByRole("tab", { name: /settings/i });
expect(settingsTab).toBeInTheDocument();
});
const settingsTab = screen.getByRole("tab", { name: /settings/i });
await userEvent.click(settingsTab);
await waitFor(() => {
expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument();
});
});
it("should display guardrails when present", async () => {
vi.mocked(useAuthorized).mockReturnValue(baseUseAuthorizedMock);
const keyDataWithGuardrails = {
...MOCK_KEY_DATA,
metadata: {
...MOCK_KEY_DATA.metadata,
guardrails: ["guardrail-1", "guardrail-2"],
},
};
render(
<KeyInfoView
keyData={keyDataWithGuardrails}
onClose={() => { }}
keyId={"test-key-id"}
onKeyDataUpdate={() => { }}
teams={[]}
/>,
);
await waitFor(() => {
expect(screen.getByText("Guardrails")).toBeInTheDocument();
});
});
it("should display policies when present", async () => {
vi.mocked(useAuthorized).mockReturnValue(baseUseAuthorizedMock);
const keyDataWithPolicies = {
...MOCK_KEY_DATA,
metadata: {
...MOCK_KEY_DATA.metadata,
policies: ["policy-1"],
},
};
render(
<KeyInfoView
keyData={keyDataWithPolicies}
onClose={() => { }}
keyId={"test-key-id"}
onKeyDataUpdate={() => { }}
teams={[]}
/>,
);
await waitFor(() => {
expect(screen.getByText("Policies")).toBeInTheDocument();
});
});
it("should display no key found message when keyData is undefined", async () => {
vi.mocked(useAuthorized).mockReturnValue(baseUseAuthorizedMock);
render(
<KeyInfoView
keyData={undefined}
onClose={() => { }}
keyId={"test-key-id"}
onKeyDataUpdate={() => { }}
teams={[]}
/>,
);
await waitFor(() => {
expect(screen.getByText("Key not found")).toBeInTheDocument();
});
});
});
@@ -1,9 +1,10 @@
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import { formatNumberWithCommas, copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
import { ArrowLeftIcon, RefreshIcon, TrashIcon } from "@heroicons/react/outline";
import { Badge, Button, Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title } from "@tremor/react";
import { Button as AntdButton, Form, Tooltip } from "antd";
import { Button as AntdButton, Form, Tag, Tooltip } from "antd";
import { CheckIcon, CopyIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { isProxyAdminRole, isUserTeamAdminForSingleTeam, rolesWithWriteAccess } from "../../utils/roles";
@@ -14,12 +15,11 @@ import { extractLoggingSettings, formatMetadataForDisplay, stripTagsFromMetadata
import { KeyResponse } from "../key_team_helpers/key_list";
import LoggingSettingsView from "../logging_settings_view";
import NotificationManager from "../molecules/notifications_manager";
import { keyDeleteCall, keyUpdateCall, getPolicyInfoWithGuardrails } from "../networking";
import { getPolicyInfoWithGuardrails, keyDeleteCall, keyUpdateCall } from "../networking";
import ObjectPermissionsView from "../object_permissions_view";
import { RegenerateKeyModal } from "../organisms/regenerate_key_modal";
import { parseErrorMessage } from "../shared/errorUtils";
import { KeyEditView } from "./key_edit_view";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
interface KeyInfoViewProps {
keyId: string;
@@ -206,8 +206,8 @@ export default function KeyInfoView({
...(formValues.logging_settings ? { logging: formValues.logging_settings } : {}),
...(formValues.disabled_callbacks?.length > 0
? {
litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks),
}
litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks),
}
: {}),
};
} catch (error) {
@@ -225,8 +225,8 @@ export default function KeyInfoView({
...(formValues.logging_settings ? { logging: formValues.logging_settings } : {}),
...(formValues.disabled_callbacks?.length > 0
? {
litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks),
}
litellm_disabled_callbacks: mapDisplayToInternalNames(formValues.disabled_callbacks),
}
: {}),
};
}
@@ -334,7 +334,6 @@ export default function KeyInfoView({
});
return `${dateStr} at ${timeStr}`;
};
console.log("userRole", userRole);
const canModifyKey =
isProxyAdminRole(userRole || "") ||
@@ -364,11 +363,10 @@ export default function KeyInfoView({
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"
}`}
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>
@@ -691,10 +689,10 @@ export default function KeyInfoView({
<div className="flex flex-wrap gap-2 mt-1">
{Array.isArray(currentKeyData.metadata?.tags) && currentKeyData.metadata.tags.length > 0
? currentKeyData.metadata.tags.map((tag, index) => (
<span key={index} className="px-2 mr-2 py-1 bg-blue-100 rounded text-xs">
{tag}
</span>
))
<span key={index} className="px-2 mr-2 py-1 bg-blue-100 rounded text-xs">
{tag}
</span>
))
: "No tags specified"}
</div>
</div>
@@ -704,24 +702,39 @@ export default function KeyInfoView({
<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>
))
<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">Allowed Routes</Text>
<div className="flex flex-wrap gap-2 mt-1">
{Array.isArray(currentKeyData.allowed_routes) && currentKeyData.allowed_routes.length > 0 ? (
currentKeyData.allowed_routes.map((route, index) => (
<span key={index} className="px-2 py-1 bg-blue-100 rounded text-xs">
{route}
</span>
))
) : (
<Tag color="green">All routes allowed</Tag>
)}
</div>
</div>
<div>
<Text className="font-medium">Allowed Pass Through Routes</Text>
<Text>
{Array.isArray(currentKeyData.metadata?.allowed_passthrough_routes) &&
currentKeyData.metadata.allowed_passthrough_routes.length > 0
currentKeyData.metadata.allowed_passthrough_routes.length > 0
? currentKeyData.metadata.allowed_passthrough_routes.map((route, index) => (
<span key={index} className="px-2 mr-2 py-1 bg-blue-100 rounded text-xs">
{route}
</span>
))
<span key={index} className="px-2 mr-2 py-1 bg-blue-100 rounded text-xs">
{route}
</span>
))
: "No pass through routes specified"}
</Text>
</div>