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 }) => (
),
@@ -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