Merge pull request #18504 from BerriAI/litellm_ui_keys_table_refactor

[Refactor] UI - Keys Table
This commit is contained in:
yuneng-jiang 2025-12-29 17:15:57 -08:00 committed by GitHub
commit 566eb35747
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 124 additions and 354 deletions

View file

@ -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>

View file

@ -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();

View file

@ -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>

View file

@ -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 || []);

View file

@ -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"]);

View file

@ -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);

View file

@ -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",

View file

@ -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(() => {

View file

@ -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}
/>

View file

@ -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 &&

View file

@ -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} />