'use client'; import { useState, useEffect } from 'react'; import { usePluginStore } from '@/stores/plugin-store'; import { usePolicyStore } from '@/stores/policy-store'; import { SettingsSection, ToggleSwitch } from './settings-section'; import { cn } from '@/lib/utils'; import { AlertTriangle, Puzzle, Lock, Server } from 'lucide-react'; import { toast } from '@/stores/toast-store'; import type { InstalledPlugin, PluginStatus, SettingFieldSchema } from '@/lib/plugin-types'; const STATUS_COLORS: Record = { installed: 'bg-muted text-muted-foreground', enabled: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', running: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400', disabled: 'bg-muted text-muted-foreground', error: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', }; export function PluginsSettings() { const { plugins, enablePlugin, disablePlugin, updatePluginSettings, initializePlugins, initialized } = usePluginStore(); const { isFeatureEnabled, isPluginForceEnabled, isPluginApproved, fetchPolicy, loaded } = usePolicyStore(); const [expandedPlugin, setExpandedPlugin] = useState(null); useEffect(() => { if (!loaded) { fetchPolicy(); } initializePlugins(); }, [fetchPolicy, initializePlugins, loaded]); // Listen for "expand this plugin" events fired by the settings search when // the user clicks a plugin-setting sub-result. Expanding the card mounts // the per-field rows so the highlight effect can find them. useEffect(() => { const handler = (e: Event) => { const id = (e as CustomEvent<{ pluginId: string }>).detail?.pluginId; if (id) setExpandedPlugin(id); }; window.addEventListener('settings-plugin-expand', handler); return () => window.removeEventListener('settings-plugin-expand', handler); }, []); if (!isFeatureEnabled('pluginsEnabled')) { return null; } const handleToggle = async (plugin: InstalledPlugin) => { if (!initialized) return; const isForceEnabled = plugin.forceEnabled || isPluginForceEnabled(plugin.id); if (isForceEnabled) { toast.info(`Plugin "${plugin.name}" is forced by admin and cannot be disabled`); return; } const requireApproval = isFeatureEnabled('requirePluginApproval'); const isApproved = plugin.adminApproved || plugin.managed || isPluginApproved(plugin.id); if (!plugin.enabled && requireApproval && !isApproved) { toast.info(`Plugin "${plugin.name}" requires admin approval before it can be enabled`); return; } if (plugin.enabled) { disablePlugin(plugin.id); toast.info(`Plugin "${plugin.name}" disabled`); } else { await enablePlugin(plugin.id); toast.success(`Plugin "${plugin.name}" enabled`); } }; return ( {/* Plugin List */} {plugins.length === 0 ? (

No plugins available

Your administrator has not deployed any plugins

) : (
{plugins.map(plugin => { const requireApproval = isFeatureEnabled('requirePluginApproval'); const isApproved = plugin.adminApproved || plugin.managed || isPluginApproved(plugin.id); const needsApproval = requireApproval && !isApproved; return ( setExpandedPlugin(expandedPlugin === plugin.id ? null : plugin.id)} onToggle={() => handleToggle(plugin)} onUpdateSettings={(settings) => updatePluginSettings(plugin.id, settings)} /> ); })}
)} {!initialized && plugins.length > 0 && (

Syncing plugin policy and managed state...

)}
); } // ─── Plugin Card ───────────────────────────────────────────── interface PluginCardProps { plugin: InstalledPlugin; isExpanded: boolean; isForceEnabled: boolean; isManaged: boolean; needsApproval: boolean; controlsDisabled: boolean; onToggleExpand: () => void; onToggle: () => void; onUpdateSettings: (settings: Record) => void; } function PluginCard({ plugin, isExpanded, isForceEnabled, isManaged, needsApproval, controlsDisabled, onToggleExpand, onToggle, onUpdateSettings }: PluginCardProps) { return (
{/* Header */}
{plugin.name} {plugin.status} {isForceEnabled && ( Forced )} {isManaged && ( Managed )} {needsApproval && ( Awaiting approval )}
{plugin.author} v{plugin.version} {plugin.type}
{/* Expanded Details */} {isExpanded && (
{isForceEnabled && (

This plugin is forced by an administrator and cannot be disabled.

)} {needsApproval && (

This plugin is awaiting admin approval and cannot be enabled until an administrator approves it.

)} {/* Description */} {plugin.description && (

{plugin.description}

)} {/* Error */} {plugin.error && (
{plugin.error}
)} {/* Permissions */} {plugin.permissions.length > 0 && (
Permissions:
{plugin.permissions.map(perm => ( {perm} ))}
)} {/* Settings (auto-generated from schema) */} {plugin.settingsSchema && Object.keys(plugin.settingsSchema).length > 0 && (
Settings: {Object.entries(plugin.settingsSchema).map(([key, schema]) => ( onUpdateSettings({ [key]: value })} /> ))}
)}
)}
); } // ─── Auto-generated Setting Field ──────────────────────────── interface PluginSettingFieldProps { fieldKey: string; schema: SettingFieldSchema; value: unknown; onChange: (value: unknown) => void; } function PluginSettingField({ schema, value, onChange }: PluginSettingFieldProps) { switch (schema.type) { case 'boolean': return (
{schema.label} {schema.description &&

{schema.description}

}
onChange(v)} />
); case 'select': return (
{schema.label} {schema.description &&

{schema.description}

}
); case 'string': return (
{schema.label} {schema.description &&

{schema.description}

} onChange(e.target.value)} className="mt-1 w-full text-xs bg-background border border-border rounded px-2 py-1 text-foreground" />
); case 'number': return (
{schema.label} {schema.description &&

{schema.description}

}
onChange(Number(e.target.value))} className="w-20 text-xs bg-background border border-border rounded px-2 py-1 text-foreground" />
); default: return null; } }