diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useKeys.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useKeys.ts new file mode 100644 index 0000000000..8ae4d76ff5 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useKeys.ts @@ -0,0 +1,36 @@ +import { keepPreviousData, useQuery, UseQueryResult } from "@tanstack/react-query"; +import { createQueryKeys } from "../common/queryKeysFactory"; +import { keyListCall } from "@/components/networking"; +import { KeyResponse } from "@/components/key_team_helpers/key_list"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const keyKeys = createQueryKeys("keys"); + +export interface KeysResponse { + keys: KeyResponse[]; + total_count: number; + current_page: number; + total_pages: number; +} + +export const useKeys = (page: number, pageSize: number): UseQueryResult => { + const { accessToken } = useAuthorized(); + + return useQuery({ + queryKey: keyKeys.list({ page, limit: pageSize }), + queryFn: async () => + await keyListCall( + accessToken!, + null, // organizationID + null, // teamID + null, // selectedKeyAlias + null, // userID + null, // keyHash + page, + pageSize, + ), + enabled: Boolean(accessToken), + staleTime: 30000, // 30 seconds + placeholderData: keepPreviousData, + }); +}; diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx index 4f40ed99a8..71573712cd 100644 --- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx +++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx @@ -136,13 +136,7 @@ it("should render VirtualKeysTable component", () => { keys: [mockKey], setKeys: vi.fn(), isLoading: false, - pagination: { - currentPage: 1, - totalPages: 1, - totalCount: 1, - }, - onPageChange: vi.fn(), - pageSize: 50, + totalCount: 1, teams: [mockTeam], selectedTeam: null, setSelectedTeam: vi.fn(), @@ -166,13 +160,7 @@ it("should display key information correctly", async () => { keys: [mockKey], setKeys: vi.fn(), isLoading: false, - pagination: { - currentPage: 1, - totalPages: 1, - totalCount: 1, - }, - onPageChange: vi.fn(), - pageSize: 50, + totalCount: 1, teams: [mockTeam], selectedTeam: null, setSelectedTeam: vi.fn(), @@ -200,13 +188,7 @@ it("should display user email correctly", async () => { keys: [mockKey], setKeys: vi.fn(), isLoading: false, - pagination: { - currentPage: 1, - totalPages: 1, - totalCount: 1, - }, - onPageChange: vi.fn(), - pageSize: 50, + totalCount: 1, teams: [mockTeam], selectedTeam: null, setSelectedTeam: vi.fn(), diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx index 128129ce66..8cfe5cc2f1 100644 --- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx @@ -1,12 +1,14 @@ "use client"; -import { Setter } from "@/types"; -import { formatNumberWithCommas, updateExistingKeys } from "@/utils/dataUtils"; +import { useKeys } from "@/app/(dashboard)/hooks/keys/useKeys"; +import { formatNumberWithCommas } from "@/utils/dataUtils"; import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, SwitchVerticalIcon } from "@heroicons/react/outline"; import { ColumnDef, flexRender, getCoreRowModel, + getPaginationRowModel, getSortedRowModel, + PaginationState, SortingState, useReactTable, } from "@tanstack/react-table"; @@ -30,27 +32,10 @@ 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 VirtualKeysTableProps { - keys: KeyResponse[]; - setKeys: (keys: KeyResponse[] | ((prev: KeyResponse[]) => KeyResponse[])) => void; - isLoading?: boolean; - pagination: { - currentPage: number; - totalPages: number; - totalCount: number; - }; - onPageChange: (page: number) => void; - pageSize?: number; teams: Team[] | null; - selectedTeam: Team | null; - setSelectedTeam: (team: Team | null) => void; - selectedKeyAlias: string | null; - setSelectedKeyAlias: Setter; organizations: Organization[] | null; - setCurrentOrg: React.Dispatch>; - refresh?: () => void; onSortChange?: (sortBy: string, sortOrder: "asc" | "desc") => void; currentSort?: { sortBy: string; @@ -63,21 +48,8 @@ interface VirtualKeysTableProps { * The team selector and filtering have been removed so that all keys are shown. */ -export function VirtualKeysTable({ - keys, - setKeys, - isLoading = false, - pagination, - onPageChange, - pageSize = 50, - teams, - organizations, - refresh, - onSortChange, - currentSort, -}: VirtualKeysTableProps) { - const { accessToken, userId: userID, userRole, premiumUser } = useAuthorized(); - const [selectedKeyId, setSelectedKeyId] = useState(null); +export function VirtualKeysTable({ teams, organizations, onSortChange, currentSort }: VirtualKeysTableProps) { + const [selectedKey, setSelectedKey] = useState(null); const [sorting, setSorting] = React.useState(() => { if (currentSort) { return [ @@ -94,22 +66,33 @@ export function VirtualKeysTable({ }, ]; }); + const [tablePagination, setTablePagination] = React.useState({ + pageIndex: 0, + pageSize: 100, + }); + + const { + data: keys, + isPending: isLoading, + refetch, + } = useKeys(tablePagination.pageIndex + 1, tablePagination.pageSize); + const totalCount = keys?.total_count || 0; const [expandedAccordions, setExpandedAccordions] = useState>({}); // Use the filter logic hook const { filters, filteredKeys, allKeyAliases, allTeams, allOrganizations, handleFilterChange, handleFilterReset } = useFilterLogic({ - keys, + keys: keys?.keys || [], teams, organizations, }); // Add a useEffect to call refresh when a key is created useEffect(() => { - if (refresh) { + if (refetch) { const handleStorageChange = () => { - refresh(); + refetch(); }; // Listen for storage events that might indicate a key was created @@ -119,7 +102,7 @@ export function VirtualKeysTable({ window.removeEventListener("storage", handleStorageChange); }; } - }, [refresh]); + }, [refetch]); const columns: ColumnDef[] = [ { @@ -145,7 +128,7 @@ export function VirtualKeysTable({ 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)} + onClick={() => setSelectedKey(info.row.original)} > {info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "-"} @@ -496,6 +479,7 @@ export function VirtualKeysTable({ columnResizeDirection: "ltr", state: { sorting, + pagination: tablePagination, }, onSortingChange: (updaterOrValue) => { const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue; @@ -514,10 +498,14 @@ export function VirtualKeysTable({ onSortChange?.(sortBy, sortOrder); } }, + onPaginationChange: setTablePagination, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), enableSorting: true, manualSorting: false, + manualPagination: true, + pageCount: Math.ceil(totalCount / tablePagination.pageSize), }); // Update local sorting state when currentSort prop changes @@ -534,26 +522,11 @@ export function VirtualKeysTable({ return (
- {selectedKeyId ? ( + {selectedKey ? ( 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 - }} + keyId={selectedKey.token} + onClose={() => setSelectedKey(null)} + keyData={selectedKey} teams={allTeams} /> ) : ( @@ -572,26 +545,30 @@ export function VirtualKeysTable({ Showing{" "} {isLoading ? "..." - : `${(pagination.currentPage - 1) * pageSize + 1} - ${Math.min(pagination.currentPage * pageSize, pagination.totalCount)}`}{" "} - of {isLoading ? "..." : pagination.totalCount} results + : `${table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} - ${Math.min( + (table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize, + totalCount, + )}`}{" "} + of {isLoading ? "..." : totalCount} results
- Page {isLoading ? "..." : pagination.currentPage} of {isLoading ? "..." : pagination.totalPages} + Page {isLoading ? "..." : table.getState().pagination.pageIndex + 1} of{" "} + {isLoading ? "..." : table.getPageCount()} -
-
-
-
- - - -
-
-

- Warning: You are about to delete this Virtual Key. -

-

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

-
-
-

Are you sure you want to delete this Virtual Key?

-
- - setDeleteConfirmInput(e.target.value)} - placeholder="Enter key name exactly" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-base" - autoFocus - /> -
-
-
-
- - -
- - - ); - })()} - - {/* Regenerate Key Form Modal */} - { - setRegenerateDialogVisible(false); - regenerateForm.resetFields(); - }} - footer={[ - , - , - ]} - > - {premiumUser ? ( -
{ - if ("duration" in changedValues) { - handleRegenerateFormChange("duration", changedValues.duration); - } - }} - > - - - - - - - - - - - - - - - -
- Current expiry:{" "} - {selectedToken?.expires != null ? new Date(selectedToken.expires).toLocaleString() : "Never"} -
- {newExpiryTime &&
New expiry: {newExpiryTime}
} -
- ) : ( -
-

Upgrade to use this feature

- -
- )} -
- - {/* Regenerated Key Display Modal */} - {regeneratedKey && ( - setRegeneratedKey(null)} - footer={[ - , - ]} - > - - Regenerated Key - -

- Please replace your old key with the new key generated. For security reasons,{" "} - you will not be able to view it again through your LiteLLM account. If you lose this secret key, - you will need to generate a new one. -

- - - Key Alias: -
-
-                  {selectedToken?.key_alias || "No alias set"}
-                
-
- New Virtual Key: -
-
{regeneratedKey}
-
- NotificationManager.success({ description: "Virtual Key copied to clipboard" })} - > - - - -
-
- )} + ); }; diff --git a/ui/litellm-dashboard/src/components/user_dashboard.tsx b/ui/litellm-dashboard/src/components/user_dashboard.tsx index 4f910452bd..c55cb09457 100644 --- a/ui/litellm-dashboard/src/components/user_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/user_dashboard.tsx @@ -1,23 +1,23 @@ "use client"; -import React, { useState, useEffect } from "react"; -import { - userInfoCall, - modelAvailableCall, - getProxyUISettings, - Organization, - keyInfoCall, - getProxyBaseUrl, -} from "./networking"; -import { fetchTeams } from "./common_components/fetch_teams"; -import { Grid, Col } from "@tremor/react"; -import CreateKey from "./organisms/create_key_button"; -import ViewKeyTable from "./templates/view_key_table"; -import Onboarding from "../app/onboarding/page"; -import { useSearchParams } from "next/navigation"; -import { KeyResponse, Team } from "./key_team_helpers/key_list"; -import { jwtDecode } from "jwt-decode"; -import { Typography } from "antd"; import { clearTokenCookies } from "@/utils/cookieUtils"; +import { Col, Grid } from "@tremor/react"; +import { Typography } from "antd"; +import { jwtDecode } from "jwt-decode"; +import { useSearchParams } from "next/navigation"; +import React, { useEffect, useState } from "react"; +import Onboarding from "../app/onboarding/page"; +import { fetchTeams } from "./common_components/fetch_teams"; +import { KeyResponse, Team } from "./key_team_helpers/key_list"; +import { + getProxyBaseUrl, + getProxyUISettings, + keyInfoCall, + modelAvailableCall, + Organization, + userInfoCall, +} from "./networking"; +import CreateKey from "./organisms/create_key_button"; +import { VirtualKeysTable } from "./VirtualKeysPage/VirtualKeysTable"; export interface ProxySettings { PROXY_BASE_URL: string | null; @@ -361,25 +361,7 @@ const UserDashboard: React.FC = ({ addKey={addKey} premiumUser={premiumUser} /> - - +