diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/edit/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/edit/page.tsx index e526976..cc348ba 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/edit/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/edit/page.tsx @@ -37,7 +37,19 @@ import { import { Checkbox } from "@/components/ui/checkbox"; import { Skeleton } from "@/components/ui/skeleton"; -// Schema for PAT input when editing repos +// Helper function to get connector type display name (copied from manage page) +const getConnectorTypeDisplay = (type: string): string => { + const typeMap: Record = { + "SERPER_API": "Serper API", + "TAVILY_API": "Tavily API", + "SLACK_CONNECTOR": "Slack", + "NOTION_CONNECTOR": "Notion", + "GITHUB_CONNECTOR": "GitHub", + }; + return typeMap[type] || type; +}; + +// Schema for PAT input when editing GitHub repos (remains separate) const githubPatSchema = z.object({ github_pat: z.string() .min(20, { message: "GitHub Personal Access Token seems too short." }) @@ -47,9 +59,15 @@ const githubPatSchema = z.object({ }); type GithubPatFormValues = z.infer; -// Schema for main edit form (just the name for now) +// Updated schema for main edit form - includes optional fields for other connector configs const editConnectorSchema = z.object({ name: z.string().min(3, { message: "Connector name must be at least 3 characters." }), + // Add optional fields for other connector types' configs + SLACK_BOT_TOKEN: z.string().optional(), + NOTION_INTEGRATION_TOKEN: z.string().optional(), + SERPER_API_KEY: z.string().optional(), + TAVILY_API_KEY: z.string().optional(), + // GITHUB_PAT is handled separately via patForm for repo editing flow }); type EditConnectorFormValues = z.infer; @@ -63,9 +81,9 @@ interface GithubRepo { last_updated: string | null; } -type EditMode = 'viewing' | 'editing_repos'; +type EditMode = 'viewing' | 'editing_repos'; // Only relevant for GitHub -export default function EditGithubConnectorPage() { +export default function EditConnectorPage() { // Renamed for clarity const router = useRouter(); const params = useParams(); const searchSpaceId = params.search_space_id as string; @@ -74,54 +92,74 @@ export default function EditGithubConnectorPage() { const { connectors, updateConnector, isLoading: connectorsLoading } = useSearchSourceConnectors(); const [connector, setConnector] = useState(null); + const [originalConfig, setOriginalConfig] = useState | null>(null); // Store original config object + + // GitHub specific state (only used if connector type is GitHub) const [currentSelectedRepos, setCurrentSelectedRepos] = useState([]); - const [originalPat, setOriginalPat] = useState(""); // State to hold the initial PAT - const [editMode, setEditMode] = useState('viewing'); - const [fetchedRepos, setFetchedRepos] = useState(null); // Null indicates not fetched yet for edit - const [newSelectedRepos, setNewSelectedRepos] = useState([]); // Tracks selections *during* edit + const [originalPat, setOriginalPat] = useState(""); + const [editMode, setEditMode] = useState('viewing'); + const [fetchedRepos, setFetchedRepos] = useState(null); + const [newSelectedRepos, setNewSelectedRepos] = useState([]); const [isFetchingRepos, setIsFetchingRepos] = useState(false); + const [isSaving, setIsSaving] = useState(false); - // Form for just the PAT input + // Form for GitHub PAT input (only used for GitHub repo editing) const patForm = useForm({ resolver: zodResolver(githubPatSchema), - defaultValues: { github_pat: "" }, // Default empty, will be reset + defaultValues: { github_pat: "" }, }); - // Form for the main connector details (e.g., name) + // Main form for connector details (name + simple config fields) const editForm = useForm({ resolver: zodResolver(editConnectorSchema), - defaultValues: { name: "" }, + defaultValues: { + name: "", + SLACK_BOT_TOKEN: "", + NOTION_INTEGRATION_TOKEN: "", + SERPER_API_KEY: "", + TAVILY_API_KEY: "", + }, }); - // Effect to find and set the current connector details on load + // Effect to load connector data useEffect(() => { - if (!connectorsLoading && connectors.length > 0 && !connector) { // Added !connector check to prevent loop + if (!connectorsLoading && connectors.length > 0 && !connector) { const currentConnector = connectors.find(c => c.id === connectorId); - if (currentConnector && currentConnector.connector_type === 'GITHUB_CONNECTOR') { + if (currentConnector) { setConnector(currentConnector); - const savedRepos = currentConnector.config?.repo_full_names || []; - const savedPat = currentConnector.config?.GITHUB_PAT || ""; - setCurrentSelectedRepos(savedRepos); - setNewSelectedRepos(savedRepos); - setOriginalPat(savedPat); // Store the original PAT - editForm.reset({ name: currentConnector.name }); - patForm.reset({ github_pat: savedPat }); // Also reset PAT form initially - } else if (currentConnector) { - toast.error("This connector is not a GitHub connector."); - router.push(`/dashboard/${searchSpaceId}/connectors`); + setOriginalConfig(currentConnector.config || {}); // Store original config + + // Reset main form with common and type-specific fields + editForm.reset({ + name: currentConnector.name, + SLACK_BOT_TOKEN: currentConnector.config?.SLACK_BOT_TOKEN || "", + NOTION_INTEGRATION_TOKEN: currentConnector.config?.NOTION_INTEGRATION_TOKEN || "", + SERPER_API_KEY: currentConnector.config?.SERPER_API_KEY || "", + TAVILY_API_KEY: currentConnector.config?.TAVILY_API_KEY || "", + }); + + // If GitHub, set up GitHub-specific state + if (currentConnector.connector_type === 'GITHUB_CONNECTOR') { + const savedRepos = currentConnector.config?.repo_full_names || []; + const savedPat = currentConnector.config?.GITHUB_PAT || ""; + setCurrentSelectedRepos(savedRepos); + setNewSelectedRepos(savedRepos); + setOriginalPat(savedPat); + patForm.reset({ github_pat: savedPat }); + setEditMode('viewing'); // Start in viewing mode for repos + } } else { toast.error("Connector not found."); router.push(`/dashboard/${searchSpaceId}/connectors`); } } - }, [connectorId, connectors, connectorsLoading, router, searchSpaceId, connector]); // Removed editForm, patForm from dependencies + }, [connectorId, connectors, connectorsLoading, router, searchSpaceId, connector, editForm, patForm]); // Fetch repositories using the entered PAT const handleFetchRepositories = async (values: GithubPatFormValues) => { setIsFetchingRepos(true); setFetchedRepos(null); - // No need for patInputValue state, values.github_pat has the submitted value try { const token = localStorage.getItem('surfsense_bearer_token'); if (!token) throw new Error('No authentication token found'); @@ -137,21 +175,17 @@ export default function EditGithubConnectorPage() { body: JSON.stringify({ github_pat: values.github_pat }) } ); - if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || `Failed to fetch repositories: ${response.statusText}`); } - const data: GithubRepo[] = await response.json(); setFetchedRepos(data); - // Reset selection based on currently SAVED repos when fetching - setNewSelectedRepos(currentSelectedRepos); + setNewSelectedRepos(currentSelectedRepos); toast.success(`Found ${data.length} repositories. Select which ones to index.`); } catch (error) { console.error("Error fetching GitHub repositories:", error); toast.error(error instanceof Error ? error.message : "Failed to fetch repositories."); - // Don't clear PAT form on error, let user fix it } finally { setIsFetchingRepos(false); } @@ -166,62 +200,97 @@ export default function EditGithubConnectorPage() { ); }; - // Save all changes (name and potentially repo selection + PAT) + // Save changes - updated to handle different connector types const handleSaveChanges = async (formData: EditConnectorFormValues) => { - if (!connector) return; + if (!connector || !originalConfig) return; setIsSaving(true); const updatePayload: Partial = {}; let configChanged = false; + let newConfig: Record | null = null; // 1. Check if name changed if (formData.name !== connector.name) { updatePayload.name = formData.name; } - // 2. Check PAT and Repo changes - const currentPatInForm = patForm.getValues('github_pat'); - let patChanged = false; + // 2. Check for config changes based on connector type + switch (connector.connector_type) { + case 'GITHUB_CONNECTOR': + const currentPatInForm = patForm.getValues('github_pat'); + const patChanged = currentPatInForm !== originalPat; + const initialRepoSet = new Set(currentSelectedRepos); + const newRepoSet = new Set(newSelectedRepos); + const reposChanged = initialRepoSet.size !== newRepoSet.size || ![...initialRepoSet].every(repo => newRepoSet.has(repo)); - // Check if PAT input field was actually edited - if (editMode === 'editing_repos' && currentPatInForm !== originalPat) { - patChanged = true; + if (patChanged || (editMode === 'editing_repos' && reposChanged && fetchedRepos !== null)) { + if (!currentPatInForm || !(currentPatInForm.startsWith('ghp_') || currentPatInForm.startsWith('github_pat_'))) { + toast.error("Invalid GitHub PAT format. Cannot save."); + setIsSaving(false); return; + } + newConfig = { + GITHUB_PAT: currentPatInForm, + repo_full_names: newSelectedRepos, + }; + if (reposChanged && newSelectedRepos.length === 0) { + toast.warning("Warning: No repositories selected."); + } + } + break; + + case 'SLACK_CONNECTOR': + if (formData.SLACK_BOT_TOKEN !== originalConfig.SLACK_BOT_TOKEN) { + if (!formData.SLACK_BOT_TOKEN) { + toast.error("Slack Bot Token cannot be empty."); setIsSaving(false); return; + } + newConfig = { SLACK_BOT_TOKEN: formData.SLACK_BOT_TOKEN }; + } + break; + + case 'NOTION_CONNECTOR': + if (formData.NOTION_INTEGRATION_TOKEN !== originalConfig.NOTION_INTEGRATION_TOKEN) { + if (!formData.NOTION_INTEGRATION_TOKEN) { + toast.error("Notion Integration Token cannot be empty."); setIsSaving(false); return; + } + newConfig = { NOTION_INTEGRATION_TOKEN: formData.NOTION_INTEGRATION_TOKEN }; + } + break; + + case 'SERPER_API': + if (formData.SERPER_API_KEY !== originalConfig.SERPER_API_KEY) { + if (!formData.SERPER_API_KEY) { + toast.error("Serper API Key cannot be empty."); setIsSaving(false); return; + } + newConfig = { SERPER_API_KEY: formData.SERPER_API_KEY }; + } + break; + + case 'TAVILY_API': + if (formData.TAVILY_API_KEY !== originalConfig.TAVILY_API_KEY) { + if (!formData.TAVILY_API_KEY) { + toast.error("Tavily API Key cannot be empty."); setIsSaving(false); return; + } + newConfig = { TAVILY_API_KEY: formData.TAVILY_API_KEY }; + } + break; + + // Add cases for other connector types if necessary } - // Check if repo selection was modified - const initialRepoSet = new Set(currentSelectedRepos); - const newRepoSet = new Set(newSelectedRepos); - const reposChanged = initialRepoSet.size !== newRepoSet.size || ![...initialRepoSet].every(repo => newRepoSet.has(repo)); - - // If PAT was changed OR repos were changed (implying PAT was involved) - if (patChanged || (editMode === 'editing_repos' && reposChanged && fetchedRepos !== null)) { - // Validate the PAT from the form before including it - if (!currentPatInForm || !(currentPatInForm.startsWith('ghp_') || currentPatInForm.startsWith('github_pat_'))) { - toast.error("Invalid GitHub PAT format in the input field. Cannot save config changes."); - setIsSaving(false); - return; - } - - updatePayload.config = { - // Use the PAT value currently in the form field - GITHUB_PAT: currentPatInForm, - // Use the latest repo selection state - repo_full_names: newSelectedRepos, - }; - configChanged = true; // Mark config as changed - - if (reposChanged && newSelectedRepos.length === 0) { - toast.warning("Warning: You haven't selected any repositories. The connector won't index anything."); - } + // If config was determined to have changed, add it to the payload + if (newConfig !== null) { + updatePayload.config = newConfig; + configChanged = true; } // 3. Check if there are actual changes to save if (Object.keys(updatePayload).length === 0) { toast.info("No changes detected."); setIsSaving(false); - setEditMode('viewing'); - // Reset PAT form to original value if returning to view mode without saving PAT change - patForm.reset({ github_pat: originalPat }); + if (connector.connector_type === 'GITHUB_CONNECTOR') { + setEditMode('viewing'); + patForm.reset({ github_pat: originalPat }); + } return; } @@ -230,29 +299,38 @@ export default function EditGithubConnectorPage() { await updateConnector(connectorId, updatePayload); toast.success("Connector updated successfully!"); - // Update local state based on what was *actually* saved - if (updatePayload.config) { - setCurrentSelectedRepos(updatePayload.config.repo_full_names || []); - setOriginalPat(updatePayload.config.GITHUB_PAT || ""); - // Reset PAT form with the newly saved PAT - patForm.reset({ github_pat: updatePayload.config.GITHUB_PAT || "" }); - } else { - // If config wasn't in payload, ensure PAT form is reset to original value - patForm.reset({ github_pat: originalPat }); - } - // Update connector name state if it changed (or rely on hook refresh) + // Update local state after successful save + const newlySavedConfig = updatePayload.config || originalConfig; + setOriginalConfig(newlySavedConfig); if (updatePayload.name) { - setConnector(prev => prev ? { ...prev, name: updatePayload.name! } : null); + setConnector(prev => prev ? { ...prev, name: updatePayload.name!, config: newlySavedConfig } : null); + editForm.setValue('name', updatePayload.name); + } else { + setConnector(prev => prev ? { ...prev, config: newlySavedConfig } : null); } - // Reset edit state - setEditMode('viewing'); - setFetchedRepos(null); - // Reset working selection to match saved state (use the updated currentSelectedRepos) - setNewSelectedRepos(updatePayload.config?.repo_full_names || currentSelectedRepos); + if (connector.connector_type === 'GITHUB_CONNECTOR' && configChanged) { + const savedGitHubConfig = newlySavedConfig as { GITHUB_PAT?: string; repo_full_names?: string[] }; + setCurrentSelectedRepos(savedGitHubConfig.repo_full_names || []); + setOriginalPat(savedGitHubConfig.GITHUB_PAT || ""); + setNewSelectedRepos(savedGitHubConfig.repo_full_names || []); + patForm.reset({ github_pat: savedGitHubConfig.GITHUB_PAT || "" }); + } else if (connector.connector_type === 'SLACK_CONNECTOR' && configChanged) { + editForm.setValue('SLACK_BOT_TOKEN', newlySavedConfig.SLACK_BOT_TOKEN || ""); + } // Add similar blocks for Notion, Serper, Tavily + else if (connector.connector_type === 'NOTION_CONNECTOR' && configChanged) { + editForm.setValue('NOTION_INTEGRATION_TOKEN', newlySavedConfig.NOTION_INTEGRATION_TOKEN || ""); + } else if (connector.connector_type === 'SERPER_API' && configChanged) { + editForm.setValue('SERPER_API_KEY', newlySavedConfig.SERPER_API_KEY || ""); + } else if (connector.connector_type === 'TAVILY_API' && configChanged) { + editForm.setValue('TAVILY_API_KEY', newlySavedConfig.TAVILY_API_KEY || ""); + } - // Optionally redirect or rely on hook refresh - // router.push(`/dashboard/${searchSpaceId}/connectors`); + // Reset GitHub specific edit state + if (connector.connector_type === 'GITHUB_CONNECTOR') { + setEditMode('viewing'); + setFetchedRepos(null); + } } catch (error) { console.error("Error updating connector:", error); @@ -298,26 +376,27 @@ export default function EditGithubConnectorPage() { > - Edit GitHub Connector + {/* Title can be dynamic based on type */} + + {/* TODO: Make icon dynamic */} + Edit {getConnectorTypeDisplay(connector.connector_type)} Connector + - Modify the connector name and repository selections. To change repository selections, you need to re-enter your PAT. + Modify the connector name and configuration. - {/* Use editForm for the main form structure */}
- {/* Connector Name Field */} + {/* Name Field (Common) */} ( Connector Name - - - + )} @@ -325,108 +404,137 @@ export default function EditGithubConnectorPage() {
- {/* Repository Selection Section */} -
-

Repository Selection

+ {/* --- Conditional Configuration Section --- */} +

Configuration

- {editMode === 'viewing' && ( -
- Currently Indexed Repositories: - {currentSelectedRepos.length > 0 ? ( -
    - {currentSelectedRepos.map(repo =>
  • {repo}
  • )} -
- ) : ( -

(No repositories currently selected for indexing)

- )} - - Click "Change Selection" to re-enter your PAT and update the list. -
- )} - - {editMode === 'editing_repos' && ( -
- {/* PAT Input Section (No nested Form provider) */} - {/* We still use patForm fields but trigger validation manually */} -
- ( + {/* == GitHub == */} + {connector.connector_type === 'GITHUB_CONNECTOR' && ( +
+

Repository Selection & Access

+ {editMode === 'viewing' && ( +
+ Currently Indexed Repositories: + {currentSelectedRepos.length > 0 ? ( +
    + {currentSelectedRepos.map(repo =>
  • {repo}
  • )} +
+ ) : ( +

(No repositories currently selected)

+ )} + + To change repo selections or update the PAT, click above. +
+ )} + {editMode === 'editing_repos' && ( +
+ {/* PAT Input */} +
+ ( - Re-enter PAT to Fetch Repos - - - + GitHub PAT + + Enter PAT to fetch/update repos or if you need to update the stored token. - )} - /> - +
+ {/* Repo List */} + {isFetchingRepos && } + {!isFetchingRepos && fetchedRepos !== null && ( + fetchedRepos.length === 0 ? ( + No Repositories FoundCheck PAT & permissions. + ) : ( +
+ Select Repositories to Index ({newSelectedRepos.length} selected): +
+ {fetchedRepos.map((repo) => ( +
+ handleRepoSelectionChange(repo.full_name, !!checked)} /> + +
+ ))} +
+
+ ) + )} +
+ )} +
+ )} - {/* Fetched Repository List (shown after fetch) */} - {isFetchingRepos && } - {!isFetchingRepos && fetchedRepos !== null && ( - fetchedRepos.length === 0 ? ( - - - No Repositories Found - Check the PAT and permissions. - - ) : ( -
- Select Repositories to Index ({newSelectedRepos.length} selected): -
- {fetchedRepos.map((repo) => ( -
- handleRepoSelectionChange(repo.full_name, !!checked)} - /> - -
- ))} -
-
- ) - )} - -
- )} -
+ {/* == Slack == */} + {connector.connector_type === 'SLACK_CONNECTOR' && ( + ( + + Slack Bot Token + + Update the Slack Bot Token if needed. + + + )} + /> + )} + + {/* == Notion == */} + {connector.connector_type === 'NOTION_CONNECTOR' && ( + ( + + Notion Integration Token + + Update the Notion Integration Token if needed. + + + )} + /> + )} + + {/* == Serper API == */} + {connector.connector_type === 'SERPER_API' && ( + ( + + Serper API Key + + Update the Serper API Key if needed. + + + )} + /> + )} + + {/* == Tavily API == */} + {connector.connector_type === 'TAVILY_API' && ( + ( + + Tavily API Key + + Update the Tavily API Key if needed. + + + )} + /> + )} -