mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat(plugins UI): refactor to use MUI and RA components
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
78f5ffce99
commit
dd6d18de0a
@ -373,7 +373,8 @@
|
||||
"invalidJson": "Configuration must be valid JSON"
|
||||
},
|
||||
"messages": {
|
||||
"configHelp": "Enter plugin configuration as a JSON object. Leave empty if the plugin requires no configuration."
|
||||
"configHelp": "Enter plugin configuration as a JSON object. Leave empty if the plugin requires no configuration.",
|
||||
"clickPermissions": "Click a permission for details"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -162,6 +162,7 @@ const PluginList = (props) => {
|
||||
) : (
|
||||
<Datagrid rowClick="show">
|
||||
<TextField source="id" />
|
||||
<ManifestField source="name" />
|
||||
{!isXsmall && <ManifestField source="description" />}
|
||||
<ManifestField source="version" />
|
||||
<ToggleEnabledInput source={'enabled'} />
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import React, { useState, useCallback, useMemo } from 'react'
|
||||
import {
|
||||
Show,
|
||||
SimpleShowLayout,
|
||||
TextField,
|
||||
ShowContextProvider,
|
||||
useShowController,
|
||||
useShowContext,
|
||||
useTranslate,
|
||||
useUpdate,
|
||||
useNotify,
|
||||
useRefresh,
|
||||
useRecordContext,
|
||||
Toolbar,
|
||||
SaveButton,
|
||||
Title as RaTitle,
|
||||
Loading,
|
||||
} from 'react-admin'
|
||||
import {
|
||||
Typography,
|
||||
@ -25,112 +24,120 @@ import {
|
||||
Chip,
|
||||
Tooltip,
|
||||
Link,
|
||||
Grid,
|
||||
useMediaQuery,
|
||||
Button,
|
||||
ClickAwayListener,
|
||||
} from '@material-ui/core'
|
||||
import Alert from '@material-ui/lab/Alert'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { MdExpandMore, MdError } from 'react-icons/md'
|
||||
import { MdExpandMore, MdSave } from 'react-icons/md'
|
||||
import { Title, DateField } from '../common'
|
||||
import { validateJson } from './jsonValidation'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
padding: theme.spacing(2),
|
||||
maxWidth: 900,
|
||||
},
|
||||
section: {
|
||||
marginBottom: theme.spacing(3),
|
||||
},
|
||||
sectionTitle: {
|
||||
marginBottom: theme.spacing(1),
|
||||
fontWeight: 600,
|
||||
},
|
||||
errorBox: {
|
||||
backgroundColor: theme.palette.error.light,
|
||||
color: theme.palette.error.contrastText,
|
||||
padding: theme.spacing(2),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
marginBottom: theme.spacing(2),
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
errorIcon: {
|
||||
marginTop: 2,
|
||||
},
|
||||
manifestBox: {
|
||||
backgroundColor:
|
||||
theme.palette.type === 'dark'
|
||||
? theme.palette.grey[900]
|
||||
: theme.palette.grey[100],
|
||||
padding: theme.spacing(2),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.85rem',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'auto',
|
||||
maxHeight: 400,
|
||||
},
|
||||
configInput: {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.85rem',
|
||||
},
|
||||
toolbar: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
infoGrid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'auto 1fr',
|
||||
gap: theme.spacing(1, 2),
|
||||
'& dt': {
|
||||
const useStyles = makeStyles(
|
||||
(theme) => ({
|
||||
root: {
|
||||
padding: theme.spacing(2),
|
||||
maxWidth: 900,
|
||||
},
|
||||
section: {
|
||||
marginBottom: theme.spacing(3),
|
||||
},
|
||||
sectionTitle: {
|
||||
marginBottom: theme.spacing(1),
|
||||
fontWeight: 600,
|
||||
},
|
||||
manifestBox: {
|
||||
backgroundColor:
|
||||
theme.palette.type === 'dark'
|
||||
? theme.palette.grey[900]
|
||||
: theme.palette.grey[100],
|
||||
padding: theme.spacing(2),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.85rem',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'auto',
|
||||
maxHeight: 400,
|
||||
},
|
||||
configInput: {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.85rem',
|
||||
},
|
||||
saveButton: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
infoGrid: {
|
||||
'& .MuiGrid-item': {
|
||||
paddingTop: theme.spacing(0.5),
|
||||
paddingBottom: theme.spacing(0.5),
|
||||
},
|
||||
},
|
||||
infoLabel: {
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
'& dd': {
|
||||
margin: 0,
|
||||
},
|
||||
},
|
||||
pathField: {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.85rem',
|
||||
wordBreak: 'break-all',
|
||||
},
|
||||
permissionsContainer: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: theme.spacing(0.5),
|
||||
},
|
||||
permissionChip: {
|
||||
fontSize: '0.75rem',
|
||||
},
|
||||
tooltipContent: {
|
||||
'& code': {
|
||||
pathField: {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.8em',
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
padding: '1px 4px',
|
||||
borderRadius: 2,
|
||||
fontSize: '0.85rem',
|
||||
wordBreak: 'break-all',
|
||||
},
|
||||
},
|
||||
}))
|
||||
permissionsContainer: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: theme.spacing(0.5),
|
||||
},
|
||||
permissionChip: {
|
||||
fontSize: '0.75rem',
|
||||
},
|
||||
tooltipContent: {
|
||||
'& code': {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.8em',
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
padding: '1px 4px',
|
||||
borderRadius: 2,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ name: 'NDPluginShow' },
|
||||
)
|
||||
|
||||
const PermissionChip = ({ label, permission }) => {
|
||||
const classes = useStyles()
|
||||
// Helper component for permission chips with clickable persistent tooltips
|
||||
const PermissionChip = ({ label, permission, classes }) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
if (!permission) return null
|
||||
|
||||
const hasHosts = permission.allowedHosts?.length > 0
|
||||
const hasTooltip = permission.reason || hasHosts
|
||||
|
||||
const handleClick = () => {
|
||||
if (hasTooltip) {
|
||||
setOpen((prev) => !prev)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const tooltipContent = (
|
||||
<Box className={classes.tooltipContent}>
|
||||
{permission.reason && <Typography variant="body2">{permission.reason}</Typography>}
|
||||
{permission.reason && (
|
||||
<Typography variant="body2">{permission.reason}</Typography>
|
||||
)}
|
||||
{hasHosts && (
|
||||
<Box mt={permission.reason ? 0.5 : 0}>
|
||||
<Typography variant="caption" component="div">
|
||||
Allowed hosts: {permission.allowedHosts.map((host, i) => (
|
||||
<span key={host}>{i > 0 && ', '}<code>{host}</code></span>
|
||||
Allowed hosts:{' '}
|
||||
{permission.allowedHosts.map((host, i) => (
|
||||
<span key={host}>
|
||||
{i > 0 && ', '}
|
||||
<code>{host}</code>
|
||||
</span>
|
||||
))}
|
||||
</Typography>
|
||||
</Box>
|
||||
@ -138,41 +145,334 @@ const PermissionChip = ({ label, permission }) => {
|
||||
</Box>
|
||||
)
|
||||
|
||||
const hasTooltip = permission.reason || hasHosts
|
||||
|
||||
const chip = (
|
||||
<Chip
|
||||
size="small"
|
||||
label={label}
|
||||
className={classes.permissionChip}
|
||||
onClick={hasTooltip ? handleClick : undefined}
|
||||
clickable={hasTooltip}
|
||||
/>
|
||||
)
|
||||
|
||||
return hasTooltip ? (
|
||||
<Tooltip title={tooltipContent} arrow>
|
||||
{chip}
|
||||
</Tooltip>
|
||||
) : chip
|
||||
}
|
||||
if (!hasTooltip) {
|
||||
return chip
|
||||
}
|
||||
|
||||
const PluginTitle = ({ record }) => {
|
||||
const translate = useTranslate()
|
||||
const resourceName = translate('resources.plugin.name', { smart_count: 1 })
|
||||
return (
|
||||
<Title subTitle={`${resourceName} ${record ? `"${record.id}"` : ''}`} />
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<div>
|
||||
<Tooltip
|
||||
title={tooltipContent}
|
||||
arrow
|
||||
open={open}
|
||||
disableFocusListener
|
||||
disableHoverListener
|
||||
disableTouchListener
|
||||
PopperProps={{
|
||||
disablePortal: true,
|
||||
}}
|
||||
>
|
||||
{chip}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
)
|
||||
}
|
||||
|
||||
const PluginShowContent = () => {
|
||||
const record = useRecordContext()
|
||||
// Info row component for responsive grid
|
||||
const InfoRow = ({ label, children, classes, isSmall }) => (
|
||||
<>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className={classes.infoLabel}
|
||||
component={isSmall ? 'div' : 'span'}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={8}>
|
||||
<Typography variant="body2" component="div">
|
||||
{children}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
|
||||
// Error display section
|
||||
const ErrorSection = ({ error, translate }) => {
|
||||
if (!error) return null
|
||||
|
||||
return (
|
||||
<Alert severity="error" style={{ marginBottom: 16 }}>
|
||||
<Typography variant="subtitle2">
|
||||
{translate('resources.plugin.fields.lastError')}
|
||||
</Typography>
|
||||
<Typography variant="body2">{error}</Typography>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
// Status card with enable/disable toggle
|
||||
const StatusCard = ({ record, classes, translate, onToggle, loading }) => (
|
||||
<Card className={classes.section}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" className={classes.sectionTitle}>
|
||||
{translate('resources.plugin.sections.status')}
|
||||
</Typography>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={record.enabled}
|
||||
onChange={onToggle}
|
||||
disabled={loading}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={translate(
|
||||
record.enabled
|
||||
? 'resources.plugin.actions.disable'
|
||||
: 'resources.plugin.actions.enable',
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
// Plugin information card
|
||||
const InfoCard = ({ record, manifest, classes, translate, isSmall }) => (
|
||||
<Card className={classes.section}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" className={classes.sectionTitle}>
|
||||
{translate('resources.plugin.sections.info')}
|
||||
</Typography>
|
||||
<Grid container spacing={1} className={classes.infoGrid}>
|
||||
<InfoRow
|
||||
label={translate('resources.plugin.fields.id')}
|
||||
classes={classes}
|
||||
isSmall={isSmall}
|
||||
>
|
||||
{record.id}
|
||||
</InfoRow>
|
||||
|
||||
{manifest?.name && (
|
||||
<InfoRow
|
||||
label={translate('resources.plugin.fields.name')}
|
||||
classes={classes}
|
||||
isSmall={isSmall}
|
||||
>
|
||||
{manifest.name}
|
||||
</InfoRow>
|
||||
)}
|
||||
|
||||
{manifest?.version && (
|
||||
<InfoRow
|
||||
label={translate('resources.plugin.fields.version')}
|
||||
classes={classes}
|
||||
isSmall={isSmall}
|
||||
>
|
||||
{manifest.version}
|
||||
</InfoRow>
|
||||
)}
|
||||
|
||||
{manifest?.description && (
|
||||
<InfoRow
|
||||
label={translate('resources.plugin.fields.description')}
|
||||
classes={classes}
|
||||
isSmall={isSmall}
|
||||
>
|
||||
{manifest.description}
|
||||
</InfoRow>
|
||||
)}
|
||||
|
||||
{manifest?.author && (
|
||||
<InfoRow
|
||||
label={translate('resources.plugin.fields.author')}
|
||||
classes={classes}
|
||||
isSmall={isSmall}
|
||||
>
|
||||
{manifest.author}
|
||||
</InfoRow>
|
||||
)}
|
||||
|
||||
{manifest?.website && (
|
||||
<InfoRow
|
||||
label={translate('resources.plugin.fields.website')}
|
||||
classes={classes}
|
||||
isSmall={isSmall}
|
||||
>
|
||||
<Link
|
||||
href={manifest.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{manifest.website}
|
||||
</Link>
|
||||
</InfoRow>
|
||||
)}
|
||||
|
||||
{manifest?.permissions && (
|
||||
<InfoRow
|
||||
label={translate('resources.plugin.fields.permissions')}
|
||||
classes={classes}
|
||||
isSmall={isSmall}
|
||||
>
|
||||
<Box className={classes.permissionsContainer}>
|
||||
<PermissionChip
|
||||
label="HTTP"
|
||||
permission={manifest.permissions.http}
|
||||
classes={classes}
|
||||
/>
|
||||
<PermissionChip
|
||||
label="Subsonic API"
|
||||
permission={manifest.permissions.subsonicapi}
|
||||
classes={classes}
|
||||
/>
|
||||
<PermissionChip
|
||||
label="Scheduler"
|
||||
permission={manifest.permissions.scheduler}
|
||||
classes={classes}
|
||||
/>
|
||||
<PermissionChip
|
||||
label="WebSocket"
|
||||
permission={manifest.permissions.websocket}
|
||||
classes={classes}
|
||||
/>
|
||||
<PermissionChip
|
||||
label="Artwork"
|
||||
permission={manifest.permissions.artwork}
|
||||
classes={classes}
|
||||
/>
|
||||
<PermissionChip
|
||||
label="Cache"
|
||||
permission={manifest.permissions.cache}
|
||||
classes={classes}
|
||||
/>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
style={{ marginTop: 4, display: 'block' }}
|
||||
>
|
||||
{translate('resources.plugin.messages.clickPermissions')}
|
||||
</Typography>
|
||||
</InfoRow>
|
||||
)}
|
||||
|
||||
<InfoRow
|
||||
label={translate('resources.plugin.fields.path')}
|
||||
classes={classes}
|
||||
isSmall={isSmall}
|
||||
>
|
||||
<span className={classes.pathField}>{record.path}</span>
|
||||
</InfoRow>
|
||||
|
||||
<InfoRow
|
||||
label={translate('resources.plugin.fields.updatedAt')}
|
||||
classes={classes}
|
||||
isSmall={isSmall}
|
||||
>
|
||||
<DateField record={record} source="updatedAt" showTime />
|
||||
</InfoRow>
|
||||
|
||||
<InfoRow
|
||||
label={translate('resources.plugin.fields.createdAt')}
|
||||
classes={classes}
|
||||
isSmall={isSmall}
|
||||
>
|
||||
<DateField record={record} source="createdAt" showTime />
|
||||
</InfoRow>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
// Manifest accordion
|
||||
const ManifestSection = ({ manifestJson, classes, translate }) => (
|
||||
<Accordion className={classes.section}>
|
||||
<AccordionSummary expandIcon={<MdExpandMore />}>
|
||||
<Typography variant="h6">
|
||||
{translate('resources.plugin.sections.manifest')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Box className={classes.manifestBox} width="100%">
|
||||
{manifestJson}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
|
||||
// Configuration editor card
|
||||
const ConfigCard = ({
|
||||
config,
|
||||
configError,
|
||||
isDirty,
|
||||
loading,
|
||||
classes,
|
||||
translate,
|
||||
onConfigChange,
|
||||
onSave,
|
||||
}) => (
|
||||
<Card className={classes.section}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" className={classes.sectionTitle}>
|
||||
{translate('resources.plugin.sections.configuration')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
{translate('resources.plugin.messages.configHelp')}
|
||||
</Typography>
|
||||
<MuiTextField
|
||||
multiline
|
||||
fullWidth
|
||||
minRows={4}
|
||||
maxRows={15}
|
||||
variant="outlined"
|
||||
value={config}
|
||||
onChange={onConfigChange}
|
||||
error={!!configError}
|
||||
helperText={configError}
|
||||
placeholder="{}"
|
||||
InputProps={{
|
||||
className: classes.configInput,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<MdSave />}
|
||||
onClick={onSave}
|
||||
disabled={!isDirty || !!configError || loading}
|
||||
className={classes.saveButton}
|
||||
>
|
||||
{translate('ra.action.save')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
// Main show layout component
|
||||
const PluginShowLayout = () => {
|
||||
const { record, isPending, error } = useShowContext()
|
||||
const classes = useStyles()
|
||||
const translate = useTranslate()
|
||||
const notify = useNotify()
|
||||
const refresh = useRefresh()
|
||||
const isSmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
||||
|
||||
const [config, setConfig] = useState(record?.config || '')
|
||||
const [config, setConfig] = useState('')
|
||||
const [configError, setConfigError] = useState(null)
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
const [configInitialized, setConfigInitialized] = useState(false)
|
||||
|
||||
// Initialize config when record loads
|
||||
React.useEffect(() => {
|
||||
if (record && !configInitialized) {
|
||||
setConfig(record.config || '')
|
||||
setConfigInitialized(true)
|
||||
}
|
||||
}, [record, configInitialized])
|
||||
|
||||
const [updatePlugin, { loading }] = useUpdate(
|
||||
'plugin',
|
||||
@ -186,9 +486,9 @@ const PluginShowContent = () => {
|
||||
setIsDirty(false)
|
||||
notify('resources.plugin.notifications.updated', 'info')
|
||||
},
|
||||
onFailure: (error) => {
|
||||
onFailure: (err) => {
|
||||
notify(
|
||||
error?.message || 'resources.plugin.notifications.error',
|
||||
err?.message || 'resources.plugin.notifications.error',
|
||||
'warning',
|
||||
)
|
||||
},
|
||||
@ -196,6 +496,7 @@ const PluginShowContent = () => {
|
||||
)
|
||||
|
||||
const handleToggleEnabled = useCallback(() => {
|
||||
if (!record) return
|
||||
updatePlugin('plugin', record.id, { enabled: !record.enabled }, record)
|
||||
}, [updatePlugin, record])
|
||||
|
||||
@ -216,206 +517,96 @@ const PluginShowContent = () => {
|
||||
)
|
||||
|
||||
const handleSaveConfig = useCallback(() => {
|
||||
if (configError) {
|
||||
if (configError || !record) {
|
||||
notify('resources.plugin.validation.invalidJson', 'warning')
|
||||
return
|
||||
}
|
||||
updatePlugin('plugin', record.id, { config }, record)
|
||||
}, [updatePlugin, record, config, configError, notify])
|
||||
|
||||
// Parse manifest
|
||||
const { manifest, manifestJson } = useMemo(() => {
|
||||
if (!record?.manifest) return { manifest: null, manifestJson: '' }
|
||||
try {
|
||||
const parsed = JSON.parse(record.manifest)
|
||||
return { manifest: parsed, manifestJson: JSON.stringify(parsed, null, 2) }
|
||||
} catch {
|
||||
return { manifest: null, manifestJson: record.manifest }
|
||||
}
|
||||
}, [record?.manifest])
|
||||
|
||||
// Handle loading state
|
||||
if (isPending) {
|
||||
return <Loading />
|
||||
}
|
||||
|
||||
// Handle error state
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error">{translate('ra.notification.http_error')}</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
// Handle missing record
|
||||
if (!record) {
|
||||
return null
|
||||
}
|
||||
|
||||
let manifest = null
|
||||
let manifestJson = ''
|
||||
try {
|
||||
manifest = JSON.parse(record.manifest)
|
||||
manifestJson = JSON.stringify(manifest, null, 2)
|
||||
} catch {
|
||||
manifestJson = record.manifest
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
{/* Error Section */}
|
||||
{record.lastError && (
|
||||
<Box className={classes.errorBox}>
|
||||
<MdError size={20} className={classes.errorIcon} />
|
||||
<Box>
|
||||
<Typography variant="subtitle2">
|
||||
{translate('resources.plugin.fields.lastError')}
|
||||
</Typography>
|
||||
<Typography variant="body2">{record.lastError}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Status - Enable/Disable Switch Only */}
|
||||
<Card className={classes.section}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" className={classes.sectionTitle}>
|
||||
{translate('resources.plugin.sections.status')}
|
||||
</Typography>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={record.enabled}
|
||||
onChange={handleToggleEnabled}
|
||||
disabled={loading}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={translate(
|
||||
record.enabled
|
||||
? 'resources.plugin.actions.disable'
|
||||
: 'resources.plugin.actions.enable',
|
||||
)}
|
||||
<>
|
||||
<RaTitle
|
||||
title={
|
||||
<Title
|
||||
subTitle={`${translate('resources.plugin.name', { smart_count: 1 })} "${record.id}"`}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
}
|
||||
/>
|
||||
<Box className={classes.root}>
|
||||
<ErrorSection error={record.lastError} translate={translate} />
|
||||
|
||||
{/* Plugin Info */}
|
||||
<Card className={classes.section}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" className={classes.sectionTitle}>
|
||||
{translate('resources.plugin.sections.info')}
|
||||
</Typography>
|
||||
<dl className={classes.infoGrid}>
|
||||
<dt>{translate('resources.plugin.fields.id')}</dt>
|
||||
<dd>{record.id}</dd>
|
||||
<StatusCard
|
||||
record={record}
|
||||
classes={classes}
|
||||
translate={translate}
|
||||
onToggle={handleToggleEnabled}
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
{manifest?.name && (
|
||||
<>
|
||||
<dt>{translate('resources.plugin.fields.name')}</dt>
|
||||
<dd>{manifest.name}</dd>
|
||||
</>
|
||||
)}
|
||||
<InfoCard
|
||||
record={record}
|
||||
manifest={manifest}
|
||||
classes={classes}
|
||||
translate={translate}
|
||||
isSmall={isSmall}
|
||||
/>
|
||||
|
||||
{manifest?.version && (
|
||||
<>
|
||||
<dt>{translate('resources.plugin.fields.version')}</dt>
|
||||
<dd>{manifest.version}</dd>
|
||||
</>
|
||||
)}
|
||||
<ManifestSection
|
||||
manifestJson={manifestJson}
|
||||
classes={classes}
|
||||
translate={translate}
|
||||
/>
|
||||
|
||||
{manifest?.description && (
|
||||
<>
|
||||
<dt>{translate('resources.plugin.fields.description')}</dt>
|
||||
<dd>{manifest.description}</dd>
|
||||
</>
|
||||
)}
|
||||
|
||||
{manifest?.author && (
|
||||
<>
|
||||
<dt>{translate('resources.plugin.fields.author')}</dt>
|
||||
<dd>{manifest.author}</dd>
|
||||
</>
|
||||
)}
|
||||
|
||||
{manifest?.website && (
|
||||
<>
|
||||
<dt>{translate('resources.plugin.fields.website')}</dt>
|
||||
<dd>
|
||||
<Link
|
||||
href={manifest.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{manifest.website}
|
||||
</Link>
|
||||
</dd>
|
||||
</>
|
||||
)}
|
||||
|
||||
{manifest?.permissions && (
|
||||
<>
|
||||
<dt>{translate('resources.plugin.fields.permissions')}</dt>
|
||||
<dd className={classes.permissionsContainer}>
|
||||
<PermissionChip label="HTTP" permission={manifest.permissions.http} />
|
||||
<PermissionChip label="Subsonic API" permission={manifest.permissions.subsonicapi} />
|
||||
<PermissionChip label="Scheduler" permission={manifest.permissions.scheduler} />
|
||||
<PermissionChip label="WebSocket" permission={manifest.permissions.websocket} />
|
||||
<PermissionChip label="Artwork" permission={manifest.permissions.artwork} />
|
||||
<PermissionChip label="Cache" permission={manifest.permissions.cache} />
|
||||
</dd>
|
||||
</>
|
||||
)}
|
||||
|
||||
<dt>{translate('resources.plugin.fields.path')}</dt>
|
||||
<dd className={classes.pathField}>{record.path}</dd>
|
||||
|
||||
<dt>{translate('resources.plugin.fields.updatedAt')}</dt>
|
||||
<dd>
|
||||
<DateField record={record} source="updatedAt" showTime />
|
||||
</dd>
|
||||
|
||||
<dt>{translate('resources.plugin.fields.createdAt')}</dt>
|
||||
<dd>
|
||||
<DateField record={record} source="createdAt" showTime />
|
||||
</dd>
|
||||
</dl>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Manifest (Collapsible) */}
|
||||
<Accordion className={classes.section}>
|
||||
<AccordionSummary expandIcon={<MdExpandMore />}>
|
||||
<Typography variant="h6">
|
||||
{translate('resources.plugin.sections.manifest')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Box className={classes.manifestBox} width="100%">
|
||||
{manifestJson}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
{/* Configuration */}
|
||||
<Card className={classes.section}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" className={classes.sectionTitle}>
|
||||
{translate('resources.plugin.sections.configuration')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
{translate('resources.plugin.messages.configHelp')}
|
||||
</Typography>
|
||||
<MuiTextField
|
||||
multiline
|
||||
fullWidth
|
||||
minRows={4}
|
||||
maxRows={15}
|
||||
variant="outlined"
|
||||
value={config}
|
||||
onChange={handleConfigChange}
|
||||
error={!!configError}
|
||||
helperText={configError}
|
||||
placeholder="{}"
|
||||
InputProps={{
|
||||
className: classes.configInput,
|
||||
}}
|
||||
/>
|
||||
<Toolbar className={classes.toolbar}>
|
||||
<SaveButton
|
||||
handleSubmitWithRedirect={handleSaveConfig}
|
||||
disabled={!isDirty || !!configError || loading}
|
||||
saving={loading}
|
||||
/>
|
||||
</Toolbar>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
<ConfigCard
|
||||
config={config}
|
||||
configError={configError}
|
||||
isDirty={isDirty}
|
||||
loading={loading}
|
||||
classes={classes}
|
||||
translate={translate}
|
||||
onConfigChange={handleConfigChange}
|
||||
onSave={handleSaveConfig}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const PluginShow = (props) => {
|
||||
const controllerProps = useShowController(props)
|
||||
return (
|
||||
<Show title={<PluginTitle />} actions={false} {...props}>
|
||||
<SimpleShowLayout>
|
||||
<PluginShowContent />
|
||||
</SimpleShowLayout>
|
||||
</Show>
|
||||
<ShowContextProvider value={controllerProps}>
|
||||
<PluginShowLayout />
|
||||
</ShowContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user