diff --git a/ui/litellm-dashboard/src/app/(dashboard)/skills/_components/add_marketplace_form.tsx b/ui/litellm-dashboard/src/app/(dashboard)/skills/_components/add_marketplace_form.tsx index b13908d4b0c..14425a24e3a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/skills/_components/add_marketplace_form.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/skills/_components/add_marketplace_form.tsx @@ -1,7 +1,9 @@ import React, { useState } from "react"; -import { Modal, Form, Input, Button } from "antd"; -import { registerClaudeCodeMarketplace } from "@/components/networking"; +import { Modal, Form, Input, Button, Spin, List, Tag } from "antd"; +import { CheckCircleFilled, CloseCircleFilled, ExclamationCircleFilled } from "@ant-design/icons"; +import { registerClaudeCodeMarketplace, getClaudeCodePluginsList } from "@/components/networking"; import NotificationsManager from "@/components/molecules/notifications_manager"; +import { MarketplaceSource, PluginListItem } from "@/components/claude_code_plugins/types"; interface AddMarketplaceFormProps { visible: boolean; @@ -15,13 +17,30 @@ interface AddMarketplaceFormValues { name?: string; } +type Step = "form" | "importing" | "result"; + const AddMarketplaceForm: React.FC = ({ visible, onClose, accessToken, onSuccess }) => { const [form] = Form.useForm(); - const [isSubmitting, setIsSubmitting] = useState(false); + const [step, setStep] = useState("form"); + const [marketplace, setMarketplace] = useState(null); + const [loadedSkills, setLoadedSkills] = useState([]); + const [errorMessage, setErrorMessage] = useState(null); + + const resetAndClose = () => { + form.resetFields(); + setStep("form"); + setMarketplace(null); + setLoadedSkills([]); + setErrorMessage(null); + onClose(); + }; const handleCancel = () => { - form.resetFields(); - onClose(); + // Once import has started, the marketplace already exists server-side + // regardless of whether this dialog stays open - closing early shouldn't + // look like nothing happened, so still refresh the lists behind it. + if (step !== "form") onSuccess(); + resetAndClose(); }; const handleSubmit = async (values: AddMarketplaceFormValues) => { @@ -30,56 +49,137 @@ const AddMarketplaceForm: React.FC = ({ visible, onClos return; } - setIsSubmitting(true); + setStep("importing"); + setErrorMessage(null); try { - await registerClaudeCodeMarketplace(accessToken, { + const response = await registerClaudeCodeMarketplace(accessToken, { source: values.source.trim(), ...(values.name?.trim() ? { name: values.name.trim() } : {}), }); - NotificationsManager.success("Marketplace imported successfully"); - form.resetFields(); + setMarketplace(response.marketplace); + + const skillsResponse = await getClaudeCodePluginsList(accessToken, false); + const prefix = `${response.marketplace.name}--`; + setLoadedSkills(skillsResponse.plugins.filter((p: PluginListItem) => p.name.startsWith(prefix))); + onSuccess(); - onClose(); + setStep("result"); } catch (error) { console.error("Error registering marketplace:", error); - const reason = error instanceof Error && error.message ? error.message : "Failed to import marketplace"; - NotificationsManager.error(`Failed to import marketplace: ${reason}`); - } finally { - setIsSubmitting(false); + setErrorMessage(error instanceof Error && error.message ? error.message : "Failed to import marketplace"); + setStep("result"); } }; - return ( - -
- - - + const renderForm = () => ( + + + + - - - + + + - -
- - + +
+
+
+ ); + + const renderImporting = () => ( +
+ +

Importing marketplace and loading its skills…

+

This can take a few seconds for repositories with many skills.

+
+ ); + + const renderResult = () => { + if (errorMessage) { + return ( +
+
+ +
+

Import failed

+

{errorMessage}

+
+
+
+ +
- - +
+ ); + } + + const skippedCount = marketplace?.skipped_count ?? 0; + + return ( +
+
+ +
+

+ {loadedSkills.length} skill{loadedSkills.length === 1 ? "" : "s"} loaded from "{marketplace?.name} + " +

+ {skippedCount > 0 && ( +

+ + {skippedCount} skill{skippedCount === 1 ? "" : "s"} could not be fetched after retries. Use + "Sync now" on the marketplace to try again. +

+ )} +
+
+ +
+ ( + +
+ {skill.name} + {skill.enabled ? "public" : "private"} +
+
+ )} + /> +
+ +
+ +
+
+ ); + }; + + return ( + + {step === "form" && renderForm()} + {step === "importing" && renderImporting()} + {step === "result" && renderResult()} ); }; diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins/types.ts b/ui/litellm-dashboard/src/components/claude_code_plugins/types.ts index 1fc10206710..c71ffa33941 100644 --- a/ui/litellm-dashboard/src/components/claude_code_plugins/types.ts +++ b/ui/litellm-dashboard/src/components/claude_code_plugins/types.ts @@ -102,6 +102,7 @@ export interface MarketplaceSource { sync_error?: string; last_synced_at?: string; plugin_count: number; + skipped_count: number; created_at: string; updated_at: string; }