diff --git a/ui/src/plugin/PluginList.jsx b/ui/src/plugin/PluginList.jsx index 1449f8daa..5dee41838 100644 --- a/ui/src/plugin/PluginList.jsx +++ b/ui/src/plugin/PluginList.jsx @@ -1,20 +1,15 @@ -import React, { useCallback } from 'react' +import React, { useMemo } from 'react' import { Datagrid, TextField, - useUpdate, - useNotify, - useRefresh, useRecordContext, useTranslate, - FunctionField, - useResourceContext, } from 'react-admin' -import Switch from '@material-ui/core/Switch' import { makeStyles } from '@material-ui/core/styles' import { useMediaQuery, Tooltip, Chip, Typography } from '@material-ui/core' import { MdError } from 'react-icons/md' import { List, DateField, SimpleList } from '../common' +import ToggleEnabledSwitch from './ToggleEnabledSwitch' const useStyles = makeStyles((theme) => ({ errorIcon: { @@ -26,84 +21,8 @@ const useStyles = makeStyles((theme) => ({ backgroundColor: theme.palette.error.light, color: theme.palette.error.contrastText, }, - enabledSwitch: { - '& .MuiSwitch-colorSecondary.Mui-checked': { - color: theme.palette.success?.main || theme.palette.primary.main, - }, - '& .MuiSwitch-colorSecondary.Mui-checked + .MuiSwitch-track': { - backgroundColor: - theme.palette.success?.main || theme.palette.primary.main, - }, - }, })) -const ToggleEnabledInput = ({ props }) => { - const resource = useResourceContext(props) - const record = useRecordContext(props) - const notify = useNotify() - const refresh = useRefresh() - const translate = useTranslate() - const classes = useStyles() - - const [toggleEnabled, { loading }] = useUpdate( - resource, - record.id, - { enabled: !record.enabled }, - record, - { - undoable: false, - onSuccess: () => { - refresh() - notify( - record.enabled - ? 'resources.plugin.notifications.disabled' - : 'resources.plugin.notifications.enabled', - 'info', - ) - }, - onFailure: (error) => { - notify( - error?.message || 'resources.plugin.notifications.error', - 'warning', - ) - }, - }, - ) - - const handleClick = useCallback( - (e) => { - e.stopPropagation() - toggleEnabled() - }, - [toggleEnabled], - ) - - const hasError = !!record.lastError - const isDisabled = loading || hasError - - return ( - - - - - - ) -} - const ErrorIndicator = () => { const record = useRecordContext() const translate = useTranslate() @@ -125,19 +44,26 @@ const ErrorIndicator = () => { ) } -const ManifestField = ({ source }) => { +const useManifest = () => { const record = useRecordContext() + return useMemo(() => { + if (!record?.manifest) return null + try { + return JSON.parse(record.manifest) + } catch { + return null + } + }, [record?.manifest]) +} - if (!record?.manifest) { - return null +const ManifestField = ({ source }) => { + const manifest = useManifest() + + if (!manifest) { + return - } - try { - const manifest = JSON.parse(record.manifest) - return {manifest[source] || '-'} - } catch { - return - - } + return {manifest[source] || '-'} } const PluginList = (props) => { @@ -170,7 +96,7 @@ const PluginList = (props) => { {!isXsmall && } - + diff --git a/ui/src/plugin/PluginList.test.jsx b/ui/src/plugin/PluginList.test.jsx index be61d4609..e1fb7eb5f 100644 --- a/ui/src/plugin/PluginList.test.jsx +++ b/ui/src/plugin/PluginList.test.jsx @@ -11,18 +11,17 @@ vi.mock('react-admin', async () => { useNotify: vi.fn(() => vi.fn()), useRefresh: vi.fn(() => vi.fn()), useTranslate: vi.fn(() => (key) => key), + useResourceContext: vi.fn(() => 'plugin'), useRecordContext: vi.fn(() => ({ id: 'test-plugin', manifest: JSON.stringify({ + name: 'Test Plugin', version: '1.0.0', description: 'Test plugin', }), enabled: true, lastError: null, })), - FunctionField: ({ render, label }) => ( - {render && render()} - ), Datagrid: ({ children }) => ( {children}
), @@ -54,6 +53,11 @@ vi.mock('@material-ui/core', async () => { } }) +// Mock ToggleEnabledSwitch +vi.mock('./ToggleEnabledSwitch', () => ({ + default: () => , +})) + import PluginList from './PluginList' describe('PluginList', () => { diff --git a/ui/src/plugin/PluginShow.jsx b/ui/src/plugin/PluginShow.jsx index 9c318536e..8ddc76773 100644 --- a/ui/src/plugin/PluginShow.jsx +++ b/ui/src/plugin/PluginShow.jsx @@ -13,8 +13,6 @@ import { import { Typography, Box, - Switch, - FormControlLabel, Card, CardContent, TextField as MuiTextField, @@ -34,6 +32,7 @@ import { makeStyles } from '@material-ui/core/styles' import { MdExpandMore, MdSave } from 'react-icons/md' import { Title, DateField } from '../common' import { validateJson } from './jsonValidation' +import ToggleEnabledSwitch from './ToggleEnabledSwitch' const useStyles = makeStyles( (theme) => ({ @@ -215,46 +214,14 @@ const ErrorSection = ({ error, translate }) => { } // Status card with enable/disable toggle -const StatusCard = ({ - record, - classes, - translate, - onToggle, - loading, - hasError, -}) => { - const isDisabled = loading || hasError - +const StatusCard = ({ classes, translate }) => { return ( {translate('resources.plugin.sections.status')} - - - } - label={translate( - record.enabled - ? 'resources.plugin.actions.disable' - : 'resources.plugin.actions.enable', - )} - /> - + ) @@ -332,53 +299,32 @@ const InfoCard = ({ record, manifest, classes, translate, isSmall }) => ( )} - {manifest?.permissions && ( - - - - - - - - - - 0 && ( + - {translate('resources.plugin.messages.clickPermissions')} - - - )} + + {Object.entries(manifest.permissions).map(([key, value]) => ( + + ))} + + + {translate('resources.plugin.messages.clickPermissions')} + + + )} { }, ) - const handleToggleEnabled = useCallback(() => { - if (!record) return - updatePlugin('plugin', record.id, { enabled: !record.enabled }, record) - }, [updatePlugin, record]) - const handleConfigChange = useCallback( (e) => { const value = e.target.value @@ -584,14 +525,7 @@ const PluginShowLayout = () => { - + ({ + enabledSwitch: { + '& .MuiSwitch-colorSecondary.Mui-checked': { + color: theme.palette.success?.main || theme.palette.primary.main, + }, + '& .MuiSwitch-colorSecondary.Mui-checked + .MuiSwitch-track': { + backgroundColor: + theme.palette.success?.main || theme.palette.primary.main, + }, + }, +})) + +/** + * Shared toggle switch for enabling/disabling plugins. + * Used in both PluginList (compact) and PluginShow (with label). + * + * @param {Object} props + * @param {boolean} [props.showLabel=false] - Whether to show the enable/disable label + * @param {string} [props.size='small'] - Switch size ('small' or 'medium') + */ +const ToggleEnabledSwitch = ({ showLabel = false, size = 'small' }) => { + const resource = useResourceContext() + const record = useRecordContext() + const notify = useNotify() + const refresh = useRefresh() + const translate = useTranslate() + const classes = useStyles() + + const [toggleEnabled, { loading }] = useUpdate( + resource, + record?.id, + { enabled: !record?.enabled }, + record, + { + undoable: false, + onSuccess: () => { + refresh() + notify( + record?.enabled + ? 'resources.plugin.notifications.disabled' + : 'resources.plugin.notifications.enabled', + 'info', + ) + }, + onFailure: (error) => { + notify( + error?.message || 'resources.plugin.notifications.error', + 'warning', + ) + }, + }, + ) + + const handleClick = useCallback( + (e) => { + e.stopPropagation() + toggleEnabled() + }, + [toggleEnabled], + ) + + const hasError = !!record?.lastError + const isDisabled = loading || hasError + + const tooltipTitle = useMemo(() => { + if (hasError) { + return translate('resources.plugin.actions.disabledDueToError') + } + if (!showLabel) { + return translate( + record?.enabled + ? 'resources.plugin.actions.disable' + : 'resources.plugin.actions.enable', + ) + } + return '' + }, [hasError, showLabel, record?.enabled, translate]) + + const switchElement = ( + + ) + + if (showLabel) { + return ( + + + + ) + } + + return ( + + {switchElement} + + ) +} + +export default ToggleEnabledSwitch