import React, { useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { usePlugins, useInstallPlugin, useActivatePlugin, useDeactivatePlugin, useUninstallPlugin, useUploadPlugin, useInstallPluginFromUrl, } from '@/api/hooks'; import { Button } from '@/components/ui/Button'; import { Card } from '@/components/ui/Card'; import { Badge } from '@/components/ui/Badge'; import { EmptyState } from '@/components/ui/EmptyState'; import { Skeleton } from '@/components/ui/Skeleton'; import { ConfirmDialog } from '@/components/ui/ConfirmDialog'; import { useToast } from '@/components/ui/Toast'; interface Plugin { name: string; display_name?: string; description?: string; version?: string; status: 'discovered' | 'installed' | 'active' | 'inactive'; installed?: boolean; active?: boolean; } function statusBadgeVariant(status: string): 'secondary' | 'primary' | 'success' | 'warning' { switch (status) { case 'active': return 'success'; case 'installed': case 'inactive': return 'warning'; default: return 'secondary'; } } function statusLabel(status: string, t: (k: string) => string): string { switch (status) { case 'active': return t('settings.pluginActive'); case 'installed': return t('settings.pluginInstalled'); case 'inactive': return t('settings.pluginInactive'); default: return t('settings.pluginDiscovered'); } } function InstallPluginSection() { const { t } = useTranslation(); const toast = useToast(); const uploadMutation = useUploadPlugin(); const installUrlMutation = useInstallPluginFromUrl(); const fileInputRef = useRef(null); const [url, setUrl] = useState(''); const [selectedFile, setSelectedFile] = useState(null); const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0] || null; setSelectedFile(file); }; const handleUpload = async () => { if (!selectedFile) { toast.error(t('settings.pluginSelectFile')); return; } try { await uploadMutation.mutateAsync(selectedFile); toast.success(t('settings.pluginUploadedSuccess')); setSelectedFile(null); if (fileInputRef.current) { fileInputRef.current.value = ''; } } catch (err: any) { toast.error(err.message || t('common.error')); } }; const handleInstallUrl = async () => { if (!url.trim()) { toast.error(t('settings.pluginEnterUrl')); return; } try { await installUrlMutation.mutateAsync(url.trim()); toast.success(t('settings.pluginUrlInstalledSuccess')); setUrl(''); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const isBusy = uploadMutation.isPending || installUrlMutation.isPending; return (

{t('settings.installPlugin')}

{/* ZIP Upload */}
{selectedFile && (

{selectedFile.name} ({(selectedFile.size / 1024).toFixed(1)} KB)

)}
{/* URL Install */}
setUrl(e.target.value)} placeholder="https://example.com/plugin.zip" className="flex-1 rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" data-testid="plugin-url-input" />
); } export function SettingsPluginsPage() { const { t } = useTranslation(); const toast = useToast(); const { data, isLoading, isError, error, refetch } = usePlugins(); const installMutation = useInstallPlugin(); const activateMutation = useActivatePlugin(); const deactivateMutation = useDeactivatePlugin(); const uninstallMutation = useUninstallPlugin(); const [confirmUninstall, setConfirmUninstall] = useState(null); const [confirmRemoveData, setConfirmRemoveData] = useState(false); const plugins: Plugin[] = data?.plugins ?? data ?? []; const handleInstall = async (plugin: Plugin) => { try { await installMutation.mutateAsync(plugin.name); toast.success(t('settings.pluginInstalledSuccess')); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const handleActivate = async (plugin: Plugin) => { try { await activateMutation.mutateAsync(plugin.name); toast.success(t('settings.pluginActivatedSuccess')); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const handleDeactivate = async (plugin: Plugin) => { try { await deactivateMutation.mutateAsync(plugin.name); toast.success(t('settings.pluginDeactivatedSuccess')); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const handleUninstall = async () => { if (!confirmUninstall) return; try { await uninstallMutation.mutateAsync({ name: confirmUninstall.name, removeData: confirmRemoveData }); toast.success(t('settings.pluginUninstalledSuccess')); setConfirmUninstall(null); setConfirmRemoveData(false); } catch (err: any) { toast.error(err.message || t('common.error')); } }; if (isLoading) { return (
); } if (isError) { return (

{t('settings.plugins')}

refetch()}>{t('common.reset')}} >

{error?.message || t('common.error')}

); } return (

{t('settings.plugins')}

{/* Install Plugin Section */} {plugins.length === 0 ? ( ) : (
{plugins.map((plugin) => { const status = plugin.active ? 'active' : plugin.installed ? 'inactive' : plugin.status || 'discovered'; const isBusy = installMutation.isPending || activateMutation.isPending || deactivateMutation.isPending || uninstallMutation.isPending; return (

{plugin.display_name || plugin.name}

{statusLabel(status, t)} {plugin.version && ( v{plugin.version} )}
{plugin.description && (

{plugin.description}

)}
{status === 'discovered' && ( )} {status === 'inactive' && ( <> )} {status === 'active' && ( <> )}
); })}
)} { setConfirmUninstall(null); setConfirmRemoveData(false); }} >
); }