mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
fix(ui): show loading state instead of stale rows while a table search is pending
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
4fbe2276a1
commit
d36cdcb8f9
8 changed files with 59 additions and 7 deletions
|
|
@ -152,6 +152,28 @@ describe("useResourceList", () => {
|
|||
await waitFor(() => expect(lastCall().page_size).toBe(25));
|
||||
});
|
||||
|
||||
it("reports loading while a new search request is still pending", async () => {
|
||||
let resolveSecond: ((value: ResourceListPage<Row>) => void) | undefined;
|
||||
const fetchPage = vi.fn((query: ResourceListQuery) => {
|
||||
calls.push(query);
|
||||
if (calls.length === 1) return Promise.resolve(page([{ id: "a" }], 3));
|
||||
return new Promise<ResourceListPage<Row>>((resolve) => {
|
||||
resolveSecond = resolve;
|
||||
});
|
||||
});
|
||||
const { result } = renderList({ fetchPage });
|
||||
await waitFor(() => expect(result.current.rows).toEqual([{ id: "a" }]));
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
|
||||
act(() => result.current.onSearchChange("zzz"));
|
||||
await waitFor(() => expect(lastCall().q).toBe("zzz"));
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
|
||||
act(() => resolveSecond?.(page([], 0)));
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
expect(result.current.rows).toEqual([]);
|
||||
});
|
||||
|
||||
it("surfaces a failed page as an error instead of empty rows", async () => {
|
||||
const fetchPage = vi.fn(() => Promise.reject(new Error("boom")));
|
||||
const { result } = renderList({ fetchPage });
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export function useResourceList<TRow>(options: UseResourceListOptions<TRow>): Re
|
|||
enabled,
|
||||
placeholderData: (previous) => previous,
|
||||
};
|
||||
const { data, isLoading, isFetching, error, refetch: refetchQuery } = useQuery(queryOptions);
|
||||
const { data, isLoading, isPlaceholderData, isFetching, error, refetch: refetchQuery } = useQuery(queryOptions);
|
||||
|
||||
const toFirstPage = useCallback(() => setPagination((previous) => ({ ...previous, pageIndex: 0 })), []);
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ export function useResourceList<TRow>(options: UseResourceListOptions<TRow>): Re
|
|||
return {
|
||||
rows,
|
||||
rowCount: data?.meta.total_count ?? 0,
|
||||
isLoading,
|
||||
isLoading: isLoading || isPlaceholderData,
|
||||
isFetching,
|
||||
error,
|
||||
refetch,
|
||||
|
|
|
|||
|
|
@ -344,5 +344,16 @@ describe("ViewUserDashboard", () => {
|
|||
expect(latest[4]).toBeNull();
|
||||
expect(latest[2]).toBe(1);
|
||||
});
|
||||
|
||||
it("replaces the previous rows with the loading state while the search request is pending", async () => {
|
||||
renderDashboard();
|
||||
expect(await screen.findByText("test@example.com")).toBeInTheDocument();
|
||||
|
||||
userListCall.mockReturnValue(new Promise(() => undefined));
|
||||
fireEvent.change(screen.getByPlaceholderText("Search by email or ID…"), { target: { value: "zzznomatch" } });
|
||||
|
||||
expect(await screen.findByText("Loading users…")).toBeInTheDocument();
|
||||
expect(screen.queryByText("test@example.com")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
<UsersTable
|
||||
data={users}
|
||||
rowCount={totalUserCount}
|
||||
isLoading={userListQuery.isLoading}
|
||||
isLoading={userListQuery.isLoading || userListQuery.isPlaceholderData}
|
||||
possibleUIRoles={possibleUIRoles}
|
||||
teams={teams}
|
||||
sorting={sorting}
|
||||
|
|
|
|||
|
|
@ -134,6 +134,14 @@ it("shows a loading state on initial load and hides the data", () => {
|
|||
expect(screen.queryByText("Acme Team")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("replaces the previous rows with the loading state while a new search is pending", () => {
|
||||
mockUseTeamsTable.mockReturnValue(teamsResult([mockTeam], {}, { isPlaceholderData: true, isFetching: true }));
|
||||
renderTable();
|
||||
|
||||
expect(screen.getByText("Loading teams...")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Acme Team")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("sort contract – only backend-sortable columns are sortable", () => {
|
||||
it("requests the default created_at descending sort on first render", () => {
|
||||
renderTable();
|
||||
|
|
|
|||
|
|
@ -83,7 +83,8 @@ export function TeamsTable({ userRole, userID, onSelectTeam, onEditTeam, onDelet
|
|||
|
||||
const {
|
||||
data: teamsResponse,
|
||||
isPending: isLoading,
|
||||
isPending,
|
||||
isPlaceholderData,
|
||||
isFetching,
|
||||
refetch,
|
||||
} = useTeamsTable(tablePagination.pageIndex + 1, tablePagination.pageSize, teamListOptions);
|
||||
|
|
@ -161,7 +162,7 @@ export function TeamsTable({ userRole, userID, onSelectTeam, onEditTeam, onDelet
|
|||
onColumnFiltersChange={handleColumnFiltersChange}
|
||||
enableColumnResizing
|
||||
columnResizeMode="onChange"
|
||||
isLoading={isLoading}
|
||||
isLoading={isPending || isPlaceholderData}
|
||||
loadingMessage="Loading teams..."
|
||||
noDataMessage="No teams found"
|
||||
fillHeight
|
||||
|
|
|
|||
|
|
@ -289,6 +289,15 @@ it("should show a loading state on the initial load and hide the data", () => {
|
|||
expect(screen.queryByText("Test Key Alias")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("replaces the previous rows with the loading state while a new search is pending", () => {
|
||||
mockUseKeys.mockReturnValue(keysResult([mockKey], {}, { isPlaceholderData: true, isFetching: true }));
|
||||
|
||||
renderWithProviders(<VirtualKeysTable />);
|
||||
|
||||
expect(screen.getByText("Loading keys...")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Key Alias")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show 'No keys found' message when the key list is empty", () => {
|
||||
mockUseKeys.mockReturnValue(keysResult([]));
|
||||
|
||||
|
|
|
|||
|
|
@ -128,7 +128,8 @@ export function VirtualKeysTable({ headerActions }: VirtualKeysTableProps) {
|
|||
|
||||
const {
|
||||
data: keys,
|
||||
isPending: isLoading,
|
||||
isPending,
|
||||
isPlaceholderData,
|
||||
isFetching,
|
||||
refetch,
|
||||
} = useKeys(tablePagination.pageIndex + 1, tablePagination.pageSize, keyListOptions);
|
||||
|
|
@ -280,7 +281,7 @@ export function VirtualKeysTable({ headerActions }: VirtualKeysTableProps) {
|
|||
onColumnFiltersChange={handleColumnFiltersChange}
|
||||
enableColumnResizing
|
||||
columnResizeMode="onChange"
|
||||
isLoading={isLoading}
|
||||
isLoading={isPending || isPlaceholderData}
|
||||
loadingMessage="Loading keys..."
|
||||
noDataMessage="No keys found"
|
||||
fillHeight
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue