feat(ui): enable admin to view all valid keys created on the proxy

This commit is contained in:
Krrish Dholakia 2024-02-05 19:28:57 -08:00
parent cdbbedec36
commit f2a7e2ee98
7 changed files with 312 additions and 209 deletions

View file

@ -322,6 +322,7 @@ async def user_api_key_auth(
f"Malformed API Key passed in. Ensure Key has `Bearer ` prefix. Passed in: {passed_in_key}" f"Malformed API Key passed in. Ensure Key has `Bearer ` prefix. Passed in: {passed_in_key}"
) )
### CHECK IF ADMIN ###
# note: never string compare api keys, this is vulenerable to a time attack. Use secrets.compare_digest instead # note: never string compare api keys, this is vulenerable to a time attack. Use secrets.compare_digest instead
is_master_key_valid = secrets.compare_digest(api_key, master_key) is_master_key_valid = secrets.compare_digest(api_key, master_key)
if is_master_key_valid: if is_master_key_valid:
@ -454,6 +455,12 @@ async def user_api_key_auth(
if _user is None: if _user is None:
continue continue
assert isinstance(_user, dict) assert isinstance(_user, dict)
# check if user is admin #
if (
_user.get("user_role", None) is not None
and _user.get("user_role") == "proxy_admin"
):
return UserAPIKeyAuth(api_key=master_key)
# Token exists, not expired now check if its in budget for the user # Token exists, not expired now check if its in budget for the user
user_max_budget = _user.get("max_budget", None) user_max_budget = _user.get("max_budget", None)
user_current_spend = _user.get("spend", None) user_current_spend = _user.get("spend", None)
@ -597,10 +604,13 @@ async def user_api_key_auth(
# check if user can access this route # check if user can access this route
query_params = request.query_params query_params = request.query_params
user_id = query_params.get("user_id") user_id = query_params.get("user_id")
verbose_proxy_logger.debug(
f"user_id: {user_id} & valid_token.user_id: {valid_token.user_id}"
)
if user_id != valid_token.user_id: if user_id != valid_token.user_id:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="user not allowed to access this key's info", detail="key not allowed to access this user's info",
) )
elif route == "/user/update": elif route == "/user/update":
raise HTTPException( raise HTTPException(
@ -1846,6 +1856,9 @@ async def startup_event():
if prisma_client is not None and master_key is not None: if prisma_client is not None and master_key is not None:
# add master key to db # add master key to db
user_id = "default_user_id"
if os.getenv("PROXY_ADMIN_ID", None) is not None:
user_id = os.getenv("PROXY_ADMIN_ID")
asyncio.create_task( asyncio.create_task(
generate_key_helper_fn( generate_key_helper_fn(
duration=None, duration=None,
@ -1854,7 +1867,8 @@ async def startup_event():
config={}, config={},
spend=0, spend=0,
token=master_key, token=master_key,
user_id="default_user_id", user_id=user_id,
user_role="proxy_admin",
) )
) )
@ -3380,12 +3394,13 @@ async def auth_callback(request: Request):
result = await microsoft_sso.verify_and_process(request) result = await microsoft_sso.verify_and_process(request)
# User is Authe'd in - generate key for the UI to access Proxy # User is Authe'd in - generate key for the UI to access Proxy
user_id = getattr(result, "email", None) user_email = getattr(result, "email", None)
user_id = getattr(result, "id", None)
if user_id is None: if user_id is None:
user_id = getattr(result, "first_name", "") + getattr(result, "last_name", "") user_id = getattr(result, "first_name", "") + getattr(result, "last_name", "")
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": user_id, "team_id": "litellm-dashboard", "user_email": user_email} # type: ignore
) )
key = response["token"] # type: ignore key = response["token"] # type: ignore
@ -3393,10 +3408,25 @@ async def auth_callback(request: Request):
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(
{"user_id": user_id, "key": key}, "secret", algorithm="HS256" {
"user_id": user_id,
"key": key,
"user_email": user_email,
"user_role": user_role,
},
"secret",
algorithm="HS256",
) )
litellm_dashboard_ui += "?userID=" + user_id + "&token=" + jwt_token litellm_dashboard_ui += "?userID=" + user_id + "&token=" + jwt_token
@ -3409,10 +3439,18 @@ async def auth_callback(request: Request):
"/user/info", tags=["user management"], dependencies=[Depends(user_api_key_auth)] "/user/info", tags=["user management"], dependencies=[Depends(user_api_key_auth)]
) )
async def user_info( async def user_info(
user_id: str = fastapi.Query(..., description="User ID in the request parameters") user_id: Optional[str] = fastapi.Query(
default=None, description="User ID in the request parameters"
)
): ):
""" """
Use this to get user information. (user row + all user key info) Use this to get user information. (user row + all user key info)
Example request
```
curl -X GET 'http://localhost:8000/user/info?user_id=krrish7%40berri.ai' \
--header 'Authorization: Bearer sk-1234'
```
""" """
global prisma_client global prisma_client
try: try:
@ -3421,11 +3459,25 @@ async def user_info(
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"
) )
## GET USER ROW ## ## GET USER ROW ##
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)
else:
user_info = None
## GET ALL KEYS ## ## GET ALL KEYS ##
keys = await prisma_client.get_data( keys = await prisma_client.get_data(
user_id=user_id, table_name="key", query_type="find_all" user_id=user_id,
table_name="key",
query_type="find_all",
expires=datetime.now(),
) )
if user_info is None:
## make sure we still return a total spend ##
spend = 0
for k in keys:
spend += getattr(k, "spend", 0)
user_info = {"spend": spend}
## REMOVE HASHED TOKEN INFO before returning ## ## REMOVE HASHED TOKEN INFO before returning ##
for key in keys: for key in keys:
try: try:

