build(ui/litellm-dashboard): ui cleanup

This commit is contained in:
Krrish Dholakia 2024-01-27 17:33:09 -08:00
parent c15f3559ca
commit 00a25ebfb8
8 changed files with 1135 additions and 129 deletions

View file

@ -141,11 +141,33 @@ class GenerateRequestBase(LiteLLMBase):
class GenerateKeyRequest(GenerateRequestBase): class GenerateKeyRequest(GenerateRequestBase):
key_alias: Optional[str] = None key_alias: Optional[str] = None
duration: Optional[str] = "1h" duration: Optional[str] = None
aliases: Optional[dict] = {} aliases: Optional[dict] = {}
config: Optional[dict] = {} config: Optional[dict] = {}
class GenerateKeyResponse(GenerateKeyRequest):
key: str
key_name: Optional[str] = None
expires: Optional[datetime]
user_id: str
@root_validator(pre=True)
def set_model_info(cls, values):
if values.get("token") is not None:
values.update({"key": values.get("token")})
dict_fields = ["metadata", "aliases", "config"]
for field in dict_fields:
value = values.get(field)
if value is not None and isinstance(value, str):
try:
values[field] = json.loads(value)
except json.JSONDecodeError:
raise ValueError(f"Field {field} should be a valid dictionary")
return values
class UpdateKeyRequest(GenerateKeyRequest): class UpdateKeyRequest(GenerateKeyRequest):
# Note: the defaults of all Params here MUST BE NONE # Note: the defaults of all Params here MUST BE NONE
# else they will get overwritten # else they will get overwritten
@ -174,12 +196,6 @@ class UserAPIKeyAuth(LiteLLMBase): # the expected response object for user api
rpm_limit: Optional[int] = None rpm_limit: Optional[int] = None
class GenerateKeyResponse(LiteLLMBase):
key: str
expires: Optional[datetime]
user_id: str
class DeleteKeyRequest(LiteLLMBase): class DeleteKeyRequest(LiteLLMBase):
keys: List keys: List

View file

