mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-09 16:24:38 +00:00
UI - v1.74.3-stable QA fixes (#12559)
* fix chat ui * fixes for types * ui - fix back button on MCP * mcp - show existing groups when creating new mcps * fix design * fix color scheme * fix linting * fixes for mapped tests
This commit is contained in:
@@ -177,8 +177,8 @@ class MCPServerManager:
|
||||
mcp_server_cost_info=_mcp_info.get("mcp_server_cost_info", None),
|
||||
),
|
||||
# Stdio-specific fields
|
||||
command=mcp_server.command,
|
||||
args=mcp_server.args,
|
||||
command=getattr(mcp_server, 'command', None),
|
||||
args=getattr(mcp_server, 'args', None) or [],
|
||||
env=env_dict,
|
||||
)
|
||||
self.registry[mcp_server.server_id] = new_server
|
||||
|
||||
@@ -852,8 +852,8 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase):
|
||||
mcp_access_groups: List[str] = Field(default_factory=list)
|
||||
# Stdio-specific fields
|
||||
command: Optional[str] = None
|
||||
args: Optional[List[str]] = None
|
||||
env: Optional[Dict[str, str]] = None
|
||||
args: List[str] = Field(default_factory=list)
|
||||
env: Dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
@@ -884,8 +884,8 @@ class UpdateMCPServerRequest(LiteLLMPydanticObjectBase):
|
||||
mcp_access_groups: List[str] = Field(default_factory=list)
|
||||
# Stdio-specific fields
|
||||
command: Optional[str] = None
|
||||
args: Optional[List[str]] = None
|
||||
env: Optional[Dict[str, str]] = None
|
||||
args: List[str] = Field(default_factory=list)
|
||||
env: Dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
@@ -923,8 +923,8 @@ class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase):
|
||||
mcp_info: Optional[MCPInfo] = None
|
||||
# Stdio-specific fields
|
||||
command: Optional[str] = None
|
||||
args: Optional[List[str]] = None
|
||||
env: Optional[Dict[str, str]] = None
|
||||
args: List[str] = Field(default_factory=list)
|
||||
env: Dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class NewUserRequestTeam(LiteLLMPydanticObjectBase):
|
||||
|
||||
@@ -247,9 +247,9 @@ if MCP_AVAILABLE:
|
||||
updated_at=datetime.now(),
|
||||
mcp_info=_server_config.mcp_info,
|
||||
# Stdio-specific fields
|
||||
command=_server_config.command,
|
||||
args=_server_config.args,
|
||||
env=_server_config.env,
|
||||
command=getattr(_server_config, 'command', None),
|
||||
args=getattr(_server_config, 'args', None) or [],
|
||||
env=getattr(_server_config, 'env', None) or {},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -271,9 +271,9 @@ if MCP_AVAILABLE:
|
||||
mcp_info=server.mcp_info,
|
||||
teams=cast(List[Dict[str, str | None]], server_to_teams_map.get(server.server_id, [])),
|
||||
# Stdio-specific fields
|
||||
command=server.command,
|
||||
args=server.args,
|
||||
env=server.env,
|
||||
command=getattr(server, 'command', None),
|
||||
args=getattr(server, 'args', None) or [],
|
||||
env=getattr(server, 'env', None) or {},
|
||||
)
|
||||
for server in LIST_MCP_SERVERS
|
||||
]
|
||||
|
||||
@@ -617,9 +617,9 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
||||
<Card className="w-full rounded-xl shadow-md overflow-hidden">
|
||||
<div className="flex h-[80vh] w-full gap-4">
|
||||
{/* Left Sidebar with Controls */}
|
||||
<div className="w-1/4 p-4 bg-gray-50">
|
||||
<div className="w-1/4 p-4 bg-gray-50 overflow-y-auto">
|
||||
<Title className="text-xl font-semibold mb-6 mt-2">Configurations</Title>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
|
||||
<KeyOutlined className="mr-2" /> API Key Source
|
||||
@@ -799,7 +799,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 mt-6">
|
||||
<div className="space-y-2 mt-4">
|
||||
<TremorButton
|
||||
onClick={clearChatHistory}
|
||||
className="w-full bg-gray-100 hover:bg-gray-200 text-gray-700 border-gray-300"
|
||||
|
||||
@@ -51,12 +51,14 @@ const MCPServerSelector: React.FC<MCPServerSelectorProps> = ({
|
||||
...accessGroups.map(group => ({
|
||||
label: group,
|
||||
value: group,
|
||||
isAccessGroup: true
|
||||
isAccessGroup: true,
|
||||
searchText: `${group} Access Group`
|
||||
})),
|
||||
...mcpServers.map(server => ({
|
||||
label: `${server.alias || server.server_id} (${server.server_id})`,
|
||||
value: server.server_id,
|
||||
isAccessGroup: false
|
||||
isAccessGroup: false,
|
||||
searchText: `${server.alias || server.server_id} ${server.server_id} MCP Server`
|
||||
}))
|
||||
];
|
||||
|
||||
@@ -82,29 +84,39 @@ const MCPServerSelector: React.FC<MCPServerSelectorProps> = ({
|
||||
value={selectedValues}
|
||||
loading={loading}
|
||||
className={className}
|
||||
optionFilterProp="label"
|
||||
showSearch
|
||||
style={{ width: '100%' }}
|
||||
disabled={disabled}
|
||||
filterOption={(input, option) => {
|
||||
const searchText = options.find(opt => opt.value === option?.value)?.searchText || '';
|
||||
return searchText.toLowerCase().includes(input.toLowerCase());
|
||||
}}
|
||||
>
|
||||
{options.map(opt => (
|
||||
<Select.Option
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
label={opt.label}
|
||||
>
|
||||
{opt.isAccessGroup && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
width: 10,
|
||||
height: 10,
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
background: '#1890ff',
|
||||
marginRight: 8,
|
||||
verticalAlign: 'middle',
|
||||
background: opt.isAccessGroup ? '#52c41a' : '#1890ff',
|
||||
flexShrink: 0,
|
||||
}} />
|
||||
)}
|
||||
{opt.label}
|
||||
{opt.isAccessGroup && <span style={{ color: '#1890ff', marginLeft: 8 }}>(Access Group)</span>}
|
||||
<span style={{ flex: 1 }}>{opt.label}</span>
|
||||
<span style={{
|
||||
color: opt.isAccessGroup ? '#52c41a' : '#1890ff',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
opacity: 0.8
|
||||
}}>
|
||||
{opt.isAccessGroup ? 'Access Group' : 'MCP Server'}
|
||||
</span>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
@@ -18,6 +18,7 @@ interface CreateMCPServerProps {
|
||||
onCreateSuccess: (newMcpServer: MCPServer) => void
|
||||
isModalVisible: boolean
|
||||
setModalVisible: (visible: boolean) => void
|
||||
availableAccessGroups: string[]
|
||||
}
|
||||
|
||||
const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
@@ -26,14 +27,15 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
onCreateSuccess,
|
||||
isModalVisible,
|
||||
setModalVisible,
|
||||
availableAccessGroups,
|
||||
}) => {
|
||||
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 [searchValue, setSearchValue] = useState<string>("")
|
||||
|
||||
const handleCreate = async (formValues: Record<string, any>) => {
|
||||
setIsLoading(true)
|
||||
@@ -132,6 +134,35 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Generate options with existing groups and potential new group
|
||||
const getAccessGroupOptions = () => {
|
||||
const existingOptions = availableAccessGroups.map((group: string) => ({
|
||||
value: group,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span className="font-medium">{group}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// If search value doesn't match any existing group and is not empty, add "create new group" option
|
||||
if (searchValue && !availableAccessGroups.some(group => group.toLowerCase().includes(searchValue.toLowerCase()))) {
|
||||
existingOptions.push({
|
||||
value: searchValue,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<span className="font-medium">{searchValue}</span>
|
||||
<span className="text-gray-400 text-xs ml-1">create new group</span>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return existingOptions
|
||||
}
|
||||
|
||||
// rendering
|
||||
if (!isAdminRole(userRole)) {
|
||||
return null
|
||||
@@ -304,12 +335,13 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
mode="tags"
|
||||
showSearch
|
||||
placeholder="Select existing groups or type to create new ones"
|
||||
optionFilterProp="children"
|
||||
optionFilterProp="value"
|
||||
filterOption={(input, option) =>
|
||||
(option?.value ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
onSearch={(value) => setSearchValue(value)}
|
||||
tokenSeparators={[","]}
|
||||
options={mcpAccessGroups.map((group) => ({
|
||||
value: group,
|
||||
label: group,
|
||||
}))}
|
||||
options={getAccessGroupOptions()}
|
||||
maxTagCount="responsive"
|
||||
allowClear
|
||||
/>
|
||||
|
||||
@@ -11,13 +11,15 @@ interface MCPServerEditProps {
|
||||
accessToken: string | null;
|
||||
onCancel: () => void;
|
||||
onSuccess: (server: MCPServer) => void;
|
||||
availableAccessGroups: string[];
|
||||
}
|
||||
|
||||
const MCPServerEdit: React.FC<MCPServerEditProps> = ({ mcpServer, accessToken, onCancel, onSuccess }) => {
|
||||
const MCPServerEdit: React.FC<MCPServerEditProps> = ({ mcpServer, accessToken, onCancel, onSuccess, availableAccessGroups }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [costConfig, setCostConfig] = useState<MCPServerCostInfo>({});
|
||||
const [tools, setTools] = useState<any[]>([]);
|
||||
const [isLoadingTools, setIsLoadingTools] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState<string>("");
|
||||
|
||||
// Initialize cost config from existing server data
|
||||
useEffect(() => {
|
||||
@@ -75,6 +77,35 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({ mcpServer, accessToken, o
|
||||
}
|
||||
};
|
||||
|
||||
// Generate options with existing groups and potential new group
|
||||
const getAccessGroupOptions = () => {
|
||||
const existingOptions = availableAccessGroups.map((group: string) => ({
|
||||
value: group,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span className="font-medium">{group}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// If search value doesn't match any existing group and is not empty, add "create new group" option
|
||||
if (searchValue && !availableAccessGroups.some(group => group.toLowerCase().includes(searchValue.toLowerCase()))) {
|
||||
existingOptions.push({
|
||||
value: searchValue,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<span className="font-medium">{searchValue}</span>
|
||||
<span className="text-gray-400 text-xs ml-1">create new group</span>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return existingOptions
|
||||
}
|
||||
|
||||
const handleSave = async (values: Record<string, any>) => {
|
||||
if (!accessToken) return;
|
||||
try {
|
||||
@@ -160,8 +191,15 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({ mcpServer, accessToken, o
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
placeholder="Add or select access groups"
|
||||
tokenSeparators={[',']}
|
||||
optionFilterProp="value"
|
||||
filterOption={(input, option) =>
|
||||
(option?.value ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
onSearch={(value) => setSearchValue(value)}
|
||||
options={getAccessGroupOptions()}
|
||||
// Ensure value is always an array of strings
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
/>
|
||||
|
||||
@@ -17,6 +17,7 @@ interface MCPServerViewProps {
|
||||
accessToken: string | null
|
||||
userRole: string | null
|
||||
userID: string | null
|
||||
availableAccessGroups: string[]
|
||||
}
|
||||
|
||||
export const MCPServerView: React.FC<MCPServerViewProps> = ({
|
||||
@@ -27,6 +28,7 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
|
||||
accessToken,
|
||||
userRole,
|
||||
userID,
|
||||
availableAccessGroups,
|
||||
}) => {
|
||||
const [editing, setEditing] = useState(isEditing)
|
||||
const [showFullUrl, setShowFullUrl] = useState(false)
|
||||
@@ -47,7 +49,7 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
|
||||
<div className="p-4 max-w-full">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<Button icon={ArrowLeftIcon} variant="light" className="mb-4">
|
||||
<Button icon={ArrowLeftIcon} variant="light" className="mb-4" onClick={onBack}>
|
||||
Back to All Servers
|
||||
</Button>
|
||||
<Title>{mcpServer.alias}</Title>
|
||||
@@ -131,6 +133,7 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
|
||||
accessToken={accessToken}
|
||||
onCancel={() => setEditing(false)}
|
||||
onSuccess={handleSuccess}
|
||||
availableAccessGroups={availableAccessGroups}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -80,7 +80,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
||||
// 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)))
|
||||
return Array.from(new Set(mcpServers.flatMap((server) => server.mcp_access_groups).filter((group): group is string => group != null)))
|
||||
}, [mcpServers])
|
||||
|
||||
// Handle team filter change
|
||||
@@ -198,6 +198,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
availableAccessGroups={uniqueMcpAccessGroups}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full">
|
||||
@@ -281,6 +282,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
||||
onCreateSuccess={handleCreateSuccess}
|
||||
isModalVisible={isModalVisible}
|
||||
setModalVisible={setModalVisible}
|
||||
availableAccessGroups={uniqueMcpAccessGroups}
|
||||
/>
|
||||
<Title>MCP Servers</Title>
|
||||
<Text className="text-tremor-content mt-2">Configure and manage your MCP servers</Text>
|
||||
|
||||
@@ -80,9 +80,9 @@ export function MCPServerPermissions({
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ServerIcon className="h-4 w-4 text-green-600" />
|
||||
<ServerIcon className="h-4 w-4 text-gray-600" />
|
||||
<Text className="font-semibold text-gray-900">MCP Servers</Text>
|
||||
<Badge color="green" size="xs">
|
||||
<Badge color="gray" size="xs">
|
||||
{totalCount}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -92,7 +92,7 @@ export function MCPServerPermissions({
|
||||
item.type === 'server' ? (
|
||||
<Tooltip key={index} title={`Full ID: ${item.value}`} placement="top">
|
||||
<div
|
||||
className="inline-flex items-center px-3 py-1.5 rounded-lg bg-green-50 border border-green-200 text-green-800 text-sm font-medium cursor-help"
|
||||
className="inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-50 border border-blue-200 text-blue-800 text-sm font-medium cursor-help"
|
||||
>
|
||||
{getMCPServerDisplayName(item.value)}
|
||||
</div>
|
||||
@@ -100,10 +100,10 @@ export function MCPServerPermissions({
|
||||
) : (
|
||||
<div
|
||||
key={index}
|
||||
className="inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-50 border border-blue-200 text-blue-800 text-sm font-medium"
|
||||
className="inline-flex items-center px-3 py-1.5 rounded-lg bg-green-50 border border-green-200 text-green-800 text-sm font-medium"
|
||||
>
|
||||
<span className="inline-block w-2 h-2 bg-blue-500 rounded-full mr-2"></span>
|
||||
{getAccessGroupDisplayName(item.value)} <span className="ml-1 text-xs text-blue-500">(Access Group)</span>
|
||||
<span className="inline-block w-2 h-2 bg-green-500 rounded-full mr-2"></span>
|
||||
{getAccessGroupDisplayName(item.value)} <span className="ml-1 text-xs text-green-500">(Access Group)</span>
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user