import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useRef, useState } from 'react'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { PLUGIN_INSTANCE_MODES, type PluginInstanceMode, type PluginView } from '@dorfteich/shared'; import { FormError } from '../components/forms'; import { apiDelete, apiGet, apiPatch, apiUploadFile } from '../lib/api'; /** * Site Admin plugin administration (issue #72): the installed-plugin list with * each plugin's declared permissions surfaced prominently (security.md), an * upload dialog that shows validation errors, and the instance-mode switch * (disabled / optional / required). Mode changes and installs are audit-logged * server-side. */ export function PluginManager(): React.JSX.Element { const { t } = useTranslation('plugins'); const queryClient = useQueryClient(); const fileRef = useRef(null); const [uploadError, setUploadError] = useState(null); const plugins = useQuery({ queryKey: ['admin', 'plugins'], queryFn: () => apiGet('/admin/plugins'), }); const invalidate = () => queryClient.invalidateQueries({ queryKey: ['admin', 'plugins'] }); const install = useMutation({ mutationFn: (file: File) => apiUploadFile('/admin/plugins', file), onSuccess: () => { setUploadError(null); if (fileRef.current) fileRef.current.value = ''; void invalidate(); }, onError: (error) => setUploadError(error), }); const setMode = useMutation({ mutationFn: ({ id, mode }: { id: string; mode: PluginInstanceMode }) => apiPatch(`/admin/plugins/${id}/mode`, { mode }), onSuccess: () => void invalidate(), }); const uninstall = useMutation({ mutationFn: (id: string) => apiDelete(`/admin/plugins/${id}`), onSuccess: () => void invalidate(), onError: (error) => setUploadError(error), }); return (

{t('admin.title')}

{t('admin.intro')}

{plugins.data && plugins.data.length === 0 ?

{t('admin.empty')}

: null}
    {plugins.data?.map((plugin) => (
  • {plugin.name} v{plugin.version} {plugin.kind}
    {t('admin.permissions')}:{' '} {plugin.permissions.length === 0 ? ( {t('admin.noPermissions')} ) : ( plugin.permissions.map((permission) => ( {permission} )) )}
    {t(`mode.hint.${plugin.mode}`)} {t('admin.preview')}
  • ))}
); }