View file

@ -559,6 +559,17 @@ class PrismaClient:
# The asterisk before `user_id_list` unpacks the list into separate arguments # The asterisk before `user_id_list` unpacks the list into separate arguments
response = await self.db.query_raw(sql_query) response = await self.db.query_raw(sql_query)
elif query_type == "find_all": elif query_type == "find_all":
if expires is not None:
response = await self.db.litellm_usertable.find_many( # type: ignore
order={"spend": "desc"},
where={ # type:ignore
"OR": [
{"expires": None}, # type:ignore
{"expires": {"gt": expires}}, # type:ignore
],
},
)
else:
response = await self.db.litellm_usertable.find_many( # type: ignore response = await self.db.litellm_usertable.find_many( # type: ignore
order={"spend": "desc"}, order={"spend": "desc"},
) )

View file

@ -1,40 +1,50 @@
"use client"; "use client";
import Link from 'next/link'; import Link from "next/link";
import Image from 'next/image' import Image from "next/image";
import React, { useState } from 'react'; import React, { useState } from "react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { Button, Text, Metric,Title, TextInput, Grid, Col, Card } from "@tremor/react"; import {
Button,
Text,
Metric,
Title,
TextInput,
Grid,
Col,
Card,
} from "@tremor/react";
// Define the props type // Define the props type
interface NavbarProps { interface NavbarProps {
userID: string | null; userID: string | null;
userRole: string | null; userRole: string | null;
userEmail: string | null;
} }
const Navbar: React.FC<NavbarProps> = ({ userID, userRole }) => { const Navbar: React.FC<NavbarProps> = ({ userID, userRole, userEmail }) => {
console.log("User ID:", userID); console.log("User ID:", userID);
console.log("userEmail:", userEmail);
return ( return (
<nav className="left-0 right-0 top-0 flex justify-between items-center h-12 mb-4"> <nav className="left-0 right-0 top-0 flex justify-between items-center h-12 mb-4">
<div className="text-left mx-4 my-2 absolute top-0 left-0"> <div className="text-left mx-4 my-2 absolute top-0 left-0">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<Link href="/"> <Link href="/">
<button className="text-gray-800 text-2xl px-4 py-1 rounded text-center">🚅 LiteLLM</button> <button className="text-gray-800 text-2xl px-4 py-1 rounded text-center">
🚅 LiteLLM
</button>
</Link> </Link>
</div> </div>
</div> </div>
<div className="text-right mx-4 my-2 absolute top-0 right-0"> <div className="text-right mx-4 my-2 absolute top-0 right-0">
<Button variant='secondary'> <Button variant="secondary">
{userID} {userEmail}
<p> <p>Role: {userRole}</p>
Role: {userRole} <p>ID: {userID}</p>
</p>
</Button> </Button>
</div> </div>
</nav> </nav>
) );
} };
export default Navbar; export default Navbar;

