diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins/add_plugin_form.tsx b/ui/litellm-dashboard/src/components/claude_code_plugins/add_plugin_form.tsx new file mode 100644 index 00000000000..217851f128f --- /dev/null +++ b/ui/litellm-dashboard/src/components/claude_code_plugins/add_plugin_form.tsx @@ -0,0 +1,328 @@ +import React, { useState } from "react"; +import { Modal, Form, Input, Select, message } from "antd"; +import { Button } from "@tremor/react"; +import { registerClaudeCodePlugin } from "../networking"; +import { + validatePluginName, + isValidSemanticVersion, + isValidEmail, + isValidUrl, + parseKeywords, +} from "./helpers"; + +const { TextArea } = Input; +const { Option } = Select; + +interface AddPluginFormProps { + visible: boolean; + onClose: () => void; + accessToken: string | null; + onSuccess: () => void; +} + +const PREDEFINED_CATEGORIES = [ + "Development", + "Productivity", + "Learning", + "Security", + "Data & Analytics", + "Integration", + "Testing", + "Documentation", +]; + +const AddPluginForm: React.FC = ({ + visible, + onClose, + accessToken, + onSuccess, +}) => { + const [form] = Form.useForm(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [sourceType, setSourceType] = useState<"github" | "url">("github"); + + const handleSubmit = async (values: any) => { + if (!accessToken) { + message.error("No access token available"); + return; + } + + // Validate plugin name + if (!validatePluginName(values.name)) { + message.error( + "Plugin name must be kebab-case (lowercase letters, numbers, and hyphens only)" + ); + return; + } + + // Validate semantic version if provided + if (values.version && !isValidSemanticVersion(values.version)) { + message.error( + "Version must be in semantic versioning format (e.g., 1.0.0)" + ); + return; + } + + // Validate email if provided + if (values.authorEmail && !isValidEmail(values.authorEmail)) { + message.error("Invalid email format"); + return; + } + + // Validate homepage URL if provided + if (values.homepage && !isValidUrl(values.homepage)) { + message.error("Invalid homepage URL format"); + return; + } + + setIsSubmitting(true); + try { + // Build plugin data + const pluginData: any = { + name: values.name.trim(), + source: + sourceType === "github" + ? { + source: "github", + repo: values.repo.trim(), + } + : { + source: "url", + url: values.url.trim(), + }, + }; + + // Add optional fields + if (values.version) { + pluginData.version = values.version.trim(); + } + if (values.description) { + pluginData.description = values.description.trim(); + } + if (values.authorName || values.authorEmail) { + pluginData.author = {}; + if (values.authorName) { + pluginData.author.name = values.authorName.trim(); + } + if (values.authorEmail) { + pluginData.author.email = values.authorEmail.trim(); + } + } + if (values.homepage) { + pluginData.homepage = values.homepage.trim(); + } + if (values.category) { + pluginData.category = values.category; + } + if (values.keywords) { + pluginData.keywords = parseKeywords(values.keywords); + } + + await registerClaudeCodePlugin(accessToken, pluginData); + message.success("Plugin registered successfully"); + form.resetFields(); + setSourceType("github"); + onSuccess(); + onClose(); + } catch (error) { + console.error("Error registering plugin:", error); + message.error("Failed to register plugin"); + } finally { + setIsSubmitting(false); + } + }; + + const handleCancel = () => { + form.resetFields(); + setSourceType("github"); + onClose(); + }; + + const handleSourceTypeChange = (value: "github" | "url") => { + setSourceType(value); + // Clear repo/url fields when switching + form.setFieldsValue({ repo: undefined, url: undefined }); + }; + + return ( + +
+ {/* Plugin Name */} + + + + + {/* Source Type */} + + + + + {/* GitHub Repository */} + {sourceType === "github" && ( + + + + )} + + {/* Git URL */} + {sourceType === "url" && ( + + + + )} + + {/* Version */} + + + + + {/* Description */} + +