Merge branch 'main' into litellm_invite_link_flow_2

This commit is contained in:
Ishaan Jaff 2024-05-31 08:14:52 -07:00 committed by GitHub
commit f9862be049
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
100 changed files with 5297 additions and 883 deletions

View file

@ -18,7 +18,7 @@ import Usage from "../components/usage";
import { jwtDecode } from "jwt-decode";
import { Typography } from "antd";
export function formatUserRole(userRole: string) {
function formatUserRole(userRole: string) {
if (!userRole) {
return "Undefined Role";
}

View file

@ -58,6 +58,7 @@ import {
User,
setCallbacksCall,
invitationCreateCall,
getPossibleUserRoles,
} from "./networking";
const AdminPanel: React.FC<AdminPanelProps> = ({
@ -83,6 +84,9 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
useState(false);
const router = useRouter();
const [baseUrl, setBaseUrl] = useState("");
const [isInstructionsModalVisible, setIsInstructionsModalVisible] = useState(false);
const [possibleUIRoles, setPossibleUIRoles] = useState<null | Record<string, Record<string, string>>>(null);
let nonSssoUrl;
try {
@ -163,6 +167,9 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
console.log(`proxy admins: ${proxyAdmins}`);
console.log(`combinedList: ${combinedList}`);
setAdmins(combinedList);
const availableUserRoles = await getPossibleUserRoles(accessToken);
setPossibleUIRoles(availableUserRoles);
}
};
@ -435,7 +442,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
? member["user_id"]
: null}
</TableCell>
<TableCell>{member["user_role"]}</TableCell>
<TableCell> {possibleUIRoles?.[member?.user_role]?.ui_label || "-"}</TableCell>
<TableCell>
<Icon
icon={PencilAltIcon}

View file

@ -149,6 +149,12 @@ const ChatUI: React.FC<ChatUIProps> = ({
});
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
handleSendMessage();
}
};
const handleSendMessage = async () => {
if (inputMessage.trim() === "") return;
@ -260,6 +266,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
type="text"
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyDown={handleKeyDown} // Add this line
placeholder="Type your message..."
/>
<Button

View file

@ -0,0 +1,138 @@
import { useEffect, useState } from 'react';
import {
Dialog,
DialogPanel,
TextInput,
Button,
Select,
SelectItem,
Text,
Title,
Subtitle,
} from '@tremor/react';
import {
Button as Button2,
Modal,
Form,
Input,
Select as Select2,
InputNumber,
message,
} from "antd";
interface EditUserModalProps {
visible: boolean;
possibleUIRoles: null | Record<string, Record<string, string>>;
onCancel: () => void;
user: any;
onSubmit: (data: any) => void;
}
const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles, onCancel, user, onSubmit }) => {
const [editedUser, setEditedUser] = useState(user);
const [form] = Form.useForm();
useEffect(() => {
form.resetFields();
}, [user]);
const handleCancel = async () => {
form.resetFields();
onCancel();
};
const handleEditSubmit = async (formValues: Record<string, any>) => {
// Call API to update team with teamId and values
onSubmit(formValues);
form.resetFields();
onCancel();
};
if (!user) {
return null;
}
return (
<Modal
visible={visible}
onCancel={handleCancel}
footer={null}
title={"Edit User " + user.user_id}
width={1000}
>
<Form
form={form}
onFinish={handleEditSubmit}
initialValues={user} // Pass initial values here
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
>
<>
<Form.Item
className="mt-8"
label="User Email"
tooltip="Email of the User"
name="user_email">
<TextInput />
</Form.Item>
<Form.Item
label="user_id"
name="user_id"
hidden={true}
>
<TextInput />
</Form.Item>
<Form.Item
label="User Role"
name="user_role"
>
<Select2>
{possibleUIRoles &&
Object.entries(possibleUIRoles).map(([role, { ui_label, description }]) => (
<SelectItem key={role} value={role} title={ui_label}>
<div className='flex'>
{ui_label} <p className="ml-2" style={{ color: "gray", fontSize: "12px" }}>{description}</p>
</div>
</SelectItem>
))}
</Select2>
</Form.Item>
<Form.Item
label="Spend (USD)"
name="spend"
tooltip="(float) - Spend of all LLM calls completed by this user"
>
<InputNumber min={0} step={1} />
</Form.Item>
<Form.Item
label="User Budget (USD)"
name="max_budget"
tooltip="(float) - Maximum budget of this user"
>
<InputNumber min={0} step={1} />
</Form.Item>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 htmlType="submit">Save</Button2>
</div>
</>
</Form>
</Modal>
);
};
export default EditUserModal;

