Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions dashboard/src/pages/Plugins.css
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,18 @@
box-shadow: 0 0 0 3px rgba(37, 211, 102, 0.1);
}

.config-form .form-group small {
display: block;
margin-top: 0.4rem;
font-size: 0.75rem;
color: var(--text-muted);
line-height: 1.4;
}

.config-form .form-group .required-mark {
color: var(--danger, #e5484d);
}

.toggle-group {
display: flex;
align-items: center;
Expand Down
106 changes: 104 additions & 2 deletions dashboard/src/pages/Plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export default function Plugins() {
browserArgs: '--no-sandbox --disable-gpu',
});
const [savingConfig, setSavingConfig] = useState(false);
// Values for a schema-driven (non-engine) plugin's config form, keyed by configSchema property.
const [schemaConfig, setSchemaConfig] = useState<Record<string, unknown>>({});

const refetchAll = () => {
void queryClient.invalidateQueries({ queryKey: queryKeys.plugins });
Expand Down Expand Up @@ -112,9 +114,32 @@ export default function Plugins() {

const handleOpenConfig = (plugin: Plugin) => {
setConfigPlugin(plugin);
// Seed the schema form from the plugin's saved config, falling back to each field's default.
if (plugin.configSchema?.properties) {
const initial: Record<string, unknown> = {};
for (const [key, field] of Object.entries(plugin.configSchema.properties)) {
initial[key] = plugin.config[key] ?? field.default ?? (field.type === 'boolean' ? false : '');
}
setSchemaConfig(initial);
}
setShowConfigModal(true);
};

const handleSaveSchemaConfig = async () => {
if (!configPlugin) return;
setSavingConfig(true);
try {
await pluginsApi.updateConfig(configPlugin.id, schemaConfig);
void queryClient.invalidateQueries({ queryKey: queryKeys.plugins });
toast.success(t('plugins.toasts.savedTitle'), t('plugins.toasts.savedDesc'));
setShowConfigModal(false);
} catch (err) {
toast.error(t('plugins.toasts.saveFailed'), err instanceof Error ? err.message : t('common.unknownError'));
} finally {
setSavingConfig(false);
}
};

const handleSaveConfig = async () => {
setSavingConfig(true);
try {
Expand Down Expand Up @@ -401,6 +426,79 @@ export default function Plugins() {
</div>
</div>
</>
) : configPlugin.configSchema && Object.keys(configPlugin.configSchema.properties).length > 0 ? (
<div className="config-form">
{Object.entries(configPlugin.configSchema.properties).map(([key, field]) => {
const value = schemaConfig[key];
const label = field.title || key;

if (field.type === 'boolean') {
return (
<div className="form-group toggle-group" key={key}>
<div className="toggle-info">
<label>{label}</label>
{field.description && <small>{field.description}</small>}
</div>
<label className="toggle-switch">
<input
type="checkbox"
checked={Boolean(value)}
onChange={e => setSchemaConfig({ ...schemaConfig, [key]: e.target.checked })}
/>
<span className="toggle-slider"></span>
</label>
</div>
);
}

if (field.enum && field.enum.length > 0) {
return (
<div className="form-group" key={key}>
<label>{label}</label>
<select
value={String(value ?? '')}
onChange={e => setSchemaConfig({ ...schemaConfig, [key]: e.target.value })}
>
{field.enum.map(opt => (
<option key={String(opt)} value={String(opt)}>
{String(opt)}
</option>
))}
</select>
{field.description && <small>{field.description}</small>}
</div>
);
}

const inputType = field.type === 'number' ? 'number' : field.secret ? 'password' : 'text';
return (
<div className="form-group" key={key}>
<label>
{label}
{field.required && <span className="required-mark"> *</span>}
</label>
<input
type={inputType}
value={value === undefined || value === null ? '' : String(value)}
placeholder={field.default !== undefined ? String(field.default) : undefined}
autoComplete={field.secret ? 'new-password' : undefined}
onChange={e =>
setSchemaConfig({
...schemaConfig,
[key]:
field.type === 'number'
? e.target.value === ''
? ''
: Number(e.target.value)
: e.target.value,
})
}
/>
{field.description && <small>{field.description}</small>}
</div>
);
})}
</div>
) : (
<div className="no-config">
<Settings size={48} style={{ opacity: 0.3 }} />
Expand All @@ -413,11 +511,15 @@ export default function Plugins() {
<button className="btn-secondary" onClick={() => setShowConfigModal(false)}>
{t('common.cancel')}
</button>
{configPlugin.type === 'engine' && (
{configPlugin.type === 'engine' ? (
<button className="btn-primary" onClick={handleSaveConfig} disabled={savingConfig}>
{savingConfig ? <Loader2 size={16} className="animate-spin" /> : t('plugins.config.save')}
</button>
)}
) : configPlugin.configSchema && Object.keys(configPlugin.configSchema.properties).length > 0 ? (
<button className="btn-primary" onClick={handleSaveSchemaConfig} disabled={savingConfig}>
{savingConfig ? <Loader2 size={16} className="animate-spin" /> : t('plugins.config.save')}
</button>
) : null}
</div>
</div>
</div>
Expand Down
18 changes: 18 additions & 0 deletions dashboard/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,22 @@ export const settingsApi = {
// Plugin Types
// =============================================================================

/** Field definition within a plugin's config schema (mirrors the backend PluginConfigSchema). */
export interface PluginConfigField {
type: 'string' | 'number' | 'boolean' | 'array' | 'object';
title?: string;
description?: string;
default?: unknown;
enum?: unknown[];
required?: boolean;
secret?: boolean;
}

export interface PluginConfigSchema {
type: 'object';
properties: Record<string, PluginConfigField>;
}

export interface Plugin {
id: string;
name: string;
Expand All @@ -568,6 +584,8 @@ export interface Plugin {
config: Record<string, unknown>;
builtIn: boolean;
provides: string[];
/** Declared config fields, when the plugin exposes a schema (drives the dashboard config form). */
configSchema?: PluginConfigSchema;
loadedAt?: string;
enabledAt?: string;
error?: string;
Expand Down