diff --git a/ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.tsx b/ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.tsx index 4a808ca489d..94be4be6864 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.tsx @@ -1,9 +1,126 @@ -import React from "react"; +import React, { useState } from "react"; import { Form, Input, InputNumber, Select, Tooltip } from "antd"; -import { InfoCircleOutlined } from "@ant-design/icons"; +import { InfoCircleOutlined, PlusOutlined, CloseOutlined } from "@ant-design/icons"; import { Button, TextInput } from "@tremor/react"; import { OAUTH_FLOW } from "./types"; +// ── Quick-add presets for common providers ──────────────────────────────────── +const CLAIM_PRESETS = [ + { label: "Slack enterprise", field: "enterprise_id", placeholder: "e.g. E04XXXXXXX" }, + { label: "Jira cloud", field: "cloud_id", placeholder: "e.g. abc-123" }, + { label: "GitHub Enterprise", field: "enterprise", placeholder: "e.g. my-org" }, +]; + +interface ClaimRow { field: string; value: string } + +interface TokenClaimsBuilderProps { + value?: string; // JSON string from antd Form + onChange?: (v: string) => void; +} + +const TokenClaimsBuilder: React.FC = ({ value, onChange }) => { + const parse = (v?: string): ClaimRow[] => { + if (!v || v.trim() === "") return []; + try { + const obj = JSON.parse(v); + return Object.entries(obj).map(([field, val]) => ({ field, value: String(val) })); + } catch { + return []; + } + }; + + const [rows, setRows] = useState(() => parse(value)); + + const emit = (next: ClaimRow[]) => { + setRows(next); + const nonempty = next.filter(r => r.field.trim() !== ""); + if (nonempty.length === 0) { onChange?.(""); return; } + const obj: Record = {}; + nonempty.forEach(r => { obj[r.field.trim()] = r.value; }); + onChange?.(JSON.stringify(obj)); + }; + + const update = (i: number, key: keyof ClaimRow, val: string) => { + const next = rows.map((r, idx) => idx === i ? { ...r, [key]: val } : r); + emit(next); + }; + + const remove = (i: number) => emit(rows.filter((_, idx) => idx !== i)); + + const addPreset = (field: string) => { + if (rows.some(r => r.field === field)) return; + emit([...rows, { field, value: "" }]); + }; + + const addBlank = () => emit([...rows, { field: "", value: "" }]); + + return ( +
+ {/* Quick-add chips */} +
+ Quick add: + {CLAIM_PRESETS.map(p => ( + + ))} +
+ + {/* Rows */} + {rows.length > 0 && ( +
+
+ Token field + Required value + +
+ {rows.map((row, i) => { + const preset = CLAIM_PRESETS.find(p => p.field === row.field); + return ( +
+ update(i, "field", e.target.value)} + /> + update(i, "value", e.target.value)} + /> + +
+ ); + })} +
+ )} + + +
+ ); +}; + interface OAuthFlowStatus { startOAuthFlow: () => void; status: string; @@ -154,30 +271,13 @@ const OAuthFormFields: React.FC = ({ } name="token_validation_json" - rules={[ - { - validator: (_: any, value: string) => { - if (!value || value.trim() === "") return Promise.resolve(); - try { - JSON.parse(value); - return Promise.resolve(); - } catch { - return Promise.reject(new Error("Must be valid JSON")); - } - }, - }, - ]} > - +