View file

@ -79,7 +79,7 @@ const Sidebar: React.FC<SidebarProps> = ({
{userRole == "Admin" ? (
<Menu.Item key="5" onClick={() => setPage("users")}>
<Text>Users</Text>
<Text>Internal Users</Text>
</Menu.Item>
) : null}
@ -91,7 +91,7 @@ const Sidebar: React.FC<SidebarProps> = ({
{userRole == "Admin" ? (
<Menu.Item key="9" onClick={() => setPage("budgets")}>
<Text>Rate Limits</Text>
<Text>Budgets</Text>
</Menu.Item>
) : null}

View file

@ -49,6 +49,8 @@ import {
getCallbacksCall,
setCallbacksCall,
modelSettingsCall,
adminGlobalActivityExceptions,
adminGlobalActivityExceptionsPerDeployment,
} from "./networking";
import { BarChart, AreaChart } from "@tremor/react";
import {
@ -109,6 +111,13 @@ interface RetryPolicyObject {
[key: string]: { [retryPolicyKey: string]: number } | undefined;
}
interface GlobalExceptionActivityData {
sum_num_rate_limit_exceptions: number;
daily_data: { date: string; num_rate_limit_exceptions: number; }[];
}
//["OpenAI", "Azure OpenAI", "Anthropic", "Gemini (Google AI Studio)", "Amazon Bedrock", "OpenAI-Compatible Endpoints (Groq, Together AI, Mistral AI, etc.)"]
interface ProviderFields {
@ -301,6 +310,9 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
useState<RetryPolicyObject | null>(null);
const [defaultRetry, setDefaultRetry] = useState<number>(0);
const [globalExceptionData, setGlobalExceptionData] = useState<GlobalExceptionActivityData>({} as GlobalExceptionActivityData);
const [globalExceptionPerDeployment, setGlobalExceptionPerDeployment] = useState<any[]>([]);
function formatCreatedAt(createdAt: string | null) {
if (createdAt) {
const date = new Date(createdAt);
@ -643,6 +655,29 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
dateValue.to?.toISOString()
);
const dailyExceptions = await adminGlobalActivityExceptions(
accessToken,
dateValue.from?.toISOString().split('T')[0],
dateValue.to?.toISOString().split('T')[0],
_initial_model_group,
);
setGlobalExceptionData(dailyExceptions);
const dailyExceptionsPerDeplyment = await adminGlobalActivityExceptionsPerDeployment(
accessToken,
dateValue.from?.toISOString().split('T')[0],
dateValue.to?.toISOString().split('T')[0],
_initial_model_group,
)
setGlobalExceptionPerDeployment(dailyExceptionsPerDeplyment);
console.log("dailyExceptions:", dailyExceptions);
console.log("dailyExceptionsPerDeplyment:", dailyExceptionsPerDeplyment);
console.log("slowResponses:", slowResponses);
setSlowResponsesData(slowResponses);
@ -905,6 +940,30 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
console.log("slowResponses:", slowResponses);
setSlowResponsesData(slowResponses);
if (modelGroup) {
const dailyExceptions = await adminGlobalActivityExceptions(
accessToken,
startTime?.toISOString().split('T')[0],
endTime?.toISOString().split('T')[0],
modelGroup,
);
setGlobalExceptionData(dailyExceptions);
const dailyExceptionsPerDeplyment = await adminGlobalActivityExceptionsPerDeployment(
accessToken,
startTime?.toISOString().split('T')[0],
endTime?.toISOString().split('T')[0],
modelGroup,
)
setGlobalExceptionPerDeployment(dailyExceptionsPerDeplyment);
}
} catch (error) {
console.error("Failed to fetch model metrics", error);
}
@ -1475,7 +1534,8 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
)}
{selectedProvider != Providers.Bedrock &&
selectedProvider != Providers.Vertex_AI &&
dynamicProviderForm === undefined && (
(dynamicProviderForm === undefined ||
dynamicProviderForm.fields.length == 0) && (
<Form.Item
rules={[{ required: true, message: "Required" }]}
label="API Key"
@ -1777,18 +1837,110 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
</Card>
</Col>
</Grid>
<Card className="mt-4">
<Title>Exceptions per Model</Title>
<BarChart
className="h-72"
data={modelExceptions}
index="model"
categories={allExceptions}
stack={true}
colors={["indigo-300", "rose-200", "#ffcc33"]}
yAxisWidth={30}
/>
</Card>
<Grid numItems={1} className="gap-2 w-full mt-2">
<Card>
<Title>All Up Rate Limit Errors (429) for {selectedModelGroup}</Title>
<Grid numItems={1}>
<Col>
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>Num Rate Limit Errors { (globalExceptionData.sum_num_rate_limit_exceptions)}</Subtitle>
<BarChart
className="h-40"
data={globalExceptionData.daily_data}
index="date"
colors={['rose']}
categories={['num_rate_limit_exceptions']}
onValueChange={(v) => console.log(v)}
/>
</Col>
<Col>
{/* <BarChart
className="h-40"
data={modelExceptions}
index="model"
categories={allExceptions}
stack={true}
yAxisWidth={30}
/> */}
</Col>
</Grid>
</Card>
{
premiumUser ? (
<>
{globalExceptionPerDeployment.map((globalActivity, index) => (
<Card key={index}>
<Title>{globalActivity.api_base ? globalActivity.api_base : "Unknown API Base"}</Title>
<Grid numItems={1}>
<Col>
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>Num Rate Limit Errors (429) {(globalActivity.sum_num_rate_limit_exceptions)}</Subtitle>
<BarChart
className="h-40"
data={globalActivity.daily_data}
index="date"
colors={['rose']}
categories={['num_rate_limit_exceptions']}
onValueChange={(v) => console.log(v)}
/>
</Col>
</Grid>
</Card>
))}
</>
) :
<>
{globalExceptionPerDeployment && globalExceptionPerDeployment.length > 0 &&
globalExceptionPerDeployment.slice(0, 1).map((globalActivity, index) => (
<Card key={index}>
<Title> Rate Limit Errors by Deployment</Title>
<p className="mb-2 text-gray-500 italic text-[12px]">Upgrade to see exceptions for all deployments</p>
<Button variant="primary" className="mb-2">
<a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank">
Get Free Trial
</a>
</Button>
<Card>
<Title>{globalActivity.api_base}</Title>
<Grid numItems={1}>
<Col>
<Subtitle
style={{
fontSize: "15px",
fontWeight: "normal",
color: "#535452",
}}
>
Num Rate Limit Errors {(globalActivity.sum_num_rate_limit_exceptions)}
</Subtitle>
<BarChart
className="h-40"
data={globalActivity.daily_data}
index="date"
colors={['rose']}
categories={['num_rate_limit_exceptions']}
onValueChange={(v) => console.log(v)}
/>
</Col>
</Grid>
</Card>
</Card>
))}
</>
}
</Grid>
</TabPanel>
<TabPanel>
<div className="flex items-center">

View file

@ -39,7 +39,9 @@ const Navbar: React.FC<NavbarProps> = ({
// const userColors = require('./ui_colors.json') || {};
const isLocal = process.env.NODE_ENV === "development";
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
const imageUrl = isLocal ? "http://localhost:4000/get_image" : "/get_image";
const logoutUrl = proxyBaseUrl ? `${proxyBaseUrl}` : `/`;
const items: MenuProps["items"] = [
{
@ -52,6 +54,14 @@ const Navbar: React.FC<NavbarProps> = ({
</>
),
},
{
key: "2",
label: (
<Link href={logoutUrl}>
<p>Logout</p>
</Link>
),
}
];
return (

View file

@ -1270,6 +1270,100 @@ export const adminGlobalActivityPerModel = async (
}
};
export const adminGlobalActivityExceptions = async (
accessToken: String,
startTime: String | undefined,
endTime: String | undefined,
modelGroup: String,
) => {
try {
let url = proxyBaseUrl
? `${proxyBaseUrl}/global/activity/exceptions`
: `/global/activity/exceptions`;
if (startTime && endTime) {
url += `?start_date=${startTime}&end_date=${endTime}`;
}
if (modelGroup) {
url += `&model_group=${modelGroup}`;
}
const requestOptions: {
method: string;
headers: {
Authorization: string;
};
} = {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
},
};
const response = await fetch(url, requestOptions);
if (!response.ok) {
const errorData = await response.text();
throw new Error("Network response was not ok");
}
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error("Failed to fetch spend data:", error);
throw error;
}
};
export const adminGlobalActivityExceptionsPerDeployment = async (
accessToken: String,
startTime: String | undefined,
endTime: String | undefined,
modelGroup: String,
) => {
try {
let url = proxyBaseUrl
? `${proxyBaseUrl}/global/activity/exceptions/deployment`
: `/global/activity/exceptions/deployment`;
if (startTime && endTime) {
url += `?start_date=${startTime}&end_date=${endTime}`;
}
if (modelGroup) {
url += `&model_group=${modelGroup}`;
}
const requestOptions: {
method: string;
headers: {
Authorization: string;
};
} = {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
},
};
const response = await fetch(url, requestOptions);
if (!response.ok) {
const errorData = await response.text();
throw new Error("Network response was not ok");
}
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error("Failed to fetch spend data:", error);
throw error;
}
};
export const adminTopModelsCall = async (accessToken: String) => {
try {
let url = proxyBaseUrl
@ -1465,6 +1559,34 @@ export const userGetAllUsersCall = async (
}
};
export const getPossibleUserRoles = async (
accessToken: String,
) => {
try {
const url = proxyBaseUrl
? `${proxyBaseUrl}/user/available_roles`
: `/user/available_roles`;
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.text();
throw new Error("Network response was not ok");
}
const data = await response.json();
console.log("response from user/available_role", data);
return data;
// Handle success - you might want to update some state or UI based on the created key
} catch (error) {
throw error;
}
};
export const teamCreateCall = async (
accessToken: string,
formValues: Record<string, any> // Assuming formValues is an object

View file

@ -188,6 +188,43 @@ const Settings: React.FC<SettingsPageProps> = ({
console.log("Selected values:", values);
};
const handleSaveEmailSettings = () => {
if (!accessToken) {
return;
}
let updatedVariables: Record<string, string> = {};
alerts
.filter((alert) => alert.name === "email")
.forEach((alert) => {
Object.entries(alert.variables ?? {}).forEach(([key, value]) => {
const inputElement = document.querySelector(`input[name="${key}"]`) as HTMLInputElement;
if (inputElement && inputElement.value) {
updatedVariables[key] = inputElement?.value;
}
});
});
console.log("updatedVariables", updatedVariables);
//filter out null / undefined values for updatedVariables
const payload = {
general_settings: {
alerting: ["email"],
},
environment_variables: updatedVariables,
};
try {
setCallbacksCall(accessToken, payload);
} catch (error) {
message.error("Failed to update alerts: " + error, 20);
}
message.success("Email settings updated successfully");
}
const handleSaveAlerts = () => {
if (!accessToken) {
return;
@ -369,7 +406,8 @@ const Settings: React.FC<SettingsPageProps> = ({
<TabList variant="line" defaultValue="1">
<Tab value="1">Logging Callbacks</Tab>
<Tab value="2">Alerting Types</Tab>
<Tab value="2">Alerting Settings</Tab>
<Tab value="3">Alerting Settings</Tab>
<Tab value="4">Email Alerts</Tab>
</TabList>
<TabPanels>
<TabPanel>
@ -526,6 +564,142 @@ const Settings: React.FC<SettingsPageProps> = ({
premiumUser={premiumUser}
/>
</TabPanel>
<TabPanel>
<Card>
<Title>Email Settings</Title>
<Text>
<a href="https://docs.litellm.ai/docs/proxy/email" target="_blank" style={{ color: "blue" }}> LiteLLM Docs: email alerts</a> <br/>
</Text>
<div className="flex w-full">
{alerts
.filter((alert) => alert.name === "email")
.map((alert, index) => (
<TableCell key={index}>
<ul>
<Grid numItems={2}>
{Object.entries(alert.variables ?? {}).map(([key, value]) => (
<li key={key} className="mx-2 my-2">
{ premiumUser!= true && (key === "EMAIL_LOGO_URL" || key === "EMAIL_SUPPORT_CONTACT") ? (
<div>
<a
href="https://forms.gle/W3U4PZpJGFHWtHyA9"
target="_blank"
>
<Text className="mt-2">
{" "}
{key}
</Text>
</a>
<TextInput
name={key}
defaultValue={value as string}
type="password"
disabled={true}
style={{ width: "400px" }}
/>
</div>
) : (
<div>
<Text className="mt-2">{key}</Text>
<TextInput
name={key}
defaultValue={value as string}
type="password"
style={{ width: "400px" }}
/>
</div>
)}
{/* Added descriptions for input fields */}
<p style={{ fontSize: "small", fontStyle: "italic" }}>
{key === "SMTP_HOST" && (
<div style={{ color: "gray" }}>
Enter the SMTP host address, e.g. `smtp.resend.com`
<span style={{ color: "red" }}> Required * </span>
</div>
)}
{key === "SMTP_PORT" && (
<div style={{ color: "gray" }}>
Enter the SMTP port number, e.g. `587`
<span style={{ color: "red" }}> Required * </span>
</div>
)}
{key === "SMTP_USERNAME" && (
<div style={{ color: "gray" }}>
Enter the SMTP username, e.g. `username`
<span style={{ color: "red" }}> Required * </span>
</div>
)}
{key === "SMTP_PASSWORD" && (
<span style={{ color: "red" }}> Required * </span>
)}
{key === "SMTP_SENDER_EMAIL" && (
<div style={{ color: "gray" }}>
Enter the sender email address, e.g. `sender@berri.ai`
<span style={{ color: "red" }}> Required * </span>
</div>
)}
{key === "TEST_EMAIL_ADDRESS" && (
<div style={{ color: "gray" }}>
Email Address to send `Test Email Alert` to. example: `info@berri.ai`
<span style={{ color: "red" }}> Required * </span>
</div>
)
}
{key === "EMAIL_LOGO_URL" && (
<div style={{ color: "gray" }}>
(Optional) Customize the Logo that appears in the email, pass a url to your logo
</div>
)
}
{key === "EMAIL_SUPPORT_CONTACT" && (
<div style={{ color: "gray" }}>
(Optional) Customize the support email address that appears in the email. Default is support@berri.ai
</div>
)
}
</p>
</li>
))}
</Grid>
</ul>
</TableCell>
))}
</div>
<Button
className="mt-2"
onClick={() => handleSaveEmailSettings()}
>
Save Changes
</Button>
<Button
onClick={() =>
serviceHealthCheck(accessToken, "email")
}
className="mx-2"
>
Test Email Alerts
</Button>
</Card>
</TabPanel>
</TabPanels>
</TabGroup>
</Grid>

View file

@ -162,6 +162,17 @@ const UsagePage: React.FC<UsagePageProps> = ({
console.log("keys in usage", keys);
console.log("premium user in usage", premiumUser);
function valueFormatterNumbers(number: number) {
const formatter = new Intl.NumberFormat('en-US', {
maximumFractionDigits: 0,
notation: 'compact',
compactDisplay: 'short',
});
return formatter.format(number);
}
const updateEndUserData = async (startTime: Date | undefined, endTime: Date | undefined, uiSelectedKey: string | null) => {
if (!startTime || !endTime || !accessToken) {
return;
@ -482,10 +493,11 @@ const UsagePage: React.FC<UsagePageProps> = ({
<Title>All Up</Title>
<Grid numItems={2}>
<Col>
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>API Requests {globalActivity.sum_api_requests}</Subtitle>
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>API Requests { valueFormatterNumbers(globalActivity.sum_api_requests)}</Subtitle>
<AreaChart
className="h-40"
data={globalActivity.daily_data}
valueFormatter={valueFormatterNumbers}
index="date"
colors={['cyan']}
categories={['api_requests']}
@ -494,10 +506,11 @@ const UsagePage: React.FC<UsagePageProps> = ({
</Col>
<Col>
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>Tokens {globalActivity.sum_total_tokens}</Subtitle>
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>Tokens { valueFormatterNumbers(globalActivity.sum_total_tokens)}</Subtitle>
<BarChart
className="h-40"
data={globalActivity.daily_data}
valueFormatter={valueFormatterNumbers}
index="date"
colors={['cyan']}
categories={['total_tokens']}
@ -517,24 +530,26 @@ const UsagePage: React.FC<UsagePageProps> = ({
<Title>{globalActivity.model}</Title>
<Grid numItems={2}>
<Col>
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>API Requests {globalActivity.sum_api_requests}</Subtitle>
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>API Requests {valueFormatterNumbers(globalActivity.sum_api_requests)}</Subtitle>
<AreaChart
className="h-40"
data={globalActivity.daily_data}
index="date"
colors={['cyan']}
categories={['api_requests']}
valueFormatter={valueFormatterNumbers}
onValueChange={(v) => console.log(v)}
/>
</Col>
<Col>
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>Tokens {globalActivity.sum_total_tokens}</Subtitle>
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>Tokens {valueFormatterNumbers(globalActivity.sum_total_tokens)}</Subtitle>
<BarChart
className="h-40"
data={globalActivity.daily_data}
index="date"
colors={['cyan']}
categories={['total_tokens']}
valueFormatter={valueFormatterNumbers}
onValueChange={(v) => console.log(v)}
/>
</Col>
@ -565,7 +580,7 @@ const UsagePage: React.FC<UsagePageProps> = ({
color: "#535452",
}}
>
API Requests {globalActivity.sum_api_requests}
API Requests {valueFormatterNumbers(globalActivity.sum_api_requests)}
</Subtitle>
<AreaChart
className="h-40"
@ -573,6 +588,7 @@ const UsagePage: React.FC<UsagePageProps> = ({
index="date"
colors={['cyan']}
categories={['api_requests']}
valueFormatter={valueFormatterNumbers}
onValueChange={(v) => console.log(v)}
/>
</Col>
@ -584,13 +600,14 @@ const UsagePage: React.FC<UsagePageProps> = ({
color: "#535452",
}}
>
Tokens {globalActivity.sum_total_tokens}
Tokens {valueFormatterNumbers(globalActivity.sum_total_tokens)}
</Subtitle>
<BarChart
className="h-40"
data={globalActivity.daily_data}
index="date"
colors={['cyan']}
valueFormatter={valueFormatterNumbers}
categories={['total_tokens']}
onValueChange={(v) => console.log(v)}
/>

View file

@ -24,12 +24,22 @@ import {
Icon,
TextInput,
} from "@tremor/react";
import { userInfoCall } from "./networking";
import {
message,
} from "antd";
import { userInfoCall, userUpdateUserCall, getPossibleUserRoles } from "./networking";
import { Badge, BadgeDelta, Button } from "@tremor/react";
import RequestAccess from "./request_model_access";
import CreateUser from "./create_user_button";
import EditUserModal from "./edit_user";
import Paragraph from "antd/es/skeleton/Paragraph";
import InformationCircleIcon from "@heroicons/react/outline/InformationCircleIcon";
import {
PencilAltIcon,
InformationCircleIcon,
TrashIcon,
} from "@heroicons/react/outline";
interface ViewUserDashboardProps {
accessToken: string | null;
@ -55,8 +65,40 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
const [currentPage, setCurrentPage] = useState(0);
const [openDialogId, setOpenDialogId] = React.useState<null | number>(null);
const [selectedItem, setSelectedItem] = useState<null | any>(null);
const [editModalVisible, setEditModalVisible] = useState(false);
const [selectedUser, setSelectedUser] = useState(null);
const [possibleUIRoles, setPossibleUIRoles] = useState<Record<string, Record<string, string>>>({});
const defaultPageSize = 25;
const handleEditCancel = async () => {
setSelectedUser(null);
setEditModalVisible(false);
};
const handleEditSubmit = async (editedUser: any) => {
console.log("inside handleEditSubmit:", editedUser);
if (!accessToken || !token || !userRole || !userID) {
return;
}
try {
await userUpdateUserCall(accessToken, editedUser, null);
message.success(`User ${editedUser.user_id} updated successfully`);
} catch (error) {
console.error("There was an error updating the user", error);
}
if (userData) {
const updatedUserData = userData.map((user) =>
user.user_id === editedUser.user_id ? editedUser : user
);
setUserData(updatedUserData);
}
setSelectedUser(null);
setEditModalVisible(false);
// Close the modal
};
useEffect(() => {
if (!accessToken || !token || !userRole || !userID) {
return;
@ -74,11 +116,16 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
);
console.log("user data response:", userDataResponse);
setUserData(userDataResponse);
const availableUserRoles = await getPossibleUserRoles(accessToken);
setPossibleUIRoles(availableUserRoles);
} catch (error) {
console.error("There was an error fetching the model data", error);
}
};
if (accessToken && token && userRole && userID) {
fetchData();
}
@ -126,14 +173,10 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
return (
<div style={{ width: "100%" }}>
<Grid className="gap-2 p-2 h-[80vh] w-full mt-8">
<Grid className="gap-2 p-2 h-[90vh] w-full mt-8">
<CreateUser userID={userID} accessToken={accessToken} teams={teams} />
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[80vh] mb-4">
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[90vh] mb-4">
<div className="mb-4 mt-1">
<Text>
These are Users on LiteLLM that created API Keys. Automatically
tracked by LiteLLM
</Text>
</div>
<TabGroup>
<TabPanels>
@ -143,25 +186,23 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
<TableRow>
<TableHeaderCell>User ID</TableHeaderCell>
<TableHeaderCell>User Email</TableHeaderCell>
<TableHeaderCell>User Models</TableHeaderCell>
<TableHeaderCell>Role</TableHeaderCell>
<TableHeaderCell>User Spend ($ USD)</TableHeaderCell>
<TableHeaderCell>User Max Budget ($ USD)</TableHeaderCell>
<TableHeaderCell>User API Key Aliases</TableHeaderCell>
<TableHeaderCell>API Keys</TableHeaderCell>
<TableHeaderCell></TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{userData.map((user: any) => (
<TableRow key={user.user_id}>
<TableCell>{user.user_id}</TableCell>
<TableCell>{user.user_email}</TableCell>
<TableCell>{user.user_id || "-"}</TableCell>
<TableCell>{user.user_email || "-"}</TableCell>
<TableCell>
{user.models && user.models.length > 0
? user.models
: "All Models"}
{possibleUIRoles?.[user?.user_role]?.ui_label || "-"}
</TableCell>
<TableCell>
{user.spend ? user.spend?.toFixed(2) : 0}
{user.spend ? user.spend?.toFixed(2) : "-"}
</TableCell>
<TableCell>
{user.max_budget ? user.max_budget : "Unlimited"}
@ -173,9 +214,13 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
(key: any) => key !== null
).length > 0 ? (
<Badge size={"xs"} color={"indigo"}>
{user.key_aliases
.filter((key: any) => key !== null)
.join(", ")}
{
user.key_aliases.filter(
(key: any) => key !== null
).length
}
&nbsp;Keys
</Badge>
) : (
<Badge size={"xs"} color={"gray"}>
@ -188,12 +233,23 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
</Badge>
)}
{/* <Text>{user.key_aliases.filter(key => key !== null).length} Keys</Text> */}
{/* <Icon icon={InformationCircleIcon} onClick= {() => {
</Grid>
</TableCell>
<TableCell>
<Icon icon={PencilAltIcon} onClick= {() => {
setSelectedUser(user)
setEditModalVisible(true)
}}>View Keys</Icon>
{/*
<Icon icon={TrashIcon} onClick= {() => {
setOpenDialogId(user.user_id)
setSelectedItem(user)
}}>View Keys</Icon> */}
</Grid>
</TableCell>
</TableRow>
))}
</TableBody>
@ -226,30 +282,16 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
</TabPanel>
</TabPanels>
</TabGroup>
<EditUserModal
visible={editModalVisible}
possibleUIRoles={possibleUIRoles}
onCancel={handleEditCancel}
user={selectedUser}
onSubmit={handleEditSubmit}
/>
</Card>
{renderPagination()}
</Grid>
{/* <Dialog
open={openDialogId !== null}
onClose={() => {
setOpenDialogId(null);
}}
>
<DialogPanel>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<Title>Key Aliases</Title>
<Text>
{selectedItem && selectedItem.key_aliases
? selectedItem.key_aliases.filter(key => key !== null).length > 0
? selectedItem.key_aliases.filter(key => key !== null).join(', ')
: 'No Keys'
: "No Keys"}
</Text>
</div>
</DialogPanel>
</Dialog> */}
</div>
);
};