mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-26 03:04:13 +00:00
feat(proxy_server.py): show admin global spend as time series data
This commit is contained in:
parent
e39ce9b119
commit
43da22ae13
8 changed files with 422 additions and 98 deletions
|
@ -3015,16 +3015,7 @@ async def info_key_fn(
|
||||||
tags=["budget & spend Tracking"],
|
tags=["budget & spend Tracking"],
|
||||||
dependencies=[Depends(user_api_key_auth)],
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
)
|
)
|
||||||
async def spend_key_fn(
|
async def spend_key_fn():
|
||||||
start_date: Optional[str] = fastapi.Query(
|
|
||||||
default=None,
|
|
||||||
description="Time from which to start viewing key spend",
|
|
||||||
),
|
|
||||||
end_date: Optional[str] = fastapi.Query(
|
|
||||||
default=None,
|
|
||||||
description="Time till which to view key spend",
|
|
||||||
),
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
View all keys created, ordered by spend
|
View all keys created, ordered by spend
|
||||||
|
|
||||||
|
@ -3041,41 +3032,8 @@ async def spend_key_fn(
|
||||||
f"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys"
|
f"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
key_info = await prisma_client.get_data(table_name="key", query_type="find_all")
|
||||||
start_date is not None
|
return key_info
|
||||||
and isinstance(start_date, str)
|
|
||||||
and end_date is not None
|
|
||||||
and isinstance(end_date, str)
|
|
||||||
):
|
|
||||||
# Convert the date strings to datetime objects
|
|
||||||
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d")
|
|
||||||
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d")
|
|
||||||
|
|
||||||
# SQL query
|
|
||||||
response = await prisma_client.db.litellm_spendlogs.group_by(
|
|
||||||
by=["api_key", "startTime"],
|
|
||||||
where={
|
|
||||||
"startTime": {
|
|
||||||
"gte": start_date_obj, # Greater than or equal to Start Date
|
|
||||||
"lte": end_date_obj, # Less than or equal to End Date
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sum={
|
|
||||||
"spend": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: Execute SQL query and return the results
|
|
||||||
|
|
||||||
return {
|
|
||||||
"message": "This is your SQL query",
|
|
||||||
"response": response,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
key_info = await prisma_client.get_data(
|
|
||||||
table_name="key", query_type="find_all"
|
|
||||||
)
|
|
||||||
return key_info
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
@ -3157,6 +3115,14 @@ async def view_spend_logs(
|
||||||
default=None,
|
default=None,
|
||||||
description="request_id to get spend logs for specific request_id. If none passed then pass spend logs for all requests",
|
description="request_id to get spend logs for specific request_id. If none passed then pass spend logs for all requests",
|
||||||
),
|
),
|
||||||
|
start_date: Optional[str] = fastapi.Query(
|
||||||
|
default=None,
|
||||||
|
description="Time from which to start viewing key spend",
|
||||||
|
),
|
||||||
|
end_date: Optional[str] = fastapi.Query(
|
||||||
|
default=None,
|
||||||
|
description="Time till which to view key spend",
|
||||||
|
),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
View all spend logs, if request_id is provided, only logs for that request_id will be returned
|
View all spend logs, if request_id is provided, only logs for that request_id will be returned
|
||||||
|
@ -3193,7 +3159,76 @@ async def view_spend_logs(
|
||||||
f"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys"
|
f"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys"
|
||||||
)
|
)
|
||||||
spend_logs = []
|
spend_logs = []
|
||||||
if api_key is not None and isinstance(api_key, str):
|
if (
|
||||||
|
start_date is not None
|
||||||
|
and isinstance(start_date, str)
|
||||||
|
and end_date is not None
|
||||||
|
and isinstance(end_date, str)
|
||||||
|
):
|
||||||
|
# Convert the date strings to datetime objects
|
||||||
|
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d")
|
||||||
|
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d")
|
||||||
|
|
||||||
|
filter_query = {
|
||||||
|
"startTime": {
|
||||||
|
"gte": start_date_obj, # Greater than or equal to Start Date
|
||||||
|
"lte": end_date_obj, # Less than or equal to End Date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if api_key is not None and isinstance(api_key, str):
|
||||||
|
filter_query["api_key"] = api_key # type: ignore
|
||||||
|
elif request_id is not None and isinstance(request_id, str):
|
||||||
|
filter_query["request_id"] = request_id # type: ignore
|
||||||
|
elif user_id is not None and isinstance(user_id, str):
|
||||||
|
filter_query["user"] = user_id # type: ignore
|
||||||
|
|
||||||
|
# SQL query
|
||||||
|
response = await prisma_client.db.litellm_spendlogs.group_by(
|
||||||
|
by=["startTime"],
|
||||||
|
where=filter_query, # type: ignore
|
||||||
|
sum={
|
||||||
|
"spend": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
isinstance(response, list)
|
||||||
|
and len(response) > 0
|
||||||
|
and isinstance(response[0], dict)
|
||||||
|
):
|
||||||
|
result: dict = {}
|
||||||
|
for record in response:
|
||||||
|
dt_object = datetime.strptime(
|
||||||
|
str(record["startTime"]), "%Y-%m-%dT%H:%M:%S.%fZ"
|
||||||
|
) # type: ignore
|
||||||
|
date = dt_object.date()
|
||||||
|
if date not in result:
|
||||||
|
result[date] = {}
|
||||||
|
result[date]["spend"] = (
|
||||||
|
result[date].get("spend", 0) + record["_sum"]["spend"]
|
||||||
|
)
|
||||||
|
return_list = []
|
||||||
|
final_date = None
|
||||||
|
for k, v in sorted(result.items()):
|
||||||
|
return_list.append({**v, "startTime": k})
|
||||||
|
final_date = k
|
||||||
|
|
||||||
|
end_date_date = end_date_obj.date()
|
||||||
|
if final_date is not None and final_date < end_date_date:
|
||||||
|
current_date = final_date + timedelta(days=1)
|
||||||
|
while current_date <= end_date_date:
|
||||||
|
# Represent current_date as string because original response has it this way
|
||||||
|
return_list.append(
|
||||||
|
{"startTime": current_date, "spend": 0}
|
||||||
|
) # If no data, will stay as zero
|
||||||
|
current_date += timedelta(days=1) # Move on to the next day
|
||||||
|
|
||||||
|
return return_list
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
elif api_key is not None and isinstance(api_key, str):
|
||||||
if api_key.startswith("sk-"):
|
if api_key.startswith("sk-"):
|
||||||
hashed_token = prisma_client.hash_token(token=api_key)
|
hashed_token = prisma_client.hash_token(token=api_key)
|
||||||
else:
|
else:
|
||||||
|
@ -3478,12 +3513,22 @@ async def login(request: Request):
|
||||||
if secrets.compare_digest(username, ui_username) and secrets.compare_digest(
|
if secrets.compare_digest(username, ui_username) and secrets.compare_digest(
|
||||||
password, ui_password
|
password, ui_password
|
||||||
):
|
):
|
||||||
|
user_role = "app_owner"
|
||||||
user_id = username
|
user_id = username
|
||||||
# User is Authe'd in - generate key for the UI to access Proxy
|
key_user_id = user_id
|
||||||
|
if (
|
||||||
|
os.getenv("PROXY_ADMIN_ID", None) is not None
|
||||||
|
and os.environ["PROXY_ADMIN_ID"] == user_id
|
||||||
|
) or user_id == "admin":
|
||||||
|
# checks if user is admin
|
||||||
|
user_role = "app_admin"
|
||||||
|
key_user_id = os.getenv("PROXY_ADMIN_ID", "default_user_id")
|
||||||
|
|
||||||
|
# Admin is Authe'd in - generate key for the UI to access Proxy
|
||||||
|
|
||||||
if os.getenv("DATABASE_URL") is not None:
|
if os.getenv("DATABASE_URL") is not None:
|
||||||
response = await generate_key_helper_fn(
|
response = await generate_key_helper_fn(
|
||||||
**{"duration": "1hr", "key_max_budget": 0, "models": [], "aliases": {}, "config": {}, "spend": 0, "user_id": user_id, "team_id": "litellm-dashboard"} # type: ignore
|
**{"duration": "1hr", "key_max_budget": 0, "models": [], "aliases": {}, "config": {}, "spend": 0, "user_id": key_user_id, "team_id": "litellm-dashboard"} # type: ignore
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
response = {
|
response = {
|
||||||
|
@ -3492,18 +3537,9 @@ async def login(request: Request):
|
||||||
}
|
}
|
||||||
|
|
||||||
key = response["token"] # type: ignore
|
key = response["token"] # type: ignore
|
||||||
user_id = response["user_id"] # type: ignore
|
|
||||||
|
|
||||||
litellm_dashboard_ui = "/ui/"
|
litellm_dashboard_ui = "/ui/"
|
||||||
|
|
||||||
user_role = "app_owner"
|
|
||||||
if (
|
|
||||||
os.getenv("PROXY_ADMIN_ID", None) is not None
|
|
||||||
and os.environ["PROXY_ADMIN_ID"] == user_id
|
|
||||||
):
|
|
||||||
# checks if user is admin
|
|
||||||
user_role = "app_admin"
|
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
jwt_token = jwt.encode(
|
jwt_token = jwt.encode(
|
||||||
|
|
|
@ -240,7 +240,10 @@ class ProxyLogging:
|
||||||
else:
|
else:
|
||||||
user_info = str(user_info)
|
user_info = str(user_info)
|
||||||
# percent of max_budget left to spend
|
# percent of max_budget left to spend
|
||||||
percent_left = (user_max_budget - user_current_spend) / user_max_budget
|
if user_max_budget > 0:
|
||||||
|
percent_left = (user_max_budget - user_current_spend) / user_max_budget
|
||||||
|
else:
|
||||||
|
percent_left = 0
|
||||||
verbose_proxy_logger.debug(
|
verbose_proxy_logger.debug(
|
||||||
f"Budget Alerts: Percent left: {percent_left} for {user_info}"
|
f"Budget Alerts: Percent left: {percent_left} for {user_info}"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,12 +1,93 @@
|
||||||
import React, { Suspense } from "react";
|
"use client";
|
||||||
|
import React, { Suspense, useEffect, useState } from "react";
|
||||||
|
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 Sidebar from "../components/leftnav";
|
||||||
|
import Usage from "../components/usage";
|
||||||
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
|
||||||
const CreateKeyPage = () => {
|
const CreateKeyPage = () => {
|
||||||
|
const [userRole, setUserRole] = useState<null | string>(null);
|
||||||
|
const [userEmail, setUserEmail] = useState<null | string>(null);
|
||||||
|
const [page, setPage] = useState("api-keys");
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const userID = searchParams.get("userID");
|
||||||
|
const token = searchParams.get("token");
|
||||||
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (token) {
|
||||||
|
const decoded = jwtDecode(token) as { [key: string]: any };
|
||||||
|
if (decoded) {
|
||||||
|
// cast decoded to dictionary
|
||||||
|
console.log("Decoded token:", decoded);
|
||||||
|
|
||||||
|
console.log("Decoded key:", decoded.key);
|
||||||
|
// set accessToken
|
||||||
|
setAccessToken(decoded.key);
|
||||||
|
|
||||||
|
// check if userRole is defined
|
||||||
|
if (decoded.user_role) {
|
||||||
|
const formattedUserRole = formatUserRole(decoded.user_role);
|
||||||
|
console.log("Decoded user_role:", formattedUserRole);
|
||||||
|
setUserRole(formattedUserRole);
|
||||||
|
} else {
|
||||||
|
console.log("User role not defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoded.user_email) {
|
||||||
|
setUserEmail(decoded.user_email);
|
||||||
|
} else {
|
||||||
|
console.log(`User Email is not set ${decoded}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
function formatUserRole(userRole: string) {
|
||||||
|
if (!userRole) {
|
||||||
|
return "Undefined Role";
|
||||||
|
}
|
||||||
|
console.log(`Received user role: ${userRole}`);
|
||||||
|
switch (userRole.toLowerCase()) {
|
||||||
|
case "app_owner":
|
||||||
|
return "App Owner";
|
||||||
|
case "demo_app_owner":
|
||||||
|
return "App Owner";
|
||||||
|
case "app_admin":
|
||||||
|
return "Admin";
|
||||||
|
case "app_user":
|
||||||
|
return "App User";
|
||||||
|
default:
|
||||||
|
return "Unknown Role";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
<div className="flex min-h-screen flex-col ">
|
<div className="flex flex-col min-h-screen">
|
||||||
<UserDashboard />
|
<Navbar userID={userID} userRole={userRole} userEmail={userEmail} />
|
||||||
</div>
|
<div className="flex flex-1 overflow-auto">
|
||||||
|
<Sidebar setPage={setPage} />
|
||||||
|
{page == "api-keys" ? (
|
||||||
|
<UserDashboard
|
||||||
|
userID={userID}
|
||||||
|
userRole={userRole}
|
||||||
|
setUserRole={setUserRole}
|
||||||
|
userEmail={userEmail}
|
||||||
|
setUserEmail={setUserEmail}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Usage
|
||||||
|
userID={userID}
|
||||||
|
userRole={userRole}
|
||||||
|
token={token}
|
||||||
|
accessToken={accessToken}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
32
ui/litellm-dashboard/src/components/leftnav.tsx
Normal file
32
ui/litellm-dashboard/src/components/leftnav.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { Layout, Menu } from "antd";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const { Sider } = Layout;
|
||||||
|
|
||||||
|
// Define the props type
|
||||||
|
interface SidebarProps {
|
||||||
|
setPage: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Sidebar: React.FC<SidebarProps> = ({ setPage }) => {
|
||||||
|
return (
|
||||||
|
<Layout style={{ minHeight: "100vh", maxWidth: "120px" }}>
|
||||||
|
<Sider width={120}>
|
||||||
|
<Menu
|
||||||
|
mode="inline"
|
||||||
|
defaultSelectedKeys={["1"]}
|
||||||
|
style={{ height: "100%", borderRight: 0 }}
|
||||||
|
>
|
||||||
|
<Menu.Item key="1" onClick={() => setPage("api-keys")}>
|
||||||
|
API Keys
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="2" onClick={() => setPage("usage")}>
|
||||||
|
Usage
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
</Sider>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
|
@ -162,3 +162,40 @@ export const keySpendLogsCall = async (accessToken: String, token: String) => {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const userSpendLogsCall = async (
|
||||||
|
accessToken: String,
|
||||||
|
token: String,
|
||||||
|
userRole: String,
|
||||||
|
userID: String,
|
||||||
|
startTime: String,
|
||||||
|
endTime: String
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
let url = proxyBaseUrl ? `${proxyBaseUrl}/spend/logs` : `/spend/logs`;
|
||||||
|
if (userRole == "App Owner") {
|
||||||
|
url = `${url}/?user_id=${userID}&start_date=${startTime}&end_date=${endTime}`;
|
||||||
|
} else {
|
||||||
|
url = `${url}/?start_date=${startTime}&end_date=${endTime}`;
|
||||||
|
}
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.text();
|
||||||
|
message.error(errorData);
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create key:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
114
ui/litellm-dashboard/src/components/usage.tsx
Normal file
114
ui/litellm-dashboard/src/components/usage.tsx
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import { BarChart, Card, Title } from "@tremor/react";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Grid, Col, Text } from "@tremor/react";
|
||||||
|
import { userSpendLogsCall } from "./networking";
|
||||||
|
import { AreaChart, Flex, Switch, Subtitle } from "@tremor/react";
|
||||||
|
|
||||||
|
interface UsagePageProps {
|
||||||
|
accessToken: string | null;
|
||||||
|
token: string | null;
|
||||||
|
userRole: string | null;
|
||||||
|
userID: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataType = {
|
||||||
|
api_key: string;
|
||||||
|
startTime: string;
|
||||||
|
_sum: {
|
||||||
|
spend: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const UsagePage: React.FC<UsagePageProps> = ({
|
||||||
|
accessToken,
|
||||||
|
token,
|
||||||
|
userRole,
|
||||||
|
userID,
|
||||||
|
}) => {
|
||||||
|
const currentDate = new Date();
|
||||||
|
const [keySpendData, setKeySpendData] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const firstDay = new Date(
|
||||||
|
currentDate.getFullYear(),
|
||||||
|
currentDate.getMonth(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
const lastDay = new Date(
|
||||||
|
currentDate.getFullYear(),
|
||||||
|
currentDate.getMonth() + 1,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
let startTime = formatDate(firstDay);
|
||||||
|
let endTime = formatDate(lastDay);
|
||||||
|
|
||||||
|
function formatDate(date: Date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
let month = date.getMonth() + 1; // JS month index starts from 0
|
||||||
|
let day = date.getDate();
|
||||||
|
|
||||||
|
// Pad with 0 if month or day is less than 10
|
||||||
|
const monthStr = month < 10 ? "0" + month : month;
|
||||||
|
const dayStr = day < 10 ? "0" + day : day;
|
||||||
|
|
||||||
|
return `${year}-${monthStr}-${dayStr}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Start date is ${startTime}`);
|
||||||
|
console.log(`End date is ${endTime}`);
|
||||||
|
|
||||||
|
const valueFormatter = (number: number) =>
|
||||||
|
`$ ${new Intl.NumberFormat("us").format(number).toString()}`;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (accessToken && token && userRole && userID) {
|
||||||
|
const cachedKeySpendData = localStorage.getItem("keySpendData");
|
||||||
|
if (cachedKeySpendData) {
|
||||||
|
setKeySpendData(JSON.parse(cachedKeySpendData));
|
||||||
|
} else {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await userSpendLogsCall(
|
||||||
|
accessToken,
|
||||||
|
token,
|
||||||
|
userRole,
|
||||||
|
userID,
|
||||||
|
startTime,
|
||||||
|
endTime
|
||||||
|
);
|
||||||
|
setKeySpendData(response);
|
||||||
|
localStorage.setItem("keySpendData", JSON.stringify(response));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("There was an error fetching the data", error);
|
||||||
|
// Optionally, update your UI to reflect the error state here as well
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [accessToken, token, userRole, userID]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<Grid numItems={1} className="gap-0 p-10 h-[75vh] w-full">
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Card>
|
||||||
|
<Title>Monthly Spend</Title>
|
||||||
|
<BarChart
|
||||||
|
data={keySpendData}
|
||||||
|
index="startTime"
|
||||||
|
categories={["spend"]}
|
||||||
|
colors={["blue"]}
|
||||||
|
valueFormatter={valueFormatter}
|
||||||
|
yAxisWidth={100}
|
||||||
|
tickGap={5}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UsagePage;
|
|
@ -20,7 +20,21 @@ type UserSpendData = {
|
||||||
max_budget?: number | null;
|
max_budget?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserDashboard = () => {
|
interface UserDashboardProps {
|
||||||
|
userID: string | null;
|
||||||
|
userRole: string | null;
|
||||||
|
userEmail: string | null;
|
||||||
|
setUserRole: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
setUserEmail: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||||
|
userID,
|
||||||
|
userRole,
|
||||||
|
setUserRole,
|
||||||
|
userEmail,
|
||||||
|
setUserEmail,
|
||||||
|
}) => {
|
||||||
const [data, setData] = useState<null | any[]>(null); // Keep the initialization of state here
|
const [data, setData] = useState<null | any[]>(null); // Keep the initialization of state here
|
||||||
const [userSpendData, setUserSpendData] = useState<UserSpendData | null>(
|
const [userSpendData, setUserSpendData] = useState<UserSpendData | null>(
|
||||||
null
|
null
|
||||||
|
@ -28,13 +42,10 @@ const UserDashboard = () => {
|
||||||
|
|
||||||
// Assuming useSearchParams() hook exists and works in your setup
|
// Assuming useSearchParams() hook exists and works in your setup
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const userID = searchParams.get("userID");
|
|
||||||
const viewSpend = searchParams.get("viewSpend");
|
const viewSpend = searchParams.get("viewSpend");
|
||||||
|
|
||||||
const token = searchParams.get("token");
|
const token = searchParams.get("token");
|
||||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||||
const [userRole, setUserRole] = useState<string | null>(null);
|
|
||||||
const [userEmail, setUserEmail] = useState<string | null>(null);
|
|
||||||
|
|
||||||
function formatUserRole(userRole: string) {
|
function formatUserRole(userRole: string) {
|
||||||
if (!userRole) {
|
if (!userRole) {
|
||||||
|
@ -84,17 +95,29 @@ const UserDashboard = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (userID && accessToken && userRole && !data) {
|
if (userID && accessToken && userRole && !data) {
|
||||||
const fetchData = async () => {
|
const cachedData = localStorage.getItem("userData");
|
||||||
try {
|
const cachedSpendData = localStorage.getItem("userSpendData");
|
||||||
const response = await userInfoCall(accessToken, userID, userRole);
|
if (cachedData && cachedSpendData) {
|
||||||
setUserSpendData(response["user_info"]);
|
setData(JSON.parse(cachedData));
|
||||||
setData(response["keys"]); // Assuming this is the correct path to your data
|
setUserSpendData(JSON.parse(cachedSpendData));
|
||||||
} catch (error) {
|
} else {
|
||||||
console.error("There was an error fetching the data", error);
|
const fetchData = async () => {
|
||||||
// Optionally, update your UI to reflect the error state here as well
|
try {
|
||||||
}
|
const response = await userInfoCall(accessToken, userID, userRole);
|
||||||
};
|
setUserSpendData(response["user_info"]);
|
||||||
fetchData();
|
setData(response["keys"]); // Assuming this is the correct path to your data
|
||||||
|
localStorage.setItem("userData", JSON.stringify(response["keys"]));
|
||||||
|
localStorage.setItem(
|
||||||
|
"userSpendData",
|
||||||
|
JSON.stringify(response["user_info"])
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("There was an error fetching the data", error);
|
||||||
|
// Optionally, update your UI to reflect the error state here as well
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [userID, token, accessToken, data]);
|
}, [userID, token, accessToken, data]);
|
||||||
|
|
||||||
|
@ -117,7 +140,6 @@ const UserDashboard = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Navbar userID={userID} userRole={userRole} userEmail={userEmail} />
|
|
||||||
<Grid numItems={1} className="gap-0 p-10 h-[75vh] w-full">
|
<Grid numItems={1} className="gap-0 p-10 h-[75vh] w-full">
|
||||||
<Col numColSpan={1}>
|
<Col numColSpan={1}>
|
||||||
<ViewUserSpend userID={userID} userSpendData={userSpendData} />
|
<ViewUserSpend userID={userID} userSpendData={userSpendData} />
|
||||||
|
|
|
@ -72,21 +72,6 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
setKeyToDelete(null);
|
setKeyToDelete(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// const handleDelete = async (token: String) => {
|
|
||||||
// if (data == null) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// try {
|
|
||||||
// await keyDeleteCall(accessToken, token);
|
|
||||||
// // Successfully completed the deletion. Update the state to trigger a rerender.
|
|
||||||
// const filteredData = data.filter((item) => item.token !== token);
|
|
||||||
// setData(filteredData);
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error("Error deleting the key:", error);
|
|
||||||
// // Handle any error situations, such as displaying an error message to the user.
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -147,7 +132,11 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
<Text>{JSON.stringify(item.models)}</Text>
|
<Text>{JSON.stringify(item.models)}</Text>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Text>TPM Limit: {item.tpm_limit? item.tpm_limit : "Unlimited"} <br></br> RPM Limit: {item.rpm_limit? item.rpm_limit : "Unlimited"}</Text>
|
<Text>
|
||||||
|
TPM Limit: {item.tpm_limit ? item.tpm_limit : "Unlimited"}{" "}
|
||||||
|
<br></br> RPM Limit:{" "}
|
||||||
|
{item.rpm_limit ? item.rpm_limit : "Unlimited"}
|
||||||
|
</Text>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{item.expires != null ? (
|
{item.expires != null ? (
|
||||||
|
@ -180,12 +169,18 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
{isDeleteModalOpen && (
|
{isDeleteModalOpen && (
|
||||||
<div className="fixed z-10 inset-0 overflow-y-auto">
|
<div className="fixed z-10 inset-0 overflow-y-auto">
|
||||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<div className="fixed inset-0 transition-opacity" aria-hidden="true">
|
<div
|
||||||
|
className="fixed inset-0 transition-opacity"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
|
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Panel */}
|
{/* Modal Panel */}
|
||||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
<span
|
||||||
|
className="hidden sm:inline-block sm:align-middle sm:h-screen"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
​
|
​
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
@ -194,9 +189,13 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div className="sm:flex sm:items-start">
|
<div className="sm:flex sm:items-start">
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Delete Key</h3>
|
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
Delete Key
|
||||||
|
</h3>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-sm text-gray-500">Are you sure you want to delete this key ?</p>
|
<p className="text-sm text-gray-500">
|
||||||
|
Are you sure you want to delete this key ?
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue