add plugin lefnat

This commit is contained in:
Ishaan Jaffer 2026-01-19 14:56:52 -08:00
parent 875c89c09b
commit bf876e2838
3 changed files with 341 additions and 0 deletions

View file

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

View file

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

View 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;