fix: SettingsPlugins.tsx with actual content - plugin management page
This commit is contained in:
@@ -1 +1,255 @@
|
||||
§§include(/tmp/leocrm-changes/frontend/src/pages/SettingsPlugins.tsx)
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePlugins, useInstallPlugin, useActivatePlugin, useDeactivatePlugin, useUninstallPlugin } 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');
|
||||
}
|
||||
}
|
||||
|
||||
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<Plugin | null>(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 (
|
||||
<div className="p-6 max-w-4xl mx-auto" data-testid="settings-plugins-page">
|
||||
<Skeleton className="h-8 w-48 mb-6" />
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-20" />
|
||||
<Skeleton className="h-20" />
|
||||
<Skeleton className="h-20" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto" data-testid="settings-plugins-page">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-6">{t('settings.plugins')}</h1>
|
||||
<EmptyState
|
||||
title={t('common.error')}
|
||||
action={<Button onClick={() => refetch()}>{t('common.reset')}</Button>}
|
||||
>
|
||||
<p className="text-sm text-secondary-500">{error?.message || t('common.error')}</p>
|
||||
</EmptyState>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto" data-testid="settings-plugins-page">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-2xl font-bold text-secondary-900">{t('settings.plugins')}</h1>
|
||||
<Button variant="secondary" size="sm" onClick={() => refetch()} data-testid="refresh-plugins-btn">
|
||||
{t('common.reset')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{plugins.length === 0 ? (
|
||||
<EmptyState title={t('settings.noPlugins')} />
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{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 (
|
||||
<Card key={plugin.name}>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h3 className="text-lg font-semibold text-secondary-900">
|
||||
{plugin.display_name || plugin.name}
|
||||
</h3>
|
||||
<Badge variant={statusBadgeVariant(status)}>
|
||||
{statusLabel(status, t)}
|
||||
</Badge>
|
||||
{plugin.version && (
|
||||
<span className="text-xs text-secondary-400">v{plugin.version}</span>
|
||||
)}
|
||||
</div>
|
||||
{plugin.description && (
|
||||
<p className="text-sm text-secondary-600">{plugin.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{status === 'discovered' && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleInstall(plugin)}
|
||||
isLoading={installMutation.isPending}
|
||||
disabled={isBusy}
|
||||
data-testid={`install-plugin-${plugin.name}`}
|
||||
>
|
||||
{t('settings.pluginInstall')}
|
||||
</Button>
|
||||
)}
|
||||
{status === 'inactive' && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleActivate(plugin)}
|
||||
isLoading={activateMutation.isPending}
|
||||
disabled={isBusy}
|
||||
data-testid={`activate-plugin-${plugin.name}`}
|
||||
>
|
||||
{t('settings.pluginActivate')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={() => setConfirmUninstall(plugin)}
|
||||
disabled={isBusy}
|
||||
data-testid={`uninstall-plugin-${plugin.name}`}
|
||||
>
|
||||
{t('settings.pluginUninstall')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{status === 'active' && (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => handleDeactivate(plugin)}
|
||||
isLoading={deactivateMutation.isPending}
|
||||
disabled={isBusy}
|
||||
data-testid={`deactivate-plugin-${plugin.name}`}
|
||||
>
|
||||
{t('settings.pluginDeactivate')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={() => setConfirmUninstall(plugin)}
|
||||
disabled={isBusy}
|
||||
data-testid={`uninstall-plugin-${plugin.name}`}
|
||||
>
|
||||
{t('settings.pluginUninstall')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!confirmUninstall}
|
||||
title={t('settings.pluginUninstall')}
|
||||
message={`${t('settings.pluginUninstallConfirm')}: ${confirmUninstall?.display_name || confirmUninstall?.name}?`}
|
||||
variant="danger"
|
||||
onConfirm={handleUninstall}
|
||||
onCancel={() => {
|
||||
setConfirmUninstall(null);
|
||||
setConfirmRemoveData(false);
|
||||
}}
|
||||
>
|
||||
<label className="flex items-center gap-2 mt-3 cursor-pointer min-h-touch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={confirmRemoveData}
|
||||
onChange={(e) => setConfirmRemoveData(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-secondary-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
<span className="text-sm text-secondary-700">{t('settings.pluginRemoveData')}</span>
|
||||
</label>
|
||||
</ConfirmDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user