add plugin on marketplace / ai hub

This commit is contained in:
Ishaan Jaffer 2026-01-19 14:57:09 -08:00
parent 357287e6e3
commit d9df5e46eb
7 changed files with 479 additions and 1 deletions

View file

@ -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: <TagsOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
{
key: "27",
page: "claude-code-plugins",
label: "Claude Code Plugins",
icon: <ToolOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
{ key: "4", page: "usage", label: "Old Usage", icon: <BarChartOutlined style={{ fontSize: 18 }} /> },
],
},

View file

@ -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() {
<SearchTools accessToken={accessToken} userRole={userRole} userID={userID} />
) : page == "tag-management" ? (
<TagManagement accessToken={accessToken} userRole={userRole} userID={userID} />
) : page == "claude-code-plugins" ? (
<ClaudeCodePluginsPanel accessToken={accessToken} userRole={userRole} />
) : page == "vector-stores" ? (
<VectorStoreManagement accessToken={accessToken} userRole={userRole} userID={userID} />
) : page == "new_usage" ? (

View file

@ -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<ClaudeCodeMarketplaceTabProps> = ({
publicPage = false,
}) => {
const [marketplaceData, setMarketplaceData] =
useState<MarketplaceResponse | null>(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 (
<Card>
<div className="text-center p-12">
<Text className="text-gray-500">
Failed to load marketplace. Please try again later.
</Text>
</div>
</Card>
);
}
return (
<div className="space-y-4">
{/* Search Bar */}
<div className="max-w-md">
<Input
placeholder="Search plugins by name, description, or keywords..."
prefix={<SearchOutlined className="text-gray-400" />}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
allowClear
size="large"
/>
</div>
{/* Category Tabs */}
<TabGroup index={selectedCategoryIndex} onIndexChange={setSelectedCategoryIndex}>
<TabList className="mb-4">
{categories.map((category) => {
// Count plugins in this category
const categoryPlugins = filterPluginsByCategory(
marketplaceData?.plugins || [],
category
);
const count = filterPluginsBySearch(
categoryPlugins,
searchTerm
).length;
return (
<Tab key={category}>
{category} {count > 0 && `(${count})`}
</Tab>
);
})}
</TabList>
<TabPanels>
{categories.map((category) => (
<TabPanel key={category}>
<Card>
{/* Plugin Table */}
<ModelDataTable
columns={columns}
data={filteredPlugins}
isLoading={isLoading}
defaultSorting={[{ id: "name", desc: false }]}
/>
</Card>
{/* Footer Info */}
<div className="mt-4 text-center space-y-2">
<Text className="text-sm text-gray-600">
Showing {filteredPlugins.length} of{" "}
{marketplaceData?.plugins.length || 0} plugin
{marketplaceData?.plugins.length !== 1 ? "s" : ""}
{searchTerm && ` matching "${searchTerm}"`}
{selectedCategory !== "All" && ` in ${selectedCategory}`}
</Text>
</div>
</TabPanel>
))}
</TabPanels>
</TabGroup>
</div>
);
};
export default ClaudeCodeMarketplaceTab;

View file

@ -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<ModelHubTableProps> = ({ accessToken, publicPage,
</div>
)}
{/* Tab System for Model Hub, Agent Hub, and MCP Hub */}
{/* Tab System for Model Hub, Agent Hub, MCP Hub, and Plugin Marketplace */}
<TabGroup>
<TabList className="mb-4">
<Tab>Model Hub</Tab>
<Tab>Agent Hub</Tab>
<Tab>MCP Hub</Tab>
<Tab>Claude Code Plugin Marketplace</Tab>
</TabList>
<TabPanels>
@ -462,6 +464,11 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
</Text>
</div>
</TabPanel>
{/* Plugin Marketplace Tab */}
<TabPanel>
<ClaudeCodeMarketplaceTab publicPage={publicPage} />
</TabPanel>
</TabPanels>
</TabGroup>
</div>

View file

@ -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<MarketplacePluginEntry>[] => {
const allColumns: ColumnDef<MarketplacePluginEntry>[] = [
{
header: "Plugin Name",
accessorKey: "name",
enableSorting: true,
sortingFn: "alphanumeric",
cell: ({ row }) => {
const plugin = row.original;
const installCommand = formatInstallCommand(plugin);
return (
<div className="space-y-1">
<div className="flex items-center space-x-2">
<Text className="font-medium text-sm">{plugin.name}</Text>
<Tooltip title="Copy install command">
<CopyOutlined
onClick={() => copyToClipboard(installCommand)}
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
/>
</Tooltip>
</div>
{/* Show description on mobile */}
<div className="md:hidden">
<Text className="text-xs text-gray-600">
{plugin.description || "No description"}
</Text>
</div>
</div>
);
},
},
{
header: "Description",
accessorKey: "description",
enableSorting: true,
sortingFn: "alphanumeric",
cell: ({ row }) => {
const plugin = row.original;
return (
<Text className="text-xs line-clamp-2">
{plugin.description || "-"}
</Text>
);
},
meta: {
className: "hidden md:table-cell",
},
},
{
header: "Version",
accessorKey: "version",
enableSorting: true,
sortingFn: "alphanumeric",
cell: ({ row }) => {
const plugin = row.original;
return plugin.version ? (
<Badge color="blue" size="sm">
v{plugin.version}
</Badge>
) : (
<Text className="text-xs text-gray-400">-</Text>
);
},
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 ? (
<Badge color={badgeColor} size="sm">
{plugin.category}
</Badge>
) : (
<Badge color="gray" size="sm">
Uncategorized
</Badge>
);
},
meta: {
className: "hidden lg:table-cell",
},
},
{
header: "Source",
accessorKey: "source",
enableSorting: false,
cell: ({ row }) => {
const plugin = row.original;
const sourceText = getSourceDisplayText(plugin.source);
return <Text className="text-xs text-gray-600">{sourceText}</Text>;
},
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 (
<div className="flex flex-wrap gap-1">
{keywords.map((keyword, index) => (
<Badge key={index} color="gray" size="xs">
{keyword}
</Badge>
))}
{remaining > 0 && (
<Badge color="gray" size="xs">
+{remaining}
</Badge>
)}
</div>
);
},
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 (
<div className="flex items-center space-x-2">
<code className="text-xs bg-gray-100 px-2 py-1 rounded font-mono truncate max-w-[200px]">
{installCommand}
</code>
<Tooltip title="Copy command">
<Button
size="xs"
variant="secondary"
icon={CopyOutlined}
onClick={() => copyToClipboard(installCommand)}
/>
</Tooltip>
</div>
);
},
},
];
return allColumns;
};

View file

@ -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
}

View file

@ -256,6 +256,13 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
icon: <TagsOutlined />,
roles: all_admin_roles,
},
{
key: "claude-code-plugins",
page: "claude-code-plugins",
label: "Claude Code Plugins",
icon: <ToolOutlined />,
roles: all_admin_roles,
},
{
key: "4",
page: "usage",