mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
refactor(ui): migrate users dashboard to shadcn (#36642)
* test(ui): characterize users dashboard behavior * refactor(ui): migrate users dashboard to shadcn * fix(ui): preserve users tab state
This commit is contained in:
parent
4445eb71f6
commit
d329951999
3 changed files with 102 additions and 34 deletions
|
|
@ -1878,9 +1878,6 @@
|
|||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 3
|
||||
},
|
||||
"prefer-const": {
|
||||
"count": 1
|
||||
},
|
||||
|
|
|
|||
|
|
@ -37,6 +37,20 @@ vi.mock("./view_users/user_info_view", () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
vi.mock("./default-user-settings/DefaultUserSettingsForm", () => ({
|
||||
DefaultUserSettingsForm: function DefaultUserSettingsFormMock() {
|
||||
const [value, setValue] = React.useState("");
|
||||
return (
|
||||
<section aria-label="Default user settings panel">
|
||||
<label>
|
||||
Default setting
|
||||
<input value={value} onChange={(event) => setValue(event.target.value)} />
|
||||
</label>
|
||||
</section>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock NotificationsManager
|
||||
vi.mock("@/components/molecules/notifications_manager", () => ({
|
||||
default: {
|
||||
|
|
@ -78,10 +92,10 @@ const defaultProps = {
|
|||
teams: [],
|
||||
};
|
||||
|
||||
const renderDashboard = () =>
|
||||
const renderDashboard = (overrides: Partial<typeof defaultProps> = {}) =>
|
||||
renderWithProviders(
|
||||
<QueryClientProvider client={createQueryClient()}>
|
||||
<ViewUserDashboard {...defaultProps} />
|
||||
<ViewUserDashboard {...defaultProps} {...overrides} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
|
|
@ -107,6 +121,51 @@ describe("ViewUserDashboard", () => {
|
|||
expect(screen.getAllByText("Default User Settings").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("switches between the users table and default settings tabs for proxy admins", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderDashboard();
|
||||
|
||||
expect(await screen.findByText("test@example.com")).toBeInTheDocument();
|
||||
|
||||
const usersTab = screen.getByRole("tab", { name: "Users" });
|
||||
const settingsTab = screen.getByRole("tab", { name: "Default User Settings" });
|
||||
expect(usersTab).toHaveAttribute("aria-selected", "true");
|
||||
|
||||
await user.click(settingsTab);
|
||||
|
||||
expect(settingsTab).toHaveAttribute("aria-selected", "true");
|
||||
expect(usersTab).toHaveAttribute("aria-selected", "false");
|
||||
expect(screen.getByRole("region", { name: "Default user settings panel" })).toBeInTheDocument();
|
||||
await user.type(screen.getByRole("textbox", { name: "Default setting" }), "unsaved change");
|
||||
|
||||
await user.click(usersTab);
|
||||
|
||||
expect(usersTab).toHaveAttribute("aria-selected", "true");
|
||||
expect(settingsTab).toHaveAttribute("aria-selected", "false");
|
||||
|
||||
await user.click(settingsTab);
|
||||
|
||||
expect(screen.getByRole("textbox", { name: "Default setting" })).toHaveValue("unsaved change");
|
||||
});
|
||||
|
||||
it("shows the users table without admin controls for non-proxy admins", async () => {
|
||||
renderDashboard({ userRole: "Internal User" });
|
||||
|
||||
expect(await screen.findByText("test@example.com")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("tab")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("toggle-user-selection")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps actions unavailable while the user list is loading", () => {
|
||||
userListCall.mockReturnValue(new Promise(() => undefined));
|
||||
|
||||
renderDashboard();
|
||||
|
||||
expect(screen.getByText("Loading users…")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("toggle-user-selection")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("bulk-edit-users")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show delete modal after choosing delete from the row actions menu", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderDashboard();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
|
||||
import { parseAsString, useQueryState } from "nuqs";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { Button } from "antd";
|
||||
import BulkEditUserModal from "./BulkEditUsers";
|
||||
import { CreateUserButton } from "@/components/CreateUserButton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import EditUserModal from "./edit_user";
|
||||
import {
|
||||
getPossibleUserRoles,
|
||||
|
|
@ -35,7 +36,6 @@ import { DefaultUserSettingsForm } from "./default-user-settings/DefaultUserSett
|
|||
import { UsersTable } from "./view_users/UsersTable";
|
||||
import UserInfoView from "./view_users/user_info_view";
|
||||
import { UserInfo } from "@/components/networking";
|
||||
import { Skeleton } from "antd";
|
||||
|
||||
interface ViewUserDashboardProps {
|
||||
accessToken: string | null;
|
||||
|
|
@ -352,14 +352,14 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="w-full p-8 overflow-hidden">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-full overflow-hidden p-8">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex space-x-3">
|
||||
{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 }} />
|
||||
<Skeleton className="h-9 w-28" />
|
||||
<Skeleton className="h-9 w-36" />
|
||||
<Skeleton className="h-9 w-28" />
|
||||
</>
|
||||
)}
|
||||
{!userListQuery.isLoading && userID && accessToken && (
|
||||
|
|
@ -375,9 +375,9 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
|
||||
{isProxyAdmin && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleToggleSelectionMode}
|
||||
type={selectionMode ? "primary" : "default"}
|
||||
className="flex items-center"
|
||||
variant={selectionMode ? "default" : "outline"}
|
||||
data-testid="toggle-user-selection"
|
||||
>
|
||||
{selectionMode ? "Cancel Selection" : "Select Users"}
|
||||
|
|
@ -386,10 +386,9 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
|
||||
{isProxyAdmin && selectionMode && (
|
||||
<Button
|
||||
type="primary"
|
||||
type="button"
|
||||
onClick={() => setIsBulkEditModalVisible(true)}
|
||||
disabled={selectedUsers.length === 0}
|
||||
className="flex items-center"
|
||||
data-testid="bulk-edit-users"
|
||||
>
|
||||
Bulk Edit ({selectedUsers.length} selected)
|
||||
|
|
@ -401,26 +400,39 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
</div>
|
||||
|
||||
{isProxyAdmin ? (
|
||||
<TabGroup defaultIndex={0}>
|
||||
<TabList className="mb-4">
|
||||
<Tab>Users</Tab>
|
||||
<Tab>Default User Settings</Tab>
|
||||
</TabList>
|
||||
<Tabs defaultValue="users" className="gap-0">
|
||||
<TabsList variant="line" className="mb-4">
|
||||
<TabsTrigger value="users" className="flex-none data-active:text-primary after:bg-primary">
|
||||
Users
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="default-settings" className="flex-none data-active:text-primary after:bg-primary">
|
||||
Default User Settings
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>{usersTable}</TabPanel>
|
||||
<TabsContent value="users" keepMounted>
|
||||
{usersTable}
|
||||
</TabsContent>
|
||||
|
||||
<TabPanel>
|
||||
{!userID || !userRole || !accessToken ? (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<Skeleton active paragraph={{ rows: 4 }} />
|
||||
<TabsContent value="default-settings" keepMounted>
|
||||
{!userID || !userRole || !accessToken ? (
|
||||
<div
|
||||
className="flex h-64 items-center justify-center"
|
||||
role="status"
|
||||
aria-label="Loading default user settings"
|
||||
>
|
||||
<div className="w-full max-w-lg space-y-3">
|
||||
<Skeleton className="h-5 w-1/3" />
|
||||
<Skeleton className="h-5 w-full" />
|
||||
<Skeleton className="h-5 w-full" />
|
||||
<Skeleton className="h-5 w-2/3" />
|
||||
</div>
|
||||
) : (
|
||||
<DefaultUserSettingsForm possibleUIRoles={possibleUIRoles} />
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
) : (
|
||||
<DefaultUserSettingsForm possibleUIRoles={possibleUIRoles} />
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
usersTable
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue