From 3ae8fe3a7ef95a33698b687278012b4e91114f5a Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 1 Jan 2026 21:41:31 +0530 Subject: [PATCH] refactor: simplify connector management and update dashboard layout - Removed the Connectors management page and integrated its functionality into a popup for better user experience. - Updated the DashboardLayout to reflect changes in navigation, removing references to the Connectors page. - Streamlined the breadcrumb component by eliminating unnecessary connector-related sections. - Enhanced the ConnectorIndicator to facilitate easier access to connector management features. - Improved overall UI consistency and accessibility across the dashboard components. --- .../connectors/(manage)/page.tsx | 857 ------------------ .../dashboard/[search_space_id]/layout.tsx | 40 +- .../assistant-ui/composer-action.tsx | 15 +- .../hooks/use-connector-dialog.ts | 26 +- .../tabs/all-connectors-tab.tsx | 20 +- .../components/dashboard-breadcrumb.tsx | 50 - .../hooks/use-connector-edit-page.ts | 56 +- 7 files changed, 65 insertions(+), 999 deletions(-) delete mode 100644 surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx deleted file mode 100644 index 614958018..000000000 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx +++ /dev/null @@ -1,857 +0,0 @@ -"use client"; - -import { format } from "date-fns"; -import { useAtomValue } from "jotai"; -import { - Calendar as CalendarIcon, - Clock, - Edit, - Folder, - HardDrive, - Info, - Loader2, - Plus, - RefreshCw, - Trash2, -} from "lucide-react"; -import { motion } from "motion/react"; -import { useParams, useRouter } from "next/navigation"; -import { useTranslations } from "next-intl"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; -import { - deleteConnectorMutationAtom, - indexConnectorMutationAtom, - updateConnectorMutationAtom, -} from "@/atoms/connectors/connector-mutation.atoms"; -import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { Calendar } from "@/components/ui/calendar"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { EnumConnectorName } from "@/contracts/enums/connector"; -import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; -import { cn } from "@/lib/utils"; -import { GoogleDriveFolderTree } from "@/components/connectors/google-drive-folder-tree"; - -export default function ConnectorsPage() { - const t = useTranslations("connectors"); - const tCommon = useTranslations("common"); - - // Helper function to format date with time - const formatDateTime = (dateString: string | null): string => { - if (!dateString) return t("never"); - - const date = new Date(dateString); - return new Intl.DateTimeFormat("en-US", { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }).format(date); - }; - const router = useRouter(); - const params = useParams(); - const searchSpaceId = params.search_space_id as string; - const today = new Date(); - - const { data: connectors = [], isLoading, error } = useAtomValue(connectorsAtom); - - const { mutateAsync: deleteConnector } = useAtomValue(deleteConnectorMutationAtom); - const { mutateAsync: indexConnector } = useAtomValue(indexConnectorMutationAtom); - const { mutateAsync: updateConnector } = useAtomValue(updateConnectorMutationAtom); - - const [connectorToDelete, setConnectorToDelete] = useState(null); - const [indexingConnectorId, setIndexingConnectorId] = useState(null); - const [datePickerOpen, setDatePickerOpen] = useState(false); - const [selectedConnectorForIndexing, setSelectedConnectorForIndexing] = useState( - null - ); - const [startDate, setStartDate] = useState(undefined); - const [endDate, setEndDate] = useState(undefined); - - // Periodic indexing state - const [periodicDialogOpen, setPeriodicDialogOpen] = useState(false); - const [selectedConnectorForPeriodic, setSelectedConnectorForPeriodic] = useState( - null - ); - const [periodicEnabled, setPeriodicEnabled] = useState(false); - const [frequencyMinutes, setFrequencyMinutes] = useState("1440"); - const [customFrequency, setCustomFrequency] = useState(""); - const [isSavingPeriodic, setIsSavingPeriodic] = useState(false); - - // Google Drive folder and file selection state - const [driveFolderDialogOpen, setDriveFolderDialogOpen] = useState(false); - const [selectedFolders, setSelectedFolders] = useState>([]); - const [selectedFiles, setSelectedFiles] = useState>([]); - - useEffect(() => { - if (error) { - toast.error(t("failed_load")); - console.error("Error fetching connectors:", error); - } - }, [error, t]); - - // Handle connector deletion - const handleDeleteConnector = async () => { - if (connectorToDelete === null) return; - - try { - await deleteConnector({ id: connectorToDelete }); - } catch (error) { - console.error("Error deleting connector:", error); - } finally { - setConnectorToDelete(null); - } - }; - - // Handle opening date picker for indexing - const handleOpenDatePicker = (connectorId: number) => { - // Check if this is a Google Drive connector - const connector = connectors.find((c) => c.id === connectorId); - if (connector?.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR) { - // Open folder selection dialog for Google Drive - handleOpenDriveFolderDialog(connectorId); - } else { - // Open date picker for other connectors - setSelectedConnectorForIndexing(connectorId); - setDatePickerOpen(true); - } - }; - - const handleOpenDriveFolderDialog = (connectorId: number) => { - setSelectedConnectorForIndexing(connectorId); - setDriveFolderDialogOpen(true); - }; - - // Handle Google Drive folder and file indexing - const handleIndexGoogleDrive = async () => { - if (selectedConnectorForIndexing === null || (selectedFolders.length === 0 && selectedFiles.length === 0)) { - toast.error("Please select at least one folder or file"); - return; - } - - setDriveFolderDialogOpen(false); - - try { - setIndexingConnectorId(selectedConnectorForIndexing); - - await indexConnector({ - connector_id: selectedConnectorForIndexing, - body: { - folders: selectedFolders, - files: selectedFiles, - }, - queryParams: { - search_space_id: searchSpaceId, - }, - }); - toast.success(t("indexing_started")); - } catch (error) { - console.error("Error indexing connector content:", error); - toast.error(error instanceof Error ? error.message : t("indexing_failed")); - } finally { - setIndexingConnectorId(null); - setSelectedConnectorForIndexing(null); - setSelectedFolders([]); - setSelectedFiles([]); - } - }; - - // Handle connector indexing with dates - const handleIndexConnector = async () => { - if (selectedConnectorForIndexing === null) return; - - setDatePickerOpen(false); - - try { - setIndexingConnectorId(selectedConnectorForIndexing); - const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined; - const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined; - - await indexConnector({ - connector_id: selectedConnectorForIndexing, - queryParams: { - search_space_id: searchSpaceId, - start_date: startDateStr, - end_date: endDateStr, - }, - }); - toast.success(t("indexing_started")); - } catch (error) { - console.error("Error indexing connector content:", error); - toast.error(error instanceof Error ? error.message : t("indexing_failed")); - } finally { - setIndexingConnectorId(null); - setSelectedConnectorForIndexing(null); - setStartDate(undefined); - setEndDate(undefined); - } - }; - - // Handle indexing without date picker (for quick indexing) - const handleQuickIndexConnector = async (connectorId: number) => { - setIndexingConnectorId(connectorId); - try { - await indexConnector({ - connector_id: connectorId, - queryParams: { - search_space_id: searchSpaceId, - }, - }); - toast.success(t("indexing_started")); - } catch (error) { - console.error("Error indexing connector content:", error); - toast.error(error instanceof Error ? error.message : t("indexing_failed")); - } finally { - setIndexingConnectorId(null); - } - }; - - // Handle opening periodic indexing dialog - const handleOpenPeriodicDialog = (connectorId: number) => { - const connector = connectors.find((c) => c.id === connectorId); - if (!connector) return; - - setSelectedConnectorForPeriodic(connectorId); - setPeriodicEnabled(connector.periodic_indexing_enabled); - - if (connector.indexing_frequency_minutes) { - // Check if it's a preset value - const presetValues = ["15", "60", "360", "720", "1440", "10080"]; - if (presetValues.includes(connector.indexing_frequency_minutes.toString())) { - setFrequencyMinutes(connector.indexing_frequency_minutes.toString()); - setCustomFrequency(""); - } else { - setFrequencyMinutes("custom"); - setCustomFrequency(connector.indexing_frequency_minutes.toString()); - } - } else { - setFrequencyMinutes("1440"); - setCustomFrequency(""); - } - - setPeriodicDialogOpen(true); - }; - - // Handle saving periodic indexing configuration - const handleSavePeriodicIndexing = async () => { - if (selectedConnectorForPeriodic === null) return; - - const connector = connectors.find((c) => c.id === selectedConnectorForPeriodic); - if (!connector) return; - - setIsSavingPeriodic(true); - try { - // Determine the frequency value - let frequency: number | null = null; - if (periodicEnabled) { - if (frequencyMinutes === "custom") { - frequency = parseInt(customFrequency, 10); - if (isNaN(frequency) || frequency <= 0) { - toast.error("Please enter a valid frequency in minutes"); - setIsSavingPeriodic(false); - return; - } - } else { - frequency = parseInt(frequencyMinutes, 10); - } - } - - await updateConnector({ - id: selectedConnectorForPeriodic, - data: { - periodic_indexing_enabled: periodicEnabled, - indexing_frequency_minutes: frequency, - }, - }); - - toast.success( - periodicEnabled - ? "Periodic indexing enabled successfully" - : "Periodic indexing disabled successfully" - ); - setPeriodicDialogOpen(false); - } catch (error) { - console.error("Error updating periodic indexing:", error); - toast.error(error instanceof Error ? error.message : "Failed to update periodic indexing"); - } finally { - setIsSavingPeriodic(false); - setSelectedConnectorForPeriodic(null); - } - }; - - // Format frequency for display - const formatFrequency = (minutes: number): string => { - if (minutes < 60) return `${minutes}m`; - if (minutes < 1440) return `${Math.floor(minutes / 60)}h`; - if (minutes < 10080) return `${Math.floor(minutes / 1440)}d`; - return `${Math.floor(minutes / 10080)}w`; - }; - - return ( -
- -
-

{t("title")}

-

{t("subtitle")}

-
- -
- - {isLoading ? ( -
-
-
-
-
-
- ) : connectors.length === 0 ? ( -
-

{t("no_connectors")}

-

{t("no_connectors_desc")}

- -
- ) : ( -
- - - - {t("name")} - {t("type")} - {t("last_indexed")} - {t("periodic")} - {t("actions")} - - - - {connectors.map((connector) => ( - - {connector.name} - {getConnectorIcon(connector.connector_type)} - - {connector.is_indexable - ? formatDateTime(connector.last_indexed_at) - : t("not_indexable")} - - - {connector.is_indexable ? ( - connector.periodic_indexing_enabled ? ( - - - -
- - - {connector.indexing_frequency_minutes - ? formatFrequency(connector.indexing_frequency_minutes) - : "Enabled"} - -
-
- -

- Runs every {connector.indexing_frequency_minutes} minutes - {connector.next_scheduled_at && ( - <> -
- Next: {formatDateTime(connector.next_scheduled_at)} - - )} -

-
-
-
- ) : ( - Disabled - ) - ) : ( - - - )} -
- -
- {connector.is_indexable && ( -
- - - - - - -

- {connector.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR - ? "Select folder to index" - : t("index_date_range")} -

-
-
-
- {/* Hide quick index button for Google Drive (requires folder selection) */} - {connector.connector_type !== EnumConnectorName.GOOGLE_DRIVE_CONNECTOR && ( - - - - - - -

{t("quick_index_auto")}

-
-
-
- )} -
- )} - {connector.is_indexable && ( - - - - - - -

Configure Periodic Indexing

-
-
-
- )} - - - - - - - - {t("delete_connector")} - - {t("delete_confirm")} - - - - setConnectorToDelete(null)}> - {tCommon("cancel")} - - - {tCommon("delete")} - - - - -
-
-
- ))} -
-
-
- )} - - {/* Date Picker Dialog */} - - - - {t("select_date_range")} - {t("select_date_range_desc")} - -
-
-
- - - - - - - - - -
-
- - - - - - - - - -
-
-
- - - -
-
- - - - -
-
- - {/* Google Drive Folder Selection Dialog */} - - - - Select Google Drive Folders & Files - - - - Select folders and/or individual files to index. For folders, only files directly in each folder will be - processed—subfolders must be selected separately. - - - -
-
- - {selectedConnectorForIndexing && ( - { - setSelectedFolders(folders); - }} - selectedFiles={selectedFiles} - onSelectFiles={(files) => { - setSelectedFiles(files); - }} - /> - )} -
- {(selectedFolders.length > 0 || selectedFiles.length > 0) && ( -
- {selectedFolders.length > 0 && ( -
-

- Selected {selectedFolders.length} folder{selectedFolders.length > 1 ? "s" : ""}: -

-
- {selectedFolders.map((folder) => ( -

- 📁 {folder.name} -

- ))} -
-
- )} - {selectedFiles.length > 0 && ( -
-

- Selected {selectedFiles.length} file{selectedFiles.length > 1 ? "s" : ""}: -

-
- {selectedFiles.map((file) => ( -

- 📄 {file.name} -

- ))} -
-
- )} -
- )} -
- - - - -
-
- - {/* Periodic Indexing Configuration Dialog */} - - - - Configure Periodic Indexing - - Set up automatic indexing at regular intervals for this connector. - - -
-
-
- -

