From 32b4a09c0e803e1a2f66bbac8150da7108bb811b Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:07:15 +0530 Subject: [PATCH] feat: Refactor connector popup to include new components for active and all connectors, enhance indexing configuration view, and improve date range selection functionality. --- .../assistant-ui/connector-popup.tsx | 1022 ++--------------- .../connector-popup/active-connectors-tab.tsx | 152 +++ .../connector-popup/all-connectors-tab.tsx | 125 ++ .../connector-popup/connector-card.tsx | 66 ++ .../connector-popup/connector-constants.ts | 134 +++ .../connector-dialog-header.tsx | 89 ++ .../connector-popup/date-range-selector.tsx | 140 +++ .../assistant-ui/connector-popup/index.ts | 19 + .../indexing-configuration-view.tsx | 121 ++ .../connector-popup/periodic-sync-config.tsx | 65 ++ .../connector-popup/use-connector-dialog.ts | 295 +++++ 11 files changed, 1298 insertions(+), 930 deletions(-) create mode 100644 surfsense_web/components/assistant-ui/connector-popup/active-connectors-tab.tsx create mode 100644 surfsense_web/components/assistant-ui/connector-popup/all-connectors-tab.tsx create mode 100644 surfsense_web/components/assistant-ui/connector-popup/connector-card.tsx create mode 100644 surfsense_web/components/assistant-ui/connector-popup/connector-constants.ts create mode 100644 surfsense_web/components/assistant-ui/connector-popup/connector-dialog-header.tsx create mode 100644 surfsense_web/components/assistant-ui/connector-popup/date-range-selector.tsx create mode 100644 surfsense_web/components/assistant-ui/connector-popup/index.ts create mode 100644 surfsense_web/components/assistant-ui/connector-popup/indexing-configuration-view.tsx create mode 100644 surfsense_web/components/assistant-ui/connector-popup/periodic-sync-config.tsx create mode 100644 surfsense_web/components/assistant-ui/connector-popup/use-connector-dialog.ts diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index e55c13448..1e9e09869 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -1,197 +1,30 @@ "use client"; -import { format, subDays, subYears } from "date-fns"; import { useAtomValue } from "jotai"; -import { - ArrowLeft, - Cable, - Calendar as CalendarIcon, - Check, - ChevronRight, - Loader2, - Search, -} from "lucide-react"; -import Link from "next/link"; -import { useRouter, useSearchParams } from "next/navigation"; -import { type FC, useCallback, useEffect, useMemo, useState } from "react"; -import { toast } from "sonner"; -import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; -import { indexConnectorMutationAtom, updateConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms"; -import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; +import { Cable, Loader2 } from "lucide-react"; +import { type FC, useMemo } from "react"; import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms"; +import { useLogsSummary } from "@/hooks/use-logs"; +import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { Dialog, DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, } from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Calendar } from "@/components/ui/calendar"; -import { Label } from "@/components/ui/label"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { Switch } from "@/components/ui/switch"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { EnumConnectorName } from "@/contracts/enums/connector"; -import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; -import type { SearchSourceConnector } from "@/contracts/types/connector.types"; -import { useLogsSummary } from "@/hooks/use-logs"; -import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors"; -import { authenticatedFetch } from "@/lib/auth-utils"; -import { queryClient } from "@/lib/query-client/client"; -import { cacheKeys } from "@/lib/query-client/cache-keys"; -import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; -import { cn } from "@/lib/utils"; - -// Type for the indexing configuration state -interface IndexingConfigState { - connectorType: string; - connectorId: number; - connectorTitle: string; -} - -// OAuth Connectors (Quick Connect) -const OAUTH_CONNECTORS = [ - { - id: "google-gmail-connector", - title: "Gmail", - description: "Search through your emails", - connectorType: EnumConnectorName.GOOGLE_GMAIL_CONNECTOR, - authEndpoint: "/api/v1/auth/google/gmail/connector/add/", - }, - { - id: "google-calendar-connector", - title: "Google Calendar", - description: "Search through your events", - connectorType: EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR, - authEndpoint: "/api/v1/auth/google/calendar/connector/add/", - }, - { - id: "airtable-connector", - title: "Airtable", - description: "Search your Airtable bases", - connectorType: EnumConnectorName.AIRTABLE_CONNECTOR, - authEndpoint: "/api/v1/auth/airtable/connector/add/", - }, -]; - -// Non-OAuth Connectors -const OTHER_CONNECTORS = [ - { - id: "slack-connector", - title: "Slack", - description: "Search Slack messages", - connectorType: EnumConnectorName.SLACK_CONNECTOR, - }, - { - id: "discord-connector", - title: "Discord", - description: "Search Discord messages", - connectorType: EnumConnectorName.DISCORD_CONNECTOR, - }, - { - id: "notion-connector", - title: "Notion", - description: "Search Notion pages", - connectorType: EnumConnectorName.NOTION_CONNECTOR, - }, - { - id: "confluence-connector", - title: "Confluence", - description: "Search documentation", - connectorType: EnumConnectorName.CONFLUENCE_CONNECTOR, - }, - { - id: "bookstack-connector", - title: "BookStack", - description: "Search BookStack docs", - connectorType: EnumConnectorName.BOOKSTACK_CONNECTOR, - }, - { - id: "github-connector", - title: "GitHub", - description: "Search repositories", - connectorType: EnumConnectorName.GITHUB_CONNECTOR, - }, - { - id: "linear-connector", - title: "Linear", - description: "Search issues & projects", - connectorType: EnumConnectorName.LINEAR_CONNECTOR, - }, - { - id: "jira-connector", - title: "Jira", - description: "Search Jira issues", - connectorType: EnumConnectorName.JIRA_CONNECTOR, - }, - { - id: "clickup-connector", - title: "ClickUp", - description: "Search ClickUp tasks", - connectorType: EnumConnectorName.CLICKUP_CONNECTOR, - }, - { - id: "luma-connector", - title: "Luma", - description: "Search Luma events", - connectorType: EnumConnectorName.LUMA_CONNECTOR, - }, - { - id: "elasticsearch-connector", - title: "Elasticsearch", - description: "Search ES indexes", - connectorType: EnumConnectorName.ELASTICSEARCH_CONNECTOR, - }, - { - id: "webcrawler-connector", - title: "Web Pages", - description: "Crawl web content", - connectorType: EnumConnectorName.WEBCRAWLER_CONNECTOR, - }, - { - id: "tavily-api", - title: "Tavily AI", - description: "Search with Tavily", - connectorType: EnumConnectorName.TAVILY_API, - }, - { - id: "searxng", - title: "SearxNG", - description: "Search with SearxNG", - connectorType: EnumConnectorName.SEARXNG_API, - }, - { - id: "linkup-api", - title: "Linkup API", - description: "Search with Linkup", - connectorType: EnumConnectorName.LINKUP_API, - }, - { - id: "baidu-search-api", - title: "Baidu Search", - description: "Search with Baidu", - connectorType: EnumConnectorName.BAIDU_SEARCH_API, - }, -]; - import { Tabs, TabsContent, - TabsList, - TabsTrigger, } from "@/components/ui/tabs"; +import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; +import { cn } from "@/lib/utils"; +import { AllConnectorsTab } from "./connector-popup/all-connectors-tab"; +import { ActiveConnectorsTab } from "./connector-popup/active-connectors-tab"; +import { ConnectorDialogHeader } from "./connector-popup/connector-dialog-header"; +import { IndexingConfigurationView } from "./connector-popup/indexing-configuration-view"; +import { useConnectorDialog } from "./connector-popup/use-connector-dialog"; +import type { SearchSourceConnector } from "@/contracts/types/connector.types"; export const ConnectorIndicator: FC = () => { - const router = useRouter(); - const searchParams = useSearchParams(); const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const { connectors, isLoading: connectorsLoading, refreshConnectors } = useSearchSourceConnectors( false, @@ -199,37 +32,6 @@ export const ConnectorIndicator: FC = () => { ); const { data: documentTypeCounts, isLoading: documentTypesLoading } = useAtomValue(documentTypeCountsAtom); - const { data: allConnectors, refetch: refetchAllConnectors } = useAtomValue(connectorsAtom); - const { mutateAsync: indexConnector } = useAtomValue(indexConnectorMutationAtom); - const { mutateAsync: updateConnector } = useAtomValue(updateConnectorMutationAtom); - const [isOpen, setIsOpen] = useState(false); - const [activeTab, setActiveTab] = useState("all"); - const [connectingId, setConnectingId] = useState(null); - const [isScrolled, setIsScrolled] = useState(false); - const [searchQuery, setSearchQuery] = useState(""); - - // Indexing configuration state (shown after OAuth success) - const [indexingConfig, setIndexingConfig] = useState(null); - const [startDate, setStartDate] = useState(undefined); - const [endDate, setEndDate] = useState(undefined); - const [isStartingIndexing, setIsStartingIndexing] = useState(false); - - // Periodic indexing state - const [periodicEnabled, setPeriodicEnabled] = useState(false); - const [frequencyMinutes, setFrequencyMinutes] = useState("1440"); // Default: daily - - // Helper function to get frequency label - const getFrequencyLabel = useCallback((minutes: string): string => { - switch (minutes) { - case "15": return "15 minutes"; - case "60": return "hour"; - case "360": return "6 hours"; - case "720": return "12 hours"; - case "1440": return "day"; - case "10080": return "week"; - default: return `${minutes} minutes`; - } - }, []); // Track active indexing tasks const { summary: logsSummary } = useLogsSummary( @@ -248,7 +50,6 @@ export const ConnectorIndicator: FC = () => { logsSummary.active_tasks .filter((task) => task.source?.includes("connector_indexing")) .map((task) => { - // Extract connector ID from task metadata or source const match = task.source?.match(/connector[_-]?(\d+)/i); return match ? parseInt(match[1], 10) : null; }) @@ -258,172 +59,32 @@ export const ConnectorIndicator: FC = () => { const isLoading = connectorsLoading || documentTypesLoading; - // Synchronize state with URL query params - useEffect(() => { - const modalParam = searchParams.get("modal"); - const tabParam = searchParams.get("tab"); - const viewParam = searchParams.get("view"); - const connectorParam = searchParams.get("connector"); - - if (modalParam === "connectors") { - if (!isOpen) setIsOpen(true); - - // Detect tab from URL query param - if (tabParam === "active" || tabParam === "all") { - if (activeTab !== tabParam) setActiveTab(tabParam); - } - - // Restore indexing config view from URL if present (e.g., on page refresh) - if (viewParam === "configure" && connectorParam && !indexingConfig) { - const oauthConnector = OAUTH_CONNECTORS.find(c => c.id === connectorParam); - if (oauthConnector && allConnectors) { - const existingConnector = allConnectors.find( - (c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType - ); - if (existingConnector) { - setIndexingConfig({ - connectorType: oauthConnector.connectorType, - connectorId: existingConnector.id, - connectorTitle: oauthConnector.title, - }); - } - } - } - } else { - if (isOpen) setIsOpen(false); - } - }, [searchParams, isOpen, activeTab, indexingConfig, allConnectors]); - - // Detect OAuth success and transition to config view - useEffect(() => { - const success = searchParams.get("success"); - const connectorParam = searchParams.get("connector"); - const modalParam = searchParams.get("modal"); - - if (success === "true" && connectorParam && searchSpaceId && modalParam === "connectors") { - // Find the OAuth connector info - const oauthConnector = OAUTH_CONNECTORS.find(c => c.id === connectorParam); - if (oauthConnector) { - // Refetch connectors to get the newly created connector - refetchAllConnectors().then((result) => { - const newConnector = result.data?.find( - (c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType - ); - if (newConnector) { - setIndexingConfig({ - connectorType: oauthConnector.connectorType, - connectorId: newConnector.id, - connectorTitle: oauthConnector.title, - }); - setIsOpen(true); - // Update URL to reflect config view (replace success with view=configure) - const url = new URL(window.location.href); - url.searchParams.delete("success"); - url.searchParams.set("view", "configure"); - // Keep connector param for URL restoration - window.history.replaceState({}, "", url.toString()); - } - }); - } - } - }, [searchParams, searchSpaceId, refetchAllConnectors]); - - // Handle starting indexing - const handleStartIndexing = useCallback(async () => { - if (!indexingConfig || !searchSpaceId) return; - - setIsStartingIndexing(true); - try { - const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined; - const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined; - - // Update periodic indexing settings if enabled - if (periodicEnabled) { - const frequency = parseInt(frequencyMinutes, 10); - await updateConnector({ - id: indexingConfig.connectorId, - data: { - periodic_indexing_enabled: true, - indexing_frequency_minutes: frequency, - }, - }); - } - - await indexConnector({ - connector_id: indexingConfig.connectorId, - queryParams: { - search_space_id: searchSpaceId, - start_date: startDateStr, - end_date: endDateStr, - }, - }); - - toast.success(`${indexingConfig.connectorTitle} indexing started`, { - description: periodicEnabled - ? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutes)}.` - : "You can continue working while we sync your data.", - }); - - // Close the config view and reset state - setIndexingConfig(null); - setStartDate(undefined); - setEndDate(undefined); - setPeriodicEnabled(false); - setFrequencyMinutes("1440"); - - // Clear config-related URL params and switch to active tab - const url = new URL(window.location.href); - url.searchParams.delete("view"); - url.searchParams.delete("connector"); - url.searchParams.set("tab", "active"); - window.history.replaceState({}, "", url.toString()); - setActiveTab("active"); - - // Refresh connectors list - refreshConnectors(); - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); - } catch (error) { - console.error("Error starting indexing:", error); - toast.error("Failed to start indexing"); - } finally { - setIsStartingIndexing(false); - } - }, [indexingConfig, searchSpaceId, startDate, endDate, indexConnector, updateConnector, periodicEnabled, frequencyMinutes, refreshConnectors, getFrequencyLabel]); - - // Handle skipping indexing for now - const handleSkipIndexing = useCallback(() => { - setIndexingConfig(null); - setStartDate(undefined); - setEndDate(undefined); - setPeriodicEnabled(false); - setFrequencyMinutes("1440"); - - // Clear config-related URL params - const url = new URL(window.location.href); - url.searchParams.delete("view"); - url.searchParams.delete("connector"); - window.history.replaceState({}, "", url.toString()); - }, []); - - // Quick date range handlers - const handleLast30Days = useCallback(() => { - const today = new Date(); - setStartDate(subDays(today, 30)); - setEndDate(today); - }, []); - - const handleLastYear = useCallback(() => { - const today = new Date(); - setStartDate(subYears(today, 1)); - setEndDate(today); - }, []); - - const handleClearDates = useCallback(() => { - setStartDate(undefined); - setEndDate(undefined); - }, []); + // Use the custom hook for dialog state management + const { + isOpen, + activeTab, + connectingId, + isScrolled, + searchQuery, + indexingConfig, + startDate, + endDate, + isStartingIndexing, + periodicEnabled, + frequencyMinutes, + allConnectors, + setSearchQuery, + setStartDate, + setEndDate, + setPeriodicEnabled, + setFrequencyMinutes, + handleOpenChange, + handleTabChange, + handleScroll, + handleConnectOAuth, + handleStartIndexing, + handleSkipIndexing, + } = useConnectorDialog(); // Get document types that have documents in the search space const activeDocumentTypes = documentTypeCounts @@ -439,94 +100,6 @@ export const ConnectorIndicator: FC = () => { (allConnectors || []).map((c: SearchSourceConnector) => c.connector_type) ); - // Filter connectors based on search - const filteredOAuth = OAUTH_CONNECTORS.filter(c => - c.title.toLowerCase().includes(searchQuery.toLowerCase()) || - c.description.toLowerCase().includes(searchQuery.toLowerCase()) - ); - - const filteredOther = OTHER_CONNECTORS.filter(c => - c.title.toLowerCase().includes(searchQuery.toLowerCase()) || - c.description.toLowerCase().includes(searchQuery.toLowerCase()) - ); - - // Handle OAuth connection - const handleConnectOAuth = useCallback( - async (connector: (typeof OAUTH_CONNECTORS)[0]) => { - if (!searchSpaceId || !connector.authEndpoint) return; - - try { - setConnectingId(connector.id); - const response = await authenticatedFetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${connector.authEndpoint}?space_id=${searchSpaceId}`, - { method: "GET" } - ); - - if (!response.ok) { - throw new Error(`Failed to initiate ${connector.title} OAuth`); - } - - const data = await response.json(); - window.location.href = data.auth_url; - } catch (error) { - console.error(`Error connecting to ${connector.title}:`, error); - toast.error(`Failed to connect to ${connector.title}`); - } finally { - setConnectingId(null); - } - }, - [searchSpaceId] - ); - - const handleOpenChange = useCallback( - (open: boolean) => { - setIsOpen(open); - - if (open) { - // Add modal query params to current URL - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("tab", activeTab); - window.history.pushState({ modal: true }, "", url.toString()); - } else { - // Remove modal query params from URL - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("success"); - url.searchParams.delete("connector"); - url.searchParams.delete("view"); - window.history.pushState({ modal: false }, "", url.toString()); - setIsScrolled(false); - setSearchQuery(""); - // Reset indexing config when closing - if (!isStartingIndexing) { - setIndexingConfig(null); - setStartDate(undefined); - setEndDate(undefined); - setPeriodicEnabled(false); - setFrequencyMinutes("1440"); - } - } - }, - [activeTab, isStartingIndexing] - ); - - const handleTabChange = useCallback( - (value: string) => { - setActiveTab(value); - // Update tab query param - const url = new URL(window.location.href); - url.searchParams.set("tab", value); - window.history.replaceState({ modal: true }, "", url.toString()); - }, - [] - ); - - const handleScroll = useCallback((e: React.UIEvent) => { - setIsScrolled(e.currentTarget.scrollTop > 0); - }, []); - if (!searchSpaceId) return null; return ( @@ -559,477 +132,66 @@ export const ConnectorIndicator: FC = () => { )} - - {/* Indexing Configuration View - shown after OAuth success */} - {indexingConfig ? ( -
- {/* Fixed Header */} -
- {/* Back button */} - + + {/* Indexing Configuration View - shown after OAuth success */} + {indexingConfig ? ( + handleStartIndexing(refreshConnectors)} + onSkip={handleSkipIndexing} + /> + ) : ( + + {/* Header */} + - {/* Success header */} -
-
- -
-
-

- {indexingConfig.connectorTitle} Connected! -

-

- Configure when to start syncing your data -

-
-
-
+ {/* Content */} +
+
+
+ + + - {/* Scrollable Content */} -
-
-
-

Select Date Range

-

- Choose how far back you want to sync your data. You can always re-index later with different dates. -

- -
- {/* Start Date */} -
- - - - - - - date > new Date()} - /> - - -
- - {/* End Date */} -
- - - - - - - date > new Date() || (startDate ? date < startDate : false)} - /> - - -
-
- - {/* Quick date range buttons */} -
- - - -
-
- - {/* Periodic Indexing Configuration */} -
-
-
-

Enable Periodic Sync

-

- Automatically re-index at regular intervals -

-
- -
- - {periodicEnabled && ( -
-
- - -
-
- )} -
- - {/* Info box */} -
-
- {getConnectorIcon(indexingConfig.connectorType, "size-4")} -
-
-

Indexing runs in the background

-

- You can continue using SurfSense while we sync your data. Check the Active tab to see progress. -

-
-
-
-
- - {/* Fixed Footer - Action buttons */} -
- - -
-
- ) : ( - - {/* Header */} -
- - Connectors - - Search across all your apps and data in one place. - - - -
- - - All Connectors - - - - Active - - - {totalSourceCount > 0 && ( - - {totalSourceCount} - - )} - - - -
-
- - setSearchQuery(e.target.value)} +
+ {/* Bottom fade shadow */} +
-
- - {/* Content */} -
-
-
- - {/* Quick Connect */} - {filteredOAuth.length > 0 && ( -
-
-

Quick Connect

-
-
- {filteredOAuth.map((connector) => { - const isConnected = connectedTypes.has(connector.connectorType); - const isConnecting = connectingId === connector.id; - - return ( -
-
- {getConnectorIcon(connector.connectorType, "size-6")} -
-
-
- {connector.title} - {isConnected && ( - - )} -
-

- {isConnected ? "Connected" : connector.description} -

-
- -
- ); - })} -
-
- )} - - {/* More Integrations */} - {filteredOther.length > 0 && ( -
-
-

More Integrations

-
-
- {filteredOther.map((connector) => { - const isConnected = connectedTypes.has(connector.connectorType); - - return ( - -
- {getConnectorIcon(connector.connectorType, "size-6")} -
-
-
- {connector.title} - {isConnected && ( - - )} -
-

- {connector.description} -

-
- - - ); - })} -
-
- )} -
- - - {hasSources ? ( -
-
-

Currently Active

-
-
- {activeDocumentTypes.map(([docType, count]) => ( -
-
- {getConnectorIcon(docType, "size-6")} -
-
-

- {getDocumentTypeLabel(docType)} -

-

- {count as number} documents indexed -

-
-
- ))} - {connectors.map((connector) => { - const isIndexing = indexingConnectorIds.has(connector.id); - const activeTask = logsSummary?.active_tasks?.find( - (task) => task.source?.includes(`connector_${connector.id}`) || task.source?.includes(`connector-${connector.id}`) - ); - - return ( -
-
- {getConnectorIcon(connector.connector_type, "size-6")} -
-
-

- {connector.name} -

- {isIndexing ? ( -

- - Indexing... - {activeTask?.message && ( - - • {activeTask.message} - - )} -

- ) : ( -

- {connector.last_indexed_at - ? `Last indexed: ${format(new Date(connector.last_indexed_at), "MMM d, yyyy")}` - : "Never indexed"} -

- )} -
- -
- ); - })} -
-
- ) : ( -
-
- -
-

No active sources

-

- Connect your first service to start searching across all your data. -

- - Browse available connectors - -
- )} -
-
-
- {/* Bottom fade shadow */} -
-
- - )} + + )} ); diff --git a/surfsense_web/components/assistant-ui/connector-popup/active-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/active-connectors-tab.tsx new file mode 100644 index 000000000..323fa34e7 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/active-connectors-tab.tsx @@ -0,0 +1,152 @@ +"use client"; + +import { format } from "date-fns"; +import { Cable, Loader2 } from "lucide-react"; +import { useRouter } from "next/navigation"; +import type { FC } from "react"; +import { Button } from "@/components/ui/button"; +import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; +import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; +import type { SearchSourceConnector } from "@/contracts/types/connector.types"; +import { cn } from "@/lib/utils"; +import { + TabsContent, + TabsTrigger, +} from "@/components/ui/tabs"; + +interface ActiveConnectorsTabProps { + hasSources: boolean; + totalSourceCount: number; + activeDocumentTypes: Array<[string, number]>; + connectors: SearchSourceConnector[]; + indexingConnectorIds: Set; + logsSummary: any; + searchSpaceId: string; + onTabChange: (value: string) => void; +} + +export const ActiveConnectorsTab: FC = ({ + hasSources, + activeDocumentTypes, + connectors, + indexingConnectorIds, + logsSummary, + searchSpaceId, + onTabChange, +}) => { + const router = useRouter(); + + return ( + + {hasSources ? ( +
+
+

+ Currently Active +

+
+
+ {activeDocumentTypes.map(([docType, count]) => ( +
+
+ {getConnectorIcon(docType, "size-6")} +
+
+

+ {getDocumentTypeLabel(docType)} +

+

+ {count as number} documents indexed +

+
+
+ ))} + {connectors.map((connector) => { + const isIndexing = indexingConnectorIds.has(connector.id); + const activeTask = logsSummary?.active_tasks?.find( + (task: any) => + task.source?.includes(`connector_${connector.id}`) || + task.source?.includes(`connector-${connector.id}`) + ); + + return ( +
+
+ {getConnectorIcon(connector.connector_type, "size-6")} +
+
+

+ {connector.name} +

+ {isIndexing ? ( +

+ + Indexing... + {activeTask?.message && ( + + • {activeTask.message} + + )} +

+ ) : ( +

+ {connector.last_indexed_at + ? `Last indexed: ${format(new Date(connector.last_indexed_at), "MMM d, yyyy")}` + : "Never indexed"} +

+ )} +
+ +
+ ); + })} +
+
+ ) : ( +
+
+ +
+

No active sources

+

+ Connect your first service to start searching across all your data. +

+ onTabChange("all")}> + Browse available connectors + +
+ )} +
+ ); +}; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/all-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/all-connectors-tab.tsx new file mode 100644 index 000000000..e2f735b13 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/all-connectors-tab.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { ChevronRight } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { type FC } from "react"; +import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; +import { OAUTH_CONNECTORS, OTHER_CONNECTORS } from "./connector-constants"; +import { ConnectorCard } from "./connector-card"; + +interface AllConnectorsTabProps { + searchQuery: string; + searchSpaceId: string; + connectedTypes: Set; + connectingId: string | null; + onConnectOAuth: (connector: (typeof OAUTH_CONNECTORS)[0]) => void; +} + +export const AllConnectorsTab: FC = ({ + searchQuery, + searchSpaceId, + connectedTypes, + connectingId, + onConnectOAuth, +}) => { + const router = useRouter(); + + // Filter connectors based on search + const filteredOAuth = OAUTH_CONNECTORS.filter( + (c) => + c.title.toLowerCase().includes(searchQuery.toLowerCase()) || + c.description.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const filteredOther = OTHER_CONNECTORS.filter( + (c) => + c.title.toLowerCase().includes(searchQuery.toLowerCase()) || + c.description.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return ( +
+ {/* Quick Connect */} + {filteredOAuth.length > 0 && ( +
+
+

+ Quick Connect +

+
+
+ {filteredOAuth.map((connector) => { + const isConnected = connectedTypes.has(connector.connectorType); + const isConnecting = connectingId === connector.id; + + return ( + onConnectOAuth(connector)} + onManage={() => + router.push( + `/dashboard/${searchSpaceId}/connectors/add/${connector.id}` + ) + } + /> + ); + })} +
+
+ )} + + {/* More Integrations */} + {filteredOther.length > 0 && ( +
+
+

+ More Integrations +

+
+
+ {filteredOther.map((connector) => { + const isConnected = connectedTypes.has(connector.connectorType); + + return ( + +
+ {getConnectorIcon(connector.connectorType, "size-6")} +
+
+
+ + {connector.title} + + {isConnected && ( + + )} +
+

+ {connector.description} +

+
+ + + ); + })} +
+
+ )} +
+ ); +}; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-card.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-card.tsx new file mode 100644 index 000000000..d1f79b16d --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-card.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { Loader2 } from "lucide-react"; +import { type FC } from "react"; +import { Button } from "@/components/ui/button"; +import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; + +interface ConnectorCardProps { + id: string; + title: string; + description: string; + connectorType: string; + isConnected?: boolean; + isConnecting?: boolean; + onConnect?: () => void; + onManage?: () => void; +} + +export const ConnectorCard: FC = ({ + id, + title, + description, + connectorType, + isConnected = false, + isConnecting = false, + onConnect, + onManage, +}) => { + return ( +
+
+ {getConnectorIcon(connectorType, "size-6")} +
+
+
+ {title} + {isConnected && ( + + )} +
+

+ {isConnected ? "Connected" : description} +

+
+ +
+ ); +}; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/connector-constants.ts new file mode 100644 index 000000000..65d5bd516 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-constants.ts @@ -0,0 +1,134 @@ +import { EnumConnectorName } from "@/contracts/enums/connector"; + +// OAuth Connectors (Quick Connect) +export const OAUTH_CONNECTORS = [ + { + id: "google-gmail-connector", + title: "Gmail", + description: "Search through your emails", + connectorType: EnumConnectorName.GOOGLE_GMAIL_CONNECTOR, + authEndpoint: "/api/v1/auth/google/gmail/connector/add/", + }, + { + id: "google-calendar-connector", + title: "Google Calendar", + description: "Search through your events", + connectorType: EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR, + authEndpoint: "/api/v1/auth/google/calendar/connector/add/", + }, + { + id: "airtable-connector", + title: "Airtable", + description: "Search your Airtable bases", + connectorType: EnumConnectorName.AIRTABLE_CONNECTOR, + authEndpoint: "/api/v1/auth/airtable/connector/add/", + }, +] as const; + +// Non-OAuth Connectors +export const OTHER_CONNECTORS = [ + { + id: "slack-connector", + title: "Slack", + description: "Search Slack messages", + connectorType: EnumConnectorName.SLACK_CONNECTOR, + }, + { + id: "discord-connector", + title: "Discord", + description: "Search Discord messages", + connectorType: EnumConnectorName.DISCORD_CONNECTOR, + }, + { + id: "notion-connector", + title: "Notion", + description: "Search Notion pages", + connectorType: EnumConnectorName.NOTION_CONNECTOR, + }, + { + id: "confluence-connector", + title: "Confluence", + description: "Search documentation", + connectorType: EnumConnectorName.CONFLUENCE_CONNECTOR, + }, + { + id: "bookstack-connector", + title: "BookStack", + description: "Search BookStack docs", + connectorType: EnumConnectorName.BOOKSTACK_CONNECTOR, + }, + { + id: "github-connector", + title: "GitHub", + description: "Search repositories", + connectorType: EnumConnectorName.GITHUB_CONNECTOR, + }, + { + id: "linear-connector", + title: "Linear", + description: "Search issues & projects", + connectorType: EnumConnectorName.LINEAR_CONNECTOR, + }, + { + id: "jira-connector", + title: "Jira", + description: "Search Jira issues", + connectorType: EnumConnectorName.JIRA_CONNECTOR, + }, + { + id: "clickup-connector", + title: "ClickUp", + description: "Search ClickUp tasks", + connectorType: EnumConnectorName.CLICKUP_CONNECTOR, + }, + { + id: "luma-connector", + title: "Luma", + description: "Search Luma events", + connectorType: EnumConnectorName.LUMA_CONNECTOR, + }, + { + id: "elasticsearch-connector", + title: "Elasticsearch", + description: "Search ES indexes", + connectorType: EnumConnectorName.ELASTICSEARCH_CONNECTOR, + }, + { + id: "webcrawler-connector", + title: "Web Pages", + description: "Crawl web content", + connectorType: EnumConnectorName.WEBCRAWLER_CONNECTOR, + }, + { + id: "tavily-api", + title: "Tavily AI", + description: "Search with Tavily", + connectorType: EnumConnectorName.TAVILY_API, + }, + { + id: "searxng", + title: "SearxNG", + description: "Search with SearxNG", + connectorType: EnumConnectorName.SEARXNG_API, + }, + { + id: "linkup-api", + title: "Linkup API", + description: "Search with Linkup", + connectorType: EnumConnectorName.LINKUP_API, + }, + { + id: "baidu-search-api", + title: "Baidu Search", + description: "Search with Baidu", + connectorType: EnumConnectorName.BAIDU_SEARCH_API, + }, +] as const; + +// Type for the indexing configuration state +export interface IndexingConfigState { + connectorType: string; + connectorId: number; + connectorTitle: string; +} + diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-dialog-header.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-dialog-header.tsx new file mode 100644 index 000000000..d4126f2ce --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-dialog-header.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { Search } from "lucide-react"; +import { type FC } from "react"; +import { + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + TabsList, + TabsTrigger, +} from "@/components/ui/tabs"; +import { cn } from "@/lib/utils"; + +interface ConnectorDialogHeaderProps { + activeTab: string; + totalSourceCount: number; + searchQuery: string; + onTabChange: (value: string) => void; + onSearchChange: (query: string) => void; + isScrolled: boolean; +} + +export const ConnectorDialogHeader: FC = ({ + activeTab, + totalSourceCount, + searchQuery, + onTabChange, + onSearchChange, + isScrolled, +}) => { + return ( +
+ + + Connectors + + + Search across all your apps and data in one place. + + + +
+ + + All Connectors + + + + Active + + + {totalSourceCount > 0 && ( + + {totalSourceCount} + + )} + + + +
+
+ + onSearchChange(e.target.value)} + /> +
+
+
+
+ ); +}; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/date-range-selector.tsx b/surfsense_web/components/assistant-ui/connector-popup/date-range-selector.tsx new file mode 100644 index 000000000..1112f3f36 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/date-range-selector.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { format, subDays, subYears } from "date-fns"; +import { Calendar as CalendarIcon } from "lucide-react"; +import type { FC } from "react"; +import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; +import { Label } from "@/components/ui/label"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; + +interface DateRangeSelectorProps { + startDate: Date | undefined; + endDate: Date | undefined; + onStartDateChange: (date: Date | undefined) => void; + onEndDateChange: (date: Date | undefined) => void; +} + +export const DateRangeSelector: FC = ({ + startDate, + endDate, + onStartDateChange, + onEndDateChange, +}) => { + const handleLast30Days = () => { + const today = new Date(); + onStartDateChange(subDays(today, 30)); + onEndDateChange(today); + }; + + const handleLastYear = () => { + const today = new Date(); + onStartDateChange(subYears(today, 1)); + onEndDateChange(today); + }; + + const handleClearDates = () => { + onStartDateChange(undefined); + onEndDateChange(undefined); + }; + + return ( +
+

Select Date Range

+

+ Choose how far back you want to sync your data. You can always re-index later with different dates. +

+ +
+ {/* Start Date */} +
+ + + + + + + date > new Date()} + /> + + +
+ + {/* End Date */} +
+ + + + + + + date > new Date() || (startDate ? date < startDate : false)} + /> + + +
+
+ + {/* Quick date range buttons */} +
+ + + +
+
+ ); +}; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/index.ts b/surfsense_web/components/assistant-ui/connector-popup/index.ts new file mode 100644 index 000000000..da1d4639e --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/index.ts @@ -0,0 +1,19 @@ +// Main component export +export { ConnectorIndicator } from "../connector-popup"; + +// Sub-components (if needed for external use) +export { ConnectorCard } from "./connector-card"; +export { DateRangeSelector } from "./date-range-selector"; +export { PeriodicSyncConfig } from "./periodic-sync-config"; +export { IndexingConfigurationView } from "./indexing-configuration-view"; +export { ConnectorDialogHeader } from "./connector-dialog-header"; +export { AllConnectorsTab } from "./all-connectors-tab"; +export { ActiveConnectorsTab } from "./active-connectors-tab"; + +// Constants and types +export { OAUTH_CONNECTORS, OTHER_CONNECTORS } from "./connector-constants"; +export type { IndexingConfigState } from "./connector-constants"; + +// Hooks +export { useConnectorDialog } from "./use-connector-dialog"; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/indexing-configuration-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/indexing-configuration-view.tsx new file mode 100644 index 000000000..cb9c3b66b --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/indexing-configuration-view.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { ArrowLeft, Check, Loader2 } from "lucide-react"; +import { type FC } from "react"; +import { Button } from "@/components/ui/button"; +import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; +import type { IndexingConfigState } from "./connector-constants"; +import { DateRangeSelector } from "./date-range-selector"; +import { PeriodicSyncConfig } from "./periodic-sync-config"; + +interface IndexingConfigurationViewProps { + config: IndexingConfigState; + startDate: Date | undefined; + endDate: Date | undefined; + periodicEnabled: boolean; + frequencyMinutes: string; + isStartingIndexing: boolean; + onStartDateChange: (date: Date | undefined) => void; + onEndDateChange: (date: Date | undefined) => void; + onPeriodicEnabledChange: (enabled: boolean) => void; + onFrequencyChange: (frequency: string) => void; + onStartIndexing: () => void; + onSkip: () => void; +} + +export const IndexingConfigurationView: FC = ({ + config, + startDate, + endDate, + periodicEnabled, + frequencyMinutes, + isStartingIndexing, + onStartDateChange, + onEndDateChange, + onPeriodicEnabledChange, + onFrequencyChange, + onStartIndexing, + onSkip, +}) => { + return ( +
+ {/* Fixed Header */} +
+ {/* Back button */} + + + {/* Success header */} +
+
+ +
+
+

