mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat(plugins): surface the valid agent names in logs and the Plugins UI (#5910)
The agent name used in the `Agents` config option comes from the .ndp file name, not from the manifest. The Plugins UI showed the ID but never said what it was for, so renaming a plugin file silently breaks the config with only a Debug-level "Unknown agent ignored" line to go on. Add a caption under the ID in the Plugins UI, and list the accepted names alongside the rejected one in that log line. Related to navidrome/apple-music-plugin#14
This commit is contained in:
parent
59448e9283
commit
b134f16fd5
@ -4,6 +4,7 @@ import (
|
||||
"cmp"
|
||||
"context"
|
||||
"errors"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -126,12 +127,19 @@ func (a *Agents) getEnabledAgentNames() []enabledAgent {
|
||||
} else if isPlugin {
|
||||
validAgents = append(validAgents, enabledAgent{name: name, isPlugin: true})
|
||||
} else {
|
||||
log.Debug("Unknown agent ignored", "name", name)
|
||||
log.Debug("Unknown agent ignored", "name", name, "available", availableAgentNames(availablePlugins))
|
||||
}
|
||||
}
|
||||
return validAgents
|
||||
}
|
||||
|
||||
// availableAgentNames returns every name accepted by the Agents config option.
|
||||
func availableAgentNames(plugins []string) []string {
|
||||
names := append(slices.Collect(maps.Keys(Map)), plugins...)
|
||||
slices.Sort(names)
|
||||
return names
|
||||
}
|
||||
|
||||
func (a *Agents) getAgent(ea enabledAgent) Interface {
|
||||
if ea.isPlugin {
|
||||
// Try to load WASM plugin agent (if plugin loader is available)
|
||||
|
||||
@ -3,6 +3,7 @@ package agents
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
@ -91,6 +92,22 @@ var _ = Describe("Agents", func() {
|
||||
Expect(ags).ToNot(ContainElement("disabled"))
|
||||
})
|
||||
|
||||
Describe("availableAgentNames", func() {
|
||||
It("combines built-in agents with the given plugins", func() {
|
||||
names := availableAgentNames([]string{"apple-music"})
|
||||
Expect(names).To(ContainElements("apple-music", LocalAgentName, "fake", "empty"))
|
||||
})
|
||||
|
||||
It("returns the names sorted", func() {
|
||||
names := availableAgentNames([]string{"zz-plugin", "aa-plugin"})
|
||||
Expect(slices.IsSorted(names)).To(BeTrue())
|
||||
})
|
||||
|
||||
It("works when there are no plugins", func() {
|
||||
Expect(availableAgentNames(nil)).To(ContainElement(LocalAgentName))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetArtistMBID", func() {
|
||||
It("returns on first match", func() {
|
||||
Expect(ag.GetArtistMBID(ctx, "123", "test")).To(Equal("mbid"))
|
||||
|
||||
@ -394,6 +394,7 @@
|
||||
"invalidJson": "A configuração deve ser um JSON válido"
|
||||
},
|
||||
"messages": {
|
||||
"idHelp": "O ID do plugin, derivado do nome do arquivo. Use-o ao referenciar este plugin em opções de configuração, como Agents.",
|
||||
"configHelp": "Configure o plugin usando pares chave-valor. Deixe vazio se o plugin não precisa de configuração.",
|
||||
"clickPermissions": "Clique em uma permissão para ver detalhes",
|
||||
"noConfig": "Nenhuma configuração definida",
|
||||
|
||||
@ -397,6 +397,7 @@
|
||||
"invalidJson": "Configuration must be valid JSON"
|
||||
},
|
||||
"messages": {
|
||||
"idHelp": "The plugin ID, derived from its file name. Use it when referencing this plugin in configuration options, such as Agents.",
|
||||
"configHelp": "Configure the plugin using key-value pairs. Leave empty if the plugin requires no configuration.",
|
||||
"configValidationError": "Configuration validation failed:",
|
||||
"schemaRenderError": "Unable to render configuration form. The plugin's schema may be invalid.",
|
||||
|
||||
@ -123,6 +123,13 @@ export const InfoCard = ({ record, manifest, classes, translate, isSmall }) => (
|
||||
isSmall={isSmall}
|
||||
>
|
||||
{record.id}
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
className={classes.fieldHelp}
|
||||
>
|
||||
{translate('resources.plugin.messages.idHelp')}
|
||||
</Typography>
|
||||
</InfoRow>
|
||||
|
||||
{manifest?.name && (
|
||||
@ -201,7 +208,7 @@ export const InfoCard = ({ record, manifest, classes, translate, isSmall }) => (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
style={{ marginTop: 4, display: 'block' }}
|
||||
className={classes.fieldHelp}
|
||||
>
|
||||
{translate('resources.plugin.messages.clickPermissions')}
|
||||
</Typography>
|
||||
|
||||
41
ui/src/plugin/InfoCard.test.jsx
Normal file
41
ui/src/plugin/InfoCard.test.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
|
||||
vi.mock('../common', () => ({
|
||||
DateField: ({ source }) => <span data-testid={`date-${source}`} />,
|
||||
}))
|
||||
|
||||
const { InfoCard } = await import('./InfoCard')
|
||||
|
||||
const record = {
|
||||
id: 'apple-music',
|
||||
path: '/data/plugins/apple-music.ndp',
|
||||
updatedAt: '2026-01-01T00:00:00Z',
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
}
|
||||
|
||||
const renderCard = () =>
|
||||
render(
|
||||
<InfoCard
|
||||
record={record}
|
||||
manifest={{ name: 'Apple Music Metadata Agent' }}
|
||||
classes={{}}
|
||||
translate={(key) => key}
|
||||
isSmall={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
describe('InfoCard', () => {
|
||||
it('shows the plugin ID', () => {
|
||||
renderCard()
|
||||
expect(screen.getByText('apple-music')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('explains that the ID is the name used in config options', () => {
|
||||
renderCard()
|
||||
expect(
|
||||
screen.getByText('resources.plugin.messages.idHelp'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -45,6 +45,10 @@ export const usePluginShowStyles = makeStyles(
|
||||
fontSize: '0.85rem',
|
||||
wordBreak: 'break-all',
|
||||
},
|
||||
fieldHelp: {
|
||||
marginTop: theme.spacing(0.5),
|
||||
display: 'block',
|
||||
},
|
||||
permissionsContainer: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user