View file

@ -1,15 +1,15 @@
/** /**
* Helper file for calls being made to proxy * Helper file for calls being made to proxy
*/ */
import { message } from 'antd'; import { message } from "antd";
const proxyBaseUrl = null; const isLocal = process.env.NODE_ENV === "development";
// const proxyBaseUrl = "http://localhost:4000" // http://localhost:4000 const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
export const keyCreateCall = async ( export const keyCreateCall = async (
accessToken: string, accessToken: string,
userID: string, userID: string,
formValues: Record<string, any>, // Assuming formValues is an object formValues: Record<string, any> // Assuming formValues is an object
) => { ) => {
try { try {
console.log("Form Values in keyCreateCall:", formValues); // Log the form values before making the API call console.log("Form Values in keyCreateCall:", formValues); // Log the form values before making the API call
@ -18,7 +18,7 @@ export const keyCreateCall = async (
if (formValues.description) { if (formValues.description) {
// add to formValues.metadata // add to formValues.metadata
if (!formValues.metadata) { if (!formValues.metadata) {
formValues.metadata = {} formValues.metadata = {};
} }
// value needs to be in "", valid JSON // value needs to be in "", valid JSON
formValues.metadata.description = formValues.description; formValues.metadata.description = formValues.description;
@ -69,14 +69,10 @@ export const keyCreateCall = async (
} }
}; };
export const keyDeleteCall = async (accessToken: String, user_key: String) => {
export const keyDeleteCall = async (
accessToken: String,
user_key: String
) => {
try { try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/key/delete` : `/key/delete`; const url = proxyBaseUrl ? `${proxyBaseUrl}/key/delete` : `/key/delete`;
console.log("in keyDeleteCall:", user_key) console.log("in keyDeleteCall:", user_key);
const response = await fetch(url, { const response = await fetch(url, {
method: "POST", method: "POST",
@ -108,21 +104,22 @@ export const keyDeleteCall = async (
export const userInfoCall = async ( export const userInfoCall = async (
accessToken: String, accessToken: String,
userID: String userID: String,
userRole: String
) => { ) => {
try { try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/user/info` : `/user/info`; let url = proxyBaseUrl ? `${proxyBaseUrl}/user/info` : `/user/info`;
console.log("in userInfoCall:", url) if (userRole == "App Owner") {
const response = await fetch( url = `${url}/?user_id=${userID}`;
`${url}/?user_id=${userID}`, }
{ message.info("Requesting user data");
const response = await fetch(url, {
method: "GET", method: "GET",
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
} });
);
if (!response.ok) { if (!response.ok) {
const errorData = await response.text(); const errorData = await response.text();
@ -131,7 +128,7 @@ export const userInfoCall = async (
} }
const data = await response.json(); const data = await response.json();
console.log(data); message.info("Received user data");
return data; return data;
// Handle success - you might want to update some state or UI based on the created key // Handle success - you might want to update some state or UI based on the created key
} catch (error) { } catch (error) {
@ -140,24 +137,17 @@ export const userInfoCall = async (
} }
}; };
export const keySpendLogsCall = async (accessToken: String, token: String) => {
export const keySpendLogsCall = async (
accessToken: String,
token: String
) => {
try { try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/spend/logs` : `/spend/logs`; const url = proxyBaseUrl ? `${proxyBaseUrl}/spend/logs` : `/spend/logs`;
console.log("in keySpendLogsCall:", url) console.log("in keySpendLogsCall:", url);
const response = await fetch( const response = await fetch(`${url}/?api_key=${token}`, {
`${url}/?api_key=${token}`,
{
method: "GET", method: "GET",
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
} });
);
if (!response.ok) { if (!response.ok) {
const errorData = await response.text(); const errorData = await response.text();
message.error(errorData); message.error(errorData);
@ -171,4 +161,4 @@ export const keySpendLogsCall = async (
console.error("Failed to create key:", error); console.error("Failed to create key:", error);
throw error; throw error;
} }
} };

View file

@ -6,21 +6,25 @@ import CreateKey from "./create_key_button";
import ViewKeyTable from "./view_key_table"; import ViewKeyTable from "./view_key_table";
import ViewUserSpend from "./view_user_spend"; import ViewUserSpend from "./view_user_spend";
import EnterProxyUrl from "./enter_proxy_url"; import EnterProxyUrl from "./enter_proxy_url";
import { message } from "antd";
import Navbar from "./navbar"; import Navbar from "./navbar";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { jwtDecode } from "jwt-decode"; import { jwtDecode } from "jwt-decode";
const proxyBaseUrl = null; const isLocal = process.env.NODE_ENV === "development";
// const proxyBaseUrl = "http://localhost:4000" // http://localhost:4000 console.log("isLocal:", isLocal);
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
type UserSpendData = { type UserSpendData = {
spend: number; spend: number;
max_budget?: number | null; max_budget?: number | null;
} };
const UserDashboard = () => { const UserDashboard = () => {
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>(null); const [userSpendData, setUserSpendData] = useState<UserSpendData | null>(
null
);
// Assuming useSearchParams() hook exists and works in your setup // Assuming useSearchParams() hook exists and works in your setup
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@ -30,19 +34,19 @@ const UserDashboard = () => {
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 [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) {
return "Undefined Role"; return "Undefined Role";
} }
console.log(`Received user role: ${userRole}`);
switch (userRole.toLowerCase()) { switch (userRole.toLowerCase()) {
case "app_owner": case "app_owner":
return "App Owner"; return "App Owner";
case "demo_app_owner": case "demo_app_owner":
return "AppOwner"; return "App Owner";
case "admin": case "app_admin":
return "Admin"; return "Admin";
case "app_user": case "app_user":
return "App User"; return "App User";
@ -53,7 +57,7 @@ const UserDashboard = () => {
// Moved useEffect inside the component and used a condition to run fetch only if the params are available // Moved useEffect inside the component and used a condition to run fetch only if the params are available
useEffect(() => { useEffect(() => {
if (token){ if (token) {
const decoded = jwtDecode(token) as { [key: string]: any }; const decoded = jwtDecode(token) as { [key: string]: any };
if (decoded) { if (decoded) {
// cast decoded to dictionary // cast decoded to dictionary
@ -71,17 +75,19 @@ const UserDashboard = () => {
} else { } else {
console.log("User role not defined"); console.log("User role not defined");
} }
if (decoded.user_email) {
setUserEmail(decoded.user_email);
} else {
console.log(`User Email is not set ${decoded}`);
} }
} }
if (userID && accessToken && !data) { }
if (userID && accessToken && userRole && !data) {
const fetchData = async () => { const fetchData = async () => {
try { try {
const response = await userInfoCall( const response = await userInfoCall(accessToken, userID, userRole);
accessToken, setUserSpendData(response["user_info"]);
userID
);
console.log("Response:", response);
setUserSpendData(response["user_info"])
setData(response["keys"]); // Assuming this is the correct path to your data setData(response["keys"]); // Assuming this is the correct path to your data
} catch (error) { } catch (error) {
console.error("There was an error fetching the data", error); console.error("There was an error fetching the data", error);
@ -93,35 +99,28 @@ const UserDashboard = () => {
}, [userID, token, accessToken, data]); }, [userID, token, accessToken, data]);
if (userID == null || token == null) { if (userID == null || token == null) {
// Now you can construct the full URL // Now you can construct the full URL
const url = proxyBaseUrl ? `${proxyBaseUrl}/sso/key/generate` : `/sso/key/generate`; const url = proxyBaseUrl
? `${proxyBaseUrl}/sso/key/generate`
: `/sso/key/generate`;
console.log("Full URL:", url); console.log("Full URL:", url);
window.location.href = url; window.location.href = url;
return null; return null;
} } else if (accessToken == null) {
else if (accessToken == null) {
return null; return null;
} }
if (userRole == null) { if (userRole == null) {
setUserRole("App Owner") setUserRole("App Owner");
} }
return ( return (
<div> <div>
<Navbar <Navbar userID={userID} userRole={userRole} userEmail={userEmail} />
userID={userID}
userRole={userRole}
/>
<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 <ViewUserSpend userID={userID} userSpendData={userSpendData} />
userID={userID}
userSpendData={userSpendData}
/>
<ViewKeyTable <ViewKeyTable
userID={userID} userID={userID}
accessToken={accessToken} accessToken={accessToken}
@ -138,7 +137,6 @@ const UserDashboard = () => {
</Col> </Col>
</Grid> </Grid>
</div> </div>
); );
}; };

View file

@ -1,8 +1,26 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Button as Button2, Modal, Form, Input, InputNumber, Select, message } from "antd"; import {
import { Button, Text, Card, Table, BarChart, Title, Subtitle, BarList, Metric } from "@tremor/react"; Button as Button2,
Modal,
Form,
Input,
InputNumber,
Select,
message,
} from "antd";
import {
Button,
Text,
Card,
Table,
BarChart,
Title,
Subtitle,
BarList,
Metric,
} from "@tremor/react";
import { keySpendLogsCall } from "./networking"; import { keySpendLogsCall } from "./networking";
interface ViewKeySpendReportProps { interface ViewKeySpendReportProps {
@ -17,15 +35,27 @@ type ResponseValueType = {
startTime: string; // Assuming startTime is a string, adjust it if it's of a different type startTime: string; // Assuming startTime is a string, adjust it if it's of a different type
spend: number; // Assuming spend is a number, adjust it if it's of a different type spend: number; // Assuming spend is a number, adjust it if it's of a different type
user: string; // Assuming user is a string, adjust it if it's of a different type user: string; // Assuming user is a string, adjust it if it's of a different type
}; };
const ViewKeySpendReport: React.FC<ViewKeySpendReportProps> = ({ token, accessToken, keySpend, keyBudget, keyName }) => { const ViewKeySpendReport: React.FC<ViewKeySpendReportProps> = ({
token,
accessToken,
keySpend,
keyBudget,
keyName,
}) => {
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [data, setData] = useState<{ day: string; spend: number; }[] | null>(null); const [data, setData] = useState<{ day: string; spend: number }[] | null>(
const [userData, setUserData] = useState<{ name: string; value: number; }[] | null>(null); null
);
const [userData, setUserData] = useState<
{ name: string; value: number }[] | null
>(null);
const showModal = () => { const showModal = () => {
console.log("Show Modal triggered");
setIsModalVisible(true); setIsModalVisible(true);
fetchData();
}; };
const handleOk = () => { const handleOk = () => {
@ -42,22 +72,29 @@ const ViewKeySpendReport: React.FC<ViewKeySpendReportProps> = ({ token, accessTo
if (accessToken == null || token == null) { if (accessToken == null || token == null) {
return; return;
} }
const response = await keySpendLogsCall(accessToken=accessToken, token=token); console.log(`accessToken: ${accessToken}; token: ${token}`);
const response = await keySpendLogsCall(
(accessToken = accessToken),
(token = token)
);
console.log("Response:", response); console.log("Response:", response);
// loop through response // loop through response
// get spend, startTime for each element, place in new array // get spend, startTime for each element, place in new array
const pricePerDay: Record<string, number> = (
const pricePerDay: Record<string, number> = (Object.values(response) as ResponseValueType[]).reduce((acc: Record<string, number>, value) => { Object.values(response) as ResponseValueType[]
).reduce((acc: Record<string, number>, value) => {
const startTime = new Date(value.startTime); const startTime = new Date(value.startTime);
const day = new Intl.DateTimeFormat('en-US', { day: '2-digit', month: 'short' }).format(startTime); const day = new Intl.DateTimeFormat("en-US", {
day: "2-digit",
month: "short",
}).format(startTime);
acc[day] = (acc[day] || 0) + value.spend; acc[day] = (acc[day] || 0) + value.spend;
return acc; return acc;
}, {}); }, {});
// sort pricePerDay by day // sort pricePerDay by day
// Convert object to array of key-value pairs // Convert object to array of key-value pairs
const pricePerDayArray = Object.entries(pricePerDay); const pricePerDayArray = Object.entries(pricePerDay);
@ -72,17 +109,17 @@ const ViewKeySpendReport: React.FC<ViewKeySpendReportProps> = ({ token, accessTo
// Convert the sorted array back to an object // Convert the sorted array back to an object
const sortedPricePerDay = Object.fromEntries(pricePerDayArray); const sortedPricePerDay = Object.fromEntries(pricePerDayArray);
console.log(sortedPricePerDay); console.log(sortedPricePerDay);
const pricePerUser: Record<string, number> = (Object.values(response) as ResponseValueType[]).reduce((acc: Record<string, number>, value) => { const pricePerUser: Record<string, number> = (
Object.values(response) as ResponseValueType[]
).reduce((acc: Record<string, number>, value) => {
const user = value.user; const user = value.user;
acc[user] = (acc[user] || 0) + value.spend; acc[user] = (acc[user] || 0) + value.spend;
return acc; return acc;
}, {}); }, {});
console.log(pricePerDay); console.log(pricePerDay);
console.log(pricePerUser); console.log(pricePerUser);
@ -97,11 +134,15 @@ const ViewKeySpendReport: React.FC<ViewKeySpendReportProps> = ({ token, accessTo
arrayBarChart.push({ day: key, spend: value }); arrayBarChart.push({ day: key, spend: value });
} }
// get 5 most expensive users // get 5 most expensive users
const sortedUsers = Object.entries(pricePerUser).sort((a, b) => b[1] - a[1]); const sortedUsers = Object.entries(pricePerUser).sort(
(a, b) => b[1] - a[1]
);
const top5Users = sortedUsers.slice(0, 5); const top5Users = sortedUsers.slice(0, 5);
const userChart = top5Users.map(([key, value]) => ({ name: key, value: value })); const userChart = top5Users.map(([key, value]) => ({
name: key,
value: value,
}));
setData(arrayBarChart); setData(arrayBarChart);
setUserData(userChart); setUserData(userChart);
@ -112,11 +153,10 @@ const ViewKeySpendReport: React.FC<ViewKeySpendReportProps> = ({ token, accessTo
} }
}; };
useEffect(() => { // useEffect(() => {
// Fetch data only when the token changes // // Fetch data only when the token changes
fetchData(); // fetchData();
}, [token]); // Dependency array containing the 'token' variable // }, [token]); // Dependency array containing the 'token' variable
if (!token) { if (!token) {
return null; return null;
@ -134,7 +174,7 @@ const ViewKeySpendReport: React.FC<ViewKeySpendReportProps> = ({ token, accessTo
onCancel={handleCancel} onCancel={handleCancel}
footer={null} footer={null}
> >
<Title style={{ textAlign: 'left' }}>Key Name: {keyName}</Title> <Title style={{ textAlign: "left" }}>Key Name: {keyName}</Title>
<Metric>Monthly Spend ${keySpend}</Metric> <Metric>Monthly Spend ${keySpend}</Metric>
@ -153,14 +193,9 @@ const ViewKeySpendReport: React.FC<ViewKeySpendReportProps> = ({ token, accessTo
<Title className="mt-6">Top 5 Users Spend (USD)</Title> <Title className="mt-6">Top 5 Users Spend (USD)</Title>
<Card className="mb-6"> <Card className="mb-6">
{userData && ( {userData && (
<BarList <BarList className="mt-6" data={userData} color="teal" />
className="mt-6"
data={userData}
color="teal"
/>
)} )}
</Card> </Card>
</Modal> </Modal>
</div> </div>
); );

View file

@ -1,5 +1,5 @@
"use client"; "use client";
import React, { useEffect } from "react"; import React, { useEffect, useState } from "react";
import { keyDeleteCall } from "./networking"; import { keyDeleteCall } from "./networking";
import { StatusOnlineIcon, TrashIcon } from "@heroicons/react/outline"; import { StatusOnlineIcon, TrashIcon } from "@heroicons/react/outline";
import { import {
@ -32,6 +32,8 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
data, data,
setData, setData,
}) => { }) => {
const [isButtonClicked, setIsButtonClicked] = useState(false);
const handleDelete = async (token: String) => { const handleDelete = async (token: String) => {
if (data == null) { if (data == null) {
return; return;
@ -116,8 +118,13 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
/> />
</TableCell> </TableCell>
<TableCell> <TableCell>
<ViewKeySpendReport token={item.token} accessToken={accessToken} keySpend={item.spend} keyBudget={item.max_budget} keyName={item.key_name} /> <ViewKeySpendReport
token={item.token}
accessToken={accessToken}
keySpend={item.spend}
keyBudget={item.max_budget}
keyName={item.key_name}
/>
</TableCell> </TableCell>
</TableRow> </TableRow>
); );