mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
refactor(mcp): move submission rules into Team MCPs tab, grouped free-form UI
Folds the Standards tab into Team MCPs. Submission Rules panel now lives at the top of the Team MCPs tab — collapsible, shows active rules as chips when closed, expands to a grouped checkbox editor (Documentation / Source / Connection / Security). Removes the separate Standards tab from the nav. MCPStandardsSettings.tsx is now constants-only (FIELD_GROUPS, MCP_REQUIRED_FIELD_DEFS, SETTINGS_KEY) — the UI lives in MCPSubmissionsTab.
This commit is contained in:
parent
9acb0636fc
commit
ab657fc242
3 changed files with 200 additions and 148 deletions
|
|
@ -1,14 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import { getGeneralSettingsCall, updateConfigFieldSetting } from "../networking";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import { MCPServer } from "./types";
|
||||
|
||||
interface MCPStandardsSettingsProps {
|
||||
accessToken: string | null;
|
||||
}
|
||||
|
||||
export interface RequiredFieldDef {
|
||||
key: string;
|
||||
label: string;
|
||||
|
|
@ -16,146 +9,64 @@ export interface RequiredFieldDef {
|
|||
check: (server: MCPServer) => boolean;
|
||||
}
|
||||
|
||||
export const MCP_REQUIRED_FIELD_DEFS: RequiredFieldDef[] = [
|
||||
export interface FieldGroup {
|
||||
label: string;
|
||||
fields: RequiredFieldDef[];
|
||||
}
|
||||
|
||||
export const FIELD_GROUPS: FieldGroup[] = [
|
||||
{
|
||||
key: "description",
|
||||
label: "Description",
|
||||
description: "Server must have a non-empty description.",
|
||||
check: (s) => !!s.description?.trim(),
|
||||
label: "Documentation",
|
||||
fields: [
|
||||
{
|
||||
key: "description",
|
||||
label: "Description",
|
||||
description: "Must have a non-empty description",
|
||||
check: (s) => !!s.description?.trim(),
|
||||
},
|
||||
{
|
||||
key: "alias",
|
||||
label: "Alias",
|
||||
description: "Must have a display alias",
|
||||
check: (s) => !!s.alias?.trim(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "source_url",
|
||||
label: "GitHub / Source URL",
|
||||
description: "Server must have a link to the source repository.",
|
||||
check: (s) => !!s.source_url?.trim(),
|
||||
label: "Source",
|
||||
fields: [
|
||||
{
|
||||
key: "source_url",
|
||||
label: "GitHub / Source URL",
|
||||
description: "Must link to a source repository",
|
||||
check: (s) => !!s.source_url?.trim(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "alias",
|
||||
label: "Alias",
|
||||
description: "Server must have a human-readable alias.",
|
||||
check: (s) => !!s.alias?.trim(),
|
||||
label: "Connection",
|
||||
fields: [
|
||||
{
|
||||
key: "url",
|
||||
label: "Server URL",
|
||||
description: "Must have a URL configured",
|
||||
check: (s) => !!s.url?.trim(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "auth_type",
|
||||
label: "Auth configured",
|
||||
description: "Server must have an auth type set (not 'none').",
|
||||
check: (s) => !!s.auth_type && s.auth_type !== "none",
|
||||
},
|
||||
{
|
||||
key: "url",
|
||||
label: "Server URL",
|
||||
description: "Server must have a URL configured.",
|
||||
check: (s) => !!s.url?.trim(),
|
||||
label: "Security",
|
||||
fields: [
|
||||
{
|
||||
key: "auth_type",
|
||||
label: "Auth configured",
|
||||
description: "Must use authentication (not 'none')",
|
||||
check: (s) => !!s.auth_type && s.auth_type !== "none",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const SETTINGS_KEY = "mcp_required_fields";
|
||||
export const MCP_REQUIRED_FIELD_DEFS: RequiredFieldDef[] = FIELD_GROUPS.flatMap((g) => g.fields);
|
||||
|
||||
export default function MCPStandardsSettings({ accessToken }: MCPStandardsSettingsProps) {
|
||||
const [requiredFields, setRequiredFields] = useState<string[]>([]);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const loadSettings = useCallback(async () => {
|
||||
if (!accessToken) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const settings = await getGeneralSettingsCall(accessToken);
|
||||
const rows: Array<{ field_name: string; field_value: unknown }> = Array.isArray(settings?.data)
|
||||
? settings.data
|
||||
: [];
|
||||
const row = rows.find((r) => r.field_name === SETTINGS_KEY);
|
||||
if (row && Array.isArray(row.field_value)) {
|
||||
setRequiredFields(row.field_value as string[]);
|
||||
}
|
||||
} catch {
|
||||
// leave defaults
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [accessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
}, [loadSettings]);
|
||||
|
||||
const toggleField = (key: string) => {
|
||||
setRequiredFields((prev) =>
|
||||
prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key],
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!accessToken) return;
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await updateConfigFieldSetting(accessToken, SETTINGS_KEY, requiredFields);
|
||||
NotificationsManager.success("Standards saved");
|
||||
} catch {
|
||||
NotificationsManager.fromBackend("Failed to save standards");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-2xl">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-base font-semibold text-gray-900">MCP Submission Standards</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Choose which fields are required for a submission to pass your standards. Each submission
|
||||
card in the Team MCPs tab will show a green ✓ or red ✗ for each requirement.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-sm text-gray-400">Loading…</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{MCP_REQUIRED_FIELD_DEFS.map((field) => {
|
||||
const enabled = requiredFields.includes(field.key);
|
||||
return (
|
||||
<div
|
||||
key={field.key}
|
||||
className={`flex items-center justify-between px-4 py-3 rounded-lg border transition-colors ${
|
||||
enabled ? "border-blue-200 bg-blue-50" : "border-gray-200 bg-white"
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0 mr-4">
|
||||
<div className="text-sm font-medium text-gray-900">{field.label}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">{field.description}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
onClick={() => toggleField(field.key)}
|
||||
className={`relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${
|
||||
enabled ? "bg-blue-500" : "bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||
enabled ? "translate-x-4" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSaving || isLoading}
|
||||
onClick={handleSave}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 rounded-md transition-colors"
|
||||
>
|
||||
{isSaving ? "Saving…" : "Save Standards"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const SETTINGS_KEY = "mcp_required_fields";
|
||||
|
|
|
|||
|
|
@ -7,15 +7,19 @@ import {
|
|||
XIcon,
|
||||
AlertCircleIcon,
|
||||
ServerIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
SettingsIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
fetchMCPSubmissions,
|
||||
approveMCPServer,
|
||||
rejectMCPServer,
|
||||
getGeneralSettingsCall,
|
||||
updateConfigFieldSetting,
|
||||
} from "@/components/networking";
|
||||
import { MCPServer, MCPSubmissionsSummary } from "./types";
|
||||
import { MCP_REQUIRED_FIELD_DEFS } from "./MCPStandardsSettings";
|
||||
import { FIELD_GROUPS, MCP_REQUIRED_FIELD_DEFS, SETTINGS_KEY } from "./MCPStandardsSettings";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
|
||||
type MCPStatus = "active" | "pending_review" | "rejected";
|
||||
|
|
@ -137,6 +141,128 @@ function ConfirmDialog({ action, serverName, onConfirm, onCancel }: ConfirmDialo
|
|||
);
|
||||
}
|
||||
|
||||
type SubmissionRulesPanelProps = {
|
||||
requiredFields: string[];
|
||||
onChange: (fields: string[]) => void;
|
||||
onSave: () => Promise<void>;
|
||||
isSaving: boolean;
|
||||
};
|
||||
|
||||
function SubmissionRulesPanel({ requiredFields, onChange, onSave, isSaving }: SubmissionRulesPanelProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const activeLabels = MCP_REQUIRED_FIELD_DEFS.filter((f) => requiredFields.includes(f.key));
|
||||
|
||||
const toggle = (key: string) => {
|
||||
onChange(requiredFields.includes(key) ? requiredFields.filter((k) => k !== key) : [...requiredFields, key]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-5 border border-gray-200 rounded-lg bg-white overflow-hidden">
|
||||
{/* Header — always visible */}
|
||||
<div
|
||||
className="flex items-center justify-between px-4 py-3 cursor-pointer select-none"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<SettingsIcon className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-sm font-semibold text-gray-800">Submission Rules</span>
|
||||
{activeLabels.length > 0 ? (
|
||||
<span className="text-xs text-gray-500">
|
||||
({activeLabels.length} required field{activeLabels.length !== 1 ? "s" : ""})
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400 italic">no rules set</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Active rule chips — collapsed view */}
|
||||
{!expanded && activeLabels.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 max-w-md">
|
||||
{activeLabels.map((f) => (
|
||||
<span
|
||||
key={f.key}
|
||||
className="inline-flex items-center gap-1 text-xs bg-blue-50 text-blue-700 border border-blue-200 px-2 py-0.5 rounded-full"
|
||||
>
|
||||
<CheckIcon className="h-3 w-3" />
|
||||
{f.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{expanded ? (
|
||||
<ChevronUpIcon className="h-4 w-4 text-gray-400" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded editor */}
|
||||
{expanded && (
|
||||
<div className="border-t border-gray-100 px-4 pt-4 pb-4">
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
Select which fields must be filled in before a submission is considered compliant.
|
||||
LiteLLM will show ✓ / ✗ for each rule on every submission card below.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-5">
|
||||
{FIELD_GROUPS.map((group) => (
|
||||
<div key={group.label}>
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
|
||||
{group.label}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{group.fields.map((field) => {
|
||||
const active = requiredFields.includes(field.key);
|
||||
return (
|
||||
<label
|
||||
key={field.key}
|
||||
className="flex items-start gap-2.5 cursor-pointer group"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={active}
|
||||
onChange={() => toggle(field.key)}
|
||||
className="mt-0.5 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-800 group-hover:text-blue-700 transition-colors">
|
||||
{field.label}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">{field.description}</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-5 flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSaving}
|
||||
onClick={async () => {
|
||||
await onSave();
|
||||
setExpanded(false);
|
||||
}}
|
||||
className="px-4 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 rounded-md transition-colors"
|
||||
>
|
||||
{isSaving ? "Saving…" : "Save Rules"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(false)}
|
||||
className="px-4 py-1.5 text-sm font-medium text-gray-600 hover:text-gray-900 border border-gray-200 rounded-md hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type MCPServerCardProps = {
|
||||
server: MCPServer;
|
||||
onApprove: () => void;
|
||||
|
|
@ -273,6 +399,7 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) {
|
|||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [requiredFields, setRequiredFields] = useState<string[]>([]);
|
||||
const [isSavingRules, setIsSavingRules] = useState(false);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!accessToken) {
|
||||
|
|
@ -289,7 +416,7 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) {
|
|||
setSummary(res);
|
||||
if (settings?.data && Array.isArray(settings.data)) {
|
||||
const row = settings.data.find(
|
||||
(r: { field_name: string; field_value: unknown }) => r.field_name === "mcp_required_fields",
|
||||
(r: { field_name: string; field_value: unknown }) => r.field_name === SETTINGS_KEY,
|
||||
);
|
||||
if (row && Array.isArray(row.field_value)) {
|
||||
setRequiredFields(row.field_value as string[]);
|
||||
|
|
@ -306,6 +433,19 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) {
|
|||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const handleSaveRules = async () => {
|
||||
if (!accessToken) return;
|
||||
setIsSavingRules(true);
|
||||
try {
|
||||
await updateConfigFieldSetting(accessToken, SETTINGS_KEY, requiredFields);
|
||||
NotificationsManager.success("Submission rules saved");
|
||||
} catch {
|
||||
NotificationsManager.fromBackend("Failed to save submission rules");
|
||||
} finally {
|
||||
setIsSavingRules(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filtered = summary.items.filter((s) => {
|
||||
if (statusFilter !== "all" && s.approval_status !== statusFilter) return false;
|
||||
if (search.trim()) {
|
||||
|
|
@ -343,6 +483,14 @@ export function MCPSubmissionsTab({ accessToken }: MCPSubmissionsTabProps) {
|
|||
|
||||
return (
|
||||
<div className="p-6">
|
||||
{/* Submission Rules panel */}
|
||||
<SubmissionRulesPanel
|
||||
requiredFields={requiredFields}
|
||||
onChange={setRequiredFields}
|
||||
onSave={handleSaveRules}
|
||||
isSaving={isSavingRules}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||
<StatCard label="Total Submitted" value={summary.total} color="text-gray-900" />
|
||||
<StatCard label="Pending Review" value={summary.pending_review} color="text-yellow-600" />
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import MCPSemanticFilterSettings from "../Settings/AdminSettings/MCPSemanticFilt
|
|||
import MCPNetworkSettings from "./MCPNetworkSettings";
|
||||
import MCPDiscovery from "./mcp_discovery";
|
||||
import { ByokCredentialModal } from "./ByokCredentialModal";
|
||||
import MCPStandardsSettings from "./MCPStandardsSettings";
|
||||
|
||||
const { Text: AntdText, Title: AntdTitle } = Typography;
|
||||
const EDIT_OAUTH_UI_STATE_KEY = "litellm-mcp-oauth-edit-state";
|
||||
|
|
@ -345,7 +344,6 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
|||
<Tab>Semantic Filter</Tab>
|
||||
<Tab>Network Settings</Tab>
|
||||
{isAdminRole(userRole) && <Tab><span className="flex items-center gap-2">Team MCPs <NewBadge /></span></Tab>}
|
||||
{isAdminRole(userRole) && <Tab>Standards</Tab>}
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
|
|
@ -434,11 +432,6 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
|||
<MCPSubmissionsTab accessToken={accessToken} />
|
||||
</TabPanel>
|
||||
)}
|
||||
{isAdminRole(userRole) && (
|
||||
<TabPanel>
|
||||
<MCPStandardsSettings accessToken={accessToken} />
|
||||
</TabPanel>
|
||||
)}
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue