diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index 08f0156a619..3345bf83a9f 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -66,6 +66,7 @@ "src/components/playground/chat_ui/A2AMetrics.tsx", "src/components/common_components/PassThroughGuardrailsSection.tsx", "src/components/claude_code_plugins/MakeSkillPublicForm.tsx", + "src/components/claude_code_plugins/add_plugin_form.tsx", "src/components/mcp_tools/MCPPermissionManagement.tsx", "src/components/model_add/credentials.tsx", "src/components/model_dashboard/health_check_columns.tsx", diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins/MakeSkillPublicForm.tsx b/ui/litellm-dashboard/src/components/claude_code_plugins/MakeSkillPublicForm.tsx index ffacaffe1b1..5afcdd65093 100644 --- a/ui/litellm-dashboard/src/components/claude_code_plugins/MakeSkillPublicForm.tsx +++ b/ui/litellm-dashboard/src/components/claude_code_plugins/MakeSkillPublicForm.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect } from "react"; -import { Form, Steps } from "antd"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -9,6 +8,8 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { cn } from "@/lib/utils"; +import { Check } from "lucide-react"; import { enableClaudeCodePlugin, disableClaudeCodePlugin, @@ -16,8 +17,6 @@ import { import NotificationsManager from "../molecules/notifications_manager"; import { Plugin } from "./types"; -const { Step } = Steps; - interface MakeSkillPublicFormProps { visible: boolean; onClose: () => void; @@ -26,6 +25,49 @@ interface MakeSkillPublicFormProps { onSuccess: () => void; } +function Stepper({ current, steps }: { current: number; steps: string[] }) { + return ( +
    + {steps.map((label, i) => { + const active = i === current; + const completed = i < current; + return ( +
  1. +
    + {completed ? : i + 1} +
    + + {label} + + {i < steps.length - 1 && ( +
    + )} +
  2. + ); + })} +
+ ); +} + const MakeSkillPublicForm: React.FC = ({ visible, onClose, @@ -36,12 +78,10 @@ const MakeSkillPublicForm: React.FC = ({ const [currentStep, setCurrentStep] = useState(0); const [selectedSkills, setSelectedSkills] = useState>(new Set()); const [loading, setLoading] = useState(false); - const [form] = Form.useForm(); const handleClose = () => { setCurrentStep(0); setSelectedSkills(new Set()); - form.resetFields(); onClose(); }; @@ -251,40 +291,35 @@ const MakeSkillPublicForm: React.FC = ({ Publish to Skill Hub -
- - - - + - {currentStep === 0 ? renderStep1() : renderStep2()} + {currentStep === 0 ? renderStep1() : renderStep2()} -
- -
- {currentStep === 0 && ( - - )} - {currentStep === 1 && ( - - )} -
+
+ +
+ {currentStep === 0 && ( + + )} + {currentStep === 1 && ( + + )}
- +
); 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 index 7fdf7fbf299..f4612548082 100644 --- 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 @@ -1,8 +1,23 @@ import React, { useState } from "react"; -import { Modal, Form, Input, Select } from "antd"; +import { useForm, Controller } from "react-hook-form"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; import MessageManager from "@/components/molecules/message_manager"; -// eslint-disable-next-line litellm-ui/no-banned-ui-imports -import { Button } from "@tremor/react"; import { registerClaudeCodePlugin } from "../networking"; import { validatePluginName, @@ -12,9 +27,6 @@ import { parseKeywords, } from "./helpers"; -const { TextArea } = Input; -const { Option } = Select; - interface AddPluginFormProps { visible: boolean; onClose: () => void; @@ -22,6 +34,20 @@ interface AddPluginFormProps { onSuccess: () => void; } +interface FormValues { + skillUrl: string; + name: string; + domain?: string; + namespace?: string; + description?: string; + category?: string; + keywords?: string; + version?: string; + authorName?: string; + authorEmail?: string; + homepage?: string; +} + const PREDEFINED_CATEGORIES = [ "Development", "Productivity", @@ -113,7 +139,21 @@ const AddPluginForm: React.FC = ({ accessToken, onSuccess, }) => { - const [form] = Form.useForm(); + const form = useForm({ + defaultValues: { + skillUrl: "", + name: "", + domain: "", + namespace: "", + description: "", + category: "", + keywords: "", + version: "", + authorName: "", + authorEmail: "", + }, + }); + const { register, handleSubmit, setValue, getValues, reset, control, formState } = form; const [isSubmitting, setIsSubmitting] = useState(false); const [urlPreview, setUrlPreview] = useState(null); @@ -122,15 +162,14 @@ const AddPluginForm: React.FC = ({ const preview = parseGitHubUrl(val); setUrlPreview(preview); if (preview) { - // Auto-fill name only if it's currently empty - const currentName = form.getFieldValue("name"); + const currentName = getValues("name"); if (!currentName) { - form.setFieldsValue({ name: preview.suggestedName }); + setValue("name", preview.suggestedName, { shouldValidate: false }); } } }; - const handleSubmit = async (values: any) => { + const onSubmit = async (values: FormValues) => { if (!accessToken) { MessageManager.error("No access token available"); return; @@ -185,7 +224,7 @@ const AddPluginForm: React.FC = ({ await registerClaudeCodePlugin(accessToken, pluginData); MessageManager.success("Skill registered successfully"); - form.resetFields(); + reset(); setUrlPreview(null); onSuccess(); onClose(); @@ -198,168 +237,247 @@ const AddPluginForm: React.FC = ({ }; const handleCancel = () => { - form.resetFields(); + reset(); setUrlPreview(null); onClose(); }; + const urlRegister = register("skillUrl", { + required: "Please enter a GitHub URL", + }); + return ( - (!o ? handleCancel() : undefined)} > -
- {/* Smart URL Input */} - + + Add New Skill + + - - - - {/* Parsed preview */} - {urlPreview && ( -
- Detected: {urlPreview.label} + {/* Smart URL Input */} +
+ + { + urlRegister.onChange(e); + handleUrlChange(e); + }} + /> +

+ Paste a GitHub URL — repo, folder, or file link. E.g. + github.com/org/repo or github.com/org/repo/tree/main/my-skill +

+ {formState.errors.skillUrl && ( +

+ {formState.errors.skillUrl.message as string} +

+ )}
- )} - {/* Skill Name */} - - - + {/* Parsed preview */} + {urlPreview && ( +
+ Detected: {urlPreview.label} +
+ )} - {/* Domain and Namespace — side by side */} -
- - - - - - -
+ {/* Skill Name */} +
+ + +

+ Unique identifier in kebab-case format (e.g., my-skill) +

+ {formState.errors.name && ( +

+ {formState.errors.name.message as string} +

+ )} +
- {/* Description */} - -