diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx index 06da61a3762..260cac16e02 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx @@ -120,6 +120,8 @@ const routeFor = (slug: string): string => { return "experimental/api-playground"; case "tag-management": return "experimental/tag-management"; + case "claude-code-plugins": + return "experimental/claude-code-plugins"; case "usage": // "Old Usage" return "experimental/old-usage"; @@ -257,6 +259,13 @@ const menuItems: MenuItemCfg[] = [ icon: , roles: all_admin_roles, }, + { + key: "27", + page: "claude-code-plugins", + label: "Claude Code Plugins", + icon: , + roles: all_admin_roles, + }, { key: "4", page: "usage", label: "Old Usage", icon: }, ], }, diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 8a56287e156..8ca25eb9e4c 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -8,6 +8,7 @@ import AdminPanel from "@/components/admins"; import AgentsPanel from "@/components/agents"; import BudgetPanel from "@/components/budgets/budget_panel"; import CacheDashboard from "@/components/cache_dashboard"; +import ClaudeCodePluginsPanel from "@/components/claude_code_plugins"; import { fetchTeams } from "@/components/common_components/fetch_teams"; import LoadingScreen from "@/components/common_components/LoadingScreen"; import { CostTrackingSettings } from "@/components/CostTrackingSettings"; @@ -530,6 +531,8 @@ export default function CreateKeyPage() { ) : page == "tag-management" ? ( + ) : page == "claude-code-plugins" ? ( + ) : page == "vector-stores" ? ( ) : page == "new_usage" ? ( diff --git a/ui/litellm-dashboard/src/components/AIHub/ClaudeCodeMarketplaceTab.tsx b/ui/litellm-dashboard/src/components/AIHub/ClaudeCodeMarketplaceTab.tsx new file mode 100644 index 00000000000..df022f4a749 --- /dev/null +++ b/ui/litellm-dashboard/src/components/AIHub/ClaudeCodeMarketplaceTab.tsx @@ -0,0 +1,162 @@ +import React, { useState, useEffect, useMemo } from "react"; +import { Input } from "antd"; +import { Card, TabGroup, TabList, Tab, TabPanels, TabPanel, Text } from "@tremor/react"; +import { SearchOutlined } from "@ant-design/icons"; +import { getClaudeCodeMarketplace } from "../networking"; +import { ModelDataTable } from "../model_dashboard/table"; +import { getMarketplaceTableColumns } from "./marketplace_table_columns"; +import NotificationsManager from "../molecules/notifications_manager"; +import { + MarketplaceResponse, + MarketplacePluginEntry, +} from "../claude_code_plugins/types"; +import { + extractCategories, + filterPluginsBySearch, + filterPluginsByCategory, +} from "../claude_code_plugins/helpers"; + +interface ClaudeCodeMarketplaceTabProps { + publicPage?: boolean; +} + +const ClaudeCodeMarketplaceTab: React.FC = ({ + publicPage = false, +}) => { + const [marketplaceData, setMarketplaceData] = + useState(null); + const [isLoading, setIsLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [selectedCategoryIndex, setSelectedCategoryIndex] = useState(0); + + useEffect(() => { + fetchMarketplace(); + }, []); + + const fetchMarketplace = async () => { + setIsLoading(true); + try { + const data: MarketplaceResponse = await getClaudeCodeMarketplace(); + console.log("Claude Code marketplace:", data); + setMarketplaceData(data); + } catch (error) { + console.error("Error fetching marketplace:", error); + } finally { + setIsLoading(false); + } + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + NotificationsManager.success("Copied to clipboard!"); + }; + + // Extract unique categories from plugins + const categories = useMemo(() => { + if (!marketplaceData) return ["All"]; + return extractCategories(marketplaceData.plugins); + }, [marketplaceData]); + + // Get selected category name + const selectedCategory = categories[selectedCategoryIndex] || "All"; + + // Filter plugins by search and category + const filteredPlugins = useMemo(() => { + if (!marketplaceData) return []; + + let plugins = marketplaceData.plugins; + + // Apply category filter + plugins = filterPluginsByCategory(plugins, selectedCategory); + + // Apply search filter + plugins = filterPluginsBySearch(plugins, searchTerm); + + return plugins; + }, [marketplaceData, selectedCategory, searchTerm]); + + const columns = useMemo( + () => getMarketplaceTableColumns(copyToClipboard, publicPage), + [publicPage] + ); + + if (!marketplaceData && !isLoading) { + return ( + +
+ + Failed to load marketplace. Please try again later. + +
+
+ ); + } + + return ( +
+ {/* Search Bar */} +
+ } + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + allowClear + size="large" + /> +
+ + {/* Category Tabs */} + + + {categories.map((category) => { + // Count plugins in this category + const categoryPlugins = filterPluginsByCategory( + marketplaceData?.plugins || [], + category + ); + const count = filterPluginsBySearch( + categoryPlugins, + searchTerm + ).length; + + return ( + + {category} {count > 0 && `(${count})`} + + ); + })} + + + + {categories.map((category) => ( + + + {/* Plugin Table */} + + + + {/* Footer Info */} +
+ + Showing {filteredPlugins.length} of{" "} + {marketplaceData?.plugins.length || 0} plugin + {marketplaceData?.plugins.length !== 1 ? "s" : ""} + {searchTerm && ` matching "${searchTerm}"`} + {selectedCategory !== "All" && ` in ${selectedCategory}`} + +
+
+ ))} +
+
+
+ ); +}; + +export default ClaudeCodeMarketplaceTab; diff --git a/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx b/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx index 7c538b618f1..23bfb7d219f 100644 --- a/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx @@ -5,6 +5,7 @@ import MakeModelPublicForm from "@/components/AIHub/forms/MakeModelPublicForm"; import { mcpHubColumns, MCPServerData } from "@/components/mcp_hub_table_columns"; import { modelHubColumns } from "@/components/model_hub_table_columns"; import UsefulLinksManagement from "@/components/AIHub/UsefulLinksManagement"; +import ClaudeCodeMarketplaceTab from "@/components/AIHub/ClaudeCodeMarketplaceTab"; import { ModelDataTable } from "@/components/model_dashboard/table"; import ModelFilters from "@/components/model_filters"; import NotificationsManager from "@/components/molecules/notifications_manager"; @@ -372,12 +373,13 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage, )} - {/* Tab System for Model Hub, Agent Hub, and MCP Hub */} + {/* Tab System for Model Hub, Agent Hub, MCP Hub, and Plugin Marketplace */} Model Hub Agent Hub MCP Hub + Claude Code Plugin Marketplace @@ -462,6 +464,11 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage, + + {/* Plugin Marketplace Tab */} + + + diff --git a/ui/litellm-dashboard/src/components/AIHub/marketplace_table_columns.tsx b/ui/litellm-dashboard/src/components/AIHub/marketplace_table_columns.tsx new file mode 100644 index 00000000000..ed17e84c23e --- /dev/null +++ b/ui/litellm-dashboard/src/components/AIHub/marketplace_table_columns.tsx @@ -0,0 +1,178 @@ +import { ColumnDef } from "@tanstack/react-table"; +import { Button, Badge, Text } from "@tremor/react"; +import { Tooltip } from "antd"; +import { CopyOutlined } from "@ant-design/icons"; +import { MarketplacePluginEntry } from "@/components/claude_code_plugins/types"; +import { + formatInstallCommand, + getCategoryBadgeColor, + getSourceDisplayText, +} from "@/components/claude_code_plugins/helpers"; + +export const getMarketplaceTableColumns = ( + copyToClipboard: (text: string) => void, + publicPage: boolean = false, +): ColumnDef[] => { + const allColumns: ColumnDef[] = [ + { + header: "Plugin Name", + accessorKey: "name", + enableSorting: true, + sortingFn: "alphanumeric", + cell: ({ row }) => { + const plugin = row.original; + const installCommand = formatInstallCommand(plugin); + + return ( +
+
+ {plugin.name} + + copyToClipboard(installCommand)} + className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs" + /> + +
+ {/* Show description on mobile */} +
+ + {plugin.description || "No description"} + +
+
+ ); + }, + }, + { + header: "Description", + accessorKey: "description", + enableSorting: true, + sortingFn: "alphanumeric", + cell: ({ row }) => { + const plugin = row.original; + + return ( + + {plugin.description || "-"} + + ); + }, + meta: { + className: "hidden md:table-cell", + }, + }, + { + header: "Version", + accessorKey: "version", + enableSorting: true, + sortingFn: "alphanumeric", + cell: ({ row }) => { + const plugin = row.original; + + return plugin.version ? ( + + v{plugin.version} + + ) : ( + - + ); + }, + meta: { + className: "hidden lg:table-cell", + }, + }, + { + header: "Category", + accessorKey: "category", + enableSorting: true, + sortingFn: "alphanumeric", + cell: ({ row }) => { + const plugin = row.original; + const badgeColor = getCategoryBadgeColor(plugin.category); + + return plugin.category ? ( + + {plugin.category} + + ) : ( + + Uncategorized + + ); + }, + meta: { + className: "hidden lg:table-cell", + }, + }, + { + header: "Source", + accessorKey: "source", + enableSorting: false, + cell: ({ row }) => { + const plugin = row.original; + const sourceText = getSourceDisplayText(plugin.source); + + return {sourceText}; + }, + meta: { + className: "hidden xl:table-cell", + }, + }, + { + header: "Keywords", + accessorKey: "keywords", + enableSorting: false, + cell: ({ row }) => { + const plugin = row.original; + const keywords = plugin.keywords?.slice(0, 3) || []; + const remaining = (plugin.keywords?.length || 0) - 3; + + return ( +
+ {keywords.map((keyword, index) => ( + + {keyword} + + ))} + {remaining > 0 && ( + + +{remaining} + + )} +
+ ); + }, + meta: { + className: "hidden xl:table-cell", + }, + }, + { + header: "Install Command", + id: "install_command", + enableSorting: false, + cell: ({ row }) => { + const plugin = row.original; + const installCommand = formatInstallCommand(plugin); + + return ( +
+ + {installCommand} + + +
+ ); + }, + }, + ]; + + return allColumns; +}; diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins/types.ts b/ui/litellm-dashboard/src/components/claude_code_plugins/types.ts new file mode 100644 index 00000000000..2c1e6a96546 --- /dev/null +++ b/ui/litellm-dashboard/src/components/claude_code_plugins/types.ts @@ -0,0 +1,112 @@ +/** + * TypeScript types for Claude Code Marketplace + * Matches backend API types from /litellm/types/proxy/claude_code_endpoints.py + */ + +export interface PluginSource { + source: "github" | "url"; + repo?: string; // Format: "org/repo" for GitHub + url?: string; // Full URL for other sources +} + +export interface PluginAuthor { + name: string; + email?: string; +} + +export interface Plugin { + id: string; + name: string; // kebab-case + version?: string; // semantic version + description?: string; + source: PluginSource; + author?: PluginAuthor; + homepage?: string; + keywords?: string[]; + category?: string; + enabled: boolean; + created_at?: string; + updated_at?: string; + created_by?: string; +} + +export interface PluginListItem { + id: string; + name: string; + version?: string; + description?: string; + source: PluginSource; + author?: PluginAuthor; + homepage?: string; + keywords?: string[]; + category?: string; + enabled: boolean; + created_at?: string; + updated_at?: string; +} + +export interface ListPluginsResponse { + plugins: PluginListItem[]; + count: number; +} + +export interface RegisterPluginRequest { + name: string; + source: PluginSource; + version?: string; + description?: string; + author?: PluginAuthor; + homepage?: string; + keywords?: string[]; + category?: string; +} + +export interface RegisterPluginResponse { + plugin: Plugin; + action: "created" | "updated"; + message: string; +} + +// Public marketplace types +export interface MarketplacePluginEntry { + name: string; + source: PluginSource; + version?: string; + description?: string; + author?: PluginAuthor; + homepage?: string; + keywords?: string[]; + category?: string; +} + +export interface MarketplaceOwner { + name: string; + email?: string; +} + +export interface MarketplaceResponse { + name: string; // Marketplace name (e.g., "litellm") + owner: MarketplaceOwner; + plugins: MarketplacePluginEntry[]; +} + +// UI-specific types +export interface CategoryTab { + key: string; + label: string; + count: number; +} + +export interface PluginFormData { + name: string; + sourceType: "github" | "url"; + repo: string; + url: string; + version: string; + description: string; + authorName: string; + authorEmail: string; + homepage: string; + category: string; + keywords: string; // Comma-separated string, will be split into array +} diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 5bd756f1d1d..1aa1df14a10 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -256,6 +256,13 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse icon: , roles: all_admin_roles, }, + { + key: "claude-code-plugins", + page: "claude-code-plugins", + label: "Claude Code Plugins", + icon: , + roles: all_admin_roles, + }, { key: "4", page: "usage",