Merge pull request #3572 from powerhouseofthecell/feature/enforce-unique-key-and-team-aliases

enforce unique key and team aliases in the ui
This commit is contained in:
Krish Dholakia 2024-05-10 17:53:26 -07:00 committed by GitHub
commit 69068f9577
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 495 additions and 339 deletions

View file

@ -2,8 +2,17 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { Button, TextInput, Grid, Col } from "@tremor/react"; import { Button, TextInput, Grid, Col } from "@tremor/react";
import { Card, Metric, Text, Title, Subtitle, Accordion, AccordionHeader, AccordionBody, } from "@tremor/react"; import {
import { CopyToClipboard } from 'react-copy-to-clipboard'; Card,
Metric,
Text,
Title,
Subtitle,
Accordion,
AccordionHeader,
AccordionBody,
} from "@tremor/react";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { import {
Button as Button2, Button as Button2,
Modal, Modal,
@ -13,7 +22,11 @@ import {
Select, Select,
message, message,
} from "antd"; } from "antd";
import { keyCreateCall, slackBudgetAlertsHealthCheck, modelAvailableCall } from "./networking"; import {
keyCreateCall,
slackBudgetAlertsHealthCheck,
modelAvailableCall,
} from "./networking";
const { Option } = Select; const { Option } = Select;
@ -59,7 +72,11 @@ const CreateKey: React.FC<CreateKeyProps> = ({
} }
if (accessToken !== null) { if (accessToken !== null) {
const model_available = await modelAvailableCall(accessToken, userID, userRole); const model_available = await modelAvailableCall(
accessToken,
userID,
userRole
);
let available_model_names = model_available["data"].map( let available_model_names = model_available["data"].map(
(element: { id: string }) => element.id (element: { id: string }) => element.id
); );
@ -70,12 +87,25 @@ const CreateKey: React.FC<CreateKeyProps> = ({
console.error("Error fetching user models:", error); console.error("Error fetching user models:", error);
} }
}; };
fetchUserModels(); fetchUserModels();
}, [accessToken, userID, userRole]); }, [accessToken, userID, userRole]);
const handleCreate = async (formValues: Record<string, any>) => { const handleCreate = async (formValues: Record<string, any>) => {
try { try {
const newKeyAlias = formValues?.key_alias ?? "";
const newKeyTeamId = formValues?.team_id ?? null;
const existingKeyAliases =
data
?.filter((k) => k.team_id === newKeyTeamId)
.map((k) => k.key_alias) ?? [];
if (existingKeyAliases.includes(newKeyAlias)) {
throw new Error(
`Key alias ${newKeyAlias} already exists for team with ID ${newKeyTeamId}, please provide another key alias`
);
}
message.info("Making API Call"); message.info("Making API Call");
setIsModalVisible(true); setIsModalVisible(true);
const response = await keyCreateCall(accessToken, userID, formValues); const response = await keyCreateCall(accessToken, userID, formValues);
@ -89,12 +119,13 @@ const CreateKey: React.FC<CreateKeyProps> = ({
localStorage.removeItem("userData" + userID); localStorage.removeItem("userData" + userID);
} catch (error) { } catch (error) {
console.error("Error creating the key:", error); console.error("Error creating the key:", error);
message.error(`Error creating the key: ${error}`, 20);
} }
}; };
const handleCopy = () => { const handleCopy = () => {
message.success('API Key copied to clipboard'); message.success("API Key copied to clipboard");
}; };
useEffect(() => { useEffect(() => {
let tempModelsToPick = []; let tempModelsToPick = [];
@ -119,7 +150,6 @@ const CreateKey: React.FC<CreateKeyProps> = ({
setModelsToPick(tempModelsToPick); setModelsToPick(tempModelsToPick);
}, [team, userModels]); }, [team, userModels]);
return ( return (
<div> <div>
@ -141,140 +171,164 @@ const CreateKey: React.FC<CreateKeyProps> = ({
wrapperCol={{ span: 16 }} wrapperCol={{ span: 16 }}
labelAlign="left" labelAlign="left"
> >
<> <>
<Form.Item <Form.Item
label="Key Name" label="Key Name"
name="key_alias" name="key_alias"
rules={[{ required: true, message: 'Please input a key name' }]} rules={[{ required: true, message: "Please input a key name" }]}
help="required" help="required"
> >
<TextInput placeholder="" /> <TextInput placeholder="" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="Team ID" label="Team ID"
name="team_id" name="team_id"
hidden={true} hidden={true}
initialValue={team ? team["team_id"] : null} initialValue={team ? team["team_id"] : null}
valuePropName="team_id" valuePropName="team_id"
className="mt-8"
>
<Input value={team ? team["team_alias"] : ""} disabled />
</Form.Item>
<Form.Item
label="Models"
name="models"
rules={[{ required: true, message: 'Please select a model' }]}
help="required"
>
<Select
mode="multiple"
placeholder="Select models"
style={{ width: "100%" }}
onChange={(values) => {
// Check if "All Team Models" is selected
const isAllTeamModelsSelected = values.includes("all-team-models");
// If "All Team Models" is selected, deselect all other models
if (isAllTeamModelsSelected) {
const newValues = ["all-team-models"];
// You can call the form's setFieldsValue method to update the value
form.setFieldsValue({ models: newValues });
}
}}
>
<Option key="all-team-models" value="all-team-models">
All Team Models
</Option>
{
modelsToPick.map((model: string) => (
(
<Option key={model} value={model}>
{model}
</Option>
)
))
}
</Select>
</Form.Item>
<Accordion className="mt-20 mb-8" >
<AccordionHeader>
<b>Optional Settings</b>
</AccordionHeader>
<AccordionBody>
<Form.Item
className="mt-8"
label="Max Budget (USD)"
name="max_budget"
help={`Budget cannot exceed team max budget: $${team?.max_budget !== null && team?.max_budget !== undefined ? team?.max_budget : 'unlimited'}`}
rules={[
{
validator: async (_, value) => {
if (value && team && team.max_budget !== null && value > team.max_budget) {
throw new Error(`Budget cannot exceed team max budget: $${team.max_budget}`);
}
},
},
]}
>
<InputNumber step={0.01} precision={2} width={200} />
</Form.Item>
<Form.Item
className="mt-8" className="mt-8"
label="Reset Budget" >
name="budget_duration" <Input value={team ? team["team_alias"] : ""} disabled />
help={`Team Reset Budget: ${team?.budget_duration !== null && team?.budget_duration !== undefined ? team?.budget_duration : 'None'}`} </Form.Item>
>
<Select defaultValue={null} placeholder="n/a"> <Form.Item
<Select.Option value="24h">daily</Select.Option> label="Models"
<Select.Option value="30d">monthly</Select.Option> name="models"
</Select> rules={[{ required: true, message: "Please select a model" }]}
</Form.Item> help="required"
<Form.Item >
className="mt-8" <Select
label="Tokens per minute Limit (TPM)" mode="multiple"
name="tpm_limit" placeholder="Select models"
help={`TPM cannot exceed team TPM limit: ${team?.tpm_limit !== null && team?.tpm_limit !== undefined ? team?.tpm_limit : 'unlimited'}`} style={{ width: "100%" }}
rules={[ onChange={(values) => {
{ // Check if "All Team Models" is selected
validator: async (_, value) => { const isAllTeamModelsSelected =
if (value && team && team.tpm_limit !== null && value > team.tpm_limit) { values.includes("all-team-models");
throw new Error(`TPM limit cannot exceed team TPM limit: ${team.tpm_limit}`);
} // If "All Team Models" is selected, deselect all other models
}, if (isAllTeamModelsSelected) {
}, const newValues = ["all-team-models"];
]} // You can call the form's setFieldsValue method to update the value
> form.setFieldsValue({ models: newValues });
<InputNumber step={1} width={400} /> }
</Form.Item> }}
<Form.Item >
className="mt-8" <Option key="all-team-models" value="all-team-models">
label="Requests per minute Limit (RPM)" All Team Models
name="rpm_limit" </Option>
help={`RPM cannot exceed team RPM limit: ${team?.rpm_limit !== null && team?.rpm_limit !== undefined ? team?.rpm_limit : 'unlimited'}`} {modelsToPick.map((model: string) => (
rules={[ <Option key={model} value={model}>
{ {model}
validator: async (_, value) => { </Option>
if (value && team && team.rpm_limit !== null && value > team.rpm_limit) { ))}
throw new Error(`RPM limit cannot exceed team RPM limit: ${team.rpm_limit}`); </Select>
} </Form.Item>
}, <Accordion className="mt-20 mb-8">
}, <AccordionHeader>
]} <b>Optional Settings</b>
> </AccordionHeader>
<InputNumber step={1} width={400} /> <AccordionBody>
</Form.Item> <Form.Item
<Form.Item label="Expire Key (eg: 30s, 30h, 30d)" name="duration" className="mt-8"> className="mt-8"
<TextInput placeholder="" /> label="Max Budget (USD)"
</Form.Item> name="max_budget"
<Form.Item label="Metadata" name="metadata"> help={`Budget cannot exceed team max budget: $${team?.max_budget !== null && team?.max_budget !== undefined ? team?.max_budget : "unlimited"}`}
<Input.TextArea rows={4} placeholder="Enter metadata as JSON" /> rules={[
</Form.Item> {
validator: async (_, value) => {
if (
value &&
team &&
team.max_budget !== null &&
value > team.max_budget
) {
throw new Error(
`Budget cannot exceed team max budget: $${team.max_budget}`
);
}
},
},
]}
>
<InputNumber step={0.01} precision={2} width={200} />
</Form.Item>
<Form.Item
className="mt-8"
label="Reset Budget"
name="budget_duration"
help={`Team Reset Budget: ${team?.budget_duration !== null && team?.budget_duration !== undefined ? team?.budget_duration : "None"}`}
>
<Select defaultValue={null} placeholder="n/a">
<Select.Option value="24h">daily</Select.Option>
<Select.Option value="30d">monthly</Select.Option>
</Select>
</Form.Item>
<Form.Item
className="mt-8"
label="Tokens per minute Limit (TPM)"
name="tpm_limit"
help={`TPM cannot exceed team TPM limit: ${team?.tpm_limit !== null && team?.tpm_limit !== undefined ? team?.tpm_limit : "unlimited"}`}
rules={[
{
validator: async (_, value) => {
if (
value &&
team &&
team.tpm_limit !== null &&
value > team.tpm_limit
) {
throw new Error(
`TPM limit cannot exceed team TPM limit: ${team.tpm_limit}`
);
}
},
},
]}
>
<InputNumber step={1} width={400} />
</Form.Item>
<Form.Item
className="mt-8"
label="Requests per minute Limit (RPM)"
name="rpm_limit"
help={`RPM cannot exceed team RPM limit: ${team?.rpm_limit !== null && team?.rpm_limit !== undefined ? team?.rpm_limit : "unlimited"}`}
rules={[
{
validator: async (_, value) => {
if (
value &&
team &&
team.rpm_limit !== null &&
value > team.rpm_limit
) {
throw new Error(
`RPM limit cannot exceed team RPM limit: ${team.rpm_limit}`
);
}
},
},
]}
>
<InputNumber step={1} width={400} />
</Form.Item>
<Form.Item
label="Expire Key (eg: 30s, 30h, 30d)"
name="duration"
className="mt-8"
>
<TextInput placeholder="" />
</Form.Item>
<Form.Item label="Metadata" name="metadata">
<Input.TextArea
rows={4}
placeholder="Enter metadata as JSON"
/>
</Form.Item>
</AccordionBody>
</Accordion>
</>
</AccordionBody>
</Accordion>
</>
<div style={{ textAlign: "right", marginTop: "10px" }}> <div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 htmlType="submit">Create Key</Button2> <Button2 htmlType="submit">Create Key</Button2>
</div> </div>
@ -288,36 +342,45 @@ const CreateKey: React.FC<CreateKeyProps> = ({
footer={null} footer={null}
> >
<Grid numItems={1} className="gap-2 w-full"> <Grid numItems={1} className="gap-2 w-full">
<Title>Save your Key</Title>
<Title>Save your Key</Title> <Col numColSpan={1}>
<Col numColSpan={1}> <p>
<p> Please save this secret key somewhere safe and accessible. For
Please save this secret key somewhere safe and accessible. For security reasons, <b>you will not be able to view it again</b>{" "}
security reasons, <b>you will not be able to view it again</b>{" "} through your LiteLLM account. If you lose this secret key, you
through your LiteLLM account. If you lose this secret key, you will need to generate a new one.
will need to generate a new one. </p>
</p> </Col>
</Col> <Col numColSpan={1}>
<Col numColSpan={1}> {apiKey != null ? (
{apiKey != null ? ( <div>
<div>
<Text className="mt-3">API Key:</Text> <Text className="mt-3">API Key:</Text>
<div style={{ background: '#f8f8f8', padding: '10px', borderRadius: '5px', marginBottom: '10px' }}> <div
<pre style={{ wordWrap: 'break-word', whiteSpace: 'normal' }}>{apiKey}</pre> style={{
</div> background: "#f8f8f8",
padding: "10px",
<CopyToClipboard text={apiKey} onCopy={handleCopy}> borderRadius: "5px",
marginBottom: "10px",
}}
>
<pre
style={{ wordWrap: "break-word", whiteSpace: "normal" }}
>
{apiKey}
</pre>
</div>
<CopyToClipboard text={apiKey} onCopy={handleCopy}>
<Button className="mt-3">Copy API Key</Button> <Button className="mt-3">Copy API Key</Button>
</CopyToClipboard> </CopyToClipboard>
{/* <Button className="mt-3" onClick={sendSlackAlert}> {/* <Button className="mt-3" onClick={sendSlackAlert}>
Test Key Test Key
</Button> */} </Button> */}
</div> </div>
) : ( ) : (
<Text>Key being created, this might take 30s</Text> <Text>Key being created, this might take 30s</Text>
)} )}
</Col> </Col>
</Grid> </Grid>
</Modal> </Modal>
)} )}

View file

@ -2,7 +2,13 @@ import React, { useState, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { Typography } from "antd"; import { Typography } from "antd";
import { teamDeleteCall, teamUpdateCall, teamInfoCall } from "./networking"; import { teamDeleteCall, teamUpdateCall, teamInfoCall } from "./networking";
import { InformationCircleIcon, PencilAltIcon, PencilIcon, StatusOnlineIcon, TrashIcon } from "@heroicons/react/outline"; import {
InformationCircleIcon,
PencilAltIcon,
PencilIcon,
StatusOnlineIcon,
TrashIcon,
} from "@heroicons/react/outline";
import { import {
Button as Button2, Button as Button2,
Modal, Modal,
@ -46,8 +52,12 @@ interface EditTeamModalProps {
onSubmit: (data: FormData) => void; // Assuming FormData is the type of data to be submitted onSubmit: (data: FormData) => void; // Assuming FormData is the type of data to be submitted
} }
import {
import { teamCreateCall, teamMemberAddCall, Member, modelAvailableCall } from "./networking"; teamCreateCall,
teamMemberAddCall,
Member,
modelAvailableCall,
} from "./networking";
const Team: React.FC<TeamProps> = ({ const Team: React.FC<TeamProps> = ({
teams, teams,
@ -63,7 +73,6 @@ const Team: React.FC<TeamProps> = ({
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const [editModalVisible, setEditModalVisible] = useState(false); const [editModalVisible, setEditModalVisible] = useState(false);
const [selectedTeam, setSelectedTeam] = useState<null | any>( const [selectedTeam, setSelectedTeam] = useState<null | any>(
teams ? teams[0] : null teams ? teams[0] : null
); );
@ -76,127 +85,125 @@ const Team: React.FC<TeamProps> = ({
// store team info as {"team_id": team_info_object} // store team info as {"team_id": team_info_object}
const [perTeamInfo, setPerTeamInfo] = useState<Record<string, any>>({}); const [perTeamInfo, setPerTeamInfo] = useState<Record<string, any>>({});
const EditTeamModal: React.FC<EditTeamModalProps> = ({
visible,
onCancel,
team,
onSubmit,
}) => {
const [form] = Form.useForm();
const EditTeamModal: React.FC<EditTeamModalProps> = ({ visible, onCancel, team, onSubmit }) => { const handleOk = () => {
const [form] = Form.useForm(); form
.validateFields()
.then((values) => {
const updatedValues = { ...values, team_id: team.team_id };
onSubmit(updatedValues);
form.resetFields();
})
.catch((error) => {
console.error("Validation failed:", error);
});
};
const handleOk = () => { return (
form
.validateFields()
.then((values) => {
const updatedValues = {...values, team_id: team.team_id};
onSubmit(updatedValues);
form.resetFields();
})
.catch((error) => {
console.error("Validation failed:", error);
});
};
return (
<Modal <Modal
title="Edit Team" title="Edit Team"
visible={visible} visible={visible}
width={800} width={800}
footer={null} footer={null}
onOk={handleOk} onOk={handleOk}
onCancel={onCancel} onCancel={onCancel}
>
<Form
form={form}
onFinish={handleEditSubmit}
initialValues={team} // Pass initial values here
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
> >
<> <Form
<Form.Item form={form}
label="Team Name" onFinish={handleEditSubmit}
name="team_alias" initialValues={team} // Pass initial values here
rules={[{ required: true, message: 'Please input a team name' }]} labelCol={{ span: 8 }}
> wrapperCol={{ span: 16 }}
<Input /> labelAlign="left"
</Form.Item> >
<Form.Item label="Models" name="models"> <>
<Select2 <Form.Item
mode="multiple" label="Team Name"
placeholder="Select models" name="team_alias"
style={{ width: "100%" }} rules={[{ required: true, message: "Please input a team name" }]}
> >
<Select2.Option key="all-proxy-models" value="all-proxy-models"> <Input />
{"All Proxy Models"} </Form.Item>
</Select2.Option> <Form.Item label="Models" name="models">
{userModels && userModels.map((model) => ( <Select2
<Select2.Option key={model} value={model}> mode="multiple"
{model} placeholder="Select models"
</Select2.Option> style={{ width: "100%" }}
))} >
<Select2.Option key="all-proxy-models" value="all-proxy-models">
</Select2> {"All Proxy Models"}
</Form.Item> </Select2.Option>
<Form.Item label="Max Budget (USD)" name="max_budget"> {userModels &&
<InputNumber step={0.01} precision={2} width={200} /> userModels.map((model) => (
</Form.Item> <Select2.Option key={model} value={model}>
<Form.Item {model}
label="Tokens per minute Limit (TPM)" </Select2.Option>
name="tpm_limit" ))}
> </Select2>
<InputNumber step={1} width={400} /> </Form.Item>
</Form.Item> <Form.Item label="Max Budget (USD)" name="max_budget">
<Form.Item <InputNumber step={0.01} precision={2} width={200} />
label="Requests per minute Limit (RPM)" </Form.Item>
name="rpm_limit" <Form.Item label="Tokens per minute Limit (TPM)" name="tpm_limit">
> <InputNumber step={1} width={400} />
<InputNumber step={1} width={400} /> </Form.Item>
</Form.Item> <Form.Item label="Requests per minute Limit (RPM)" name="rpm_limit">
<Form.Item <InputNumber step={1} width={400} />
label="Requests per minute Limit (RPM)" </Form.Item>
name="team_id" <Form.Item
hidden={true} label="Requests per minute Limit (RPM)"
></Form.Item> name="team_id"
</> hidden={true}
<div style={{ textAlign: "right", marginTop: "10px" }}> ></Form.Item>
<Button2 htmlType="submit">Edit Team</Button2> </>
</div> <div style={{ textAlign: "right", marginTop: "10px" }}>
</Form> <Button2 htmlType="submit">Edit Team</Button2>
</Modal> </div>
); </Form>
}; </Modal>
const handleEditClick = (team: any) => {
setSelectedTeam(team);
setEditModalVisible(true);
};
const handleEditCancel = () => {
setEditModalVisible(false);
setSelectedTeam(null);
};
const handleEditSubmit = async (formValues: Record<string, any>) => {
// Call API to update team with teamId and values
const teamId = formValues.team_id; // get team_id
console.log("handleEditSubmit:", formValues);
if (accessToken == null) {
return;
}
let newTeamValues = await teamUpdateCall(accessToken, formValues);
// Update the teams state with the updated team data
if (teams) {
const updatedTeams = teams.map((team) =>
team.team_id === teamId ? newTeamValues.data : team
); );
setTeams(updatedTeams); };
}
message.success("Team updated successfully");
setEditModalVisible(false); const handleEditClick = (team: any) => {
setSelectedTeam(null); setSelectedTeam(team);
}; setEditModalVisible(true);
};
const handleEditCancel = () => {
setEditModalVisible(false);
setSelectedTeam(null);
};
const handleEditSubmit = async (formValues: Record<string, any>) => {
// Call API to update team with teamId and values
const teamId = formValues.team_id; // get team_id
console.log("handleEditSubmit:", formValues);
if (accessToken == null) {
return;
}
let newTeamValues = await teamUpdateCall(accessToken, formValues);
// Update the teams state with the updated team data
if (teams) {
const updatedTeams = teams.map((team) =>
team.team_id === teamId ? newTeamValues.data : team
);
setTeams(updatedTeams);
}
message.success("Team updated successfully");
setEditModalVisible(false);
setSelectedTeam(null);
};
const handleOk = () => { const handleOk = () => {
setIsTeamModalVisible(false); setIsTeamModalVisible(false);
@ -224,9 +231,6 @@ const handleEditSubmit = async (formValues: Record<string, any>) => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
}; };
const confirmDelete = async () => { const confirmDelete = async () => {
if (teamToDelete == null || teams == null || accessToken == null) { if (teamToDelete == null || teams == null || accessToken == null) {
return; return;
@ -235,7 +239,9 @@ const handleEditSubmit = async (formValues: Record<string, any>) => {
try { try {
await teamDeleteCall(accessToken, teamToDelete); await teamDeleteCall(accessToken, teamToDelete);
// Successfully completed the deletion. Update the state to trigger a rerender. // Successfully completed the deletion. Update the state to trigger a rerender.
const filteredData = teams.filter((item) => item.team_id !== teamToDelete); const filteredData = teams.filter(
(item) => item.team_id !== teamToDelete
);
setTeams(filteredData); setTeams(filteredData);
} catch (error) { } catch (error) {
console.error("Error deleting the team:", error); console.error("Error deleting the team:", error);
@ -253,8 +259,6 @@ const handleEditSubmit = async (formValues: Record<string, any>) => {
setTeamToDelete(null); setTeamToDelete(null);
}; };
useEffect(() => { useEffect(() => {
const fetchUserModels = async () => { const fetchUserModels = async () => {
try { try {
@ -263,7 +267,11 @@ const handleEditSubmit = async (formValues: Record<string, any>) => {
} }
if (accessToken !== null) { if (accessToken !== null) {
const model_available = await modelAvailableCall(accessToken, userID, userRole); const model_available = await modelAvailableCall(
accessToken,
userID,
userRole
);
let available_model_names = model_available["data"].map( let available_model_names = model_available["data"].map(
(element: { id: string }) => element.id (element: { id: string }) => element.id
); );
@ -275,7 +283,6 @@ const handleEditSubmit = async (formValues: Record<string, any>) => {
} }
}; };
const fetchTeamInfo = async () => { const fetchTeamInfo = async () => {
try { try {
if (userID === null || userRole === null || accessToken === null) { if (userID === null || userRole === null || accessToken === null) {
@ -288,22 +295,21 @@ const handleEditSubmit = async (formValues: Record<string, any>) => {
console.log("fetching team info:"); console.log("fetching team info:");
let _team_id_to_info: Record<string, any> = {}; let _team_id_to_info: Record<string, any> = {};
for (let i = 0; i < teams?.length; i++) { for (let i = 0; i < teams?.length; i++) {
let _team_id = teams[i].team_id; let _team_id = teams[i].team_id;
const teamInfo = await teamInfoCall(accessToken, _team_id); const teamInfo = await teamInfoCall(accessToken, _team_id);
console.log("teamInfo response:", teamInfo); console.log("teamInfo response:", teamInfo);
if (teamInfo !== null) { if (teamInfo !== null) {
_team_id_to_info = {..._team_id_to_info, [_team_id]: teamInfo}; _team_id_to_info = { ..._team_id_to_info, [_team_id]: teamInfo };
} }
} }
setPerTeamInfo(_team_id_to_info); setPerTeamInfo(_team_id_to_info);
} catch (error) { } catch (error) {
console.error("Error fetching team info:", error); console.error("Error fetching team info:", error);
} }
}; };
fetchUserModels(); fetchUserModels();
fetchTeamInfo(); fetchTeamInfo();
}, [accessToken, userID, userRole, teams]); }, [accessToken, userID, userRole, teams]);
@ -311,6 +317,15 @@ const handleEditSubmit = async (formValues: Record<string, any>) => {
const handleCreate = async (formValues: Record<string, any>) => { const handleCreate = async (formValues: Record<string, any>) => {
try { try {
if (accessToken != null) { if (accessToken != null) {
const newTeamAlias = formValues?.team_alias;
const existingTeamAliases = teams?.map((t) => t.team_alias) ?? [];
if (existingTeamAliases.includes(newTeamAlias)) {
throw new Error(
`Team alias ${newTeamAlias} already exists, please pick another alias`
);
}
message.info("Creating Team"); message.info("Creating Team");
const response: any = await teamCreateCall(accessToken, formValues); const response: any = await teamCreateCall(accessToken, formValues);
if (teams !== null) { if (teams !== null) {
@ -364,7 +379,7 @@ const handleEditSubmit = async (formValues: Record<string, any>) => {
console.error("Error creating the team:", error); console.error("Error creating the team:", error);
} }
}; };
console.log(`received teams ${teams}`); console.log(`received teams ${JSON.stringify(teams)}`);
return ( return (
<div className="w-full mx-4"> <div className="w-full mx-4">
<Grid numItems={1} className="gap-2 p-8 h-[75vh] w-full mt-2"> <Grid numItems={1} className="gap-2 p-8 h-[75vh] w-full mt-2">
@ -387,55 +402,124 @@ const handleEditSubmit = async (formValues: Record<string, any>) => {
{teams && teams.length > 0 {teams && teams.length > 0
? teams.map((team: any) => ( ? teams.map((team: any) => (
<TableRow key={team.team_id}> <TableRow key={team.team_id}>
<TableCell style={{ maxWidth: "4px", whiteSpace: "pre-wrap", overflow: "hidden" }}>{team["team_alias"]}</TableCell> <TableCell
<TableCell style={{ maxWidth: "4px", whiteSpace: "pre-wrap", overflow: "hidden" }}>{team["spend"]}</TableCell> style={{
<TableCell style={{ maxWidth: "4px", whiteSpace: "pre-wrap", overflow: "hidden" }}> maxWidth: "4px",
whiteSpace: "pre-wrap",
overflow: "hidden",
}}
>
{team["team_alias"]}
</TableCell>
<TableCell
style={{
maxWidth: "4px",
whiteSpace: "pre-wrap",
overflow: "hidden",
}}
>
{team["spend"]}
</TableCell>
<TableCell
style={{
maxWidth: "4px",
whiteSpace: "pre-wrap",
overflow: "hidden",
}}
>
{team["max_budget"] ? team["max_budget"] : "No limit"} {team["max_budget"] ? team["max_budget"] : "No limit"}
</TableCell> </TableCell>
<TableCell style={{ maxWidth: "8-x", whiteSpace: "pre-wrap", overflow: "hidden" }}> <TableCell
style={{
maxWidth: "8-x",
whiteSpace: "pre-wrap",
overflow: "hidden",
}}
>
{Array.isArray(team.models) ? ( {Array.isArray(team.models) ? (
<div style={{ display: "flex", flexDirection: "column" }}> <div
style={{
display: "flex",
flexDirection: "column",
}}
>
{team.models.length === 0 ? ( {team.models.length === 0 ? (
<Badge size={"xs"} className="mb-1" color="red"> <Badge size={"xs"} className="mb-1" color="red">
<Text>All Proxy Models</Text> <Text>All Proxy Models</Text>
</Badge> </Badge>
) : ( ) : (
team.models.map((model: string, index: number) => ( team.models.map(
model === "all-proxy-models" ? ( (model: string, index: number) =>
<Badge key={index} size={"xs"} className="mb-1" color="red"> model === "all-proxy-models" ? (
<Text>All Proxy Models</Text> <Badge
</Badge> key={index}
) : ( size={"xs"}
<Badge key={index} size={"xs"} className="mb-1" color="blue"> className="mb-1"
<Text>{model.length > 30 ? `${model.slice(0, 30)}...` : model}</Text> color="red"
</Badge> >
) <Text>All Proxy Models</Text>
)) </Badge>
) : (
<Badge
key={index}
size={"xs"}
className="mb-1"
color="blue"
>
<Text>
{model.length > 30
? `${model.slice(0, 30)}...`
: model}
</Text>
</Badge>
)
)
)} )}
</div> </div>
) : null} ) : null}
</TableCell> </TableCell>
<TableCell style={{ maxWidth: "4px", whiteSpace: "pre-wrap", overflow: "hidden" }}> <TableCell
style={{
maxWidth: "4px",
whiteSpace: "pre-wrap",
overflow: "hidden",
}}
>
<Text> <Text>
TPM:{" "} TPM: {team.tpm_limit ? team.tpm_limit : "Unlimited"}{" "}
{team.tpm_limit ? team.tpm_limit : "Unlimited"}{" "}
<br></br>RPM:{" "} <br></br>RPM:{" "}
{team.rpm_limit ? team.rpm_limit : "Unlimited"} {team.rpm_limit ? team.rpm_limit : "Unlimited"}
</Text> </Text>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Text>{perTeamInfo && team.team_id && perTeamInfo[team.team_id] && perTeamInfo[team.team_id].keys && perTeamInfo[team.team_id].keys.length} Keys</Text> <Text>
<Text>{perTeamInfo && team.team_id && perTeamInfo[team.team_id] && perTeamInfo[team.team_id].team_info && perTeamInfo[team.team_id].team_info.members_with_roles && perTeamInfo[team.team_id].team_info.members_with_roles.length} Members</Text> {perTeamInfo &&
team.team_id &&
perTeamInfo[team.team_id] &&
perTeamInfo[team.team_id].keys &&
perTeamInfo[team.team_id].keys.length}{" "}
Keys
</Text>
<Text>
{perTeamInfo &&
team.team_id &&
perTeamInfo[team.team_id] &&
perTeamInfo[team.team_id].team_info &&
perTeamInfo[team.team_id].team_info
.members_with_roles &&
perTeamInfo[team.team_id].team_info
.members_with_roles.length}{" "}
Members
</Text>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Icon <Icon
icon={PencilAltIcon} icon={PencilAltIcon}
size="sm" size="sm"
onClick={() => handleEditClick(team)} onClick={() => handleEditClick(team)}
/> />
<Icon <Icon
onClick={() => handleDelete(team.team_id)} onClick={() => handleDelete(team.team_id)}
icon={TrashIcon} icon={TrashIcon}
size="sm" size="sm"
@ -481,7 +565,11 @@ const handleEditSubmit = async (formValues: Record<string, any>) => {
</div> </div>
</div> </div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"> <div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<Button onClick={confirmDelete} color="red" className="ml-2"> <Button
onClick={confirmDelete}
color="red"
className="ml-2"
>
Delete Delete
</Button> </Button>
<Button onClick={cancelDelete}>Cancel</Button> <Button onClick={cancelDelete}>Cancel</Button>
@ -515,10 +603,12 @@ const handleEditSubmit = async (formValues: Record<string, any>) => {
labelAlign="left" labelAlign="left"
> >
<> <>
<Form.Item <Form.Item
label="Team Name" label="Team Name"
name="team_alias" name="team_alias"
rules={[{ required: true, message: 'Please input a team name' }]} rules={[
{ required: true, message: "Please input a team name" },
]}
> >
<TextInput placeholder="" /> <TextInput placeholder="" />
</Form.Item> </Form.Item>
@ -528,7 +618,10 @@ const handleEditSubmit = async (formValues: Record<string, any>) => {
placeholder="Select models" placeholder="Select models"
style={{ width: "100%" }} style={{ width: "100%" }}
> >
<Select2.Option key="all-proxy-models" value="all-proxy-models"> <Select2.Option
key="all-proxy-models"
value="all-proxy-models"
>
All Proxy Models All Proxy Models
</Select2.Option> </Select2.Option>
{userModels.map((model) => ( {userModels.map((model) => (
@ -606,8 +699,8 @@ const handleEditSubmit = async (formValues: Record<string, any>) => {
{member["user_email"] {member["user_email"]
? member["user_email"] ? member["user_email"]
: member["user_id"] : member["user_id"]
? member["user_id"] ? member["user_id"]
: null} : null}
</TableCell> </TableCell>
<TableCell>{member["role"]}</TableCell> <TableCell>{member["role"]}</TableCell>
</TableRow> </TableRow>
@ -618,13 +711,13 @@ const handleEditSubmit = async (formValues: Record<string, any>) => {
</Table> </Table>
</Card> </Card>
{selectedTeam && ( {selectedTeam && (
<EditTeamModal <EditTeamModal
visible={editModalVisible} visible={editModalVisible}
onCancel={handleEditCancel} onCancel={handleEditCancel}
team={selectedTeam} team={selectedTeam}
onSubmit={handleEditSubmit} onSubmit={handleEditSubmit}
/> />
)} )}
</Col> </Col>
<Col numColSpan={1}> <Col numColSpan={1}>
<Button <Button