forked from phoenix/litellm-mirror
Merge pull request #2101 from BerriAI/litellm_allow_admin_create_users
[FEAT] UI - View all users
This commit is contained in:
commit
c656eaf7a4
5 changed files with 126 additions and 6 deletions
|
@ -4020,7 +4020,11 @@ async def user_auth(request: Request):
|
||||||
async def user_info(
|
async def user_info(
|
||||||
user_id: Optional[str] = fastapi.Query(
|
user_id: Optional[str] = fastapi.Query(
|
||||||
default=None, description="User ID in the request parameters"
|
default=None, description="User ID in the request parameters"
|
||||||
)
|
),
|
||||||
|
view_all: bool = fastapi.Query(
|
||||||
|
default=False,
|
||||||
|
description="set to true to View all users. When using view_all, don't pass user_id",
|
||||||
|
),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Use this to get user information. (user row + all user key info)
|
Use this to get user information. (user row + all user key info)
|
||||||
|
@ -4040,6 +4044,11 @@ async def user_info(
|
||||||
## GET USER ROW ##
|
## GET USER ROW ##
|
||||||
if user_id is not None:
|
if user_id is not None:
|
||||||
user_info = await prisma_client.get_data(user_id=user_id)
|
user_info = await prisma_client.get_data(user_id=user_id)
|
||||||
|
elif view_all == True:
|
||||||
|
user_info = await prisma_client.get_data(
|
||||||
|
table_name="user", query_type="find_all"
|
||||||
|
)
|
||||||
|
return user_info
|
||||||
else:
|
else:
|
||||||
user_info = None
|
user_info = None
|
||||||
## GET ALL TEAMS ##
|
## GET ALL TEAMS ##
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useSearchParams } from "next/navigation";
|
||||||
import Navbar from "../components/navbar";
|
import Navbar from "../components/navbar";
|
||||||
import UserDashboard from "../components/user_dashboard";
|
import UserDashboard from "../components/user_dashboard";
|
||||||
import ModelDashboard from "@/components/model_dashboard";
|
import ModelDashboard from "@/components/model_dashboard";
|
||||||
|
import ViewUserDashboard from "@/components/view_users";
|
||||||
import ChatUI from "@/components/chat_ui";
|
import ChatUI from "@/components/chat_ui";
|
||||||
import Sidebar from "../components/leftnav";
|
import Sidebar from "../components/leftnav";
|
||||||
import Usage from "../components/usage";
|
import Usage from "../components/usage";
|
||||||
|
@ -68,12 +69,13 @@ const CreateKeyPage = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
<div className="flex flex-col min-h-screen">
|
<div className="flex flex-col min-h-screen">
|
||||||
<Navbar userID={userID} userRole={userRole} userEmail={userEmail} />
|
<Navbar userID={userID} userRole={userRole} userEmail={userEmail} />
|
||||||
<div className="flex flex-1 overflow-auto">
|
<div className="flex flex-1 overflow-auto">
|
||||||
<Sidebar setPage={setPage} />
|
<Sidebar setPage={setPage} userRole={userRole}/>
|
||||||
{page == "api-keys" ? (
|
{page == "api-keys" ? (
|
||||||
<UserDashboard
|
<UserDashboard
|
||||||
userID={userID}
|
userID={userID}
|
||||||
|
@ -97,6 +99,14 @@ const CreateKeyPage = () => {
|
||||||
accessToken={accessToken}
|
accessToken={accessToken}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
: page == "users" ? (
|
||||||
|
<ViewUserDashboard
|
||||||
|
userID={userID}
|
||||||
|
userRole={userRole}
|
||||||
|
token={token}
|
||||||
|
accessToken={accessToken}
|
||||||
|
/>
|
||||||
|
)
|
||||||
: (
|
: (
|
||||||
<Usage
|
<Usage
|
||||||
userID={userID}
|
userID={userID}
|
||||||
|
|
|
@ -6,9 +6,10 @@ const { Sider } = Layout;
|
||||||
// Define the props type
|
// Define the props type
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
setPage: React.Dispatch<React.SetStateAction<string>>;
|
setPage: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
userRole: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sidebar: React.FC<SidebarProps> = ({ setPage }) => {
|
const Sidebar: React.FC<SidebarProps> = ({ setPage, userRole }) => {
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: "100vh", maxWidth: "120px" }}>
|
<Layout style={{ minHeight: "100vh", maxWidth: "120px" }}>
|
||||||
<Sider width={120}>
|
<Sider width={120}>
|
||||||
|
@ -29,6 +30,13 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage }) => {
|
||||||
<Menu.Item key="4" onClick={() => setPage("usage")}>
|
<Menu.Item key="4" onClick={() => setPage("usage")}>
|
||||||
Usage
|
Usage
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
{
|
||||||
|
userRole == "Admin" ?
|
||||||
|
<Menu.Item key="5" onClick={() => setPage("users")}>
|
||||||
|
Users
|
||||||
|
</Menu.Item>
|
||||||
|
: null
|
||||||
|
}
|
||||||
</Menu>
|
</Menu>
|
||||||
</Sider>
|
</Sider>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -104,14 +104,19 @@ export const keyDeleteCall = async (accessToken: String, user_key: String) => {
|
||||||
|
|
||||||
export const userInfoCall = async (
|
export const userInfoCall = async (
|
||||||
accessToken: String,
|
accessToken: String,
|
||||||
userID: String,
|
userID: String | null,
|
||||||
userRole: String
|
userRole: String,
|
||||||
|
viewAll: Boolean = false
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/user/info` : `/user/info`;
|
let url = proxyBaseUrl ? `${proxyBaseUrl}/user/info` : `/user/info`;
|
||||||
if (userRole == "App Owner") {
|
if (userRole == "App Owner" && userID) {
|
||||||
url = `${url}/?user_id=${userID}`;
|
url = `${url}/?user_id=${userID}`;
|
||||||
}
|
}
|
||||||
|
console.log("in userInfoCall viewAll=", viewAll);
|
||||||
|
if (viewAll) {
|
||||||
|
url = `${url}/?view_all=true`;
|
||||||
|
}
|
||||||
message.info("Requesting user data");
|
message.info("Requesting user data");
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|
88
ui/litellm-dashboard/src/components/view_users.tsx
Normal file
88
ui/litellm-dashboard/src/components/view_users.tsx
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Card, Title, Subtitle, Table, TableHead, TableRow, TableCell, TableBody, Metric, Grid } from "@tremor/react";
|
||||||
|
import { userInfoCall } from "./networking";
|
||||||
|
import { Badge, BadgeDelta, Button } from '@tremor/react';
|
||||||
|
import RequestAccess from "./request_model_access";
|
||||||
|
|
||||||
|
interface ViewUserDashboardProps {
|
||||||
|
accessToken: string | null;
|
||||||
|
token: string | null;
|
||||||
|
userRole: string | null;
|
||||||
|
userID: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
|
accessToken,
|
||||||
|
token,
|
||||||
|
userRole,
|
||||||
|
userID,
|
||||||
|
}) => {
|
||||||
|
const [userData, setuserData] = useState<null | any[]>(null);
|
||||||
|
const [pendingRequests, setPendingRequests] = useState<any[]>([]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!accessToken || !token || !userRole || !userID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
// Replace with your actual API call for model data
|
||||||
|
const userDataResponse = await userInfoCall(accessToken, null, userRole, true);
|
||||||
|
console.log("user data response:", userDataResponse);
|
||||||
|
setuserData(userDataResponse);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("There was an error fetching the model data", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (accessToken && token && userRole && userID) {
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
}, [accessToken, token, userRole, userID]);
|
||||||
|
|
||||||
|
if (!userData) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken || !token || !userRole || !userID) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// when users click request access show pop up to allow them to request access
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<Grid className="gap-2 p-10 h-[75vh] w-full">
|
||||||
|
<Card>
|
||||||
|
<Table className="mt-5">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell><Title>User ID </Title></TableCell>
|
||||||
|
<TableCell><Title>User Role</Title></TableCell>
|
||||||
|
<TableCell><Title>User Models</Title></TableCell>
|
||||||
|
<TableCell><Title>User Spend ($ USD)</Title></TableCell>
|
||||||
|
<TableCell><Title>User Max Budget ($ USD)</Title></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{userData.map((user: any) => (
|
||||||
|
<TableRow key={user.user_id}>
|
||||||
|
<TableCell><Title>{user.user_id}</Title></TableCell>
|
||||||
|
<TableCell><Title>{user.user_role ? user.user_role : "app_user"}</Title></TableCell>
|
||||||
|
<TableCell><Title>{user.models && user.models.length > 0 ? user.models : "All Models"}</Title></TableCell>
|
||||||
|
<TableCell><Title>{user.spend ? user.spend : 0}</Title></TableCell>
|
||||||
|
<TableCell><Title>{user.max_budget ? user.max_budget : "Unlimited"}</Title></TableCell>
|
||||||
|
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewUserDashboard;
|
Loading…
Add table
Add a link
Reference in a new issue