mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
add plugin lefnat
This commit is contained in:
parent
875c89c09b
commit
bf876e2838
3 changed files with 341 additions and 0 deletions
|
|
@ -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 (
|
||||
<ClaudeCodePluginsPanel
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClaudeCodePluginsPage;
|
||||
|
|
@ -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<PluginCardProps> = ({ 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 (
|
||||
<Card
|
||||
className="hover:shadow-lg transition-shadow duration-200 h-full flex flex-col"
|
||||
decoration="top"
|
||||
decorationColor={categoryBadgeColor}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{plugin.name}
|
||||
</h3>
|
||||
{plugin.version && (
|
||||
<Badge color="blue" size="xs">
|
||||
v{plugin.version}
|
||||
</Badge>
|
||||
)}
|
||||
{plugin.category && (
|
||||
<Badge color={categoryBadgeColor} size="xs">
|
||||
{plugin.category}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{sourceLink && (
|
||||
<Tooltip title="View source repository">
|
||||
<a
|
||||
href={sourceLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-500 hover:text-blue-500"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ExternalLinkIcon className="h-5 w-5" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="mb-4 flex-1">
|
||||
{plugin.description ? (
|
||||
<Text className="text-sm text-gray-600 line-clamp-3">
|
||||
{plugin.description}
|
||||
</Text>
|
||||
) : (
|
||||
<Text className="text-sm text-gray-400 italic">
|
||||
No description available
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Keywords */}
|
||||
{displayKeywords.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-4">
|
||||
{displayKeywords.map((keyword, index) => (
|
||||
<Badge key={index} color="gray" size="xs" className="text-xs">
|
||||
{keyword}
|
||||
</Badge>
|
||||
))}
|
||||
{remainingKeywords > 0 && (
|
||||
<Badge color="gray" size="xs" className="text-xs">
|
||||
+{remainingKeywords} more
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Author */}
|
||||
{plugin.author && (
|
||||
<div className="mb-4">
|
||||
<Text className="text-xs text-gray-500">
|
||||
By {plugin.author.name}
|
||||
{plugin.author.email && ` (${plugin.author.email})`}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Homepage Link */}
|
||||
{plugin.homepage && (
|
||||
<div className="mb-4">
|
||||
<a
|
||||
href={plugin.homepage}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-blue-500 hover:text-blue-700 flex items-center gap-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Visit homepage
|
||||
<ExternalLinkIcon className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Install Command */}
|
||||
<div className="mt-auto pt-4 border-t border-gray-100">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Text className="text-xs text-gray-500 mb-1">Install command</Text>
|
||||
<Tooltip title={installCommand}>
|
||||
<code className="block text-xs bg-gray-50 px-2 py-1 rounded font-mono text-gray-700 truncate">
|
||||
{installCommand}
|
||||
</code>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip title="Copy install command">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
icon={CopyOutlined}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(installCommand);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginCard;
|
||||
169
ui/litellm-dashboard/src/components/claude_code_plugins.tsx
Normal file
169
ui/litellm-dashboard/src/components/claude_code_plugins.tsx
Normal file
|
|
@ -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<ClaudeCodePluginsPanelProps> = ({
|
||||
accessToken,
|
||||
userRole,
|
||||
}) => {
|
||||
const [pluginsList, setPluginsList] = useState<Plugin[]>([]);
|
||||
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<string | null>(
|
||||
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 (
|
||||
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<h1 className="text-2xl font-bold">Claude Code Plugins</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
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{" "}
|
||||
<code className="bg-gray-100 px-1 rounded">/claude-code/marketplace.json</code>.
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<Button onClick={handleAddPlugin} disabled={!accessToken || !isAdmin}>
|
||||
+ Add New Plugin
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedPluginId ? (
|
||||
<PluginInfoView
|
||||
pluginId={selectedPluginId}
|
||||
onClose={() => setSelectedPluginId(null)}
|
||||
accessToken={accessToken}
|
||||
isAdmin={isAdmin}
|
||||
onPluginUpdated={fetchPlugins}
|
||||
/>
|
||||
) : (
|
||||
<PluginTable
|
||||
pluginsList={pluginsList}
|
||||
isLoading={isLoading}
|
||||
onDeleteClick={handleDeleteClick}
|
||||
accessToken={accessToken}
|
||||
onPluginUpdated={fetchPlugins}
|
||||
isAdmin={isAdmin}
|
||||
onPluginClick={(id) => setSelectedPluginId(id)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AddPluginForm
|
||||
visible={isAddModalVisible}
|
||||
onClose={handleCloseModal}
|
||||
accessToken={accessToken}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
|
||||
{pluginToDelete && (
|
||||
<Modal
|
||||
title="Delete Plugin"
|
||||
open={pluginToDelete !== null}
|
||||
onOk={handleDeleteConfirm}
|
||||
onCancel={handleDeleteCancel}
|
||||
confirmLoading={isDeleting}
|
||||
okText="Delete"
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<p>
|
||||
Are you sure you want to delete plugin:{" "}
|
||||
<strong>{pluginToDelete.displayName}</strong>?
|
||||
</p>
|
||||
<p>This action cannot be undone.</p>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClaudeCodePluginsPanel;
|
||||
Loading…
Add table
Reference in a new issue