mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-03 07:23:33 +00:00
feat(ui): add FetchTools management page and team permissions
- Create FetchTools React components (CRUD, selector, view) - Add fetch_tools networking API functions - Add fetch-tools page to dashboard navigation - Add fetch_tools to team permissions (TeamInfo, OldTeams, CreateTeamModal) - Format all files with prettier
This commit is contained in:
parent
ad37273ba7
commit
3abab76fe5
13 changed files with 1316 additions and 351 deletions
|
|
@ -27,6 +27,7 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|||
import { organizationKeys } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
|
||||
import MCPToolPermissions from "@/components/mcp_server_management/MCPToolPermissions";
|
||||
import SearchToolSelector from "@/components/SearchTools/SearchToolSelector";
|
||||
import FetchToolSelector from "@/components/FetchTools/FetchToolSelector";
|
||||
|
||||
interface ModelAliases {
|
||||
[key: string]: string;
|
||||
|
|
@ -229,6 +230,10 @@ const CreateTeamModal = ({
|
|||
Array.isArray(formValues.object_permission_search_tools) &&
|
||||
formValues.object_permission_search_tools.length > 0;
|
||||
|
||||
const hasFetchTools =
|
||||
Array.isArray(formValues.object_permission_fetch_tools) &&
|
||||
formValues.object_permission_fetch_tools.length > 0;
|
||||
|
||||
if (
|
||||
(formValues.allowed_vector_store_ids && formValues.allowed_vector_store_ids.length > 0) ||
|
||||
(formValues.allowed_mcp_servers_and_groups &&
|
||||
|
|
@ -236,7 +241,8 @@ const CreateTeamModal = ({
|
|||
formValues.allowed_mcp_servers_and_groups.accessGroups?.length > 0 ||
|
||||
formValues.allowed_mcp_servers_and_groups.toolPermissions)) ||
|
||||
hasAgents ||
|
||||
hasSearchTools
|
||||
hasSearchTools ||
|
||||
hasFetchTools
|
||||
) {
|
||||
if (!formValues.object_permission) {
|
||||
formValues.object_permission = {};
|
||||
|
|
@ -278,6 +284,11 @@ const CreateTeamModal = ({
|
|||
formValues.object_permission.search_tools = formValues.object_permission_search_tools;
|
||||
delete formValues.object_permission_search_tools;
|
||||
}
|
||||
|
||||
if (hasFetchTools) {
|
||||
formValues.object_permission.fetch_tools = formValues.object_permission_fetch_tools;
|
||||
delete formValues.object_permission_fetch_tools;
|
||||
}
|
||||
}
|
||||
|
||||
// Transform allowed_mcp_access_groups into object_permission
|
||||
|
|
@ -776,6 +787,34 @@ const CreateTeamModal = ({
|
|||
</AccordionBody>
|
||||
</Accordion>
|
||||
|
||||
<Accordion className="mt-4 mb-4">
|
||||
<AccordionHeader>
|
||||
<b>Fetch Tool Settings</b>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Allowed Fetch Tools{" "}
|
||||
<Tooltip title="Select which fetch tools this team can access. Leave empty to allow all fetch tools.">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="object_permission_fetch_tools"
|
||||
className="mt-4"
|
||||
help="Restrict which configured fetch tools keys on this team may call."
|
||||
>
|
||||
<FetchToolSelector
|
||||
onChange={(vals: string[]) => form.setFieldValue("object_permission_fetch_tools", vals)}
|
||||
value={form.getFieldValue("object_permission_fetch_tools")}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select fetch tools (optional, empty = all allowed)"
|
||||
/>
|
||||
</Form.Item>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
|
||||
<Accordion className="mt-8 mb-8">
|
||||
<AccordionHeader>
|
||||
<b>Logging Settings</b>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,13 @@ import { Team } from "@/components/key_team_helpers/key_list";
|
|||
import { MCPServers } from "@/components/mcp_tools";
|
||||
import ModelHubTable from "@/components/AIHub/ModelHubTable";
|
||||
import Navbar from "@/components/navbar";
|
||||
import { getUiConfig, Organization, proxyBaseUrl, setGlobalLitellmHeaderName, getInProductNudgesCall } from "@/components/networking";
|
||||
import {
|
||||
getUiConfig,
|
||||
Organization,
|
||||
proxyBaseUrl,
|
||||
setGlobalLitellmHeaderName,
|
||||
getInProductNudgesCall,
|
||||
} from "@/components/networking";
|
||||
import NewUsagePage from "@/components/UsagePage/components/UsagePageView";
|
||||
import OldTeams from "@/components/OldTeams";
|
||||
import { fetchUserModels, CreateKeyPrefillData } from "@/components/organisms/create_key_button";
|
||||
|
|
@ -28,6 +34,7 @@ import PassThroughSettings from "@/components/pass_through_settings";
|
|||
import PromptsPanel from "@/components/prompts";
|
||||
import PublicModelHub from "@/components/public_model_hub";
|
||||
import { SearchTools } from "@/components/SearchTools";
|
||||
import { FetchTools } from "@/components/FetchTools";
|
||||
import Settings from "@/components/settings";
|
||||
import { SurveyPrompt, SurveyModal, ClaudeCodePrompt, ClaudeCodeModal } from "@/components/survey";
|
||||
import TagManagement from "@/components/tag_management";
|
||||
|
|
@ -46,7 +53,13 @@ import ViewUserDashboard from "@/components/view_users";
|
|||
import { ThemeProvider } from "@/contexts/ThemeContext";
|
||||
import { clearTokenCookies, getCookie } from "@/utils/cookieUtils";
|
||||
import { isJwtExpired } from "@/utils/jwtUtils";
|
||||
import { buildLoginUrlWithReturn, consumeReturnUrl, isValidReturnUrl, normalizeUrlForCompare, storeReturnUrl } from "@/utils/returnUrlUtils";
|
||||
import {
|
||||
buildLoginUrlWithReturn,
|
||||
consumeReturnUrl,
|
||||
isValidReturnUrl,
|
||||
normalizeUrlForCompare,
|
||||
storeReturnUrl,
|
||||
} from "@/utils/returnUrlUtils";
|
||||
import { formatUserRole, isAdminRole } from "@/utils/roles";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
|
|
@ -137,15 +150,13 @@ function CreateKeyPageContent() {
|
|||
|
||||
// Validate owned_by against allowed values
|
||||
const validOwnedByValues = ["you", "service_account", "another_user"];
|
||||
const validatedOwnedBy = ownedBy && validOwnedByValues.includes(ownedBy)
|
||||
? (ownedBy as CreateKeyPrefillData["owned_by"])
|
||||
: undefined;
|
||||
const validatedOwnedBy =
|
||||
ownedBy && validOwnedByValues.includes(ownedBy) ? (ownedBy as CreateKeyPrefillData["owned_by"]) : undefined;
|
||||
|
||||
// Validate key_type against allowed values
|
||||
const validKeyTypes = ["default", "llm_api", "management"];
|
||||
const validatedKeyType = keyType && validKeyTypes.includes(keyType)
|
||||
? (keyType as CreateKeyPrefillData["key_type"])
|
||||
: undefined;
|
||||
const validatedKeyType =
|
||||
keyType && validKeyTypes.includes(keyType) ? (keyType as CreateKeyPrefillData["key_type"]) : undefined;
|
||||
|
||||
// Sanitize key_alias (limit length, trim whitespace)
|
||||
const sanitizedKeyAlias = keyAlias
|
||||
|
|
@ -157,8 +168,8 @@ function CreateKeyPageContent() {
|
|||
? modelsParam
|
||||
.split(",")
|
||||
.slice(0, 100) // Limit number of models to prevent DoS
|
||||
.map(m => m.trim().slice(0, 256)) // Limit individual model name length
|
||||
.filter(m => m.length > 0) // Remove empty strings
|
||||
.map((m) => m.trim().slice(0, 256)) // Limit individual model name length
|
||||
.filter((m) => m.length > 0) // Remove empty strings
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
|
|
@ -359,7 +370,9 @@ function CreateKeyPageContent() {
|
|||
if (accessToken && userID && userRole) {
|
||||
v2TeamListCall(accessToken, 1, 100, {
|
||||
userID: userRole !== "Admin" && userRole !== "Admin Viewer" ? userID : null,
|
||||
}).then((response) => setTeams(response.teams ?? [])).catch(console.error);
|
||||
})
|
||||
.then((response) => setTeams(response.teams ?? []))
|
||||
.catch(console.error);
|
||||
}
|
||||
if (accessToken) {
|
||||
fetchOrganizations(accessToken, setOrganizations);
|
||||
|
|
@ -453,239 +466,237 @@ function CreateKeyPageContent() {
|
|||
|
||||
return (
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<ConfigProvider theme={{
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm: isDarkMode ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
||||
}}>
|
||||
<ThemeProvider accessToken={accessToken}>
|
||||
{invitation_id ? (
|
||||
<UserDashboard
|
||||
}}
|
||||
>
|
||||
<ThemeProvider accessToken={accessToken}>
|
||||
{invitation_id ? (
|
||||
<UserDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
premiumUser={premiumUser}
|
||||
teams={teams}
|
||||
keys={keys}
|
||||
setUserRole={setUserRole}
|
||||
userEmail={userEmail}
|
||||
setUserEmail={setUserEmail}
|
||||
setTeams={setTeams}
|
||||
setKeys={setKeys}
|
||||
organizations={organizations}
|
||||
addKey={addKey}
|
||||
createClicked={createClicked}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Navbar
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
premiumUser={premiumUser}
|
||||
teams={teams}
|
||||
keys={keys}
|
||||
setUserRole={setUserRole}
|
||||
userEmail={userEmail}
|
||||
setUserEmail={setUserEmail}
|
||||
setTeams={setTeams}
|
||||
setKeys={setKeys}
|
||||
organizations={organizations}
|
||||
addKey={addKey}
|
||||
createClicked={createClicked}
|
||||
setProxySettings={setProxySettings}
|
||||
proxySettings={proxySettings}
|
||||
accessToken={accessToken}
|
||||
isPublicPage={false}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
onToggleSidebar={toggleSidebar}
|
||||
isDarkMode={isDarkMode}
|
||||
toggleDarkMode={toggleDarkMode}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Navbar
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
premiumUser={premiumUser}
|
||||
userEmail={userEmail}
|
||||
setProxySettings={setProxySettings}
|
||||
proxySettings={proxySettings}
|
||||
accessToken={accessToken}
|
||||
isPublicPage={false}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
onToggleSidebar={toggleSidebar}
|
||||
isDarkMode={isDarkMode}
|
||||
toggleDarkMode={toggleDarkMode}
|
||||
/>
|
||||
<div className="flex flex-1">
|
||||
<div className="mt-2">
|
||||
<div className="flex flex-1">
|
||||
<div className="mt-2">
|
||||
<SidebarProvider setPage={updatePage} defaultSelectedKey={page} sidebarCollapsed={sidebarCollapsed} />
|
||||
</div>
|
||||
{page == "api-keys" ? (
|
||||
<UserDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
{page == "api-keys" ? (
|
||||
<UserDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
premiumUser={premiumUser}
|
||||
teams={teams}
|
||||
keys={keys}
|
||||
setUserRole={setUserRole}
|
||||
userEmail={userEmail}
|
||||
setUserEmail={setUserEmail}
|
||||
setTeams={setTeams}
|
||||
setKeys={setKeys}
|
||||
organizations={organizations}
|
||||
addKey={addKey}
|
||||
createClicked={createClicked}
|
||||
autoOpenCreate={autoOpenCreate}
|
||||
prefillData={prefillData}
|
||||
/>
|
||||
) : page == "models" ? (
|
||||
<OldModelDashboard
|
||||
token={token}
|
||||
keys={keys}
|
||||
modelData={modelData}
|
||||
setModelData={setModelData}
|
||||
premiumUser={premiumUser}
|
||||
teams={teams}
|
||||
/>
|
||||
) : page == "llm-playground" ? (
|
||||
<PlaygroundPage />
|
||||
) : page == "users" ? (
|
||||
<ViewUserDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
keys={keys}
|
||||
teams={teams}
|
||||
accessToken={accessToken}
|
||||
setKeys={setKeys}
|
||||
/>
|
||||
) : page == "teams" ? (
|
||||
<OldTeams
|
||||
teams={teams}
|
||||
setTeams={setTeams}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
organizations={organizations}
|
||||
premiumUser={premiumUser}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
) : page == "organizations" ? (
|
||||
<Organizations
|
||||
organizations={organizations}
|
||||
setOrganizations={setOrganizations}
|
||||
userModels={userModels}
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "admin-panel" ? (
|
||||
<AdminPanel proxySettings={proxySettings} />
|
||||
) : page == "logging-and-alerts" ? (
|
||||
<Settings userID={userID} userRole={userRole} accessToken={accessToken} premiumUser={premiumUser} />
|
||||
) : page == "budgets" ? (
|
||||
<BudgetPanel accessToken={accessToken} />
|
||||
) : page == "guardrails" ? (
|
||||
<GuardrailsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "policies" ? (
|
||||
<PoliciesPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "agents" ? (
|
||||
<AgentsPanel accessToken={accessToken} userRole={userRole} teams={teams} />
|
||||
) : page == "prompts" ? (
|
||||
<PromptsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "transform-request" ? (
|
||||
<TransformRequestPanel accessToken={accessToken} />
|
||||
) : page == "router-settings" ? (
|
||||
<GeneralSettings
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
modelData={modelData}
|
||||
/>
|
||||
) : page == "ui-theme" ? (
|
||||
<UIThemeSettings userID={userID} userRole={userRole} accessToken={accessToken} />
|
||||
) : page == "cost-tracking" ? (
|
||||
<CostTrackingSettings userID={userID} userRole={userRole} accessToken={accessToken} />
|
||||
) : page == "model-hub-table" ? (
|
||||
isAdminRole(userRole) ? (
|
||||
<ModelHubTable
|
||||
accessToken={accessToken}
|
||||
publicPage={false}
|
||||
premiumUser={premiumUser}
|
||||
teams={teams}
|
||||
keys={keys}
|
||||
setUserRole={setUserRole}
|
||||
userEmail={userEmail}
|
||||
setUserEmail={setUserEmail}
|
||||
setTeams={setTeams}
|
||||
setKeys={setKeys}
|
||||
organizations={organizations}
|
||||
addKey={addKey}
|
||||
createClicked={createClicked}
|
||||
autoOpenCreate={autoOpenCreate}
|
||||
prefillData={prefillData}
|
||||
/>
|
||||
) : page == "models" ? (
|
||||
<OldModelDashboard
|
||||
token={token}
|
||||
keys={keys}
|
||||
modelData={modelData}
|
||||
setModelData={setModelData}
|
||||
premiumUser={premiumUser}
|
||||
teams={teams}
|
||||
/>
|
||||
) : page == "llm-playground" ? (
|
||||
<PlaygroundPage />
|
||||
) : page == "users" ? (
|
||||
<ViewUserDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
keys={keys}
|
||||
teams={teams}
|
||||
accessToken={accessToken}
|
||||
setKeys={setKeys}
|
||||
/>
|
||||
) : page == "teams" ? (
|
||||
<OldTeams
|
||||
teams={teams}
|
||||
setTeams={setTeams}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
organizations={organizations}
|
||||
premiumUser={premiumUser}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
) : page == "organizations" ? (
|
||||
<Organizations
|
||||
organizations={organizations}
|
||||
setOrganizations={setOrganizations}
|
||||
userModels={userModels}
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "admin-panel" ? (
|
||||
<AdminPanel
|
||||
proxySettings={proxySettings}
|
||||
/>
|
||||
) : page == "logging-and-alerts" ? (
|
||||
<Settings userID={userID} userRole={userRole} accessToken={accessToken} premiumUser={premiumUser} />
|
||||
) : page == "budgets" ? (
|
||||
<BudgetPanel accessToken={accessToken} />
|
||||
) : page == "guardrails" ? (
|
||||
<GuardrailsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "policies" ? (
|
||||
<PoliciesPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "agents" ? (
|
||||
<AgentsPanel accessToken={accessToken} userRole={userRole} teams={teams} />
|
||||
) : page == "prompts" ? (
|
||||
<PromptsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "transform-request" ? (
|
||||
<TransformRequestPanel accessToken={accessToken} />
|
||||
) : page == "router-settings" ? (
|
||||
<GeneralSettings
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
modelData={modelData}
|
||||
/>
|
||||
) : page == "ui-theme" ? (
|
||||
<UIThemeSettings userID={userID} userRole={userRole} accessToken={accessToken} />
|
||||
) : page == "cost-tracking" ? (
|
||||
<CostTrackingSettings userID={userID} userRole={userRole} accessToken={accessToken} />
|
||||
) : page == "model-hub-table" ? (
|
||||
isAdminRole(userRole) ? (
|
||||
<ModelHubTable
|
||||
accessToken={accessToken}
|
||||
publicPage={false}
|
||||
premiumUser={premiumUser}
|
||||
userRole={userRole}
|
||||
/>
|
||||
) : (
|
||||
<PublicModelHub accessToken={accessToken} isEmbedded={true} />
|
||||
)
|
||||
) : page == "caching" ? (
|
||||
<CacheDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "pass-through-settings" ? (
|
||||
<PassThroughSettings
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
modelData={modelData}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "logs" ? (
|
||||
<SpendLogsTable
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "mcp-servers" ? (
|
||||
<MCPServers accessToken={accessToken} userRole={userRole} userID={userID} />
|
||||
) : page == "search-tools" ? (
|
||||
<SearchTools accessToken={accessToken} userRole={userRole} userID={userID} />
|
||||
) : page == "tag-management" ? (
|
||||
<TagManagement accessToken={accessToken} userRole={userRole} userID={userID} />
|
||||
) : page == "skills" || page == "claude-code-plugins" ? (
|
||||
<ClaudeCodePluginsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "access-groups" ? (
|
||||
<AccessGroupsPage />
|
||||
) : page == "projects" ? (
|
||||
<ProjectsPage />
|
||||
) : page == "vector-stores" ? (
|
||||
<VectorStoreManagement accessToken={accessToken} userRole={userRole} userID={userID} />
|
||||
) : page == "tool-policies" ? (
|
||||
<ToolPoliciesView accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "workflows" ? (
|
||||
<WorkflowRuns accessToken={accessToken} />
|
||||
) : page == "memory" ? (
|
||||
<MemoryView
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
/>
|
||||
) : page == "guardrails-monitor" ? (
|
||||
<GuardrailsMonitorView accessToken={accessToken} />
|
||||
) : page == "new_usage" ? (
|
||||
<NewUsagePage
|
||||
teams={(teams as Team[]) ?? []}
|
||||
organizations={(organizations as Organization[]) ?? []}
|
||||
/>
|
||||
) : (
|
||||
<Usage
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
keys={keys}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Survey Components */}
|
||||
<SurveyPrompt
|
||||
isVisible={showSurveyPrompt}
|
||||
onOpen={handleOpenSurvey}
|
||||
onDismiss={handleDismissSurveyPrompt}
|
||||
/>
|
||||
<SurveyModal
|
||||
isOpen={showSurveyModal}
|
||||
onClose={handleSurveyModalClose}
|
||||
onComplete={handleSurveyComplete}
|
||||
/>
|
||||
|
||||
{/* Claude Code Components */}
|
||||
<ClaudeCodePrompt
|
||||
isVisible={showClaudeCodePrompt}
|
||||
onOpen={handleOpenClaudeCode}
|
||||
onDismiss={handleDismissClaudeCodePrompt}
|
||||
/>
|
||||
<ClaudeCodeModal
|
||||
isOpen={showClaudeCodeModal}
|
||||
onClose={handleClaudeCodeModalClose}
|
||||
onComplete={handleClaudeCodeComplete}
|
||||
/>
|
||||
<PublicModelHub accessToken={accessToken} isEmbedded={true} />
|
||||
)
|
||||
) : page == "caching" ? (
|
||||
<CacheDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "pass-through-settings" ? (
|
||||
<PassThroughSettings
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
modelData={modelData}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "logs" ? (
|
||||
<SpendLogsTable
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : page == "mcp-servers" ? (
|
||||
<MCPServers accessToken={accessToken} userRole={userRole} userID={userID} />
|
||||
) : page == "search-tools" ? (
|
||||
<SearchTools accessToken={accessToken} userRole={userRole} userID={userID} />
|
||||
) : page == "fetch-tools" ? (
|
||||
<FetchTools accessToken={accessToken} userRole={userRole} userID={userID} />
|
||||
) : page == "tag-management" ? (
|
||||
<TagManagement accessToken={accessToken} userRole={userRole} userID={userID} />
|
||||
) : page == "skills" || page == "claude-code-plugins" ? (
|
||||
<ClaudeCodePluginsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "access-groups" ? (
|
||||
<AccessGroupsPage />
|
||||
) : page == "projects" ? (
|
||||
<ProjectsPage />
|
||||
) : page == "vector-stores" ? (
|
||||
<VectorStoreManagement accessToken={accessToken} userRole={userRole} userID={userID} />
|
||||
) : page == "tool-policies" ? (
|
||||
<ToolPoliciesView accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "workflows" ? (
|
||||
<WorkflowRuns accessToken={accessToken} />
|
||||
) : page == "memory" ? (
|
||||
<MemoryView accessToken={accessToken} userID={userID} userRole={userRole} />
|
||||
) : page == "guardrails-monitor" ? (
|
||||
<GuardrailsMonitorView accessToken={accessToken} />
|
||||
) : page == "new_usage" ? (
|
||||
<NewUsagePage
|
||||
teams={(teams as Team[]) ?? []}
|
||||
organizations={(organizations as Organization[]) ?? []}
|
||||
/>
|
||||
) : (
|
||||
<Usage
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
keys={keys}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ThemeProvider>
|
||||
</ConfigProvider>
|
||||
|
||||
{/* Survey Components */}
|
||||
<SurveyPrompt
|
||||
isVisible={showSurveyPrompt}
|
||||
onOpen={handleOpenSurvey}
|
||||
onDismiss={handleDismissSurveyPrompt}
|
||||
/>
|
||||
<SurveyModal
|
||||
isOpen={showSurveyModal}
|
||||
onClose={handleSurveyModalClose}
|
||||
onComplete={handleSurveyComplete}
|
||||
/>
|
||||
|
||||
{/* Claude Code Components */}
|
||||
<ClaudeCodePrompt
|
||||
isVisible={showClaudeCodePrompt}
|
||||
onOpen={handleOpenClaudeCode}
|
||||
onDismiss={handleDismissClaudeCodePrompt}
|
||||
/>
|
||||
<ClaudeCodeModal
|
||||
isOpen={showClaudeCodeModal}
|
||||
onClose={handleClaudeCodeModalClose}
|
||||
onComplete={handleClaudeCodeComplete}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ThemeProvider>
|
||||
</ConfigProvider>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { Form, Input, Modal, Select, Spin } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import { createFetchTool } from "../networking";
|
||||
import { AvailableFetchProvider, FetchTool } from "./types";
|
||||
|
||||
interface CreateFetchToolProps {
|
||||
accessToken: string | null;
|
||||
availableProviders: AvailableFetchProvider[];
|
||||
onSuccess: (newFetchTool: FetchTool) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const CreateFetchTool: React.FC<CreateFetchToolProps> = ({ accessToken, availableProviders, onSuccess, onCancel }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setIsLoading(true);
|
||||
|
||||
const fetchToolData = {
|
||||
fetch_tool_name: values.fetch_tool_name,
|
||||
litellm_params: {
|
||||
fetch_provider: values.fetch_provider,
|
||||
api_key: values.api_key,
|
||||
api_base: values.api_base,
|
||||
timeout: values.timeout ? parseFloat(values.timeout) : undefined,
|
||||
max_retries: values.max_retries ? parseInt(values.max_retries) : undefined,
|
||||
},
|
||||
fetch_tool_info: values.description
|
||||
? {
|
||||
description: values.description,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const result = await createFetchTool(accessToken, fetchToolData);
|
||||
NotificationsManager.success("Fetch tool created successfully");
|
||||
form.resetFields();
|
||||
onSuccess(result);
|
||||
} catch (error) {
|
||||
console.error("Failed to create fetch tool:", error);
|
||||
NotificationsManager.error("Failed to create fetch tool");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Spin spinning={isLoading} indicator={<LoadingOutlined spin />} size="large">
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="fetch_tool_name"
|
||||
label="Fetch Tool Name"
|
||||
rules={[{ required: true, message: "Please enter a fetch tool name" }]}
|
||||
>
|
||||
<Input placeholder="e.g., my-firecrawl-fetch" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="fetch_provider"
|
||||
label="Fetch Provider"
|
||||
rules={[{ required: true, message: "Please select a fetch provider" }]}
|
||||
>
|
||||
<Select placeholder="Select a fetch provider">
|
||||
{availableProviders.map((provider) => (
|
||||
<Select.Option key={provider.provider_name} value={provider.provider_name}>
|
||||
{provider.ui_friendly_name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="api_key" label="API Key" extra="API key for the fetch provider">
|
||||
<Input.Password placeholder="Enter API key" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="api_base" label="API Base URL" extra="Optional: Custom API base URL for self-hosted instances">
|
||||
<Input placeholder="https://api.firecrawl.dev" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="timeout" label="Timeout (seconds)">
|
||||
<Input type="number" placeholder="30" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="max_retries" label="Max Retries">
|
||||
<Input type="number" placeholder="3" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label="Description">
|
||||
<Input.TextArea rows={3} placeholder="Description of this fetch tool" />
|
||||
</Form.Item>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<button onClick={onCancel} className="px-4 py-2 border rounded hover:bg-gray-50" disabled={isLoading}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Create Fetch Tool
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateFetchTool;
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import { Tag } from "antd";
|
||||
import { ColumnsType } from "antd/es/table";
|
||||
import TableIconActionButton from "../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
|
||||
import { FetchTool } from "./types";
|
||||
|
||||
export const fetchToolColumns = (
|
||||
onView: (fetchToolId: string) => void,
|
||||
onEdit: (fetchToolId: string) => void,
|
||||
onDelete: (fetchToolId: string) => void,
|
||||
availableProviders: Array<{ provider_name: string; ui_friendly_name: string }>,
|
||||
): ColumnsType<FetchTool> => [
|
||||
{
|
||||
title: "Fetch Tool ID",
|
||||
dataIndex: "fetch_tool_id",
|
||||
key: "fetch_tool_id",
|
||||
render: (_, tool) => {
|
||||
const isFromConfig = tool.is_from_config;
|
||||
|
||||
if (isFromConfig) {
|
||||
return <span className="text-xs">-</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => onView(tool.fetch_tool_id!)}
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left cursor-pointer max-w-40"
|
||||
>
|
||||
<span className="truncate block">{tool.fetch_tool_id}</span>
|
||||
</button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "fetch_tool_name",
|
||||
key: "fetch_tool_name",
|
||||
render: (name: string) => <span className="font-medium">{name}</span>,
|
||||
},
|
||||
{
|
||||
title: "Provider",
|
||||
key: "provider",
|
||||
render: (_, tool) => {
|
||||
const provider = tool.litellm_params.fetch_provider;
|
||||
const providerInfo = availableProviders.find((p) => p.provider_name === provider);
|
||||
const displayName = providerInfo?.ui_friendly_name || provider;
|
||||
|
||||
return <span className="text-sm">{displayName}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Created At",
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
render: (_, tool) => {
|
||||
return <span className="text-xs">{tool.created_at ? new Date(tool.created_at).toLocaleDateString() : "-"}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Updated At",
|
||||
dataIndex: "updated_at",
|
||||
key: "updated_at",
|
||||
render: (_, tool) => {
|
||||
return <span className="text-xs">{tool.updated_at ? new Date(tool.updated_at).toLocaleDateString() : "-"}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Source",
|
||||
key: "source",
|
||||
render: (_, tool) => {
|
||||
const isFromConfig = tool.is_from_config ?? false;
|
||||
|
||||
return <Tag color={isFromConfig ? "default" : "blue"}>{isFromConfig ? "Config" : "DB"}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Actions",
|
||||
key: "actions",
|
||||
render: (_, tool) => {
|
||||
const toolId = tool.fetch_tool_id;
|
||||
const isFromConfig = tool.is_from_config ?? false;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
tooltipText="Edit fetch tool"
|
||||
disabled={isFromConfig}
|
||||
disabledTooltipText="Config fetch tool cannot be edited on the dashboard. Please edit it from the config file."
|
||||
onClick={() => {
|
||||
if (toolId && !isFromConfig) {
|
||||
onEdit(toolId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
tooltipText="Delete fetch tool"
|
||||
disabled={isFromConfig}
|
||||
disabledTooltipText="Config fetch tool cannot be deleted on the dashboard. Please delete it from the config file."
|
||||
onClick={() => {
|
||||
if (toolId && !isFromConfig) {
|
||||
onDelete(toolId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Select } from "antd";
|
||||
import { fetchFetchTools } from "../networking";
|
||||
|
||||
export interface FetchToolSelectorProps {
|
||||
onChange: (selected: string[]) => void;
|
||||
value?: string[];
|
||||
className?: string;
|
||||
accessToken: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const FetchToolSelector: React.FC<FetchToolSelectorProps> = ({
|
||||
onChange,
|
||||
value,
|
||||
className,
|
||||
accessToken,
|
||||
placeholder = "Select fetch tools (optional)",
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [options, setOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
if (!accessToken) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await fetchFetchTools(accessToken);
|
||||
const tools = Array.isArray(data?.fetch_tools) ? data.fetch_tools : Array.isArray(data?.data) ? data.data : [];
|
||||
setOptions(
|
||||
tools
|
||||
.map((tool: { fetch_tool_name?: string }) => tool?.fetch_tool_name)
|
||||
.filter((name: unknown): name is string => typeof name === "string" && name.length > 0)
|
||||
.map((name: string) => ({ label: name, value: name })),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Failed to load fetch tools:", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
load();
|
||||
}, [accessToken]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
loading={loading}
|
||||
className={className}
|
||||
options={options}
|
||||
style={{ width: "100%" }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default FetchToolSelector;
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import React from "react";
|
||||
import { Button, Tag } from "antd";
|
||||
import { AvailableFetchProvider, FetchTool } from "./types";
|
||||
|
||||
interface FetchToolViewProps {
|
||||
fetchTool: FetchTool;
|
||||
onBack: () => void;
|
||||
isEditing: boolean;
|
||||
accessToken: string;
|
||||
availableProviders: AvailableFetchProvider[];
|
||||
}
|
||||
|
||||
const FetchToolView: React.FC<FetchToolViewProps> = ({ fetchTool, onBack, availableProviders }) => {
|
||||
const providerInfo = availableProviders.find((p) => p.provider_name === fetchTool.litellm_params?.fetch_provider);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">{fetchTool.fetch_tool_name}</h2>
|
||||
<p className="text-gray-500 mt-1">
|
||||
{providerInfo?.ui_friendly_name || fetchTool.litellm_params?.fetch_provider}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={onBack}>Back to List</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-50 p-4 rounded">
|
||||
<h3 className="font-semibold mb-2">Configuration</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<span className="text-gray-500">Provider:</span>
|
||||
<span className="ml-2">{fetchTool.litellm_params?.fetch_provider}</span>
|
||||
</div>
|
||||
{fetchTool.litellm_params?.api_base && (
|
||||
<div>
|
||||
<span className="text-gray-500">API Base:</span>
|
||||
<span className="ml-2">{fetchTool.litellm_params.api_base}</span>
|
||||
</div>
|
||||
)}
|
||||
{fetchTool.litellm_params?.timeout && (
|
||||
<div>
|
||||
<span className="text-gray-500">Timeout:</span>
|
||||
<span className="ml-2">{fetchTool.litellm_params.timeout}s</span>
|
||||
</div>
|
||||
)}
|
||||
{fetchTool.litellm_params?.max_retries && (
|
||||
<div>
|
||||
<span className="text-gray-500">Max Retries:</span>
|
||||
<span className="ml-2">{fetchTool.litellm_params.max_retries}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{fetchTool.fetch_tool_info?.description && (
|
||||
<div className="bg-gray-50 p-4 rounded">
|
||||
<h3 className="font-semibold mb-2">Description</h3>
|
||||
<p>{fetchTool.fetch_tool_info.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Tag color={fetchTool.is_from_config ? "default" : "blue"}>
|
||||
{fetchTool.is_from_config ? "Config" : "Database"}
|
||||
</Tag>
|
||||
{fetchTool.created_at && (
|
||||
<Tag color="green">Created: {new Date(fetchTool.created_at).toLocaleDateString()}</Tag>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FetchToolView;
|
||||
309
ui/litellm-dashboard/src/components/FetchTools/FetchTools.tsx
Normal file
309
ui/litellm-dashboard/src/components/FetchTools/FetchTools.tsx
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
import { isAdminRole } from "@/utils/roles";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Button, Text, Title } from "@tremor/react";
|
||||
import { Form, Input, Modal, Select, Spin, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import DeleteResourceModal from "../common_components/DeleteResourceModal";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import { deleteFetchTool, fetchAvailableFetchProviders, fetchFetchTools, updateFetchTool } from "../networking";
|
||||
import CreateFetchTool from "./CreateFetchTool";
|
||||
import { fetchToolColumns } from "./FetchToolColumn";
|
||||
import { FetchToolView } from "./FetchToolView";
|
||||
import { AvailableFetchProvider, FetchTool } from "./types";
|
||||
|
||||
interface FetchToolsProps {
|
||||
accessToken: string | null;
|
||||
userRole: string | null;
|
||||
userID: string | null;
|
||||
}
|
||||
|
||||
const FetchTools: React.FC<FetchToolsProps> = ({ accessToken, userRole, userID }) => {
|
||||
const {
|
||||
data: fetchTools,
|
||||
isLoading: isLoadingTools,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["fetchTools"],
|
||||
queryFn: () => {
|
||||
if (!accessToken) throw new Error("Access Token required");
|
||||
return fetchFetchTools(accessToken).then((res) => res.fetch_tools || []);
|
||||
},
|
||||
enabled: !!accessToken,
|
||||
}) as { data: FetchTool[]; isLoading: boolean; refetch: () => void };
|
||||
|
||||
const { data: providersResponse, isLoading: isLoadingProviders } = useQuery({
|
||||
queryKey: ["fetchProviders"],
|
||||
queryFn: () => {
|
||||
if (!accessToken) throw new Error("Access Token required");
|
||||
return fetchAvailableFetchProviders(accessToken);
|
||||
},
|
||||
enabled: !!accessToken,
|
||||
}) as { data: { providers: AvailableFetchProvider[] }; isLoading: boolean };
|
||||
|
||||
const availableProviders = providersResponse?.providers || [];
|
||||
|
||||
// State
|
||||
const [toolIdToDelete, setToolToDelete] = useState<string | null>(null);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [selectedToolId, setSelectedToolId] = useState<string | null>(null);
|
||||
const [editTool, setEditTool] = useState(false);
|
||||
const [isCreateModalVisible, setCreateModalVisible] = useState(false);
|
||||
const [isEditModalVisible, setEditModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
fetchToolColumns(
|
||||
(toolId: string) => {
|
||||
setSelectedToolId(toolId);
|
||||
setEditTool(false);
|
||||
},
|
||||
(toolId: string) => {
|
||||
const tool = fetchTools?.find((t) => t.fetch_tool_id === toolId);
|
||||
if (tool) {
|
||||
form.setFieldsValue({
|
||||
fetch_tool_name: tool.fetch_tool_name,
|
||||
fetch_provider: tool.litellm_params.fetch_provider,
|
||||
api_key: tool.litellm_params.api_key,
|
||||
api_base: tool.litellm_params.api_base,
|
||||
timeout: tool.litellm_params.timeout,
|
||||
max_retries: tool.litellm_params.max_retries,
|
||||
description: tool.fetch_tool_info?.description,
|
||||
});
|
||||
setSelectedToolId(toolId);
|
||||
setEditModalVisible(true);
|
||||
}
|
||||
},
|
||||
handleDelete,
|
||||
availableProviders,
|
||||
),
|
||||
[availableProviders, fetchTools, form],
|
||||
);
|
||||
|
||||
function handleDelete(toolId: string) {
|
||||
setToolToDelete(toolId);
|
||||
setIsDeleteModalOpen(true);
|
||||
}
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (toolIdToDelete == null || accessToken == null) {
|
||||
return;
|
||||
}
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await deleteFetchTool(accessToken, toolIdToDelete);
|
||||
NotificationsManager.success("Deleted fetch tool successfully");
|
||||
setIsDeleteModalOpen(false);
|
||||
setToolToDelete(null);
|
||||
refetch();
|
||||
} catch (error) {
|
||||
console.error("Error deleting the fetch tool:", error);
|
||||
NotificationsManager.error("Failed to delete fetch tool");
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelDelete = () => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setToolToDelete(null);
|
||||
};
|
||||
|
||||
const toolToDeleteObj = fetchTools?.find((t) => t.fetch_tool_id === toolIdToDelete);
|
||||
const providerInfo = toolToDeleteObj
|
||||
? availableProviders.find((p) => p.provider_name === toolToDeleteObj.litellm_params.fetch_provider)
|
||||
: null;
|
||||
|
||||
const handleCreateSuccess = (newFetchTool: FetchTool) => {
|
||||
setCreateModalVisible(false);
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleEditSubmit = async () => {
|
||||
if (!accessToken || !selectedToolId) return;
|
||||
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const fetchToolData = {
|
||||
fetch_tool_name: values.fetch_tool_name,
|
||||
litellm_params: {
|
||||
fetch_provider: values.fetch_provider,
|
||||
api_key: values.api_key,
|
||||
api_base: values.api_base,
|
||||
timeout: values.timeout ? parseFloat(values.timeout) : undefined,
|
||||
max_retries: values.max_retries ? parseInt(values.max_retries) : undefined,
|
||||
},
|
||||
fetch_tool_info: values.description
|
||||
? {
|
||||
description: values.description,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
await updateFetchTool(accessToken, selectedToolId, fetchToolData);
|
||||
NotificationsManager.success("Fetch tool updated successfully");
|
||||
setEditModalVisible(false);
|
||||
form.resetFields();
|
||||
setSelectedToolId(null);
|
||||
refetch();
|
||||
} catch (error) {
|
||||
console.error("Failed to update fetch tool:", error);
|
||||
NotificationsManager.error("Failed to update fetch tool");
|
||||
}
|
||||
};
|
||||
|
||||
const renderEditForm = () => (
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="fetch_tool_name"
|
||||
label="Fetch Tool Name"
|
||||
rules={[{ required: true, message: "Please enter a fetch tool name" }]}
|
||||
>
|
||||
<Input placeholder="e.g., my-firecrawl-fetch" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="fetch_provider"
|
||||
label="Fetch Provider"
|
||||
rules={[{ required: true, message: "Please select a fetch provider" }]}
|
||||
>
|
||||
<Select placeholder="Select a fetch provider" loading={isLoadingProviders}>
|
||||
{availableProviders.map((provider) => (
|
||||
<Select.Option key={provider.provider_name} value={provider.provider_name}>
|
||||
{provider.ui_friendly_name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="api_key" label="API Key" extra="API key for the fetch provider">
|
||||
<Input.Password placeholder="Enter API key" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="api_base" label="API Base URL" extra="Optional: Custom API base URL for self-hosted instances">
|
||||
<Input placeholder="https://api.firecrawl.dev" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label="Description">
|
||||
<Input.TextArea rows={3} placeholder="Description of this fetch tool" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
|
||||
if (!accessToken || !userRole || !userID) {
|
||||
console.log("Missing required authentication parameters", { accessToken, userRole, userID });
|
||||
return <div className="p-6 text-center text-gray-500">Missing required authentication parameters.</div>;
|
||||
}
|
||||
|
||||
const ToolsTab = () =>
|
||||
selectedToolId ? (
|
||||
<FetchToolView
|
||||
fetchTool={
|
||||
fetchTools?.find((tool: FetchTool) => tool.fetch_tool_id === selectedToolId) || {
|
||||
fetch_tool_id: "",
|
||||
fetch_tool_name: "",
|
||||
litellm_params: {
|
||||
fetch_provider: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
onBack={() => {
|
||||
setEditTool(false);
|
||||
setSelectedToolId(null);
|
||||
refetch();
|
||||
}}
|
||||
isEditing={editTool}
|
||||
accessToken={accessToken}
|
||||
availableProviders={availableProviders}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full">
|
||||
<Spin spinning={isLoadingTools} indicator={<LoadingOutlined spin />} size="large">
|
||||
<Table
|
||||
bordered
|
||||
dataSource={fetchTools || []}
|
||||
columns={columns}
|
||||
rowKey={(record) => record.fetch_tool_id || record.fetch_tool_name}
|
||||
pagination={false}
|
||||
locale={{
|
||||
emptyText: "No fetch tools configured",
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full p-6">
|
||||
<DeleteResourceModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title="Delete Fetch Tool"
|
||||
message="Are you sure you want to delete this fetch tool? This action cannot be undone."
|
||||
resourceInformationTitle="Fetch Tool Information"
|
||||
resourceInformation={[
|
||||
{ label: "Fetch Tool Name", value: toolToDeleteObj?.fetch_tool_name || "" },
|
||||
{
|
||||
label: "Provider",
|
||||
value: providerInfo?.ui_friendly_name || toolToDeleteObj?.litellm_params?.fetch_provider || "",
|
||||
},
|
||||
]}
|
||||
onConfirm={confirmDelete}
|
||||
onCancel={cancelDelete}
|
||||
isDeleting={isDeleting}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col h-full" style={{ maxHeight: "100vh" }}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<Title className="text-lg">Fetch Tools</Title>
|
||||
<Text>Manage your fetch tools for web content extraction</Text>
|
||||
</div>
|
||||
<Button type="primary" onClick={() => setCreateModalVisible(true)} className="bg-blue-600 hover:bg-blue-700">
|
||||
+ Create Fetch Tool
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ToolsTab />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create Modal */}
|
||||
<Modal
|
||||
title="Create Fetch Tool"
|
||||
open={isCreateModalVisible}
|
||||
onCancel={() => setCreateModalVisible(false)}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<CreateFetchTool
|
||||
accessToken={accessToken}
|
||||
availableProviders={availableProviders}
|
||||
onSuccess={handleCreateSuccess}
|
||||
onCancel={() => setCreateModalVisible(false)}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* Edit Modal */}
|
||||
<Modal
|
||||
title="Edit Fetch Tool"
|
||||
open={isEditModalVisible}
|
||||
onCancel={() => {
|
||||
setEditModalVisible(false);
|
||||
form.resetFields();
|
||||
setSelectedToolId(null);
|
||||
}}
|
||||
onOk={handleEditSubmit}
|
||||
okText="Save Changes"
|
||||
width={800}
|
||||
>
|
||||
{renderEditForm()}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FetchTools;
|
||||
1
ui/litellm-dashboard/src/components/FetchTools/index.tsx
Normal file
1
ui/litellm-dashboard/src/components/FetchTools/index.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as FetchTools } from "./FetchTools";
|
||||
36
ui/litellm-dashboard/src/components/FetchTools/types.tsx
Normal file
36
ui/litellm-dashboard/src/components/FetchTools/types.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
export interface FetchToolLiteLLMParams {
|
||||
fetch_provider: string;
|
||||
api_key?: string;
|
||||
api_base?: string;
|
||||
timeout?: number;
|
||||
max_retries?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface FetchToolInfo {
|
||||
description?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface FetchTool {
|
||||
fetch_tool_id?: string;
|
||||
fetch_tool_name: string;
|
||||
litellm_params: FetchToolLiteLLMParams;
|
||||
fetch_tool_info?: FetchToolInfo;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
is_from_config?: boolean;
|
||||
}
|
||||
|
||||
export interface FetchToolsResponse {
|
||||
fetch_tools: FetchTool[];
|
||||
}
|
||||
|
||||
export interface AvailableFetchProvider {
|
||||
provider_name: string;
|
||||
ui_friendly_name: string;
|
||||
}
|
||||
|
||||
export interface AvailableFetchProvidersResponse {
|
||||
providers: AvailableFetchProvider[];
|
||||
}
|
||||
|
|
@ -516,6 +516,10 @@ const Teams: React.FC<TeamProps> = ({
|
|||
Array.isArray(formValues.object_permission_search_tools) &&
|
||||
formValues.object_permission_search_tools.length > 0;
|
||||
|
||||
const hasFetchTools =
|
||||
Array.isArray(formValues.object_permission_fetch_tools) &&
|
||||
formValues.object_permission_fetch_tools.length > 0;
|
||||
|
||||
if (
|
||||
(formValues.allowed_vector_store_ids && formValues.allowed_vector_store_ids.length > 0) ||
|
||||
(formValues.allowed_mcp_servers_and_groups &&
|
||||
|
|
@ -579,6 +583,14 @@ const Teams: React.FC<TeamProps> = ({
|
|||
delete formValues.object_permission_search_tools;
|
||||
}
|
||||
|
||||
if (hasFetchTools) {
|
||||
if (!formValues.object_permission) {
|
||||
formValues.object_permission = {};
|
||||
}
|
||||
formValues.object_permission.fetch_tools = formValues.object_permission_fetch_tools;
|
||||
delete formValues.object_permission_fetch_tools;
|
||||
}
|
||||
|
||||
// Add model_aliases if any are defined
|
||||
if (Object.keys(modelAliases).length > 0) {
|
||||
formValues.model_aliases = modelAliases;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
BookOutlined,
|
||||
CreditCardOutlined,
|
||||
DatabaseOutlined,
|
||||
DownloadOutlined,
|
||||
ExperimentOutlined,
|
||||
ExportOutlined,
|
||||
FileTextOutlined,
|
||||
|
|
@ -32,7 +33,13 @@ import {
|
|||
import type { MenuProps } from "antd";
|
||||
import { ConfigProvider, Layout, Menu } from "antd";
|
||||
import { useMemo } from "react";
|
||||
import { all_admin_roles, internalUserRoles, isAdminRole, isUserTeamAdminForAnyTeam, rolesWithWriteAccess } from "../utils/roles";
|
||||
import {
|
||||
all_admin_roles,
|
||||
internalUserRoles,
|
||||
isAdminRole,
|
||||
isUserTeamAdminForAnyTeam,
|
||||
rolesWithWriteAccess,
|
||||
} from "../utils/roles";
|
||||
import NewBadge from "./common_components/NewBadge";
|
||||
import type { Organization } from "./networking";
|
||||
import UsageIndicator from "./UsageIndicator";
|
||||
|
|
@ -169,11 +176,7 @@ const menuGroups: MenuGroup[] = [
|
|||
{
|
||||
key: "policies",
|
||||
page: "policies",
|
||||
label: (
|
||||
<span className="flex items-center gap-4">
|
||||
Policies
|
||||
</span>
|
||||
),
|
||||
label: <span className="flex items-center gap-4">Policies</span>,
|
||||
icon: <AuditOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
|
|
@ -189,6 +192,12 @@ const menuGroups: MenuGroup[] = [
|
|||
label: "Search Tools",
|
||||
icon: <SearchOutlined />,
|
||||
},
|
||||
{
|
||||
key: "fetch-tools",
|
||||
page: "fetch-tools",
|
||||
label: "Fetch Tools",
|
||||
icon: <DownloadOutlined />,
|
||||
},
|
||||
{
|
||||
key: "vector-stores",
|
||||
page: "vector-stores",
|
||||
|
|
@ -342,7 +351,7 @@ const menuGroups: MenuGroup[] = [
|
|||
page: "usage",
|
||||
label: "Old Usage",
|
||||
icon: <BarChartOutlined />,
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
@ -381,7 +390,10 @@ const menuGroups: MenuGroup[] = [
|
|||
page: "admin-panel",
|
||||
label: (
|
||||
<span className="flex items-center gap-2">
|
||||
Admin Settings <NewBadge dot><span /></NewBadge>
|
||||
Admin Settings{" "}
|
||||
<NewBadge dot>
|
||||
<span />
|
||||
</NewBadge>
|
||||
</span>
|
||||
),
|
||||
icon: <SettingOutlined />,
|
||||
|
|
@ -407,7 +419,17 @@ const menuGroups: MenuGroup[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapsed = false, enabledPagesInternalUsers, enableProjectsUI, disableAgentsForInternalUsers, allowAgentsForTeamAdmins, disableVectorStoresForInternalUsers, allowVectorStoresForTeamAdmins }) => {
|
||||
const Sidebar: React.FC<SidebarProps> = ({
|
||||
setPage,
|
||||
defaultSelectedKey,
|
||||
collapsed = false,
|
||||
enabledPagesInternalUsers,
|
||||
enableProjectsUI,
|
||||
disableAgentsForInternalUsers,
|
||||
allowAgentsForTeamAdmins,
|
||||
disableVectorStoresForInternalUsers,
|
||||
allowVectorStoresForTeamAdmins,
|
||||
}) => {
|
||||
const { userId, accessToken, userRole } = useAuthorized();
|
||||
const { data: organizations } = useOrganizations();
|
||||
const { data: teams } = useTeams();
|
||||
|
|
@ -438,11 +460,7 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
|
|||
|
||||
// Wrap label in <a> so every nav item supports right-click → "Open in new tab"
|
||||
// and Ctrl/Cmd+click to open in a new tab, while preserving SPA navigation for normal clicks.
|
||||
const renderNavLink = (
|
||||
label: React.ReactNode,
|
||||
page: string,
|
||||
externalUrl?: string,
|
||||
): React.ReactNode => {
|
||||
const renderNavLink = (label: React.ReactNode, page: string, externalUrl?: string): React.ReactNode => {
|
||||
if (externalUrl) {
|
||||
return (
|
||||
<a
|
||||
|
|
@ -460,7 +478,11 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
|
|||
const migratedRoute = MIGRATED_PAGES[page];
|
||||
const href = migratedRoute
|
||||
? migratedHref(migratedRoute)
|
||||
: (() => { const params = new URLSearchParams(window.location.search); params.set("page", page); return `?${params.toString()}`; })();
|
||||
: (() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set("page", page);
|
||||
return `?${params.toString()}`;
|
||||
})();
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
|
|
@ -516,8 +538,20 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
|
|||
|
||||
// Hide agents and vector-stores pages for non-admin users when disabled,
|
||||
// unless allow_*_for_team_admins is on and the user is a team admin.
|
||||
if (!isAdmin && item.key === "agents" && disableAgentsForInternalUsers && !(allowAgentsForTeamAdmins && isTeamAdmin)) return false;
|
||||
if (!isAdmin && item.key === "vector-stores" && disableVectorStoresForInternalUsers && !(allowVectorStoresForTeamAdmins && isTeamAdmin)) return false;
|
||||
if (
|
||||
!isAdmin &&
|
||||
item.key === "agents" &&
|
||||
disableAgentsForInternalUsers &&
|
||||
!(allowAgentsForTeamAdmins && isTeamAdmin)
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
!isAdmin &&
|
||||
item.key === "vector-stores" &&
|
||||
disableVectorStoresForInternalUsers &&
|
||||
!(allowVectorStoresForTeamAdmins && isTeamAdmin)
|
||||
)
|
||||
return false;
|
||||
|
||||
// Existing role check
|
||||
if (item.roles && !item.roles.includes(userRole)) return false;
|
||||
|
|
@ -526,9 +560,7 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
|
|||
if (!isAdmin && enabledPagesInternalUsers !== null && enabledPagesInternalUsers !== undefined) {
|
||||
// If item has children, check if any children are visible
|
||||
if (item.children && item.children.length > 0) {
|
||||
const hasVisibleChildren = item.children.some((child) =>
|
||||
enabledPagesInternalUsers.includes(child.page)
|
||||
);
|
||||
const hasVisibleChildren = item.children.some((child) => enabledPagesInternalUsers.includes(child.page));
|
||||
if (hasVisibleChildren) {
|
||||
console.log(`[LeftNav] Parent "${item.page}" (${item.key}): VISIBLE (has visible children)`);
|
||||
return true;
|
||||
|
|
@ -593,12 +625,12 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
|
|||
})),
|
||||
onClick: !item.children
|
||||
? () => {
|
||||
if (item.external_url) {
|
||||
window.open(item.external_url, "_blank");
|
||||
} else {
|
||||
navigateToPage(item.page);
|
||||
if (item.external_url) {
|
||||
window.open(item.external_url, "_blank");
|
||||
} else {
|
||||
navigateToPage(item.page);
|
||||
}
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
})),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7067,6 +7067,160 @@ export const testSearchToolConnection = async (accessToken: string, litellmParam
|
|||
}
|
||||
};
|
||||
|
||||
// Fetch Tools API calls
|
||||
export const fetchFetchTools = async (accessToken: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/fetch_tools/list` : `/fetch_tools/list`;
|
||||
console.log("Fetching fetch tools from:", url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: HTTP_REQUEST.GET,
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Fetched fetch tools:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch fetch tools:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createFetchTool = async (accessToken: string, formValues: Record<string, any>) => {
|
||||
try {
|
||||
console.log("Creating fetch tool with values:", formValues);
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/fetch_tools` : `/fetch_tools`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: HTTP_REQUEST.POST,
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
fetch_tool: formValues,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Created fetch tool:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to create fetch tool:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateFetchTool = async (accessToken: string, fetchToolId: string, formValues: Record<string, any>) => {
|
||||
try {
|
||||
console.log("Updating fetch tool with ID:", fetchToolId, "values:", formValues);
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/fetch_tools/${fetchToolId}` : `/fetch_tools/${fetchToolId}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: HTTP_REQUEST.PUT,
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
fetch_tool: formValues,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Updated fetch tool:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to update fetch tool:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteFetchTool = async (accessToken: string, fetchToolId: string) => {
|
||||
try {
|
||||
const url = (proxyBaseUrl ? `${proxyBaseUrl}` : "") + `/fetch_tools/${fetchToolId}`;
|
||||
console.log("Deleting fetch tool:", fetchToolId);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: HTTP_REQUEST.DELETE,
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Deleted fetch tool:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to delete fetch tool:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchAvailableFetchProviders = async (accessToken: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/fetch_tools/ui/available_providers`
|
||||
: `/fetch_tools/ui/available_providers`;
|
||||
console.log("Fetching available fetch providers from:", url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: HTTP_REQUEST.GET,
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Fetched available fetch providers:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch available fetch providers:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const listMCPTools = async (
|
||||
accessToken: string,
|
||||
serverId: string,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,14 @@ import { useGuardrails } from "@/app/(dashboard)/hooks/guardrails/useGuardrails"
|
|||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
|
||||
import { isProxyAdminRole } from "@/utils/roles";
|
||||
import { EditOutlined, GlobalOutlined, InfoCircleOutlined, MinusCircleOutlined, PlusOutlined, SaveOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
EditOutlined,
|
||||
GlobalOutlined,
|
||||
InfoCircleOutlined,
|
||||
MinusCircleOutlined,
|
||||
PlusOutlined,
|
||||
SaveOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ArrowLeftIcon } from "@heroicons/react/outline";
|
||||
import { Accordion, AccordionBody, AccordionHeader, Badge, Card, Grid, Text, TextInput, Title } from "@tremor/react";
|
||||
import { Button, Form, Input, InputNumber, Select, Space, Switch, Tabs, Tag, Tooltip } from "antd";
|
||||
|
|
@ -43,6 +50,7 @@ import ObjectPermissionsView from "../object_permissions_view";
|
|||
import NumericalInput from "../shared/numerical_input";
|
||||
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector";
|
||||
import SearchToolSelector from "../SearchTools/SearchToolSelector";
|
||||
import FetchToolSelector from "../FetchTools/FetchToolSelector";
|
||||
import EditLoggingSettings from "./EditLoggingSettings";
|
||||
import RouterSettingsAccordion, { RouterSettingsAccordionRef } from "../common_components/RouterSettingsAccordion";
|
||||
import MemberModal from "./EditMembership";
|
||||
|
|
@ -120,6 +128,7 @@ export interface TeamData {
|
|||
agents?: string[];
|
||||
agent_access_groups?: string[];
|
||||
search_tools?: string[];
|
||||
fetch_tools?: string[];
|
||||
};
|
||||
team_member_budget_table: {
|
||||
max_budget: number;
|
||||
|
|
@ -227,10 +236,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
|
||||
const canEditTeam = is_team_admin || is_proxy_admin || is_org_admin || isOrgAdminForTeam;
|
||||
const visibleTabs = useMemo(() => getTeamInfoVisibleTabs(canEditTeam), [canEditTeam]);
|
||||
const defaultTabKey = useMemo(
|
||||
() => getTeamInfoDefaultTab(editTeam, canEditTeam),
|
||||
[editTeam, canEditTeam]
|
||||
);
|
||||
const defaultTabKey = useMemo(() => getTeamInfoDefaultTab(editTeam, canEditTeam), [editTeam, canEditTeam]);
|
||||
|
||||
const fetchTeamInfo = async () => {
|
||||
try {
|
||||
|
|
@ -322,7 +328,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
console.error(`Failed to fetch guardrails for policy ${policyName}:`, error);
|
||||
guardrailsMap[policyName] = [];
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
setPolicyGuardrails(guardrailsMap);
|
||||
} catch (error) {
|
||||
|
|
@ -498,9 +504,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
const killSwitchOnAtSave = values.disable_global_guardrails === true;
|
||||
const optedOutGlobalGuardrails = killSwitchOnAtSave
|
||||
? Array.from(globalGuardrailNames)
|
||||
: Array.from(globalGuardrailNames).filter(
|
||||
(n) => !(values.guardrails || []).includes(n),
|
||||
);
|
||||
: Array.from(globalGuardrailNames).filter((n) => !(values.guardrails || []).includes(n));
|
||||
|
||||
const updateData: any = {
|
||||
team_id: teamId,
|
||||
|
|
@ -522,16 +526,14 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
soft_budget_alerting_emails:
|
||||
typeof values.soft_budget_alerting_emails === "string"
|
||||
? values.soft_budget_alerting_emails
|
||||
.split(",")
|
||||
.map((email: string) => email.trim())
|
||||
.filter((email: string) => email.length > 0)
|
||||
.split(",")
|
||||
.map((email: string) => email.trim())
|
||||
.filter((email: string) => email.length > 0)
|
||||
: values.soft_budget_alerting_emails || [],
|
||||
...(secretManagerSettings !== undefined ? { secret_manager_settings: secretManagerSettings } : {}),
|
||||
},
|
||||
...(values.policies?.length > 0 ? { policies: values.policies } : {}),
|
||||
...(values.organization_id !== info.organization_id
|
||||
? { organization_id: values.organization_id ?? null }
|
||||
: {}),
|
||||
...(values.organization_id !== info.organization_id ? { organization_id: values.organization_id ?? null } : {}),
|
||||
};
|
||||
|
||||
updateData.max_budget = mapEmptyStringToNull(updateData.max_budget);
|
||||
|
|
@ -599,6 +601,10 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
updateData.object_permission.search_tools = values.object_permission_search_tools;
|
||||
}
|
||||
|
||||
if (Array.isArray(values.object_permission_fetch_tools)) {
|
||||
updateData.object_permission.fetch_tools = values.object_permission_fetch_tools;
|
||||
}
|
||||
|
||||
// Pass access_group_ids to the update request
|
||||
if (values.access_group_ids !== undefined) {
|
||||
updateData.access_group_ids = values.access_group_ids;
|
||||
|
|
@ -620,8 +626,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
!(Array.isArray(value) && value.length === 0);
|
||||
|
||||
const hasNewValues = Object.values(currentRouterSettings.router_settings).some(isMeaningfulValue);
|
||||
const hadExistingSettings = info.router_settings &&
|
||||
Object.values(info.router_settings).some(isMeaningfulValue);
|
||||
const hadExistingSettings = info.router_settings && Object.values(info.router_settings).some(isMeaningfulValue);
|
||||
|
||||
// Send if there are new values OR if the user is clearing existing ones
|
||||
if (hasNewValues || hadExistingSettings) {
|
||||
|
|
@ -659,10 +664,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
);
|
||||
const effectiveGuardrails: string[] = initialKillSwitchOn
|
||||
? nonGlobalOptIns
|
||||
: [
|
||||
...Array.from(globalGuardrailNames).filter((n) => !optedOutGlobals.has(n)),
|
||||
...nonGlobalOptIns,
|
||||
];
|
||||
: [...Array.from(globalGuardrailNames).filter((n) => !optedOutGlobals.has(n)), ...nonGlobalOptIns];
|
||||
|
||||
const preventTagMouseDown = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -699,12 +701,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
<div className="p-4">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftIcon className="h-4 w-4" />}
|
||||
onClick={onClose}
|
||||
className="mb-4"
|
||||
>
|
||||
<Button type="text" icon={<ArrowLeftIcon className="h-4 w-4" />} onClick={onClose} className="mb-4">
|
||||
Back to Teams
|
||||
</Button>
|
||||
<Title>{info.team_alias}</Title>
|
||||
|
|
@ -715,10 +712,11 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
size="small"
|
||||
icon={copiedStates["team-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
|
||||
onClick={() => copyToClipboard(info.team_id, "team-id")}
|
||||
className={`left-2 z-10 transition-all duration-200 ${copiedStates["team-id"]
|
||||
? "text-green-600 bg-green-50 border-green-200"
|
||||
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
className={`left-2 z-10 transition-all duration-200 ${
|
||||
copiedStates["team-id"]
|
||||
? "text-green-600 bg-green-50 border-green-200"
|
||||
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -868,13 +866,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
{
|
||||
key: TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS,
|
||||
label: TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS],
|
||||
children: (
|
||||
<TeamVirtualKeysTable
|
||||
teamId={teamId}
|
||||
teamAlias={info.team_alias}
|
||||
organization={organization}
|
||||
/>
|
||||
),
|
||||
children: <TeamVirtualKeysTable teamId={teamId} teamAlias={info.team_alias} organization={organization} />,
|
||||
},
|
||||
{
|
||||
key: TEAM_INFO_TAB_KEYS.MEMBERS,
|
||||
|
|
@ -893,9 +885,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
{
|
||||
key: TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS,
|
||||
label: TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS],
|
||||
children: (
|
||||
<MemberPermissions teamId={teamId} accessToken={accessToken} canEditTeam={canEditTeam} />
|
||||
),
|
||||
children: <MemberPermissions teamId={teamId} accessToken={accessToken} canEditTeam={canEditTeam} />,
|
||||
},
|
||||
{
|
||||
key: TEAM_INFO_TAB_KEYS.SETTINGS,
|
||||
|
|
@ -905,7 +895,9 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Team Settings</Title>
|
||||
{canEditTeam && !isEditing && (
|
||||
<Button icon={<EditOutlined className="h-4 w-4" />} onClick={() => setIsEditing(true)}>Edit Settings</Button>
|
||||
<Button icon={<EditOutlined className="h-4 w-4" />} onClick={() => setIsEditing(true)}>
|
||||
Edit Settings
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -933,6 +925,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
tpm_limit: info.tpm_limit,
|
||||
rpm_limit: info.rpm_limit,
|
||||
object_permission_search_tools: info.object_permission?.search_tools || [],
|
||||
object_permission_fetch_tools: info.object_permission?.fetch_tools || [],
|
||||
modelLimits: Array.from(
|
||||
new Set([
|
||||
...Object.keys(info.metadata?.model_tpm_limit ?? {}),
|
||||
|
|
@ -953,16 +946,22 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
guardrails: effectiveGuardrails,
|
||||
policies: info.policies || [],
|
||||
disable_global_guardrails: info.metadata?.disable_global_guardrails || false,
|
||||
soft_budget_alerting_emails:
|
||||
Array.isArray(info.metadata?.soft_budget_alerting_emails)
|
||||
? info.metadata.soft_budget_alerting_emails.join(", ")
|
||||
: "",
|
||||
soft_budget_alerting_emails: Array.isArray(info.metadata?.soft_budget_alerting_emails)
|
||||
? info.metadata.soft_budget_alerting_emails.join(", ")
|
||||
: "",
|
||||
metadata: info.metadata
|
||||
? JSON.stringify(
|
||||
(({ logging, secret_manager_settings, soft_budget_alerting_emails, model_tpm_limit, model_rpm_limit, ...rest }) => rest)(info.metadata),
|
||||
null,
|
||||
2,
|
||||
)
|
||||
(({
|
||||
logging,
|
||||
secret_manager_settings,
|
||||
soft_budget_alerting_emails,
|
||||
model_tpm_limit,
|
||||
model_rpm_limit,
|
||||
...rest
|
||||
}) => rest)(info.metadata),
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "",
|
||||
logging_settings: info.metadata?.logging || [],
|
||||
secret_manager_settings: info.metadata?.secret_manager_settings
|
||||
|
|
@ -1008,7 +1007,8 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
options={{
|
||||
includeSpecialOptions: true,
|
||||
includeUserModels: !teamData?.team_info?.organization_id,
|
||||
showAllProxyModelsOverride: isProxyAdminRole(userRole) && !teamData?.team_info?.organization_id,
|
||||
showAllProxyModelsOverride:
|
||||
isProxyAdminRole(userRole) && !teamData?.team_info?.organization_id,
|
||||
}}
|
||||
context="team"
|
||||
dataTestId="models-select"
|
||||
|
|
@ -1037,7 +1037,8 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<Text className="text-xs text-gray-500 mb-4">
|
||||
Optional defaults applied when members join this team. All fields can be overridden per member.
|
||||
Optional defaults applied when members join this team. All fields can be overridden per
|
||||
member.
|
||||
</Text>
|
||||
<Form.Item
|
||||
label={
|
||||
|
|
@ -1126,11 +1127,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Space
|
||||
key={key}
|
||||
style={{ display: "flex", marginBottom: 8 }}
|
||||
align="baseline"
|
||||
>
|
||||
<Space key={key} style={{ display: "flex", marginBottom: 8 }} align="baseline">
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, "model"]}
|
||||
|
|
@ -1140,9 +1137,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
validator: (_, value) => {
|
||||
if (!value) return Promise.resolve();
|
||||
const all = form.getFieldValue("modelLimits") ?? [];
|
||||
const dupes = all.filter(
|
||||
(entry: { model?: string }) => entry?.model === value,
|
||||
);
|
||||
const dupes = all.filter((entry: { model?: string }) => entry?.model === value);
|
||||
if (dupes.length > 1) {
|
||||
return Promise.reject(new Error("Duplicate model"));
|
||||
}
|
||||
|
|
@ -1182,19 +1177,11 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
<Form.Item {...restField} name={[name, "rpm"]}>
|
||||
<InputNumber placeholder="RPM Limit" min={0} />
|
||||
</Form.Item>
|
||||
<MinusCircleOutlined
|
||||
onClick={() => remove(name)}
|
||||
style={{ color: "#ef4444" }}
|
||||
/>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} style={{ color: "#ef4444" }} />
|
||||
</Space>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
Add Model Limit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
|
@ -1260,11 +1247,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
{(guardrailsData?.guardrails ?? [])
|
||||
.filter((g) => !g.litellm_params?.default_on)
|
||||
.map((g) => (
|
||||
<Select.Option
|
||||
key={g.guardrail_name}
|
||||
value={g.guardrail_name}
|
||||
label={g.guardrail_name}
|
||||
>
|
||||
<Select.Option key={g.guardrail_name} value={g.guardrail_name} label={g.guardrail_name}>
|
||||
{g.guardrail_name}
|
||||
</Select.Option>
|
||||
))}
|
||||
|
|
@ -1406,6 +1389,26 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
</AccordionBody>
|
||||
</Accordion>
|
||||
|
||||
<Accordion className="mt-4 mb-4">
|
||||
<AccordionHeader>
|
||||
<b>Fetch Tool Settings</b>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<Form.Item
|
||||
label="Allowed Fetch Tools"
|
||||
name="object_permission_fetch_tools"
|
||||
tooltip="Select which fetch tools this team can access. Leave empty to allow all fetch tools."
|
||||
>
|
||||
<FetchToolSelector
|
||||
onChange={(vals: string[]) => form.setFieldValue("object_permission_fetch_tools", vals)}
|
||||
value={form.getFieldValue("object_permission_fetch_tools")}
|
||||
accessToken={accessToken || ""}
|
||||
placeholder="Select fetch tools (optional, empty = all allowed)"
|
||||
/>
|
||||
</Form.Item>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
|
||||
<Form.Item label="Organization" name="organization_id">
|
||||
<Select
|
||||
allowClear
|
||||
|
|
@ -1466,7 +1469,12 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
<Button onClick={() => setIsEditing(false)} disabled={isTeamSaving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button icon={<SaveOutlined className="h-4 w-4" />} type="primary" htmlType="submit" loading={isTeamSaving}>
|
||||
<Button
|
||||
icon={<SaveOutlined className="h-4 w-4" />}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={isTeamSaving}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -1545,9 +1553,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
{info.metadata?.soft_budget_alerting_emails &&
|
||||
Array.isArray(info.metadata.soft_budget_alerting_emails) &&
|
||||
info.metadata.soft_budget_alerting_emails.length > 0 && (
|
||||
<div>
|
||||
Soft Budget Alerting Emails: {info.metadata.soft_budget_alerting_emails.join(", ")}
|
||||
</div>
|
||||
<div>Soft Budget Alerting Emails: {info.metadata.soft_budget_alerting_emails.join(", ")}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -1565,14 +1571,14 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Router Settings</Text>
|
||||
{info.router_settings && Object.values(info.router_settings).some(
|
||||
(v) => v !== null && v !== undefined && v !== "" && !(Array.isArray(v) && v.length === 0)
|
||||
{info.router_settings &&
|
||||
Object.values(info.router_settings).some(
|
||||
(v) => v !== null && v !== undefined && v !== "" && !(Array.isArray(v) && v.length === 0),
|
||||
) ? (
|
||||
<div className="mt-1 space-y-1">
|
||||
{info.router_settings.routing_strategy && (
|
||||
<div>
|
||||
Routing Strategy:{" "}
|
||||
<Badge color="blue">{info.router_settings.routing_strategy}</Badge>
|
||||
Routing Strategy: <Badge color="blue">{info.router_settings.routing_strategy}</Badge>
|
||||
</div>
|
||||
)}
|
||||
{info.router_settings.num_retries != null && (
|
||||
|
|
@ -1584,18 +1590,16 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
{info.router_settings.cooldown_time != null && (
|
||||
<div>Cooldown Time: {info.router_settings.cooldown_time}s</div>
|
||||
)}
|
||||
{info.router_settings.timeout != null && (
|
||||
<div>Timeout: {info.router_settings.timeout}s</div>
|
||||
)}
|
||||
{info.router_settings.timeout != null && <div>Timeout: {info.router_settings.timeout}s</div>}
|
||||
{info.router_settings.retry_after != null && (
|
||||
<div>Retry After: {info.router_settings.retry_after}s</div>
|
||||
)}
|
||||
{info.router_settings.fallbacks && Array.isArray(info.router_settings.fallbacks) && info.router_settings.fallbacks.length > 0 && (
|
||||
<div>Fallbacks: {info.router_settings.fallbacks.length} configured</div>
|
||||
)}
|
||||
{info.router_settings.enable_tag_filtering && (
|
||||
<div>Tag Filtering: Enabled</div>
|
||||
)}
|
||||
{info.router_settings.fallbacks &&
|
||||
Array.isArray(info.router_settings.fallbacks) &&
|
||||
info.router_settings.fallbacks.length > 0 && (
|
||||
<div>Fallbacks: {info.router_settings.fallbacks.length} configured</div>
|
||||
)}
|
||||
{info.router_settings.enable_tag_filtering && <div>Tag Filtering: Enabled</div>}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-400">No router settings configured</div>
|
||||
|
|
@ -1641,13 +1645,12 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
].filter(tab => visibleTabs.includes(tab.key))}
|
||||
].filter((tab) => visibleTabs.includes(tab.key))}
|
||||
/>
|
||||
|
||||
<MemberModal
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue