mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
Merge pull request #18504 from BerriAI/litellm_ui_keys_table_refactor
[Refactor] UI - Keys Table
This commit is contained in:
commit
566eb35747
11 changed files with 124 additions and 354 deletions
|
|
@ -268,16 +268,7 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
|
|||
|
||||
{/* Content */}
|
||||
<div className="p-6 h-full">
|
||||
<KeyInfoView
|
||||
keyId={selectedKey}
|
||||
onClose={handleClose}
|
||||
keyData={keyData}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
teams={teams}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
<KeyInfoView keyId={selectedKey} onClose={handleClose} keyData={keyData} teams={teams} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { vi, it, expect } from "vitest";
|
||||
import { renderWithProviders } from "../../tests/test-utils";
|
||||
import { AllKeysTable } from "./all_keys_table";
|
||||
import { KeyResponse, Team } from "./key_team_helpers/key_list";
|
||||
import { Organization } from "./networking";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import { VirtualKeysTable } from "./VirtualKeysTable";
|
||||
import { KeyResponse, Team } from "../key_team_helpers/key_list";
|
||||
import { Organization } from "../networking";
|
||||
|
||||
// Mock network calls
|
||||
vi.mock("./networking", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./networking")>();
|
||||
const actual = await importOriginal<typeof import("../networking")>();
|
||||
return {
|
||||
...actual,
|
||||
userListCall: vi.fn().mockResolvedValue({
|
||||
|
|
@ -131,7 +131,7 @@ const mockOrganization: Organization = {
|
|||
members: [],
|
||||
};
|
||||
|
||||
it("should render AllKeysTable component", () => {
|
||||
it("should render VirtualKeysTable component", () => {
|
||||
const mockProps = {
|
||||
keys: [mockKey],
|
||||
setKeys: vi.fn(),
|
||||
|
|
@ -156,7 +156,7 @@ it("should render AllKeysTable component", () => {
|
|||
premiumUser: false,
|
||||
};
|
||||
|
||||
renderWithProviders(<AllKeysTable {...mockProps} />);
|
||||
renderWithProviders(<VirtualKeysTable {...mockProps} />);
|
||||
|
||||
expect(screen.getByText("Test Key Alias")).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -186,7 +186,7 @@ it("should display key information correctly", async () => {
|
|||
premiumUser: false,
|
||||
};
|
||||
|
||||
renderWithProviders(<AllKeysTable {...mockProps} />);
|
||||
renderWithProviders(<VirtualKeysTable {...mockProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Key Alias")).toBeInTheDocument();
|
||||
|
|
@ -220,7 +220,7 @@ it("should display user email correctly", async () => {
|
|||
premiumUser: false,
|
||||
};
|
||||
|
||||
renderWithProviders(<AllKeysTable {...mockProps} />);
|
||||
renderWithProviders(<VirtualKeysTable {...mockProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("user@example.com")).toBeInTheDocument();
|
||||
|
|
@ -4,8 +4,6 @@ import { formatNumberWithCommas, updateExistingKeys } from "@/utils/dataUtils";
|
|||
import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, SwitchVerticalIcon } from "@heroicons/react/outline";
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnResizeDirection,
|
||||
ColumnResizeMode,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
|
|
@ -16,8 +14,6 @@ import {
|
|||
Badge,
|
||||
Button,
|
||||
Icon,
|
||||
Select,
|
||||
SelectItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
|
|
@ -28,14 +24,15 @@ import {
|
|||
} from "@tremor/react";
|
||||
import { Tooltip } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
|
||||
import { useFilterLogic } from "./key_team_helpers/filter_logic";
|
||||
import { KeyResponse, Team } from "./key_team_helpers/key_list";
|
||||
import FilterComponent, { FilterOption } from "./molecules/filter";
|
||||
import { Organization } from "./networking";
|
||||
import KeyInfoView from "./templates/key_info_view";
|
||||
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
|
||||
import { useFilterLogic } from "../key_team_helpers/filter_logic";
|
||||
import { KeyResponse, Team } from "../key_team_helpers/key_list";
|
||||
import FilterComponent, { FilterOption } from "../molecules/filter";
|
||||
import { Organization } from "../networking";
|
||||
import KeyInfoView from "../templates/key_info_view";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
interface AllKeysTableProps {
|
||||
interface VirtualKeysTableProps {
|
||||
keys: KeyResponse[];
|
||||
setKeys: (keys: KeyResponse[] | ((prev: KeyResponse[]) => KeyResponse[])) => void;
|
||||
isLoading?: boolean;
|
||||
|
|
@ -51,9 +48,6 @@ interface AllKeysTableProps {
|
|||
setSelectedTeam: (team: Team | null) => void;
|
||||
selectedKeyAlias: string | null;
|
||||
setSelectedKeyAlias: Setter<string | null>;
|
||||
accessToken: string | null;
|
||||
userID: string | null;
|
||||
userRole: string | null;
|
||||
organizations: Organization[] | null;
|
||||
setCurrentOrg: React.Dispatch<React.SetStateAction<Organization | null>>;
|
||||
refresh?: () => void;
|
||||
|
|
@ -62,61 +56,14 @@ interface AllKeysTableProps {
|
|||
sortBy: string;
|
||||
sortOrder: "asc" | "desc";
|
||||
};
|
||||
premiumUser: boolean;
|
||||
setAccessToken?: (token: string) => void;
|
||||
}
|
||||
|
||||
// Define columns similar to our logs table
|
||||
|
||||
interface UserResponse {
|
||||
user_id: string;
|
||||
user_email: string;
|
||||
user_role: string;
|
||||
}
|
||||
|
||||
const TeamFilter = ({
|
||||
teams,
|
||||
selectedTeam,
|
||||
setSelectedTeam,
|
||||
}: {
|
||||
teams: Team[] | null;
|
||||
selectedTeam: Team | null;
|
||||
setSelectedTeam: (team: Team | null) => void;
|
||||
}) => {
|
||||
const handleTeamChange = (value: string) => {
|
||||
const team = teams?.find((t) => t.team_id === value);
|
||||
setSelectedTeam(team || null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">Where Team is</span>
|
||||
<Select
|
||||
value={selectedTeam?.team_id || ""}
|
||||
onValueChange={handleTeamChange}
|
||||
placeholder="Team ID"
|
||||
className="w-[400px]"
|
||||
>
|
||||
<SelectItem value="team_id">Team ID</SelectItem>
|
||||
{teams?.map((team) => (
|
||||
<SelectItem key={team.team_id} value={team.team_id}>
|
||||
<span className="font-medium">{team.team_alias}</span>{" "}
|
||||
<span className="text-gray-500">({team.team_id})</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* AllKeysTable – a new table for keys that mimics the table styling used in view_logs.
|
||||
* VirtualKeysTable – a new table for keys that mimics the table styling used in view_logs.
|
||||
* The team selector and filtering have been removed so that all keys are shown.
|
||||
*/
|
||||
|
||||
export function AllKeysTable({
|
||||
export function VirtualKeysTable({
|
||||
keys,
|
||||
setKeys,
|
||||
isLoading = false,
|
||||
|
|
@ -124,24 +71,13 @@ export function AllKeysTable({
|
|||
onPageChange,
|
||||
pageSize = 50,
|
||||
teams,
|
||||
selectedTeam,
|
||||
setSelectedTeam,
|
||||
selectedKeyAlias,
|
||||
setSelectedKeyAlias,
|
||||
accessToken,
|
||||
userID,
|
||||
userRole,
|
||||
organizations,
|
||||
setCurrentOrg,
|
||||
refresh,
|
||||
onSortChange,
|
||||
currentSort,
|
||||
premiumUser,
|
||||
setAccessToken,
|
||||
}: AllKeysTableProps) {
|
||||
}: VirtualKeysTableProps) {
|
||||
const { accessToken, userId: userID, userRole, premiumUser } = useAuthorized();
|
||||
const [selectedKeyId, setSelectedKeyId] = useState<string | null>(null);
|
||||
const [columnResizeMode, setColumnResizeMode] = React.useState<ColumnResizeMode>("onChange");
|
||||
const [columnResizeDirection, setColumnResizeDirection] = React.useState<ColumnResizeDirection>("ltr");
|
||||
const [sorting, setSorting] = React.useState<SortingState>(() => {
|
||||
if (currentSort) {
|
||||
return [
|
||||
|
|
@ -167,7 +103,6 @@ export function AllKeysTable({
|
|||
keys,
|
||||
teams,
|
||||
organizations,
|
||||
accessToken,
|
||||
});
|
||||
|
||||
// Add a useEffect to call refresh when a key is created
|
||||
|
|
@ -557,8 +492,8 @@ export function AllKeysTable({
|
|||
const table = useReactTable({
|
||||
data: filteredKeys,
|
||||
columns: columns.filter((col) => col.id !== "expander"),
|
||||
columnResizeMode,
|
||||
columnResizeDirection,
|
||||
columnResizeMode: "onChange",
|
||||
columnResizeDirection: "ltr",
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
|
|
@ -619,12 +554,7 @@ export function AllKeysTable({
|
|||
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">
|
||||
|
|
@ -736,10 +666,6 @@ export function AllKeysTable({
|
|||
userSelect: "none",
|
||||
touchAction: "none",
|
||||
opacity: header.column.getIsResizing() ? 1 : 0,
|
||||
transform:
|
||||
columnResizeMode === "onEnd" && header.column.getIsResizing()
|
||||
? `translateX(${(table.options.columnResizeDirection === "rtl" ? -1 : 1) * (table.getState().columnSizingInfo.deltaOffset ?? 0)}px)`
|
||||
: "",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -6,6 +6,7 @@ import { useQuery } from "@tanstack/react-query";
|
|||
import { fetchAllKeyAliases, fetchAllOrganizations, fetchAllTeams } from "./filter_helpers";
|
||||
import { debounce } from "lodash";
|
||||
import { defaultPageSize } from "../constants";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
export interface FilterState {
|
||||
"Team ID": string;
|
||||
|
|
@ -21,12 +22,10 @@ export function useFilterLogic({
|
|||
keys,
|
||||
teams,
|
||||
organizations,
|
||||
accessToken,
|
||||
}: {
|
||||
keys: KeyResponse[];
|
||||
teams: Team[] | null;
|
||||
organizations: Organization[] | null;
|
||||
accessToken: string | null;
|
||||
}) {
|
||||
const defaultFilters: FilterState = {
|
||||
"Team ID": "",
|
||||
|
|
@ -36,6 +35,7 @@ export function useFilterLogic({
|
|||
"Sort By": "created_at",
|
||||
"Sort Order": "desc",
|
||||
};
|
||||
const { accessToken } = useAuthorized();
|
||||
const [filters, setFilters] = useState<FilterState>(defaultFilters);
|
||||
const [allTeams, setAllTeams] = useState<Team[]>(teams || []);
|
||||
const [allOrganizations, setAllOrganizations] = useState<Organization[]>(organizations || []);
|
||||
|
|
|
|||
|
|
@ -401,7 +401,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
|||
console.log("key create Response:", response);
|
||||
|
||||
// Add the data to the state in the parent component
|
||||
// Also directly update the keys list in AllKeysTable without an API call
|
||||
// Also directly update the keys list in VirtualKeysTable without an API call
|
||||
addKey(response);
|
||||
|
||||
setApiKey(response["key"]);
|
||||
|
|
|
|||
|
|
@ -1,31 +1,22 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Text, TextInput, Title, Grid, Col } from "@tremor/react";
|
||||
import { Modal, Form, InputNumber } from "antd";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { Button, Col, Grid, Text, TextInput, Title } from "@tremor/react";
|
||||
import { Form, InputNumber, Modal } from "antd";
|
||||
import { add } from "date-fns";
|
||||
import { regenerateKeyCall } from "../networking";
|
||||
import { KeyResponse } from "../key_team_helpers/key_list";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { KeyResponse } from "../key_team_helpers/key_list";
|
||||
import NotificationManager from "../molecules/notifications_manager";
|
||||
import { regenerateKeyCall } from "../networking";
|
||||
|
||||
interface RegenerateKeyModalProps {
|
||||
selectedToken: KeyResponse | null;
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
accessToken: string | null;
|
||||
premiumUser: boolean;
|
||||
setAccessToken?: (token: string) => void;
|
||||
onKeyUpdate?: (updatedKeyData: Partial<KeyResponse>) => void;
|
||||
}
|
||||
|
||||
export function RegenerateKeyModal({
|
||||
selectedToken,
|
||||
visible,
|
||||
onClose,
|
||||
accessToken,
|
||||
premiumUser,
|
||||
setAccessToken,
|
||||
onKeyUpdate,
|
||||
}: RegenerateKeyModalProps) {
|
||||
export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdate }: RegenerateKeyModalProps) {
|
||||
const { accessToken } = useAuthorized();
|
||||
const [form] = Form.useForm();
|
||||
const [regeneratedKey, setRegeneratedKey] = useState<string | null>(null);
|
||||
const [regenerateFormData, setRegenerateFormData] = useState<any>(null);
|
||||
|
|
@ -132,14 +123,6 @@ export function RegenerateKeyModal({
|
|||
|
||||
console.log("Updated key data with new token:", updatedKeyData); // Debug log
|
||||
|
||||
// If user regenerated their own auth key, update both local and global access tokens
|
||||
if (isOwnKey) {
|
||||
setCurrentAccessToken(response.key); // Update local token immediately
|
||||
if (setAccessToken) {
|
||||
setAccessToken(response.key); // Update global token
|
||||
}
|
||||
}
|
||||
|
||||
// Update the parent component with new key data
|
||||
if (onKeyUpdate) {
|
||||
onKeyUpdate(updatedKeyData);
|
||||
|
|
|
|||
|
|
@ -2,15 +2,21 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
// ---- Hoisted shared mocks (safe to use inside vi.mock factories) ----
|
||||
const { keyUpdateCallMock, keyDeleteCallMock } = vi.hoisted(() => {
|
||||
const { keyUpdateCallMock, keyDeleteCallMock, mockUseAuthorized } = vi.hoisted(() => {
|
||||
return {
|
||||
keyUpdateCallMock: vi.fn().mockResolvedValue({}),
|
||||
keyDeleteCallMock: vi.fn().mockResolvedValue({}),
|
||||
mockUseAuthorized: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// ---- Module mocks ----
|
||||
|
||||
// Mock useAuthorized hook FIRST (before component imports it)
|
||||
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
|
||||
default: mockUseAuthorized,
|
||||
}));
|
||||
|
||||
// Networking: wire the hoisted fns so we can assert calls later
|
||||
vi.mock("../networking", () => {
|
||||
return {
|
||||
|
|
@ -29,12 +35,12 @@ vi.mock("../molecules/notifications_manager", () => {
|
|||
return { default: Notifications };
|
||||
});
|
||||
|
||||
// Roles: ensure 'admin' has write access and include all role helper functions
|
||||
// Roles: ensure 'Admin' has write access and include all role helper functions
|
||||
vi.mock("../../utils/roles", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../utils/roles")>();
|
||||
return {
|
||||
...actual,
|
||||
rolesWithWriteAccess: ["admin"],
|
||||
rolesWithWriteAccess: ["Admin"],
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -243,20 +249,6 @@ vi.mock("@/app/(dashboard)/hooks/useTeams", () => ({
|
|||
})),
|
||||
}));
|
||||
|
||||
// Mock useAuthorized hook to avoid Next.js router dependency
|
||||
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
|
||||
default: vi.fn(() => ({
|
||||
accessToken: "access_abc",
|
||||
userId: "user_1",
|
||||
userRole: "admin",
|
||||
premiumUser: true,
|
||||
token: "token_123",
|
||||
userEmail: "test@example.com",
|
||||
disabledPersonalKeyCreation: false,
|
||||
showSSOBanner: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
// KeyEditView mock: triggers onSubmit with our injected form values
|
||||
vi.mock("./key_edit_view", async () => {
|
||||
const React = await import("react");
|
||||
|
|
@ -302,21 +294,29 @@ const baseKeyData = {
|
|||
next_rotation_at: null as any,
|
||||
};
|
||||
|
||||
const renderView = (premiumUser: boolean) =>
|
||||
render(
|
||||
const renderView = (premiumUser: boolean) => {
|
||||
// Configure the mock for this test
|
||||
mockUseAuthorized.mockReturnValue({
|
||||
accessToken: "access_abc",
|
||||
userId: "user_1",
|
||||
userRole: "Admin",
|
||||
premiumUser,
|
||||
token: "token_123",
|
||||
userEmail: "test@example.com",
|
||||
disabledPersonalKeyCreation: false,
|
||||
showSSOBanner: false,
|
||||
});
|
||||
|
||||
return render(
|
||||
<KeyInfoView
|
||||
keyId="tok_123"
|
||||
onClose={() => {}}
|
||||
keyData={baseKeyData as any}
|
||||
onKeyDataUpdate={() => {}}
|
||||
accessToken="access_abc"
|
||||
userID="user_1"
|
||||
userRole="admin"
|
||||
teams={[]}
|
||||
premiumUser={premiumUser}
|
||||
setAccessToken={() => {}}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
|
@ -328,6 +328,7 @@ describe("KeyInfoView handleKeyUpdate premium guard", () => {
|
|||
it("removes guardrails & prompts for non-premium users and prevents metadata.guardrails", async () => {
|
||||
renderView(false); // premiumUser = false
|
||||
|
||||
fireEvent.click(screen.getByText("Settings"));
|
||||
fireEvent.click(screen.getByText("Edit Settings"));
|
||||
(globalThis as any).__TEST_FORM_VALUES = {
|
||||
token: "tok_123",
|
||||
|
|
@ -352,6 +353,7 @@ describe("KeyInfoView handleKeyUpdate premium guard", () => {
|
|||
it("preserves guardrails & prompts for premium users and includes metadata.guardrails", async () => {
|
||||
renderView(true); // premiumUser = true
|
||||
|
||||
fireEvent.click(screen.getByText("Settings"));
|
||||
fireEvent.click(screen.getByText("Edit Settings"));
|
||||
(globalThis as any).__TEST_FORM_VALUES = {
|
||||
token: "tok_123",
|
||||
|
|
@ -378,6 +380,7 @@ describe("KeyInfoView handleKeyUpdate empty strings", () => {
|
|||
it(`maps empty strings to null for ${limit}`, async () => {
|
||||
renderView(true); // premiumUser = true
|
||||
|
||||
fireEvent.click(screen.getByText("Settings"));
|
||||
fireEvent.click(screen.getByText("Edit Settings"));
|
||||
(globalThis as any).__TEST_FORM_VALUES = {
|
||||
token: "tok_123",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import useTeams from "@/app/(dashboard)/hooks/useTeams";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { KeyResponse, Team } from "../key_team_helpers/key_list";
|
||||
|
|
@ -8,6 +9,10 @@ vi.mock("@/app/(dashboard)/hooks/useTeams", () => ({
|
|||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("KeyInfoView", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(useTeams).mockReturnValue({
|
||||
|
|
@ -85,17 +90,27 @@ describe("KeyInfoView", () => {
|
|||
key_rotation_at: undefined,
|
||||
};
|
||||
|
||||
// Base mock for useAuthorized hook
|
||||
const baseUseAuthorizedMock = {
|
||||
accessToken: "test-token",
|
||||
userId: "test-user",
|
||||
userRole: "admin",
|
||||
premiumUser: true,
|
||||
token: "test-token",
|
||||
userEmail: null,
|
||||
disabledPersonalKeyCreation: null,
|
||||
showSSOBanner: false,
|
||||
};
|
||||
|
||||
it("should render tags", async () => {
|
||||
vi.mocked(useAuthorized).mockReturnValue(baseUseAuthorizedMock);
|
||||
|
||||
const { getByText } = render(
|
||||
<KeyInfoView
|
||||
keyData={MOCK_KEY_DATA}
|
||||
onClose={() => {}}
|
||||
keyId={"test-key-id"}
|
||||
onKeyDataUpdate={() => {}}
|
||||
accessToken={"test-token"}
|
||||
userID={"test-user"}
|
||||
userRole={"admin"}
|
||||
premiumUser={true}
|
||||
teams={[]}
|
||||
/>,
|
||||
);
|
||||
|
|
@ -105,16 +120,14 @@ describe("KeyInfoView", () => {
|
|||
});
|
||||
|
||||
it("should not render tags in metadata textarea", async () => {
|
||||
vi.mocked(useAuthorized).mockReturnValue(baseUseAuthorizedMock);
|
||||
|
||||
const { container, getByText } = render(
|
||||
<KeyInfoView
|
||||
keyData={MOCK_KEY_DATA}
|
||||
onClose={() => {}}
|
||||
keyId={"test-key-id"}
|
||||
onKeyDataUpdate={() => {}}
|
||||
accessToken={"test-token"}
|
||||
userID={"test-user"}
|
||||
userRole={"admin"}
|
||||
premiumUser={true}
|
||||
teams={[]}
|
||||
/>,
|
||||
);
|
||||
|
|
@ -132,19 +145,15 @@ describe("KeyInfoView", () => {
|
|||
setTeams: vi.fn(),
|
||||
});
|
||||
|
||||
vi.mocked(useAuthorized).mockReturnValue({
|
||||
...baseUseAuthorizedMock,
|
||||
userId: "proxy-admin-user",
|
||||
userRole: "proxy_admin",
|
||||
});
|
||||
|
||||
const keyData = { ...MOCK_KEY_DATA, user_id: "other-user-id" };
|
||||
render(
|
||||
<KeyInfoView
|
||||
keyData={keyData}
|
||||
onClose={() => {}}
|
||||
keyId={"test-key-id"}
|
||||
onKeyDataUpdate={() => {}}
|
||||
accessToken={"test-token"}
|
||||
userID={"proxy-admin-user"}
|
||||
userRole={"proxy_admin"}
|
||||
premiumUser={true}
|
||||
teams={[]}
|
||||
/>,
|
||||
<KeyInfoView keyData={keyData} onClose={() => {}} keyId={"test-key-id"} onKeyDataUpdate={() => {}} teams={[]} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -180,19 +189,15 @@ describe("KeyInfoView", () => {
|
|||
setTeams: vi.fn(),
|
||||
});
|
||||
|
||||
vi.mocked(useAuthorized).mockReturnValue({
|
||||
...baseUseAuthorizedMock,
|
||||
userId: teamAdminUserId,
|
||||
userRole: "user",
|
||||
});
|
||||
|
||||
const keyData = { ...MOCK_KEY_DATA, team_id: teamId, user_id: "other-user-id" };
|
||||
render(
|
||||
<KeyInfoView
|
||||
keyData={keyData}
|
||||
onClose={() => {}}
|
||||
keyId={"test-key-id"}
|
||||
onKeyDataUpdate={() => {}}
|
||||
accessToken={"test-token"}
|
||||
userID={teamAdminUserId}
|
||||
userRole={"user"}
|
||||
premiumUser={true}
|
||||
teams={[]}
|
||||
/>,
|
||||
<KeyInfoView keyData={keyData} onClose={() => {}} keyId={"test-key-id"} onKeyDataUpdate={() => {}} teams={[]} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -207,20 +212,16 @@ describe("KeyInfoView", () => {
|
|||
setTeams: vi.fn(),
|
||||
});
|
||||
|
||||
vi.mocked(useAuthorized).mockReturnValue({
|
||||
...baseUseAuthorizedMock,
|
||||
userId: "owner-user-id",
|
||||
userRole: "user",
|
||||
});
|
||||
|
||||
const ownerUserId = "owner-user-id";
|
||||
const keyData = { ...MOCK_KEY_DATA, user_id: ownerUserId };
|
||||
render(
|
||||
<KeyInfoView
|
||||
keyData={keyData}
|
||||
onClose={() => {}}
|
||||
keyId={"test-key-id"}
|
||||
onKeyDataUpdate={() => {}}
|
||||
accessToken={"test-token"}
|
||||
userID={ownerUserId}
|
||||
userRole={"user"}
|
||||
premiumUser={true}
|
||||
teams={[]}
|
||||
/>,
|
||||
<KeyInfoView keyData={keyData} onClose={() => {}} keyId={"test-key-id"} onKeyDataUpdate={() => {}} teams={[]} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -235,19 +236,15 @@ describe("KeyInfoView", () => {
|
|||
setTeams: vi.fn(),
|
||||
});
|
||||
|
||||
vi.mocked(useAuthorized).mockReturnValue({
|
||||
...baseUseAuthorizedMock,
|
||||
userId: "other-user-id",
|
||||
userRole: "user",
|
||||
});
|
||||
|
||||
const keyData = { ...MOCK_KEY_DATA, user_id: "owner-user-id" };
|
||||
render(
|
||||
<KeyInfoView
|
||||
keyData={keyData}
|
||||
onClose={() => {}}
|
||||
keyId={"test-key-id"}
|
||||
onKeyDataUpdate={() => {}}
|
||||
accessToken={"test-token"}
|
||||
userID={"other-user-id"}
|
||||
userRole={"user"}
|
||||
premiumUser={true}
|
||||
teams={[]}
|
||||
/>,
|
||||
<KeyInfoView keyData={keyData} onClose={() => {}} keyId={"test-key-id"} onKeyDataUpdate={() => {}} teams={[]} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -262,20 +259,16 @@ describe("KeyInfoView", () => {
|
|||
setTeams: vi.fn(),
|
||||
});
|
||||
|
||||
vi.mocked(useAuthorized).mockReturnValue({
|
||||
...baseUseAuthorizedMock,
|
||||
userId: "internal-viewer-user-id",
|
||||
userRole: "Internal Viewer",
|
||||
});
|
||||
|
||||
const ownerUserId = "internal-viewer-user-id";
|
||||
const keyData = { ...MOCK_KEY_DATA, user_id: ownerUserId };
|
||||
render(
|
||||
<KeyInfoView
|
||||
keyData={keyData}
|
||||
onClose={() => {}}
|
||||
keyId={"test-key-id"}
|
||||
onKeyDataUpdate={() => {}}
|
||||
accessToken={"test-token"}
|
||||
userID={ownerUserId}
|
||||
userRole={"Internal Viewer"}
|
||||
premiumUser={true}
|
||||
teams={[]}
|
||||
/>,
|
||||
<KeyInfoView keyData={keyData} onClose={() => {}} keyId={"test-key-id"} onKeyDataUpdate={() => {}} teams={[]} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ 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;
|
||||
|
|
@ -26,12 +27,7 @@ interface KeyInfoViewProps {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -43,19 +39,14 @@ interface KeyInfoViewProps {
|
|||
* ─────────────────────────────────────────────────────────────────────────
|
||||
*/
|
||||
export default function KeyInfoView({
|
||||
keyId,
|
||||
onClose,
|
||||
keyData,
|
||||
accessToken,
|
||||
userID,
|
||||
userRole,
|
||||
teams,
|
||||
onKeyDataUpdate,
|
||||
onDelete,
|
||||
premiumUser,
|
||||
setAccessToken,
|
||||
backButtonText = "Back to Keys",
|
||||
}: KeyInfoViewProps) {
|
||||
const { accessToken, userId: userID, userRole, premiumUser } = useAuthorized();
|
||||
const { teams: teamsData } = useTeams();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
|
@ -400,9 +391,6 @@ export default function KeyInfoView({
|
|||
selectedToken={currentKeyData}
|
||||
visible={isRegenerateModalOpen}
|
||||
onClose={() => setIsRegenerateModalOpen(false)}
|
||||
accessToken={accessToken}
|
||||
premiumUser={premiumUser}
|
||||
setAccessToken={setAccessToken}
|
||||
onKeyUpdate={handleRegenerateKeyUpdate}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,27 +5,10 @@ import { Form, InputNumber, Modal } from "antd";
|
|||
import { add } from "date-fns";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { AllKeysTable } from "../all_keys_table";
|
||||
import { fetchAvailableModelsForTeamOrKey } from "../key_team_helpers/fetch_available_models_team_key";
|
||||
import { VirtualKeysTable } from "../VirtualKeysPage/VirtualKeysTable";
|
||||
import useKeyList, { KeyResponse, Team } from "../key_team_helpers/key_list";
|
||||
import { keyDeleteCall, Organization, regenerateKeyCall } from "../networking";
|
||||
|
||||
import NotificationManager from "../molecules/notifications_manager";
|
||||
|
||||
interface EditKeyModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
token: any; // Assuming TeamType is a type representing your team object
|
||||
onSubmit: (data: FormData) => void; // Assuming FormData is the type of data to be submitted
|
||||
}
|
||||
|
||||
interface ModelLimitModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
token: KeyResponse;
|
||||
onSubmit: (updatedMetadata: any) => void;
|
||||
accessToken: string;
|
||||
}
|
||||
import { keyDeleteCall, Organization, regenerateKeyCall } from "../networking";
|
||||
|
||||
// Define the props type
|
||||
interface ViewKeyTableProps {
|
||||
|
|
@ -47,39 +30,6 @@ interface ViewKeyTableProps {
|
|||
setAccessToken?: (token: string) => void;
|
||||
}
|
||||
|
||||
interface ItemData {
|
||||
key_alias: string | null;
|
||||
key_name: string;
|
||||
spend: string;
|
||||
max_budget: string | null;
|
||||
models: string[];
|
||||
tpm_limit: string | null;
|
||||
rpm_limit: string | null;
|
||||
token: string;
|
||||
token_id: string | null;
|
||||
id: number;
|
||||
team_id: string;
|
||||
metadata: any;
|
||||
user_id: string | null;
|
||||
expires: any;
|
||||
budget_duration: string | null;
|
||||
budget_reset_at: string | null;
|
||||
// Add any other properties that exist in the item data
|
||||
}
|
||||
|
||||
interface ModelLimits {
|
||||
[key: string]: number; // Index signature allowing string keys
|
||||
}
|
||||
|
||||
interface CombinedLimit {
|
||||
tpm: number;
|
||||
rpm: number;
|
||||
}
|
||||
|
||||
interface CombinedLimits {
|
||||
[key: string]: CombinedLimit; // Index signature allowing string keys
|
||||
}
|
||||
|
||||
const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||
userID,
|
||||
userRole,
|
||||
|
|
@ -98,24 +48,10 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
createClicked,
|
||||
setAccessToken,
|
||||
}) => {
|
||||
const [isButtonClicked, setIsButtonClicked] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [keyToDelete, setKeyToDelete] = useState<string | null>(null);
|
||||
const [deleteConfirmInput, setDeleteConfirmInput] = useState("");
|
||||
const [selectedItem, setSelectedItem] = useState<KeyResponse | null>(null);
|
||||
const [spendData, setSpendData] = useState<{ day: string; spend: number }[] | null>(null);
|
||||
|
||||
// NEW: Declare filter states for team and key alias.
|
||||
const [teamFilter, setTeamFilter] = useState<string>(selectedTeam?.team_id || "");
|
||||
|
||||
// Keep the team filter in sync with the incoming prop.
|
||||
useEffect(() => {
|
||||
setTeamFilter(selectedTeam?.team_id || "");
|
||||
}, [selectedTeam]);
|
||||
|
||||
// Build a memoized filters object for the backend call.
|
||||
|
||||
// Pass filters into the hook so the API call includes these query parameters.
|
||||
const { keys, isLoading, error, pagination, refresh, setKeys } = useKeyList({
|
||||
selectedTeam: selectedTeam || undefined,
|
||||
currentOrg,
|
||||
|
|
@ -129,21 +65,13 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
refresh({ page: newPage });
|
||||
};
|
||||
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [infoDialogVisible, setInfoDialogVisible] = useState(false);
|
||||
const [selectedToken, setSelectedToken] = useState<KeyResponse | null>(null);
|
||||
const [userModels, setUserModels] = useState<string[]>([]);
|
||||
const initialKnownTeamIDs: Set<string> = new Set();
|
||||
const [modelLimitModalVisible, setModelLimitModalVisible] = useState(false);
|
||||
const [regenerateDialogVisible, setRegenerateDialogVisible] = useState(false);
|
||||
const [regeneratedKey, setRegeneratedKey] = useState<string | null>(null);
|
||||
const [regenerateFormData, setRegenerateFormData] = useState<any>(null);
|
||||
const [regenerateForm] = Form.useForm();
|
||||
const [newExpiryTime, setNewExpiryTime] = useState<string | null>(null);
|
||||
|
||||
const [knownTeamIDs, setKnownTeamIDs] = useState(initialKnownTeamIDs);
|
||||
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const calculateNewExpiryTime = (duration: string | undefined) => {
|
||||
if (!duration) {
|
||||
|
|
@ -190,36 +118,6 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
console.log("calculateNewExpiryTime:", newExpiryTime);
|
||||
}, [selectedToken, regenerateFormData?.duration]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserModels = async () => {
|
||||
try {
|
||||
if (userID === null || userRole === null || accessToken === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const models = await fetchAvailableModelsForTeamOrKey(userID, userRole, accessToken);
|
||||
if (models) {
|
||||
setUserModels(models);
|
||||
}
|
||||
} catch (error) {
|
||||
NotificationManager.error({ description: "Error fetching user models" });
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserModels();
|
||||
}, [accessToken, userID, userRole]);
|
||||
|
||||
useEffect(() => {
|
||||
if (teams) {
|
||||
const teamIDSet: Set<string> = new Set();
|
||||
teams.forEach((team: any, index: number) => {
|
||||
const team_obj: string = team.team_id;
|
||||
teamIDSet.add(team_obj);
|
||||
});
|
||||
setKnownTeamIDs(teamIDSet);
|
||||
}
|
||||
}, [teams]);
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (keyToDelete == null || keys == null) {
|
||||
return;
|
||||
|
|
@ -305,7 +203,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<AllKeysTable
|
||||
<VirtualKeysTable
|
||||
keys={keys}
|
||||
setKeys={setKeys}
|
||||
isLoading={isLoading}
|
||||
|
|
@ -315,16 +213,11 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
teams={teams}
|
||||
selectedTeam={selectedTeam}
|
||||
setSelectedTeam={setSelectedTeam}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
organizations={organizations}
|
||||
setCurrentOrg={setCurrentOrg}
|
||||
refresh={refresh}
|
||||
selectedKeyAlias={selectedKeyAlias}
|
||||
setSelectedKeyAlias={setSelectedKeyAlias}
|
||||
premiumUser={premiumUser}
|
||||
setAccessToken={setAccessToken}
|
||||
/>
|
||||
|
||||
{isDeleteModalOpen &&
|
||||
|
|
|
|||
|
|
@ -526,12 +526,8 @@ export default function SpendLogsTable({
|
|||
<KeyInfoView
|
||||
keyId={selectedKeyIdInfoView}
|
||||
keyData={selectedKeyInfo}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
teams={allTeams}
|
||||
onClose={() => setSelectedKeyIdInfoView(null)}
|
||||
premiumUser={premiumUser}
|
||||
backButtonText="Back to Logs"
|
||||
/>
|
||||
) : selectedSessionId ? (
|
||||
|
|
@ -968,10 +964,7 @@ export function RequestViewer({ row }: { row: Row<LogEntry> }) {
|
|||
</div>
|
||||
|
||||
{/* Cost Breakdown - Show if cost breakdown data is available */}
|
||||
<CostBreakdownViewer
|
||||
costBreakdown={row.original.metadata?.cost_breakdown}
|
||||
totalSpend={row.original.spend || 0}
|
||||
/>
|
||||
<CostBreakdownViewer costBreakdown={row.original.metadata?.cost_breakdown} totalSpend={row.original.spend || 0} />
|
||||
|
||||
{/* Configuration Info Message - Show when data is missing */}
|
||||
<ConfigInfoMessage show={missingData} />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue