/** * Plugin Configuration Dialog - Configuration interface for plugins */ "use client" import React, { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Switch } from '@/components/ui/switch'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Settings, Save, RotateCw, AlertCircle, CheckCircle, Info, Eye, EyeOff } from 'lucide-react'; import { usePlugin, type PluginInfo, type PluginConfiguration } from '../../contexts/PluginContext'; import { apiClient } from '@/lib/api-client'; interface PluginConfigurationDialogProps { plugin: PluginInfo; open: boolean; onOpenChange: (open: boolean) => void; } interface FormField { key: string; type: string; label: string; description?: string; required?: boolean; default?: any; options?: string[] | { value: string; label: string }[]; validation?: { min?: number; max?: number; pattern?: string; }; } export const PluginConfigurationDialog: React.FC = ({ plugin, open, onOpenChange }) => { const { getPluginConfiguration, savePluginConfiguration, getPluginSchema, pluginConfigurations } = usePlugin(); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const [schema, setSchema] = useState(null); const [config, setConfig] = useState>({}); const [formValues, setFormValues] = useState>({}); const [testingConnection, setTestingConnection] = useState(false); const [testingCredentials, setTestingCredentials] = useState(false); const [credentialsTestResult, setCredentialsTestResult] = useState<{ success: boolean; message: string; } | null>(null); const [showApiToken, setShowApiToken] = useState(false); // Load configuration and schema when dialog opens useEffect(() => { if (open && plugin.id) { loadPluginData(); } }, [open, plugin.id]); // Reset success message after 5 seconds (extended for better visibility) useEffect(() => { if (success) { const timer = setTimeout(() => setSuccess(false), 5000); return () => clearTimeout(timer); } }, [success]); const loadPluginData = async () => { setLoading(true); setError(null); try { // Load schema and current configuration const [schemaData, configData] = await Promise.all([ getPluginSchema(plugin.id), getPluginConfiguration(plugin.id) ]); setSchema(schemaData); setConfig(configData?.configuration || {}); // Initialize form values with current config or defaults const initialValues: Record = {}; if (schemaData?.properties) { Object.entries(schemaData.properties).forEach(([key, field]: [string, any]) => { initialValues[key] = configData?.configuration?.[key] ?? field.default ?? ''; }); } setFormValues(initialValues); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load configuration'); } finally { setLoading(false); } }; const handleSave = async () => { setSaving(true); setError(null); try { // Prepare config data - if API token is empty and we have existing config, preserve existing token const configToSave = { ...formValues }; // If api_token is empty/missing and we have existing config with a token, preserve it if ((!configToSave.api_token || configToSave.api_token.trim() === '') && config.api_token) { configToSave.api_token = config.api_token; // Keep existing token } const success = await savePluginConfiguration(plugin.id, configToSave); if (success) { setConfig(configToSave); setSuccess(true); // Auto-close dialog after successful save (after a brief delay to show success message) setTimeout(() => { onOpenChange(false); }, 2000); } else { setError('Failed to save configuration'); } } catch (err) { const errorMsg = err instanceof Error ? err.message : 'Failed to save configuration'; setError(errorMsg); } finally { setSaving(false); } }; const handleFieldChange = (key: string, value: any) => { setFormValues(prev => ({ ...prev, [key]: value })); setSuccess(false); // Clear success message when editing }; const shouldShowField = (key: string, field: any) => { // Check if field has conditional visibility if (field.depends_on) { const dependsOnField = field.depends_on.field; const dependsOnValue = field.depends_on.value; const currentValue = formValues[dependsOnField]; return currentValue === dependsOnValue; } return true; }; const handleTestConnection = async () => { if (!schema?.validation?.connection_test) return; const testConfig = schema.validation.connection_test; const testData: Record = {}; // Collect required fields for testing testConfig.fields.forEach((fieldKey: string) => { testData[fieldKey] = formValues[fieldKey]; }); setTestingConnection(true); setError(null); try { let result; if (testConfig.method === 'GET') { result = await apiClient.get(testConfig.endpoint); } else if (testConfig.method === 'POST') { result = await apiClient.post(testConfig.endpoint, testData); } else if (testConfig.method === 'PUT') { result = await apiClient.put(testConfig.endpoint, testData); } else { throw new Error(`Unsupported method: ${testConfig.method}`); } if (result.status === 'success') { setSuccess(true); setError(null); setTimeout(() => setSuccess(false), 3000); } else { setError(result.message || testConfig.error_field || 'Connection test failed'); } } catch (err) { setError(`Connection test error: ${err instanceof Error ? err.message : 'Unknown error'}`); } finally { setTestingConnection(false); } }; const handleTestCredentials = async () => { if (!formValues.zammad_url || !formValues.api_token) { setCredentialsTestResult({ success: false, message: 'Please provide both Zammad URL and API Token' }); return; } setTestingCredentials(true); setCredentialsTestResult(null); try { // Test credentials using Zammad API test endpoint const result = await apiClient.post(`/api-internal/v1/plugins/${plugin.id}/test-credentials`, { zammad_url: formValues.zammad_url, api_token: formValues.api_token }); if (result.success) { setCredentialsTestResult({ success: true, message: result.message || 'Credentials verified successfully!' }); // Auto-hide success message after 3 seconds setTimeout(() => setCredentialsTestResult(null), 3000); } else { setCredentialsTestResult({ success: false, message: result.message || result.error || 'Credential test failed' }); } } catch (err) { setCredentialsTestResult({ success: false, message: `Test failed: ${err instanceof Error ? err.message : 'Network error'}` }); } finally { setTestingCredentials(false); } }; const renderNestedField = (fieldId: string, key: string, field: any, value: any, onChange: (value: any) => void) => { const fieldType = field.type || 'string'; switch (fieldType) { case 'boolean': return (
{field.description && (

{field.description}

)}
); case 'select': case 'enum': return (
{field.description && (

{field.description}

)}
); case 'number': case 'integer': return (
onChange(fieldType === 'integer' ? parseInt(e.target.value) || field.default || 0 : parseFloat(e.target.value) || field.default || 0)} placeholder={field.placeholder || `Enter ${field.title || key}`} min={field.minimum} max={field.maximum} /> {field.description && (

{field.description}

)}
); default: return (
onChange(e.target.value)} placeholder={field.placeholder || `Enter ${field.title || key}`} pattern={field.pattern} /> {field.description && (

{field.description}

)}
); } }; const renderField = (key: string, field: any) => { // Check if field should be shown based on dependencies if (!shouldShowField(key, field)) { return null; } const value = formValues[key] ?? field.default ?? ''; const fieldType = field.type || 'string'; switch (fieldType) { case 'boolean': return (
handleFieldChange(key, checked)} />
{field.description && (

{field.description}

)}
); case 'select': case 'enum': return (
{field.description && (

{field.description}

)}
); case 'textarea': return (