@ -1386,12 +1386,7 @@ async def generate_key_helper_fn(
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
return { return key_data
"token": token,
"expires": expires,
"user_id": user_id,
"max_budget": max_budget,
}
async def delete_verification_token(tokens: List): async def delete_verification_token(tokens: List):
@ -2399,12 +2394,9 @@ async def generate_key_fn(
data_json["key_budget_duration"] = data_json.pop("budget_duration", None) data_json["key_budget_duration"] = data_json.pop("budget_duration", None)
response = await generate_key_helper_fn(**data_json) response = await generate_key_helper_fn(**data_json)
return GenerateKeyResponse( return GenerateKeyResponse(**response)
key=response["token"],
expires=response["expires"],
user_id=response["user_id"],
)
except Exception as e: except Exception as e:
traceback.print_exc()
if isinstance(e, HTTPException): if isinstance(e, HTTPException):
raise ProxyException( raise ProxyException(
message=getattr(e, "detail", f"Authentication Error({str(e)})"), message=getattr(e, "detail", f"Authentication Error({str(e)})"),

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,7 @@
"@heroicons/react": "^1.0.6", "@heroicons/react": "^1.0.6",
"@remixicon/react": "^4.1.1", "@remixicon/react": "^4.1.1",
"@tremor/react": "^3.13.3", "@tremor/react": "^3.13.3",
"antd": "^5.13.2",
"next": "14.1.0", "next": "14.1.0",
"react": "^18", "react": "^18",
"react-dom": "^18" "react-dom": "^18"

View file

@ -1,39 +1,91 @@
"use client"; "use client";
import React, { use } from "react"; import React, { useState, useEffect, useRef } from "react";
import { Button, TextInput } from "@tremor/react"; import { Button, TextInput, Grid, Col } from "@tremor/react";
import { message } from "antd";
import { Card, Metric, Text } from "@tremor/react"; import { Card, Metric, Text } from "@tremor/react";
import { createKeyCall } from "./networking"; import { keyCreateCall } from "./networking";
// Define the props type // Define the props type
interface CreateKeyProps { interface CreateKeyProps {
userID: string; userID: string;
accessToken: string; accessToken: string;
proxyBaseUrl: string; proxyBaseUrl: string;
data: any[] | null;
setData: React.Dispatch<React.SetStateAction<any[] | null>>;
} }
import { Modal, Button as Button2 } from "antd";
const CreateKey: React.FC<CreateKeyProps> = ({ const CreateKey: React.FC<CreateKeyProps> = ({
userID, userID,
accessToken, accessToken,
proxyBaseUrl, proxyBaseUrl,
data,
setData,
}) => { }) => {
const handleClick = () => { const [isModalVisible, setIsModalVisible] = useState(false);
console.log("Hello World"); const [apiKey, setApiKey] = useState(null);
const handleOk = () => {
// Handle the OK action
console.log("OK Clicked");
setIsModalVisible(false);
};
const handleCancel = () => {
// Handle the cancel action or closing the modal
console.log("Modal closed");
setIsModalVisible(false);
setApiKey(null);
};
const handleCreate = async () => {
if (data == null) {
return;
}
try {
message.info("Making API Call");
setIsModalVisible(true);
const response = await keyCreateCall(proxyBaseUrl, accessToken, userID);
// Successfully completed the deletion. Update the state to trigger a rerender.
setData([...data, response]);
setApiKey(response["key"]);
message.success("API Key Created");
} catch (error) {
console.error("Error deleting the key:", error);
// Handle any error situations, such as displaying an error message to the user.
}
}; };
return ( return (
<Button <div>
className="mx-auto" <Button className="mx-auto" onClick={handleCreate}>
onClick={() => + Create New Key
createKeyCall( </Button>
(proxyBaseUrl = proxyBaseUrl), <Modal
(accessToken = accessToken), title="Save your key"
(userID = userID) open={isModalVisible}
) onOk={handleOk}
} onCancel={handleCancel}
> >
+ Create New Key <Grid numItems={1} className="gap-2 w-full">
</Button> <Col numColSpan={1}>
<p>
Please save this secret key somewhere safe and accessible. For
security reasons, <b>you won't be able to view it again</b>{" "}
through your LiteLLM account. If you lose this secret key, you'll
need to generate a new one.
</p>
</Col>
<Col numColSpan={1}>
{apiKey != null ? (
<Text>API Key: {apiKey}</Text>
) : (
<Text>Key being created, this might take 30s</Text>
)}
</Col>
</Grid>
</Modal>
</div>
); );
}; };

View file

@ -2,7 +2,7 @@
* Helper file for calls being made to proxy * Helper file for calls being made to proxy
*/ */
export const createKeyCall = async ( export const keyCreateCall = async (
proxyBaseUrl: String, proxyBaseUrl: String,
accessToken: String, accessToken: String,
userID: String userID: String
@ -27,9 +27,42 @@ export const createKeyCall = async (
const data = await response.json(); const data = await response.json();
console.log(data); console.log(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) {
console.error("Failed to create key:", error); console.error("Failed to create key:", error);
throw error;
}
};
export const keyDeleteCall = async (
proxyBaseUrl: String,
accessToken: String,
user_key: String
) => {
try {
const response = await fetch(`${proxyBaseUrl}/key/delete`, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
keys: [user_key],
}),
});
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = await response.json();
console.log(data);
return data;
// Handle success - you might want to update some state or UI based on the created key
} catch (error) {
console.error("Failed to create key:", error);
throw error;
} }
}; };

View file

@ -1,32 +1,41 @@
"use client"; "use client";
import React from "react"; import React, { useState, useEffect } from "react";
import { userInfoCall } from "./networking";
import { Grid, Col, Card, Text } from "@tremor/react"; import { Grid, Col, Card, Text } from "@tremor/react";
import CreateKey from "./create_key_button"; import CreateKey from "./create_key_button";
import ViewKeyTable from "./view_key_table"; import ViewKeyTable from "./view_key_table";
import EnterProxyUrl from "./enter_proxy_url"; import EnterProxyUrl from "./enter_proxy_url";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
const UserDashboard = () => {
const [data, setData] = useState<null | any[]>(null); // Keep the initialization of state here
export default function UserDashboard() { // Assuming useSearchParams() hook exists and works in your setup
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const userID = searchParams.get("userID"); const userID = searchParams.get("userID");
const accessToken = searchParams.get("accessToken"); const accessToken = searchParams.get("accessToken");
const proxyBaseUrl = searchParams.get("proxyBaseUrl"); const proxyBaseUrl = searchParams.get("proxyBaseUrl");
const handleProxyUrlChange = (url: string) => { // Moved useEffect inside the component and used a condition to run fetch only if the params are available
// Do something with the entered proxy URL, e.g., save it in the state useEffect(() => {
console.log('Entered Proxy URL:', url); if (userID && accessToken && proxyBaseUrl && !data) {
}; const fetchData = async () => {
try {
const response = await userInfoCall(
proxyBaseUrl,
accessToken,
userID
);
setData(response["keys"]); // Assuming this is the correct path to your data
} 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, accessToken, proxyBaseUrl, data]);
if (proxyBaseUrl == null) { if (!userID || !accessToken || !proxyBaseUrl || !data) {
return (
<div>
<EnterProxyUrl onUrlChange={handleProxyUrlChange} />
</div>
);
}
if (userID == null || accessToken == null || proxyBaseUrl == null) {
return ( return (
<Card <Card
className="max-w-xs mx-auto" className="max-w-xs mx-auto"
@ -39,19 +48,25 @@ export default function UserDashboard() {
} }
return ( return (
<Grid numItems={1} className="gap-0 p-10 h-[75vh]"> <Grid numItems={1} className="gap-0 p-10 h-[75vh] w-full">
<Col numColSpan={1}> <Col numColSpan={1}>
<ViewKeyTable <ViewKeyTable
userID={userID} userID={userID}
accessToken={accessToken} accessToken={accessToken}
proxyBaseUrl={proxyBaseUrl} proxyBaseUrl={proxyBaseUrl}
data={data}
setData={setData}
/> />
<CreateKey <CreateKey
userID={userID} userID={userID}
accessToken={accessToken} accessToken={accessToken}
proxyBaseUrl={proxyBaseUrl} proxyBaseUrl={proxyBaseUrl}
data={data}
setData={setData}
/> />
</Col> </Col>
</Grid> </Grid>
); );
} };
export default UserDashboard;

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import React, { useEffect } from "react";
import { userInfoCall } from "./networking"; import { keyDeleteCall } from "./networking";
import { StatusOnlineIcon } from "@heroicons/react/outline"; import { StatusOnlineIcon, TrashIcon } from "@heroicons/react/outline";
import { import {
Badge, Badge,
Card, Card,
@ -13,6 +13,7 @@ import {
TableRow, TableRow,
Text, Text,
Title, Title,
Icon,
} from "@tremor/react"; } from "@tremor/react";
// Define the props type // Define the props type
@ -20,96 +21,75 @@ interface ViewKeyTableProps {
userID: string; userID: string;
accessToken: string; accessToken: string;
proxyBaseUrl: string; proxyBaseUrl: string;
data: any[] | null;
setData: React.Dispatch<React.SetStateAction<any[] | null>>;
} }
const data = [
{
key_alias: "my test key",
key_name: "sk-...hd74",
spend: 23.0,
expires: "active",
token: "23902dwojd90",
},
{
key_alias: "my test key",
key_name: "sk-...hd74",
spend: 23.0,
expires: "active",
token: "23902dwojd90",
},
{
key_alias: "my test key",
key_name: "sk-...hd74",
spend: 23.0,
expires: "active",
token: "23902dwojd90",
},
{
key_alias: "my test key",
key_name: "sk-...hd74",
spend: 23.0,
expires: "active",
token: "23902dwojd90",
},
];
const ViewKeyTable: React.FC<ViewKeyTableProps> = ({ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
userID, userID,
accessToken, accessToken,
proxyBaseUrl, proxyBaseUrl,
data,
setData,
}) => { }) => {
const [data, setData] = useState(null); // State to store the data from the API const handleDelete = async (token: String) => {
if (data == null) {
useEffect(() => { return;
const fetchData = async () => { }
try { try {
const response = await userInfoCall( await keyDeleteCall(proxyBaseUrl, accessToken, token);
(proxyBaseUrl = proxyBaseUrl), // Successfully completed the deletion. Update the state to trigger a rerender.
(accessToken = accessToken), const filteredData = data.filter((item) => item.token !== token);
(userID = userID) setData(filteredData);
); } catch (error) {
setData(response["keys"]); // Update state with the fetched data console.error("Error deleting the key:", error);
} catch (error) { // Handle any error situations, such as displaying an error message to the user.
console.error("There was an error fetching the data", error); }
// Optionally, update your UI to reflect the error state };
}
};
fetchData(); // Call the async function to fetch data
}, []); // Empty dependency array
if (data == null) { if (data == null) {
return; return;
} }
console.log("RERENDER TRIGGERED");
return ( return (
<Card className="flex-auto overflow-y-auto max-h-[50vh] mb-4"> <Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh] mb-4">
<Title>API Keys</Title> <Title>API Keys</Title>
<Table className="mt-5"> <Table className="mt-5">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableHeaderCell>Alias</TableHeaderCell>
<TableHeaderCell>Secret Key</TableHeaderCell> <TableHeaderCell>Secret Key</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell> <TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell> <TableHeaderCell>Expires</TableHeaderCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{data.map((item) => ( {data.map((item) => {
<TableRow key={item.token}> console.log(item);
<TableCell>{item.key_alias}</TableCell> return (
<TableCell> <TableRow key={item.token}>
<Text>{item.key_name}</Text> <TableCell>
</TableCell> <Text>{item.key_name}</Text>
<TableCell> </TableCell>
<Text>{item.spend}</Text> <TableCell>
</TableCell> <Text>{item.spend}</Text>
<TableCell> </TableCell>
<Badge color="emerald" icon={StatusOnlineIcon}> <TableCell>
{item.expires} {item.expires != null ? (
</Badge> <Text>{item.expires}</Text>
</TableCell> ) : (
</TableRow> <Text>Never expires</Text>
))} )}
</TableCell>
<TableCell>
<Icon
onClick={() => handleDelete(item.token)}
icon={TrashIcon}
size="xs"
/>
</TableCell>
</TableRow>
);
})}
</TableBody> </TableBody>
</Table> </Table>
</Card> </Card>