Create and view organizations + assign org admins on the Proxy UI (#7557)

* feat: initial commit for new 'organizations' tab on ui

* build(ui/): create generic card for rendering complete org data table

can be reused in teams as well

simplifies things

* build(ui/): display created orgs on ui

* build(ui/): support adding orgs via UI

* build(ui/): add org in selection dropdown

* build(organizations.tsx): allow assigning org admins

* build(ui/): show org members on ui

* build(ui/): cleanup + show actual models on org dropdown

* build(ui/): explain user roles within organization
This commit is contained in:
Krish Dholakia 2025-01-04 17:31:24 -08:00 committed by GitHub
parent 46d9d29bff
commit f1540ceeab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1014 additions and 6 deletions

View file

@ -16,5 +16,5 @@ model_list:
prompt_id: "jokes" prompt_id: "jokes"
litellm_settings: # litellm_settings:
callbacks: ["otel"] # callbacks: ["otel"]

View file

@ -4,6 +4,7 @@ Endpoints for /organization operations
/organization/new /organization/new
/organization/update /organization/update
/organization/delete /organization/delete
/organization/member_add
/organization/info /organization/info
/organization/list /organization/list
""" """
@ -230,7 +231,9 @@ async def list_organization(
status_code=400, status_code=400,
detail={"error": CommonProxyErrors.db_not_connected_error.value}, detail={"error": CommonProxyErrors.db_not_connected_error.value},
) )
response = await prisma_client.db.litellm_organizationtable.find_many() response = await prisma_client.db.litellm_organizationtable.find_many(
include={"members": True}
)
return response return response

View file

@ -56,6 +56,7 @@ model LiteLLM_OrganizationTable {
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
teams LiteLLM_TeamTable[] teams LiteLLM_TeamTable[]
users LiteLLM_UserTable[] users LiteLLM_UserTable[]
members LiteLLM_OrganizationMembership[] @relation("OrganizationToMembership")
} }
// Model info for teams, just has model aliases for now. // Model info for teams, just has model aliases for now.
@ -253,8 +254,11 @@ model LiteLLM_OrganizationMembership {
// relations // relations
user LiteLLM_UserTable @relation(fields: [user_id], references: [user_id]) user LiteLLM_UserTable @relation(fields: [user_id], references: [user_id])
organization LiteLLM_OrganizationTable @relation("OrganizationToMembership", fields: [organization_id], references: [organization_id])
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
@@id([user_id, organization_id]) @@id([user_id, organization_id])
@@unique([user_id, organization_id]) @@unique([user_id, organization_id])
} }

View file

@ -56,6 +56,7 @@ model LiteLLM_OrganizationTable {
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
teams LiteLLM_TeamTable[] teams LiteLLM_TeamTable[]
users LiteLLM_UserTable[] users LiteLLM_UserTable[]
members LiteLLM_OrganizationMembership[] @relation("OrganizationToMembership")
} }
// Model info for teams, just has model aliases for now. // Model info for teams, just has model aliases for now.
@ -253,8 +254,11 @@ model LiteLLM_OrganizationMembership {
// relations // relations
user LiteLLM_UserTable @relation(fields: [user_id], references: [user_id]) user LiteLLM_UserTable @relation(fields: [user_id], references: [user_id])
organization LiteLLM_OrganizationTable @relation("OrganizationToMembership", fields: [organization_id], references: [organization_id])
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
@@id([user_id, organization_id]) @@id([user_id, organization_id])
@@unique([user_id, organization_id]) @@unique([user_id, organization_id])
} }

View file

