import ConfirmDeletePluginDialog from "@/app/workspace/plugins/dialogs/confirmDeletePluginDialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { CodeEditor } from "@/components/ui/codeEditor"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { setPluginFormDirtyState, useAppDispatch, useAppSelector, useUpdatePluginMutation } from "@/lib/store"; import { PluginType } from "@/lib/types/plugins"; import { cn } from "@/lib/utils"; import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib"; import { zodResolver } from "@hookform/resolvers/zod"; import { PlusIcon, SaveIcon, Trash2Icon } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import * as z from "zod"; interface Props { onDelete: () => void; onCreate: (pluginName: string) => void; } const pluginFormSchema = z.object({ name: z.string().min(1, "Name is required"), enabled: z.boolean(), path: z.string().optional(), config: z.string().optional(), hasConfig: z.boolean(), }); type PluginFormValues = z.infer; const getPluginTypeColor = (type: PluginType) => { switch (type) { case "llm": return "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"; case "mcp": return "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300"; case "http": return "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300"; default: return "bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300"; } }; export default function PluginsView(props: Props) { const dispatch = useAppDispatch(); const hasUpdatePluginAccess = useRbac(RbacResource.Plugins, RbacOperation.Update); const hasDeletePluginAccess = useRbac(RbacResource.Plugins, RbacOperation.Delete); const [updatePlugin, { isLoading }] = useUpdatePluginMutation(); const selectedPlugin = useAppSelector((state) => state.plugin.selectedPlugin); const [showConfig, setShowConfig] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const form = useForm({ resolver: zodResolver(pluginFormSchema), defaultValues: { name: selectedPlugin?.name || "", enabled: selectedPlugin?.enabled || false, path: selectedPlugin?.path || undefined, config: selectedPlugin?.config ? JSON.stringify(selectedPlugin.config, null, 2) : undefined, hasConfig: Boolean(selectedPlugin?.config && Object.keys(selectedPlugin.config).length > 0), }, }); // Update form when selectedPlugin changes useEffect(() => { if (selectedPlugin) { const hasConfig = Boolean(selectedPlugin.config && Object.keys(selectedPlugin.config).length > 0); setShowConfig(hasConfig); form.reset({ name: selectedPlugin.name, enabled: selectedPlugin.enabled, path: selectedPlugin.path, config: hasConfig ? JSON.stringify(selectedPlugin.config, null, 2) : undefined, hasConfig, }); } }, [selectedPlugin]); // Track form dirty state useEffect(() => { const isDirty = form.formState.isDirty; dispatch(setPluginFormDirtyState(isDirty)); }, [form.formState.isDirty, dispatch]); const onSubmit = async (values: PluginFormValues) => { if (!selectedPlugin) return; try { let config; if (values.hasConfig && values.config) { try { config = JSON.parse(values.config); } catch { toast.error("Invalid JSON in configuration"); return; } } await updatePlugin({ name: selectedPlugin.name, data: { enabled: values.enabled, path: values.path ?? undefined, ...(config !== undefined && { config }), }, }).unwrap(); toast.success("Plugin updated successfully"); form.reset(values); } catch { toast.error("Failed to update plugin"); } }; const onError = () => { toast.error("Please fix the form errors before submitting"); }; const handleDeleteClick = () => { setShowDeleteDialog(true); }; const handleDeleteCancel = () => { setShowDeleteDialog(false); }; const handleDeleteSuccess = () => { setShowDeleteDialog(false); toast.success("Plugin deleted successfully"); props.onDelete(); }; if (!selectedPlugin) { return (

No plugin selected

); } const isErrorLog = (log: string) => { const errorKeywords = ["error", "failed", "exception", "panic", "fatal", "ERR"]; return errorKeywords.some((keyword) => log.toLowerCase().includes(keyword.toLowerCase())); }; return (

Plugin Configuration

( Name The name of the plugin )} /> {selectedPlugin.status?.types && selectedPlugin.status.types.length > 0 && ( Types
{selectedPlugin.status.types.map((type) => ( {type} ))}
)} (
Enabled Enable or disable this plugin
)} /> ( Path The file system path to the plugin )} /> {!showConfig ? ( ) : ( (
Configuration (JSON)
Plugin configuration in JSON format
)} /> )}
{selectedPlugin.status?.status !== "active" && (
{selectedPlugin.status?.logs && selectedPlugin.status.logs.length > 0 && (
{selectedPlugin.status.logs.map((log, index) => (
{log}
))}
)}
)}
{selectedPlugin && ( )}
); }