mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
Merge pull request #18619 from BerriAI/litellm_ui_keys_loading_2
[Feature] UI - Virtual Keys Table Loading State Between Pages
This commit is contained in:
commit
359b8df8b2
2 changed files with 142 additions and 11 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { screen, waitFor, fireEvent } from "@testing-library/react";
|
||||
import { vi, it, expect, beforeEach, MockedFunction } from "vitest";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import { VirtualKeysTable } from "./VirtualKeysTable";
|
||||
|
|
@ -264,3 +264,133 @@ it("should show skeleton loaders when isLoading is true", () => {
|
|||
expect(screen.queryByText("Test Key Alias")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Team")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show 'No keys found' message when filteredKeys is empty", () => {
|
||||
// Mock empty filteredKeys
|
||||
mockUseFilterLogic.mockReturnValue({
|
||||
filters: {
|
||||
"Team ID": "",
|
||||
"Organization ID": "",
|
||||
"Key Alias": "",
|
||||
"User ID": "",
|
||||
"Sort By": "created_at",
|
||||
"Sort Order": "desc",
|
||||
},
|
||||
filteredKeys: [],
|
||||
allKeyAliases: [],
|
||||
allTeams: [mockTeam],
|
||||
allOrganizations: [mockOrganization],
|
||||
handleFilterChange: vi.fn(),
|
||||
handleFilterReset: vi.fn(),
|
||||
});
|
||||
|
||||
const mockProps = {
|
||||
teams: [mockTeam],
|
||||
organizations: [mockOrganization],
|
||||
onSortChange: vi.fn(),
|
||||
currentSort: {
|
||||
sortBy: "created_at",
|
||||
sortOrder: "desc" as const,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(<VirtualKeysTable {...mockProps} />);
|
||||
|
||||
expect(screen.getByText("No keys found")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle models with more than 3 entries to trigger expansion UI", () => {
|
||||
const keyWithManyModels = {
|
||||
...mockKey,
|
||||
models: ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo", "claude-3", "claude-3-5-sonnet"],
|
||||
};
|
||||
|
||||
mockUseFilterLogic.mockReturnValue({
|
||||
filters: {
|
||||
"Team ID": "",
|
||||
"Organization ID": "",
|
||||
"Key Alias": "",
|
||||
"User ID": "",
|
||||
"Sort By": "created_at",
|
||||
"Sort Order": "desc",
|
||||
},
|
||||
filteredKeys: [keyWithManyModels],
|
||||
allKeyAliases: ["test-key-alias"],
|
||||
allTeams: [mockTeam],
|
||||
allOrganizations: [mockOrganization],
|
||||
handleFilterChange: vi.fn(),
|
||||
handleFilterReset: vi.fn(),
|
||||
});
|
||||
|
||||
const mockProps = {
|
||||
teams: [mockTeam],
|
||||
organizations: [mockOrganization],
|
||||
onSortChange: vi.fn(),
|
||||
currentSort: {
|
||||
sortBy: "created_at",
|
||||
sortOrder: "desc" as const,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(<VirtualKeysTable {...mockProps} />);
|
||||
|
||||
// This test ensures the ChevronDownIcon import (line 6) is used
|
||||
// by having a key with > 3 models which triggers the expansion logic
|
||||
// that uses ChevronDownIcon and ChevronRightIcon
|
||||
expect(screen.getByText("Test Key Alias")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render table headers correctly", () => {
|
||||
const mockProps = {
|
||||
teams: [mockTeam],
|
||||
organizations: [mockOrganization],
|
||||
onSortChange: vi.fn(),
|
||||
currentSort: {
|
||||
sortBy: "created_at",
|
||||
sortOrder: "desc" as const,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(<VirtualKeysTable {...mockProps} />);
|
||||
|
||||
// Check that main headers are rendered (testing the header.isPlaceholder condition path)
|
||||
expect(screen.getByText("Key ID")).toBeInTheDocument();
|
||||
expect(screen.getByText("Key Alias")).toBeInTheDocument();
|
||||
expect(screen.getByText("Team Alias")).toBeInTheDocument();
|
||||
expect(screen.getByText("Models")).toBeInTheDocument();
|
||||
expect(screen.getByText("Spend (USD)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle column resizing hover events", () => {
|
||||
const mockProps = {
|
||||
teams: [mockTeam],
|
||||
organizations: [mockOrganization],
|
||||
onSortChange: vi.fn(),
|
||||
currentSort: {
|
||||
sortBy: "created_at",
|
||||
sortOrder: "desc" as const,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(<VirtualKeysTable {...mockProps} />);
|
||||
|
||||
// Find a header cell with data-header-id attribute
|
||||
const headerCell = document.querySelector("[data-header-id]") as HTMLElement;
|
||||
|
||||
expect(headerCell).toBeInTheDocument();
|
||||
|
||||
// Check that the resizer element exists within the header
|
||||
const resizer = headerCell?.querySelector(".resizer") as HTMLElement;
|
||||
expect(resizer).toBeInTheDocument();
|
||||
|
||||
// Initially, resizer should have opacity 0
|
||||
expect(resizer.style.opacity).toBe("0");
|
||||
|
||||
// Simulate mouse enter using fireEvent - should set opacity to 0.5 (lines 612-616)
|
||||
fireEvent.mouseEnter(headerCell);
|
||||
expect(resizer.style.opacity).toBe("0.5");
|
||||
|
||||
// Simulate mouse leave using fireEvent - should set opacity back to 0 (lines 618-622)
|
||||
fireEvent.mouseLeave(headerCell);
|
||||
expect(resizer.style.opacity).toBe("0");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -68,12 +68,13 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
|
|||
});
|
||||
const [tablePagination, setTablePagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 100,
|
||||
pageSize: 50,
|
||||
});
|
||||
|
||||
const {
|
||||
data: keys,
|
||||
isPending: isLoading,
|
||||
isFetching,
|
||||
refetch,
|
||||
} = useKeys(tablePagination.pageIndex + 1, tablePagination.pageSize);
|
||||
const totalCount = keys?.total_count || 0;
|
||||
|
|
@ -545,8 +546,8 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
|
|||
</div>
|
||||
|
||||
<div className="flex items-center justify-between w-full mb-4">
|
||||
{isLoading ? (
|
||||
<Skeleton.Input active style={{ width: 200, height: 20 }} />
|
||||
{isLoading || isFetching ? (
|
||||
<Skeleton.Node active style={{ width: 200, height: 20 }} />
|
||||
) : (
|
||||
<span className="inline-flex text-sm text-gray-700">
|
||||
Showing {rangeLabel} of {totalCount} results
|
||||
|
|
@ -554,32 +555,32 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
|
|||
)}
|
||||
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{isLoading ? (
|
||||
<Skeleton.Input active size="small" style={{ width: 50, height: 20 }} />
|
||||
{isLoading || isFetching ? (
|
||||
<Skeleton.Node active style={{ width: 74, height: 20 }} />
|
||||
) : (
|
||||
<span className="text-sm text-gray-700">
|
||||
Page {pageIndex + 1} of {table.getPageCount()}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
{isLoading || isFetching ? (
|
||||
<Skeleton.Button active size="small" style={{ width: 84, height: 30 }} />
|
||||
) : (
|
||||
<button
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={isLoading || !table.getCanPreviousPage()}
|
||||
disabled={isLoading || isFetching || !table.getCanPreviousPage()}
|
||||
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
{isLoading || isFetching ? (
|
||||
<Skeleton.Button active size="small" style={{ width: 58, height: 30 }} />
|
||||
) : (
|
||||
<button
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={isLoading || !table.getCanNextPage()}
|
||||
disabled={isLoading || isFetching || !table.getCanNextPage()}
|
||||
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Next
|
||||
|
|
@ -664,7 +665,7 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
|
|||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
{isLoading || isFetching ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue