mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
feat(ui): migrate tool policies and organizations to shadcn
Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
511049458d
commit
0fd4379e32
4 changed files with 549 additions and 364 deletions
|
|
@ -24,6 +24,8 @@
|
|||
"src/components/molecules/notifications_manager.tsx",
|
||||
"src/components/molecules/message_manager.tsx",
|
||||
"src/components/ToolPolicies/PolicySelect.tsx",
|
||||
"src/components/ToolDetail.tsx",
|
||||
"src/components/ToolPolicies.tsx",
|
||||
"src/components/GuardrailsMonitor/GuardrailConfig.tsx",
|
||||
"src/components/GuardrailsMonitor/GuardrailDetail.tsx",
|
||||
"src/components/GuardrailsMonitor/LogViewer.tsx",
|
||||
|
|
|
|||
|
|
@ -2,8 +2,16 @@
|
|||
|
||||
import { ArrowLeft as ArrowLeftOutlined, History as HistoryOutlined, Wrench as ToolOutlined } from "lucide-react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button, Select, Spin } from "antd";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import TeamDropdown from "@/components/common_components/team_dropdown";
|
||||
import { LogViewer } from "@/components/GuardrailsMonitor/LogViewer";
|
||||
import type { LogEntry } from "@/components/GuardrailsMonitor/mockData";
|
||||
|
|
@ -16,7 +24,6 @@ import {
|
|||
keyListCall,
|
||||
teamListCall,
|
||||
updateToolPolicy,
|
||||
type ToolPolicyOption,
|
||||
type ToolPolicyOverrideRow,
|
||||
} from "@/components/networking";
|
||||
import type { Team } from "@/components/key_team_helpers/key_list";
|
||||
|
|
@ -210,7 +217,7 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
|
|||
if (detailLoading && !detail) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Spin size="large" />
|
||||
<Skeleton className="h-10 w-32" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -218,9 +225,11 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
|
|||
if (detailError && !detail) {
|
||||
return (
|
||||
<div>
|
||||
<Button type="link" icon={<ArrowLeftOutlined />} onClick={onBack} className="pl-0 mb-4">
|
||||
<Button variant="link" onClick={onBack} className="pl-0 mb-4">
|
||||
<ArrowLeftOutlined className="mr-1 h-4 w-4" />
|
||||
Back to Tool Policies
|
||||
</Button>
|
||||
{/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */}
|
||||
<p className="text-red-600">Failed to load tool details.</p>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -243,11 +252,11 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
|
|||
<div>
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
variant="link"
|
||||
onClick={onBack}
|
||||
className="pl-0 mb-4"
|
||||
>
|
||||
<ArrowLeftOutlined className="mr-1 h-4 w-4" />
|
||||
Back to Tool Policies
|
||||
</Button>
|
||||
|
||||
|
|
@ -341,9 +350,9 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
|
|||
{!ov.team_id && !ov.key_hash ? "—" : ""}
|
||||
</span>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="text-destructive"
|
||||
disabled={overrideSaving}
|
||||
onClick={() => handleRemoveOverride(ov)}
|
||||
>
|
||||
|
|
@ -392,29 +401,28 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
|
|||
/>
|
||||
) : (
|
||||
<Select
|
||||
placeholder="Select key"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
value={blockKey ? blockKey.token : undefined}
|
||||
onChange={(token) => {
|
||||
value={blockKey ? blockKey.token : ""}
|
||||
onValueChange={(token) => {
|
||||
const k = keys.find((x) => x.token === token);
|
||||
setBlockKey(k ?? null);
|
||||
}}
|
||||
options={keys.map((k) => ({
|
||||
value: k.token,
|
||||
label: k.key_alias || k.token?.substring?.(0, 12) || k.token,
|
||||
}))}
|
||||
className="w-full"
|
||||
style={{ minWidth: 200 }}
|
||||
/>
|
||||
>
|
||||
<SelectTrigger className="w-full min-w-[200px]">
|
||||
<SelectValue placeholder="Select key" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{keys.map((k) => (
|
||||
<SelectItem key={k.token} value={k.token}>
|
||||
{k.key_alias || k.token?.substring?.(0, 12) || k.token}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
variant="destructive"
|
||||
disabled={overrideSaving || (blockScope === "team" ? !blockTeamId : !blockKey?.token)}
|
||||
loading={overrideSaving}
|
||||
onClick={handleAddOverride}
|
||||
>
|
||||
Block for {blockScope}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import React, { useCallback, useDeferredValue, useEffect, useMemo, useState } from "react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
|
||||
import { Button, Switch, Tooltip } from "antd";
|
||||
import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react";
|
||||
import { TimeCell } from "./view_logs/time_cell";
|
||||
import type { SortState } from "./common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
|
||||
|
|
@ -53,6 +59,23 @@ interface ToolPoliciesProps {
|
|||
onSelectTool?: (toolName: string) => void;
|
||||
}
|
||||
|
||||
function TT({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>{children}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{title}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelectTool }) => {
|
||||
const [tools, setTools] = useState<ToolRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -263,6 +286,7 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="w-full">
|
||||
<h1 className="text-2xl font-semibold text-gray-900 mb-6">Tool Policies</h1>
|
||||
|
||||
|
|
@ -348,7 +372,7 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-900">Live Tail</span>
|
||||
<Switch checked={isLiveTail} onChange={setIsLiveTail} />
|
||||
<Switch checked={isLiveTail} onCheckedChange={setIsLiveTail} />
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
@ -476,9 +500,9 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
onClick={() => onSelectTool?.(tool.tool_name)}
|
||||
className="text-left w-full font-mono text-xs max-w-[20ch] truncate block font-medium text-blue-600 hover:text-blue-800 hover:underline focus:outline-none focus:ring-0"
|
||||
>
|
||||
<Tooltip title={onSelectTool ? "Click to view details and block for team/key" : tool.tool_name}>
|
||||
<TT title={onSelectTool ? "Click to view details and block for team/key" : tool.tool_name}>
|
||||
<span>{tool.tool_name}</span>
|
||||
</Tooltip>
|
||||
</TT>
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell className="py-0.5 max-h-8">
|
||||
|
|
@ -505,26 +529,26 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden whitespace-nowrap">
|
||||
<Tooltip title={tool.team_id ?? "-"}>
|
||||
<TT title={tool.team_id ?? "-"}>
|
||||
<span className="max-w-[15ch] truncate block">{tool.team_id ?? "-"}</span>
|
||||
</Tooltip>
|
||||
</TT>
|
||||
</TableCell>
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden whitespace-nowrap">
|
||||
<Tooltip title={tool.key_hash ?? "-"}>
|
||||
<TT title={tool.key_hash ?? "-"}>
|
||||
<span className="font-mono max-w-[15ch] truncate block text-blue-600">
|
||||
{tool.key_hash ?? "-"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</TT>
|
||||
</TableCell>
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden whitespace-nowrap">
|
||||
<Tooltip title={tool.key_alias ?? "-"}>
|
||||
<TT title={tool.key_alias ?? "-"}>
|
||||
<span className="max-w-[15ch] truncate block">{tool.key_alias ?? "-"}</span>
|
||||
</Tooltip>
|
||||
</TT>
|
||||
</TableCell>
|
||||
<TableCell className="py-0.5 max-h-8 overflow-hidden whitespace-nowrap">
|
||||
<Tooltip title={tool.user_agent ?? "-"}>
|
||||
<TT title={tool.user_agent ?? "-"}>
|
||||
<span className="font-mono max-w-[20ch] truncate block text-xs text-gray-500">{tool.user_agent ?? "-"}</span>
|
||||
</Tooltip>
|
||||
</TT>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
|
|
@ -559,6 +583,7 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
</div>
|
||||
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,52 @@
|
|||
import OrganizationFilters, { FilterState } from "@/app/(dashboard)/organizations/OrganizationFilters";
|
||||
import { Info as InfoCircleOutlined } from "lucide-react";
|
||||
import { ChevronDown as ChevronDownIcon, ChevronRight as ChevronRightIcon, RefreshCcw as RefreshIcon } from "lucide-react";
|
||||
import {
|
||||
ChevronDown as ChevronDownIcon,
|
||||
ChevronRight as ChevronRightIcon,
|
||||
RefreshCcw as RefreshIcon,
|
||||
} from "lucide-react";
|
||||
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Grid,
|
||||
Icon,
|
||||
Tab,
|
||||
TabGroup,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@tremor/react";
|
||||
import { Form, Input, Modal, Select as Select2, Tooltip } from "antd";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import React, { useState } from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { formatNumberWithCommas } from "../utils/dataUtils";
|
||||
import DeleteResourceModal from "./common_components/DeleteResourceModal";
|
||||
import TableIconActionButton from "./common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
|
||||
|
|
@ -50,6 +72,18 @@ interface OrganizationsTableProps {
|
|||
premiumUser: boolean;
|
||||
}
|
||||
|
||||
interface OrgFormValues {
|
||||
organization_alias: string;
|
||||
models: string[];
|
||||
max_budget: number | null;
|
||||
budget_duration: string;
|
||||
tpm_limit: number | null;
|
||||
rpm_limit: number | null;
|
||||
allowed_vector_store_ids: string[];
|
||||
allowed_mcp_servers_and_groups: { servers?: string[]; accessGroups?: string[] } | null;
|
||||
metadata: string;
|
||||
}
|
||||
|
||||
export const fetchOrganizations = async (
|
||||
accessToken: string,
|
||||
setOrganizations: (organizations: Organization[]) => void,
|
||||
|
|
@ -72,13 +106,28 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
|
|||
setOrganizations,
|
||||
premiumUser,
|
||||
}) => {
|
||||
void userModels;
|
||||
void currentOrg;
|
||||
void guardrailsList;
|
||||
const [selectedOrgId, setSelectedOrgId] = useState<string | null>(null);
|
||||
const [editOrg, setEditOrg] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [orgToDelete, setOrgToDelete] = useState<string | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isOrgModalVisible, setIsOrgModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const form = useForm<OrgFormValues>({
|
||||
defaultValues: {
|
||||
organization_alias: "",
|
||||
models: [],
|
||||
max_budget: null,
|
||||
budget_duration: "",
|
||||
tpm_limit: null,
|
||||
rpm_limit: null,
|
||||
allowed_vector_store_ids: [],
|
||||
allowed_mcp_servers_and_groups: null,
|
||||
metadata: "",
|
||||
},
|
||||
});
|
||||
const [expandedAccordions, setExpandedAccordions] = useState<Record<string, boolean>>({});
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [filters, setFilters] = useState<FilterState>({
|
||||
|
|
@ -91,7 +140,6 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
|
|||
const handleFilterChange = (key: keyof FilterState, value: string) => {
|
||||
const newFilters = { ...filters, [key]: value };
|
||||
setFilters(newFilters);
|
||||
// Call organizationListCall with the new filters
|
||||
if (accessToken) {
|
||||
organizationListCall(accessToken, newFilters.org_id || null, newFilters.org_alias || null)
|
||||
.then((response) => {
|
||||
|
|
@ -112,7 +160,6 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
|
|||
sort_by: "created_at",
|
||||
sort_order: "desc",
|
||||
});
|
||||
// Reset organizations list
|
||||
if (accessToken) {
|
||||
organizationListCall(accessToken, null, null)
|
||||
.then((response) => {
|
||||
|
|
@ -128,7 +175,6 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
|
|||
|
||||
const handleDelete = (orgId: string | null) => {
|
||||
if (!orgId) return;
|
||||
|
||||
setOrgToDelete(orgId);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
|
@ -143,7 +189,6 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
|
|||
|
||||
setIsDeleteModalOpen(false);
|
||||
setOrgToDelete(null);
|
||||
// Refresh organizations list
|
||||
await fetchOrganizations(accessToken, setOrganizations, filters.org_id || null, filters.org_alias || null);
|
||||
} catch (error) {
|
||||
console.error("Error deleting organization:", error);
|
||||
|
|
@ -157,69 +202,68 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
|
|||
setOrgToDelete(null);
|
||||
};
|
||||
|
||||
const handleCreate = async (values: any) => {
|
||||
const handleCreate = form.handleSubmit(async (values) => {
|
||||
try {
|
||||
if (!accessToken) return;
|
||||
|
||||
console.log(`values in organizations new create call: ${JSON.stringify(values)}`);
|
||||
const payload: Record<string, any> = { ...values };
|
||||
|
||||
// Transform allowed_vector_store_ids and allowed_mcp_servers_and_groups into object_permission
|
||||
if (
|
||||
(values.allowed_vector_store_ids && values.allowed_vector_store_ids.length > 0) ||
|
||||
(values.allowed_mcp_servers_and_groups &&
|
||||
(values.allowed_mcp_servers_and_groups.servers?.length > 0 ||
|
||||
values.allowed_mcp_servers_and_groups.accessGroups?.length > 0))
|
||||
((values.allowed_mcp_servers_and_groups.servers?.length ?? 0) > 0 ||
|
||||
(values.allowed_mcp_servers_and_groups.accessGroups?.length ?? 0) > 0))
|
||||
) {
|
||||
values.object_permission = {};
|
||||
payload.object_permission = {};
|
||||
if (values.allowed_vector_store_ids && values.allowed_vector_store_ids.length > 0) {
|
||||
values.object_permission.vector_stores = values.allowed_vector_store_ids;
|
||||
delete values.allowed_vector_store_ids;
|
||||
payload.object_permission.vector_stores = values.allowed_vector_store_ids;
|
||||
delete payload.allowed_vector_store_ids;
|
||||
}
|
||||
if (values.allowed_mcp_servers_and_groups) {
|
||||
if (values.allowed_mcp_servers_and_groups.servers?.length > 0) {
|
||||
values.object_permission.mcp_servers = values.allowed_mcp_servers_and_groups.servers;
|
||||
if ((values.allowed_mcp_servers_and_groups.servers?.length ?? 0) > 0) {
|
||||
payload.object_permission.mcp_servers = values.allowed_mcp_servers_and_groups.servers;
|
||||
}
|
||||
if (values.allowed_mcp_servers_and_groups.accessGroups?.length > 0) {
|
||||
values.object_permission.mcp_access_groups = values.allowed_mcp_servers_and_groups.accessGroups;
|
||||
if ((values.allowed_mcp_servers_and_groups.accessGroups?.length ?? 0) > 0) {
|
||||
payload.object_permission.mcp_access_groups = values.allowed_mcp_servers_and_groups.accessGroups;
|
||||
}
|
||||
delete values.allowed_mcp_servers_and_groups;
|
||||
delete payload.allowed_mcp_servers_and_groups;
|
||||
}
|
||||
}
|
||||
|
||||
await organizationCreateCall(accessToken, values);
|
||||
await organizationCreateCall(accessToken, payload);
|
||||
NotificationsManager.success("Organization created successfully");
|
||||
setIsOrgModalVisible(false);
|
||||
form.resetFields();
|
||||
// Refresh organizations list
|
||||
form.reset();
|
||||
fetchOrganizations(accessToken, setOrganizations, filters.org_id || null, filters.org_alias || null);
|
||||
} catch (error) {
|
||||
console.error("Error creating organization:", error);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsOrgModalVisible(false);
|
||||
form.resetFields();
|
||||
form.reset();
|
||||
};
|
||||
|
||||
if (!premiumUser) {
|
||||
return (
|
||||
<div>
|
||||
<Text>
|
||||
<p className="text-sm">
|
||||
This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key{" "}
|
||||
<a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer">
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="w-full mx-4 h-[75vh]">
|
||||
<Grid numItems={1} className="gap-2 p-8 w-full mt-2">
|
||||
<Col numColSpan={1} className="flex flex-col gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 p-8 w-full mt-2">
|
||||
<div className="col-span-1 flex flex-col gap-2">
|
||||
{(userRole === "Admin" || userRole === "Org Admin") && (
|
||||
<Button className="w-fit" onClick={() => setIsOrgModalVisible(true)}>
|
||||
+ Create New Organization
|
||||
|
|
@ -233,324 +277,429 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
|
|||
setEditOrg(false);
|
||||
}}
|
||||
accessToken={accessToken}
|
||||
is_org_admin={true} // You'll need to implement proper org admin check
|
||||
is_org_admin={true}
|
||||
is_proxy_admin={userRole === "Admin"}
|
||||
userModels={userModels}
|
||||
editOrg={editOrg}
|
||||
/>
|
||||
) : (
|
||||
<TabGroup className="gap-2 h-[75vh] w-full">
|
||||
<TabList className="flex justify-between mt-2 w-full items-center">
|
||||
<Tabs defaultValue="your-organizations" className="gap-2 h-[75vh] w-full">
|
||||
<TabsList className="flex justify-between mt-2 w-full items-center">
|
||||
<div className="flex">
|
||||
<Tab>Your Organizations</Tab>
|
||||
<TabsTrigger value="your-organizations">Your Organizations</TabsTrigger>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
|
||||
<Icon
|
||||
icon={RefreshIcon}
|
||||
variant="shadow"
|
||||
size="xs"
|
||||
{lastRefreshed && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Last Refreshed: {lastRefreshed}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="self-center"
|
||||
onClick={handleRefreshClick}
|
||||
/>
|
||||
aria-label="Refresh"
|
||||
>
|
||||
<RefreshIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Text>Click on “Organization ID” to view organization details.</Text>
|
||||
<Grid numItems={1} className="gap-2 pt-2 pb-2 h-[75vh] w-full mt-2">
|
||||
<Col numColSpan={1}>
|
||||
<Card className="w-full mx-auto flex-auto overflow-hidden overflow-y-auto max-h-[50vh]">
|
||||
<div className="border-b px-6 py-4">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<OrganizationFilters
|
||||
filters={filters}
|
||||
showFilters={showFilters}
|
||||
onToggleFilters={setShowFilters}
|
||||
onChange={handleFilterChange}
|
||||
onReset={handleFilterReset}
|
||||
/>
|
||||
</div>
|
||||
</TabsList>
|
||||
<TabsContent value="your-organizations">
|
||||
<p className="text-sm">
|
||||
Click on “Organization ID” to view organization details.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-2 pt-2 pb-2 h-[75vh] w-full mt-2">
|
||||
<div className="col-span-1">
|
||||
<Card className="w-full mx-auto flex-auto overflow-hidden overflow-y-auto max-h-[50vh]">
|
||||
<div className="border-b border-border px-6 py-4">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<OrganizationFilters
|
||||
filters={filters}
|
||||
showFilters={showFilters}
|
||||
onToggleFilters={setShowFilters}
|
||||
onChange={handleFilterChange}
|
||||
onReset={handleFilterReset}
|
||||
/>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Organization ID</TableHeaderCell>
|
||||
<TableHeaderCell>Organization Name</TableHeaderCell>
|
||||
<TableHeaderCell>Created</TableHeaderCell>
|
||||
<TableHeaderCell>Spend (USD)</TableHeaderCell>
|
||||
<TableHeaderCell>Budget (USD)</TableHeaderCell>
|
||||
<TableHeaderCell>Models</TableHeaderCell>
|
||||
<TableHeaderCell>TPM / RPM Limits</TableHeaderCell>
|
||||
<TableHeaderCell>Info</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Organization ID</TableHeaderCell>
|
||||
<TableHeaderCell>Organization Name</TableHeaderCell>
|
||||
<TableHeaderCell>Created</TableHeaderCell>
|
||||
<TableHeaderCell>Spend (USD)</TableHeaderCell>
|
||||
<TableHeaderCell>Budget (USD)</TableHeaderCell>
|
||||
<TableHeaderCell>Models</TableHeaderCell>
|
||||
<TableHeaderCell>TPM / RPM Limits</TableHeaderCell>
|
||||
<TableHeaderCell>Info</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{organizations && organizations.length > 0
|
||||
? organizations
|
||||
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
||||
.map((org: Organization) => (
|
||||
<TableRow key={org.organization_id}>
|
||||
<TableCell>
|
||||
<div className="overflow-hidden">
|
||||
<Tooltip title={org.organization_id}>
|
||||
<TableBody>
|
||||
{organizations && organizations.length > 0
|
||||
? organizations
|
||||
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
||||
.map((org: Organization) => (
|
||||
<TableRow key={org.organization_id}>
|
||||
<TableCell>
|
||||
<div className="overflow-hidden">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px] h-auto"
|
||||
onClick={() => setSelectedOrgId(org.organization_id)}
|
||||
>
|
||||
{org.organization_id?.slice(0, 7)}
|
||||
...
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{org.organization_alias}</TableCell>
|
||||
<TableCell>
|
||||
{org.created_at ? new Date(org.created_at).toLocaleDateString() : "N/A"}
|
||||
</TableCell>
|
||||
<TableCell>{formatNumberWithCommas(org.spend, 4)}</TableCell>
|
||||
<TableCell>
|
||||
{org.litellm_budget_table?.max_budget !== null &&
|
||||
org.litellm_budget_table?.max_budget !== undefined
|
||||
? org.litellm_budget_table?.max_budget
|
||||
: "No limit"}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
style={{
|
||||
maxWidth: "8-x",
|
||||
whiteSpace: "pre-wrap",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
className={org.models.length > 3 ? "px-0" : ""}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
{Array.isArray(org.models) ? (
|
||||
<div className="flex flex-col">
|
||||
{org.models.length === 0 ? (
|
||||
<Badge size={"xs"} className="mb-1" color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
</Badge>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-start">
|
||||
{org.models.length > 3 && (
|
||||
<div>
|
||||
<Icon
|
||||
icon={
|
||||
expandedAccordions[org.organization_id || ""]
|
||||
? ChevronDownIcon
|
||||
: ChevronRightIcon
|
||||
}
|
||||
className="cursor-pointer"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setExpandedAccordions((prev) => ({
|
||||
...prev,
|
||||
[org.organization_id || ""]:
|
||||
!prev[org.organization_id || ""],
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{org.organization_id}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{org.organization_alias}</TableCell>
|
||||
<TableCell>
|
||||
{org.created_at ? new Date(org.created_at).toLocaleDateString() : "N/A"}
|
||||
</TableCell>
|
||||
<TableCell>{formatNumberWithCommas(org.spend, 4)}</TableCell>
|
||||
<TableCell>
|
||||
{org.litellm_budget_table?.max_budget !== null &&
|
||||
org.litellm_budget_table?.max_budget !== undefined
|
||||
? org.litellm_budget_table?.max_budget
|
||||
: "No limit"}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
style={{
|
||||
whiteSpace: "pre-wrap",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
className={org.models.length > 3 ? "px-0" : ""}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
{Array.isArray(org.models) ? (
|
||||
<div className="flex flex-col">
|
||||
{org.models.length === 0 ? (
|
||||
<Badge variant="destructive" className="mb-1 w-fit">
|
||||
All Proxy Models
|
||||
</Badge>
|
||||
) : (
|
||||
<div className="flex items-start">
|
||||
{org.models.length > 3 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5"
|
||||
onClick={() => {
|
||||
setExpandedAccordions((prev) => ({
|
||||
...prev,
|
||||
[org.organization_id || ""]:
|
||||
!prev[org.organization_id || ""],
|
||||
}));
|
||||
}}
|
||||
aria-label="Toggle models"
|
||||
>
|
||||
{expandedAccordions[org.organization_id || ""] ? (
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
) : (
|
||||
<ChevronRightIcon className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{org.models.slice(0, 3).map((model, index) =>
|
||||
model === "all-proxy-models" ? (
|
||||
<Badge key={index} variant="destructive">
|
||||
All Proxy Models
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge key={index} variant="secondary">
|
||||
{model.length > 30
|
||||
? `${getModelDisplayName(model).slice(0, 30)}...`
|
||||
: getModelDisplayName(model)}
|
||||
</Badge>
|
||||
),
|
||||
)}
|
||||
{org.models.length > 3 &&
|
||||
!expandedAccordions[org.organization_id || ""] && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
+{org.models.length - 3}{" "}
|
||||
{org.models.length - 3 === 1 ? "more model" : "more models"}
|
||||
</Badge>
|
||||
)}
|
||||
{expandedAccordions[org.organization_id || ""] && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{org.models.slice(0, 3).map((model, index) =>
|
||||
{org.models.slice(3).map((model, index) =>
|
||||
model === "all-proxy-models" ? (
|
||||
<Badge key={index} size={"xs"} color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
<Badge key={index + 3} variant="destructive">
|
||||
All Proxy Models
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge key={index} size={"xs"} color="blue">
|
||||
<Text>
|
||||
{model.length > 30
|
||||
? `${getModelDisplayName(model).slice(0, 30)}...`
|
||||
: getModelDisplayName(model)}
|
||||
</Text>
|
||||
<Badge key={index + 3} variant="secondary">
|
||||
{model.length > 30
|
||||
? `${getModelDisplayName(model).slice(0, 30)}...`
|
||||
: getModelDisplayName(model)}
|
||||
</Badge>
|
||||
),
|
||||
)}
|
||||
{org.models.length > 3 &&
|
||||
!expandedAccordions[org.organization_id || ""] && (
|
||||
<Badge size={"xs"} color="gray" className="cursor-pointer">
|
||||
<Text>
|
||||
+{org.models.length - 3}{" "}
|
||||
{org.models.length - 3 === 1
|
||||
? "more model"
|
||||
: "more models"}
|
||||
</Text>
|
||||
</Badge>
|
||||
)}
|
||||
{expandedAccordions[org.organization_id || ""] && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{org.models.slice(3).map((model, index) =>
|
||||
model === "all-proxy-models" ? (
|
||||
<Badge key={index + 3} size={"xs"} color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge key={index + 3} size={"xs"} color="blue">
|
||||
<Text>
|
||||
{model.length > 30
|
||||
? `${getModelDisplayName(model).slice(0, 30)}...`
|
||||
: getModelDisplayName(model)}
|
||||
</Text>
|
||||
</Badge>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>
|
||||
TPM:{" "}
|
||||
{org.litellm_budget_table?.tpm_limit
|
||||
? org.litellm_budget_table?.tpm_limit
|
||||
: "Unlimited"}
|
||||
<br />
|
||||
RPM:{" "}
|
||||
{org.litellm_budget_table?.rpm_limit
|
||||
? org.litellm_budget_table?.rpm_limit
|
||||
: "Unlimited"}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{org.members?.length || 0} Members</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{userRole === "Admin" && (
|
||||
<>
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
tooltipText="Edit organization"
|
||||
onClick={() => {
|
||||
setSelectedOrgId(org.organization_id);
|
||||
setEditOrg(true);
|
||||
}}
|
||||
/>
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
tooltipText="Delete organization"
|
||||
onClick={() => handleDelete(org.organization_id)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
: null}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</Col>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm">
|
||||
TPM:{" "}
|
||||
{org.litellm_budget_table?.tpm_limit
|
||||
? org.litellm_budget_table?.tpm_limit
|
||||
: "Unlimited"}
|
||||
<br />
|
||||
RPM:{" "}
|
||||
{org.litellm_budget_table?.rpm_limit
|
||||
? org.litellm_budget_table?.rpm_limit
|
||||
: "Unlimited"}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm">{org.members?.length || 0} Members</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{userRole === "Admin" && (
|
||||
<>
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
tooltipText="Edit organization"
|
||||
onClick={() => {
|
||||
setSelectedOrgId(org.organization_id);
|
||||
setEditOrg(true);
|
||||
}}
|
||||
/>
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
tooltipText="Delete organization"
|
||||
onClick={() => handleDelete(org.organization_id)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
: null}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</Col>
|
||||
</Grid>
|
||||
<Modal title="Create Organization" visible={isOrgModalVisible} width={800} footer={null} onCancel={handleCancel}>
|
||||
<Form form={form} onFinish={handleCreate} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} labelAlign="left">
|
||||
<Form.Item
|
||||
label="Organization Name"
|
||||
name="organization_alias"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please input an organization name",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextInput placeholder="" />
|
||||
</Form.Item>
|
||||
<Form.Item label="Models" name="models">
|
||||
<ModelSelect
|
||||
options={{ showAllProxyModelsOverride: true, includeSpecialOptions: true }}
|
||||
value={form.getFieldValue("models")}
|
||||
onChange={(values) => form.setFieldValue("models", values)}
|
||||
context="organization"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form.Item label="Max Budget (USD)" name="max_budget">
|
||||
<NumericalInput step={0.01} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<Form.Item 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">
|
||||
<NumericalInput step={1} width={400} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Requests per minute Limit (RPM)" name="rpm_limit">
|
||||
<NumericalInput step={1} width={400} />
|
||||
</Form.Item>
|
||||
<Dialog
|
||||
open={isOrgModalVisible}
|
||||
onOpenChange={(o) => {
|
||||
if (!o) handleCancel();
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-[800px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Organization</DialogTitle>
|
||||
</DialogHeader>
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleCreate} className="space-y-4">
|
||||
<div className="grid grid-cols-[1fr_2fr] gap-4 items-start">
|
||||
<Label htmlFor="org-alias" className="mt-2 text-left">
|
||||
Organization Name <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="org-alias"
|
||||
{...form.register("organization_alias", {
|
||||
required: "Please input an organization name",
|
||||
})}
|
||||
/>
|
||||
{form.formState.errors.organization_alias && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.organization_alias.message as string}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Allowed Vector Stores{" "}
|
||||
<Tooltip title="Select which vector stores this organization can access by default. Leave empty for access to all vector stores">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="allowed_vector_store_ids"
|
||||
className="mt-4"
|
||||
help="Select vector stores this organization can access. Leave empty for access to all vector stores"
|
||||
>
|
||||
<VectorStoreSelector
|
||||
onChange={(values) => form.setFieldValue("allowed_vector_store_ids", values)}
|
||||
value={form.getFieldValue("allowed_vector_store_ids")}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select vector stores (optional)"
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="grid grid-cols-[1fr_2fr] gap-4 items-start">
|
||||
<Label className="mt-2 text-left">Models</Label>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="models"
|
||||
render={({ field }) => (
|
||||
<ModelSelect
|
||||
options={{ showAllProxyModelsOverride: true, includeSpecialOptions: true }}
|
||||
value={field.value}
|
||||
onChange={(values) => field.onChange(values)}
|
||||
context="organization"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Allowed MCP Servers{" "}
|
||||
<Tooltip title="Select which MCP servers and access groups this organization can access by default.">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="allowed_mcp_servers_and_groups"
|
||||
className="mt-4"
|
||||
help="Select MCP servers and access groups this organization can access."
|
||||
>
|
||||
<MCPServerSelector
|
||||
onChange={(values) => form.setFieldValue("allowed_mcp_servers_and_groups", values)}
|
||||
value={form.getFieldValue("allowed_mcp_servers_and_groups")}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select MCP servers and access groups (optional)"
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="grid grid-cols-[1fr_2fr] gap-4 items-center">
|
||||
<Label className="text-left">Max Budget (USD)</Label>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="max_budget"
|
||||
render={({ field }) => (
|
||||
<NumericalInput
|
||||
step={0.01}
|
||||
width={200}
|
||||
value={field.value as any}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Form.Item label="Metadata" name="metadata">
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<div className="grid grid-cols-[1fr_2fr] gap-4 items-center">
|
||||
<Label className="text-left">Reset Budget</Label>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="budget_duration"
|
||||
render={({ field }) => (
|
||||
<Select value={field.value || ""} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="n/a" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="24h">daily</SelectItem>
|
||||
<SelectItem value="7d">weekly</SelectItem>
|
||||
<SelectItem value="30d">monthly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button type="submit">Create Organization</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
<div className="grid grid-cols-[1fr_2fr] gap-4 items-center">
|
||||
<Label className="text-left">Tokens per minute Limit (TPM)</Label>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="tpm_limit"
|
||||
render={({ field }) => (
|
||||
<NumericalInput
|
||||
step={1}
|
||||
width={400}
|
||||
value={field.value as any}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[1fr_2fr] gap-4 items-center">
|
||||
<Label className="text-left">Requests per minute Limit (RPM)</Label>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="rpm_limit"
|
||||
render={({ field }) => (
|
||||
<NumericalInput
|
||||
step={1}
|
||||
width={400}
|
||||
value={field.value as any}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[1fr_2fr] gap-4 items-start mt-4">
|
||||
<Label className="mt-2 text-left inline-flex items-center gap-1">
|
||||
Allowed Vector Stores
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoCircleOutlined className="h-3.5 w-3.5 ml-1" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Select which vector stores this organization can access by default. Leave
|
||||
empty for access to all vector stores
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Label>
|
||||
<div className="space-y-1">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="allowed_vector_store_ids"
|
||||
render={({ field }) => (
|
||||
<VectorStoreSelector
|
||||
onChange={(values) => field.onChange(values)}
|
||||
value={field.value}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select vector stores (optional)"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Select vector stores this organization can access. Leave empty for access to all
|
||||
vector stores
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[1fr_2fr] gap-4 items-start mt-4">
|
||||
<Label className="mt-2 text-left inline-flex items-center gap-1">
|
||||
Allowed MCP Servers
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoCircleOutlined className="h-3.5 w-3.5 ml-1" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Select which MCP servers and access groups this organization can access by
|
||||
default.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Label>
|
||||
<div className="space-y-1">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="allowed_mcp_servers_and_groups"
|
||||
render={({ field }) => (
|
||||
<MCPServerSelector
|
||||
onChange={(values) => field.onChange(values)}
|
||||
value={field.value as any}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select MCP servers and access groups (optional)"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Select MCP servers and access groups this organization can access.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[1fr_2fr] gap-4 items-start">
|
||||
<Label className="mt-2 text-left">Metadata</Label>
|
||||
<Textarea rows={4} {...form.register("metadata")} />
|
||||
</div>
|
||||
|
||||
<div className="text-right mt-3">
|
||||
<Button type="submit">Create Organization</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<DeleteResourceModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
|
|
@ -563,6 +712,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
|
|||
confirmLoading={isDeleting}
|
||||
/>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue