mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
[Feature] UI - Virtual Keys: Add refetch button and keep stale data during refetch
Show a Fetch/Fetching button next to "Showing X of Y results" that acts as both a manual refetch trigger and a loading indicator. The "Loading keys..." message now only appears on initial load; subsequent refetches keep the table visible with stale data (via React Query's keepPreviousData). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6fe82d3886
commit
9ee489863d
2 changed files with 100 additions and 19 deletions
|
|
@ -262,8 +262,8 @@ it("should display user email correctly", async () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should show skeleton loaders when isLoading is true", () => {
|
||||
// Mock loading state
|
||||
it("should show loading message only on initial load (isPending)", () => {
|
||||
// Mock initial loading state
|
||||
mockUseKeys.mockReturnValue({
|
||||
data: null,
|
||||
isPending: true,
|
||||
|
|
@ -283,7 +283,7 @@ it("should show skeleton loaders when isLoading is true", () => {
|
|||
|
||||
renderWithProviders(<VirtualKeysTable {...mockProps} />);
|
||||
|
||||
// Check that loading message is shown
|
||||
// Check that loading message is shown on initial load
|
||||
expect(screen.getByText("🚅 Loading keys...")).toBeInTheDocument();
|
||||
|
||||
// Check that actual key data is not shown
|
||||
|
|
@ -795,3 +795,63 @@ describe("pagination display – total count and page count", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("refetch button", () => {
|
||||
it("should show Fetch button in normal state", () => {
|
||||
renderWithProviders(<VirtualKeysTable {...defaultMockProps} />);
|
||||
|
||||
const fetchButton = screen.getByTitle("Fetch data");
|
||||
expect(fetchButton).toBeInTheDocument();
|
||||
expect(fetchButton).not.toBeDisabled();
|
||||
expect(screen.getByText("Fetch")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show Fetching state and keep table data visible during refetch", () => {
|
||||
mockUseKeys.mockReturnValue({
|
||||
data: {
|
||||
keys: [mockKey],
|
||||
total_count: 1,
|
||||
current_page: 1,
|
||||
total_pages: 1,
|
||||
} as KeysResponse,
|
||||
isPending: false,
|
||||
isFetching: true,
|
||||
refetch: vi.fn(),
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<VirtualKeysTable {...defaultMockProps} />);
|
||||
|
||||
// Button should show "Fetching" and be disabled
|
||||
expect(screen.getByText("Fetching")).toBeInTheDocument();
|
||||
const fetchButton = screen.getByTitle("Fetch data");
|
||||
expect(fetchButton).toBeDisabled();
|
||||
|
||||
// Table data should still be visible (stale data)
|
||||
expect(screen.getByText("Test Key Alias")).toBeInTheDocument();
|
||||
|
||||
// "Loading keys..." should NOT appear during refetch
|
||||
expect(screen.queryByText("🚅 Loading keys...")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call refetch when Fetch button is clicked", () => {
|
||||
const mockRefetch = vi.fn();
|
||||
mockUseKeys.mockReturnValue({
|
||||
data: {
|
||||
keys: [mockKey],
|
||||
total_count: 1,
|
||||
current_page: 1,
|
||||
total_pages: 1,
|
||||
} as KeysResponse,
|
||||
isPending: false,
|
||||
isFetching: false,
|
||||
refetch: mockRefetch,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<VirtualKeysTable {...defaultMockProps} />);
|
||||
|
||||
const fetchButton = screen.getByTitle("Fetch data");
|
||||
fireEvent.click(fetchButton);
|
||||
|
||||
expect(mockRefetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ import {
|
|||
TableRow,
|
||||
Text,
|
||||
} from "@tremor/react";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Popover, Skeleton, Tooltip } from "antd";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { InfoCircleOutlined, SyncOutlined } from "@ant-design/icons";
|
||||
import { Button as AntButton, Popover, Skeleton, Tooltip } from "antd";
|
||||
import React, { useEffect, useDeferredValue, useMemo, useState } from "react";
|
||||
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
|
||||
import { useFilterLogic } from "../key_team_helpers/filter_logic";
|
||||
import { PaginatedKeyAliasSelect } from "../KeyAliasSelect/PaginatedKeyAliasSelect/PaginatedKeyAliasSelect";
|
||||
|
|
@ -97,6 +97,15 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
|
|||
organizations,
|
||||
});
|
||||
|
||||
// Defer the transition so the button stays in loading state until the table
|
||||
// has rendered with the new data (mirrors the spend-logs pattern)
|
||||
const isFetchingDeferred = useDeferredValue(isFetching);
|
||||
const isButtonLoading = isFetching || isFetchingDeferred;
|
||||
|
||||
const handleRefresh = () => {
|
||||
refetch();
|
||||
};
|
||||
|
||||
const totalCount = filteredTotalCount ?? keys?.total_count ?? 0;
|
||||
|
||||
// Add a useEffect to call refresh when a key is created
|
||||
|
|
@ -606,16 +615,28 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
|
|||
</div>
|
||||
|
||||
<div className="flex items-center justify-between w-full mb-4">
|
||||
{isLoading || isFetching ? (
|
||||
<Skeleton.Node active style={{ width: 200, height: 20 }} />
|
||||
) : (
|
||||
<span className="inline-flex text-sm text-gray-700">
|
||||
Showing {rangeLabel} of {totalCount} results
|
||||
</span>
|
||||
)}
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{isLoading ? (
|
||||
<Skeleton.Node active style={{ width: 200, height: 20 }} />
|
||||
) : (
|
||||
<span className="inline-flex text-sm text-gray-700">
|
||||
Showing {rangeLabel} of {totalCount} results
|
||||
</span>
|
||||
)}
|
||||
|
||||
<AntButton
|
||||
type="default"
|
||||
icon={<SyncOutlined spin={isButtonLoading} />}
|
||||
onClick={handleRefresh}
|
||||
disabled={isButtonLoading}
|
||||
title="Fetch data"
|
||||
>
|
||||
{isButtonLoading ? "Fetching" : "Fetch"}
|
||||
</AntButton>
|
||||
</div>
|
||||
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{isLoading || isFetching ? (
|
||||
{isLoading ? (
|
||||
<Skeleton.Node active style={{ width: 74, height: 20 }} />
|
||||
) : (
|
||||
<span className="text-sm text-gray-700">
|
||||
|
|
@ -623,24 +644,24 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
|
|||
</span>
|
||||
)}
|
||||
|
||||
{isLoading || isFetching ? (
|
||||
{isLoading ? (
|
||||
<Skeleton.Button active size="small" style={{ width: 84, height: 30 }} />
|
||||
) : (
|
||||
<button
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={isLoading || isFetching || !table.getCanPreviousPage()}
|
||||
disabled={isLoading || !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 || isFetching ? (
|
||||
{isLoading ? (
|
||||
<Skeleton.Button active size="small" style={{ width: 58, height: 30 }} />
|
||||
) : (
|
||||
<button
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={isLoading || isFetching || !table.getCanNextPage()}
|
||||
disabled={isLoading || !table.getCanNextPage()}
|
||||
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Next
|
||||
|
|
@ -725,7 +746,7 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
|
|||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading || isFetching ? (
|
||||
{isLoading ? (
|
||||
<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