mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-02 14:22:16 +00:00
Merge pull request #17168 from BerriAI/litellm_ui_users_loading
[Feature] UI - Better Loading State for Internal User Page
This commit is contained in:
@@ -11,6 +11,10 @@ test.describe("User Info View", () => {
|
||||
test("should display user info when clicking on user ID", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Wait for loading state to disappear
|
||||
await page.waitForSelector('text="🚅 Loading users..."', {
|
||||
state: "hidden",
|
||||
});
|
||||
// Wait for users table to load
|
||||
await page.waitForSelector("table");
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import SSOSettings from "./SSOSettings";
|
||||
import { columns } from "./view_users/columns";
|
||||
import { UserDataTable } from "./view_users/table";
|
||||
import { UserInfo } from "./view_users/types";
|
||||
import { Skeleton } from "antd";
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
@@ -277,14 +278,6 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
|
||||
});
|
||||
const possibleUIRoles = userRolesQuery.data;
|
||||
|
||||
if (userListQuery.isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!accessToken || !token || !userRole || !userID) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const tableColumns = columns(
|
||||
possibleUIRoles,
|
||||
(user) => {
|
||||
@@ -300,21 +293,31 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
|
||||
<div className="w-full p-8 overflow-hidden">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex space-x-3">
|
||||
<CreateUser userID={userID} accessToken={accessToken} teams={teams} possibleUIRoles={possibleUIRoles} />
|
||||
{userListQuery.isLoading ? (
|
||||
<>
|
||||
<Skeleton.Button active size="default" shape="default" style={{ width: 110, height: 36 }} />
|
||||
<Skeleton.Button active size="default" shape="default" style={{ width: 145, height: 36 }} />
|
||||
<Skeleton.Button active size="default" shape="default" style={{ width: 110, height: 36 }} />
|
||||
</>
|
||||
) : userID && accessToken ? (
|
||||
<>
|
||||
<CreateUser userID={userID} accessToken={accessToken} teams={teams} possibleUIRoles={possibleUIRoles} />
|
||||
|
||||
<Button
|
||||
onClick={handleToggleSelectionMode}
|
||||
variant={selectionMode ? "primary" : "secondary"}
|
||||
className="flex items-center"
|
||||
>
|
||||
{selectionMode ? "Cancel Selection" : "Select Users"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleToggleSelectionMode}
|
||||
variant={selectionMode ? "primary" : "secondary"}
|
||||
className="flex items-center"
|
||||
>
|
||||
{selectionMode ? "Cancel Selection" : "Select Users"}
|
||||
</Button>
|
||||
|
||||
{selectionMode && (
|
||||
<Button onClick={handleBulkEdit} disabled={selectedUsers.length === 0} className="flex items-center">
|
||||
Bulk Edit ({selectedUsers.length} selected)
|
||||
</Button>
|
||||
)}
|
||||
{selectionMode && (
|
||||
<Button onClick={handleBulkEdit} disabled={selectedUsers.length === 0} className="flex items-center">
|
||||
Bulk Edit ({selectedUsers.length} selected)
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -358,12 +361,18 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<SSOSettings
|
||||
accessToken={accessToken}
|
||||
possibleUIRoles={possibleUIRoles}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
/>
|
||||
{!userID || !userRole || !accessToken ? (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<Skeleton active paragraph={{ rows: 4 }} />
|
||||
</div>
|
||||
) : (
|
||||
<SSOSettings
|
||||
accessToken={accessToken}
|
||||
possibleUIRoles={possibleUIRoles}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
/>
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
|
||||
@@ -94,4 +94,88 @@ describe("UserDataTable", () => {
|
||||
|
||||
expect(onSortChange).toHaveBeenCalledWith("user_email", "desc");
|
||||
});
|
||||
|
||||
it("should show skeleton loaders when isLoading is true", () => {
|
||||
const filters = {
|
||||
email: "",
|
||||
user_id: "",
|
||||
user_role: "",
|
||||
sso_user_id: "",
|
||||
team: "",
|
||||
model: "",
|
||||
min_spend: null,
|
||||
max_spend: null,
|
||||
sort_by: "",
|
||||
sort_order: "asc" as const,
|
||||
};
|
||||
|
||||
const updateFilters = vi.fn();
|
||||
|
||||
render(
|
||||
<UserDataTable
|
||||
data={[]}
|
||||
columns={[]}
|
||||
accessToken={null}
|
||||
userRole={"Admin"}
|
||||
possibleUIRoles={null}
|
||||
filters={filters}
|
||||
updateFilters={updateFilters}
|
||||
initialFilters={filters}
|
||||
teams={[]}
|
||||
handleEdit={vi.fn()}
|
||||
handleDelete={vi.fn()}
|
||||
handleResetPassword={vi.fn()}
|
||||
userListResponse={{ users: [], total: 0, page: 1, page_size: 25, total_pages: 1 }}
|
||||
currentPage={1}
|
||||
handlePageChange={vi.fn()}
|
||||
isLoading={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/Showing/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /Previous/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /Next/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show actual content when isLoading is false", () => {
|
||||
const filters = {
|
||||
email: "",
|
||||
user_id: "",
|
||||
user_role: "",
|
||||
sso_user_id: "",
|
||||
team: "",
|
||||
model: "",
|
||||
min_spend: null,
|
||||
max_spend: null,
|
||||
sort_by: "",
|
||||
sort_order: "asc" as const,
|
||||
};
|
||||
|
||||
const updateFilters = vi.fn();
|
||||
|
||||
render(
|
||||
<UserDataTable
|
||||
data={[]}
|
||||
columns={[]}
|
||||
accessToken={null}
|
||||
userRole={"Admin"}
|
||||
possibleUIRoles={null}
|
||||
filters={filters}
|
||||
updateFilters={updateFilters}
|
||||
initialFilters={filters}
|
||||
teams={[]}
|
||||
handleEdit={vi.fn()}
|
||||
handleDelete={vi.fn()}
|
||||
handleResetPassword={vi.fn()}
|
||||
userListResponse={{ users: [], total: 0, page: 1, page_size: 25, total_pages: 1 }}
|
||||
currentPage={1}
|
||||
handlePageChange={vi.fn()}
|
||||
isLoading={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Showing/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /Previous/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /Next/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ColumnDef, flexRender, getCoreRowModel, SortingState, useReactTable } f
|
||||
import React from "react";
|
||||
import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell, Select, SelectItem } from "@tremor/react";
|
||||
import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline";
|
||||
import { Skeleton } from "antd";
|
||||
import { UserInfo } from "./types";
|
||||
import UserInfoView from "./user_info_view";
|
||||
import { columns as createColumns } from "./columns";
|
||||
@@ -348,40 +349,53 @@ export function UserDataTable({
|
||||
|
||||
{/* Results Count and Pagination */}
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-700">
|
||||
Showing{" "}
|
||||
{userListResponse && userListResponse.users && userListResponse.users.length > 0
|
||||
? (userListResponse.page - 1) * userListResponse.page_size + 1
|
||||
: 0}{" "}
|
||||
-{" "}
|
||||
{userListResponse && userListResponse.users
|
||||
? Math.min(userListResponse.page * userListResponse.page_size, userListResponse.total)
|
||||
: 0}{" "}
|
||||
of {userListResponse ? userListResponse.total : 0} results
|
||||
</span>
|
||||
{isLoading ? (
|
||||
<Skeleton.Input active style={{ width: 192, height: 20 }} />
|
||||
) : (
|
||||
<span className="text-sm text-gray-700">
|
||||
Showing{" "}
|
||||
{userListResponse && userListResponse.users && userListResponse.users.length > 0
|
||||
? (userListResponse.page - 1) * userListResponse.page_size + 1
|
||||
: 0}{" "}
|
||||
-{" "}
|
||||
{userListResponse && userListResponse.users
|
||||
? Math.min(userListResponse.page * userListResponse.page_size, userListResponse.total)
|
||||
: 0}{" "}
|
||||
of {userListResponse ? userListResponse.total : 0} results
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Pagination Buttons */}
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className={`px-3 py-1 text-sm border rounded-md ${
|
||||
currentPage === 1 ? "bg-gray-100 text-gray-400 cursor-not-allowed" : "hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={!userListResponse || currentPage >= userListResponse.total_pages}
|
||||
className={`px-3 py-1 text-sm border rounded-md ${
|
||||
!userListResponse || currentPage >= userListResponse.total_pages
|
||||
? "bg-gray-100 text-gray-400 cursor-not-allowed"
|
||||
: "hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Skeleton.Button active size="small" style={{ width: 80, height: 30 }} />
|
||||
<Skeleton.Button active size="small" style={{ width: 60, height: 30 }} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className={`px-3 py-1 text-sm border rounded-md ${
|
||||
currentPage === 1 ? "bg-gray-100 text-gray-400 cursor-not-allowed" : "hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={!userListResponse || currentPage >= userListResponse.total_pages}
|
||||
className={`px-3 py-1 text-sm border rounded-md ${
|
||||
!userListResponse || currentPage >= userListResponse.total_pages
|
||||
? "bg-gray-100 text-gray-400 cursor-not-allowed"
|
||||
: "hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user