@ -6,6 +6,7 @@ import UserDashboard from "../components/user_dashboard";
import ModelDashboard from "@/components/model_dashboard"; import ModelDashboard from "@/components/model_dashboard";
import ViewUserDashboard from "@/components/view_users"; import ViewUserDashboard from "@/components/view_users";
import Teams from "@/components/teams"; import Teams from "@/components/teams";
import Organizations from "@/components/organizations";
import AdminPanel from "@/components/admins"; import AdminPanel from "@/components/admins";
import Settings from "@/components/settings"; import Settings from "@/components/settings";
import GeneralSettings from "@/components/general_settings"; import GeneralSettings from "@/components/general_settings";
@ -239,6 +240,15 @@ const CreateKeyPage = () => {
userID={userID} userID={userID}
userRole={userRole} userRole={userRole}
/> />
) : page == "organizations" ? (
<Organizations
teams={teams}
setTeams={setTeams}
searchParams={searchParams}
accessToken={accessToken}
userID={userID}
userRole={userRole}
/>
) : page == "admin-panel" ? ( ) : page == "admin-panel" ? (
<AdminPanel <AdminPanel
setTeams={setTeams} setTeams={setTeams}

View file

@ -0,0 +1,163 @@
import React from 'react';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
Text,
Badge,
Icon,
Button,
Card,
} from "@tremor/react";
import { Tooltip } from "antd";
interface Column {
header: string;
accessor: string;
cellRenderer?: (value: any, row: any) => React.ReactNode;
width?: string;
style?: React.CSSProperties;
}
interface Action<T = any> {
icon?: React.ComponentType<any>;
onClick: (item: T) => void;
condition?: () => boolean;
tooltip?: string;
}
interface DeleteModalProps {
isOpen: boolean;
onConfirm: () => void;
onCancel: () => void;
title: string;
message: string;
}
interface DataTableProps {
data: any[];
columns: Column[];
actions?: Action[];
emptyMessage?: string;
deleteModal?: DeleteModalProps;
}
const DataTable: React.FC<DataTableProps> = ({
data,
columns,
actions,
emptyMessage = "No data available",
deleteModal
}) => {
const renderCell = (column: Column, row: any) => {
const value = row[column.accessor];
if (column.cellRenderer) {
return column.cellRenderer(value, row);
}
// Default cell rendering based on value type
if (Array.isArray(value)) {
return (
<div style={{ display: "flex", flexDirection: "column" }}>
{value.length === 0 ? (
<Badge size="xs" className="mb-1" color="red">
<Text>None</Text>
</Badge>
) : (
value.map((item: any, index: number) => (
<Badge
key={index}
size="xs"
className="mb-1"
color="blue"
>
<Text>
{String(item).length > 30
? `${String(item).slice(0, 30)}...`
: item}
</Text>
</Badge>
))
)}
</div>
);
}
return value?.toString() || '';
};
return (
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[40vh]">
<Table>
<TableHead>
<TableRow>
{columns.map((column, index) => (
<TableHeaderCell key={index}>{column.header}</TableHeaderCell>
))}
{actions && actions.length > 0 && (
<TableHeaderCell>Actions</TableHeaderCell>
)}
</TableRow>
</TableHead>
<TableBody>
{data && data.length > 0 ? (
data.map((row, rowIndex) => (
<TableRow key={rowIndex}>
{columns.map((column, colIndex) => (
<TableCell
key={colIndex}
style={{
maxWidth: column.width || "4px",
whiteSpace: "pre-wrap",
overflow: "hidden",
...column.style
}}
>
{column.accessor === 'id' ? (
<Tooltip title={row[column.accessor]}>
{renderCell(column, row)}
</Tooltip>
) : (
renderCell(column, row)
)}
</TableCell>
))}
{actions && actions.length > 0 && (
<TableCell>
{actions.map((action, actionIndex) => (
action.condition?.(row) !== false && (
<Tooltip key={actionIndex} title={action.tooltip}>
<Icon
icon={action.icon}
size="sm"
onClick={() => action.onClick(row)}
className="cursor-pointer mx-1"
/>
</Tooltip>
)
))}
</TableCell>
)}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length + (actions ? 1 : 0)}>
<Text className="text-center">{emptyMessage}</Text>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Card>
);
};
export default DataTable;
export type { Action, Column, DataTableProps, DeleteModalProps };

View file

@ -33,6 +33,7 @@ const menuItems: MenuItem[] = [
{ key: "2", page: "models", label: "Models", roles: all_admin_roles }, { key: "2", page: "models", label: "Models", roles: all_admin_roles },
{ key: "4", page: "usage", label: "Usage"}, // all roles { key: "4", page: "usage", label: "Usage"}, // all roles
{ key: "6", page: "teams", label: "Teams" }, { key: "6", page: "teams", label: "Teams" },
{ key: "17", page: "organizations", label: "Organizations" },
{ key: "5", page: "users", label: "Internal Users", roles: all_admin_roles }, { key: "5", page: "users", label: "Internal Users", roles: all_admin_roles },
{ key: "8", page: "settings", label: "Logging & Alerts", roles: all_admin_roles }, { key: "8", page: "settings", label: "Logging & Alerts", roles: all_admin_roles },
{ key: "9", page: "caching", label: "Caching", roles: all_admin_roles }, { key: "9", page: "caching", label: "Caching", roles: all_admin_roles },

View file

@ -742,6 +742,70 @@ export const teamListCall = async (
} }
}; };
export const organizationListCall = async (accessToken: String) => {
/**
* Get all organizations on proxy
*/
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/organization/list` : `/organization/list`;
const response = await fetch(url, {
method: "GET",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.text();
handleError(errorData);
throw new Error("Network response was not ok");
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to create key:", error);
throw error;
}
};
export const organizationCreateCall = async (
accessToken: string,
formValues: Record<string, any> // Assuming formValues is an object
) => {
try {
console.log("Form Values in organizationCreateCall:", formValues); // Log the form values before making the API call
const url = proxyBaseUrl ? `${proxyBaseUrl}/organization/new` : `/organization/new`;
const response = await fetch(url, {
method: "POST",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
...formValues, // Include formValues in the request body
}),
});
if (!response.ok) {
const errorData = await response.text();
handleError(errorData);
console.error("Error response from the server:", errorData);
throw new Error("Network response was not ok");
}
const data = await response.json();
console.log("API Response:", 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;
}
};
export const getTotalSpendCall = async (accessToken: String) => { export const getTotalSpendCall = async (accessToken: String) => {
/** /**
* Get all models on proxy * Get all models on proxy
@ -2203,6 +2267,46 @@ export const teamMemberAddCall = async (
} }
}; };
export const organizationMemberAddCall = async (
accessToken: string,
organizationId: string,
formValues: Member // Assuming formValues is an object
) => {
try {
console.log("Form Values in teamMemberAddCall:", formValues); // Log the form values before making the API call
const url = proxyBaseUrl
? `${proxyBaseUrl}/organization/member_add`
: `/organization/member_add`;
const response = await fetch(url, {
method: "POST",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
organization_id: organizationId,
member: formValues, // Include formValues in the request body
}),
});
if (!response.ok) {
const errorData = await response.text();
handleError(errorData);
console.error("Error response from the server:", errorData);
throw new Error(errorData);
}
const data = await response.json();
console.log("API Response:", 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 organization member:", error);
throw error;
}
};
export const userUpdateUserCall = async ( export const userUpdateUserCall = async (
accessToken: string, accessToken: string,
formValues: any, // Assuming formValues is an object formValues: any, // Assuming formValues is an object

View file

@ -0,0 +1,184 @@
import {
Button as Button2,
Modal,
Form,
Select as Select2,
InputNumber,
message,
} from "antd";
import {
TextInput,
Button,
} from "@tremor/react";
import { organizationCreateCall } from "../networking";
// types.ts
export interface FormData {
name: string;
models: string[];
maxBudget: number | null;
budgetDuration: string | null;
tpmLimit: number | null;
rpmLimit: number | null;
}
export interface OrganizationFormProps {
title?: string;
onCancel?: () => void;
accessToken: string | null;
availableModels?: string[];
initialValues?: Partial<FormData>;
submitButtonText?: string;
modelSelectionType?: 'single' | 'multiple';
}
// OrganizationForm.tsx
import React, { useState } from 'react';
const onSubmit = async (formValues: Record<string, any>, accessToken: string | null, setIsModalVisible: any) => {
if (accessToken == null) {
return;
}
try {
message.info("Creating Organization");
console.log("formValues: " + JSON.stringify(formValues));
const response: any = await organizationCreateCall(accessToken, formValues);
console.log(`response for organization create call: ${response}`);
message.success("Organization created");
sessionStorage.removeItem('organizations');
setIsModalVisible(false);
} catch (error) {
console.error("Error creating the organization:", error);
message.error("Error creating the organization: " + error, 20);
}
}
const OrganizationForm: React.FC<OrganizationFormProps> = ({
title = "Create Organization",
onCancel,
accessToken,
availableModels = [],
initialValues = {},
submitButtonText = "Create",
modelSelectionType = "multiple",
}) => {
const [form] = Form.useForm();
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
const [formData, setFormData] = useState<FormData>({
name: initialValues.name || '',
models: initialValues.models || [],
maxBudget: initialValues.maxBudget || null,
budgetDuration: initialValues.budgetDuration || null,
tpmLimit: initialValues.tpmLimit || null,
rpmLimit: initialValues.rpmLimit || null
});
console.log(`availableModels: ${availableModels}`)
const handleSubmit = async (formValues: Record<string, any>) => {
if (accessToken == null) {
return;
}
await onSubmit(formValues, accessToken, setIsModalVisible);
setIsModalVisible(false);
};
const handleCancel = (): void => {
setIsModalVisible(false);
if (onCancel) onCancel();
};
return (
<div className="w-full">
<Button
onClick={() => setIsModalVisible(true)}
className="mx-auto"
type="button"
>
+ Create New {title}
</Button>
<Modal
title={`Create ${title}`}
visible={isModalVisible}
width={800}
footer={null}
onCancel={handleCancel}
>
<Form
form={form}
onFinish={handleSubmit}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
>
<>
<Form.Item
label={`${title} Name`}
name="organization_alias"
rules={[
{ required: true, message: `Please input a ${title} name` },
]}
>
<TextInput placeholder="" />
</Form.Item>
<Form.Item label="Models" name="models">
<Select2
mode="multiple"
placeholder="Select models"
style={{ width: "100%" }}
>
<Select2.Option
key="all-proxy-models"
value="all-proxy-models"
>
All Proxy Models
</Select2.Option>
{availableModels.map((model) => (
<Select2.Option key={model} value={model}>
{model}
</Select2.Option>
))}
</Select2>
</Form.Item>
<Form.Item label="Max Budget (USD)" name="max_budget">
<InputNumber step={0.01} precision={2} width={200} />
</Form.Item>
<Form.Item
className="mt-8"
label="Reset Budget"
name="budget_duration"
>
<Select2 defaultValue={null} placeholder="n/a">
<Select2.Option value="24h">daily</Select2.Option>
<Select2.Option value="7d">weekly</Select2.Option>
<Select2.Option value="30d">monthly</Select2.Option>
</Select2>
</Form.Item>
<Form.Item
label="Tokens per minute Limit (TPM)"
name="tpm_limit"
>
<InputNumber step={1} width={400} />
</Form.Item>
<Form.Item
label="Requests per minute Limit (RPM)"
name="rpm_limit"
>
<InputNumber step={1} width={400} />
</Form.Item>
</>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 htmlType="submit">{submitButtonText}</Button2>
</div>
</Form>
</Modal>
</div>
);
};
export default OrganizationForm;

View file

@ -0,0 +1,149 @@
/**
* This component is used to add an admin to an organization.
*/
import React, { FC } from 'react';
import {
Button,
Select,
Col,
Text
} from "@tremor/react";
import {
Button as Button2,
Select as Select2,
Modal,
Form,
Input,
} from "antd";
import { Organization } from "@/components/organization/types";
interface AddOrgAdminProps {
userRole: string;
userID: string;
selectedOrganization?: Organization;
onMemberAdd?: (formValues: Record<string, any>) => void;
}
const is_org_admin = (organization: any, userID: string) => {
for (let i = 0; i < organization.members_with_roles.length; i++) {
let member = organization.members_with_roles[i];
if (member.user_id == userID && member.role == "admin") {
return true;
}
}
return false;
}
const AddOrgAdmin: FC<AddOrgAdminProps> = ({
userRole,
userID,
selectedOrganization,
onMemberAdd
}) => {
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = React.useState(false);
const [form] = Form.useForm();
const handleMemberCancel = () => {
form.resetFields();
setIsAddMemberModalVisible(false);
};
const handleMemberOk = () => {
form.submit();
};
return (
<Col numColSpan={1}>
{userRole === "Admin" || (selectedOrganization && is_org_admin(selectedOrganization, userID)) ? (
<Button
className="mx-auto mb-5"
onClick={() => setIsAddMemberModalVisible(true)}
>
+ Add member
</Button>
) : null}
<Modal
title="Add member"
visible={isAddMemberModalVisible}
width={800}
footer={null}
onOk={handleMemberOk}
onCancel={handleMemberCancel}
>
<Text className='mb-2'>
User must exist in proxy. Get User ID from 'Users' tab.
</Text>
<Form
form={form}
onFinish={onMemberAdd}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
initialValues={{
role: "internal_user",
}}
>
<Form.Item label="Email" name="user_email" className="mb-4">
<Input
name="user_email"
className="px-3 py-2 border rounded-md w-full"
/>
</Form.Item>
<div className="text-center mb-4">OR</div>
<Form.Item label="User ID" name="user_id" className="mb-4">
<Input
name="user_id"
className="px-3 py-2 border rounded-md w-full"
/>
</Form.Item>
<Form.Item label="Member Role" name="role" className="mb-4">
<Select2 defaultValue="user">
<Select2.Option value="org_admin"><div className="flex">
Org Admin{" "}
<p
className="ml-2"
style={{ color: "gray", fontSize: "12px" }}
>
Can add and remove members, and change their roles.
</p>
</div>
</Select2.Option>
<Select2.Option value="internal_user">
<div className="flex">
Internal User{" "}
<p
className="ml-2"
style={{ color: "gray", fontSize: "12px" }}
>
Can view/create keys for themselves within organization.
</p>
</div>
</Select2.Option>
<Select2.Option value="internal_user_viewer">
<div className="flex">
Internal User Viewer{" "}
<p
className="ml-2"
style={{ color: "gray", fontSize: "12px" }}
>
Can only view their keys within organization.
</p>
</div>
</Select2.Option>
</Select2>
</Form.Item>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 htmlType="submit">Add member</Button2>
</div>
</Form>
</Modal>
</Col>
);
};
export default AddOrgAdmin;

View file

@ -0,0 +1,25 @@
import { Member } from "../networking";
export interface EditModalProps {
visible: boolean;
onCancel: () => void;
entity: Organization;
onSubmit: (entity: Organization) => void;
}
interface OrganizationMember {
user_id: string;
user_role: string;
}
export interface Organization {
organization_id: string;
organization_name: string;
spend: number;
max_budget: number | null;
models: string[];
tpm_limit: number | null;
rpm_limit: number | null;
members: OrganizationMember[] | null;
}

View file

@ -0,0 +1,88 @@
import React, { FC } from 'react';
import { Organization, EditModalProps, OrganizationMember } from "./types";
import {
TextInput,
Button,
Card,
Col,
Table,
TableHead,
TableHeaderCell,
TableBody,
TableRow,
TableCell,
} from "@tremor/react";
interface Member {
user_email?: string;
user_id?: string;
role: string;
}
interface MemberListTableProps {
selectedEntity?: Organization;
onEditSubmit: (entity: Organization) => void;
editModalComponent: React.ComponentType<EditModalProps>;
entityType: 'team' | 'organization';
}
const MemberListTable: FC<MemberListTableProps> = ({
selectedEntity,
onEditSubmit,
editModalComponent: EditModal,
entityType
}) => {
const [editModalVisible, setEditModalVisible] = React.useState(false);
const handleEditCancel = () => {
setEditModalVisible(false);
};
const handleEditSubmit = (entity: Organization) => {
onEditSubmit(entity);
setEditModalVisible(false);
};
const getMemberIdentifier = (member: Member) => {
return member.user_email || member.user_id || 'Unknown Member';
};
return (
<Col numColSpan={1}>
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]">
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>
{entityType === 'team' ? 'Team Member' : 'Organization Member'}
</TableHeaderCell>
<TableHeaderCell>Role</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{(selectedEntity?.members ?? []).map((value: OrganizationMember, index: number) => (
<TableRow key={`${value.user_id}-${index}`}>
<TableCell>{value.user_id}</TableCell>
<TableCell>{value.user_role}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
{selectedEntity && (
<EditModal
visible={editModalVisible}
onCancel={handleEditCancel}
entity={selectedEntity}
onSubmit={handleEditSubmit}
/>
)}
</Col>
);
};
export default MemberListTable;

View file

@ -0,0 +1,273 @@
import React, { useState, useEffect } from "react";
import { organizationListCall, organizationMemberAddCall, Member, modelAvailableCall } from "./networking";
import {
Col,
Grid,
} from "@tremor/react";
import OrganizationForm from "@/components/organization/add_org";
import AddOrgAdmin from "@/components/organization/add_org_admin";
import MemberListTable from "@/components/organization/view_members_of_org";
import { Select, SelectItem } from "@tremor/react";
const isLocal = process.env.NODE_ENV === "development";
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
if (isLocal != true) {
console.log = function() {};
}
interface TeamProps {
teams: any[] | null;
searchParams: any;
accessToken: string | null;
setTeams: React.Dispatch<React.SetStateAction<Object[] | null>>;
userID: string | null;
userRole: string | null;
}
import DataTable from "@/components/common_components/all_view";
import { Action } from "@/components/common_components/all_view";
import { Typography, message } from "antd";
import { Organization, EditModalProps } from "@/components/organization/types";
interface OrganizationsTableProps {
organizations: Organization[];
userRole?: string;
onEdit?: (organization: Organization) => void;
onDelete?: (organization: Organization) => void;
isDeleteModalOpen: boolean;
setIsDeleteModalOpen: (value: boolean) => void;
selectedOrganization: Organization | null;
setSelectedOrganization: (value: Organization | null) => void;
}
const EditOrganizationModal: React.FC<EditModalProps> = ({
visible,
onCancel,
entity,
onSubmit
}) => {
return <div/>;
};
// Inside your Teams component
const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
organizations,
userRole,
onEdit,
onDelete,
isDeleteModalOpen,
setIsDeleteModalOpen,
selectedOrganization,
setSelectedOrganization
}) => {
const columns = [
{
header: "Organization Name",
accessor: "organization_alias",
width: "4px",
style: {
whiteSpace: "pre-wrap",
overflow: "hidden"
}
},
{
header: "Organization ID",
accessor: "organization_id",
width: "4px",
style: {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
fontSize: "0.75em"
}
},
{
header: "Spend (USD)",
accessor: "spend"
},
{
header: "Budget (USD)",
accessor: "max_budget",
cellRenderer: (value: number | null) =>
value !== null && value !== undefined ? value : "No limit"
},
{
header: "Models",
accessor: "models"
},
{
header: "TPM / RPM Limits",
accessor: "limits",
cellRenderer: (value: any, row: Organization) => (
<div className="text-sm">
<span>TPM: {row.tpm_limit ? row.tpm_limit : "Unlimited"}</span>
<br />
<span>RPM: {row.rpm_limit ? row.rpm_limit : "Unlimited"}</span>
</div>
)
},
{
header: "Info",
accessor: "info",
cellRenderer: (value: any, row: Organization) => (
<div className="space-y-1">
<div className="text-sm">
{row.members?.length || 0} Members
</div>
</div>
)
}
];
const actions: Action[] = [
...(onEdit && userRole === "Admin" ? [{
icon: undefined, // Replace with your PencilAltIcon
onClick: (org: Organization) => onEdit(org),
condition: () => userRole === "Admin",
tooltip: "Edit organization"
}] : []),
...(onDelete && userRole === "Admin" ? [{
icon: undefined, // Replace with your TrashIcon
onClick: (org: Organization) => onDelete(org),
condition: () => userRole === "Admin",
tooltip: "Delete organization"
}] : [])
];
return (
<DataTable
data={organizations}
columns={columns}
actions={actions}
emptyMessage="No organizations available"
deleteModal={{
isOpen: isDeleteModalOpen,
onConfirm: () => {
if (selectedOrganization && onDelete) {
onDelete(selectedOrganization);
}
setIsDeleteModalOpen(false);
setSelectedOrganization(null);
},
onCancel: () => {
setIsDeleteModalOpen(false);
setSelectedOrganization(null);
},
title: "Delete Organization",
message: "Are you sure you want to delete this organization?"
}}
/>
);
};
const Organizations: React.FC<TeamProps> = ({
accessToken,
userID,
userRole,
}) => {
const [organizations, setOrganizations] = useState<Organization[]>([]);
const { Title, Paragraph } = Typography;
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedOrganization, setSelectedOrganization] = useState<Organization | null>(null);
const [userModels, setUserModels] = useState([]);
useEffect(() => {
if (!accessToken || !userID || !userRole) return;
const fetchUserModels = async () => {
try {
const model_available = await modelAvailableCall(
accessToken,
userID,
userRole
);
let available_model_names = model_available["data"].map(
(element: { id: string }) => element.id
);
console.log("available_model_names:", available_model_names);
setUserModels(available_model_names);
} catch (error) {
console.error("Error fetching user models:", error);
}
};
const fetchData = async () => {
let givenOrganizations;
givenOrganizations = await organizationListCall(accessToken)
console.log(`givenOrganizations: ${givenOrganizations}`)
setOrganizations(givenOrganizations)
sessionStorage.setItem('organizations', JSON.stringify(givenOrganizations));
}
fetchUserModels()
fetchData()
}, [accessToken]);
const handleMemberCreate = async (formValues: Record<string, any>) => {
if (!selectedOrganization || !accessToken) return;
try {
let member: Member = {
user_email: formValues.user_email,
user_id: formValues.user_id,
role: formValues.role
}
await organizationMemberAddCall(
accessToken,
selectedOrganization["organization_id"],
member
);
message.success("Member added");
} catch (error) {
console.error("Error creating the team:", error);
message.error("Error creating the organization: " + error);
}
};
return (
<div className="w-full mx-4">
<Grid numItems={1} className="gap-2 p-8 h-[75vh] w-full mt-2">
<Col numColSpan={1}>
<Title level={4}>All Organizations</Title>
{userRole ? OrganizationsTable({organizations, userRole, isDeleteModalOpen, setIsDeleteModalOpen, selectedOrganization, setSelectedOrganization}) : null}
</Col>
{userRole == "Admin" && accessToken ? <OrganizationForm
title="Organization"
accessToken={accessToken}
availableModels={userModels}
submitButtonText="Create Organization"
/> : null}
<Col numColSpan={1}>
<Title level={4}>Organization Members</Title>
<Paragraph>
If you belong to multiple organizations, this setting controls which organizations'
members you see.
</Paragraph>
{organizations && organizations.length > 0 ? (
<Select>
{organizations.map((organization: any, index) => (
<SelectItem
key={index}
value={String(index)}
onClick={() => {
setSelectedOrganization(organization);
}}
>
{organization["organization_alias"]}
</SelectItem>
))}
</Select>
) : (
<Paragraph>
No team created. <b>Defaulting to personal account.</b>
</Paragraph>
)}
</Col>
{userRole == "Admin" && userID && selectedOrganization ? <AddOrgAdmin userRole={userRole} userID={userID} selectedOrganization={selectedOrganization} onMemberAdd={handleMemberCreate} /> : null}
{userRole == "Admin" && userID && selectedOrganization ? <MemberListTable selectedEntity={selectedOrganization} onEditSubmit={() => {}} editModalComponent={EditOrganizationModal} entityType="organization" /> : null}
</Grid>
</div>
);
};
export default Organizations;