Consistent layout for Create and Back buttons on all the pages (#12542)

* create new model button

* cleanup + prettier

* prettier

* create new model button added for model and model info+ back button added

* prettier

* team and team info has create team button + back to teams button added in team_info

* prettier

* better prettier

* always show create new organization button and add back to organizations button

* minor, prettier

* prettier

* remove user header and prettier

* move top buttons to the left

* prettier

* back to all logs button style fixed

* prettier

* modify button text

* create new guardrail button style

* prettier

* button modifications

* guardrails page done

* prettier

* add new guardrail text button + prettier on mcp

* button related changes

* prettier

* mcp servers page done
This commit is contained in:
tanjiro 2025-07-13 00:18:02 +09:00 committed by GitHub
parent 749051105b
commit 2faba5f215
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 5343 additions and 5207 deletions

View file

@ -0,0 +1,7 @@
{
"semi": false,
"tabWidth": 2,
"printWidth": 120,
"trailingComma": "all",
"jsxBracketSameLine": false
}

View file

@ -1,47 +1,47 @@
import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import React, { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Button, Modal, Form, Input, message, Select, InputNumber, Select as Select2 } from "antd"
import {
Button,
Modal,
Form,
Input,
message,
Select,
InputNumber,
Select as Select2,
} from "antd";
import { Button as Button2, Text, TextInput, SelectItem, Accordion, AccordionHeader, AccordionBody, Title, } from "@tremor/react";
import OnboardingModal from "./onboarding_link";
import { InvitationLink } from "./onboarding_link";
Button as Button2,
Text,
TextInput,
SelectItem,
Accordion,
AccordionHeader,
AccordionBody,
Title,
} from "@tremor/react"
import OnboardingModal from "./onboarding_link"
import { InvitationLink } from "./onboarding_link"
import {
userCreateCall,
modelAvailableCall,
invitationCreateCall,
getProxyUISettings,
getProxyBaseUrl,
} from "./networking";
import BulkCreateUsers from "./bulk_create_users_button";
const { Option } = Select;
import { Tooltip } from "antd";
import { InfoCircleOutlined } from '@ant-design/icons';
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
import { useQueryClient } from "@tanstack/react-query";
} from "./networking"
import BulkCreateUsers from "./bulk_create_users_button"
const { Option } = Select
import { Tooltip } from "antd"
import { InfoCircleOutlined } from "@ant-design/icons"
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key"
import { useQueryClient } from "@tanstack/react-query"
interface CreateuserProps {
userID: string;
accessToken: string;
teams: any[] | null;
possibleUIRoles: null | Record<string, Record<string, string>>;
onUserCreated?: (userId: string) => void;
isEmbedded?: boolean;
userID: string
accessToken: string
teams: any[] | null
possibleUIRoles: null | Record<string, Record<string, string>>
onUserCreated?: (userId: string) => void
isEmbedded?: boolean
}
// Define an interface for the UI settings
interface UISettings {
PROXY_BASE_URL: string | null;
PROXY_LOGOUT_URL: string | null;
DEFAULT_TEAM_DISABLED: boolean;
SSO_ENABLED: boolean;
PROXY_BASE_URL: string | null
PROXY_LOGOUT_URL: string | null
DEFAULT_TEAM_DISABLED: boolean
SSO_ENABLED: boolean
}
const Createuser: React.FC<CreateuserProps> = ({
@ -52,97 +52,91 @@ const Createuser: React.FC<CreateuserProps> = ({
onUserCreated,
isEmbedded = false,
}) => {
const queryClient = useQueryClient();
const [uiSettings, setUISettings] = useState<UISettings | null>(null);
const [form] = Form.useForm();
const [isModalVisible, setIsModalVisible] = useState(false);
const [apiuser, setApiuser] = useState<boolean>(false);
const [userModels, setUserModels] = useState<string[]>([]);
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] =
useState(false);
const [invitationLinkData, setInvitationLinkData] =
useState<InvitationLink | null>(null);
const [baseUrl, setBaseUrl] = useState<string | null>(null);
const queryClient = useQueryClient()
const [uiSettings, setUISettings] = useState<UISettings | null>(null)
const [form] = Form.useForm()
const [isModalVisible, setIsModalVisible] = useState(false)
const [apiuser, setApiuser] = useState<boolean>(false)
const [userModels, setUserModels] = useState<string[]>([])
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false)
const [invitationLinkData, setInvitationLinkData] = useState<InvitationLink | null>(null)
const [baseUrl, setBaseUrl] = useState<string | null>(null)
// get all models
useEffect(() => {
const fetchData = async () => {
try {
const userRole = "any"; // You may need to get the user role dynamically
const modelDataResponse = await modelAvailableCall(
accessToken,
userID,
userRole,
);
const userRole = "any" // You may need to get the user role dynamically
const modelDataResponse = await modelAvailableCall(accessToken, userID, userRole)
// Assuming modelDataResponse.data contains an array of model objects with a 'model_name' property
const availableModels = [];
const availableModels = []
for (let i = 0; i < modelDataResponse.data.length; i++) {
const model = modelDataResponse.data[i];
availableModels.push(model.id);
const model = modelDataResponse.data[i]
availableModels.push(model.id)
}
console.log("Model data response:", modelDataResponse.data);
console.log("Available models:", availableModels);
console.log("Model data response:", modelDataResponse.data)
console.log("Available models:", availableModels)
// Assuming modelDataResponse.data contains an array of model names
setUserModels(availableModels);
setUserModels(availableModels)
// get ui settings
const uiSettingsResponse = await getProxyUISettings(accessToken);
console.log("uiSettingsResponse:", uiSettingsResponse);
const uiSettingsResponse = await getProxyUISettings(accessToken)
console.log("uiSettingsResponse:", uiSettingsResponse)
setUISettings(uiSettingsResponse);
setUISettings(uiSettingsResponse)
} catch (error) {
console.error("Error fetching model data:", error);
console.error("Error fetching model data:", error)
}
};
}
setBaseUrl(getProxyBaseUrl());
setBaseUrl(getProxyBaseUrl())
fetchData(); // Call the function to fetch model data when the component mounts
}, []); // Empty dependency array to run only once
fetchData() // Call the function to fetch model data when the component mounts
}, []) // Empty dependency array to run only once
const handleOk = () => {
setIsModalVisible(false);
form.resetFields();
};
setIsModalVisible(false)
form.resetFields()
}
const handleCancel = () => {
setIsModalVisible(false);
setApiuser(false);
form.resetFields();
};
setIsModalVisible(false)
setApiuser(false)
form.resetFields()
}
const handleCreate = async (formValues: { user_id: string, models?: string[], user_role: string }) => {
const handleCreate = async (formValues: { user_id: string; models?: string[]; user_role: string }) => {
try {
message.info("Making API Call");
message.info("Making API Call")
if (!isEmbedded) {
setIsModalVisible(true);
setIsModalVisible(true)
}
if ((!formValues.models || formValues.models.length === 0) && formValues.user_role !== "proxy_admin") {
console.log("formValues.user_role", formValues.user_role)
// If models is empty or undefined, set it to "no-default-models"
formValues.models = ["no-default-models"];
formValues.models = ["no-default-models"]
}
console.log("formValues in create user:", formValues);
const response = await userCreateCall(accessToken, null, formValues);
await queryClient.invalidateQueries({ queryKey: ['userList'] })
console.log("user create Response:", response);
setApiuser(true);
const user_id = response.data?.user_id || response.user_id;
console.log("formValues in create user:", formValues)
const response = await userCreateCall(accessToken, null, formValues)
await queryClient.invalidateQueries({ queryKey: ["userList"] })
console.log("user create Response:", response)
setApiuser(true)
const user_id = response.data?.user_id || response.user_id
// Call the callback if provided (for embedded mode)
if (onUserCreated && isEmbedded) {
onUserCreated(user_id);
form.resetFields();
return; // Skip the invitation flow when embedded
onUserCreated(user_id)
form.resetFields()
return // Skip the invitation flow when embedded
}
// only do invite link flow if sso is not enabled
if (!uiSettings?.SSO_ENABLED) {
invitationCreateCall(accessToken, user_id).then((data) => {
data.has_user_setup_sso = false;
setInvitationLinkData(data);
setIsInvitationLinkModalVisible(true);
});
data.has_user_setup_sso = false
setInvitationLinkData(data)
setIsInvitationLinkModalVisible(true)
})
} else {
// create an InvitationLink Object for this user for the SSO flow
// for SSO the invite link is the proxy base url since the User just needs to login
@ -157,52 +151,41 @@ const Createuser: React.FC<CreateuserProps> = ({
updated_at: new Date(),
updated_by: userID,
has_user_setup_sso: true,
};
setInvitationLinkData(invitationLink);
setIsInvitationLinkModalVisible(true);
}
setInvitationLinkData(invitationLink)
setIsInvitationLinkModalVisible(true)
}
message.success("API user Created");
form.resetFields();
localStorage.removeItem("userData" + userID);
message.success("API user Created")
form.resetFields()
localStorage.removeItem("userData" + userID)
} catch (error: any) {
const errorMessage = error.response?.data?.detail || error?.message || "Error creating the user";
message.error(errorMessage);
console.error("Error creating the user:", error);
const errorMessage = error.response?.data?.detail || error?.message || "Error creating the user"
message.error(errorMessage)
console.error("Error creating the user:", error)
}
};
}
// Modify the return statement to handle embedded mode
if (isEmbedded) {
return (
<Form
form={form}
onFinish={handleCreate}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
>
<Form form={form} onFinish={handleCreate} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} labelAlign="left">
<Form.Item label="User Email" name="user_email">
<TextInput placeholder="" />
</Form.Item>
<Form.Item label="User Role" name="user_role">
<Select2>
{possibleUIRoles &&
Object.entries(possibleUIRoles).map(
([role, { ui_label, description }]) => (
<SelectItem key={role} value={role} title={ui_label}>
<div className="flex">
{ui_label}{" "}
<p
className="ml-2"
style={{ color: "gray", fontSize: "12px" }}
>
{description}
</p>
</div>
</SelectItem>
),
)}
Object.entries(possibleUIRoles).map(([role, { ui_label, description }]) => (
<SelectItem key={role} value={role} title={ui_label}>
<div className="flex">
{ui_label}{" "}
<p className="ml-2" style={{ color: "gray", fontSize: "12px" }}>
{description}
</p>
</div>
</SelectItem>
))}
</Select2>
</Form.Item>
<Form.Item label="Team ID" name="team_id">
@ -224,25 +207,21 @@ const Createuser: React.FC<CreateuserProps> = ({
<Form.Item label="Metadata" name="metadata">
<Input.TextArea rows={4} placeholder="Enter metadata as JSON" />
</Form.Item>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button htmlType="submit">Create User</Button>
</div>
</Form>
);
)
}
// Original return for standalone mode
return (
<div className="flex gap-2">
<Button2 className="mx-auto mb-0" onClick={() => setIsModalVisible(true)}>
<Button2 className="mb-0" onClick={() => setIsModalVisible(true)}>
+ Invite User
</Button2>
<BulkCreateUsers
accessToken={accessToken}
teams={teams}
possibleUIRoles={possibleUIRoles}
/>
<BulkCreateUsers accessToken={accessToken} teams={teams} possibleUIRoles={possibleUIRoles} />
<Modal
title="Invite User"
visible={isModalVisible}
@ -252,46 +231,42 @@ const Createuser: React.FC<CreateuserProps> = ({
onCancel={handleCancel}
>
<Text className="mb-1">Create a User who can own keys</Text>
<Form
form={form}
onFinish={handleCreate}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
>
<Form form={form} onFinish={handleCreate} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} labelAlign="left">
<Form.Item label="User Email" name="user_email">
<TextInput placeholder="" />
</Form.Item>
<Form.Item label={
<span>
Global Proxy Role{' '}
<Tooltip title="This is the role that the user will globally on the proxy. This role is independent of any team/org specific roles.">
<InfoCircleOutlined/>
</Tooltip>
</span>
}
name="user_role">
<Form.Item
label={
<span>
Global Proxy Role{" "}
<Tooltip title="This is the role that the user will globally on the proxy. This role is independent of any team/org specific roles.">
<InfoCircleOutlined />
</Tooltip>
</span>
}
name="user_role"
>
<Select2>
{possibleUIRoles &&
Object.entries(possibleUIRoles).map(
([role, { ui_label, description }]) => (
<SelectItem key={role} value={role} title={ui_label}>
<div className="flex">
{ui_label}{" "}
<p
className="ml-2"
style={{ color: "gray", fontSize: "12px" }}
>
{description}
</p>
</div>
</SelectItem>
),
)}
Object.entries(possibleUIRoles).map(([role, { ui_label, description }]) => (
<SelectItem key={role} value={role} title={ui_label}>
<div className="flex">
{ui_label}{" "}
<p className="ml-2" style={{ color: "gray", fontSize: "12px" }}>
{description}
</p>
</div>
</SelectItem>
))}
</Select2>
</Form.Item>
<Form.Item label="Team ID" className="gap-2" name="team_id" help="If selected, user will be added as a 'user' role to the team.">
<Form.Item
label="Team ID"
className="gap-2"
name="team_id"
help="If selected, user will be added as a 'user' role to the team."
>
<Select placeholder="Select Team ID" style={{ width: "100%" }}>
{teams ? (
teams.map((team: any) => (
@ -311,38 +286,36 @@ const Createuser: React.FC<CreateuserProps> = ({
<Input.TextArea rows={4} placeholder="Enter metadata as JSON" />
</Form.Item>
<Accordion>
<AccordionHeader>
<Title>Personal Key Creation</Title>
</AccordionHeader>
<AccordionBody>
<Form.Item className="gap-2" label={
<span>
Models{' '}
<Tooltip title="Models user has access to, outside of team scope.">
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
</Tooltip>
</span>
} name="models" help="Models user has access to, outside of team scope.">
<Select2
mode="multiple"
placeholder="Select models"
style={{ width: "100%" }}
<AccordionHeader>
<Title>Personal Key Creation</Title>
</AccordionHeader>
<AccordionBody>
<Form.Item
className="gap-2"
label={
<span>
Models{" "}
<Tooltip title="Models user has access to, outside of team scope.">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="models"
help="Models user has access to, outside of team scope."
>
<Select2.Option
key="all-proxy-models"
value="all-proxy-models"
>
All Proxy Models
</Select2.Option>
{userModels.map((model) => (
<Select2.Option key={model} value={model}>
{getModelDisplayName(model)}
<Select2 mode="multiple" placeholder="Select models" style={{ width: "100%" }}>
<Select2.Option key="all-proxy-models" value="all-proxy-models">
All Proxy Models
</Select2.Option>
))}
</Select2>
</Form.Item>
</AccordionBody>
</Accordion>
{userModels.map((model) => (
<Select2.Option key={model} value={model}>
{getModelDisplayName(model)}
</Select2.Option>
))}
</Select2>
</Form.Item>
</AccordionBody>
</Accordion>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button htmlType="submit">Create User</Button>
</div>
@ -357,7 +330,7 @@ const Createuser: React.FC<CreateuserProps> = ({
/>
)}
</div>
);
};
)
}
export default Createuser;
export default Createuser

View file

@ -1,136 +1,135 @@
import React, { useState, useEffect } from "react";
import {
Card,
Text,
Button,
Icon,
TextInput,
} from "@tremor/react";
import {
PlusIcon,
} from "@heroicons/react/outline";
import { Modal, message } from "antd";
import { getGuardrailsList, deleteGuardrailCall } from "./networking";
import AddGuardrailForm from "./guardrails/add_guardrail_form";
import GuardrailTable from "./guardrails/guardrail_table";
import { isAdminRole } from "@/utils/roles";
import React, { useState, useEffect } from "react"
import { Card, Text, Button, Icon, TextInput } from "@tremor/react"
import { PlusIcon } from "@heroicons/react/outline"
import { Modal, message } from "antd"
import { getGuardrailsList, deleteGuardrailCall } from "./networking"
import AddGuardrailForm from "./guardrails/add_guardrail_form"
import GuardrailTable from "./guardrails/guardrail_table"
import { isAdminRole } from "@/utils/roles"
import GuardrailInfoView from "./guardrails/guardrail_info"
interface GuardrailsPanelProps {
accessToken: string | null;
userRole?: string;
accessToken: string | null
userRole?: string
}
interface GuardrailItem {
guardrail_id?: string;
guardrail_name: string | null;
guardrail_id?: string
guardrail_name: string | null
litellm_params: {
guardrail: string;
mode: string;
default_on: boolean;
};
guardrail_info: Record<string, any> | null;
created_at?: string;
updated_at?: string;
guardrail: string
mode: string
default_on: boolean
}
guardrail_info: Record<string, any> | null
created_at?: string
updated_at?: string
}
interface GuardrailsResponse {
guardrails: GuardrailItem[];
guardrails: GuardrailItem[]
}
const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole }) => {
const [guardrailsList, setGuardrailsList] = useState<GuardrailItem[]>([]);
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [guardrailToDelete, setGuardrailToDelete] = useState<{id: string, name: string} | null>(null);
const [isViewingGuardrailInfo, setIsViewingGuardrailInfo] = useState(false);
const isAdmin = userRole ? isAdminRole(userRole) : false;
const [guardrailsList, setGuardrailsList] = useState<GuardrailItem[]>([])
const [isAddModalVisible, setIsAddModalVisible] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [guardrailToDelete, setGuardrailToDelete] = useState<{ id: string; name: string } | null>(null)
const [selectedGuardrailId, setSelectedGuardrailId] = useState<string | null>(null)
const isAdmin = userRole ? isAdminRole(userRole) : false
const fetchGuardrails = async () => {
if (!accessToken) {
return;
return
}
setIsLoading(true);
setIsLoading(true)
try {
const response: GuardrailsResponse = await getGuardrailsList(accessToken);
console.log(`guardrails: ${JSON.stringify(response)}`);
setGuardrailsList(response.guardrails);
const response: GuardrailsResponse = await getGuardrailsList(accessToken)
console.log(`guardrails: ${JSON.stringify(response)}`)
setGuardrailsList(response.guardrails)
} catch (error) {
console.error('Error fetching guardrails:', error);
console.error("Error fetching guardrails:", error)
} finally {
setIsLoading(false);
setIsLoading(false)
}
};
}
useEffect(() => {
fetchGuardrails();
}, [accessToken]);
fetchGuardrails()
}, [accessToken])
const handleAddGuardrail = () => {
setIsAddModalVisible(true);
};
if (selectedGuardrailId) {
setSelectedGuardrailId(null)
}
setIsAddModalVisible(true)
}
const handleCloseModal = () => {
setIsAddModalVisible(false);
};
setIsAddModalVisible(false)
}
const handleSuccess = () => {
fetchGuardrails();
};
fetchGuardrails()
}
const handleDeleteClick = (guardrailId: string, guardrailName: string) => {
setGuardrailToDelete({id: guardrailId, name: guardrailName});
};
setGuardrailToDelete({ id: guardrailId, name: guardrailName })
}
const handleDeleteConfirm = async () => {
if (!guardrailToDelete || !accessToken) return;
if (!guardrailToDelete || !accessToken) return
// Log removed to maintain clean production code
setIsDeleting(true);
setIsDeleting(true)
try {
await deleteGuardrailCall(accessToken, guardrailToDelete.id);
message.success(`Guardrail "${guardrailToDelete.name}" deleted successfully`);
fetchGuardrails(); // Refresh the list
await deleteGuardrailCall(accessToken, guardrailToDelete.id)
message.success(`Guardrail "${guardrailToDelete.name}" deleted successfully`)
fetchGuardrails() // Refresh the list
} catch (error) {
console.error('Error deleting guardrail:', error);
message.error('Failed to delete guardrail');
console.error("Error deleting guardrail:", error)
message.error("Failed to delete guardrail")
} finally {
setIsDeleting(false);
setGuardrailToDelete(null);
setIsDeleting(false)
setGuardrailToDelete(null)
}
};
}
const handleDeleteCancel = () => {
setGuardrailToDelete(null);
};
setGuardrailToDelete(null)
}
return (
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
{!isViewingGuardrailInfo && (
<div className="flex justify-between items-center mb-4">
<Button
icon={PlusIcon}
onClick={handleAddGuardrail}
disabled={!accessToken}
>
Add Guardrail
</Button>
</div>
)}
<GuardrailTable
guardrailsList={guardrailsList}
isLoading={isLoading}
onDeleteClick={handleDeleteClick}
accessToken={accessToken}
onGuardrailUpdated={fetchGuardrails}
isAdmin={isAdmin}
onShowGuardrailInfo={setIsViewingGuardrailInfo}
/>
<div className="flex justify-between items-center mb-4">
<Button onClick={handleAddGuardrail} disabled={!accessToken}>
+ Add New Guardrail
</Button>
</div>
<AddGuardrailForm
{selectedGuardrailId ? (
<GuardrailInfoView
guardrailId={selectedGuardrailId}
onClose={() => setSelectedGuardrailId(null)}
accessToken={accessToken}
isAdmin={isAdmin}
/>
) : (
<GuardrailTable
guardrailsList={guardrailsList}
isLoading={isLoading}
onDeleteClick={handleDeleteClick}
accessToken={accessToken}
onGuardrailUpdated={fetchGuardrails}
isAdmin={isAdmin}
onGuardrailClick={(id) => setSelectedGuardrailId(id)}
/>
)}
<AddGuardrailForm
visible={isAddModalVisible}
onClose={handleCloseModal}
accessToken={accessToken}
@ -152,7 +151,7 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
</Modal>
)}
</div>
);
};
)
}
export default GuardrailsPanel;
export default GuardrailsPanel

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect } from "react"
import {
Card,
Title,
@ -12,131 +12,131 @@ import {
TabPanel,
TabPanels,
TextInput,
} from "@tremor/react";
import { Button, Form, Input, Select, message, Tooltip, Divider } from "antd";
import { InfoCircleOutlined } from '@ant-design/icons';
import { getGuardrailInfo, updateGuardrailCall, getGuardrailUISettings, getGuardrailProviderSpecificParams } from "@/components/networking";
import { getGuardrailLogoAndName, guardrail_provider_map } from "./guardrail_info_helpers";
import PiiConfiguration from "./pii_configuration";
import GuardrailProviderFields from "./guardrail_provider_fields";
import GuardrailOptionalParams from "./guardrail_optional_params";
} from "@tremor/react"
import { Button, Form, Input, Select, message, Tooltip, Divider } from "antd"
import { InfoCircleOutlined } from "@ant-design/icons"
import {
getGuardrailInfo,
updateGuardrailCall,
getGuardrailUISettings,
getGuardrailProviderSpecificParams,
} from "@/components/networking"
import { getGuardrailLogoAndName, guardrail_provider_map } from "./guardrail_info_helpers"
import PiiConfiguration from "./pii_configuration"
import GuardrailProviderFields from "./guardrail_provider_fields"
import GuardrailOptionalParams from "./guardrail_optional_params"
import { ArrowLeftIcon } from "@heroicons/react/outline"
export interface GuardrailInfoProps {
guardrailId: string;
onClose: () => void;
accessToken: string | null;
isAdmin: boolean;
guardrailId: string
onClose: () => void
accessToken: string | null
isAdmin: boolean
}
interface ProviderParam {
param: string;
description: string;
required: boolean;
default_value?: string;
options?: string[];
type?: string;
fields?: { [key: string]: ProviderParam };
dict_key_options?: string[];
dict_value_type?: string;
param: string
description: string
required: boolean
default_value?: string
options?: string[]
type?: string
fields?: { [key: string]: ProviderParam }
dict_key_options?: string[]
dict_value_type?: string
}
interface ProviderParamsResponse {
[provider: string]: { [key: string]: ProviderParam };
[provider: string]: { [key: string]: ProviderParam }
}
const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({
guardrailId,
onClose,
accessToken,
isAdmin
}) => {
const [guardrailData, setGuardrailData] = useState<any>(null);
const [guardrailProviderSpecificParams, setGuardrailProviderSpecificParams] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [isEditing, setIsEditing] = useState(false);
const [form] = Form.useForm();
const [selectedPiiEntities, setSelectedPiiEntities] = useState<string[]>([]);
const [selectedPiiActions, setSelectedPiiActions] = useState<{[key: string]: string}>({});
const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose, accessToken, isAdmin }) => {
const [guardrailData, setGuardrailData] = useState<any>(null)
const [guardrailProviderSpecificParams, setGuardrailProviderSpecificParams] = useState<any>(null)
const [loading, setLoading] = useState(true)
const [isEditing, setIsEditing] = useState(false)
const [form] = Form.useForm()
const [selectedPiiEntities, setSelectedPiiEntities] = useState<string[]>([])
const [selectedPiiActions, setSelectedPiiActions] = useState<{ [key: string]: string }>({})
const [guardrailSettings, setGuardrailSettings] = useState<{
supported_entities: string[];
supported_actions: string[];
supported_entities: string[]
supported_actions: string[]
pii_entity_categories: Array<{
category: string;
entities: string[];
}>;
supported_modes: string[];
} | null>(null);
category: string
entities: string[]
}>
supported_modes: string[]
} | null>(null)
const fetchGuardrailInfo = async () => {
try {
setLoading(true);
if (!accessToken) return;
const response = await getGuardrailInfo(accessToken, guardrailId);
setGuardrailData(response);
setLoading(true)
if (!accessToken) return
const response = await getGuardrailInfo(accessToken, guardrailId)
setGuardrailData(response)
// Initialize PII configuration from guardrail data
if (response.litellm_params?.pii_entities_config) {
const piiConfig = response.litellm_params.pii_entities_config;
const piiConfig = response.litellm_params.pii_entities_config
// Clear previous selections
setSelectedPiiEntities([]);
setSelectedPiiActions({});
setSelectedPiiEntities([])
setSelectedPiiActions({})
// Only if there are entities configured
if (Object.keys(piiConfig).length > 0) {
const entities: string[] = [];
const actions: {[key: string]: string} = {};
const entities: string[] = []
const actions: { [key: string]: string } = {}
Object.entries(piiConfig).forEach(([entity, action]: [string, any]) => {
entities.push(entity);
actions[entity] = typeof action === 'string' ? action : "MASK";
});
setSelectedPiiEntities(entities);
setSelectedPiiActions(actions);
entities.push(entity)
actions[entity] = typeof action === "string" ? action : "MASK"
})
setSelectedPiiEntities(entities)
setSelectedPiiActions(actions)
}
} else {
// Clear selections if no PII config exists
setSelectedPiiEntities([]);
setSelectedPiiActions({});
setSelectedPiiEntities([])
setSelectedPiiActions({})
}
} catch (error) {
message.error("Failed to load guardrail information");
console.error("Error fetching guardrail info:", error);
message.error("Failed to load guardrail information")
console.error("Error fetching guardrail info:", error)
} finally {
setLoading(false);
setLoading(false)
}
};
}
const fetchGuardrailProviderSpecificParams = async () => {
try {
if (!accessToken) return;
const response = await getGuardrailProviderSpecificParams(accessToken);
setGuardrailProviderSpecificParams(response);
if (!accessToken) return
const response = await getGuardrailProviderSpecificParams(accessToken)
setGuardrailProviderSpecificParams(response)
} catch (error) {
console.error("Error fetching guardrail provider specific params:", error);
console.error("Error fetching guardrail provider specific params:", error)
}
};
}
const fetchGuardrailUISettings = async () => {
try {
if (!accessToken) return;
const uiSettings = await getGuardrailUISettings(accessToken);
setGuardrailSettings(uiSettings);
if (!accessToken) return
const uiSettings = await getGuardrailUISettings(accessToken)
setGuardrailSettings(uiSettings)
} catch (error) {
console.error("Error fetching guardrail UI settings:", error);
console.error("Error fetching guardrail UI settings:", error)
}
};
}
useEffect(() => {
fetchGuardrailProviderSpecificParams();
}, [accessToken]);
fetchGuardrailProviderSpecificParams()
}, [accessToken])
useEffect(() => {
fetchGuardrailInfo();
fetchGuardrailUISettings();
}, [guardrailId, accessToken]);
fetchGuardrailInfo()
fetchGuardrailUISettings()
}, [guardrailId, accessToken])
// Reset form when guardrail data or provider params change
useEffect(() => {
@ -144,73 +144,71 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({
form.setFieldsValue({
guardrail_name: guardrailData.guardrail_name,
...guardrailData.litellm_params,
guardrail_info: guardrailData.guardrail_info
? JSON.stringify(guardrailData.guardrail_info, null, 2)
: "",
guardrail_info: guardrailData.guardrail_info ? JSON.stringify(guardrailData.guardrail_info, null, 2) : "",
// Include any optional_params if they exist
...(guardrailData.litellm_params?.optional_params && {
optional_params: guardrailData.litellm_params.optional_params
})
});
optional_params: guardrailData.litellm_params.optional_params,
}),
})
}
}, [guardrailData, guardrailProviderSpecificParams, form]);
}, [guardrailData, guardrailProviderSpecificParams, form])
const handlePiiEntitySelect = (entity: string) => {
setSelectedPiiEntities(prev => {
setSelectedPiiEntities((prev) => {
if (prev.includes(entity)) {
return prev.filter(e => e !== entity);
return prev.filter((e) => e !== entity)
} else {
return [...prev, entity];
return [...prev, entity]
}
});
};
})
}
const handlePiiActionSelect = (entity: string, action: string) => {
setSelectedPiiActions(prev => ({
setSelectedPiiActions((prev) => ({
...prev,
[entity]: action
}));
};
[entity]: action,
}))
}
const handleGuardrailUpdate = async (values: any) => {
try {
if (!accessToken) return;
if (!accessToken) return
// Prepare update data object - only include changed fields
const updateData: any = {
litellm_params: {}
};
litellm_params: {},
}
// Only include guardrail_name if it has changed
if (values.guardrail_name !== guardrailData.guardrail_name) {
updateData.guardrail_name = values.guardrail_name;
updateData.guardrail_name = values.guardrail_name
}
// Only include default_on if it has changed
if (values.default_on !== guardrailData.litellm_params?.default_on) {
updateData.litellm_params.default_on = values.default_on;
updateData.litellm_params.default_on = values.default_on
}
// Only include guardrail_info if it has changed
const originalGuardrailInfo = guardrailData.guardrail_info;
const newGuardrailInfo = values.guardrail_info ? JSON.parse(values.guardrail_info) : undefined;
const originalGuardrailInfo = guardrailData.guardrail_info
const newGuardrailInfo = values.guardrail_info ? JSON.parse(values.guardrail_info) : undefined
if (JSON.stringify(originalGuardrailInfo) !== JSON.stringify(newGuardrailInfo)) {
updateData.guardrail_info = newGuardrailInfo;
updateData.guardrail_info = newGuardrailInfo
}
// Only add PII entities config if there are changes
const originalPiiConfig = guardrailData.litellm_params?.pii_entities_config || {};
const newPiiEntitiesConfig: {[key: string]: string} = {};
selectedPiiEntities.forEach(entity => {
newPiiEntitiesConfig[entity] = selectedPiiActions[entity] || "MASK";
});
const originalPiiConfig = guardrailData.litellm_params?.pii_entities_config || {}
const newPiiEntitiesConfig: { [key: string]: string } = {}
selectedPiiEntities.forEach((entity) => {
newPiiEntitiesConfig[entity] = selectedPiiActions[entity] || "MASK"
})
// Only update if PII config has changed
if (JSON.stringify(originalPiiConfig) !== JSON.stringify(newPiiEntitiesConfig)) {
updateData.litellm_params.pii_entities_config = newPiiEntitiesConfig;
updateData.litellm_params.pii_entities_config = newPiiEntitiesConfig
}
/******************************
* Add provider-specific params (reusing logic from add_guardrail_form.tsx)
* ----------------------------------
@ -219,114 +217,113 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({
* Instead of copying every unknown form field, we fetch the list for
* the selected provider and ONLY pass those recognised params.
******************************/
// Get the current provider from the guardrail data
const currentProvider = Object.keys(guardrail_provider_map).find(
key => guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail
);
console.log("values: ", JSON.stringify(values));
console.log("currentProvider: ", currentProvider);
(key) => guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail,
)
console.log("values: ", JSON.stringify(values))
console.log("currentProvider: ", currentProvider)
// Use pre-fetched provider params to copy recognised params
if (guardrailProviderSpecificParams && currentProvider) {
const providerKey = guardrail_provider_map[currentProvider]?.toLowerCase();
const providerSpecificParams = guardrailProviderSpecificParams[providerKey] || {};
const allowedParams = new Set<string>();
const providerKey = guardrail_provider_map[currentProvider]?.toLowerCase()
const providerSpecificParams = guardrailProviderSpecificParams[providerKey] || {}
const allowedParams = new Set<string>()
console.log("providerSpecificParams: ", JSON.stringify(providerSpecificParams))
console.log("providerSpecificParams: ", JSON.stringify(providerSpecificParams));
// Add root-level parameters (like api_key, api_base, api_version)
Object.keys(providerSpecificParams).forEach(paramName => {
if (paramName !== 'optional_params') {
allowedParams.add(paramName);
Object.keys(providerSpecificParams).forEach((paramName) => {
if (paramName !== "optional_params") {
allowedParams.add(paramName)
}
});
})
// Add nested parameters from optional_params.fields
if (providerSpecificParams.optional_params &&
providerSpecificParams.optional_params.fields) {
Object.keys(providerSpecificParams.optional_params.fields).forEach(paramName => {
allowedParams.add(paramName);
});
if (providerSpecificParams.optional_params && providerSpecificParams.optional_params.fields) {
Object.keys(providerSpecificParams.optional_params.fields).forEach((paramName) => {
allowedParams.add(paramName)
})
}
console.log("allowedParams: ", allowedParams);
console.log("allowedParams: ", allowedParams)
allowedParams.forEach((paramName) => {
// Check for both direct parameter name and nested optional_params object
let paramValue = values[paramName];
if (paramValue === undefined || paramValue === null || paramValue === '') {
paramValue = values.optional_params?.[paramName];
let paramValue = values[paramName]
if (paramValue === undefined || paramValue === null || paramValue === "") {
paramValue = values.optional_params?.[paramName]
}
// Get the original value for comparison
const originalValue = guardrailData.litellm_params?.[paramName];
const originalValue = guardrailData.litellm_params?.[paramName]
// Check if the value has changed from the original
const hasChanged = JSON.stringify(paramValue) !== JSON.stringify(originalValue);
const hasChanged = JSON.stringify(paramValue) !== JSON.stringify(originalValue)
// Include if value has changed and has a meaningful value, OR if user explicitly cleared a value
if (hasChanged) {
if (paramValue !== undefined && paramValue !== null && paramValue !== '') {
if (paramValue !== undefined && paramValue !== null && paramValue !== "") {
// User set a new value
updateData.litellm_params[paramName] = paramValue;
} else if (originalValue !== undefined && originalValue !== null && originalValue !== '') {
updateData.litellm_params[paramName] = paramValue
} else if (originalValue !== undefined && originalValue !== null && originalValue !== "") {
// User cleared an existing value - set to null to indicate removal
updateData.litellm_params[paramName] = null;
updateData.litellm_params[paramName] = null
}
}
});
})
}
// Remove empty litellm_params object if no parameters were changed
if (Object.keys(updateData.litellm_params).length === 0) {
delete updateData.litellm_params;
delete updateData.litellm_params
}
// Only proceed with update if there are actual changes
if (Object.keys(updateData).length === 0) {
message.info("No changes detected");
setIsEditing(false);
return;
message.info("No changes detected")
setIsEditing(false)
return
}
await updateGuardrailCall(accessToken, guardrailId, updateData);
message.success("Guardrail updated successfully");
fetchGuardrailInfo();
setIsEditing(false);
await updateGuardrailCall(accessToken, guardrailId, updateData)
message.success("Guardrail updated successfully")
fetchGuardrailInfo()
setIsEditing(false)
} catch (error) {
console.error("Error updating guardrail:", error);
message.error("Failed to update guardrail");
console.error("Error updating guardrail:", error)
message.error("Failed to update guardrail")
}
};
}
if (loading) {
return <div className="p-4">Loading...</div>;
return <div className="p-4">Loading...</div>
}
if (!guardrailData) {
return <div className="p-4">Guardrail not found</div>;
return <div className="p-4">Guardrail not found</div>
}
// Format date helper function
const formatDate = (dateString?: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
return date.toLocaleString();
};
if (!dateString) return "-"
const date = new Date(dateString)
return date.toLocaleString()
}
// Format the provider display name and logo
const { logo, displayName } = getGuardrailLogoAndName(guardrailData.litellm_params?.guardrail || "");
const { logo, displayName } = getGuardrailLogoAndName(guardrailData.litellm_params?.guardrail || "")
return (
<div className="p-4">
<div className="flex justify-between items-center mb-6">
<div>
<Button onClick={onClose} className="mb-4"> Back</Button>
<Title>{guardrailData.guardrail_name || "Unnamed Guardrail"}</Title>
<Text className="text-gray-500 font-mono">{guardrailData.guardrail_id}</Text>
</div>
<div>
<TremorButton icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
Back to Guardrails
</TremorButton>
<Title>{guardrailData.guardrail_name || "Unnamed Guardrail"}</Title>
<Text className="text-gray-500 font-mono">{guardrailData.guardrail_id}</Text>
</div>
<TabGroup>
@ -343,13 +340,13 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({
<Text>Provider</Text>
<div className="mt-2 flex items-center space-x-2">
{logo && (
<img
src={logo}
alt={`${displayName} logo`}
<img
src={logo}
alt={`${displayName} logo`}
className="w-6 h-6"
onError={(e) => {
// Hide broken image
(e.target as HTMLImageElement).style.display = 'none';
;(e.target as HTMLImageElement).style.display = "none"
}}
/>
)}
@ -376,16 +373,17 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({
</Card>
</Grid>
{guardrailData.litellm_params?.pii_entities_config && Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && (
<Card className="mt-6">
<div className="flex justify-between items-center">
<Text className="font-medium">PII Protection</Text>
<Badge color="blue">
{Object.keys(guardrailData.litellm_params.pii_entities_config).length} PII entities configured
</Badge>
</div>
</Card>
)}
{guardrailData.litellm_params?.pii_entities_config &&
Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && (
<Card className="mt-6">
<div className="flex justify-between items-center">
<Text className="font-medium">PII Protection</Text>
<Badge color="blue">
{Object.keys(guardrailData.litellm_params.pii_entities_config).length} PII entities configured
</Badge>
</div>
</Card>
)}
{guardrailData.guardrail_info && Object.keys(guardrailData.guardrail_info).length > 0 && (
<Card className="mt-6">
@ -395,9 +393,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({
<div key={key} className="flex">
<Text className="font-medium w-1/3">{key}</Text>
<Text className="w-2/3">
{typeof value === 'object'
? JSON.stringify(value, null, 2)
: String(value)}
{typeof value === "object" ? JSON.stringify(value, null, 2) : String(value)}
</Text>
</div>
))}
@ -412,13 +408,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({
<Card>
<div className="flex justify-between items-center mb-4">
<Title>Guardrail Settings</Title>
{!isEditing && (
<TremorButton
onClick={() => setIsEditing(true)}
>
Edit Settings
</TremorButton>
)}
{!isEditing && <TremorButton onClick={() => setIsEditing(true)}>Edit Settings</TremorButton>}
</div>
{isEditing ? (
@ -428,13 +418,13 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({
initialValues={{
guardrail_name: guardrailData.guardrail_name,
...guardrailData.litellm_params,
guardrail_info: guardrailData.guardrail_info
? JSON.stringify(guardrailData.guardrail_info, null, 2)
guardrail_info: guardrailData.guardrail_info
? JSON.stringify(guardrailData.guardrail_info, null, 2)
: "",
// Include any optional_params if they exist
...(guardrailData.litellm_params?.optional_params && {
optional_params: guardrailData.litellm_params.optional_params
})
optional_params: guardrailData.litellm_params.optional_params,
}),
}}
layout="vertical"
>
@ -445,86 +435,77 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({
>
<TextInput />
</Form.Item>
<Form.Item
label="Default On"
name="default_on"
>
<Form.Item label="Default On" name="default_on">
<Select>
<Select.Option value={true}>Yes</Select.Option>
<Select.Option value={false}>No</Select.Option>
</Select>
</Form.Item>
{guardrailData.litellm_params?.guardrail === "presidio" && (
<>
<Divider orientation="left">PII Protection</Divider>
<div className="mb-6">
{guardrailSettings && (
<PiiConfiguration
entities={guardrailSettings.supported_entities}
actions={guardrailSettings.supported_actions}
selectedEntities={selectedPiiEntities}
selectedActions={selectedPiiActions}
onEntitySelect={handlePiiEntitySelect}
onActionSelect={handlePiiActionSelect}
entityCategories={guardrailSettings.pii_entity_categories}
/>
)}
</div>
<Divider orientation="left">PII Protection</Divider>
<div className="mb-6">
{guardrailSettings && (
<PiiConfiguration
entities={guardrailSettings.supported_entities}
actions={guardrailSettings.supported_actions}
selectedEntities={selectedPiiEntities}
selectedActions={selectedPiiActions}
onEntitySelect={handlePiiEntitySelect}
onActionSelect={handlePiiActionSelect}
entityCategories={guardrailSettings.pii_entity_categories}
/>
)}
</div>
</>
)}
<Divider orientation="left">Provider Settings</Divider>
{/* Provider-specific fields */}
<GuardrailProviderFields
selectedProvider={Object.keys(guardrail_provider_map).find(
key => guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail
) || null}
accessToken={accessToken}
<GuardrailProviderFields
selectedProvider={
Object.keys(guardrail_provider_map).find(
(key) => guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail,
) || null
}
accessToken={accessToken}
providerParams={guardrailProviderSpecificParams}
value={guardrailData.litellm_params}
/>
{/* Optional parameters */}
{guardrailProviderSpecificParams && (
{guardrailProviderSpecificParams &&
(() => {
const currentProvider = Object.keys(guardrail_provider_map).find(
key => guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail
);
if (!currentProvider) return null;
const providerKey = guardrail_provider_map[currentProvider]?.toLowerCase();
const providerFields = guardrailProviderSpecificParams[providerKey];
if (!providerFields || !providerFields.optional_params) return null;
(key) => guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail,
)
if (!currentProvider) return null
const providerKey = guardrail_provider_map[currentProvider]?.toLowerCase()
const providerFields = guardrailProviderSpecificParams[providerKey]
if (!providerFields || !providerFields.optional_params) return null
return (
<GuardrailOptionalParams
optionalParams={providerFields.optional_params}
parentFieldKey="optional_params"
values={guardrailData.litellm_params}
/>
);
})()
)}
)
})()}
<Divider orientation="left">Advanced Settings</Divider>
<Form.Item
label="Guardrail Information"
name="guardrail_info"
>
<Form.Item label="Guardrail Information" name="guardrail_info">
<Input.TextArea rows={5} />
</Form.Item>
<div className="flex justify-end gap-2 mt-6">
<Button onClick={() => setIsEditing(false)}>
Cancel
</Button>
<TremorButton>
Save Changes
</TremorButton>
<Button onClick={() => setIsEditing(false)}>Cancel</Button>
<TremorButton>Save Changes</TremorButton>
</div>
</Form>
) : (
@ -551,18 +532,20 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({
{guardrailData.litellm_params?.default_on ? "Yes" : "No"}
</Badge>
</div>
{guardrailData.litellm_params?.pii_entities_config && Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && (
<div>
<Text className="font-medium">PII Protection</Text>
<div className="mt-2">
<Badge color="blue">
{Object.keys(guardrailData.litellm_params.pii_entities_config).length} PII entities configured
</Badge>
{guardrailData.litellm_params?.pii_entities_config &&
Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && (
<div>
<Text className="font-medium">PII Protection</Text>
<div className="mt-2">
<Badge color="blue">
{Object.keys(guardrailData.litellm_params.pii_entities_config).length} PII entities
configured
</Badge>
</div>
</div>
</div>
)}
)}
<div>
<Text className="font-medium">Created At</Text>
<div>{formatDate(guardrailData.created_at)}</div>
@ -579,7 +562,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({
</TabPanels>
</TabGroup>
</div>
);
};
)
}
export default GuardrailInfoView;
export default GuardrailInfoView

View file

@ -1,22 +1,8 @@
import React, { useState } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
Icon,
Button,
} from "@tremor/react";
import {
TrashIcon,
SwitchVerticalIcon,
ChevronUpIcon,
ChevronDownIcon,
} from "@heroicons/react/outline";
import { Tooltip } from "antd";
import { Badge } from "@tremor/react";
import React, { useState } from "react"
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon, Button } from "@tremor/react"
import { TrashIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline"
import { Tooltip } from "antd"
import { Badge } from "@tremor/react"
import {
ColumnDef,
flexRender,
@ -24,34 +10,33 @@ import {
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
import { getGuardrailLogoAndName, guardrail_provider_map } from "./guardrail_info_helpers";
import EditGuardrailForm from "./edit_guardrail_form";
import GuardrailInfoView from "./guardrail_info";
} from "@tanstack/react-table"
import { getGuardrailLogoAndName, guardrail_provider_map } from "./guardrail_info_helpers"
import EditGuardrailForm from "./edit_guardrail_form"
interface GuardrailItem {
guardrail_id?: string;
guardrail_name: string | null;
guardrail_id?: string
guardrail_name: string | null
litellm_params: {
guardrail: string;
mode: string;
default_on: boolean;
pii_entities_config?: {[key: string]: string};
[key: string]: any;
};
guardrail_info: Record<string, any> | null;
created_at?: string;
updated_at?: string;
guardrail: string
mode: string
default_on: boolean
pii_entities_config?: { [key: string]: string }
[key: string]: any
}
guardrail_info: Record<string, any> | null
created_at?: string
updated_at?: string
}
interface GuardrailTableProps {
guardrailsList: GuardrailItem[];
isLoading: boolean;
onDeleteClick: (guardrailId: string, guardrailName: string) => void;
accessToken: string | null;
onGuardrailUpdated: () => void;
isAdmin?: boolean;
onShowGuardrailInfo?: (isVisible: boolean) => void;
guardrailsList: GuardrailItem[]
isLoading: boolean
onDeleteClick: (guardrailId: string, guardrailName: string) => void
accessToken: string | null
onGuardrailUpdated: () => void
isAdmin?: boolean
onGuardrailClick: (id: string) => void
}
const GuardrailTable: React.FC<GuardrailTableProps> = ({
@ -61,52 +46,29 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
accessToken,
onGuardrailUpdated,
isAdmin = false,
onShowGuardrailInfo,
onGuardrailClick,
}) => {
const [sorting, setSorting] = useState<SortingState>([
{ id: "created_at", desc: true }
]);
const [editModalVisible, setEditModalVisible] = useState(false);
const [selectedGuardrail, setSelectedGuardrail] = useState<GuardrailItem | null>(null);
const [showGuardrailInfo, setShowGuardrailInfo] = useState(false);
const [selectedGuardrailId, setSelectedGuardrailId] = useState<string | null>(null);
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }])
const [editModalVisible, setEditModalVisible] = useState(false)
const [selectedGuardrail, setSelectedGuardrail] = useState<GuardrailItem | null>(null)
// Format date helper function
const formatDate = (dateString?: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
return date.toLocaleString();
};
if (!dateString) return "-"
const date = new Date(dateString)
return date.toLocaleString()
}
const handleEditClick = (guardrail: GuardrailItem) => {
setSelectedGuardrail(guardrail);
setEditModalVisible(true);
};
setSelectedGuardrail(guardrail)
setEditModalVisible(true)
}
const handleEditSuccess = () => {
setEditModalVisible(false);
setSelectedGuardrail(null);
onGuardrailUpdated();
};
const handleGuardrailIdClick = (guardrailId: string) => {
setSelectedGuardrailId(guardrailId);
setShowGuardrailInfo(true);
onShowGuardrailInfo?.(true);
};
const handleGuardrailInfoClose = () => {
setShowGuardrailInfo(false);
setSelectedGuardrailId(null);
onShowGuardrailInfo?.(false);
};
const handleGuardrailDeleted = () => {
setShowGuardrailInfo(false);
setSelectedGuardrailId(null);
onShowGuardrailInfo?.(false);
onGuardrailUpdated();
};
setEditModalVisible(false)
setSelectedGuardrail(null)
onGuardrailUpdated()
}
const columns: ColumnDef<GuardrailItem>[] = [
{
@ -114,11 +76,11 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
accessorKey: "guardrail_id",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "")}>
<Button
<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]"
onClick={() => info.getValue() && handleGuardrailIdClick(info.getValue())}
onClick={() => info.getValue() && onGuardrailClick(info.getValue())}
>
{info.getValue() ? `${String(info.getValue()).slice(0, 7)}...` : ""}
</Button>
@ -129,115 +91,108 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
header: "Name",
accessorKey: "guardrail_name",
cell: ({ row }) => {
const guardrail = row.original;
const guardrail = row.original
return (
<Tooltip title={guardrail.guardrail_name}>
<span className="text-xs font-medium">
{guardrail.guardrail_name || "-"}
</span>
<span className="text-xs font-medium">{guardrail.guardrail_name || "-"}</span>
</Tooltip>
);
)
},
},
{
header: "Provider",
accessorKey: "litellm_params.guardrail",
cell: ({ row }) => {
const guardrail = row.original;
const { logo, displayName } = getGuardrailLogoAndName(guardrail.litellm_params.guardrail);
const guardrail = row.original
const { logo, displayName } = getGuardrailLogoAndName(guardrail.litellm_params.guardrail)
return (
<div className="flex items-center space-x-2">
{logo && (
<img
src={logo}
alt={`${displayName} logo`}
<img
src={logo}
alt={`${displayName} logo`}
className="w-4 h-4"
onError={(e) => {
// Hide broken image
(e.target as HTMLImageElement).style.display = 'none';
;(e.target as HTMLImageElement).style.display = "none"
}}
/>
)}
<span className="text-xs">{displayName}</span>
</div>
);
)
},
},
{
header: "Mode",
accessorKey: "litellm_params.mode",
cell: ({ row }) => {
const guardrail = row.original;
return (
<span className="text-xs">
{guardrail.litellm_params.mode}
</span>
);
const guardrail = row.original
return <span className="text-xs">{guardrail.litellm_params.mode}</span>
},
},
{
header: "Default On",
accessorKey: "litellm_params.default_on",
cell: ({ row }) => {
const guardrail = row.original;
const guardrail = row.original
return (
<Badge
<Badge
color={guardrail.litellm_params?.default_on ? "green" : "gray"}
className="text-xs font-normal"
size="xs"
>
{guardrail.litellm_params?.default_on ? "Default On" : "Default Off"}
</Badge>
);
)
},
},
{
header: "Created At",
accessorKey: "created_at",
cell: ({ row }) => {
const guardrail = row.original;
const guardrail = row.original
return (
<Tooltip title={guardrail.created_at}>
<span className="text-xs">
{formatDate(guardrail.created_at)}
</span>
<span className="text-xs">{formatDate(guardrail.created_at)}</span>
</Tooltip>
);
)
},
},
{
header: "Updated At",
accessorKey: "updated_at",
cell: ({ row }) => {
const guardrail = row.original;
const guardrail = row.original
return (
<Tooltip title={guardrail.updated_at}>
<span className="text-xs">
{formatDate(guardrail.updated_at)}
</span>
<span className="text-xs">{formatDate(guardrail.updated_at)}</span>
</Tooltip>
);
)
},
},
{
id: "actions",
header: "",
cell: ({ row }) => {
const guardrail = row.original;
const guardrail = row.original
return (
<div className="flex space-x-2">
<Icon
icon={TrashIcon}
size="sm"
onClick={() => guardrail.guardrail_id && onDeleteClick(guardrail.guardrail_id, guardrail.guardrail_name || 'Unnamed Guardrail')}
onClick={() =>
guardrail.guardrail_id &&
onDeleteClick(guardrail.guardrail_id, guardrail.guardrail_name || "Unnamed Guardrail")
}
className="cursor-pointer hover:text-red-500"
tooltip="Delete guardrail"
/>
</div>
);
)
},
},
];
]
const table = useReactTable({
data: guardrailsList,
@ -249,19 +204,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
enableSorting: true,
});
// If showing guardrail info, render the GuardrailInfoView
if (showGuardrailInfo && selectedGuardrailId) {
return (
<GuardrailInfoView
guardrailId={selectedGuardrailId}
onClose={handleGuardrailInfoClose}
accessToken={accessToken}
isAdmin={isAdmin}
/>
);
}
})
return (
<div className="rounded-lg custom-border relative">
@ -274,27 +217,20 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
<TableHeaderCell
key={header.id}
className={`py-1 h-8 ${
header.id === 'actions'
? 'sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]'
: ''
header.id === "actions" ? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]" : ""
}`}
onClick={header.column.getToggleSortingHandler()}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center">
{header.isPlaceholder ? null : (
flexRender(
header.column.columnDef.header,
header.getContext()
)
)}
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</div>
{header.id !== 'actions' && (
{header.id !== "actions" && (
<div className="w-4">
{header.column.getIsSorted() ? (
{
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
}[header.column.getIsSorted() as string]
) : (
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
@ -323,9 +259,9 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
<TableCell
key={cell.id}
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${
cell.column.id === 'actions'
? 'sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]'
: ''
cell.column.id === "actions"
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
: ""
}`}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
@ -353,21 +289,22 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
onClose={() => setEditModalVisible(false)}
accessToken={accessToken}
onSuccess={handleEditSuccess}
guardrailId={selectedGuardrail.guardrail_id || ''}
guardrailId={selectedGuardrail.guardrail_id || ""}
initialValues={{
guardrail_name: selectedGuardrail.guardrail_name || '',
provider: Object.keys(guardrail_provider_map).find(
key => guardrail_provider_map[key] === selectedGuardrail?.litellm_params.guardrail
) || '',
guardrail_name: selectedGuardrail.guardrail_name || "",
provider:
Object.keys(guardrail_provider_map).find(
(key) => guardrail_provider_map[key] === selectedGuardrail?.litellm_params.guardrail,
) || "",
mode: selectedGuardrail.litellm_params.mode,
default_on: selectedGuardrail.litellm_params.default_on,
pii_entities_config: selectedGuardrail.litellm_params.pii_entities_config,
...selectedGuardrail.guardrail_info
...selectedGuardrail.guardrail_info,
}}
/>
)}
</div>
);
};
)
}
export default GuardrailTable;
export default GuardrailTable

View file

@ -1,88 +1,83 @@
import React, { useState } from "react";
import {
Modal,
Tooltip,
Form,
Select,
message,
Button as AntdButton,
Input,
} from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Button, TextInput } from "@tremor/react";
import { createMCPServer } from "../networking";
import { MCPServer, MCPServerCostInfo } from "./types";
import MCPServerCostConfig from "./mcp_server_cost_config";
import MCPConnectionStatus from "./mcp_connection_status";
import StdioConfiguration from "./StdioConfiguration";
import { isAdminRole } from "@/utils/roles";
import React, { useState } from "react"
import { Modal, Tooltip, Form, Select, message, Button as AntdButton, Input } from "antd"
import { InfoCircleOutlined } from "@ant-design/icons"
import { Button, TextInput } from "@tremor/react"
import { createMCPServer } from "../networking"
import { MCPServer, MCPServerCostInfo } from "./types"
import MCPServerCostConfig from "./mcp_server_cost_config"
import MCPConnectionStatus from "./mcp_connection_status"
import StdioConfiguration from "./StdioConfiguration"
import { isAdminRole } from "@/utils/roles"
const asset_logos_folder = '../ui/assets/logos/';
export const mcpLogoImg = `${asset_logos_folder}mcp_logo.png`;
const asset_logos_folder = "../ui/assets/logos/"
export const mcpLogoImg = `${asset_logos_folder}mcp_logo.png`
interface CreateMCPServerProps {
userRole: string;
accessToken: string | null;
onCreateSuccess: (newMcpServer: MCPServer) => void;
userRole: string
accessToken: string | null
onCreateSuccess: (newMcpServer: MCPServer) => void
isModalVisible: boolean
setModalVisible: (visible: boolean) => void
}
const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
userRole,
accessToken,
onCreateSuccess,
isModalVisible,
setModalVisible,
}) => {
const [form] = Form.useForm();
const [isLoading, setIsLoading] = useState(false);
const [costConfig, setCostConfig] = useState<MCPServerCostInfo>({});
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
const [formValues, setFormValues] = useState<Record<string, any>>({});
const [tools, setTools] = useState<any[]>([]);
const [transportType, setTransportType] = useState<string>('sse');
const [form] = Form.useForm()
const [isLoading, setIsLoading] = useState(false)
const [costConfig, setCostConfig] = useState<MCPServerCostInfo>({})
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([])
const [formValues, setFormValues] = useState<Record<string, any>>({})
const [tools, setTools] = useState<any[]>([])
const [transportType, setTransportType] = useState<string>("sse")
const handleCreate = async (formValues: Record<string, any>) => {
setIsLoading(true);
setIsLoading(true)
try {
// Transform access groups into objects with name property
const accessGroups = formValues.mcp_access_groups
// Process stdio configuration if present
let stdioFields = {};
if (formValues.stdio_config && transportType === 'stdio') {
let stdioFields = {}
if (formValues.stdio_config && transportType === "stdio") {
try {
const stdioConfig = JSON.parse(formValues.stdio_config);
const stdioConfig = JSON.parse(formValues.stdio_config)
// Handle both formats:
// 1. Full mcpServers structure: {"mcpServers": {"server-name": {...}}}
// 2. Direct config: {"command": "...", "args": [...], "env": {...}}
let actualConfig = stdioConfig;
let actualConfig = stdioConfig
// If it's the full mcpServers structure, extract the first server config
if (stdioConfig.mcpServers && typeof stdioConfig.mcpServers === 'object') {
const serverNames = Object.keys(stdioConfig.mcpServers);
if (stdioConfig.mcpServers && typeof stdioConfig.mcpServers === "object") {
const serverNames = Object.keys(stdioConfig.mcpServers)
if (serverNames.length > 0) {
const firstServerName = serverNames[0];
actualConfig = stdioConfig.mcpServers[firstServerName];
const firstServerName = serverNames[0]
actualConfig = stdioConfig.mcpServers[firstServerName]
// If no alias is provided, use the server name from the JSON
if (!formValues.alias) {
formValues.alias = firstServerName.replace(/-/g, '_'); // Replace hyphens with underscores
formValues.alias = firstServerName.replace(/-/g, "_") // Replace hyphens with underscores
}
}
}
stdioFields = {
command: actualConfig.command,
args: actualConfig.args,
env: actualConfig.env
};
console.log('Parsed stdio config:', stdioFields);
env: actualConfig.env,
}
console.log("Parsed stdio config:", stdioFields)
} catch (error) {
message.error("Invalid JSON in stdio configuration");
return;
message.error("Invalid JSON in stdio configuration")
return
}
}
@ -97,305 +92,252 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
description: formValues.description,
mcp_server_cost_info: Object.keys(costConfig).length > 0 ? costConfig : null,
},
mcp_access_groups: accessGroups
};
mcp_access_groups: accessGroups,
}
console.log(`Payload: ${JSON.stringify(payload)}`);
console.log(`Payload: ${JSON.stringify(payload)}`)
if (accessToken != null) {
const response = await createMCPServer(
accessToken,
payload
);
const response = await createMCPServer(accessToken, payload)
message.success("MCP Server created successfully");
form.resetFields();
setCostConfig({});
setTools([]);
setModalVisible(false);
onCreateSuccess(response);
message.success("MCP Server created successfully")
form.resetFields()
setCostConfig({})
setTools([])
setModalVisible(false)
onCreateSuccess(response)
}
} catch (error) {
message.error("Error creating MCP Server: " + error, 20);
message.error("Error creating MCP Server: " + error, 20)
} finally {
setIsLoading(false);
setIsLoading(false)
}
};
}
// state
const [isModalVisible, setModalVisible] = useState(false);
const handleCancel = () => {
form.resetFields();
setCostConfig({});
setTools([]);
setModalVisible(false);
};
form.resetFields()
setCostConfig({})
setTools([])
setModalVisible(false)
}
const handleTransportChange = (value: string) => {
setTransportType(value);
setTransportType(value)
// Clear fields that are not relevant for the selected transport
if (value === 'stdio') {
form.setFieldsValue({ url: undefined, auth_type: undefined });
if (value === "stdio") {
form.setFieldsValue({ url: undefined, auth_type: undefined })
} else {
form.setFieldsValue({ command: undefined, args: undefined, env: undefined });
form.setFieldsValue({ command: undefined, args: undefined, env: undefined })
}
};
}
// rendering
if (!isAdminRole(userRole)) {
return null;
return null
}
return (
<div>
<Button
className="mx-auto mb-4"
onClick={() => setModalVisible(true)}
>
+ Add New MCP Server
</Button>
<Modal
title={
<div className="flex items-center space-x-3 pb-4 border-b border-gray-100">
<img
src={mcpLogoImg}
alt="MCP Logo"
className="w-8 h-8 object-contain"
style={{
height: '20px',
width: '20px',
marginRight: '8px',
objectFit: 'contain'
}}
/>
<h2 className="text-xl font-semibold text-gray-900">Add New MCP Server</h2>
</div>
}
open={isModalVisible}
width={1000}
onCancel={handleCancel}
footer={null}
className="top-8"
styles={{
body: { padding: '24px' },
header: { padding: '24px 24px 0 24px', border: 'none' },
}}
>
<div className="mt-6">
<Form
form={form}
onFinish={handleCreate}
onValuesChange={(_, allValues) => setFormValues(allValues)}
layout="vertical"
className="space-y-6"
>
<div className="grid grid-cols-1 gap-6">
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
MCP Server Name
<Tooltip title="Best practice: Use a descriptive name that indicates the server's purpose (e.g., 'GitHub_MCP', 'Email_Service'). Hyphens '-' are not allowed; use underscores '_' instead.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="alias"
rules={[
{ required: false, message: "Please enter a server name" },
{
validator: (_, value) =>
value && value.includes('-')
? Promise.reject("Server name cannot contain '-' (hyphen). Please use '_' (underscore) instead.")
: Promise.resolve(),
},
]}
>
<TextInput
placeholder="e.g., GitHub_MCP, Zapier_MCP, etc."
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<Form.Item
label={<span className="text-sm font-medium text-gray-700">Description</span>}
name="description"
rules={[
{
required: false,
message: "Please enter a server description",
},
]}
>
<TextInput
placeholder="Brief description of what this server does"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700">
Transport Type
</span>
}
name="transport"
rules={[{ required: true, message: "Please select a transport type" }]}
>
<Select
placeholder="Select transport"
className="rounded-lg"
size="large"
onChange={handleTransportChange}
value={transportType}
>
<Select.Option value="http">HTTP</Select.Option>
<Select.Option value="sse">Server-Sent Events (SSE)</Select.Option>
<Select.Option value="stdio">Standard Input/Output (stdio)</Select.Option>
</Select>
</Form.Item>
{/* URL field - only show for HTTP and SSE */}
{transportType !== 'stdio' && (
<Form.Item
label={
<span className="text-sm font-medium text-gray-700">
MCP Server URL
</span>
}
name="url"
rules={[
{ required: true, message: "Please enter a server URL" },
{ type: 'url', message: "Please enter a valid URL" }
]}
>
<TextInput
placeholder="https://your-mcp-server.com"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
)}
{/* Authentication - only show for HTTP and SSE */}
{transportType !== 'stdio' && (
<Form.Item
label={
<span className="text-sm font-medium text-gray-700">
Authentication
</span>
}
name="auth_type"
rules={[{ required: true, message: "Please select an auth type" }]}
>
<Select
placeholder="Select auth type"
className="rounded-lg"
size="large"
>
<Select.Option value="none">None</Select.Option>
<Select.Option value="api_key">API Key</Select.Option>
<Select.Option value="bearer_token">Bearer Token</Select.Option>
<Select.Option value="basic">Basic Auth</Select.Option>
</Select>
</Form.Item>
)}
{/* Stdio Configuration - only show for stdio transport */}
<StdioConfiguration isVisible={transportType === 'stdio'} />
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
MCP Version
<Tooltip title="Select the MCP specification version your server supports">
<InfoCircleOutlined className="ml-2 text-gray-400 hover:text-gray-600" />
</Tooltip>
</span>
}
name="spec_version"
rules={[
{ required: true, message: "Please select a spec version" },
]}
>
<Select
placeholder="Select MCP version"
className="rounded-lg"
size="large"
>
<Select.Option value="2025-03-26">2025-03-26 (Latest)</Select.Option>
<Select.Option value="2024-11-05">2024-11-05</Select.Option>
</Select>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
MCP Access Groups
<Tooltip title="Specify access groups for this MCP server. Users must be in at least one of these groups to access the server.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="mcp_access_groups"
className="mb-4"
>
<Select
mode="tags"
showSearch
placeholder="Select existing groups or type to create new ones"
optionFilterProp="children"
tokenSeparators={[',']}
options={mcpAccessGroups.map((group) => ({
value: group,
label: group
}))}
maxTagCount="responsive"
allowClear
/>
</Form.Item>
</div>
{/* Connection Status Section */}
<div className="mt-8 pt-6 border-t border-gray-200">
<MCPConnectionStatus
accessToken={accessToken}
formValues={formValues}
onToolsLoaded={setTools}
/>
</div>
{/* Cost Configuration Section */}
<div className="mt-6">
<MCPServerCostConfig
value={costConfig}
onChange={setCostConfig}
tools={tools}
disabled={false}
/>
</div>
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-gray-100">
<Button
variant="secondary"
onClick={handleCancel}
>
Cancel
</Button>
<Button
variant="primary"
loading={isLoading}
>
{isLoading ? 'Creating...' : 'Add MCP Server'}
</Button>
</div>
</Form>
<Modal
title={
<div className="flex items-center space-x-3 pb-4 border-b border-gray-100">
<img
src={mcpLogoImg}
alt="MCP Logo"
className="w-8 h-8 object-contain"
style={{
height: "20px",
width: "20px",
marginRight: "8px",
objectFit: "contain",
}}
/>
<h2 className="text-xl font-semibold text-gray-900">Add New MCP Server</h2>
</div>
</Modal>
}
open={isModalVisible}
width={1000}
onCancel={handleCancel}
footer={null}
className="top-8"
styles={{
body: { padding: "24px" },
header: { padding: "24px 24px 0 24px", border: "none" },
}}
>
<div className="mt-6">
<Form
form={form}
onFinish={handleCreate}
onValuesChange={(_, allValues) => setFormValues(allValues)}
layout="vertical"
className="space-y-6"
>
<div className="grid grid-cols-1 gap-6">
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
MCP Server Name
<Tooltip title="Best practice: Use a descriptive name that indicates the server's purpose (e.g., 'GitHub_MCP', 'Email_Service'). Hyphens '-' are not allowed; use underscores '_' instead.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="alias"
rules={[
{ required: false, message: "Please enter a server name" },
{
validator: (_, value) =>
value && value.includes("-")
? Promise.reject("Server name cannot contain '-' (hyphen). Please use '_' (underscore) instead.")
: Promise.resolve(),
},
]}
>
<TextInput
placeholder="e.g., GitHub_MCP, Zapier_MCP, etc."
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<Form.Item
label={<span className="text-sm font-medium text-gray-700">Description</span>}
name="description"
rules={[
{
required: false,
message: "Please enter a server description",
},
]}
>
<TextInput
placeholder="Brief description of what this server does"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
</div>
);
};
<Form.Item
label={<span className="text-sm font-medium text-gray-700">Transport Type</span>}
name="transport"
rules={[{ required: true, message: "Please select a transport type" }]}
>
<Select
placeholder="Select transport"
className="rounded-lg"
size="large"
onChange={handleTransportChange}
value={transportType}
>
<Select.Option value="http">HTTP</Select.Option>
<Select.Option value="sse">Server-Sent Events (SSE)</Select.Option>
<Select.Option value="stdio">Standard Input/Output (stdio)</Select.Option>
</Select>
</Form.Item>
export default CreateMCPServer;
{/* URL field - only show for HTTP and SSE */}
{transportType !== "stdio" && (
<Form.Item
label={<span className="text-sm font-medium text-gray-700">MCP Server URL</span>}
name="url"
rules={[
{ required: true, message: "Please enter a server URL" },
{ type: "url", message: "Please enter a valid URL" },
]}
>
<TextInput
placeholder="https://your-mcp-server.com"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
)}
{/* Authentication - only show for HTTP and SSE */}
{transportType !== "stdio" && (
<Form.Item
label={<span className="text-sm font-medium text-gray-700">Authentication</span>}
name="auth_type"
rules={[{ required: true, message: "Please select an auth type" }]}
>
<Select placeholder="Select auth type" className="rounded-lg" size="large">
<Select.Option value="none">None</Select.Option>
<Select.Option value="api_key">API Key</Select.Option>
<Select.Option value="bearer_token">Bearer Token</Select.Option>
<Select.Option value="basic">Basic Auth</Select.Option>
</Select>
</Form.Item>
)}
{/* Stdio Configuration - only show for stdio transport */}
<StdioConfiguration isVisible={transportType === "stdio"} />
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
MCP Version
<Tooltip title="Select the MCP specification version your server supports">
<InfoCircleOutlined className="ml-2 text-gray-400 hover:text-gray-600" />
</Tooltip>
</span>
}
name="spec_version"
rules={[{ required: true, message: "Please select a spec version" }]}
>
<Select placeholder="Select MCP version" className="rounded-lg" size="large">
<Select.Option value="2025-03-26">2025-03-26 (Latest)</Select.Option>
<Select.Option value="2024-11-05">2024-11-05</Select.Option>
</Select>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
MCP Access Groups
<Tooltip title="Specify access groups for this MCP server. Users must be in at least one of these groups to access the server.">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
name="mcp_access_groups"
className="mb-4"
>
<Select
mode="tags"
showSearch
placeholder="Select existing groups or type to create new ones"
optionFilterProp="children"
tokenSeparators={[","]}
options={mcpAccessGroups.map((group) => ({
value: group,
label: group,
}))}
maxTagCount="responsive"
allowClear
/>
</Form.Item>
</div>
{/* Connection Status Section */}
<div className="mt-8 pt-6 border-t border-gray-200">
<MCPConnectionStatus accessToken={accessToken} formValues={formValues} onToolsLoaded={setTools} />
</div>
{/* Cost Configuration Section */}
<div className="mt-6">
<MCPServerCostConfig value={costConfig} onChange={setCostConfig} tools={tools} disabled={false} />
</div>
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-gray-100">
<Button variant="secondary" onClick={handleCancel}>
Cancel
</Button>
<Button variant="primary" loading={isLoading}>
{isLoading ? "Creating..." : "Add MCP Server"}
</Button>
</div>
</Form>
</div>
</Modal>
)
}
export default CreateMCPServer

View file

@ -1,34 +1,22 @@
import React, { useState } from "react";
import { EyeIcon, EyeOffIcon } from "@heroicons/react/outline";
import {
Title,
Card,
Button,
Text,
Grid,
TabGroup,
TabList,
TabPanel,
TabPanels,
Tab,
Icon,
} from "@tremor/react";
import React, { useState } from "react"
import { ArrowLeftIcon, EyeIcon, EyeOffIcon } from "@heroicons/react/outline"
import { Title, Card, Button, Text, Grid, TabGroup, TabList, TabPanel, TabPanels, Tab, Icon } from "@tremor/react"
import { MCPServer, handleTransport, handleAuth } from "./types";
import { MCPServer, handleTransport, handleAuth } from "./types"
// TODO: Move Tools viewer from index file
import { MCPToolsViewer } from ".";
import MCPServerEdit from "./mcp_server_edit";
import MCPServerCostDisplay from "./mcp_server_cost_display";
import { getMaskedAndFullUrl } from "./utils";
import { MCPToolsViewer } from "."
import MCPServerEdit from "./mcp_server_edit"
import MCPServerCostDisplay from "./mcp_server_cost_display"
import { getMaskedAndFullUrl } from "./utils"
interface MCPServerViewProps {
mcpServer: MCPServer;
onBack: () => void;
isProxyAdmin: boolean;
isEditing: boolean;
accessToken: string | null;
userRole: string | null;
userID: string | null;
mcpServer: MCPServer
onBack: () => void
isProxyAdmin: boolean
isEditing: boolean
accessToken: string | null
userRole: string | null
userID: string | null
}
export const MCPServerView: React.FC<MCPServerViewProps> = ({
@ -40,27 +28,27 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
userRole,
userID,
}) => {
const [editing, setEditing] = useState(isEditing);
const [showFullUrl, setShowFullUrl] = useState(false);
const [editing, setEditing] = useState(isEditing)
const [showFullUrl, setShowFullUrl] = useState(false)
const handleSuccess = (updated: MCPServer) => {
setEditing(false);
onBack();
};
setEditing(false)
onBack()
}
const { maskedUrl, hasToken } = getMaskedAndFullUrl(mcpServer.url);
const { maskedUrl, hasToken } = getMaskedAndFullUrl(mcpServer.url)
const renderUrlWithToggle = (url: string, showFull: boolean) => {
if (!hasToken) return url;
return showFull ? url : maskedUrl;
};
if (!hasToken) return url
return showFull ? url : maskedUrl
}
return (
<div className="p-4 max-w-full">
<div className="flex justify-between items-center mb-6">
<div>
<Button onClick={onBack} className="mb-4">
Back
<Button icon={ArrowLeftIcon} variant="light" className="mb-4">
Back to All Servers
</Button>
<Title>{mcpServer.alias}</Title>
<Text className="text-gray-500 font-mono">{mcpServer.server_id}</Text>
@ -102,15 +90,8 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
{renderUrlWithToggle(mcpServer.url, showFullUrl)}
</Text>
{hasToken && (
<button
onClick={() => setShowFullUrl(!showFullUrl)}
className="p-1 hover:bg-gray-100 rounded"
>
<Icon
icon={showFullUrl ? EyeOffIcon : EyeIcon}
size="sm"
className="text-gray-500"
/>
<button onClick={() => setShowFullUrl(!showFullUrl)} className="p-1 hover:bg-gray-100 rounded">
<Icon icon={showFullUrl ? EyeOffIcon : EyeIcon} size="sm" className="text-gray-500" />
</button>
)}
</div>
@ -120,7 +101,6 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
<Title>Cost Configuration</Title>
<MCPServerCostDisplay costConfig={mcpServer.mcp_info?.mcp_server_cost_info} />
</Card>
</TabPanel>
{/* Tool Panel */}
@ -167,15 +147,8 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
<div className="font-mono break-all overflow-wrap-anywhere max-w-full flex items-center gap-2">
{renderUrlWithToggle(mcpServer.url, showFullUrl)}
{hasToken && (
<button
onClick={() => setShowFullUrl(!showFullUrl)}
className="p-1 hover:bg-gray-100 rounded"
>
<Icon
icon={showFullUrl ? EyeOffIcon : EyeIcon}
size="sm"
className="text-gray-500"
/>
<button onClick={() => setShowFullUrl(!showFullUrl)} className="p-1 hover:bg-gray-100 rounded">
<Icon icon={showFullUrl ? EyeOffIcon : EyeIcon} size="sm" className="text-gray-500" />
</button>
)}
</div>
@ -198,11 +171,8 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
{mcpServer.mcp_access_groups && mcpServer.mcp_access_groups.length > 0 ? (
<div className="flex flex-wrap gap-2">
{mcpServer.mcp_access_groups.map((group: any, index: number) => (
<span
key={index}
className="px-2 py-1 bg-gray-100 rounded-md text-sm"
>
{typeof group === 'string' ? group : group?.name ?? ''}
<span key={index} className="px-2 py-1 bg-gray-100 rounded-md text-sm">
{typeof group === "string" ? group : group?.name ?? ""}
</span>
))}
</div>
@ -222,5 +192,5 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
</TabPanels>
</TabGroup>
</div>
);
};
)
}

View file

@ -1,62 +1,29 @@
import React, { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import {
Modal,
message,
Select,
Tooltip,
} from "antd";
import {
TabPanel,
TabPanels,
TabGroup,
TabList,
Tab,
} from "@tremor/react";
import {
Grid,
Col,
Title,
Text,
} from "@tremor/react";
import { DataTable } from "../view_logs/table";
import { mcpServerColumns } from "./mcp_server_columns";
import {
deleteMCPServer,
fetchMCPServers,
} from "../networking";
import {
MCPServer,
MCPServerProps,
Team,
} from "./types";
import { isAdminRole } from "@/utils/roles";
import { MCPServerView } from "./mcp_server_view";
import CreateMCPServer from "./create_mcp_server";
import MCPConnect from "./mcp_connect";
import { QuestionCircleOutlined } from '@ant-design/icons';
import React, { useState, useEffect } from "react"
import { useQuery } from "@tanstack/react-query"
import { Modal, message, Select, Tooltip } from "antd"
import { TabPanel, TabPanels, TabGroup, TabList, Tab, Button } from "@tremor/react"
import { Grid, Col, Title, Text } from "@tremor/react"
import { DataTable } from "../view_logs/table"
import { mcpServerColumns } from "./mcp_server_columns"
import { deleteMCPServer, fetchMCPServers } from "../networking"
import { MCPServer, MCPServerProps, Team } from "./types"
import { isAdminRole } from "@/utils/roles"
import { MCPServerView } from "./mcp_server_view"
import CreateMCPServer from "./create_mcp_server"
import MCPConnect from "./mcp_connect"
import { QuestionCircleOutlined } from "@ant-design/icons"
const { Option } = Select;
const { Option } = Select
const DeleteModal: React.FC<{
isModalOpen: boolean;
title: string;
confirmDelete: () => void;
cancelDelete: () => void;
}> = ({
isModalOpen,
title,
confirmDelete,
cancelDelete,
}) => {
if (!isModalOpen) return null;
isModalOpen: boolean
title: string
confirmDelete: () => void
cancelDelete: () => void
}> = ({ isModalOpen, title, confirmDelete, cancelDelete }) => {
if (!isModalOpen) return null
return (
<Modal
open={isModalOpen}
onOk={confirmDelete}
okType="danger"
onCancel={cancelDelete}
>
<Modal open={isModalOpen} onOk={confirmDelete} okType="danger" onCancel={cancelDelete}>
<Grid numItems={1} className="gap-2 w-full">
<Title>{title}</Title>
<Col numColSpan={1}>
@ -64,14 +31,10 @@ const DeleteModal: React.FC<{
</Col>
</Grid>
</Modal>
);
};
)
}
const MCPServers: React.FC<MCPServerProps> = ({
accessToken,
userRole,
userID,
}) => {
const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID }) => {
const {
data: mcpServers,
isLoading: isLoadingServers,
@ -79,163 +42,156 @@ const MCPServers: React.FC<MCPServerProps> = ({
} = useQuery({
queryKey: ["mcpServers"],
queryFn: () => {
if (!accessToken) throw new Error("Access Token required");
return fetchMCPServers(accessToken);
if (!accessToken) throw new Error("Access Token required")
return fetchMCPServers(accessToken)
},
enabled: !!accessToken,
}) as { data: MCPServer[]; isLoading: boolean; refetch: () => void };
}) as { data: MCPServer[]; isLoading: boolean; refetch: () => void }
// state
const [serverIdToDelete, setServerToDelete] = useState<string | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedServerId, setSelectedServerId] = useState<string | null>(null);
const [editServer, setEditServer] = useState(false);
const [selectedTeam, setSelectedTeam] = useState<string>("all");
const [selectedMcpAccessGroup, setSelectedMcpAccessGroup] = useState<string>("all");
const [filteredServers, setFilteredServers] = useState<MCPServer[]>([]);
const [serverIdToDelete, setServerToDelete] = useState<string | null>(null)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [selectedServerId, setSelectedServerId] = useState<string | null>(null)
const [editServer, setEditServer] = useState(false)
const [selectedTeam, setSelectedTeam] = useState<string>("all")
const [selectedMcpAccessGroup, setSelectedMcpAccessGroup] = useState<string>("all")
const [filteredServers, setFilteredServers] = useState<MCPServer[]>([])
const [isModalVisible, setModalVisible] = useState(false)
// Get unique teams from all servers
const uniqueTeams = React.useMemo(() => {
if (!mcpServers) return [];
const teamsSet = new Set<string>();
const uniqueTeamsArray: Team[] = [];
if (!mcpServers) return []
const teamsSet = new Set<string>()
const uniqueTeamsArray: Team[] = []
mcpServers.forEach((server: MCPServer) => {
if (server.teams) {
server.teams.forEach((team: Team) => {
const teamKey = team.team_id;
const teamKey = team.team_id
if (!teamsSet.has(teamKey)) {
teamsSet.add(teamKey);
uniqueTeamsArray.push(team);
teamsSet.add(teamKey)
uniqueTeamsArray.push(team)
}
});
})
}
});
return uniqueTeamsArray;
}, [mcpServers]);
})
return uniqueTeamsArray
}, [mcpServers])
// Get unique MCP access groups from all servers
const uniqueMcpAccessGroups = React.useMemo(() => {
if (!mcpServers) return [];
return Array.from(new Set(
mcpServers.flatMap(server => server.mcp_access_groups)
));
}, [mcpServers]);
if (!mcpServers) return []
return Array.from(new Set(mcpServers.flatMap((server) => server.mcp_access_groups)))
}, [mcpServers])
// Handle team filter change
const handleTeamChange = (teamId: string) => {
setSelectedTeam(teamId);
filterServers(teamId, selectedMcpAccessGroup);
};
setSelectedTeam(teamId)
filterServers(teamId, selectedMcpAccessGroup)
}
// Handle MCP access group filter change
const handleMcpAccessGroupChange = (group: string) => {
setSelectedMcpAccessGroup(group);
filterServers(selectedTeam, group);
};
setSelectedMcpAccessGroup(group)
filterServers(selectedTeam, group)
}
// Filtering logic for both team and access group
const filterServers = (teamId: string, group: string) => {
if (!mcpServers) return setFilteredServers([]);
let filtered = mcpServers;
if (!mcpServers) return setFilteredServers([])
let filtered = mcpServers
if (teamId === "personal") {
setFilteredServers([]);
return;
setFilteredServers([])
return
}
if (teamId !== "all") {
filtered = filtered.filter(server =>
server.teams?.some(team => team.team_id === teamId)
);
filtered = filtered.filter((server) => server.teams?.some((team) => team.team_id === teamId))
}
if (group !== "all") {
filtered = filtered.filter(server =>
server.mcp_access_groups?.some((g: any) =>
typeof g === 'string' ? g === group : g && g.name === group
)
);
filtered = filtered.filter((server) =>
server.mcp_access_groups?.some((g: any) => (typeof g === "string" ? g === group : g && g.name === group)),
)
}
setFilteredServers(filtered);
};
setFilteredServers(filtered)
}
// Initial and effect-based filtering
useEffect(() => {
filterServers(selectedTeam, selectedMcpAccessGroup);
filterServers(selectedTeam, selectedMcpAccessGroup)
// eslint-disable-next-line
}, [mcpServers]);
}, [mcpServers])
const columns = React.useMemo(
() =>
mcpServerColumns(
userRole ?? "",
(serverId: string) => {
setSelectedServerId(serverId);
setEditServer(false);
setSelectedServerId(serverId)
setEditServer(false)
},
(serverId: string) => {
setSelectedServerId(serverId);
setEditServer(true);
setSelectedServerId(serverId)
setEditServer(true)
},
handleDelete
handleDelete,
),
[userRole]
);
[userRole],
)
function handleDelete(server_id: string) {
setServerToDelete(server_id);
setIsDeleteModalOpen(true);
setServerToDelete(server_id)
setIsDeleteModalOpen(true)
}
const confirmDelete = async () => {
if (serverIdToDelete == null || accessToken == null) {
return;
return
}
try {
await deleteMCPServer(accessToken, serverIdToDelete);
message.success("Deleted MCP Server successfully");
refetch();
await deleteMCPServer(accessToken, serverIdToDelete)
message.success("Deleted MCP Server successfully")
refetch()
} catch (error) {
console.error("Error deleting the mcp server:", error);
console.error("Error deleting the mcp server:", error)
}
setIsDeleteModalOpen(false);
setServerToDelete(null);
};
const cancelDelete = () => {
setIsDeleteModalOpen(false);
setServerToDelete(null);
};
if (!accessToken || !userRole || !userID) {
return (
<div className="p-6 text-center text-gray-500">
Missing required authentication parameters.
</div>
);
setIsDeleteModalOpen(false)
setServerToDelete(null)
}
const ServersTab = () => (
const cancelDelete = () => {
setIsDeleteModalOpen(false)
setServerToDelete(null)
}
const handleCreateSuccess = (newMcpServer: MCPServer) => {
setFilteredServers((prev) => [...prev, newMcpServer])
setModalVisible(false)
}
if (!accessToken || !userRole || !userID) {
return <div className="p-6 text-center text-gray-500">Missing required authentication parameters.</div>
}
const ServersTab = () =>
selectedServerId ? (
<MCPServerView
mcpServer={
filteredServers.find(
(server: MCPServer) => server.server_id === selectedServerId
) || {
server_id: '',
alias: '',
url: '',
transport: '',
spec_version: '',
auth_type: '',
created_at: '',
created_by: '',
updated_at: '',
updated_by: '',
filteredServers.find((server: MCPServer) => server.server_id === selectedServerId) || {
server_id: "",
alias: "",
url: "",
transport: "",
spec_version: "",
auth_type: "",
created_at: "",
created_by: "",
updated_at: "",
updated_by: "",
}
}
onBack={() => {
setEditServer(false);
setSelectedServerId(null);
refetch();
setEditServer(false)
setSelectedServerId(null)
refetch()
}}
isProxyAdmin={isAdminRole(userRole)}
isEditing={editServer}
@ -245,31 +201,12 @@ const MCPServers: React.FC<MCPServerProps> = ({
/>
) : (
<div className="w-full h-full">
<div className="flex justify-between items-center mb-6 px-6">
<div>
<Title>MCP Servers</Title>
<Text className="text-tremor-content">
Configure and manage your MCP servers
</Text>
</div>
{isAdminRole(userRole) && (
<CreateMCPServer
userRole={userRole}
accessToken={accessToken}
onCreateSuccess={refetch}
/>
)}
</div>
<div className="w-full px-6">
<div className="flex flex-col space-y-4">
<div className="flex items-center justify-between bg-gray-50 rounded-lg p-4 border-2 border-gray-200">
<div className="flex items-center gap-4">
<Text className="text-lg font-semibold text-gray-900">Current Team:</Text>
<Select
value={selectedTeam}
onChange={handleTeamChange}
style={{ width: 300 }}
>
<Select value={selectedTeam} onChange={handleTeamChange} style={{ width: 300 }}>
<Option value="all">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
@ -286,9 +223,7 @@ const MCPServers: React.FC<MCPServerProps> = ({
<Option key={team.team_id} value={team.team_id}>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="font-medium">
{team.team_alias || team.team_id}
</span>
<span className="font-medium">{team.team_alias || team.team_id}</span>
</div>
</Option>
))}
@ -296,14 +231,10 @@ const MCPServers: React.FC<MCPServerProps> = ({
<Text className="text-lg font-semibold text-gray-900 ml-6">
Access Group:
<Tooltip title="An MCP Access Group is a set of users or teams that have permission to access specific MCP servers. Use access groups to control and organize who can connect to which servers.">
<QuestionCircleOutlined style={{ marginLeft: 4, color: '#888' }} />
<QuestionCircleOutlined style={{ marginLeft: 4, color: "#888" }} />
</Tooltip>
</Text>
<Select
value={selectedMcpAccessGroup}
onChange={handleMcpAccessGroupChange}
style={{ width: 300 }}
>
<Select value={selectedMcpAccessGroup} onChange={handleMcpAccessGroupChange} style={{ width: 300 }}>
<Option value="all">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
@ -335,18 +266,31 @@ const MCPServers: React.FC<MCPServerProps> = ({
</div>
</div>
)
);
return (
<div className="w-full h-full">
<div className="w-full h-full p-6">
<DeleteModal
isModalOpen={isDeleteModalOpen}
title="Delete MCP Server"
confirmDelete={confirmDelete}
cancelDelete={cancelDelete}
/>
<CreateMCPServer
userRole={userRole}
accessToken={accessToken}
onCreateSuccess={handleCreateSuccess}
isModalVisible={isModalVisible}
setModalVisible={setModalVisible}
/>
<Title>MCP Servers</Title>
<Text className="text-tremor-content mt-2">Configure and manage your MCP servers</Text>
{isAdminRole(userRole) && (
<Button className="mt-4 mb-4" onClick={() => setModalVisible(true)}>
+ Add New MCP Server
</Button>
)}
<TabGroup className="w-full h-full">
<TabList className="flex justify-between mt-2 w-full items-center px-6">
<TabList className="flex justify-between mt-2 w-full items-center">
<div className="flex">
<Tab>All Servers</Tab>
<Tab>Connect</Tab>
@ -362,7 +306,7 @@ const MCPServers: React.FC<MCPServerProps> = ({
</TabPanels>
</TabGroup>
</div>
);
};
)
}
export default MCPServers;
export default MCPServers

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect } from "react"
import {
Card,
Title,
@ -18,29 +18,37 @@ import {
TableBody,
TableCell,
Button as TremorButton,
Icon
} from "@tremor/react";
import NumericalInput from "../shared/numerical_input";
import { Button, Form, Input, Select, message, Tooltip } from "antd";
import { InfoCircleOutlined } from '@ant-design/icons';
import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
import { Member, Organization, organizationInfoCall, organizationMemberAddCall, organizationMemberUpdateCall, organizationMemberDeleteCall, organizationUpdateCall } from "../networking";
import UserSearchModal from "../common_components/user_search_modal";
import MemberModal from "../team/edit_membership";
import ObjectPermissionsView from "../object_permissions_view";
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector";
import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
import { formatNumberWithCommas } from "@/utils/dataUtils";
Icon,
} from "@tremor/react"
import NumericalInput from "../shared/numerical_input"
import { Button, Form, Input, Select, message, Tooltip } from "antd"
import { InfoCircleOutlined } from "@ant-design/icons"
import { ArrowLeftIcon, PencilAltIcon, TrashIcon } from "@heroicons/react/outline"
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key"
import {
Member,
Organization,
organizationInfoCall,
organizationMemberAddCall,
organizationMemberUpdateCall,
organizationMemberDeleteCall,
organizationUpdateCall,
} from "../networking"
import UserSearchModal from "../common_components/user_search_modal"
import MemberModal from "../team/edit_membership"
import ObjectPermissionsView from "../object_permissions_view"
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector"
import MCPServerSelector from "../mcp_server_management/MCPServerSelector"
import { formatNumberWithCommas } from "@/utils/dataUtils"
interface OrganizationInfoProps {
organizationId: string;
onClose: () => void;
accessToken: string | null;
is_org_admin: boolean;
is_proxy_admin: boolean;
userModels: string[];
editOrg: boolean;
organizationId: string
onClose: () => void
accessToken: string | null
is_org_admin: boolean
is_proxy_admin: boolean
userModels: string[]
editOrg: boolean
}
const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
@ -50,40 +58,40 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
is_org_admin,
is_proxy_admin,
userModels,
editOrg
editOrg,
}) => {
const [orgData, setOrgData] = useState<Organization | null>(null);
const [loading, setLoading] = useState(true);
const [form] = Form.useForm();
const [isEditing, setIsEditing] = useState(false);
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false);
const [selectedEditMember, setSelectedEditMember] = useState<Member | null>(null);
const [orgData, setOrgData] = useState<Organization | null>(null)
const [loading, setLoading] = useState(true)
const [form] = Form.useForm()
const [isEditing, setIsEditing] = useState(false)
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false)
const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false)
const [selectedEditMember, setSelectedEditMember] = useState<Member | null>(null)
const canEditOrg = is_org_admin || is_proxy_admin;
const canEditOrg = is_org_admin || is_proxy_admin
const fetchOrgInfo = async () => {
try {
setLoading(true);
if (!accessToken) return;
const response = await organizationInfoCall(accessToken, organizationId);
setOrgData(response);
setLoading(true)
if (!accessToken) return
const response = await organizationInfoCall(accessToken, organizationId)
setOrgData(response)
} catch (error) {
message.error("Failed to load organization information");
console.error("Error fetching organization info:", error);
message.error("Failed to load organization information")
console.error("Error fetching organization info:", error)
} finally {
setLoading(false);
setLoading(false)
}
};
}
useEffect(() => {
fetchOrgInfo();
}, [organizationId, accessToken]);
fetchOrgInfo()
}, [organizationId, accessToken])
const handleMemberAdd = async (values: any) => {
try {
if (accessToken == null) {
return;
return
}
const member: Member = {
@ -91,21 +99,21 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
user_id: values.user_id,
role: values.role,
}
const response = await organizationMemberAddCall(accessToken, organizationId, member);
const response = await organizationMemberAddCall(accessToken, organizationId, member)
message.success("Organization member added successfully");
setIsAddMemberModalVisible(false);
form.resetFields();
fetchOrgInfo();
message.success("Organization member added successfully")
setIsAddMemberModalVisible(false)
form.resetFields()
fetchOrgInfo()
} catch (error) {
message.error("Failed to add organization member");
console.error("Error adding organization member:", error);
message.error("Failed to add organization member")
console.error("Error adding organization member:", error)
}
};
}
const handleMemberUpdate = async (values: any) => {
try {
if (!accessToken) return;
if (!accessToken) return
const member: Member = {
user_email: values.user_email,
@ -113,35 +121,35 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
role: values.role,
}
const response = await organizationMemberUpdateCall(accessToken, organizationId, member);
message.success("Organization member updated successfully");
setIsEditMemberModalVisible(false);
form.resetFields();
fetchOrgInfo();
const response = await organizationMemberUpdateCall(accessToken, organizationId, member)
message.success("Organization member updated successfully")
setIsEditMemberModalVisible(false)
form.resetFields()
fetchOrgInfo()
} catch (error) {
message.error("Failed to update organization member");
console.error("Error updating organization member:", error);
message.error("Failed to update organization member")
console.error("Error updating organization member:", error)
}
};
}
const handleMemberDelete = async (values: any) => {
try {
if (!accessToken) return;
if (!accessToken) return
await organizationMemberDeleteCall(accessToken, organizationId, values.user_id);
message.success("Organization member deleted successfully");
setIsEditMemberModalVisible(false);
form.resetFields();
fetchOrgInfo();
await organizationMemberDeleteCall(accessToken, organizationId, values.user_id)
message.success("Organization member deleted successfully")
setIsEditMemberModalVisible(false)
form.resetFields()
fetchOrgInfo()
} catch (error) {
message.error("Failed to delete organization member");
console.error("Error deleting organization member:", error);
message.error("Failed to delete organization member")
console.error("Error deleting organization member:", error)
}
};
}
const handleOrgUpdate = async (values: any) => {
try {
if (!accessToken) return;
if (!accessToken) return
const updateData: any = {
organization_id: organizationId,
@ -154,50 +162,55 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
budget_duration: values.budget_duration,
},
metadata: values.metadata ? JSON.parse(values.metadata) : null,
};
}
// Handle object_permission updates
if (values.vector_stores !== undefined || values.mcp_servers_and_groups !== undefined) {
updateData.object_permission = {
...orgData?.object_permission,
vector_stores: values.vector_stores || []
};
vector_stores: values.vector_stores || [],
}
if (values.mcp_servers_and_groups !== undefined) {
const { servers, accessGroups } = values.mcp_servers_and_groups || { servers: [], accessGroups: [] };
const { servers, accessGroups } = values.mcp_servers_and_groups || {
servers: [],
accessGroups: [],
}
if (servers && servers.length > 0) {
updateData.object_permission.mcp_servers = servers;
updateData.object_permission.mcp_servers = servers
}
if (accessGroups && accessGroups.length > 0) {
updateData.object_permission.mcp_access_groups = accessGroups;
updateData.object_permission.mcp_access_groups = accessGroups
}
}
}
const response = await organizationUpdateCall(accessToken, updateData);
message.success("Organization settings updated successfully");
setIsEditing(false);
fetchOrgInfo();
const response = await organizationUpdateCall(accessToken, updateData)
message.success("Organization settings updated successfully")
setIsEditing(false)
fetchOrgInfo()
} catch (error) {
message.error("Failed to update organization settings");
console.error("Error updating organization:", error);
message.error("Failed to update organization settings")
console.error("Error updating organization:", error)
}
};
}
if (loading) {
return <div className="p-4">Loading...</div>;
return <div className="p-4">Loading...</div>
}
if (!orgData) {
return <div className="p-4">Organization not found</div>;
return <div className="p-4">Organization not found</div>
}
return (
<div className="w-full h-screen p-4 bg-white">
<div className="flex justify-between items-center mb-6">
<div>
<Button onClick={onClose} className="mb-4"> Back</Button>
<TremorButton icon={ArrowLeftIcon} onClick={onClose} variant="light" className="mb-4">
Back to Organizations
</TremorButton>
<Title>{orgData.organization_alias}</Title>
<Text className="text-gray-500 font-mono">{orgData.organization_id}</Text>
</div>
@ -213,138 +226,145 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
<TabPanels>
{/* Overview Panel */}
<TabPanel>
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
<Card>
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
<Card>
<Text>Organization Details</Text>
<div className="mt-2">
<Text>Created: {new Date(orgData.created_at).toLocaleDateString()}</Text>
<Text>Updated: {new Date(orgData.updated_at).toLocaleDateString()}</Text>
<Text>Created By: {orgData.created_by}</Text>
<Text>Created: {new Date(orgData.created_at).toLocaleDateString()}</Text>
<Text>Updated: {new Date(orgData.updated_at).toLocaleDateString()}</Text>
<Text>Created By: {orgData.created_by}</Text>
</div>
</Card>
</Card>
<Card>
<Card>
<Text>Budget Status</Text>
<div className="mt-2">
<Title>${formatNumberWithCommas(orgData.spend, 4)}</Title>
<Text>of {orgData.litellm_budget_table.max_budget === null ? "Unlimited" : `$${formatNumberWithCommas(orgData.litellm_budget_table.max_budget, 4)}`}</Text>
{orgData.litellm_budget_table.budget_duration && (
<Title>${formatNumberWithCommas(orgData.spend, 4)}</Title>
<Text>
of{" "}
{orgData.litellm_budget_table.max_budget === null
? "Unlimited"
: `$${formatNumberWithCommas(orgData.litellm_budget_table.max_budget, 4)}`}
</Text>
{orgData.litellm_budget_table.budget_duration && (
<Text className="text-gray-500">Reset: {orgData.litellm_budget_table.budget_duration}</Text>
)}
)}
</div>
</Card>
</Card>
<Card>
<Card>
<Text>Rate Limits</Text>
<div className="mt-2">
<Text>TPM: {orgData.litellm_budget_table.tpm_limit || 'Unlimited'}</Text>
<Text>RPM: {orgData.litellm_budget_table.rpm_limit || 'Unlimited'}</Text>
{orgData.litellm_budget_table.max_parallel_requests && (
<Text>TPM: {orgData.litellm_budget_table.tpm_limit || "Unlimited"}</Text>
<Text>RPM: {orgData.litellm_budget_table.rpm_limit || "Unlimited"}</Text>
{orgData.litellm_budget_table.max_parallel_requests && (
<Text>Max Parallel Requests: {orgData.litellm_budget_table.max_parallel_requests}</Text>
)}
)}
</div>
</Card>
</Card>
<Card>
<Card>
<Text>Models</Text>
<div className="mt-2 flex flex-wrap gap-2">
{orgData.models.length === 0 ? (
<Badge color="red">All proxy models</Badge>
) : (
orgData.models.map((model, index) => (
<Badge key={index} color="red">
{model}
</Badge>
))
)}
{orgData.models.length === 0 ? (
<Badge color="red">All proxy models</Badge>
) : (
orgData.models.map((model, index) => (
<Badge key={index} color="red">
{model}
</Badge>
))
)}
</div>
</Card>
<Card>
</Card>
<Card>
<Text>Teams</Text>
<div className="mt-2 flex flex-wrap gap-2">
{orgData.teams?.map((team, index) => (
{orgData.teams?.map((team, index) => (
<Badge key={index} color="red">
{team.team_id}
{team.team_id}
</Badge>
))}
))}
</div>
</Card>
</Card>
<ObjectPermissionsView
objectPermission={orgData.object_permission}
variant="card"
accessToken={accessToken}
/>
<ObjectPermissionsView
objectPermission={orgData.object_permission}
variant="card"
accessToken={accessToken}
/>
</Grid>
</TabPanel>
{/* Budget Panel */}
<TabPanel>
<div className="space-y-4">
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[75vh]">
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[75vh]">
<Table>
<TableHead>
<TableHead>
<TableRow>
<TableHeaderCell>User ID</TableHeaderCell>
<TableHeaderCell>Role</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell>Created At</TableHeaderCell>
<TableHeaderCell></TableHeaderCell>
<TableHeaderCell>User ID</TableHeaderCell>
<TableHeaderCell>Role</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell>Created At</TableHeaderCell>
<TableHeaderCell></TableHeaderCell>
</TableRow>
</TableHead>
</TableHead>
<TableBody>
<TableBody>
{orgData.members?.map((member, index) => (
<TableRow key={index}>
<TableRow key={index}>
<TableCell>
<Text className="font-mono">{member.user_id}</Text>
<Text className="font-mono">{member.user_id}</Text>
</TableCell>
<TableCell>
<Text className="font-mono">{member.user_role}</Text>
<Text className="font-mono">{member.user_role}</Text>
</TableCell>
<TableCell>
<Text>${formatNumberWithCommas(member.spend, 4)}</Text>
<Text>${formatNumberWithCommas(member.spend, 4)}</Text>
</TableCell>
<TableCell>
<Text>{new Date(member.created_at).toLocaleString()}</Text>
<Text>{new Date(member.created_at).toLocaleString()}</Text>
</TableCell>
<TableCell>
{canEditOrg && (
{canEditOrg && (
<>
<Icon
<Icon
icon={PencilAltIcon}
size="sm"
onClick={() => {
setSelectedEditMember({
"role": member.user_role,
"user_email": member.user_email,
"user_id": member.user_id
});
setIsEditMemberModalVisible(true);
setSelectedEditMember({
role: member.user_role,
user_email: member.user_email,
user_id: member.user_id,
})
setIsEditMemberModalVisible(true)
}}
/>
<Icon
/>
<Icon
icon={TrashIcon}
size="sm"
onClick={() => {
handleMemberDelete(member);
handleMemberDelete(member)
}}
/>
/>
</>
)}
)}
</TableCell>
</TableRow>
</TableRow>
))}
</TableBody>
</TableBody>
</Table>
</Card>
{canEditOrg && (
<TremorButton onClick={() => {
setIsAddMemberModalVisible(true);
}}>
Add Member
</Card>
{canEditOrg && (
<TremorButton
onClick={() => {
setIsAddMemberModalVisible(true)
}}
>
Add Member
</TremorButton>
)}
)}
</div>
</TabPanel>
@ -353,12 +373,8 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
<Card className="overflow-y-auto max-h-[65vh]">
<div className="flex justify-between items-center mb-4">
<Title>Organization Settings</Title>
{(canEditOrg && !isEditing) && (
<TremorButton
onClick={() => setIsEditing(true)}
>
Edit Settings
</TremorButton>
{canEditOrg && !isEditing && (
<TremorButton onClick={() => setIsEditing(true)}>Edit Settings</TremorButton>
)}
</div>
@ -377,24 +393,26 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
vector_stores: orgData.object_permission?.vector_stores || [],
mcp_servers_and_groups: {
servers: orgData.object_permission?.mcp_servers || [],
accessGroups: orgData.object_permission?.mcp_access_groups || []
}
accessGroups: orgData.object_permission?.mcp_access_groups || [],
},
}}
layout="vertical"
>
<Form.Item
label="Organization Name"
name="organization_alias"
rules={[{ required: true, message: "Please input an organization name" }]}
rules={[
{
required: true,
message: "Please input an organization name",
},
]}
>
<TextInput />
</Form.Item>
<Form.Item label="Models" name="models">
<Select
mode="multiple"
placeholder="Select models"
>
<Select mode="multiple" placeholder="Select models">
<Select.Option key="all-proxy-models" value="all-proxy-models">
All Proxy Models
</Select.Option>
@ -428,8 +446,8 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
<Form.Item label="Vector Stores" name="vector_stores">
<VectorStoreSelector
onChange={(values) => form.setFieldValue('vector_stores', values)}
value={form.getFieldValue('vector_stores')}
onChange={(values) => form.setFieldValue("vector_stores", values)}
value={form.getFieldValue("vector_stores")}
accessToken={accessToken || ""}
placeholder="Select vector stores"
/>
@ -437,25 +455,21 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
<Form.Item label="MCP Servers & Access Groups" name="mcp_servers_and_groups">
<MCPServerSelector
onChange={(values) => form.setFieldValue('mcp_servers_and_groups', values)}
value={form.getFieldValue('mcp_servers_and_groups')}
onChange={(values) => form.setFieldValue("mcp_servers_and_groups", values)}
value={form.getFieldValue("mcp_servers_and_groups")}
accessToken={accessToken || ""}
placeholder="Select MCP servers and access groups"
/>
</Form.Item>
<Form.Item label="Metadata" name="metadata">
<Form.Item label="Metadata" name="metadata">
<Input.TextArea rows={4} />
</Form.Item>
<div className="sticky z-10 bg-white p-4 border-t border-gray-200 bottom-[-1.5rem] inset-x-[-1.5rem]">
<div className="flex justify-end items-center gap-2">
<Button onClick={() => setIsEditing(false)}>
Cancel
</Button>
<TremorButton type="submit">
Save Changes
</TremorButton>
<Button onClick={() => setIsEditing(false)}>Cancel</Button>
<TremorButton type="submit">Save Changes</TremorButton>
</div>
</div>
</Form>
@ -485,17 +499,22 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
</div>
<div>
<Text className="font-medium">Rate Limits</Text>
<div>TPM: {orgData.litellm_budget_table.tpm_limit || 'Unlimited'}</div>
<div>RPM: {orgData.litellm_budget_table.rpm_limit || 'Unlimited'}</div>
<div>TPM: {orgData.litellm_budget_table.tpm_limit || "Unlimited"}</div>
<div>RPM: {orgData.litellm_budget_table.rpm_limit || "Unlimited"}</div>
</div>
<div>
<Text className="font-medium">Budget</Text>
<div>Max: {orgData.litellm_budget_table.max_budget !== null ? `$${formatNumberWithCommas(orgData.litellm_budget_table.max_budget, 4)}` : 'No Limit'}</div>
<div>Reset: {orgData.litellm_budget_table.budget_duration || 'Never'}</div>
<div>
Max:{" "}
{orgData.litellm_budget_table.max_budget !== null
? `$${formatNumberWithCommas(orgData.litellm_budget_table.max_budget, 4)}`
: "No Limit"}
</div>
<div>Reset: {orgData.litellm_budget_table.budget_duration || "Never"}</div>
</div>
<ObjectPermissionsView
objectPermission={orgData.object_permission}
<ObjectPermissionsView
objectPermission={orgData.object_permission}
variant="inline"
className="pt-4 border-t border-gray-200"
accessToken={accessToken}
@ -513,9 +532,21 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
accessToken={accessToken}
title="Add Organization Member"
roles={[
{ label: "org_admin", value: "org_admin", description: "Can add and remove members, and change their roles." },
{ label: "internal_user", value: "internal_user", description: "Can view/create keys for themselves within organization." },
{ label: "internal_user_viewer", value: "internal_user_viewer", description: "Can only view their keys within organization." }
{
label: "org_admin",
value: "org_admin",
description: "Can add and remove members, and change their roles.",
},
{
label: "internal_user",
value: "internal_user",
description: "Can view/create keys for themselves within organization.",
},
{
label: "internal_user_viewer",
value: "internal_user_viewer",
description: "Can only view their keys within organization.",
},
]}
defaultRole="internal_user"
/>
@ -532,12 +563,12 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
roleOptions: [
{ label: "Org Admin", value: "org_admin" },
{ label: "Internal User", value: "internal_user" },
{ label: "Internal User Viewer", value: "internal_user_viewer" }
]
{ label: "Internal User Viewer", value: "internal_user_viewer" },
],
}}
/>
</div>
);
};
)
}
export default OrganizationInfoView;
export default OrganizationInfoView

File diff suppressed because it is too large Load diff

View file

@ -18,17 +18,27 @@ import {
TableHeaderCell,
TableBody,
Table,
Icon
Icon,
} from "@tremor/react";
import TeamMembersComponent from "./team_member_view";
import MemberPermissions from "./member_permissions";
import { teamInfoCall, teamMemberDeleteCall, teamMemberAddCall, teamMemberUpdateCall, Member, teamUpdateCall } from "@/components/networking";
import { Button, Form, Input, Select, message, Tooltip } from "antd";
import { InfoCircleOutlined } from '@ant-design/icons';
import {
Select as Select2,
} from "antd";
import { PencilAltIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline";
teamInfoCall,
teamMemberDeleteCall,
teamMemberAddCall,
teamMemberUpdateCall,
Member,
teamUpdateCall,
} from "@/components/networking";
import { Button, Form, Input, Select, message, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Select as Select2 } from "antd";
import {
ArrowLeftIcon,
PencilAltIcon,
PlusIcon,
TrashIcon,
} from "@heroicons/react/outline";
import MemberModal from "./edit_membership";
import UserSearchModal from "@/components/common_components/user_search_modal";
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
@ -110,23 +120,26 @@ export interface TeamInfoProps {
premiumUser?: boolean;
}
const TeamInfoView: React.FC<TeamInfoProps> = ({
teamId,
onClose,
accessToken,
is_team_admin,
const TeamInfoView: React.FC<TeamInfoProps> = ({
teamId,
onClose,
accessToken,
is_team_admin,
is_proxy_admin,
userModels,
editTeam,
premiumUser = false,
onUpdate
onUpdate,
}) => {
const [teamData, setTeamData] = useState<TeamData | null>(null);
const [loading, setLoading] = useState(true);
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
const [form] = Form.useForm();
const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false);
const [selectedEditMember, setSelectedEditMember] = useState<Member | null>(null);
const [isEditMemberModalVisible, setIsEditMemberModalVisible] =
useState(false);
const [selectedEditMember, setSelectedEditMember] = useState<Member | null>(
null
);
const [isEditing, setIsEditing] = useState(false);
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
@ -168,41 +181,43 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
const handleMemberCreate = async (values: any) => {
try {
if (accessToken == null) return;
const member: Member = {
user_email: values.user_email,
user_id: values.user_id,
role: values.role,
};
await teamMemberAddCall(accessToken, teamId, member);
message.success("Team member added successfully");
setIsAddMemberModalVisible(false);
form.resetFields();
// Fetch updated team info
const updatedTeamData = await teamInfoCall(accessToken, teamId);
setTeamData(updatedTeamData);
// Notify parent component of the update
onUpdate(updatedTeamData);
} catch (error: any) {
let errMsg = "Failed to add team member";
if (error?.raw?.detail?.error?.includes("Assigning team admins is a premium feature")) {
errMsg = "Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this.";
if (
error?.raw?.detail?.error?.includes(
"Assigning team admins is a premium feature"
)
) {
errMsg =
"Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this.";
} else if (error?.message) {
errMsg = error.message;
}
message.error(errMsg);
console.error("Error adding team member:", error);
}
};
const handleMemberUpdate = async (values: any) => {
try {
@ -214,24 +229,29 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
user_email: values.user_email,
user_id: values.user_id,
role: values.role,
}
};
message.destroy(); // Remove all existing toasts
await teamMemberUpdateCall(accessToken, teamId, member);
message.success("Team member updated successfully");
setIsEditMemberModalVisible(false);
// Fetch updated team info
const updatedTeamData = await teamInfoCall(accessToken, teamId);
setTeamData(updatedTeamData);
// Notify parent component of the update
onUpdate(updatedTeamData);
} catch (error: any) {
let errMsg = "Failed to update team member";
if (error?.raw?.detail?.includes("Assigning team admins is a premium feature")) {
errMsg = "Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this.";
if (
error?.raw?.detail?.includes(
"Assigning team admins is a premium feature"
)
) {
errMsg =
"Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this.";
} else if (error?.message) {
errMsg = error.message;
}
@ -243,7 +263,6 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
console.error("Error updating team member:", error);
}
};
const handleMemberDelete = async (member: Member) => {
try {
@ -254,11 +273,11 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
await teamMemberDeleteCall(accessToken, teamId, member);
message.success("Team member removed successfully");
// Fetch updated team info
const updatedTeamData = await teamInfoCall(accessToken, teamId);
setTeamData(updatedTeamData);
// Notify parent component of the update
onUpdate(updatedTeamData);
} catch (error) {
@ -290,7 +309,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
metadata: {
...parsedMetadata,
guardrails: values.guardrails || [],
logging: values.logging_settings || []
logging: values.logging_settings || [],
},
organization_id: values.organization_id,
};
@ -304,8 +323,14 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
}
// Handle object_permission updates
const { servers, accessGroups } = values.mcp_servers_and_groups || { servers: [], accessGroups: [] };
if ((servers && servers.length > 0) || (accessGroups && accessGroups.length > 0)) {
const { servers, accessGroups } = values.mcp_servers_and_groups || {
servers: [],
accessGroups: [],
};
if (
(servers && servers.length > 0) ||
(accessGroups && accessGroups.length > 0)
) {
updateData.object_permission = {};
if (servers && servers.length > 0) {
updateData.object_permission.mcp_servers = servers;
@ -315,9 +340,9 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
}
}
delete values.mcp_servers_and_groups;
const response = await teamUpdateCall(accessToken, updateData);
message.success("Team settings updated successfully");
setIsEditing(false);
fetchTeamInfo();
@ -340,7 +365,14 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
<div className="p-4">
<div className="flex justify-between items-center mb-6">
<div>
<Button onClick={onClose} className="mb-4"> Back</Button>
<TremorButton
icon={ArrowLeftIcon}
variant="light"
onClick={onClose}
className="mb-4"
>
Back to Teams
</TremorButton>
<Title>{info.team_alias}</Title>
<Text className="text-gray-500 font-mono">{info.team_id}</Text>
</div>
@ -350,11 +382,13 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
<TabList className="mb-4">
{[
<Tab key="overview">Overview</Tab>,
...(canEditTeam ? [
<Tab key="members">Members</Tab>,
<Tab key="member-permissions">Member Permissions</Tab>,
<Tab key="settings">Settings</Tab>
] : [])
...(canEditTeam
? [
<Tab key="members">Members</Tab>,
<Tab key="member-permissions">Member Permissions</Tab>,
<Tab key="settings">Settings</Tab>,
]
: []),
]}
</TabList>
@ -366,13 +400,26 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
<Text>Budget Status</Text>
<div className="mt-2">
<Title>${formatNumberWithCommas(info.spend, 4)}</Title>
<Text>of {info.max_budget === null ? "Unlimited" : `$${formatNumberWithCommas(info.max_budget, 4)}`}</Text>
<Text>
of{" "}
{info.max_budget === null
? "Unlimited"
: `$${formatNumberWithCommas(info.max_budget, 4)}`}
</Text>
{info.budget_duration && (
<Text className="text-gray-500">Reset: {info.budget_duration}</Text>
<Text className="text-gray-500">
Reset: {info.budget_duration}
</Text>
)}
<br/>
<br />
{info.team_member_budget_table && (
<Text className="text-gray-500">Team Member Budget: ${formatNumberWithCommas(info.team_member_budget_table.max_budget, 4)}</Text>
<Text className="text-gray-500">
Team Member Budget: $
{formatNumberWithCommas(
info.team_member_budget_table.max_budget,
4
)}
</Text>
)}
</div>
</Card>
@ -380,10 +427,12 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
<Card>
<Text>Rate Limits</Text>
<div className="mt-2">
<Text>TPM: {info.tpm_limit || 'Unlimited'}</Text>
<Text>RPM: {info.rpm_limit || 'Unlimited'}</Text>
<Text>TPM: {info.tpm_limit || "Unlimited"}</Text>
<Text>RPM: {info.rpm_limit || "Unlimited"}</Text>
{info.max_parallel_requests && (
<Text>Max Parallel Requests: {info.max_parallel_requests}</Text>
<Text>
Max Parallel Requests: {info.max_parallel_requests}
</Text>
)}
</div>
</Card>
@ -403,13 +452,13 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
</div>
</Card>
<ObjectPermissionsView
objectPermission={info.object_permission}
<ObjectPermissionsView
objectPermission={info.object_permission}
variant="card"
accessToken={accessToken}
/>
<LoggingSettingsView
<LoggingSettingsView
loggingConfigs={info.metadata?.logging || []}
variant="card"
/>
@ -431,7 +480,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
{/* Member Permissions Panel */}
{canEditTeam && (
<TabPanel>
<MemberPermissions
<MemberPermissions
teamId={teamId}
accessToken={accessToken}
canEditTeam={canEditTeam}
@ -444,10 +493,8 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
<Card className="overflow-y-auto max-h-[65vh]">
<div className="flex justify-between items-center mb-4">
<Title>Team Settings</Title>
{(canEditTeam && !isEditing) && (
<TremorButton
onClick={() => setIsEditing(true)}
>
{canEditTeam && !isEditing && (
<TremorButton onClick={() => setIsEditing(true)}>
Edit Settings
</TremorButton>
)}
@ -467,31 +514,39 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
budget_duration: info.budget_duration,
guardrails: info.metadata?.guardrails || [],
metadata: info.metadata
? JSON.stringify((({ logging, ...rest }) => rest)(info.metadata), null, 2)
? JSON.stringify(
(({ logging, ...rest }) => rest)(info.metadata),
null,
2
)
: "",
logging_settings: info.metadata?.logging || [],
organization_id: info.organization_id,
vector_stores: info.object_permission?.vector_stores || [],
mcp_servers: info.object_permission?.mcp_servers || [],
mcp_access_groups: info.object_permission?.mcp_servers || [],
mcp_servers_and_groups: info.object_permission?.mcp_servers || []
mcp_access_groups:
info.object_permission?.mcp_servers || [],
mcp_servers_and_groups:
info.object_permission?.mcp_servers || [],
}}
layout="vertical"
>
<Form.Item
label="Team Name"
name="team_alias"
rules={[{ required: true, message: "Please input a team name" }]}
rules={[
{ required: true, message: "Please input a team name" },
]}
>
<Input type=""/>
<Input type="" />
</Form.Item>
<Form.Item label="Models" name="models">
<Select
mode="multiple"
placeholder="Select models"
>
<Select.Option key="all-proxy-models" value="all-proxy-models">
<Select mode="multiple" placeholder="Select models">
<Select.Option
key="all-proxy-models"
value="all-proxy-models"
>
All Proxy Models
</Select.Option>
{Array.from(new Set(userModels)).map((model, idx) => (
@ -503,14 +558,30 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
</Form.Item>
<Form.Item label="Max Budget (USD)" name="max_budget">
<NumericalInput step={0.01} precision={2} style={{ width: "100%" }} />
<NumericalInput
step={0.01}
precision={2}
style={{ width: "100%" }}
/>
</Form.Item>
<Form.Item label="Team Member Budget (USD)" name="team_member_budget" tooltip="This is the individual budget for a user in the team.">
<NumericalInput step={0.01} precision={2} style={{ width: "100%" }} />
<Form.Item
label="Team Member Budget (USD)"
name="team_member_budget"
tooltip="This is the individual budget for a user in the team."
>
<NumericalInput
step={0.01}
precision={2}
style={{ width: "100%" }}
/>
</Form.Item>
<Form.Item label="Team Member Key Duration" name="team_member_key_duration" tooltip="Set a limit to the duration of a team member's key.">
<Form.Item
label="Team Member Key Duration"
name="team_member_key_duration"
tooltip="Set a limit to the duration of a team member's key."
>
<Select placeholder="n/a">
<Select.Option value="1d">1 day</Select.Option>
<Select.Option value="1w">1 week</Select.Option>
@ -518,7 +589,6 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
</Select>
</Form.Item>
<Form.Item label="Reset Budget" name="budget_duration">
<Select placeholder="n/a">
<Select.Option value="24h">daily</Select.Option>
@ -527,26 +597,32 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
</Select>
</Form.Item>
<Form.Item label="Tokens per minute Limit (TPM)" name="tpm_limit">
<Form.Item
label="Tokens per minute Limit (TPM)"
name="tpm_limit"
>
<NumericalInput step={1} style={{ width: "100%" }} />
</Form.Item>
<Form.Item label="Requests per minute Limit (RPM)" name="rpm_limit">
<Form.Item
label="Requests per minute Limit (RPM)"
name="rpm_limit"
>
<NumericalInput step={1} style={{ width: "100%" }} />
</Form.Item>
<Form.Item
label={
<span>
Guardrails{' '}
Guardrails{" "}
<Tooltip title="Setup your first guardrail">
<a
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
target="_blank"
<a
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</a>
</Tooltip>
</span>
@ -562,45 +638,54 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
<Form.Item label="Vector Stores" name="vector_stores">
<VectorStoreSelector
onChange={(values) => form.setFieldValue('vector_stores', values)}
value={form.getFieldValue('vector_stores')}
onChange={(values) =>
form.setFieldValue("vector_stores", values)
}
value={form.getFieldValue("vector_stores")}
accessToken={accessToken || ""}
placeholder="Select vector stores"
/>
</Form.Item>
<Form.Item label="MCP Servers / Access Groups" name="mcp_servers_and_groups">
<Form.Item
label="MCP Servers / Access Groups"
name="mcp_servers_and_groups"
>
<MCPServerSelector
onChange={val => form.setFieldValue('mcp_servers_and_groups', val)}
value={form.getFieldValue('mcp_servers_and_groups')}
accessToken={accessToken || ''}
onChange={(val) =>
form.setFieldValue("mcp_servers_and_groups", val)
}
value={form.getFieldValue("mcp_servers_and_groups")}
accessToken={accessToken || ""}
placeholder="Select MCP servers or access groups (optional)"
/>
</Form.Item>
<Form.Item label="Organization ID" name="organization_id">
<Input type=""/>
<Input type="" />
</Form.Item>
<Form.Item label="Logging Settings" name="logging_settings">
<EditLoggingSettings
value={form.getFieldValue('logging_settings')}
onChange={(values) => form.setFieldValue('logging_settings', values)}
value={form.getFieldValue("logging_settings")}
onChange={(values) =>
form.setFieldValue("logging_settings", values)
}
/>
</Form.Item>
<Form.Item label="Metadata" name="metadata">
<Input.TextArea rows={10} />
</Form.Item>
<div className="sticky z-10 bg-white p-4 border-t border-gray-200 bottom-[-1.5rem] inset-x-[-1.5rem]">
<div className="flex justify-end items-center gap-2">
<Button htmlType="button" onClick={() => setIsEditing(false)}>
<Button
htmlType="button"
onClick={() => setIsEditing(false)}
>
Cancel
</Button>
<TremorButton type="submit">
Save Changes
</TremorButton>
<TremorButton type="submit">Save Changes</TremorButton>
</div>
</div>
</Form>
@ -630,23 +715,34 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
</div>
<div>
<Text className="font-medium">Rate Limits</Text>
<div>TPM: {info.tpm_limit || 'Unlimited'}</div>
<div>RPM: {info.rpm_limit || 'Unlimited'}</div>
<div>TPM: {info.tpm_limit || "Unlimited"}</div>
<div>RPM: {info.rpm_limit || "Unlimited"}</div>
</div>
<div>
<Text className="font-medium">Team Budget</Text>
<div>Max Budget: {info.max_budget !== null ? `$${formatNumberWithCommas(info.max_budget, 4)}` : 'No Limit'}</div>
<div>Budget Reset: {info.budget_duration || 'Never'}</div>
<div>
Max Budget:{" "}
{info.max_budget !== null
? `$${formatNumberWithCommas(info.max_budget, 4)}`
: "No Limit"}
</div>
<div>Budget Reset: {info.budget_duration || "Never"}</div>
</div>
<div>
<Text className="font-medium">
Team Member Settings{' '}
Team Member Settings{" "}
<Tooltip title="These are limits on individual team members">
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</Text>
<div>Max Budget: {info.team_member_budget_table?.max_budget || 'No Limit'}</div>
<div>Key Duration: {info.metadata?.team_member_key_duration || 'No Limit'}</div>
<div>
Max Budget:{" "}
{info.team_member_budget_table?.max_budget || "No Limit"}
</div>
<div>
Key Duration:{" "}
{info.metadata?.team_member_key_duration || "No Limit"}
</div>
</div>
<div>
<Text className="font-medium">Organization ID</Text>
@ -654,19 +750,19 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
</div>
<div>
<Text className="font-medium">Status</Text>
<Badge color={info.blocked ? 'red' : 'green'}>
{info.blocked ? 'Blocked' : 'Active'}
<Badge color={info.blocked ? "red" : "green"}>
{info.blocked ? "Blocked" : "Active"}
</Badge>
</div>
<ObjectPermissionsView
objectPermission={info.object_permission}
<ObjectPermissionsView
objectPermission={info.object_permission}
variant="inline"
className="pt-4 border-t border-gray-200"
accessToken={accessToken}
/>
<LoggingSettingsView
<LoggingSettingsView
loggingConfigs={info.metadata?.logging || []}
variant="inline"
className="pt-4 border-t border-gray-200"
@ -690,8 +786,8 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
showUserId: true,
roleOptions: [
{ label: "Admin", value: "admin" },
{ label: "User", value: "user" }
]
{ label: "User", value: "user" },
],
}}
/>
@ -705,4 +801,4 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
);
};
export default TeamInfoView;
export default TeamInfoView;

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
"use client";
import React, { useState, useEffect } from "react";
"use client"
import React, { useState, useEffect } from "react"
import {
userInfoCall,
modelAvailableCall,
@ -9,67 +9,64 @@ import {
organizationListCall,
DEFAULT_ORGANIZATION,
keyInfoCall,
getProxyBaseUrl
} from "./networking";
import { fetchTeams } from "./common_components/fetch_teams";
import { Grid, Col, Card, Text, Title } from "@tremor/react";
import CreateKey from "./create_key_button";
import ViewKeyTable from "./view_key_table";
import ViewUserSpend from "./view_user_spend";
import ViewUserTeam from "./view_user_team";
import DashboardTeam from "./dashboard_default_team";
import Onboarding from "../app/onboarding/page";
import { useSearchParams, useRouter } from "next/navigation";
import { Team } from "./key_team_helpers/key_list";
import { jwtDecode } from "jwt-decode";
import { Typography } from "antd";
import { clearTokenCookies } from "@/utils/cookieUtils";
getProxyBaseUrl,
} from "./networking"
import { fetchTeams } from "./common_components/fetch_teams"
import { Grid, Col, Card, Text, Title } from "@tremor/react"
import CreateKey from "./create_key_button"
import ViewKeyTable from "./view_key_table"
import ViewUserSpend from "./view_user_spend"
import ViewUserTeam from "./view_user_team"
import DashboardTeam from "./dashboard_default_team"
import Onboarding from "../app/onboarding/page"
import { useSearchParams, useRouter } from "next/navigation"
import { Team } from "./key_team_helpers/key_list"
import { jwtDecode } from "jwt-decode"
import { Typography } from "antd"
import { clearTokenCookies } from "@/utils/cookieUtils"
export interface ProxySettings {
PROXY_BASE_URL: string | null;
PROXY_LOGOUT_URL: string | null;
DEFAULT_TEAM_DISABLED: boolean;
SSO_ENABLED: boolean;
DISABLE_EXPENSIVE_DB_QUERIES: boolean;
NUM_SPEND_LOGS_ROWS: number;
PROXY_BASE_URL: string | null
PROXY_LOGOUT_URL: string | null
DEFAULT_TEAM_DISABLED: boolean
SSO_ENABLED: boolean
DISABLE_EXPENSIVE_DB_QUERIES: boolean
NUM_SPEND_LOGS_ROWS: number
}
export type UserInfo = {
models: string[];
max_budget?: number | null;
spend: number;
models: string[]
max_budget?: number | null
spend: number
}
function getCookie(name: string) {
console.log("COOKIES", document.cookie)
const cookieValue = document.cookie
.split('; ')
.find(row => row.startsWith(name + '='));
return cookieValue ? cookieValue.split('=')[1] : null;
const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "="))
return cookieValue ? cookieValue.split("=")[1] : null
}
interface UserDashboardProps {
userID: string | null;
userRole: string | null;
userEmail: string | null;
teams: Team[] | null;
keys: any[] | null;
setUserRole: React.Dispatch<React.SetStateAction<string>>;
setUserEmail: React.Dispatch<React.SetStateAction<string | null>>;
setTeams: React.Dispatch<React.SetStateAction<Team[] | null>>;
setKeys: React.Dispatch<React.SetStateAction<Object[] | null>>;
premiumUser: boolean;
organizations: Organization[] | null;
addKey: (data: any) => void;
userID: string | null
userRole: string | null
userEmail: string | null
teams: Team[] | null
keys: any[] | null
setUserRole: React.Dispatch<React.SetStateAction<string>>
setUserEmail: React.Dispatch<React.SetStateAction<string | null>>
setTeams: React.Dispatch<React.SetStateAction<Team[] | null>>
setKeys: React.Dispatch<React.SetStateAction<Object[] | null>>
premiumUser: boolean
organizations: Organization[] | null
addKey: (data: any) => void
createClicked: boolean
}
type TeamInterface = {
models: any[];
team_id: null;
team_alias: String;
};
models: any[]
team_id: null
team_alias: String
}
const UserDashboard: React.FC<UserDashboardProps> = ({
userID,
@ -84,63 +81,61 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
premiumUser,
organizations,
addKey,
createClicked
createClicked,
}) => {
const [userSpendData, setUserSpendData] = useState<UserInfo | null>(
null
);
const [currentOrg, setCurrentOrg] = useState<Organization | null>(null);
const [userSpendData, setUserSpendData] = useState<UserInfo | null>(null)
const [currentOrg, setCurrentOrg] = useState<Organization | null>(null)
// Assuming useSearchParams() hook exists and works in your setup
const searchParams = useSearchParams()!;
const searchParams = useSearchParams()!
const token = getCookie('token');
const token = getCookie("token")
const invitation_id = searchParams.get("invitation_id");
const invitation_id = searchParams.get("invitation_id")
const [accessToken, setAccessToken] = useState<string | null>(null);
const [teamSpend, setTeamSpend] = useState<number | null>(null);
const [userModels, setUserModels] = useState<string[]>([]);
const [proxySettings, setProxySettings] = useState<ProxySettings | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null)
const [teamSpend, setTeamSpend] = useState<number | null>(null)
const [userModels, setUserModels] = useState<string[]>([])
const [proxySettings, setProxySettings] = useState<ProxySettings | null>(null)
const defaultTeam: TeamInterface = {
models: [],
team_alias: "Default Team",
team_id: null,
};
const [selectedTeam, setSelectedTeam] = useState<any | null>(null);
const [selectedKeyAlias, setSelectedKeyAlias] = useState<string | null>(null);
}
const [selectedTeam, setSelectedTeam] = useState<any | null>(null)
const [selectedKeyAlias, setSelectedKeyAlias] = useState<string | null>(null)
// check if window is not undefined
if (typeof window !== "undefined") {
window.addEventListener("beforeunload", function () {
// Clear session storage
sessionStorage.clear();
});
sessionStorage.clear()
})
}
function formatUserRole(userRole: string) {
if (!userRole) {
return "Undefined Role";
return "Undefined Role"
}
console.log(`Received user role: ${userRole}`);
console.log(`Received user role: ${userRole}`)
switch (userRole.toLowerCase()) {
case "app_owner":
return "App Owner";
return "App Owner"
case "demo_app_owner":
return "App Owner";
return "App Owner"
case "app_admin":
return "Admin";
return "Admin"
case "proxy_admin":
return "Admin";
return "Admin"
case "proxy_admin_viewer":
return "Admin Viewer";
return "Admin Viewer"
case "app_user":
return "App User";
return "App User"
case "internal_user":
return "Internal User";
return "Internal User"
case "internal_user_viewer":
return "Internal Viewer";
return "Internal Viewer"
default:
return "Unknown Role";
return "Unknown Role"
}
}
@ -148,258 +143,217 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
// Moved useEffect inside the component and used a condition to run fetch only if the params are available
useEffect(() => {
if (token) {
const decoded = jwtDecode(token) as { [key: string]: any };
const decoded = jwtDecode(token) as { [key: string]: any }
if (decoded) {
// cast decoded to dictionary
console.log("Decoded token:", decoded);
console.log("Decoded token:", decoded)
console.log("Decoded key:", decoded.key);
console.log("Decoded key:", decoded.key)
// set accessToken
setAccessToken(decoded.key);
setAccessToken(decoded.key)
// check if userRole is defined
if (decoded.user_role) {
const formattedUserRole = formatUserRole(decoded.user_role);
console.log("Decoded user_role:", formattedUserRole);
setUserRole(formattedUserRole);
const formattedUserRole = formatUserRole(decoded.user_role)
console.log("Decoded user_role:", formattedUserRole)
setUserRole(formattedUserRole)
} else {
console.log("User role not defined");
console.log("User role not defined")
}
if (decoded.user_email) {
setUserEmail(decoded.user_email);
setUserEmail(decoded.user_email)
} else {
console.log(`User Email is not set ${decoded}`);
console.log(`User Email is not set ${decoded}`)
}
}
}
if (userID && accessToken && userRole && !keys && !userSpendData) {
const cachedUserModels = sessionStorage.getItem("userModels" + userID);
const cachedUserModels = sessionStorage.getItem("userModels" + userID)
if (cachedUserModels) {
setUserModels(JSON.parse(cachedUserModels));
setUserModels(JSON.parse(cachedUserModels))
} else {
console.log(`currentOrg: ${JSON.stringify(currentOrg)}`)
const fetchData = async () => {
try {
const proxy_settings: ProxySettings = await getProxyUISettings(accessToken);
setProxySettings(proxy_settings);
const proxy_settings: ProxySettings = await getProxyUISettings(accessToken)
setProxySettings(proxy_settings)
const response = await userInfoCall(
accessToken,
userID,
userRole,
false,
null,
null
);
const response = await userInfoCall(accessToken, userID, userRole, false, null, null)
setUserSpendData(response["user_info"]);
setUserSpendData(response["user_info"])
console.log(`userSpendData: ${JSON.stringify(userSpendData)}`)
// set keys for admin and users
if (!response?.teams[0].keys) {
setKeys(response["keys"]);
setKeys(response["keys"])
} else {
setKeys(
response["keys"].concat(
response.teams
.filter((team: any) => userRole === "Admin" || team.user_id === userID)
.flatMap((team: any) => team.keys)
)
);
.flatMap((team: any) => team.keys),
),
)
}
sessionStorage.setItem(
"userData" + userID,
JSON.stringify(response["keys"])
);
sessionStorage.setItem(
"userSpendData" + userID,
JSON.stringify(response["user_info"])
);
sessionStorage.setItem("userData" + userID, JSON.stringify(response["keys"]))
sessionStorage.setItem("userSpendData" + userID, JSON.stringify(response["user_info"]))
const model_available = await modelAvailableCall(
accessToken,
userID,
userRole
);
const model_available = await modelAvailableCall(accessToken, userID, userRole)
// loop through model_info["data"] and create an array of element.model_name
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);
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)
console.log("userModels:", userModels);
console.log("userModels:", userModels)
sessionStorage.setItem(
"userModels" + userID,
JSON.stringify(available_model_names)
);
sessionStorage.setItem("userModels" + userID, JSON.stringify(available_model_names))
} catch (error: any) {
console.error("There was an error fetching the data", error);
console.error("There was an error fetching the data", error)
if (error.message.includes("Invalid proxy server token passed")) {
gotoLogin();
gotoLogin()
}
// Optionally, update your UI to reflect the error state here as well
}
};
fetchData();
fetchTeams(accessToken, userID, userRole, currentOrg, setTeams);
}
fetchData()
fetchTeams(accessToken, userID, userRole, currentOrg, setTeams)
}
}
}, [userID, token, accessToken, keys, userRole]);
}, [userID, token, accessToken, keys, userRole])
useEffect(() => {
// check key health - if it's invalid, redirect to login
if (accessToken) {
const fetchKeyInfo = async () => {
try {
const keyInfo = await keyInfoCall(accessToken, [accessToken]);
console.log("keyInfo: ", keyInfo);
const keyInfo = await keyInfoCall(accessToken, [accessToken])
console.log("keyInfo: ", keyInfo)
} catch (error: any) {
if (error.message.includes("Invalid proxy server token passed")) {
gotoLogin();
gotoLogin()
}
}
}
fetchKeyInfo();
fetchKeyInfo()
}
}, [accessToken]);
}, [accessToken])
useEffect(() => {
console.log(`currentOrg: ${JSON.stringify(currentOrg)}, accessToken: ${accessToken}, userID: ${userID}, userRole: ${userRole}`)
console.log(
`currentOrg: ${JSON.stringify(currentOrg)}, accessToken: ${accessToken}, userID: ${userID}, userRole: ${userRole}`,
)
if (accessToken) {
console.log(`fetching teams`)
fetchTeams(accessToken, userID, userRole, currentOrg, setTeams);
fetchTeams(accessToken, userID, userRole, currentOrg, setTeams)
}
}, [currentOrg]);
}, [currentOrg])
useEffect(() => {
// This code will run every time selectedTeam changes
if (
keys !== null &&
selectedTeam !== null &&
selectedTeam !== undefined &&
selectedTeam.team_id !== null
) {
let sum = 0;
if (keys !== null && selectedTeam !== null && selectedTeam !== undefined && selectedTeam.team_id !== null) {
let sum = 0
console.log(`keys: ${JSON.stringify(keys)}`)
for (const key of keys) {
if (
selectedTeam.hasOwnProperty("team_id") &&
key.team_id !== null &&
key.team_id === selectedTeam.team_id
) {
sum += key.spend;
if (selectedTeam.hasOwnProperty("team_id") && key.team_id !== null && key.team_id === selectedTeam.team_id) {
sum += key.spend
}
}
console.log(`sum: ${sum}`)
setTeamSpend(sum);
setTeamSpend(sum)
} else if (keys !== null) {
// sum the keys which don't have team-id set (default team)
let sum = 0;
let sum = 0
for (const key of keys) {
sum += key.spend;
sum += key.spend
}
setTeamSpend(sum);
setTeamSpend(sum)
}
}, [selectedTeam]);
}, [selectedTeam])
if (invitation_id != null) {
return (
<Onboarding></Onboarding>
)
return <Onboarding></Onboarding>
}
function gotoLogin() {
// Clear token cookies using the utility function
clearTokenCookies();
clearTokenCookies()
const baseUrl = getProxyBaseUrl();
const baseUrl = getProxyBaseUrl()
console.log("proxyBaseUrl:", baseUrl);
const url = baseUrl
? `${baseUrl}/sso/key/generate`
: `/sso/key/generate`;
console.log("proxyBaseUrl:", baseUrl)
console.log("Full URL:", url);
window.location.href = url;
const url = baseUrl ? `${baseUrl}/sso/key/generate` : `/sso/key/generate`
return null;
console.log("Full URL:", url)
window.location.href = url
return null
}
if (token == null) {
// user is not logged in as yet
console.log("All cookies before redirect:", document.cookie);
// user is not logged in as yet
console.log("All cookies before redirect:", document.cookie)
// Clear token cookies using the utility function
gotoLogin();
return null;
gotoLogin()
return null
} else {
// Check if token is expired
try {
const decoded = jwtDecode(token) as { [key: string]: any };
console.log("Decoded token:", decoded);
const expTime = decoded.exp;
const currentTime = Math.floor(Date.now() / 1000);
const decoded = jwtDecode(token) as { [key: string]: any }
console.log("Decoded token:", decoded)
const expTime = decoded.exp
const currentTime = Math.floor(Date.now() / 1000)
if (expTime && currentTime >= expTime) {
console.log("Token expired, redirecting to login");
gotoLogin();
return null;
console.log("Token expired, redirecting to login")
gotoLogin()
return null
}
} catch (error) {
console.error("Error decoding token:", error);
console.error("Error decoding token:", error)
// If there's an error decoding the token, consider it invalid
clearTokenCookies();
gotoLogin();
return null;
clearTokenCookies()
gotoLogin()
return null
}
if (accessToken == null) {
return null;
return null
}
}
if (userID == null) {
return (
<h1>User ID is not set</h1>
);
return <h1>User ID is not set</h1>
}
if (userRole == null) {
setUserRole("App Owner");
setUserRole("App Owner")
}
if (userRole && userRole == "Admin Viewer") {
const { Title, Paragraph } = Typography;
const { Title, Paragraph } = Typography
return (
<div>
<Title level={1}>Access Denied</Title>
<Paragraph>Ask your proxy admin for access to create keys</Paragraph>
</div>
);
)
}
console.log("inside user dashboard, selected team", selectedTeam);
console.log("All cookies after redirect:", document.cookie);
console.log("inside user dashboard, selected team", selectedTeam)
console.log("All cookies after redirect:", document.cookie)
return (
<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">
<CreateKey
<CreateKey
key={selectedTeam ? selectedTeam.team_id : null}
userID={userID}
team={selectedTeam as Team | null}
@ -431,7 +385,7 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
</Col>
</Grid>
</div>
);
};
)
}
export default UserDashboard;
export default UserDashboard

View file

@ -1,63 +1,61 @@
import React, { useState } from 'react';
import { LogEntry } from './columns';
import { DataTable } from './table';
import { columns } from './columns';
import { Card, Title, Text, Metric, AreaChart } from '@tremor/react';
import { RequestViewer } from './index';
import { formatNumberWithCommas } from '@/utils/dataUtils';
import React, { useState } from "react"
import { LogEntry } from "./columns"
import { DataTable } from "./table"
import { columns } from "./columns"
import { Card, Title, Text, Metric, AreaChart, Button as TremorButton } from "@tremor/react"
import { RequestViewer } from "./index"
import { formatNumberWithCommas } from "@/utils/dataUtils"
import { ArrowLeftIcon } from "@heroicons/react/outline"
interface SessionViewProps {
sessionId: string;
logs: LogEntry[];
onBack: () => void;
sessionId: string
logs: LogEntry[]
onBack: () => void
}
export const SessionView: React.FC<SessionViewProps> = ({ sessionId, logs, onBack }) => {
// Track which log row is expanded
const [expandedRequestId, setExpandedRequestId] = useState<string | null>(null);
const [expandedRequestId, setExpandedRequestId] = useState<string | null>(null)
// Calculate session metrics
const totalCost = logs.reduce((sum, log) => sum + (log.spend || 0), 0);
const totalTokens = logs.reduce((sum, log) => sum + (log.total_tokens || 0), 0);
const startTime = logs.length > 0 ? new Date(logs[0].startTime) : new Date();
const endTime = logs.length > 0 ? new Date(logs[logs.length - 1].endTime) : new Date();
const durationMs = endTime.getTime() - startTime.getTime();
const durationSec = (durationMs / 1000).toFixed(2);
const totalCost = logs.reduce((sum, log) => sum + (log.spend || 0), 0)
const totalTokens = logs.reduce((sum, log) => sum + (log.total_tokens || 0), 0)
const startTime = logs.length > 0 ? new Date(logs[0].startTime) : new Date()
const endTime = logs.length > 0 ? new Date(logs[logs.length - 1].endTime) : new Date()
const durationMs = endTime.getTime() - startTime.getTime()
const durationSec = (durationMs / 1000).toFixed(2)
// Prepare data for the timeline chart
const timelineData = logs.map(log => ({
const timelineData = logs.map((log) => ({
time: new Date(log.startTime).toISOString(),
tokens: log.total_tokens || 0,
cost: log.spend || 0,
}));
}))
return (
<div className="space-y-6">
{/* Header with back button */}
<div className="mb-8">
<div className="flex items-center space-x-4">
<button
onClick={onBack}
className="flex items-center text-gray-600 hover:text-gray-900 transition-colors"
>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to All Logs
</button>
</div>
<TremorButton icon={ArrowLeftIcon} variant="light" onClick={onBack} className="mb-4">
Back to All Logs
</TremorButton>
<div className="mt-4">
<h1 className="text-2xl font-semibold text-gray-900">Session Details</h1>
<div className="space-y-2">
<p className="text-sm text-gray-500 font-mono">{sessionId}</p>
<a
href="https://docs.litellm.ai/docs/proxy/ui_logs_sessions"
target="_blank"
rel="noopener noreferrer"
<a
href="https://docs.litellm.ai/docs/proxy/ui_logs_sessions"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:text-blue-800 flex items-center gap-1"
>
Get started with session management here
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
</div>
@ -80,17 +78,17 @@ export const SessionView: React.FC<SessionViewProps> = ({ sessionId, logs, onBac
</Card>
</div>
{/* Request Timeline */}
<Title>Session Logs</Title>
<div className="mt-4">
<DataTable
columns={columns}
data={logs}
renderSubComponent={RequestViewer}
getRowCanExpand={() => true}
loadingMessage="Loading logs..."
noDataMessage="No logs found"
/>
</div>
<Title>Session Logs</Title>
<div className="mt-4">
<DataTable
columns={columns}
data={logs}
renderSubComponent={RequestViewer}
getRowCanExpand={() => true}
loadingMessage="Loading logs..."
noDataMessage="No logs found"
/>
</div>
</div>
);
};
)
}

View file

@ -1,15 +1,7 @@
import React, { useState, useEffect, useCallback, useRef } from "react";
import {
Tab,
TabGroup,
TabList,
TabPanels,
TabPanel,
Select,
SelectItem,
} from "@tremor/react";
import React, { useState, useEffect, useCallback, useRef } from "react"
import { Tab, TabGroup, TabList, TabPanels, TabPanel, Select, SelectItem } from "@tremor/react"
import { message } from "antd";
import { message } from "antd"
import {
userInfoCall,
@ -19,48 +11,47 @@ import {
UserListResponse,
invitationCreateCall,
getProxyBaseUrl,
} from "./networking";
import { Button } from "@tremor/react";
import CreateUser from "./create_user_button";
import EditUserModal from "./edit_user";
import OnboardingModal from "./onboarding_link";
import { InvitationLink } from "./onboarding_link";
} from "./networking"
import { Button } from "@tremor/react"
import CreateUser from "./create_user_button"
import EditUserModal from "./edit_user"
import OnboardingModal from "./onboarding_link"
import { InvitationLink } from "./onboarding_link"
import { userDeleteCall } from "./networking";
import { columns } from "./view_users/columns";
import { UserDataTable } from "./view_users/table";
import { UserInfo } from "./view_users/types";
import SSOSettings from "./SSOSettings";
import debounce from "lodash/debounce";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { updateExistingKeys } from "@/utils/dataUtils";
import { useDebouncedState } from '@tanstack/react-pacer/debouncer'
import { userDeleteCall } from "./networking"
import { columns } from "./view_users/columns"
import { UserDataTable } from "./view_users/table"
import { UserInfo } from "./view_users/types"
import SSOSettings from "./SSOSettings"
import debounce from "lodash/debounce"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { updateExistingKeys } from "@/utils/dataUtils"
import { useDebouncedState } from "@tanstack/react-pacer/debouncer"
interface ViewUserDashboardProps {
accessToken: string | null;
token: string | null;
keys: any[] | null;
userRole: string | null;
userID: string | null;
teams: any[] | null;
setKeys: React.Dispatch<React.SetStateAction<Object[] | null>>;
accessToken: string | null
token: string | null
keys: any[] | null
userRole: string | null
userID: string | null
teams: any[] | null
setKeys: React.Dispatch<React.SetStateAction<Object[] | null>>
}
interface FilterState {
email: string;
user_id: string;
user_role: string;
sso_user_id: string;
team: string;
model: string;
min_spend: number | null;
max_spend: number | null;
sort_by: string;
sort_order: 'asc' | 'desc';
email: string
user_id: string
user_role: string
sso_user_id: string
team: string
model: string
min_spend: number | null
max_spend: number | null
sort_by: string
sort_order: "asc" | "desc"
}
const DEFAULT_PAGE_SIZE = 25;
const DEFAULT_PAGE_SIZE = 25
const initialFilters: FilterState = {
email: "",
@ -72,36 +63,28 @@ const initialFilters: FilterState = {
min_spend: null,
max_spend: null,
sort_by: "created_at",
sort_order: "desc"
sort_order: "desc",
}
const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
accessToken,
token,
userRole,
userID,
teams,
}) => {
const queryClient = useQueryClient();
const [currentPage, setCurrentPage] = useState(1);
const [editModalVisible, setEditModalVisible] = useState(false);
const [selectedUser, setSelectedUser] = useState<UserInfo | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [userToDelete, setUserToDelete] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState("users");
const [filters, setFilters] = useState<FilterState>(initialFilters);
const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, token, userRole, userID, teams }) => {
const queryClient = useQueryClient()
const [currentPage, setCurrentPage] = useState(1)
const [editModalVisible, setEditModalVisible] = useState(false)
const [selectedUser, setSelectedUser] = useState<UserInfo | null>(null)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [userToDelete, setUserToDelete] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState("users")
const [filters, setFilters] = useState<FilterState>(initialFilters)
const [debouncedFilters, setDebouncedFilters, debouncer] = useDebouncedState(filters, { wait: 300 })
const [showFilters, setShowFilters] = useState(false);
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] =
useState(false);
const [invitationLinkData, setInvitationLinkData] =
useState<InvitationLink | null>(null);
const [baseUrl, setBaseUrl] = useState<string | null>(null);
const [showFilters, setShowFilters] = useState(false)
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false)
const [invitationLinkData, setInvitationLinkData] = useState<InvitationLink | null>(null)
const [baseUrl, setBaseUrl] = useState<string | null>(null)
const handleDelete = (userId: string) => {
setUserToDelete(userId);
setIsDeleteModalOpen(true);
};
setUserToDelete(userId)
setIsDeleteModalOpen(true)
}
useEffect(() => {
return () => {
@ -110,106 +93,106 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
}, [debouncer])
useEffect(() => {
setBaseUrl(getProxyBaseUrl());
}, []);
setBaseUrl(getProxyBaseUrl())
}, [])
const updateFilters = (update: Partial<FilterState>) => {
setFilters((previousFilters) => {
const newFilters = {...previousFilters, ...update };
setDebouncedFilters(newFilters);
return newFilters;
const newFilters = { ...previousFilters, ...update }
setDebouncedFilters(newFilters)
return newFilters
})
};
}
const handleSortChange = (sortBy: string, sortOrder: 'asc' | 'desc') => {
updateFilters({ sort_by: sortBy, sort_order: sortOrder });
};
const handleSortChange = (sortBy: string, sortOrder: "asc" | "desc") => {
updateFilters({ sort_by: sortBy, sort_order: sortOrder })
}
const handleResetPassword = async (userId: string) => {
if (!accessToken) {
message.error("Access token not found");
return;
message.error("Access token not found")
return
}
try {
message.success("Generating password reset link...");
const data = await invitationCreateCall(accessToken, userId);
setInvitationLinkData(data);
setIsInvitationLinkModalVisible(true);
message.success("Generating password reset link...")
const data = await invitationCreateCall(accessToken, userId)
setInvitationLinkData(data)
setIsInvitationLinkModalVisible(true)
} catch (error) {
message.error("Failed to generate password reset link");
message.error("Failed to generate password reset link")
}
};
}
const confirmDelete = async () => {
if (userToDelete && accessToken) {
try {
await userDeleteCall(accessToken, [userToDelete]);
await userDeleteCall(accessToken, [userToDelete])
// Update the user list after deletion
queryClient.setQueriesData<UserListResponse>({ queryKey: ['userList'] }, (previousData) => {
if (previousData === undefined) return previousData;
const updatedUsers = previousData.users.filter(user => user.user_id !== userToDelete);
return { ...previousData, users: updatedUsers };
queryClient.setQueriesData<UserListResponse>({ queryKey: ["userList"] }, (previousData) => {
if (previousData === undefined) return previousData
const updatedUsers = previousData.users.filter((user) => user.user_id !== userToDelete)
return { ...previousData, users: updatedUsers }
})
message.success("User deleted successfully");
message.success("User deleted successfully")
} catch (error) {
console.error("Error deleting user:", error);
message.error("Failed to delete user");
console.error("Error deleting user:", error)
message.error("Failed to delete user")
}
}
setIsDeleteModalOpen(false);
setUserToDelete(null);
};
setIsDeleteModalOpen(false)
setUserToDelete(null)
}
const cancelDelete = () => {
setIsDeleteModalOpen(false);
setUserToDelete(null);
};
setIsDeleteModalOpen(false)
setUserToDelete(null)
}
const handleEditCancel = async () => {
setSelectedUser(null);
setEditModalVisible(false);
};
setSelectedUser(null)
setEditModalVisible(false)
}
const handleEditSubmit = async (editedUser: any) => {
console.log("inside handleEditSubmit:", editedUser);
console.log("inside handleEditSubmit:", editedUser)
if (!accessToken || !token || !userRole || !userID) {
return;
return
}
try {
const response = await userUpdateUserCall(accessToken, editedUser, null);
queryClient.setQueriesData<UserListResponse>({ queryKey: ['userList'] }, (previousData) => {
if (previousData === undefined) return previousData;
const updatedUsers = previousData.users.map(user => {
const response = await userUpdateUserCall(accessToken, editedUser, null)
queryClient.setQueriesData<UserListResponse>({ queryKey: ["userList"] }, (previousData) => {
if (previousData === undefined) return previousData
const updatedUsers = previousData.users.map((user) => {
if (user.user_id === response.data.user_id) {
return updateExistingKeys(user, response.data);
return updateExistingKeys(user, response.data)
}
return user;
});
return { ...previousData, users: updatedUsers };
return user
})
return { ...previousData, users: updatedUsers }
})
message.success(`User ${editedUser.user_id} updated successfully`);
message.success(`User ${editedUser.user_id} updated successfully`)
} catch (error) {
console.error("There was an error updating the user", error);
console.error("There was an error updating the user", error)
}
setSelectedUser(null);
setEditModalVisible(false);
setSelectedUser(null)
setEditModalVisible(false)
// Close the modal
};
}
const handlePageChange = async (newPage: number) => {
setCurrentPage(newPage);
};
setCurrentPage(newPage)
}
const userListQuery = useQuery({
queryKey: ['userList', { debouncedFilter: debouncedFilters, currentPage }],
queryKey: ["userList", { debouncedFilter: debouncedFilters, currentPage }],
queryFn: async () => {
if (!accessToken) throw new Error('Access token required');
if (!accessToken) throw new Error("Access token required")
return await userListCall(
accessToken,
@ -221,23 +204,23 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
debouncedFilters.team || null,
debouncedFilters.sso_user_id || null,
debouncedFilters.sort_by,
debouncedFilters.sort_order
);
debouncedFilters.sort_order,
)
},
enabled: Boolean(accessToken && token && userRole && userID),
placeholderData: (previousData) => previousData
});
placeholderData: (previousData) => previousData,
})
const userListResponse = userListQuery.data
const userRolesQuery = useQuery<Record<string, Record<string, string>>>({
queryKey: ['userRoles'],
queryKey: ["userRoles"],
initialData: () => ({}),
queryFn: async () => {
if (!accessToken) throw new Error('Access token required');
return await getPossibleUserRoles(accessToken);
if (!accessToken) throw new Error("Access token required")
return await getPossibleUserRoles(accessToken)
},
enabled: Boolean(accessToken && token && userRole && userID),
});
})
const possibleUIRoles = userRolesQuery.data
if (userListQuery.isLoading) {
@ -245,40 +228,34 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
}
if (!accessToken || !token || !userRole || !userID) {
return <div>Loading...</div>;
return <div>Loading...</div>
}
const tableColumns = columns(
possibleUIRoles,
(user) => {
setSelectedUser(user);
setEditModalVisible(true);
setSelectedUser(user)
setEditModalVisible(true)
},
handleDelete,
handleResetPassword,
() => {} // placeholder function, will be overridden in UserDataTable
);
() => {}, // placeholder function, will be overridden in UserDataTable
)
return (
<div className="w-full p-6">
<div className="flex items-center justify-between mb-4">
<h1 className="text-xl font-semibold">Users</h1>
<div className="flex space-x-3">
<CreateUser
userID={userID}
accessToken={accessToken}
teams={teams}
possibleUIRoles={possibleUIRoles}
/>
<CreateUser userID={userID} accessToken={accessToken} teams={teams} possibleUIRoles={possibleUIRoles} />
</div>
</div>
<TabGroup defaultIndex={0} onIndexChange={(index) => setActiveTab(index === 0 ? "users" : "settings")}>
<TabList className="mb-4">
<Tab>Users</Tab>
<Tab>Default User Settings</Tab>
</TabList>
<TabPanels>
<TabPanel>
<div className="bg-white rounded-lg shadow">
@ -312,15 +289,10 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
{/* Filter Button */}
<button
className={`px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2 ${showFilters ? 'bg-gray-100' : ''}`}
className={`px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2 ${showFilters ? "bg-gray-100" : ""}`}
onClick={() => setShowFilters(!showFilters)}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
@ -338,15 +310,10 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
<button
className="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2"
onClick={() => {
updateFilters(initialFilters);
updateFilters(initialFilters)
}}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
@ -368,7 +335,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
placeholder="Filter by User ID"
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
value={filters.user_id}
onChange={(e) => updateFilters({ user_id : e.target.value })}
onChange={(e) => updateFilters({ user_id: e.target.value })}
/>
<svg
className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500"
@ -414,7 +381,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
))}
</Select>
</div>
{/* SSO ID Search */}
<div className="relative w-64">
<input
@ -422,7 +389,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
placeholder="Filter by SSO ID"
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
value={filters.sso_user_id}
onChange={(e) => updateFilters({ sso_user_id : e.target.value })}
onChange={(e) => updateFilters({ sso_user_id: e.target.value })}
/>
</div>
</div>
@ -437,23 +404,18 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
: 0}{" "}
-{" "}
{userListResponse && userListResponse.users
? Math.min(
userListResponse.page * userListResponse.page_size,
userListResponse.total
)
? Math.min(userListResponse.page * userListResponse.page_size, userListResponse.total)
: 0}{" "}
of {userListResponse ? userListResponse.total : 0} results
</span>
{/* Pagination Buttons */}
<div className="flex space-x-2">
<button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
className={`px-3 py-1 text-sm border rounded-md ${
currentPage === 1
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: 'hover:bg-gray-50'
currentPage === 1 ? "bg-gray-100 text-gray-400 cursor-not-allowed" : "hover:bg-gray-50"
}`}
>
Previous
@ -463,8 +425,8 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
disabled={!userListResponse || currentPage >= userListResponse.total_pages}
className={`px-3 py-1 text-sm border rounded-md ${
!userListResponse || currentPage >= userListResponse.total_pages
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: 'hover:bg-gray-50'
? "bg-gray-100 text-gray-400 cursor-not-allowed"
: "hover:bg-gray-50"
}`}
>
Next
@ -483,21 +445,26 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
onSortChange={handleSortChange}
currentSort={{
sortBy: filters.sort_by,
sortOrder: filters.sort_order
sortOrder: filters.sort_order,
}}
possibleUIRoles={possibleUIRoles}
handleEdit={(user) => {
setSelectedUser(user);
setEditModalVisible(true);
setSelectedUser(user)
setEditModalVisible(true)
}}
handleDelete={handleDelete}
handleResetPassword={handleResetPassword}
/>
</div>
</TabPanel>
<TabPanel>
<SSOSettings accessToken={accessToken} possibleUIRoles={possibleUIRoles} userID={userID} userRole={userRole}/>
<SSOSettings
accessToken={accessToken}
possibleUIRoles={possibleUIRoles}
userID={userID}
userRole={userRole}
/>
</TabPanel>
</TabPanels>
</TabGroup>
@ -515,18 +482,12 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
{isDeleteModalOpen && (
<div className="fixed z-10 inset-0 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 transition-opacity"
aria-hidden="true"
>
<div className="fixed inset-0 transition-opacity" aria-hidden="true">
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
{/* Modal Panel */}
<span
className="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
@ -535,16 +496,10 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Delete User
</h3>
<h3 className="text-lg leading-6 font-medium text-gray-900">Delete User</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to delete this user?
</p>
<p className="text-sm font-medium text-gray-900 mt-2">
User ID: {userToDelete}
</p>
<p className="text-sm text-gray-500">Are you sure you want to delete this user?</p>
<p className="text-sm font-medium text-gray-900 mt-2">User ID: {userToDelete}</p>
</div>
</div>
</div>
@ -568,7 +523,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
modalType="resetPassword"
/>
</div>
);
};
)
}
export default ViewUserDashboard;
export default ViewUserDashboard

View file

@ -1,137 +1,131 @@
import React, { useState } from "react";
import React, { useState } from "react"
import { Card, Text, Button, Grid, Col, Tab, TabList, TabGroup, TabPanel, TabPanels, Title, Badge } from "@tremor/react"
import { ArrowLeftIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline"
import {
Card,
Text,
Button,
Grid,
Col,
Tab,
TabList,
TabGroup,
TabPanel,
TabPanels,
Title,
Badge,
} from "@tremor/react";
import { ArrowLeftIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline";
import { userInfoCall, userDeleteCall, userUpdateUserCall, modelAvailableCall, invitationCreateCall, getProxyBaseUrl } from "../networking";
import { message } from "antd";
import { rolesWithWriteAccess } from '../../utils/roles';
import { UserEditView } from "../user_edit_view";
import OnboardingModal, { InvitationLink } from "../onboarding_link";
import { formatNumberWithCommas } from "@/utils/dataUtils";
userInfoCall,
userDeleteCall,
userUpdateUserCall,
modelAvailableCall,
invitationCreateCall,
getProxyBaseUrl,
} from "../networking"
import { message } from "antd"
import { rolesWithWriteAccess } from "../../utils/roles"
import { UserEditView } from "../user_edit_view"
import OnboardingModal, { InvitationLink } from "../onboarding_link"
import { formatNumberWithCommas } from "@/utils/dataUtils"
interface UserInfoViewProps {
userId: string;
onClose: () => void;
accessToken: string | null;
userRole: string | null;
onDelete?: () => void;
possibleUIRoles: Record<string, Record<string, string>> | null;
initialTab?: number; // 0 for Overview, 1 for Details
startInEditMode?: boolean;
userId: string
onClose: () => void
accessToken: string | null
userRole: string | null
onDelete?: () => void
possibleUIRoles: Record<string, Record<string, string>> | null
initialTab?: number // 0 for Overview, 1 for Details
startInEditMode?: boolean
}
interface UserInfo {
user_id: string;
user_id: string
user_info: {
user_email: string | null;
user_role: string | null;
teams: any[] | null;
models: string[] | null;
max_budget: number | null;
spend: number | null;
metadata: Record<string, any> | null;
created_at: string | null;
updated_at: string | null;
};
keys: any[] | null;
teams: any[] | null;
user_email: string | null
user_role: string | null
teams: any[] | null
models: string[] | null
max_budget: number | null
spend: number | null
metadata: Record<string, any> | null
created_at: string | null
updated_at: string | null
}
keys: any[] | null
teams: any[] | null
}
export default function UserInfoView({
userId,
onClose,
accessToken,
userRole,
onDelete,
export default function UserInfoView({
userId,
onClose,
accessToken,
userRole,
onDelete,
possibleUIRoles,
initialTab = 0,
startInEditMode = false
startInEditMode = false,
}: UserInfoViewProps) {
const [userData, setUserData] = useState<UserInfo | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isEditing, setIsEditing] = useState(startInEditMode);
const [userModels, setUserModels] = useState<string[]>([]);
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false);
const [invitationLinkData, setInvitationLinkData] = useState<InvitationLink | null>(null);
const [baseUrl, setBaseUrl] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState(initialTab);
const [userData, setUserData] = useState<UserInfo | null>(null)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [isEditing, setIsEditing] = useState(startInEditMode)
const [userModels, setUserModels] = useState<string[]>([])
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false)
const [invitationLinkData, setInvitationLinkData] = useState<InvitationLink | null>(null)
const [baseUrl, setBaseUrl] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState(initialTab)
React.useEffect(() => {
setBaseUrl(getProxyBaseUrl());
}, []);
setBaseUrl(getProxyBaseUrl())
}, [])
React.useEffect(() => {
console.log(`userId: ${userId}, userRole: ${userRole}, accessToken: ${accessToken}`)
const fetchData = async () => {
try {
if (!accessToken) return;
const data = await userInfoCall(accessToken, userId, userRole || "", false, null, null, true);
setUserData(data);
if (!accessToken) return
const data = await userInfoCall(accessToken, userId, userRole || "", false, null, null, true)
setUserData(data)
// Fetch available models
const modelDataResponse = await modelAvailableCall(accessToken, userId, userRole || "");
const availableModels = modelDataResponse.data.map((model: any) => model.id);
setUserModels(availableModels);
const modelDataResponse = await modelAvailableCall(accessToken, userId, userRole || "")
const availableModels = modelDataResponse.data.map((model: any) => model.id)
setUserModels(availableModels)
} catch (error) {
console.error("Error fetching user data:", error);
message.error("Failed to fetch user data");
console.error("Error fetching user data:", error)
message.error("Failed to fetch user data")
} finally {
setIsLoading(false);
setIsLoading(false)
}
};
}
fetchData();
}, [accessToken, userId, userRole]);
fetchData()
}, [accessToken, userId, userRole])
const handleResetPassword = async () => {
if (!accessToken) {
message.error("Access token not found");
return;
message.error("Access token not found")
return
}
try {
message.success("Generating password reset link...");
const data = await invitationCreateCall(accessToken, userId);
setInvitationLinkData(data);
setIsInvitationLinkModalVisible(true);
message.success("Generating password reset link...")
const data = await invitationCreateCall(accessToken, userId)
setInvitationLinkData(data)
setIsInvitationLinkModalVisible(true)
} catch (error) {
message.error("Failed to generate password reset link");
message.error("Failed to generate password reset link")
}
};
}
const handleDelete = async () => {
try {
if (!accessToken) return;
await userDeleteCall(accessToken, [userId]);
message.success("User deleted successfully");
if (!accessToken) return
await userDeleteCall(accessToken, [userId])
message.success("User deleted successfully")
if (onDelete) {
onDelete();
onDelete()
}
onClose();
onClose()
} catch (error) {
console.error("Error deleting user:", error);
message.error("Failed to delete user");
console.error("Error deleting user:", error)
message.error("Failed to delete user")
}
};
}
const handleUserUpdate = async (formValues: Record<string, any>) => {
try {
if (!accessToken || !userData) return;
if (!accessToken || !userData) return
const response = await userUpdateUserCall(accessToken, formValues, null)
const response = await userUpdateUserCall(accessToken, formValues, null);
// Update local state with new values
setUserData({
...userData,
@ -141,59 +135,44 @@ export default function UserInfoView({
models: formValues.models,
max_budget: formValues.max_budget,
metadata: formValues.metadata,
}
});
},
})
message.success("User updated successfully");
setIsEditing(false);
message.success("User updated successfully")
setIsEditing(false)
} catch (error) {
console.error("Error updating user:", error);
message.error("Failed to update user");
console.error("Error updating user:", error)
message.error("Failed to update user")
}
};
}
if (isLoading) {
return (
<div className="p-4">
<Button
icon={ArrowLeftIcon}
variant="light"
onClick={onClose}
className="mb-4"
>
<Button icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
Back to Users
</Button>
<Text>Loading user data...</Text>
</div>
);
)
}
if (!userData) {
return (
<div className="p-4">
<Button
icon={ArrowLeftIcon}
variant="light"
onClick={onClose}
className="mb-4"
>
<Button icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
Back to Users
</Button>
<Text>User not found</Text>
</div>
);
)
}
return (
<div className="p-4">
<div className="flex justify-between items-center mb-6">
<div>
<Button
icon={ArrowLeftIcon}
variant="light"
onClick={onClose}
className="mb-4"
>
<Button icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
Back to Users
</Button>
<Title>{userData.user_info?.user_email || "User"}</Title>
@ -201,12 +180,7 @@ export default function UserInfoView({
</div>
{userRole && rolesWithWriteAccess.includes(userRole) && (
<div className="flex items-center space-x-2">
<Button
icon={RefreshIcon}
variant="secondary"
onClick={handleResetPassword}
className="flex items-center"
>
<Button icon={RefreshIcon} variant="secondary" onClick={handleResetPassword} className="flex items-center">
Reset Password
</Button>
<Button
@ -229,34 +203,26 @@ export default function UserInfoView({
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Delete User
</h3>
<h3 className="text-lg leading-6 font-medium text-gray-900">Delete User</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to delete this user?
</p>
<p className="text-sm text-gray-500">Are you sure you want to delete this user?</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<Button
onClick={handleDelete}
color="red"
className="ml-2"
>
<Button onClick={handleDelete} color="red" className="ml-2">
Delete
</Button>
<Button onClick={() => setIsDeleteModalOpen(false)}>
Cancel
</Button>
<Button onClick={() => setIsDeleteModalOpen(false)}>Cancel</Button>
</div>
</div>
</div>
@ -277,7 +243,12 @@ export default function UserInfoView({
<Text>Spend</Text>
<div className="mt-2">
<Title>${formatNumberWithCommas(userData.user_info?.spend || 0, 4)}</Title>
<Text>of {userData.user_info?.max_budget !== null ? `$${formatNumberWithCommas(userData.user_info.max_budget, 4)}` : "Unlimited"}</Text>
<Text>
of{" "}
{userData.user_info?.max_budget !== null
? `$${formatNumberWithCommas(userData.user_info.max_budget, 4)}`
: "Unlimited"}
</Text>
</div>
</Card>
@ -299,9 +270,7 @@ export default function UserInfoView({
<Text>Personal Models</Text>
<div className="mt-2">
{userData.user_info?.models?.length && userData.user_info?.models?.length > 0 ? (
userData.user_info?.models?.map((model, index) => (
<Text key={index}>{model}</Text>
))
userData.user_info?.models?.map((model, index) => <Text key={index}>{model}</Text>)
) : (
<Text>All proxy models</Text>
)}
@ -340,7 +309,7 @@ export default function UserInfoView({
<Text className="font-medium">User ID</Text>
<Text className="font-mono">{userData.user_id}</Text>
</div>
<div>
<Text className="font-medium">Email</Text>
<Text>{userData.user_info?.user_email || "Not Set"}</Text>
@ -353,12 +322,20 @@ export default function UserInfoView({
<div>
<Text className="font-medium">Created</Text>
<Text>{userData.user_info?.created_at ? new Date(userData.user_info.created_at).toLocaleString() : "Unknown"}</Text>
<Text>
{userData.user_info?.created_at
? new Date(userData.user_info.created_at).toLocaleString()
: "Unknown"}
</Text>
</div>
<div>
<Text className="font-medium">Last Updated</Text>
<Text>{userData.user_info?.updated_at ? new Date(userData.user_info.updated_at).toLocaleString() : "Unknown"}</Text>
<Text>
{userData.user_info?.updated_at
? new Date(userData.user_info.updated_at).toLocaleString()
: "Unknown"}
</Text>
</div>
<div>
@ -366,10 +343,7 @@ export default function UserInfoView({
<div className="flex flex-wrap gap-2 mt-1">
{userData.teams?.length && userData.teams?.length > 0 ? (
userData.teams?.map((team, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 rounded text-xs"
>
<span key={index} className="px-2 py-1 bg-blue-100 rounded text-xs">
{team.team_alias || team.team_id}
</span>
))
@ -384,10 +358,7 @@ export default function UserInfoView({
<div className="flex flex-wrap gap-2 mt-1">
{userData.user_info?.models?.length && userData.user_info?.models?.length > 0 ? (
userData.user_info?.models?.map((model, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 rounded text-xs"
>
<span key={index} className="px-2 py-1 bg-blue-100 rounded text-xs">
{model}
</span>
))
@ -402,10 +373,7 @@ export default function UserInfoView({
<div className="flex flex-wrap gap-2 mt-1">
{userData.keys?.length && userData.keys?.length > 0 ? (
userData.keys.map((key, index) => (
<span
key={index}
className="px-2 py-1 bg-green-100 rounded text-xs"
>
<span key={index} className="px-2 py-1 bg-green-100 rounded text-xs">
{key.key_alias || key.token}
</span>
))
@ -435,5 +403,5 @@ export default function UserInfoView({
modalType="resetPassword"
/>
</div>
);
}
)
}