mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-10 22:32:16 +02:00
- Added the ensure_publication function to create and verify the zero_publication if it is missing, ensuring idempotency during database initialization. - Integrated ensure_publication into the create_db_and_tables function to prevent zero-cache crash loops on startup. - Introduced a self-check script to validate the ensure_publication functionality on a create_all-bootstrapped database. - Updated various components to reflect the transition from search space to workspace, including adjustments in imports and routing paths.
388 lines
14 KiB
TypeScript
388 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { useAtomValue } from "jotai";
|
|
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
|
|
import { activeWorkspaceIdAtom } from "@/atoms/workspaces/workspace-query.atoms";
|
|
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
|
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
|
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
|
import { useConnectorsSync } from "@/hooks/use-connectors-sync";
|
|
import { PICKER_CLOSE_EVENT, PICKER_OPEN_EVENT } from "@/hooks/use-google-picker";
|
|
import { useZeroDocumentTypeCounts } from "@/hooks/use-zero-document-type-counts";
|
|
import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header";
|
|
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 {
|
|
COMPOSIO_CONNECTORS,
|
|
OAUTH_CONNECTORS,
|
|
} from "./connector-popup/constants/connector-constants";
|
|
import { useConnectorDialog } from "./connector-popup/hooks/use-connector-dialog";
|
|
import { useIndexingConnectors } from "./connector-popup/hooks/use-indexing-connectors";
|
|
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 interface ConnectorIndicatorHandle {
|
|
open: () => void;
|
|
}
|
|
|
|
interface ConnectorIndicatorProps {
|
|
showTrigger?: boolean;
|
|
}
|
|
|
|
export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, ConnectorIndicatorProps>(
|
|
(_props, ref) => {
|
|
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
|
|
|
|
// Real-time document type counts via Zero (updates instantly as docs are indexed)
|
|
const documentTypeCounts = useZeroDocumentTypeCounts(searchSpaceId);
|
|
// Read status inbox items from shared atom (populated by LayoutDataProvider)
|
|
// instead of creating a duplicate useInbox("status") hook.
|
|
const statusInboxItems = useAtomValue(statusInboxItemsAtom);
|
|
const inboxItems = useMemo(
|
|
() => statusInboxItems.filter((item) => item.type === "connector_indexing"),
|
|
[statusInboxItems]
|
|
);
|
|
|
|
// Use the custom hook for dialog state management
|
|
const {
|
|
isOpen,
|
|
activeTab,
|
|
connectingId,
|
|
isScrolled,
|
|
searchQuery,
|
|
indexingConfig,
|
|
indexingConnector,
|
|
indexingConnectorConfig,
|
|
editingConnector,
|
|
connectingConnectorType,
|
|
isCreatingConnector,
|
|
startDate,
|
|
endDate,
|
|
isStartingIndexing,
|
|
isSaving,
|
|
isDisconnecting,
|
|
periodicEnabled,
|
|
frequencyMinutes,
|
|
enableVisionLlm,
|
|
allConnectors,
|
|
viewingAccountsType,
|
|
viewingMCPList,
|
|
isYouTubeView,
|
|
isFromOAuth,
|
|
setSearchQuery,
|
|
setStartDate,
|
|
setEndDate,
|
|
setPeriodicEnabled,
|
|
setFrequencyMinutes,
|
|
setEnableVisionLlm,
|
|
handleOpenChange,
|
|
handleTabChange,
|
|
handleScroll,
|
|
handleConnectOAuth,
|
|
handleConnectNonOAuth,
|
|
handleCreateWebcrawler,
|
|
handleCreateYouTubeCrawler,
|
|
handleSubmitConnectForm,
|
|
handleStartIndexing,
|
|
handleSkipIndexing,
|
|
handleStartEdit,
|
|
handleSaveConnector,
|
|
handleDisconnectConnector,
|
|
handleBackFromEdit,
|
|
handleBackFromConnect,
|
|
handleBackFromYouTube,
|
|
handleViewAccountsList,
|
|
handleBackFromAccountsList,
|
|
handleBackFromMCPList,
|
|
handleAddNewMCPFromList,
|
|
handleQuickIndexConnector,
|
|
connectorConfig,
|
|
setConnectorConfig,
|
|
setIndexingConnectorConfig,
|
|
setConnectorName,
|
|
} = useConnectorDialog();
|
|
|
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
useEffect(() => {
|
|
const onOpen = () => setPickerOpen(true);
|
|
const onClose = () => setPickerOpen(false);
|
|
window.addEventListener(PICKER_OPEN_EVENT, onOpen);
|
|
window.addEventListener(PICKER_CLOSE_EVENT, onClose);
|
|
return () => {
|
|
window.removeEventListener(PICKER_OPEN_EVENT, onOpen);
|
|
window.removeEventListener(PICKER_CLOSE_EVENT, onClose);
|
|
};
|
|
}, []);
|
|
|
|
const {
|
|
connectors: connectorsFromSync = [],
|
|
loading: connectorsLoading,
|
|
error: connectorsError,
|
|
refreshConnectors: refreshConnectorsSync,
|
|
} = useConnectorsSync(searchSpaceId);
|
|
|
|
const useSyncData = connectorsFromSync.length > 0 || (connectorsLoading && !connectorsError);
|
|
const connectors = useSyncData ? connectorsFromSync : allConnectors || [];
|
|
|
|
const refreshConnectors = async () => {
|
|
if (useSyncData) {
|
|
await refreshConnectorsSync();
|
|
}
|
|
};
|
|
|
|
// Track indexing state locally - clears automatically when last_indexed_at changes via real-time sync
|
|
// Also clears when failed notifications are detected
|
|
const { indexingConnectorIds, startIndexing, stopIndexing } = useIndexingConnectors(
|
|
connectors as SearchSourceConnector[],
|
|
inboxItems
|
|
);
|
|
|
|
// Get document types that have documents in the search space
|
|
const activeDocumentTypes = documentTypeCounts
|
|
? Object.entries(documentTypeCounts).filter(([, count]) => count > 0)
|
|
: [];
|
|
|
|
const hasConnectors = connectors.length > 0;
|
|
const hasSources = hasConnectors || activeDocumentTypes.length > 0;
|
|
const totalSourceCount = connectors.length + activeDocumentTypes.length;
|
|
|
|
const activeConnectorsCount = connectors.length;
|
|
|
|
// Check which connectors are already connected
|
|
// Real-time connector updates via Zero sync
|
|
const connectedTypes = new Set<string>(
|
|
(connectors || []).map((c: SearchSourceConnector) => c.connector_type)
|
|
);
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
open: () => handleOpenChange(true),
|
|
}));
|
|
|
|
if (!searchSpaceId) return null;
|
|
|
|
return (
|
|
<Dialog open={isOpen} modal={false} onOpenChange={handleOpenChange}>
|
|
{isOpen &&
|
|
createPortal(
|
|
<div
|
|
className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
|
aria-hidden="true"
|
|
onClick={() => {
|
|
if (!pickerOpen) handleOpenChange(false);
|
|
}}
|
|
/>,
|
|
document.body
|
|
)}
|
|
|
|
<DialogContent
|
|
onFocusOutside={(e) => e.preventDefault()}
|
|
onInteractOutside={(e) => {
|
|
if (pickerOpen) e.preventDefault();
|
|
}}
|
|
onPointerDownOutside={(e) => {
|
|
if (pickerOpen) e.preventDefault();
|
|
}}
|
|
className="max-w-3xl w-[95vw] sm:w-full h-[75vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden ring-0 dark:ring-0 [&>button]:right-4 sm:[&>button]:right-12 [&>button]:top-6 sm:[&>button]:top-10 [&>button]:opacity-80 [&>button]:hover:opacity-100 [&>button]:hover:bg-accent [&>button]:hover:text-accent-foreground [&>button>svg]:size-5 select-none"
|
|
>
|
|
<DialogTitle className="sr-only">Manage Connectors</DialogTitle>
|
|
{/* YouTube Crawler View - shown when adding YouTube videos */}
|
|
{isYouTubeView && searchSpaceId ? (
|
|
<YouTubeCrawlerView searchSpaceId={searchSpaceId} onBack={handleBackFromYouTube} />
|
|
) : viewingMCPList ? (
|
|
<ConnectorAccountsListView
|
|
connectorType="MCP_CONNECTOR"
|
|
connectorTitle="MCP Connectors"
|
|
connectors={(allConnectors || []) as SearchSourceConnector[]}
|
|
indexingConnectorIds={indexingConnectorIds}
|
|
onBack={handleBackFromMCPList}
|
|
onManage={handleStartEdit}
|
|
onAddAccount={handleAddNewMCPFromList}
|
|
addButtonText="Add New MCP Server"
|
|
/>
|
|
) : viewingAccountsType ? (
|
|
<ConnectorAccountsListView
|
|
connectorType={viewingAccountsType.connectorType}
|
|
connectorTitle={viewingAccountsType.connectorTitle}
|
|
connectors={(connectors || []) as SearchSourceConnector[]}
|
|
indexingConnectorIds={indexingConnectorIds}
|
|
onBack={handleBackFromAccountsList}
|
|
onManage={handleStartEdit}
|
|
onAddAccount={() => {
|
|
// Check both OAUTH_CONNECTORS and COMPOSIO_CONNECTORS
|
|
const oauthConnector =
|
|
OAUTH_CONNECTORS.find(
|
|
(c) => c.connectorType === viewingAccountsType.connectorType
|
|
) ||
|
|
COMPOSIO_CONNECTORS.find(
|
|
(c) => c.connectorType === viewingAccountsType.connectorType
|
|
);
|
|
if (oauthConnector) {
|
|
handleConnectOAuth(oauthConnector);
|
|
}
|
|
}}
|
|
isConnecting={connectingId !== null}
|
|
/>
|
|
) : connectingConnectorType ? (
|
|
<ConnectorConnectView
|
|
connectorType={connectingConnectorType}
|
|
onSubmit={(formData) => handleSubmitConnectForm(formData, startIndexing)}
|
|
onBack={handleBackFromConnect}
|
|
isSubmitting={isCreatingConnector}
|
|
/>
|
|
) : editingConnector ? (
|
|
<ConnectorEditView
|
|
connector={{
|
|
...editingConnector,
|
|
config: connectorConfig || editingConnector.config,
|
|
name: editingConnector.name,
|
|
// Sync last_indexed_at with live data from real-time sync
|
|
last_indexed_at:
|
|
(connectors as SearchSourceConnector[]).find((c) => c.id === editingConnector.id)
|
|
?.last_indexed_at ?? editingConnector.last_indexed_at,
|
|
}}
|
|
startDate={startDate}
|
|
endDate={endDate}
|
|
periodicEnabled={periodicEnabled}
|
|
frequencyMinutes={frequencyMinutes}
|
|
enableVisionLlm={enableVisionLlm}
|
|
isSaving={isSaving}
|
|
isDisconnecting={isDisconnecting}
|
|
isIndexing={indexingConnectorIds.has(editingConnector.id)}
|
|
searchSpaceId={searchSpaceId?.toString()}
|
|
onStartDateChange={setStartDate}
|
|
onEndDateChange={setEndDate}
|
|
onPeriodicEnabledChange={setPeriodicEnabled}
|
|
onFrequencyChange={setFrequencyMinutes}
|
|
onEnableVisionLlmChange={setEnableVisionLlm}
|
|
onSave={() => {
|
|
startIndexing(editingConnector.id);
|
|
handleSaveConnector(() => refreshConnectors());
|
|
}}
|
|
onDisconnect={() => handleDisconnectConnector(() => refreshConnectors())}
|
|
onBack={handleBackFromEdit}
|
|
onQuickIndex={(() => {
|
|
const cfg = connectorConfig || editingConnector.config;
|
|
const isDriveOrOneDrive =
|
|
editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
|
|
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" ||
|
|
editingConnector.connector_type === "ONEDRIVE_CONNECTOR" ||
|
|
editingConnector.connector_type === "DROPBOX_CONNECTOR";
|
|
const hasDriveItems = isDriveOrOneDrive
|
|
? ((cfg?.selected_folders as unknown[]) ?? []).length > 0 ||
|
|
((cfg?.selected_files as unknown[]) ?? []).length > 0
|
|
: true;
|
|
if (!hasDriveItems) return undefined;
|
|
return () => {
|
|
startIndexing(editingConnector.id);
|
|
handleQuickIndexConnector(
|
|
editingConnector.id,
|
|
editingConnector.connector_type,
|
|
stopIndexing,
|
|
startDate,
|
|
endDate
|
|
);
|
|
};
|
|
})()}
|
|
onConfigChange={setConnectorConfig}
|
|
onNameChange={setConnectorName}
|
|
/>
|
|
) : indexingConfig ? (
|
|
<IndexingConfigurationView
|
|
config={indexingConfig}
|
|
connector={
|
|
indexingConnector
|
|
? {
|
|
...indexingConnector,
|
|
config: indexingConnectorConfig || indexingConnector.config,
|
|
}
|
|
: undefined
|
|
}
|
|
startDate={startDate}
|
|
endDate={endDate}
|
|
periodicEnabled={periodicEnabled}
|
|
frequencyMinutes={frequencyMinutes}
|
|
enableVisionLlm={enableVisionLlm}
|
|
isStartingIndexing={isStartingIndexing}
|
|
isFromOAuth={isFromOAuth}
|
|
onStartDateChange={setStartDate}
|
|
onEndDateChange={setEndDate}
|
|
onPeriodicEnabledChange={setPeriodicEnabled}
|
|
onFrequencyChange={setFrequencyMinutes}
|
|
onEnableVisionLlmChange={setEnableVisionLlm}
|
|
onConfigChange={setIndexingConnectorConfig}
|
|
onStartIndexing={() => {
|
|
if (indexingConfig.connectorId) {
|
|
startIndexing(indexingConfig.connectorId);
|
|
}
|
|
handleStartIndexing(() => refreshConnectors());
|
|
}}
|
|
onSkip={handleSkipIndexing}
|
|
/>
|
|
) : (
|
|
<Tabs
|
|
value={activeTab}
|
|
onValueChange={handleTabChange}
|
|
className="flex-1 flex flex-col min-h-0"
|
|
>
|
|
{/* Header */}
|
|
<ConnectorDialogHeader
|
|
activeTab={activeTab}
|
|
totalSourceCount={activeConnectorsCount}
|
|
searchQuery={searchQuery}
|
|
onTabChange={handleTabChange}
|
|
onSearchChange={setSearchQuery}
|
|
isScrolled={isScrolled}
|
|
/>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 min-h-0 relative overflow-hidden">
|
|
<div className="h-full overflow-y-auto" onScroll={handleScroll}>
|
|
<div className="px-4 sm:px-12 py-4 sm:py-8 pb-12 sm:pb-16">
|
|
<TabsContent value="all" className="m-0">
|
|
<AllConnectorsTab
|
|
searchQuery={searchQuery}
|
|
searchSpaceId={searchSpaceId}
|
|
connectedTypes={connectedTypes}
|
|
connectingId={connectingId}
|
|
allConnectors={connectors}
|
|
documentTypeCounts={documentTypeCounts}
|
|
indexingConnectorIds={indexingConnectorIds}
|
|
onConnectOAuth={handleConnectOAuth}
|
|
onConnectNonOAuth={handleConnectNonOAuth}
|
|
onCreateWebcrawler={handleCreateWebcrawler}
|
|
onCreateYouTubeCrawler={handleCreateYouTubeCrawler}
|
|
onManage={handleStartEdit}
|
|
onViewAccountsList={handleViewAccountsList}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<ActiveConnectorsTab
|
|
searchQuery={searchQuery}
|
|
hasSources={hasSources}
|
|
totalSourceCount={totalSourceCount}
|
|
activeDocumentTypes={activeDocumentTypes}
|
|
connectors={connectors as SearchSourceConnector[]}
|
|
indexingConnectorIds={indexingConnectorIds}
|
|
onTabChange={handleTabChange}
|
|
onManage={handleStartEdit}
|
|
onViewAccountsList={handleViewAccountsList}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/* Bottom fade shadow */}
|
|
<div className="absolute bottom-0 left-0 right-0 h-7 bg-linear-to-t from-popover via-popover/80 to-transparent pointer-events-none z-10" />
|
|
</div>
|
|
</Tabs>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
);
|
|
|
|
ConnectorIndicator.displayName = "ConnectorIndicator";
|