diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 8fb1e7652..c5e996c4c 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -19,9 +19,11 @@ import { ConnectorDialogHeader } from "./connector-popup/components/connector-di import { ConnectorConnectView } from "./connector-popup/connector-configs/views/connector-connect-view"; import { ConnectorEditView } from "./connector-popup/connector-configs/views/connector-edit-view"; import { IndexingConfigurationView } from "./connector-popup/connector-configs/views/indexing-configuration-view"; +import { OAUTH_CONNECTORS } from "./connector-popup/constants/connector-constants"; import { useConnectorDialog } from "./connector-popup/hooks/use-connector-dialog"; import { ActiveConnectorsTab } from "./connector-popup/tabs/active-connectors-tab"; import { AllConnectorsTab } from "./connector-popup/tabs/all-connectors-tab"; +import { ConnectorAccountsListView } from "./connector-popup/views/connector-accounts-list-view"; import { YouTubeCrawlerView } from "./connector-popup/views/youtube-crawler-view"; export const ConnectorIndicator: FC = () => { @@ -60,6 +62,7 @@ export const ConnectorIndicator: FC = () => { periodicEnabled, frequencyMinutes, allConnectors, + viewingAccountsType, setSearchQuery, setStartDate, setEndDate, @@ -81,6 +84,9 @@ export const ConnectorIndicator: FC = () => { handleBackFromEdit, handleBackFromConnect, handleBackFromYouTube, + handleViewAccountsList, + handleBackFromAccountsList, + handleAddAccountOAuth, handleQuickIndexConnector, connectorConfig, setConnectorConfig, @@ -193,6 +199,26 @@ export const ConnectorIndicator: FC = () => { {/* YouTube Crawler View - shown when adding YouTube videos */} {isYouTubeView && searchSpaceId ? ( + ) : viewingAccountsType ? ( + { + const oauthConnector = OAUTH_CONNECTORS.find( + (c) => c.connectorType === viewingAccountsType.connectorType + ); + if (oauthConnector) { + handleAddAccountOAuth(oauthConnector); + } + }} + isConnecting={connectingId !== null} + /> ) : connectingConnectorType ? ( { onCreateWebcrawler={handleCreateWebcrawler} onCreateYouTubeCrawler={handleCreateYouTubeCrawler} onManage={handleStartEdit} + onViewAccountsList={handleViewAccountsList} /> diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts index 65456689c..808c7b428 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts @@ -7,7 +7,7 @@ import { searchSourceConnectorTypeEnum } from "@/contracts/types/connector.types export const connectorPopupQueryParamsSchema = z.object({ modal: z.enum(["connectors"]).optional(), tab: z.enum(["all", "active"]).optional(), - view: z.enum(["configure", "edit", "connect", "youtube"]).optional(), + view: z.enum(["configure", "edit", "connect", "youtube", "accounts"]).optional(), connector: z.string().optional(), connectorId: z.string().optional(), connectorType: z.string().optional(), 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 8ddaa973a..a9d4871e1 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 @@ -66,6 +66,12 @@ export const useConnectorDialog = () => { const [isCreatingConnector, setIsCreatingConnector] = useState(false); const isCreatingConnectorRef = useRef(false); + // Accounts list view state (for OAuth connectors with multiple accounts) + const [viewingAccountsType, setViewingAccountsType] = useState<{ + connectorType: string; + connectorTitle: string; + } | null>(null); + // Helper function to get frequency label const getFrequencyLabel = useCallback((minutes: string): string => { switch (minutes) { @@ -114,11 +120,29 @@ export const useConnectorDialog = () => { setConnectingConnectorType(null); } + // Clear viewing accounts type if view is not "accounts" anymore + if (params.view !== "accounts" && viewingAccountsType) { + setViewingAccountsType(null); + } + // Handle connect view if (params.view === "connect" && params.connectorType && !connectingConnectorType) { setConnectingConnectorType(params.connectorType); } + // Handle accounts view + if (params.view === "accounts" && params.connectorType && !viewingAccountsType) { + const oauthConnector = OAUTH_CONNECTORS.find( + (c) => c.connectorType === params.connectorType + ); + if (oauthConnector) { + setViewingAccountsType({ + connectorType: oauthConnector.connectorType, + connectorTitle: oauthConnector.title, + }); + } + } + // Handle YouTube view if (params.view === "youtube") { // YouTube view is active - no additional state needed @@ -200,6 +224,10 @@ export const useConnectorDialog = () => { if (connectingConnectorType) { setConnectingConnectorType(null); } + // Clear viewing accounts type when modal is closed + if (viewingAccountsType) { + setViewingAccountsType(null); + } // Clear YouTube view when modal is closed (handled by view param check) } } catch (error) { @@ -207,7 +235,7 @@ export const useConnectorDialog = () => { console.warn("Invalid connector popup query params:", error); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchParams, allConnectors, editingConnector, indexingConfig, connectingConnectorType]); + }, [searchParams, allConnectors, editingConnector, indexingConfig, connectingConnectorType, viewingAccountsType]); // Detect OAuth success and transition to config view useEffect(() => { @@ -632,6 +660,71 @@ export const useConnectorDialog = () => { router.replace(url.pathname + url.search, { scroll: false }); }, [router]); + // Handle viewing accounts list for OAuth connector type + const handleViewAccountsList = useCallback( + (connector: (typeof OAUTH_CONNECTORS)[number]) => { + if (!searchSpaceId) return; + + setViewingAccountsType({ + connectorType: connector.connectorType, + connectorTitle: connector.title, + }); + + // Update URL to show accounts view + const url = new URL(window.location.href); + url.searchParams.set("modal", "connectors"); + url.searchParams.set("view", "accounts"); + url.searchParams.set("connectorType", connector.connectorType); + window.history.pushState({ modal: true }, "", url.toString()); + }, + [searchSpaceId] + ); + + // Handle going back from accounts list view + const handleBackFromAccountsList = useCallback(() => { + setViewingAccountsType(null); + const url = new URL(window.location.href); + url.searchParams.set("modal", "connectors"); + url.searchParams.set("tab", "all"); + url.searchParams.delete("view"); + url.searchParams.delete("connectorType"); + router.replace(url.pathname + url.search, { scroll: false }); + }, [router]); + + // Handle adding a new account for OAuth connector (from accounts list view) + const handleAddAccountOAuth = useCallback( + async (connector: (typeof OAUTH_CONNECTORS)[number]) => { + if (!searchSpaceId || !connector.authEndpoint) return; + + // Set connecting state + setConnectingId(connector.id); + + try { + 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(); + const validatedData = parseOAuthAuthResponse(data); + window.location.href = validatedData.auth_url; + } catch (error) { + console.error(`Error connecting to ${connector.title}:`, error); + if (error instanceof Error && error.message.includes("Invalid auth URL")) { + toast.error(`Invalid response from ${connector.title} OAuth endpoint`); + } else { + toast.error(`Failed to connect to ${connector.title}`); + } + setConnectingId(null); + } + }, + [searchSpaceId] + ); + // Handle starting indexing const handleStartIndexing = useCallback( async (refreshConnectors: () => void) => { @@ -1081,6 +1174,7 @@ export const useConnectorDialog = () => { setConnectorName(null); setConnectorConfig(null); setConnectingConnectorType(null); + setViewingAccountsType(null); setStartDate(undefined); setEndDate(undefined); setPeriodicEnabled(false); @@ -1126,6 +1220,7 @@ export const useConnectorDialog = () => { frequencyMinutes, searchSpaceId, allConnectors, + viewingAccountsType, // Setters setSearchQuery, @@ -1152,6 +1247,9 @@ export const useConnectorDialog = () => { handleBackFromEdit, handleBackFromConnect, handleBackFromYouTube, + handleViewAccountsList, + handleBackFromAccountsList, + handleAddAccountOAuth, handleQuickIndexConnector, connectorConfig, setConnectorConfig, 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 0be4e7e87..5356a2afd 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 @@ -6,7 +6,11 @@ import { Button } from "@/components/ui/button"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types"; import { ConnectorCard } from "../components/connector-card"; -import { CRAWLERS, OAUTH_CONNECTORS, OTHER_CONNECTORS } from "../constants/connector-constants"; +import { + CRAWLERS, + OAUTH_CONNECTORS, + OTHER_CONNECTORS, +} from "../constants/connector-constants"; import { getDocumentCountForConnector } from "../utils/connector-document-mapping"; /** @@ -15,271 +19,277 @@ import { getDocumentCountForConnector } from "../utils/connector-document-mappin * Returns just the identifier (e.g : john@example.com). */ export function getConnectorDisplayName(fullName: string): string { - const separatorIndex = fullName.indexOf(" - "); - if (separatorIndex !== -1) { - return fullName.substring(separatorIndex + 3); - } - return fullName; + const separatorIndex = fullName.indexOf(" - "); + if (separatorIndex !== -1) { + return fullName.substring(separatorIndex + 3); + } + return fullName; } interface AllConnectorsTabProps { - searchQuery: string; - searchSpaceId: string; - connectedTypes: Set; - connectingId: string | null; - allConnectors: SearchSourceConnector[] | undefined; - documentTypeCounts?: Record; - indexingConnectorIds?: Set; - logsSummary?: LogSummary; - onConnectOAuth: (connector: (typeof OAUTH_CONNECTORS)[number]) => void; - onConnectNonOAuth?: (connectorType: string) => void; - onCreateWebcrawler?: () => void; - onCreateYouTubeCrawler?: () => void; - onManage?: (connector: SearchSourceConnector) => void; + searchQuery: string; + searchSpaceId: string; + connectedTypes: Set; + connectingId: string | null; + allConnectors: SearchSourceConnector[] | undefined; + documentTypeCounts?: Record; + indexingConnectorIds?: Set; + logsSummary?: LogSummary; + onConnectOAuth: (connector: (typeof OAUTH_CONNECTORS)[number]) => void; + onConnectNonOAuth?: (connectorType: string) => void; + onCreateWebcrawler?: () => void; + onCreateYouTubeCrawler?: () => void; + onManage?: (connector: SearchSourceConnector) => void; + onViewAccountsList?: (connector: (typeof OAUTH_CONNECTORS)[number]) => void; } export const AllConnectorsTab: FC = ({ - searchQuery, - searchSpaceId, - connectedTypes, - connectingId, - allConnectors, - documentTypeCounts, - indexingConnectorIds, - logsSummary, - onConnectOAuth, - onConnectNonOAuth, - onCreateWebcrawler, - onCreateYouTubeCrawler, - onManage, + searchQuery, + searchSpaceId, + connectedTypes, + connectingId, + allConnectors, + documentTypeCounts, + indexingConnectorIds, + logsSummary, + onConnectOAuth, + onConnectNonOAuth, + onCreateWebcrawler, + onCreateYouTubeCrawler, + onManage, + onViewAccountsList, }) => { - // Helper to find active task for a connector - const getActiveTaskForConnector = (connectorId: number): LogActiveTask | undefined => { - if (!logsSummary?.active_tasks) return undefined; - return logsSummary.active_tasks.find( - (task: LogActiveTask) => task.connector_id === connectorId - ); - }; + // Helper to find active task for a connector + const getActiveTaskForConnector = ( + connectorId: number + ): LogActiveTask | undefined => { + if (!logsSummary?.active_tasks) return undefined; + return logsSummary.active_tasks.find( + (task: LogActiveTask) => task.connector_id === connectorId + ); + }; - // Filter connectors based on search - const filteredOAuth = OAUTH_CONNECTORS.filter( - (c) => - c.title.toLowerCase().includes(searchQuery.toLowerCase()) || - c.description.toLowerCase().includes(searchQuery.toLowerCase()) - ); + // Filter connectors based on search + const filteredOAuth = OAUTH_CONNECTORS.filter( + (c) => + c.title.toLowerCase().includes(searchQuery.toLowerCase()) || + c.description.toLowerCase().includes(searchQuery.toLowerCase()) + ); - const filteredCrawlers = CRAWLERS.filter( - (c) => - c.title.toLowerCase().includes(searchQuery.toLowerCase()) || - c.description.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const filteredCrawlers = CRAWLERS.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()) - ); + const filteredOther = OTHER_CONNECTORS.filter( + (c) => + c.title.toLowerCase().includes(searchQuery.toLowerCase()) || + c.description.toLowerCase().includes(searchQuery.toLowerCase()) + ); - return ( -
- {/* Per-Type OAuth Connector Groups */} - {filteredOAuth.map((connectorType) => { - const userConnectors = - allConnectors?.filter( - (c: SearchSourceConnector) => c.connector_type === connectorType.connectorType - ) || []; - const isConnecting = connectingId === connectorType.id; + return ( +
+ {/* Quick Connect */} + {filteredOAuth.length > 0 && ( +
+
+

+ Quick Connect +

+
+
+ {filteredOAuth.map((connector) => { + const isConnected = connectedTypes.has(connector.connectorType); + const isConnecting = connectingId === connector.id; + // Find the actual connector object if connected + const actualConnector = + isConnected && allConnectors + ? allConnectors.find( + (c: SearchSourceConnector) => + c.connector_type === connector.connectorType + ) + : undefined; - return ( -
- {/* Group Header */} -
-

- {connectorType.title} Integrations -

- {userConnectors.length > 0 && ( - - )} -
+ const documentCount = getDocumentCountForConnector( + connector.connectorType, + documentTypeCounts + ); + const isIndexing = + actualConnector && + indexingConnectorIds?.has(actualConnector.id); + const activeTask = actualConnector + ? getActiveTaskForConnector(actualConnector.id) + : undefined; -
- {userConnectors.length === 0 ? ( - onConnectOAuth(connectorType)} - /> - ) : ( - userConnectors.map((connector: SearchSourceConnector) => { - const documentCount = getDocumentCountForConnector( - connector.connector_type, - documentTypeCounts - ); - const isIndexing = indexingConnectorIds?.has(connector.id); - const activeTask = getActiveTaskForConnector(connector.id); + return ( + onConnectOAuth(connector)} + onManage={ + isConnected && onViewAccountsList + ? () => onViewAccountsList(connector) + : undefined + } + /> + ); + })} +
+
+ )} - return ( - onConnectOAuth(connectorType)} - onManage={onManage ? () => onManage(connector) : undefined} - /> - ); - }) - )} -
-
- ); - })} + {/* More Integrations */} + {filteredOther.length > 0 && ( +
+
+

+ More Integrations +

+
+
+ {filteredOther.map((connector) => { + const isConnected = connectedTypes.has(connector.connectorType); + const isConnecting = connectingId === connector.id; - {/* More Integrations */} - {filteredOther.length > 0 && ( -
-
-

More Integrations

-
-
- {filteredOther.map((connector) => { - const isConnected = connectedTypes.has(connector.connectorType); - const isConnecting = connectingId === connector.id; + // Find the actual connector object if connected + const actualConnector = + isConnected && allConnectors + ? allConnectors.find( + (c: SearchSourceConnector) => + c.connector_type === connector.connectorType + ) + : undefined; - // Find the actual connector object if connected - const actualConnector = - isConnected && allConnectors - ? allConnectors.find( - (c: SearchSourceConnector) => c.connector_type === connector.connectorType - ) - : undefined; + const documentCount = getDocumentCountForConnector( + connector.connectorType, + documentTypeCounts + ); + const isIndexing = + actualConnector && + indexingConnectorIds?.has(actualConnector.id); + const activeTask = actualConnector + ? getActiveTaskForConnector(actualConnector.id) + : undefined; - const documentCount = getDocumentCountForConnector( - connector.connectorType, - documentTypeCounts - ); - const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id); - const activeTask = actualConnector - ? getActiveTaskForConnector(actualConnector.id) - : undefined; + const handleConnect = onConnectNonOAuth + ? () => onConnectNonOAuth(connector.connectorType) + : () => {}; // Fallback - connector popup should handle all connector types - const handleConnect = onConnectNonOAuth - ? () => onConnectNonOAuth(connector.connectorType) - : () => {}; // Fallback - connector popup should handle all connector types + return ( + onManage(actualConnector) + : undefined + } + /> + ); + })} +
+
+ )} - return ( - onManage(actualConnector) : undefined - } - /> - ); - })} -
-
- )} + {/* Content Sources */} + {filteredCrawlers.length > 0 && ( +
+
+

+ Content Sources +

+
+
+ {filteredCrawlers.map((crawler) => { + const isYouTube = crawler.id === "youtube-crawler"; + const isWebcrawler = crawler.id === "webcrawler-connector"; - {/* Content Sources */} - {filteredCrawlers.length > 0 && ( -
-
-

Content Sources

-
-
- {filteredCrawlers.map((crawler) => { - const isYouTube = crawler.id === "youtube-crawler"; - const isWebcrawler = crawler.id === "webcrawler-connector"; + // For crawlers that are actual connectors, check connection status + const isConnected = crawler.connectorType + ? connectedTypes.has(crawler.connectorType) + : false; + const isConnecting = connectingId === crawler.id; - // For crawlers that are actual connectors, check connection status - const isConnected = crawler.connectorType - ? connectedTypes.has(crawler.connectorType) - : false; - const isConnecting = connectingId === crawler.id; + // Find the actual connector object if connected + const actualConnector = + isConnected && crawler.connectorType && allConnectors + ? allConnectors.find( + (c: SearchSourceConnector) => + c.connector_type === crawler.connectorType + ) + : undefined; - // Find the actual connector object if connected - const actualConnector = - isConnected && crawler.connectorType && allConnectors - ? allConnectors.find( - (c: SearchSourceConnector) => c.connector_type === crawler.connectorType - ) - : undefined; + const documentCount = crawler.connectorType + ? getDocumentCountForConnector( + crawler.connectorType, + documentTypeCounts + ) + : undefined; + const isIndexing = + actualConnector && + indexingConnectorIds?.has(actualConnector.id); + const activeTask = actualConnector + ? getActiveTaskForConnector(actualConnector.id) + : undefined; - const documentCount = crawler.connectorType - ? getDocumentCountForConnector(crawler.connectorType, documentTypeCounts) - : undefined; - const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id); - const activeTask = actualConnector - ? getActiveTaskForConnector(actualConnector.id) - : undefined; + const handleConnect = + isYouTube && onCreateYouTubeCrawler + ? onCreateYouTubeCrawler + : isWebcrawler && onCreateWebcrawler + ? onCreateWebcrawler + : crawler.connectorType && onConnectNonOAuth + ? () => { + if (crawler.connectorType) { + onConnectNonOAuth(crawler.connectorType); + } + } + : () => {}; // Fallback for non-connector crawlers - const handleConnect = - isYouTube && onCreateYouTubeCrawler - ? onCreateYouTubeCrawler - : isWebcrawler && onCreateWebcrawler - ? onCreateWebcrawler - : crawler.connectorType && onConnectNonOAuth - ? () => { - if (crawler.connectorType) { - onConnectNonOAuth(crawler.connectorType); - } - } - : () => {}; // Fallback for non-connector crawlers - - return ( - onManage(actualConnector) : undefined - } - /> - ); - })} -
-
- )} -
- ); + return ( + onManage(actualConnector) + : undefined + } + /> + ); + })} +
+ + )} +
+ ); }; diff --git a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx new file mode 100644 index 000000000..23faedc4a --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx @@ -0,0 +1,214 @@ +"use client"; + +import { differenceInDays, differenceInMinutes, format, isToday, isYesterday } from "date-fns"; +import { ArrowLeft, Loader2, Plus } from "lucide-react"; +import type { FC } from "react"; +import { Button } from "@/components/ui/button"; +import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; +import type { SearchSourceConnector } from "@/contracts/types/connector.types"; +import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types"; +import { cn } from "@/lib/utils"; +import { getDocumentCountForConnector } from "../utils/connector-document-mapping"; +import { getConnectorDisplayName } from "../tabs/all-connectors-tab"; + +interface ConnectorAccountsListViewProps { + connectorType: string; + connectorTitle: string; + connectors: SearchSourceConnector[]; + indexingConnectorIds: Set; + logsSummary: LogSummary | undefined; + documentTypeCounts?: Record; + onBack: () => void; + onManage: (connector: SearchSourceConnector) => void; + onAddAccount: () => void; + isConnecting?: boolean; +} + +/** + * Format document count (e.g., "1.2k docs", "500 docs", "1.5M docs") + */ +function formatDocumentCount(count: number | undefined): string { + if (count === undefined || count === 0) return "0 docs"; + if (count < 1000) return `${count} docs`; + if (count < 1000000) { + const k = (count / 1000).toFixed(1); + return `${k.replace(/\.0$/, "")}k docs`; + } + const m = (count / 1000000).toFixed(1); + return `${m.replace(/\.0$/, "")}M docs`; +} + +/** + * Format last indexed date with contextual messages + */ +function formatLastIndexedDate(dateString: string): string { + const date = new Date(dateString); + const now = new Date(); + const minutesAgo = differenceInMinutes(now, date); + const daysAgo = differenceInDays(now, date); + + if (minutesAgo < 1) { + return "Just now"; + } + + if (minutesAgo < 60) { + return `${minutesAgo} ${minutesAgo === 1 ? "minute" : "minutes"} ago`; + } + + if (isToday(date)) { + return `Today at ${format(date, "h:mm a")}`; + } + + if (isYesterday(date)) { + return `Yesterday at ${format(date, "h:mm a")}`; + } + + if (daysAgo < 7) { + return `${daysAgo} ${daysAgo === 1 ? "day" : "days"} ago`; + } + + return format(date, "MMM d, yyyy"); +} + +export const ConnectorAccountsListView: FC = ({ + connectorType, + connectorTitle, + connectors, + indexingConnectorIds, + logsSummary, + documentTypeCounts, + onBack, + onManage, + onAddAccount, + isConnecting = false, +}) => { + // Filter connectors to only show those of this type + const typeConnectors = connectors.filter((c) => c.connector_type === connectorType); + + return ( +
+ {/* Header */} +
+
+
+ +
+
+ {getConnectorIcon(connectorType, "size-5")} +
+
+

{connectorTitle} Accounts

+

+ {typeConnectors.length} connected account{typeConnectors.length !== 1 ? "s" : ""} +

+
+
+
+ {/* Add Account Button with dashed border */} + +
+
+ + {/* Content */} +
+ {/* Connected Accounts Grid */} +
+ {typeConnectors.map((connector) => { + const isIndexing = indexingConnectorIds.has(connector.id); + const activeTask = logsSummary?.active_tasks?.find( + (task: LogActiveTask) => task.connector_id === connector.id + ); + const documentCount = getDocumentCountForConnector( + connector.connector_type, + documentTypeCounts + ); + + return ( +
+
+ {getConnectorIcon(connector.connector_type, "size-6")} +
+
+

+ {getConnectorDisplayName(connector.name)} +

+ {isIndexing ? ( +

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

+ ) : ( +

+ {connector.last_indexed_at + ? `Last indexed: ${formatLastIndexedDate(connector.last_indexed_at)}` + : "Never indexed"} +

+ )} +

+ {formatDocumentCount(documentCount)} +

+
+ +
+ ); + })} +
+
+
+ ); +}; +