mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-25 10:44:24 +00:00
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:
parent
46d9d29bff
commit
f1540ceeab
14 changed files with 1014 additions and 6 deletions
|
@ -16,5 +16,5 @@ model_list:
|
||||||
prompt_id: "jokes"
|
prompt_id: "jokes"
|
||||||
|
|
||||||
|
|
||||||
litellm_settings:
|
# litellm_settings:
|
||||||
callbacks: ["otel"]
|
# callbacks: ["otel"]
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 };
|
|
@ -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 },
|
||||||
|
|
|
@ -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
|
||||||
|
|
184
ui/litellm-dashboard/src/components/organization/add_org.tsx
Normal file
184
ui/litellm-dashboard/src/components/organization/add_org.tsx
Normal 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;
|
|
@ -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;
|
25
ui/litellm-dashboard/src/components/organization/types.tsx
Normal file
25
ui/litellm-dashboard/src/components/organization/types.tsx
Normal 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;
|
||||||
|
}
|
|
@ -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;
|
273
ui/litellm-dashboard/src/components/organizations.tsx
Normal file
273
ui/litellm-dashboard/src/components/organizations.tsx
Normal 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;
|
Loading…
Add table
Add a link
Reference in a new issue