feat(plugins UI): enhance PluginShow with author, website, and permissions display

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-12-28 00:35:50 -05:00
parent 0cd44d2960
commit 78f5ffce99
2 changed files with 136 additions and 60 deletions

View File

@ -338,6 +338,9 @@
"name": "Name", "name": "Name",
"description": "Description", "description": "Description",
"version": "Version", "version": "Version",
"author": "Author",
"website": "Website",
"permissions": "Permissions",
"enabled": "Enabled", "enabled": "Enabled",
"status": "Status", "status": "Status",
"path": "Path", "path": "Path",

View File

@ -22,9 +22,12 @@ import {
Accordion, Accordion,
AccordionSummary, AccordionSummary,
AccordionDetails, AccordionDetails,
Chip,
Tooltip,
Link,
} from '@material-ui/core' } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { MdExpandMore, MdError, MdCheckCircle } from 'react-icons/md' import { MdExpandMore, MdError } from 'react-icons/md'
import { Title, DateField } from '../common' import { Title, DateField } from '../common'
import { validateJson } from './jsonValidation' import { validateJson } from './jsonValidation'
@ -71,15 +74,6 @@ const useStyles = makeStyles((theme) => ({
fontFamily: 'monospace', fontFamily: 'monospace',
fontSize: '0.85rem', fontSize: '0.85rem',
}, },
statusEnabled: {
color: theme.palette.success?.main || theme.palette.primary.main,
display: 'flex',
alignItems: 'center',
gap: theme.spacing(0.5),
},
statusDisabled: {
color: theme.palette.text.secondary,
},
toolbar: { toolbar: {
display: 'flex', display: 'flex',
justifyContent: 'flex-start', justifyContent: 'flex-start',
@ -104,8 +98,63 @@ const useStyles = makeStyles((theme) => ({
fontSize: '0.85rem', fontSize: '0.85rem',
wordBreak: 'break-all', 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,
},
},
})) }))
const PermissionChip = ({ label, permission }) => {
const classes = useStyles()
if (!permission) return null
const hasHosts = permission.allowedHosts?.length > 0
const tooltipContent = (
<Box className={classes.tooltipContent}>
{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>
))}
</Typography>
</Box>
)}
</Box>
)
const hasTooltip = permission.reason || hasHosts
const chip = (
<Chip
size="small"
label={label}
className={classes.permissionChip}
/>
)
return hasTooltip ? (
<Tooltip title={tooltipContent} arrow>
{chip}
</Tooltip>
) : chip
}
const PluginTitle = ({ record }) => { const PluginTitle = ({ record }) => {
const translate = useTranslate() const translate = useTranslate()
const resourceName = translate('resources.plugin.name', { smart_count: 1 }) const resourceName = translate('resources.plugin.name', { smart_count: 1 })
@ -202,46 +251,27 @@ const PluginShowContent = () => {
</Box> </Box>
)} )}
{/* Status and Enable/Disable */} {/* Status - Enable/Disable Switch Only */}
<Card className={classes.section}> <Card className={classes.section}>
<CardContent> <CardContent>
<Typography variant="h6" className={classes.sectionTitle}> <Typography variant="h6" className={classes.sectionTitle}>
{translate('resources.plugin.sections.status')} {translate('resources.plugin.sections.status')}
</Typography> </Typography>
<Box <FormControlLabel
display="flex" control={
alignItems="center" <Switch
justifyContent="space-between" checked={record.enabled}
> onChange={handleToggleEnabled}
<Box> disabled={loading}
{record.enabled ? ( color="primary"
<Typography className={classes.statusEnabled}> />
<MdCheckCircle /> }
{translate('resources.plugin.status.enabled')} label={translate(
</Typography> record.enabled
) : ( ? 'resources.plugin.actions.disable'
<Typography className={classes.statusDisabled}> : 'resources.plugin.actions.enable',
{translate('resources.plugin.status.disabled')} )}
</Typography> />
)}
</Box>
<FormControlLabel
control={
<Switch
checked={record.enabled}
onChange={handleToggleEnabled}
disabled={loading}
color="primary"
/>
}
label={translate(
record.enabled
? 'resources.plugin.actions.disable'
: 'resources.plugin.actions.enable',
)}
labelPlacement="start"
/>
</Box>
</CardContent> </CardContent>
</Card> </Card>
@ -252,9 +282,16 @@ const PluginShowContent = () => {
{translate('resources.plugin.sections.info')} {translate('resources.plugin.sections.info')}
</Typography> </Typography>
<dl className={classes.infoGrid}> <dl className={classes.infoGrid}>
<dt>{translate('resources.plugin.fields.name')}</dt> <dt>{translate('resources.plugin.fields.id')}</dt>
<dd>{record.id}</dd> <dd>{record.id}</dd>
{manifest?.name && (
<>
<dt>{translate('resources.plugin.fields.name')}</dt>
<dd>{manifest.name}</dd>
</>
)}
{manifest?.version && ( {manifest?.version && (
<> <>
<dt>{translate('resources.plugin.fields.version')}</dt> <dt>{translate('resources.plugin.fields.version')}</dt>
@ -269,6 +306,42 @@ const PluginShowContent = () => {
</> </>
)} )}
{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> <dt>{translate('resources.plugin.fields.path')}</dt>
<dd className={classes.pathField}>{record.path}</dd> <dd className={classes.pathField}>{record.path}</dd>
@ -285,6 +358,20 @@ const PluginShowContent = () => {
</CardContent> </CardContent>
</Card> </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 */} {/* Configuration */}
<Card className={classes.section}> <Card className={classes.section}>
<CardContent> <CardContent>
@ -318,20 +405,6 @@ const PluginShowContent = () => {
</Toolbar> </Toolbar>
</CardContent> </CardContent>
</Card> </Card>
{/* Manifest */}
<Accordion>
<AccordionSummary expandIcon={<MdExpandMore />}>
<Typography variant="h6">
{translate('resources.plugin.sections.manifest')}
</Typography>
</AccordionSummary>
<AccordionDetails>
<Box className={classes.manifestBox} width="100%">
{manifestJson}
</Box>
</AccordionDetails>
</Accordion>
</Box> </Box>
) )
} }