+ {config.connectorTitle} Connected! +

+

+ Configure when to start syncing your data +

+
+
+
+ + {/* Scrollable Content */} +
+
+ + + + + {/* Info box */} +
+
+ {getConnectorIcon(config.connectorType, "size-4")} +
+
+

Indexing runs in the background

+

+ You can continue using SurfSense while we sync your data. Check the Active tab to see progress. +

+
+
+
+
+ + {/* Fixed Footer - Action buttons */} +
+ + +
+
+ ); +}; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/periodic-sync-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/periodic-sync-config.tsx new file mode 100644 index 000000000..427a6ac86 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/periodic-sync-config.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { type FC } from "react"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface PeriodicSyncConfigProps { + enabled: boolean; + frequencyMinutes: string; + onEnabledChange: (enabled: boolean) => void; + onFrequencyChange: (frequency: string) => void; +} + +export const PeriodicSyncConfig: FC = ({ + enabled, + frequencyMinutes, + onEnabledChange, + onFrequencyChange, +}) => { + return ( +
+
+
+

Enable Periodic Sync

+

+ Automatically re-index at regular intervals +

+
+ +
+ + {enabled && ( +
+
+ + +
+
+ )} +
+ ); +}; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/use-connector-dialog.ts new file mode 100644 index 000000000..5a309c2ce --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/use-connector-dialog.ts @@ -0,0 +1,295 @@ +import { useAtomValue } from "jotai"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; +import { indexConnectorMutationAtom, updateConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms"; +import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; +import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; +import { authenticatedFetch } from "@/lib/auth-utils"; +import { queryClient } from "@/lib/query-client/client"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; +import { format } from "date-fns"; +import type { SearchSourceConnector } from "@/contracts/types/connector.types"; +import { OAUTH_CONNECTORS } from "./connector-constants"; +import type { IndexingConfigState } from "./connector-constants"; + +export const useConnectorDialog = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); + const { data: allConnectors, refetch: refetchAllConnectors } = useAtomValue(connectorsAtom); + const { mutateAsync: indexConnector } = useAtomValue(indexConnectorMutationAtom); + const { mutateAsync: updateConnector } = useAtomValue(updateConnectorMutationAtom); + + const [isOpen, setIsOpen] = useState(false); + const [activeTab, setActiveTab] = useState("all"); + const [connectingId, setConnectingId] = useState(null); + const [isScrolled, setIsScrolled] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [indexingConfig, setIndexingConfig] = useState(null); + const [startDate, setStartDate] = useState(undefined); + const [endDate, setEndDate] = useState(undefined); + const [isStartingIndexing, setIsStartingIndexing] = useState(false); + const [periodicEnabled, setPeriodicEnabled] = useState(false); + const [frequencyMinutes, setFrequencyMinutes] = useState("1440"); + + // Helper function to get frequency label + const getFrequencyLabel = useCallback((minutes: string): string => { + switch (minutes) { + case "15": return "15 minutes"; + case "60": return "hour"; + case "360": return "6 hours"; + case "720": return "12 hours"; + case "1440": return "day"; + case "10080": return "week"; + default: return `${minutes} minutes`; + } + }, []); + + // Synchronize state with URL query params + useEffect(() => { + const modalParam = searchParams.get("modal"); + const tabParam = searchParams.get("tab"); + const viewParam = searchParams.get("view"); + const connectorParam = searchParams.get("connector"); + + if (modalParam === "connectors") { + if (!isOpen) setIsOpen(true); + + if (tabParam === "active" || tabParam === "all") { + if (activeTab !== tabParam) setActiveTab(tabParam); + } + + if (viewParam === "configure" && connectorParam && !indexingConfig) { + const oauthConnector = OAUTH_CONNECTORS.find(c => c.id === connectorParam); + if (oauthConnector && allConnectors) { + const existingConnector = allConnectors.find( + (c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType + ); + if (existingConnector) { + setIndexingConfig({ + connectorType: oauthConnector.connectorType, + connectorId: existingConnector.id, + connectorTitle: oauthConnector.title, + }); + } + } + } + } else { + if (isOpen) setIsOpen(false); + } + }, [searchParams, isOpen, activeTab, indexingConfig, allConnectors]); + + // Detect OAuth success and transition to config view + useEffect(() => { + const success = searchParams.get("success"); + const connectorParam = searchParams.get("connector"); + const modalParam = searchParams.get("modal"); + + if (success === "true" && connectorParam && searchSpaceId && modalParam === "connectors") { + const oauthConnector = OAUTH_CONNECTORS.find(c => c.id === connectorParam); + if (oauthConnector) { + refetchAllConnectors().then((result) => { + const newConnector = result.data?.find( + (c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType + ); + if (newConnector) { + setIndexingConfig({ + connectorType: oauthConnector.connectorType, + connectorId: newConnector.id, + connectorTitle: oauthConnector.title, + }); + setIsOpen(true); + const url = new URL(window.location.href); + url.searchParams.delete("success"); + url.searchParams.set("view", "configure"); + window.history.replaceState({}, "", url.toString()); + } + }); + } + } + }, [searchParams, searchSpaceId, refetchAllConnectors]); + + // Handle OAuth connection + const handleConnectOAuth = useCallback( + async (connector: (typeof OAUTH_CONNECTORS)[0]) => { + if (!searchSpaceId || !connector.authEndpoint) return; + + try { + setConnectingId(connector.id); + const response = await authenticatedFetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${connector.authEndpoint}?space_id=${searchSpaceId}`, + { method: "GET" } + ); + + if (!response.ok) { + throw new Error(`Failed to initiate ${connector.title} OAuth`); + } + + const data = await response.json(); + window.location.href = data.auth_url; + } catch (error) { + console.error(`Error connecting to ${connector.title}:`, error); + toast.error(`Failed to connect to ${connector.title}`); + } finally { + setConnectingId(null); + } + }, + [searchSpaceId] + ); + + // Handle starting indexing + const handleStartIndexing = useCallback(async (refreshConnectors: () => void) => { + if (!indexingConfig || !searchSpaceId) return; + + setIsStartingIndexing(true); + try { + const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined; + const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined; + + if (periodicEnabled) { + const frequency = parseInt(frequencyMinutes, 10); + await updateConnector({ + id: indexingConfig.connectorId, + data: { + periodic_indexing_enabled: true, + indexing_frequency_minutes: frequency, + }, + }); + } + + await indexConnector({ + connector_id: indexingConfig.connectorId, + queryParams: { + search_space_id: searchSpaceId, + start_date: startDateStr, + end_date: endDateStr, + }, + }); + + toast.success(`${indexingConfig.connectorTitle} indexing started`, { + description: periodicEnabled + ? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutes)}.` + : "You can continue working while we sync your data.", + }); + + setIndexingConfig(null); + setStartDate(undefined); + setEndDate(undefined); + setPeriodicEnabled(false); + setFrequencyMinutes("1440"); + + const url = new URL(window.location.href); + url.searchParams.delete("view"); + url.searchParams.delete("connector"); + url.searchParams.set("tab", "active"); + window.history.replaceState({}, "", url.toString()); + setActiveTab("active"); + + refreshConnectors(); + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), + }); + } catch (error) { + console.error("Error starting indexing:", error); + toast.error("Failed to start indexing"); + } finally { + setIsStartingIndexing(false); + } + }, [indexingConfig, searchSpaceId, startDate, endDate, indexConnector, updateConnector, periodicEnabled, frequencyMinutes, getFrequencyLabel]); + + // Handle skipping indexing + const handleSkipIndexing = useCallback(() => { + setIndexingConfig(null); + setStartDate(undefined); + setEndDate(undefined); + setPeriodicEnabled(false); + setFrequencyMinutes("1440"); + + const url = new URL(window.location.href); + url.searchParams.delete("view"); + url.searchParams.delete("connector"); + window.history.replaceState({}, "", url.toString()); + }, []); + + // Handle dialog open/close + const handleOpenChange = useCallback( + (open: boolean) => { + setIsOpen(open); + + if (open) { + const url = new URL(window.location.href); + url.searchParams.set("modal", "connectors"); + url.searchParams.set("tab", activeTab); + window.history.pushState({ modal: true }, "", url.toString()); + } else { + const url = new URL(window.location.href); + url.searchParams.delete("modal"); + url.searchParams.delete("tab"); + url.searchParams.delete("success"); + url.searchParams.delete("connector"); + url.searchParams.delete("view"); + window.history.pushState({ modal: false }, "", url.toString()); + setIsScrolled(false); + setSearchQuery(""); + if (!isStartingIndexing) { + setIndexingConfig(null); + setStartDate(undefined); + setEndDate(undefined); + setPeriodicEnabled(false); + setFrequencyMinutes("1440"); + } + } + }, + [activeTab, isStartingIndexing] + ); + + // Handle tab change + const handleTabChange = useCallback( + (value: string) => { + setActiveTab(value); + const url = new URL(window.location.href); + url.searchParams.set("tab", value); + window.history.replaceState({ modal: true }, "", url.toString()); + }, + [] + ); + + // Handle scroll + const handleScroll = useCallback((e: React.UIEvent) => { + setIsScrolled(e.currentTarget.scrollTop > 0); + }, []); + + return { + // State + isOpen, + activeTab, + connectingId, + isScrolled, + searchQuery, + indexingConfig, + startDate, + endDate, + isStartingIndexing, + periodicEnabled, + frequencyMinutes, + searchSpaceId, + allConnectors, + + // Setters + setSearchQuery, + setStartDate, + setEndDate, + setPeriodicEnabled, + setFrequencyMinutes, + + // Handlers + handleOpenChange, + handleTabChange, + handleScroll, + handleConnectOAuth, + handleStartIndexing, + handleSkipIndexing, + }; +}; +