diff --git a/ui/litellm-dashboard/src/app/(dashboard)/experimental/claude-code-plugins/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/experimental/claude-code-plugins/page.tsx new file mode 100644 index 00000000000..c92c39639c6 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/experimental/claude-code-plugins/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import ClaudeCodePluginsPanel from "@/components/claude_code_plugins"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const ClaudeCodePluginsPage = () => { + const { accessToken, userRole } = useAuthorized(); + + return ( + + ); +}; + +export default ClaudeCodePluginsPage; diff --git a/ui/litellm-dashboard/src/components/AIHub/marketplace/PluginCard.tsx b/ui/litellm-dashboard/src/components/AIHub/marketplace/PluginCard.tsx new file mode 100644 index 00000000000..d8111b2da07 --- /dev/null +++ b/ui/litellm-dashboard/src/components/AIHub/marketplace/PluginCard.tsx @@ -0,0 +1,155 @@ +import React from "react"; +import { Card, Badge, Button, Text } from "@tremor/react"; +import { Tooltip } from "antd"; +import { CopyOutlined, ExternalLinkIcon } from "@heroicons/react/outline"; +import { MarketplacePluginEntry } from "@/components/claude_code_plugins/types"; +import { + formatInstallCommand, + getCategoryBadgeColor, + getSourceLink, + truncateText, +} from "@/components/claude_code_plugins/helpers"; +import NotificationsManager from "@/components/molecules/notifications_manager"; + +interface PluginCardProps { + plugin: MarketplacePluginEntry; +} + +const PluginCard: React.FC = ({ plugin }) => { + const installCommand = formatInstallCommand(plugin); + const sourceLink = getSourceLink(plugin.source); + const categoryBadgeColor = getCategoryBadgeColor(plugin.category); + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + NotificationsManager.success("Install command copied!"); + }; + + // Limit keywords display to first 5 + const displayKeywords = plugin.keywords?.slice(0, 5) || []; + const remainingKeywords = (plugin.keywords?.length || 0) - 5; + + return ( + + {/* Header */} +
+
+
+

+ {plugin.name} +

+ {plugin.version && ( + + v{plugin.version} + + )} + {plugin.category && ( + + {plugin.category} + + )} +
+
+ {sourceLink && ( + + e.stopPropagation()} + > + + + + )} +
+ + {/* Description */} +
+ {plugin.description ? ( + + {plugin.description} + + ) : ( + + No description available + + )} +
+ + {/* Keywords */} + {displayKeywords.length > 0 && ( +
+ {displayKeywords.map((keyword, index) => ( + + {keyword} + + ))} + {remainingKeywords > 0 && ( + + +{remainingKeywords} more + + )} +
+ )} + + {/* Author */} + {plugin.author && ( +
+ + By {plugin.author.name} + {plugin.author.email && ` (${plugin.author.email})`} + +
+ )} + + {/* Homepage Link */} + {plugin.homepage && ( +
+ e.stopPropagation()} + > + Visit homepage + + +
+ )} + + {/* Install Command */} +
+
+
+ Install command + + + {installCommand} + + +
+ +
+
+
+ ); +}; + +export default PluginCard; diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins.tsx b/ui/litellm-dashboard/src/components/claude_code_plugins.tsx new file mode 100644 index 00000000000..b2e66349a23 --- /dev/null +++ b/ui/litellm-dashboard/src/components/claude_code_plugins.tsx @@ -0,0 +1,169 @@ +import React, { useState, useEffect } from "react"; +import { Button } from "@tremor/react"; +import { Modal } from "antd"; +import { + getClaudeCodePluginsList, + deleteClaudeCodePlugin, +} from "./networking"; +import AddPluginForm from "./claude_code_plugins/add_plugin_form"; +import PluginTable from "./claude_code_plugins/plugin_table"; +import { isAdminRole } from "@/utils/roles"; +import PluginInfoView from "./claude_code_plugins/plugin_info"; +import NotificationsManager from "./molecules/notifications_manager"; +import { Plugin, ListPluginsResponse } from "./claude_code_plugins/types"; + +interface ClaudeCodePluginsPanelProps { + accessToken: string | null; + userRole?: string; +} + +const ClaudeCodePluginsPanel: React.FC = ({ + accessToken, + userRole, +}) => { + const [pluginsList, setPluginsList] = useState([]); + const [isAddModalVisible, setIsAddModalVisible] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [pluginToDelete, setPluginToDelete] = useState<{ + name: string; + displayName: string; + } | null>(null); + const [selectedPluginId, setSelectedPluginId] = useState( + null + ); + + const isAdmin = userRole ? isAdminRole(userRole) : false; + + const fetchPlugins = async () => { + if (!accessToken) { + return; + } + + setIsLoading(true); + try { + const response: ListPluginsResponse = await getClaudeCodePluginsList( + accessToken, + false // Get all plugins (enabled and disabled) + ); + console.log(`Claude Code plugins: ${JSON.stringify(response)}`); + setPluginsList(response.plugins); + } catch (error) { + console.error("Error fetching Claude Code plugins:", error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchPlugins(); + }, [accessToken]); + + const handleAddPlugin = () => { + if (selectedPluginId) { + setSelectedPluginId(null); + } + setIsAddModalVisible(true); + }; + + const handleCloseModal = () => { + setIsAddModalVisible(false); + }; + + const handleSuccess = () => { + fetchPlugins(); + }; + + const handleDeleteClick = (pluginName: string, displayName: string) => { + setPluginToDelete({ name: pluginName, displayName }); + }; + + const handleDeleteConfirm = async () => { + if (!pluginToDelete || !accessToken) return; + + setIsDeleting(true); + try { + await deleteClaudeCodePlugin(accessToken, pluginToDelete.name); + NotificationsManager.success( + `Plugin "${pluginToDelete.displayName}" deleted successfully` + ); + fetchPlugins(); + } catch (error) { + console.error("Error deleting plugin:", error); + NotificationsManager.error("Failed to delete plugin"); + } finally { + setIsDeleting(false); + setPluginToDelete(null); + } + }; + + const handleDeleteCancel = () => { + setPluginToDelete(null); + }; + + return ( +
+
+

Claude Code Plugins

+

+ Manage Claude Code marketplace plugins. Add, enable, disable, or + delete plugins that will be available in your marketplace catalog. + Enabled plugins will appear in the public marketplace at{" "} + /claude-code/marketplace.json. +

+
+ +
+
+ + {selectedPluginId ? ( + setSelectedPluginId(null)} + accessToken={accessToken} + isAdmin={isAdmin} + onPluginUpdated={fetchPlugins} + /> + ) : ( + setSelectedPluginId(id)} + /> + )} + + + + {pluginToDelete && ( + +

+ Are you sure you want to delete plugin:{" "} + {pluginToDelete.displayName}? +

+

This action cannot be undone.

+
+ )} +
+ ); +}; + +export default ClaudeCodePluginsPanel;