diff --git a/src/admin/plugins.rs b/src/admin/plugins.rs index 215767a..58ba0ca 100644 --- a/src/admin/plugins.rs +++ b/src/admin/plugins.rs @@ -27,6 +27,20 @@ pub(super) async fn list( let plugins = state.plugin_registry.list().await; + // Query which plugins have secrets configured + let configured_plugins: std::collections::HashSet = { + let sql = adapt_sql( + "SELECT plugin_id FROM plugin_configs WHERE config IS NOT NULL", + state.db_backend, + ); + sqlx::query_scalar::<_, String>(&sql) + .fetch_all(&state.db) + .await + .unwrap_or_default() + .into_iter() + .collect() + }; + let summaries: Vec = plugins .into_iter() .map(|p| { @@ -40,28 +54,33 @@ pub(super) async fn list( }; // Use manifest for rich secret metadata if available, otherwise fallback to basic keys - let required_secrets = if let Some(manifest) = &p.manifest { - manifest - .required_secrets - .iter() - .map(|s| super::types::SecretDefinition { - key: s.key.clone(), - name: s.name.clone(), - description: s.description.clone(), - }) - .collect() - } else { - // Legacy plugins without manifest - create minimal SecretDefinition from keys - p.info - .required_secrets - .iter() - .map(|key| super::types::SecretDefinition { - key: key.clone(), - name: key.clone(), // Use key as name for legacy - description: None, - }) - .collect() - }; + let required_secrets: Vec = + if let Some(manifest) = &p.manifest { + manifest + .required_secrets + .iter() + .map(|s| super::types::SecretDefinition { + key: s.key.clone(), + name: s.name.clone(), + description: s.description.clone(), + }) + .collect() + } else { + // Legacy plugins without manifest - create minimal SecretDefinition from keys + p.info + .required_secrets + .iter() + .map(|key| super::types::SecretDefinition { + key: key.clone(), + name: key.clone(), // Use key as name for legacy + description: None, + }) + .collect() + }; + + // Plugin is configured if it has no required secrets OR has a config entry + let secrets_configured = + required_secrets.is_empty() || configured_plugins.contains(&p.info.id); PluginSummary { id: p.info.id.clone(), @@ -73,6 +92,7 @@ pub(super) async fn list( enabled: true, // Currently all loaded plugins are enabled auth_type: p.info.auth_type.clone(), required_secrets, + secrets_configured, loaded_at: None, // Would need to track this in registry } }) @@ -136,28 +156,32 @@ pub(super) async fn add( .map_err(|e| AppError::BadRequest(format!("Failed to load plugin: {}", e)))?; // Use manifest for rich secret metadata if available - let required_secrets = if let Some(manifest) = &plugin.manifest { - manifest - .required_secrets - .iter() - .map(|s| super::types::SecretDefinition { - key: s.key.clone(), - name: s.name.clone(), - description: s.description.clone(), - }) - .collect() - } else { - plugin - .info - .required_secrets - .iter() - .map(|key| super::types::SecretDefinition { - key: key.clone(), - name: key.clone(), - description: None, - }) - .collect() - }; + let required_secrets: Vec = + if let Some(manifest) = &plugin.manifest { + manifest + .required_secrets + .iter() + .map(|s| super::types::SecretDefinition { + key: s.key.clone(), + name: s.name.clone(), + description: s.description.clone(), + }) + .collect() + } else { + plugin + .info + .required_secrets + .iter() + .map(|key| super::types::SecretDefinition { + key: key.clone(), + name: key.clone(), + description: None, + }) + .collect() + }; + + // Newly added plugins are not configured (unless they have no required secrets) + let secrets_configured = required_secrets.is_empty(); let summary = PluginSummary { id: plugin.info.id.clone(), @@ -169,6 +193,7 @@ pub(super) async fn add( enabled: true, auth_type: plugin.info.auth_type.clone(), required_secrets, + secrets_configured, loaded_at: Some(now_rfc3339()), }; @@ -272,27 +297,43 @@ pub(super) async fn reload( .map_err(|e| AppError::BadRequest(format!("Failed to reload plugin: {}", e)))?; // Use manifest for rich secret metadata if available - let required_secrets = if let Some(manifest) = &plugin.manifest { - manifest - .required_secrets - .iter() - .map(|s| super::types::SecretDefinition { - key: s.key.clone(), - name: s.name.clone(), - description: s.description.clone(), - }) - .collect() - } else { - plugin - .info - .required_secrets - .iter() - .map(|key| super::types::SecretDefinition { - key: key.clone(), - name: key.clone(), - description: None, - }) - .collect() + let required_secrets: Vec = + if let Some(manifest) = &plugin.manifest { + manifest + .required_secrets + .iter() + .map(|s| super::types::SecretDefinition { + key: s.key.clone(), + name: s.name.clone(), + description: s.description.clone(), + }) + .collect() + } else { + plugin + .info + .required_secrets + .iter() + .map(|key| super::types::SecretDefinition { + key: key.clone(), + name: key.clone(), + description: None, + }) + .collect() + }; + + // Check if reloaded plugin still has its config + let secrets_configured = required_secrets.is_empty() || { + let sql = adapt_sql( + "SELECT 1 FROM plugin_configs WHERE plugin_id = ?", + state.db_backend, + ); + sqlx::query_scalar::<_, i32>(&sql) + .bind(&plugin.info.id) + .fetch_optional(&state.db) + .await + .ok() + .flatten() + .is_some() }; let summary = PluginSummary { @@ -305,6 +346,7 @@ pub(super) async fn reload( enabled: true, auth_type: plugin.info.auth_type.clone(), required_secrets, + secrets_configured, loaded_at: Some(now_rfc3339()), }; diff --git a/src/admin/types.rs b/src/admin/types.rs index cda4826..71c18d1 100644 --- a/src/admin/types.rs +++ b/src/admin/types.rs @@ -248,6 +248,8 @@ pub(super) struct PluginSummary { pub(super) enabled: bool, pub(super) auth_type: String, pub(super) required_secrets: Vec, + /// Whether all required secrets have been configured + pub(super) secrets_configured: bool, pub(super) loaded_at: Option, } diff --git a/web/src/app/dashboard/settings/plugins/page.tsx b/web/src/app/dashboard/settings/plugins/page.tsx index 25473a7..d0cdcfa 100644 --- a/web/src/app/dashboard/settings/plugins/page.tsx +++ b/web/src/app/dashboard/settings/plugins/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import { Plus, Trash2, RefreshCw, ExternalLink, Settings, Loader2, AlertTriangle } from "lucide-react"; +import { Plus, Trash2, RefreshCw, ExternalLink, Settings, Loader2, AlertTriangle, CheckCircle2, AlertCircle } from "lucide-react"; import { useCurrentUser } from "@/hooks/use-current-user"; import { getPlugins, addPlugin, removePlugin, reloadPlugin, getPluginSecrets, updatePluginSecrets, previewPlugin, type PluginPreview } from "@/lib/api"; @@ -363,7 +363,7 @@ export default function PluginsPage() { Version Auth Type Source - Required Secrets + Status @@ -402,13 +402,19 @@ export default function PluginsPage() { -
- {plugin.required_secrets.map((secret) => ( - - {secret.name} - - ))} -
+ {plugin.required_secrets?.length === 0 ? ( + No config needed + ) : plugin.secrets_configured ? ( +
+ + Configured +
+ ) : ( +
+ + Needs config +
+ )}
diff --git a/web/src/types/plugins.ts b/web/src/types/plugins.ts index f9ecfe2..f6f9dce 100644 --- a/web/src/types/plugins.ts +++ b/web/src/types/plugins.ts @@ -14,6 +14,7 @@ export interface PluginSummary { enabled: boolean; auth_type: string; required_secrets: SecretDefinition[]; + secrets_configured: boolean; loaded_at: string | null; }