- Automatically index this connector at regular intervals -

-
- -
- - {periodicEnabled && ( -
-
- - -
- - {frequencyMinutes === "custom" && ( -
- - setCustomFrequency(e.target.value)} - /> -

- Enter the number of minutes between each indexing run -

-
- )} - -
-

Preview:

-

- {frequencyMinutes === "custom" && customFrequency - ? `Will run every ${customFrequency} minutes` - : frequencyMinutes === "15" - ? "Will run every 15 minutes" - : frequencyMinutes === "60" - ? "Will run every hour" - : frequencyMinutes === "360" - ? "Will run every 6 hours" - : frequencyMinutes === "720" - ? "Will run every 12 hours" - : frequencyMinutes === "1440" - ? "Will run daily (every 24 hours)" - : frequencyMinutes === "10080" - ? "Will run weekly (every 7 days)" - : "Select a frequency above"} -

-
-
- )} -
- - - - -
-
-
- ); -} diff --git a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx index a44592ab2..afaa3abef 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx @@ -26,28 +26,24 @@ export default function DashboardLayout({ }, ]; - const customNavMain = [ - { - title: "Chat", - url: `/dashboard/${search_space_id}/new-chat`, - icon: "SquareTerminal", - items: [], - }, - { - title: "Sources", - url: "#", - icon: "Database", - items: [ - { - title: "Manage Documents", - url: `/dashboard/${search_space_id}/documents`, - }, - { - title: "Manage Connectors", - url: `/dashboard/${search_space_id}/connectors`, - }, - ], - }, + const customNavMain = [ + { + title: "Chat", + url: `/dashboard/${search_space_id}/new-chat`, + icon: "SquareTerminal", + items: [], + }, + { + title: "Sources", + url: "#", + icon: "Database", + items: [ + { + title: "Manage Documents", + url: `/dashboard/${search_space_id}/documents`, + }, + ], + }, { title: "Logs", url: `/dashboard/${search_space_id}/logs`, diff --git a/surfsense_web/components/assistant-ui/composer-action.tsx b/surfsense_web/components/assistant-ui/composer-action.tsx index 4692ddd82..d359342d1 100644 --- a/surfsense_web/components/assistant-ui/composer-action.tsx +++ b/surfsense_web/components/assistant-ui/composer-action.tsx @@ -1,7 +1,6 @@ import { AssistantIf, ComposerPrimitive, useAssistantState } from "@assistant-ui/react"; import { useAtomValue } from "jotai"; import { AlertCircle, ArrowUpIcon, Loader2, Plus, Plug2, SquareIcon } from "lucide-react"; -import Link from "next/link"; import type { FC } from "react"; import { useCallback, useMemo, useRef, useState } from "react"; import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; @@ -148,14 +147,15 @@ const ConnectorIndicator: FC = () => { )}
- {/* Connector popup should be opened via the connector indicator button */}} > Add more sources - +
) : ( @@ -164,13 +164,14 @@ const ConnectorIndicator: FC = () => {

Add documents or connect data sources to enhance search results.

- {/* Connector popup should be opened via the connector indicator button */}} > Add Connector - + )} diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index abfdab575..97f23d126 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -688,29 +688,7 @@ export const useConnectorDialog = () => { const handleStartEdit = useCallback((connector: SearchSourceConnector) => { if (!searchSpaceId) return; - // Check if this is an OAuth connector - const isOAuthConnector = OAUTH_CONNECTORS.some( - (oauthConnector) => oauthConnector.connectorType === connector.connector_type - ); - - // Check if this is webcrawler, Tavily API, SearxNG, Linkup, Baidu, Linear, Elasticsearch, Slack, Discord, or Notion (can be managed in popup) - const isWebcrawler = connector.connector_type === EnumConnectorName.WEBCRAWLER_CONNECTOR; - const isTavilyApi = connector.connector_type === EnumConnectorName.TAVILY_API; - const isSearxng = connector.connector_type === EnumConnectorName.SEARXNG_API; - const isLinkup = connector.connector_type === EnumConnectorName.LINKUP_API; - const isBaidu = connector.connector_type === EnumConnectorName.BAIDU_SEARCH_API; - const isLinear = connector.connector_type === EnumConnectorName.LINEAR_CONNECTOR; - const isElasticsearch = connector.connector_type === EnumConnectorName.ELASTICSEARCH_CONNECTOR; - const isSlack = connector.connector_type === EnumConnectorName.SLACK_CONNECTOR; - const isDiscord = connector.connector_type === EnumConnectorName.DISCORD_CONNECTOR; - const isNotion = connector.connector_type === EnumConnectorName.NOTION_CONNECTOR; - - // If not OAuth, not webcrawler, not Tavily API, not SearxNG, not Linkup, not Baidu, not Linear, not Elasticsearch, not Slack, not Discord, and not Notion, redirect to old connector edit page - if (!isOAuthConnector && !isWebcrawler && !isTavilyApi && !isSearxng && !isLinkup && !isBaidu && !isLinear && !isElasticsearch && !isSlack && !isDiscord && !isNotion) { - router.push(`/dashboard/${searchSpaceId}/connectors/${connector.id}/edit`); - return; - } - + // All connector types should be handled in the popup edit view // Validate connector data const connectorValidation = searchSourceConnector.safeParse(connector); if (!connectorValidation.success) { @@ -733,7 +711,7 @@ export const useConnectorDialog = () => { url.searchParams.set("view", "edit"); url.searchParams.set("connectorId", connector.id.toString()); window.history.pushState({ modal: true }, "", url.toString()); - }, [searchSpaceId, router]); + }, [searchSpaceId]); // Handle saving connector changes const handleSaveConnector = useCallback(async (refreshConnectors: () => void) => { diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx index 193e3adee..1f955dc2d 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx @@ -1,7 +1,6 @@ "use client"; -import { useRouter } from "next/navigation"; -import { type FC } from "react"; +import type { FC } from "react"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types"; import { OAUTH_CONNECTORS, CRAWLERS, OTHER_CONNECTORS } from "../constants/connector-constants"; @@ -39,8 +38,6 @@ export const AllConnectorsTab: FC = ({ onCreateYouTubeCrawler, onManage, }) => { - const router = useRouter(); - // Helper to find active task for a connector const getActiveTaskForConnector = (connectorId: number): LogActiveTask | undefined => { if (!logsSummary?.active_tasks) return undefined; @@ -148,9 +145,11 @@ export const AllConnectorsTab: FC = ({ : isWebcrawler && onCreateWebcrawler ? onCreateWebcrawler : crawler.connectorType && onConnectNonOAuth - ? () => onConnectNonOAuth(crawler.connectorType!) - : crawler.connectorType - ? () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${crawler.id}`) + ? () => { + if (crawler.connectorType) { + onConnectNonOAuth(crawler.connectorType); + } + } : () => {}; // Fallback for non-connector crawlers return ( @@ -186,7 +185,6 @@ export const AllConnectorsTab: FC = ({
{filteredOther.map((connector) => { // Special handling for connectors that can be created in popup - const isWebcrawler = connector.id === "webcrawler-connector"; const isTavily = connector.id === "tavily-api"; const isSearxng = connector.id === "searxng"; const isLinkup = connector.id === "linkup-api"; @@ -216,11 +214,9 @@ export const AllConnectorsTab: FC = ({ const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id); const activeTask = actualConnector ? getActiveTaskForConnector(actualConnector.id) : undefined; - const handleConnect = isWebcrawler && onCreateWebcrawler - ? onCreateWebcrawler - : (isTavily || isSearxng || isLinkup || isBaidu || isLinear || isElasticsearch || isSlack || isDiscord || isNotion || isConfluence || isBookStack || isGithub || isJira || isClickUp || isLuma || isCircleback) && onConnectNonOAuth + const handleConnect = (isTavily || isSearxng || isLinkup || isBaidu || isLinear || isElasticsearch || isSlack || isDiscord || isNotion || isConfluence || isBookStack || isGithub || isJira || isClickUp || isLuma || isCircleback) && onConnectNonOAuth ? () => onConnectNonOAuth(connector.connectorType) - : () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`); + : () => {}; // Fallback - connector popup should handle all connector types return ( = { "new-chat": t("chat") || "Chat", documents: t("documents"), - connectors: t("connectors"), logs: t("logs"), settings: t("settings"), editor: t("editor"), @@ -156,53 +155,6 @@ export function DashboardBreadcrumb() { return breadcrumbs; } - // Handle connector sub-sections - if (section === "connectors") { - // Handle specific connector types - if (subSection === "add" && segments[4]) { - const connectorType = segments[4]; - const connectorLabels: Record = { - "github-connector": "GitHub", - "jira-connector": "Jira", - "confluence-connector": "Confluence", - "bookstack-connector": "BookStack", - "discord-connector": "Discord", - "linear-connector": "Linear", - "clickup-connector": "ClickUp", - "slack-connector": "Slack", - "notion-connector": "Notion", - "tavily-api": "Tavily API", - "linkup-api": "LinkUp API", - "luma-connector": "Luma", - "elasticsearch-connector": "Elasticsearch", - }; - - const connectorLabel = connectorLabels[connectorType] || connectorType; - breadcrumbs.push({ - label: "Connectors", - href: `/dashboard/${segments[1]}/connectors`, - }); - breadcrumbs.push({ - label: "Add Connector", - href: `/dashboard/${segments[1]}/connectors/add`, - }); - breadcrumbs.push({ label: connectorLabel }); - return breadcrumbs; - } - - const connectorLabels: Record = { - add: t("add_connector"), - manage: t("manage_connectors"), - }; - - const connectorLabel = connectorLabels[subSection] || subSection; - breadcrumbs.push({ - label: t("connectors"), - href: `/dashboard/${segments[1]}/connectors`, - }); - breadcrumbs.push({ label: connectorLabel }); - return breadcrumbs; - } // Handle other sub-sections let subSectionLabel = subSection.charAt(0).toUpperCase() + subSection.slice(1); @@ -210,8 +162,6 @@ export function DashboardBreadcrumb() { upload: t("upload_documents"), youtube: t("add_youtube"), webpage: t("add_webpages"), - add: t("add_connector"), - edit: t("edit_connector"), manage: t("manage"), }; diff --git a/surfsense_web/hooks/use-connector-edit-page.ts b/surfsense_web/hooks/use-connector-edit-page.ts index 05f5abcc2..f08642503 100644 --- a/surfsense_web/hooks/use-connector-edit-page.ts +++ b/surfsense_web/hooks/use-connector-edit-page.ts @@ -16,6 +16,7 @@ import { } from "@/components/editConnector/types"; import type { EnumConnectorName } from "@/contracts/enums/connector"; import type { SearchSourceConnector } from "@/hooks/use-search-source-connectors"; +import type { UpdateConnectorResponse } from "@/contracts/types/connector.types"; import { authenticatedFetch } from "@/lib/auth-utils"; const normalizeListInput = (value: unknown): string[] => { @@ -57,7 +58,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) // State managed by the hook const [connector, setConnector] = useState(null); - const [originalConfig, setOriginalConfig] = useState | null>(null); + const [originalConfig, setOriginalConfig] = useState | null>(null); const [isSaving, setIsSaving] = useState(false); const [currentSelectedRepos, setCurrentSelectedRepos] = useState([]); const [originalPat, setOriginalPat] = useState(""); @@ -161,18 +162,18 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) } } else { toast.error("Connector not found."); - router.push(`/dashboard/${searchSpaceId}/connectors`); + router.push(`/dashboard/${searchSpaceId}`); } } }, [ - connectorId, - connectors, - connectorsLoading, - router, - searchSpaceId, - connector, - editForm, - patForm, + connectorId, + connectors, + connectorsLoading, + router, + searchSpaceId, + connector, editForm.reset, patForm.reset + // Note: editForm and patForm are intentionally excluded from dependencies + // to prevent infinite loops. They are stable form objects from react-hook-form. ]); // Handlers managed by the hook @@ -219,7 +220,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) setIsSaving(true); const updatePayload: Partial = {}; let configChanged = false; - let newConfig: Record | null = null; + let newConfig: Record | null = null; if (formData.name !== connector.name) { updatePayload.name = formData.name; @@ -296,12 +297,14 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) return; } - const candidateConfig: Record = { SEARXNG_HOST: host }; - let hasChanges = host !== (originalConfig.SEARXNG_HOST || "").trim(); + const candidateConfig: Record = { SEARXNG_HOST: host }; + const originalHost = typeof originalConfig.SEARXNG_HOST === "string" ? originalConfig.SEARXNG_HOST : ""; + let hasChanges = host !== originalHost.trim(); const apiKey = (formData.SEARXNG_API_KEY || "").trim(); - const originalApiKey = (originalConfig.SEARXNG_API_KEY || "").trim(); - if (apiKey !== originalApiKey) { + const originalApiKey = typeof originalConfig.SEARXNG_API_KEY === "string" ? originalConfig.SEARXNG_API_KEY : ""; + const originalApiKeyTrimmed = originalApiKey.trim(); + if (apiKey !== originalApiKeyTrimmed) { candidateConfig.SEARXNG_API_KEY = apiKey || null; hasChanges = true; } @@ -321,8 +324,9 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) } const language = (formData.SEARXNG_LANGUAGE || "").trim(); - const originalLanguage = (originalConfig.SEARXNG_LANGUAGE || "").trim(); - if (language !== originalLanguage) { + const originalLanguage = typeof originalConfig.SEARXNG_LANGUAGE === "string" ? originalConfig.SEARXNG_LANGUAGE : ""; + const originalLanguageTrimmed = originalLanguage.trim(); + if (language !== originalLanguageTrimmed) { candidateConfig.SEARXNG_LANGUAGE = language || null; hasChanges = true; } @@ -490,7 +494,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) ) { newConfig = {}; - if (formData.FIRECRAWL_API_KEY && formData.FIRECRAWL_API_KEY.trim()) { + if (formData.FIRECRAWL_API_KEY?.trim()) { if (!formData.FIRECRAWL_API_KEY.startsWith("fc-")) { toast.warning( "Firecrawl API keys typically start with 'fc-'. Please verify your key." @@ -504,7 +508,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) } if (formData.INITIAL_URLS !== undefined) { - if (formData.INITIAL_URLS && formData.INITIAL_URLS.trim()) { + if (formData.INITIAL_URLS?.trim()) { newConfig.INITIAL_URLS = formData.INITIAL_URLS.trim(); } else if (originalConfig.INITIAL_URLS) { toast.info("URLs removed from crawler configuration."); @@ -530,21 +534,19 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) } try { - await updateConnector({ + const updatedConnector = await updateConnector({ id: connectorId, data: { ...updatePayload, connector_type: connector.connector_type as EnumConnectorName, }, - }); + }) as UpdateConnectorResponse; toast.success("Connector updated!"); - const newlySavedConfig = updatePayload.config || originalConfig; + // Use the response from the API which has the full merged config + const newlySavedConfig = updatedConnector.config || originalConfig; setOriginalConfig(newlySavedConfig); - if (updatePayload.name) { - setConnector((prev) => - prev ? { ...prev, name: updatePayload.name!, config: newlySavedConfig } : null - ); - } + // Update connector state with the full updated connector from the API + setConnector(updatedConnector); if (configChanged) { if (connector.connector_type === "GITHUB_CONNECTOR") { const savedGitHubConfig = newlySavedConfig as {