mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
* feat(ui): add Rescan button to plugin list empty state When no plugins are installed and the folder watcher fails to detect new plugins, users had no way to trigger a rescan. Extract RescanButton into a shared component and render it in a custom empty state for the plugin list. * refactor(ui): address review feedback for plugin empty state - Pass label translation key directly to RA Button (auto-translates) - Use within() from Testing Library instead of querySelector for scoped queries with better error messages
179 lines
4.1 KiB
JavaScript
179 lines
4.1 KiB
JavaScript
import React, { useMemo, useState, useCallback } from 'react'
|
|
import {
|
|
Button,
|
|
Datagrid,
|
|
Empty,
|
|
TextField,
|
|
TopToolbar,
|
|
useNotify,
|
|
useRecordContext,
|
|
useRefresh,
|
|
useTranslate,
|
|
} from 'react-admin'
|
|
import { makeStyles } from '@material-ui/core/styles'
|
|
import {
|
|
useMediaQuery,
|
|
Tooltip,
|
|
Chip,
|
|
Typography,
|
|
Box,
|
|
} from '@material-ui/core'
|
|
import { MdError, MdRefresh } from 'react-icons/md'
|
|
import { List, DateField, SimpleList, useResourceRefresh } from '../common'
|
|
import { httpClient } from '../dataProvider'
|
|
import ToggleEnabledSwitch from './ToggleEnabledSwitch'
|
|
|
|
const useStyles = makeStyles((theme) => ({
|
|
errorIcon: {
|
|
color: theme.palette.error.main,
|
|
marginRight: theme.spacing(0.5),
|
|
verticalAlign: 'middle',
|
|
},
|
|
errorChip: {
|
|
backgroundColor: theme.palette.error.light,
|
|
color: theme.palette.error.contrastText,
|
|
},
|
|
}))
|
|
|
|
const useManifest = () => {
|
|
const record = useRecordContext()
|
|
return useMemo(() => {
|
|
if (!record?.manifest) return null
|
|
try {
|
|
return JSON.parse(record.manifest)
|
|
} catch {
|
|
return null
|
|
}
|
|
}, [record?.manifest])
|
|
}
|
|
|
|
const EnabledOrErrorField = () => {
|
|
const record = useRecordContext()
|
|
const translate = useTranslate()
|
|
const classes = useStyles()
|
|
const manifest = useManifest()
|
|
|
|
if (record.lastError) {
|
|
return (
|
|
<Tooltip title={record.lastError}>
|
|
<Chip
|
|
size="small"
|
|
icon={<MdError className={classes.errorIcon} />}
|
|
label={translate('resources.plugin.fields.hasError')}
|
|
className={classes.errorChip}
|
|
/>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
|
|
return <ToggleEnabledSwitch source={'enabled'} manifest={manifest} />
|
|
}
|
|
|
|
const ManifestField = ({ source }) => {
|
|
const manifest = useManifest()
|
|
|
|
if (!manifest) {
|
|
return <Typography variant="body2">-</Typography>
|
|
}
|
|
|
|
return <Typography variant="body2">{manifest[source] || '-'}</Typography>
|
|
}
|
|
|
|
const RescanButton = () => {
|
|
const notify = useNotify()
|
|
const refresh = useRefresh()
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const handleRescan = useCallback(() => {
|
|
setLoading(true)
|
|
httpClient('/api/plugin/rescan', { method: 'POST' })
|
|
.then(() => {
|
|
refresh()
|
|
})
|
|
.catch((error) => {
|
|
notify(error.message || 'ra.page.error', { type: 'warning' })
|
|
})
|
|
.finally(() => {
|
|
setLoading(false)
|
|
})
|
|
}, [notify, refresh])
|
|
|
|
return (
|
|
<Button
|
|
onClick={handleRescan}
|
|
disabled={loading}
|
|
label="resources.plugin.actions.rescan"
|
|
data-testid="rescan-button"
|
|
>
|
|
<MdRefresh />
|
|
</Button>
|
|
)
|
|
}
|
|
|
|
const PluginListActions = () => {
|
|
return (
|
|
<TopToolbar>
|
|
<RescanButton />
|
|
</TopToolbar>
|
|
)
|
|
}
|
|
|
|
const PluginEmpty = () => {
|
|
return (
|
|
<>
|
|
<Empty />
|
|
<Box textAlign="center" mt={2}>
|
|
<RescanButton />
|
|
</Box>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const PluginList = (props) => {
|
|
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
|
const translate = useTranslate()
|
|
useResourceRefresh('plugin')
|
|
|
|
return (
|
|
<List
|
|
{...props}
|
|
sort={{ field: 'id', order: 'ASC' }}
|
|
exporter={false}
|
|
bulkActionButtons={false}
|
|
actions={<PluginListActions />}
|
|
empty={<PluginEmpty />}
|
|
>
|
|
{isXsmall ? (
|
|
<SimpleList
|
|
primaryText={(record) => record.id}
|
|
secondaryText={(record) => {
|
|
try {
|
|
const manifest = JSON.parse(record.manifest)
|
|
return manifest.description || ''
|
|
} catch {
|
|
return ''
|
|
}
|
|
}}
|
|
tertiaryText={(record) =>
|
|
record.enabled
|
|
? translate('resources.plugin.status.enabled')
|
|
: translate('resources.plugin.status.disabled')
|
|
}
|
|
linkType="show"
|
|
/>
|
|
) : (
|
|
<Datagrid rowClick="show">
|
|
<TextField source="id" />
|
|
<ManifestField source="name" />
|
|
{!isXsmall && <ManifestField source="description" />}
|
|
<ManifestField source="version" />
|
|
<EnabledOrErrorField source={'enabled'} />
|
|
<DateField source="updatedAt" sortByOrder={'DESC'} />
|
|
</Datagrid>
|
|
)}
|
|
</List>
|
|
)
|
|
}
|
|
|
|
export default PluginList
|