mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-21 18:55:16 +02:00
revert Composio Drive to folder tree, harden Picker for native Drive
This commit is contained in:
parent
2c9d01ba2d
commit
3bda6c1679
5 changed files with 558 additions and 436 deletions
|
|
@ -4,8 +4,9 @@ import { useAtomValue } from "jotai";
|
||||||
import { AlertTriangle, Cable, Settings } from "lucide-react";
|
import { AlertTriangle, Cable, Settings } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { type FC, forwardRef, useImperativeHandle, useMemo } from "react";
|
import { type FC, forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react";
|
||||||
import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms";
|
import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms";
|
||||||
|
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
|
||||||
import {
|
import {
|
||||||
globalNewLLMConfigsAtom,
|
globalNewLLMConfigsAtom,
|
||||||
llmPreferencesAtom,
|
llmPreferencesAtom,
|
||||||
|
|
@ -19,8 +20,8 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||||
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
|
|
||||||
import { useConnectorsElectric } from "@/hooks/use-connectors-electric";
|
import { useConnectorsElectric } from "@/hooks/use-connectors-electric";
|
||||||
|
import { PICKER_CLOSE_EVENT, PICKER_OPEN_EVENT } from "@/hooks/use-google-picker";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header";
|
import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header";
|
||||||
import { ConnectorConnectView } from "./connector-popup/connector-configs/views/connector-connect-view";
|
import { ConnectorConnectView } from "./connector-popup/connector-configs/views/connector-connect-view";
|
||||||
|
|
@ -47,400 +48,426 @@ interface ConnectorIndicatorProps {
|
||||||
|
|
||||||
export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, ConnectorIndicatorProps>(
|
export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, ConnectorIndicatorProps>(
|
||||||
({ showTrigger = true }, ref) => {
|
({ showTrigger = true }, ref) => {
|
||||||
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const { data: currentUser } = useAtomValue(currentUserAtom);
|
const { data: currentUser } = useAtomValue(currentUserAtom);
|
||||||
const { data: preferences = {}, isFetching: preferencesLoading } =
|
const { data: preferences = {}, isFetching: preferencesLoading } =
|
||||||
useAtomValue(llmPreferencesAtom);
|
useAtomValue(llmPreferencesAtom);
|
||||||
const { data: globalConfigs = [], isFetching: globalConfigsLoading } =
|
const { data: globalConfigs = [], isFetching: globalConfigsLoading } =
|
||||||
useAtomValue(globalNewLLMConfigsAtom);
|
useAtomValue(globalNewLLMConfigsAtom);
|
||||||
|
|
||||||
// Check if document summary LLM is properly configured
|
// Check if document summary LLM is properly configured
|
||||||
// - If ID is 0 (Auto mode), we need global configs to be available
|
// - If ID is 0 (Auto mode), we need global configs to be available
|
||||||
// - If ID is positive (user config) or negative (specific global config), it's configured
|
// - If ID is positive (user config) or negative (specific global config), it's configured
|
||||||
// - If ID is null/undefined, it's not configured
|
// - If ID is null/undefined, it's not configured
|
||||||
const docSummaryLlmId = preferences.document_summary_llm_id;
|
const docSummaryLlmId = preferences.document_summary_llm_id;
|
||||||
const isAutoMode = docSummaryLlmId === 0;
|
const isAutoMode = docSummaryLlmId === 0;
|
||||||
const hasGlobalConfigs = globalConfigs.length > 0;
|
const hasGlobalConfigs = globalConfigs.length > 0;
|
||||||
|
|
||||||
const hasDocumentSummaryLLM =
|
const hasDocumentSummaryLLM =
|
||||||
docSummaryLlmId !== null &&
|
docSummaryLlmId !== null &&
|
||||||
docSummaryLlmId !== undefined &&
|
docSummaryLlmId !== undefined &&
|
||||||
// If it's Auto mode, we need global configs to actually be available
|
// If it's Auto mode, we need global configs to actually be available
|
||||||
(!isAutoMode || hasGlobalConfigs);
|
(!isAutoMode || hasGlobalConfigs);
|
||||||
|
|
||||||
const llmConfigLoading = preferencesLoading || globalConfigsLoading;
|
const llmConfigLoading = preferencesLoading || globalConfigsLoading;
|
||||||
|
|
||||||
// Fetch document type counts via the lightweight /type-counts endpoint (cached 10 min)
|
// Fetch document type counts via the lightweight /type-counts endpoint (cached 10 min)
|
||||||
const { data: documentTypeCounts, isFetching: documentTypesLoading } =
|
const { data: documentTypeCounts, isFetching: documentTypesLoading } =
|
||||||
useAtomValue(documentTypeCountsAtom);
|
useAtomValue(documentTypeCountsAtom);
|
||||||
|
|
||||||
// Read status inbox items from shared atom (populated by LayoutDataProvider)
|
// Read status inbox items from shared atom (populated by LayoutDataProvider)
|
||||||
// instead of creating a duplicate useInbox("status") hook.
|
// instead of creating a duplicate useInbox("status") hook.
|
||||||
const statusInboxItems = useAtomValue(statusInboxItemsAtom);
|
const statusInboxItems = useAtomValue(statusInboxItemsAtom);
|
||||||
const inboxItems = useMemo(
|
const inboxItems = useMemo(
|
||||||
() => statusInboxItems.filter((item) => item.type === "connector_indexing"),
|
() => statusInboxItems.filter((item) => item.type === "connector_indexing"),
|
||||||
[statusInboxItems]
|
[statusInboxItems]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if YouTube view is active
|
// Check if YouTube view is active
|
||||||
const isYouTubeView = searchParams.get("view") === "youtube";
|
const isYouTubeView = searchParams.get("view") === "youtube";
|
||||||
|
|
||||||
// Use the custom hook for dialog state management
|
// Use the custom hook for dialog state management
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
activeTab,
|
activeTab,
|
||||||
connectingId,
|
connectingId,
|
||||||
isScrolled,
|
isScrolled,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
indexingConfig,
|
indexingConfig,
|
||||||
indexingConnector,
|
indexingConnector,
|
||||||
indexingConnectorConfig,
|
indexingConnectorConfig,
|
||||||
editingConnector,
|
editingConnector,
|
||||||
connectingConnectorType,
|
connectingConnectorType,
|
||||||
isCreatingConnector,
|
isCreatingConnector,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
isStartingIndexing,
|
isStartingIndexing,
|
||||||
isSaving,
|
isSaving,
|
||||||
isDisconnecting,
|
isDisconnecting,
|
||||||
periodicEnabled,
|
periodicEnabled,
|
||||||
frequencyMinutes,
|
frequencyMinutes,
|
||||||
enableSummary,
|
enableSummary,
|
||||||
allConnectors,
|
allConnectors,
|
||||||
viewingAccountsType,
|
viewingAccountsType,
|
||||||
viewingMCPList,
|
viewingMCPList,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
setStartDate,
|
setStartDate,
|
||||||
setEndDate,
|
setEndDate,
|
||||||
setPeriodicEnabled,
|
setPeriodicEnabled,
|
||||||
setFrequencyMinutes,
|
setFrequencyMinutes,
|
||||||
setEnableSummary,
|
setEnableSummary,
|
||||||
handleOpenChange,
|
handleOpenChange,
|
||||||
handleTabChange,
|
handleTabChange,
|
||||||
handleScroll,
|
handleScroll,
|
||||||
handleConnectOAuth,
|
handleConnectOAuth,
|
||||||
handleConnectNonOAuth,
|
handleConnectNonOAuth,
|
||||||
handleCreateWebcrawler,
|
handleCreateWebcrawler,
|
||||||
handleCreateYouTubeCrawler,
|
handleCreateYouTubeCrawler,
|
||||||
handleSubmitConnectForm,
|
handleSubmitConnectForm,
|
||||||
handleStartIndexing,
|
handleStartIndexing,
|
||||||
handleSkipIndexing,
|
handleSkipIndexing,
|
||||||
handleStartEdit,
|
handleStartEdit,
|
||||||
handleSaveConnector,
|
handleSaveConnector,
|
||||||
handleDisconnectConnector,
|
handleDisconnectConnector,
|
||||||
handleBackFromEdit,
|
handleBackFromEdit,
|
||||||
handleBackFromConnect,
|
handleBackFromConnect,
|
||||||
handleBackFromYouTube,
|
handleBackFromYouTube,
|
||||||
handleViewAccountsList,
|
handleViewAccountsList,
|
||||||
handleBackFromAccountsList,
|
handleBackFromAccountsList,
|
||||||
handleBackFromMCPList,
|
handleBackFromMCPList,
|
||||||
handleAddNewMCPFromList,
|
handleAddNewMCPFromList,
|
||||||
handleQuickIndexConnector,
|
handleQuickIndexConnector,
|
||||||
connectorConfig,
|
connectorConfig,
|
||||||
setConnectorConfig,
|
setConnectorConfig,
|
||||||
setIndexingConnectorConfig,
|
setIndexingConnectorConfig,
|
||||||
setConnectorName,
|
setConnectorName,
|
||||||
} = useConnectorDialog();
|
} = useConnectorDialog();
|
||||||
|
|
||||||
// Fetch connectors using Electric SQL + PGlite for real-time updates
|
const [pickerOpen, setPickerOpen] = useState(false);
|
||||||
// This provides instant updates when connectors change, without polling
|
useEffect(() => {
|
||||||
const {
|
const onOpen = () => setPickerOpen(true);
|
||||||
connectors: connectorsFromElectric = [],
|
const onClose = () => setPickerOpen(false);
|
||||||
loading: connectorsLoading,
|
window.addEventListener(PICKER_OPEN_EVENT, onOpen);
|
||||||
error: connectorsError,
|
window.addEventListener(PICKER_CLOSE_EVENT, onClose);
|
||||||
refreshConnectors: refreshConnectorsElectric,
|
return () => {
|
||||||
} = useConnectorsElectric(searchSpaceId);
|
window.removeEventListener(PICKER_OPEN_EVENT, onOpen);
|
||||||
|
window.removeEventListener(PICKER_CLOSE_EVENT, onClose);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Fallback to API if Electric is not available or fails
|
// Fetch connectors using Electric SQL + PGlite for real-time updates
|
||||||
// Use Electric data if: 1) we have data, or 2) still loading without error
|
// This provides instant updates when connectors change, without polling
|
||||||
// Use API data if: Electric failed (has error) or finished loading with no data
|
const {
|
||||||
const useElectricData =
|
connectors: connectorsFromElectric = [],
|
||||||
connectorsFromElectric.length > 0 || (connectorsLoading && !connectorsError);
|
loading: connectorsLoading,
|
||||||
const connectors = useElectricData ? connectorsFromElectric : allConnectors || [];
|
error: connectorsError,
|
||||||
|
refreshConnectors: refreshConnectorsElectric,
|
||||||
|
} = useConnectorsElectric(searchSpaceId);
|
||||||
|
|
||||||
// Manual refresh function that works with both Electric and API
|
// Fallback to API if Electric is not available or fails
|
||||||
const refreshConnectors = async () => {
|
// Use Electric data if: 1) we have data, or 2) still loading without error
|
||||||
if (useElectricData) {
|
// Use API data if: Electric failed (has error) or finished loading with no data
|
||||||
await refreshConnectorsElectric();
|
const useElectricData =
|
||||||
} else {
|
connectorsFromElectric.length > 0 || (connectorsLoading && !connectorsError);
|
||||||
// Fallback: use allConnectors from useConnectorDialog (which uses connectorsAtom)
|
const connectors = useElectricData ? connectorsFromElectric : allConnectors || [];
|
||||||
// The connectorsAtom will handle refetching if needed
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Track indexing state locally - clears automatically when Electric SQL detects last_indexed_at changed
|
// Manual refresh function that works with both Electric and API
|
||||||
// Also clears when failed notifications are detected
|
const refreshConnectors = async () => {
|
||||||
const { indexingConnectorIds, startIndexing, stopIndexing } = useIndexingConnectors(
|
if (useElectricData) {
|
||||||
connectors as SearchSourceConnector[],
|
await refreshConnectorsElectric();
|
||||||
inboxItems
|
} else {
|
||||||
);
|
// Fallback: use allConnectors from useConnectorDialog (which uses connectorsAtom)
|
||||||
|
// The connectorsAtom will handle refetching if needed
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isLoading = connectorsLoading || documentTypesLoading;
|
// Track indexing state locally - clears automatically when Electric SQL detects last_indexed_at changed
|
||||||
|
// 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 isLoading = connectorsLoading || documentTypesLoading;
|
||||||
const activeDocumentTypes = documentTypeCounts
|
|
||||||
? Object.entries(documentTypeCounts).filter(([, count]) => count > 0)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const hasConnectors = connectors.length > 0;
|
// Get document types that have documents in the search space
|
||||||
const hasSources = hasConnectors || activeDocumentTypes.length > 0;
|
const activeDocumentTypes = documentTypeCounts
|
||||||
const totalSourceCount = connectors.length + activeDocumentTypes.length;
|
? Object.entries(documentTypeCounts).filter(([, count]) => count > 0)
|
||||||
|
: [];
|
||||||
|
|
||||||
const activeConnectorsCount = connectors.length;
|
const hasConnectors = connectors.length > 0;
|
||||||
|
const hasSources = hasConnectors || activeDocumentTypes.length > 0;
|
||||||
|
const totalSourceCount = connectors.length + activeDocumentTypes.length;
|
||||||
|
|
||||||
// Check which connectors are already connected
|
const activeConnectorsCount = connectors.length;
|
||||||
// Using Electric SQL + PGlite for real-time connector updates
|
|
||||||
const connectedTypes = new Set<string>(
|
|
||||||
(connectors || []).map((c: SearchSourceConnector) => c.connector_type)
|
|
||||||
);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
// Check which connectors are already connected
|
||||||
open: () => handleOpenChange(true),
|
// Using Electric SQL + PGlite for real-time connector updates
|
||||||
}));
|
const connectedTypes = new Set<string>(
|
||||||
|
(connectors || []).map((c: SearchSourceConnector) => c.connector_type)
|
||||||
|
);
|
||||||
|
|
||||||
if (!searchSpaceId) return null;
|
useImperativeHandle(ref, () => ({
|
||||||
|
open: () => handleOpenChange(true),
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
if (!searchSpaceId) return null;
|
||||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
|
||||||
{showTrigger && (
|
|
||||||
<TooltipIconButton
|
|
||||||
data-joyride="connector-icon"
|
|
||||||
tooltip={hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"}
|
|
||||||
side="bottom"
|
|
||||||
className={cn(
|
|
||||||
"size-[34px] rounded-full p-1 flex items-center justify-center transition-colors relative",
|
|
||||||
"hover:bg-muted-foreground/15 dark:hover:bg-muted-foreground/30",
|
|
||||||
"outline-none focus:outline-none focus-visible:outline-none font-semibold text-xs",
|
|
||||||
"border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none"
|
|
||||||
)}
|
|
||||||
aria-label={
|
|
||||||
hasConnectors ? `View ${activeConnectorsCount} connectors` : "Add your first connector"
|
|
||||||
}
|
|
||||||
onClick={() => handleOpenChange(true)}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<Spinner size="sm" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Cable className="size-4 stroke-[1.5px]" />
|
|
||||||
{activeConnectorsCount > 0 && (
|
|
||||||
<span className="absolute -top-0.5 right-0 flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-medium rounded-full bg-primary text-primary-foreground shadow-sm select-none">
|
|
||||||
{activeConnectorsCount > 99 ? "99+" : activeConnectorsCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</TooltipIconButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DialogContent className="max-w-3xl w-[95vw] sm:w-full h-[75vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border ring-0 dark:ring-0 bg-muted dark:bg-muted text-foreground focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 [&>button]:right-4 sm:[&>button]:right-12 [&>button]:top-6 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button_svg]:size-5 select-none">
|
return (
|
||||||
<DialogTitle className="sr-only">Manage Connectors</DialogTitle>
|
<Dialog
|
||||||
{/* YouTube Crawler View - shown when adding YouTube videos */}
|
open={isOpen}
|
||||||
{isYouTubeView && searchSpaceId ? (
|
onOpenChange={(open) => {
|
||||||
<YouTubeCrawlerView searchSpaceId={searchSpaceId} onBack={handleBackFromYouTube} />
|
if (!open && pickerOpen) return;
|
||||||
) : viewingMCPList ? (
|
handleOpenChange(open);
|
||||||
<ConnectorAccountsListView
|
}}
|
||||||
connectorType="MCP_CONNECTOR"
|
modal={!pickerOpen}
|
||||||
connectorTitle="MCP Connectors"
|
>
|
||||||
connectors={(allConnectors || []) as SearchSourceConnector[]}
|
{showTrigger && (
|
||||||
indexingConnectorIds={indexingConnectorIds}
|
<TooltipIconButton
|
||||||
onBack={handleBackFromMCPList}
|
data-joyride="connector-icon"
|
||||||
onManage={handleStartEdit}
|
tooltip={
|
||||||
onAddAccount={handleAddNewMCPFromList}
|
hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"
|
||||||
addButtonText="Add New MCP Server"
|
|
||||||
/>
|
|
||||||
) : viewingAccountsType ? (
|
|
||||||
<ConnectorAccountsListView
|
|
||||||
connectorType={viewingAccountsType.connectorType}
|
|
||||||
connectorTitle={viewingAccountsType.connectorTitle}
|
|
||||||
connectors={(connectors || []) as SearchSourceConnector[]} // Using Electric SQL + PGlite for real-time connector updates (all connector types)
|
|
||||||
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 Electric SQL for real-time updates
|
|
||||||
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}
|
|
||||||
enableSummary={enableSummary}
|
|
||||||
isSaving={isSaving}
|
|
||||||
isDisconnecting={isDisconnecting}
|
|
||||||
isIndexing={indexingConnectorIds.has(editingConnector.id)}
|
|
||||||
searchSpaceId={searchSpaceId?.toString()}
|
|
||||||
onStartDateChange={setStartDate}
|
|
||||||
onEndDateChange={setEndDate}
|
|
||||||
onPeriodicEnabledChange={setPeriodicEnabled}
|
|
||||||
onFrequencyChange={setFrequencyMinutes}
|
|
||||||
onEnableSummaryChange={setEnableSummary}
|
|
||||||
onSave={() => {
|
|
||||||
startIndexing(editingConnector.id);
|
|
||||||
handleSaveConnector(() => refreshConnectors());
|
|
||||||
}}
|
|
||||||
onDisconnect={() => handleDisconnectConnector(() => refreshConnectors())}
|
|
||||||
onBack={handleBackFromEdit}
|
|
||||||
onQuickIndex={
|
|
||||||
editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR"
|
|
||||||
? () => {
|
|
||||||
startIndexing(editingConnector.id);
|
|
||||||
handleQuickIndexConnector(
|
|
||||||
editingConnector.id,
|
|
||||||
editingConnector.connector_type,
|
|
||||||
stopIndexing,
|
|
||||||
startDate,
|
|
||||||
endDate
|
|
||||||
);
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
onConfigChange={setConnectorConfig}
|
side="bottom"
|
||||||
onNameChange={setConnectorName}
|
className={cn(
|
||||||
/>
|
"size-[34px] rounded-full p-1 flex items-center justify-center transition-colors relative",
|
||||||
) : indexingConfig ? (
|
"hover:bg-muted-foreground/15 dark:hover:bg-muted-foreground/30",
|
||||||
<IndexingConfigurationView
|
"outline-none focus:outline-none focus-visible:outline-none font-semibold text-xs",
|
||||||
config={indexingConfig}
|
"border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none"
|
||||||
connector={
|
)}
|
||||||
indexingConnector
|
aria-label={
|
||||||
? {
|
hasConnectors
|
||||||
...indexingConnector,
|
? `View ${activeConnectorsCount} connectors`
|
||||||
config: indexingConnectorConfig || indexingConnector.config,
|
: "Add your first connector"
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
startDate={startDate}
|
onClick={() => handleOpenChange(true)}
|
||||||
endDate={endDate}
|
|
||||||
periodicEnabled={periodicEnabled}
|
|
||||||
frequencyMinutes={frequencyMinutes}
|
|
||||||
enableSummary={enableSummary}
|
|
||||||
isStartingIndexing={isStartingIndexing}
|
|
||||||
onStartDateChange={setStartDate}
|
|
||||||
onEndDateChange={setEndDate}
|
|
||||||
onPeriodicEnabledChange={setPeriodicEnabled}
|
|
||||||
onFrequencyChange={setFrequencyMinutes}
|
|
||||||
onEnableSummaryChange={setEnableSummary}
|
|
||||||
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 */}
|
{isLoading ? (
|
||||||
<ConnectorDialogHeader
|
<Spinner size="sm" />
|
||||||
activeTab={activeTab}
|
) : (
|
||||||
totalSourceCount={activeConnectorsCount}
|
<>
|
||||||
searchQuery={searchQuery}
|
<Cable className="size-4 stroke-[1.5px]" />
|
||||||
onTabChange={handleTabChange}
|
{activeConnectorsCount > 0 && (
|
||||||
onSearchChange={setSearchQuery}
|
<span className="absolute -top-0.5 right-0 flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-medium rounded-full bg-primary text-primary-foreground shadow-sm select-none">
|
||||||
isScrolled={isScrolled}
|
{activeConnectorsCount > 99 ? "99+" : activeConnectorsCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TooltipIconButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogContent className="max-w-3xl w-[95vw] sm:w-full h-[75vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border ring-0 dark:ring-0 bg-muted dark:bg-muted text-foreground focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 [&>button]:right-4 sm:[&>button]:right-12 [&>button]:top-6 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>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[]} // Using Electric SQL + PGlite for real-time connector updates (all connector types)
|
||||||
|
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 Electric SQL for real-time updates
|
||||||
|
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}
|
||||||
|
enableSummary={enableSummary}
|
||||||
|
isSaving={isSaving}
|
||||||
|
isDisconnecting={isDisconnecting}
|
||||||
|
isIndexing={indexingConnectorIds.has(editingConnector.id)}
|
||||||
|
searchSpaceId={searchSpaceId?.toString()}
|
||||||
|
onStartDateChange={setStartDate}
|
||||||
|
onEndDateChange={setEndDate}
|
||||||
|
onPeriodicEnabledChange={setPeriodicEnabled}
|
||||||
|
onFrequencyChange={setFrequencyMinutes}
|
||||||
|
onEnableSummaryChange={setEnableSummary}
|
||||||
|
onSave={() => {
|
||||||
|
startIndexing(editingConnector.id);
|
||||||
|
handleSaveConnector(() => refreshConnectors());
|
||||||
|
}}
|
||||||
|
onDisconnect={() => handleDisconnectConnector(() => refreshConnectors())}
|
||||||
|
onBack={handleBackFromEdit}
|
||||||
|
onQuickIndex={
|
||||||
|
editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR"
|
||||||
|
? () => {
|
||||||
|
startIndexing(editingConnector.id);
|
||||||
|
handleQuickIndexConnector(
|
||||||
|
editingConnector.id,
|
||||||
|
editingConnector.connector_type,
|
||||||
|
stopIndexing,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onConfigChange={setConnectorConfig}
|
||||||
|
onNameChange={setConnectorName}
|
||||||
|
/>
|
||||||
|
) : indexingConfig ? (
|
||||||
|
<IndexingConfigurationView
|
||||||
|
config={indexingConfig}
|
||||||
|
connector={
|
||||||
|
indexingConnector
|
||||||
|
? {
|
||||||
|
...indexingConnector,
|
||||||
|
config: indexingConnectorConfig || indexingConnector.config,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
periodicEnabled={periodicEnabled}
|
||||||
|
frequencyMinutes={frequencyMinutes}
|
||||||
|
enableSummary={enableSummary}
|
||||||
|
isStartingIndexing={isStartingIndexing}
|
||||||
|
onStartDateChange={setStartDate}
|
||||||
|
onEndDateChange={setEndDate}
|
||||||
|
onPeriodicEnabledChange={setPeriodicEnabled}
|
||||||
|
onFrequencyChange={setFrequencyMinutes}
|
||||||
|
onEnableSummaryChange={setEnableSummary}
|
||||||
|
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 */}
|
{/* Content */}
|
||||||
<div className="flex-1 min-h-0 relative overflow-hidden">
|
<div className="flex-1 min-h-0 relative overflow-hidden">
|
||||||
<div className="h-full overflow-y-auto" onScroll={handleScroll}>
|
<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">
|
<div className="px-4 sm:px-12 py-4 sm:py-8 pb-12 sm:pb-16">
|
||||||
{/* LLM Configuration Warning */}
|
{/* LLM Configuration Warning */}
|
||||||
{!llmConfigLoading && !hasDocumentSummaryLLM && (
|
{!llmConfigLoading && !hasDocumentSummaryLLM && (
|
||||||
<Alert variant="destructive" className="mb-6">
|
<Alert variant="destructive" className="mb-6">
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
<AlertTitle>LLM Configuration Required</AlertTitle>
|
<AlertTitle>LLM Configuration Required</AlertTitle>
|
||||||
<AlertDescription className="mt-2">
|
<AlertDescription className="mt-2">
|
||||||
<p className="mb-3">
|
<p className="mb-3">
|
||||||
{isAutoMode && !hasGlobalConfigs
|
{isAutoMode && !hasGlobalConfigs
|
||||||
? "Auto mode is selected but no global LLM configurations are available. Please configure a custom LLM in Settings to process and summarize documents from your connected sources."
|
? "Auto mode is selected but no global LLM configurations are available. Please configure a custom LLM in Settings to process and summarize documents from your connected sources."
|
||||||
: "You need to configure a Document Summary LLM before adding connectors. This LLM is used to process and summarize documents from your connected sources."}
|
: "You need to configure a Document Summary LLM before adding connectors. This LLM is used to process and summarize documents from your connected sources."}
|
||||||
</p>
|
</p>
|
||||||
<Button asChild size="sm" variant="outline">
|
<Button asChild size="sm" variant="outline">
|
||||||
<Link href={`/dashboard/${searchSpaceId}/settings?tab=models`}>
|
<Link href={`/dashboard/${searchSpaceId}/settings?tab=models`}>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
Go to Settings
|
Go to Settings
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TabsContent value="all" className="m-0">
|
<TabsContent value="all" className="m-0">
|
||||||
<AllConnectorsTab
|
<AllConnectorsTab
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
searchSpaceId={searchSpaceId}
|
||||||
|
connectedTypes={connectedTypes}
|
||||||
|
connectingId={connectingId}
|
||||||
|
allConnectors={connectors}
|
||||||
|
documentTypeCounts={documentTypeCounts}
|
||||||
|
indexingConnectorIds={indexingConnectorIds}
|
||||||
|
onConnectOAuth={hasDocumentSummaryLLM ? handleConnectOAuth : () => {}}
|
||||||
|
onConnectNonOAuth={hasDocumentSummaryLLM ? handleConnectNonOAuth : () => {}}
|
||||||
|
onCreateWebcrawler={
|
||||||
|
hasDocumentSummaryLLM ? handleCreateWebcrawler : () => {}
|
||||||
|
}
|
||||||
|
onCreateYouTubeCrawler={
|
||||||
|
hasDocumentSummaryLLM ? handleCreateYouTubeCrawler : () => {}
|
||||||
|
}
|
||||||
|
onManage={handleStartEdit}
|
||||||
|
onViewAccountsList={handleViewAccountsList}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<ActiveConnectorsTab
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
searchSpaceId={searchSpaceId}
|
hasSources={hasSources}
|
||||||
connectedTypes={connectedTypes}
|
totalSourceCount={totalSourceCount}
|
||||||
connectingId={connectingId}
|
activeDocumentTypes={activeDocumentTypes}
|
||||||
allConnectors={connectors}
|
connectors={connectors as SearchSourceConnector[]}
|
||||||
documentTypeCounts={documentTypeCounts}
|
|
||||||
indexingConnectorIds={indexingConnectorIds}
|
indexingConnectorIds={indexingConnectorIds}
|
||||||
onConnectOAuth={hasDocumentSummaryLLM ? handleConnectOAuth : () => {}}
|
onTabChange={handleTabChange}
|
||||||
onConnectNonOAuth={hasDocumentSummaryLLM ? handleConnectNonOAuth : () => {}}
|
|
||||||
onCreateWebcrawler={hasDocumentSummaryLLM ? handleCreateWebcrawler : () => {}}
|
|
||||||
onCreateYouTubeCrawler={
|
|
||||||
hasDocumentSummaryLLM ? handleCreateYouTubeCrawler : () => {}
|
|
||||||
}
|
|
||||||
onManage={handleStartEdit}
|
onManage={handleStartEdit}
|
||||||
onViewAccountsList={handleViewAccountsList}
|
onViewAccountsList={handleViewAccountsList}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
|
||||||
<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-muted via-muted/80 to-transparent pointer-events-none z-10" />
|
||||||
</div>
|
</div>
|
||||||
{/* Bottom fade shadow */}
|
</Tabs>
|
||||||
<div className="absolute bottom-0 left-0 right-0 h-7 bg-linear-to-t from-muted via-muted/80 to-transparent pointer-events-none z-10" />
|
)}
|
||||||
</div>
|
</DialogContent>
|
||||||
</Tabs>
|
</Dialog>
|
||||||
)}
|
);
|
||||||
</DialogContent>
|
}
|
||||||
</Dialog>
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ConnectorIndicator.displayName = "ConnectorIndicator";
|
ConnectorIndicator.displayName = "ConnectorIndicator";
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ import {
|
||||||
FileText,
|
FileText,
|
||||||
FolderClosed,
|
FolderClosed,
|
||||||
Image,
|
Image,
|
||||||
Loader2,
|
|
||||||
Presentation,
|
Presentation,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { ComposioDriveFolderTree } from "@/components/connectors/composio-drive-folder-tree";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import {
|
import {
|
||||||
|
|
@ -23,7 +23,6 @@ import {
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||||
import { type PickerResult, useGooglePicker } from "@/hooks/use-google-picker";
|
|
||||||
|
|
||||||
interface ComposioDriveConfigProps {
|
interface ComposioDriveConfigProps {
|
||||||
connector: SearchSourceConnector;
|
connector: SearchSourceConnector;
|
||||||
|
|
@ -31,7 +30,7 @@ interface ComposioDriveConfigProps {
|
||||||
onNameChange?: (name: string) => void;
|
onNameChange?: (name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectedItem {
|
interface SelectedFolder {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
@ -94,18 +93,20 @@ export const ComposioDriveConfig: FC<ComposioDriveConfigProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const isIndexable = connector.config?.is_indexable as boolean;
|
const isIndexable = connector.config?.is_indexable as boolean;
|
||||||
|
|
||||||
const existingFolders = (connector.config?.selected_folders as SelectedItem[] | undefined) || [];
|
const existingFolders =
|
||||||
const existingFiles = (connector.config?.selected_files as SelectedItem[] | undefined) || [];
|
(connector.config?.selected_folders as SelectedFolder[] | undefined) || [];
|
||||||
|
const existingFiles = (connector.config?.selected_files as SelectedFolder[] | undefined) || [];
|
||||||
const existingIndexingOptions =
|
const existingIndexingOptions =
|
||||||
(connector.config?.indexing_options as IndexingOptions | undefined) || DEFAULT_INDEXING_OPTIONS;
|
(connector.config?.indexing_options as IndexingOptions | undefined) || DEFAULT_INDEXING_OPTIONS;
|
||||||
|
|
||||||
const [selectedFolders, setSelectedFolders] = useState<SelectedItem[]>(existingFolders);
|
const [selectedFolders, setSelectedFolders] = useState<SelectedFolder[]>(existingFolders);
|
||||||
const [selectedFiles, setSelectedFiles] = useState<SelectedItem[]>(existingFiles);
|
const [selectedFiles, setSelectedFiles] = useState<SelectedFolder[]>(existingFiles);
|
||||||
|
const [showFolderSelector, setShowFolderSelector] = useState(false);
|
||||||
const [indexingOptions, setIndexingOptions] = useState<IndexingOptions>(existingIndexingOptions);
|
const [indexingOptions, setIndexingOptions] = useState<IndexingOptions>(existingIndexingOptions);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const folders = (connector.config?.selected_folders as SelectedItem[] | undefined) || [];
|
const folders = (connector.config?.selected_folders as SelectedFolder[] | undefined) || [];
|
||||||
const files = (connector.config?.selected_files as SelectedItem[] | undefined) || [];
|
const files = (connector.config?.selected_files as SelectedFolder[] | undefined) || [];
|
||||||
const options =
|
const options =
|
||||||
(connector.config?.indexing_options as IndexingOptions | undefined) ||
|
(connector.config?.indexing_options as IndexingOptions | undefined) ||
|
||||||
DEFAULT_INDEXING_OPTIONS;
|
DEFAULT_INDEXING_OPTIONS;
|
||||||
|
|
@ -115,8 +116,8 @@ export const ComposioDriveConfig: FC<ComposioDriveConfigProps> = ({
|
||||||
}, [connector.config]);
|
}, [connector.config]);
|
||||||
|
|
||||||
const updateConfig = (
|
const updateConfig = (
|
||||||
folders: SelectedItem[],
|
folders: SelectedFolder[],
|
||||||
files: SelectedItem[],
|
files: SelectedFolder[],
|
||||||
options: IndexingOptions
|
options: IndexingOptions
|
||||||
) => {
|
) => {
|
||||||
if (onConfigChange) {
|
if (onConfigChange) {
|
||||||
|
|
@ -129,26 +130,15 @@ export const ComposioDriveConfig: FC<ComposioDriveConfigProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePicked = useCallback(
|
const handleSelectFolders = (folders: SelectedFolder[]) => {
|
||||||
(result: PickerResult) => {
|
setSelectedFolders(folders);
|
||||||
const folders = result.folders.map((f) => ({ id: f.id, name: f.name }));
|
updateConfig(folders, selectedFiles, indexingOptions);
|
||||||
const files = result.files.map((f) => ({ id: f.id, name: f.name }));
|
};
|
||||||
setSelectedFolders(folders);
|
|
||||||
setSelectedFiles(files);
|
|
||||||
updateConfig(folders, files, indexingOptions);
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[indexingOptions, connector.config]
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const handleSelectFiles = (files: SelectedFolder[]) => {
|
||||||
openPicker,
|
setSelectedFiles(files);
|
||||||
loading: pickerLoading,
|
updateConfig(selectedFolders, files, indexingOptions);
|
||||||
error: pickerError,
|
};
|
||||||
} = useGooglePicker({
|
|
||||||
connectorId: connector.id,
|
|
||||||
onPicked: handlePicked,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleIndexingOptionChange = (key: keyof IndexingOptions, value: number | boolean) => {
|
const handleIndexingOptionChange = (key: keyof IndexingOptions, value: number | boolean) => {
|
||||||
const newOptions = { ...indexingOptions, [key]: value };
|
const newOptions = { ...indexingOptions, [key]: value };
|
||||||
|
|
@ -157,13 +147,13 @@ export const ComposioDriveConfig: FC<ComposioDriveConfigProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFolder = (folderId: string) => {
|
const handleRemoveFolder = (folderId: string) => {
|
||||||
const newFolders = selectedFolders.filter((f) => f.id !== folderId);
|
const newFolders = selectedFolders.filter((folder) => folder.id !== folderId);
|
||||||
setSelectedFolders(newFolders);
|
setSelectedFolders(newFolders);
|
||||||
updateConfig(newFolders, selectedFiles, indexingOptions);
|
updateConfig(newFolders, selectedFiles, indexingOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFile = (fileId: string) => {
|
const handleRemoveFile = (fileId: string) => {
|
||||||
const newFiles = selectedFiles.filter((f) => f.id !== fileId);
|
const newFiles = selectedFiles.filter((file) => file.id !== fileId);
|
||||||
setSelectedFiles(newFiles);
|
setSelectedFiles(newFiles);
|
||||||
updateConfig(selectedFolders, newFiles, indexingOptions);
|
updateConfig(selectedFolders, newFiles, indexingOptions);
|
||||||
};
|
};
|
||||||
|
|
@ -242,18 +232,35 @@ export const ComposioDriveConfig: FC<ComposioDriveConfigProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
{showFolderSelector ? (
|
||||||
type="button"
|
<div className="space-y-2 sm:space-y-3">
|
||||||
variant="outline"
|
<ComposioDriveFolderTree
|
||||||
onClick={openPicker}
|
connectorId={connector.id}
|
||||||
disabled={pickerLoading}
|
selectedFolders={selectedFolders}
|
||||||
className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 hover:bg-slate-400/10 dark:hover:bg-white/10 text-xs sm:text-sm h-8 sm:h-9"
|
onSelectFolders={handleSelectFolders}
|
||||||
>
|
selectedFiles={selectedFiles}
|
||||||
{pickerLoading && <Loader2 className="size-3.5 mr-1.5 animate-spin" />}
|
onSelectFiles={handleSelectFiles}
|
||||||
{totalSelected > 0 ? "Change Selection" : "Select from Google Drive"}
|
/>
|
||||||
</Button>
|
<Button
|
||||||
|
type="button"
|
||||||
{pickerError && <p className="text-xs text-destructive">{pickerError}</p>}
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowFolderSelector(false)}
|
||||||
|
className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 hover:bg-slate-400/10 dark:hover:bg-white/10 text-xs sm:text-sm h-8 sm:h-9"
|
||||||
|
>
|
||||||
|
Done Selecting
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowFolderSelector(true)}
|
||||||
|
className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 hover:bg-slate-400/10 dark:hover:bg-white/10 text-xs sm:text-sm h-8 sm:h-9"
|
||||||
|
>
|
||||||
|
{totalSelected > 0 ? "Change Selection" : "Select Folders & Files"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Indexing Options */}
|
{/* Indexing Options */}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { EnumConnectorName } from "@/contracts/enums/connector";
|
||||||
|
|
||||||
// OAuth Connectors (Quick Connect)
|
// OAuth Connectors (Quick Connect)
|
||||||
export const OAUTH_CONNECTORS = [
|
export const OAUTH_CONNECTORS = [
|
||||||
// // Uncomment for managed Google Connections
|
|
||||||
// {
|
// {
|
||||||
// id: "google-drive-connector",
|
// id: "google-drive-connector",
|
||||||
// title: "Google Drive",
|
// title: "Google Drive",
|
||||||
|
|
@ -249,19 +248,84 @@ export interface AutoIndexConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AUTO_INDEX_DEFAULTS: Record<string, AutoIndexConfig> = {
|
export const AUTO_INDEX_DEFAULTS: Record<string, AutoIndexConfig> = {
|
||||||
[EnumConnectorName.GOOGLE_GMAIL_CONNECTOR]: { daysBack: 30, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 30 days of emails." },
|
[EnumConnectorName.GOOGLE_GMAIL_CONNECTOR]: {
|
||||||
[EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR]: { daysBack: 30, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 30 days of emails." },
|
daysBack: 30,
|
||||||
[EnumConnectorName.SLACK_CONNECTOR]: { daysBack: 30, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 30 days of messages." },
|
daysForward: 0,
|
||||||
[EnumConnectorName.DISCORD_CONNECTOR]: { daysBack: 30, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 30 days of messages." },
|
frequencyMinutes: 1440,
|
||||||
[EnumConnectorName.TEAMS_CONNECTOR]: { daysBack: 30, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 30 days of messages." },
|
syncDescription: "Syncing your last 30 days of emails.",
|
||||||
[EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR]: { daysBack: 90, daysForward: 90, frequencyMinutes: 1440, syncDescription: "Syncing 90 days of past and upcoming events." },
|
},
|
||||||
[EnumConnectorName.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR]: { daysBack: 90, daysForward: 90, frequencyMinutes: 1440, syncDescription: "Syncing 90 days of past and upcoming events." },
|
[EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR]: {
|
||||||
[EnumConnectorName.LINEAR_CONNECTOR]: { daysBack: 90, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 90 days of issues." },
|
daysBack: 30,
|
||||||
[EnumConnectorName.JIRA_CONNECTOR]: { daysBack: 90, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 90 days of issues." },
|
daysForward: 0,
|
||||||
[EnumConnectorName.CLICKUP_CONNECTOR]: { daysBack: 90, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your last 90 days of tasks." },
|
frequencyMinutes: 1440,
|
||||||
[EnumConnectorName.NOTION_CONNECTOR]: { daysBack: 365, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your pages." },
|
syncDescription: "Syncing your last 30 days of emails.",
|
||||||
[EnumConnectorName.CONFLUENCE_CONNECTOR]: { daysBack: 365, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your documentation." },
|
},
|
||||||
[EnumConnectorName.AIRTABLE_CONNECTOR]: { daysBack: 365, daysForward: 0, frequencyMinutes: 1440, syncDescription: "Syncing your bases." },
|
[EnumConnectorName.SLACK_CONNECTOR]: {
|
||||||
|
daysBack: 30,
|
||||||
|
daysForward: 0,
|
||||||
|
frequencyMinutes: 1440,
|
||||||
|
syncDescription: "Syncing your last 30 days of messages.",
|
||||||
|
},
|
||||||
|
[EnumConnectorName.DISCORD_CONNECTOR]: {
|
||||||
|
daysBack: 30,
|
||||||
|
daysForward: 0,
|
||||||
|
frequencyMinutes: 1440,
|
||||||
|
syncDescription: "Syncing your last 30 days of messages.",
|
||||||
|
},
|
||||||
|
[EnumConnectorName.TEAMS_CONNECTOR]: {
|
||||||
|
daysBack: 30,
|
||||||
|
daysForward: 0,
|
||||||
|
frequencyMinutes: 1440,
|
||||||
|
syncDescription: "Syncing your last 30 days of messages.",
|
||||||
|
},
|
||||||
|
[EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR]: {
|
||||||
|
daysBack: 90,
|
||||||
|
daysForward: 90,
|
||||||
|
frequencyMinutes: 1440,
|
||||||
|
syncDescription: "Syncing 90 days of past and upcoming events.",
|
||||||
|
},
|
||||||
|
[EnumConnectorName.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR]: {
|
||||||
|
daysBack: 90,
|
||||||
|
daysForward: 90,
|
||||||
|
frequencyMinutes: 1440,
|
||||||
|
syncDescription: "Syncing 90 days of past and upcoming events.",
|
||||||
|
},
|
||||||
|
[EnumConnectorName.LINEAR_CONNECTOR]: {
|
||||||
|
daysBack: 90,
|
||||||
|
daysForward: 0,
|
||||||
|
frequencyMinutes: 1440,
|
||||||
|
syncDescription: "Syncing your last 90 days of issues.",
|
||||||
|
},
|
||||||
|
[EnumConnectorName.JIRA_CONNECTOR]: {
|
||||||
|
daysBack: 90,
|
||||||
|
daysForward: 0,
|
||||||
|
frequencyMinutes: 1440,
|
||||||
|
syncDescription: "Syncing your last 90 days of issues.",
|
||||||
|
},
|
||||||
|
[EnumConnectorName.CLICKUP_CONNECTOR]: {
|
||||||
|
daysBack: 90,
|
||||||
|
daysForward: 0,
|
||||||
|
frequencyMinutes: 1440,
|
||||||
|
syncDescription: "Syncing your last 90 days of tasks.",
|
||||||
|
},
|
||||||
|
[EnumConnectorName.NOTION_CONNECTOR]: {
|
||||||
|
daysBack: 365,
|
||||||
|
daysForward: 0,
|
||||||
|
frequencyMinutes: 1440,
|
||||||
|
syncDescription: "Syncing your pages.",
|
||||||
|
},
|
||||||
|
[EnumConnectorName.CONFLUENCE_CONNECTOR]: {
|
||||||
|
daysBack: 365,
|
||||||
|
daysForward: 0,
|
||||||
|
frequencyMinutes: 1440,
|
||||||
|
syncDescription: "Syncing your documentation.",
|
||||||
|
},
|
||||||
|
[EnumConnectorName.AIRTABLE_CONNECTOR]: {
|
||||||
|
daysBack: 365,
|
||||||
|
daysForward: 0,
|
||||||
|
frequencyMinutes: 1440,
|
||||||
|
syncDescription: "Syncing your bases.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AUTO_INDEX_CONNECTOR_TYPES = new Set<string>(Object.keys(AUTO_INDEX_DEFAULTS));
|
export const AUTO_INDEX_CONNECTOR_TYPES = new Set<string>(Object.keys(AUTO_INDEX_DEFAULTS));
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
|
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
|
||||||
|
|
||||||
export interface PickerItem {
|
export interface PickerItem {
|
||||||
|
|
@ -21,6 +21,8 @@ interface UseGooglePickerOptions {
|
||||||
|
|
||||||
const PICKER_SCRIPT_URL = "https://apis.google.com/js/api.js";
|
const PICKER_SCRIPT_URL = "https://apis.google.com/js/api.js";
|
||||||
const FOLDER_MIME = "application/vnd.google-apps.folder";
|
const FOLDER_MIME = "application/vnd.google-apps.folder";
|
||||||
|
export const PICKER_OPEN_EVENT = "google-picker-open";
|
||||||
|
export const PICKER_CLOSE_EVENT = "google-picker-close";
|
||||||
|
|
||||||
let scriptLoadPromise: Promise<void> | null = null;
|
let scriptLoadPromise: Promise<void> | null = null;
|
||||||
let pickerApiPromise: Promise<void> | null = null;
|
let pickerApiPromise: Promise<void> | null = null;
|
||||||
|
|
@ -68,6 +70,25 @@ export function useGooglePicker({ connectorId, onPicked }: UseGooglePickerOption
|
||||||
const onPickedRef = useRef(onPicked);
|
const onPickedRef = useRef(onPicked);
|
||||||
onPickedRef.current = onPicked;
|
onPickedRef.current = onPicked;
|
||||||
const openingRef = useRef(false);
|
const openingRef = useRef(false);
|
||||||
|
const pickerRef = useRef<google.picker.Picker | null>(null);
|
||||||
|
|
||||||
|
const closePicker = useCallback(() => {
|
||||||
|
if (!pickerRef.current) return;
|
||||||
|
window.dispatchEvent(new Event(PICKER_CLOSE_EVENT));
|
||||||
|
pickerRef.current.dispose();
|
||||||
|
pickerRef.current = null;
|
||||||
|
openingRef.current = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape" && pickerRef.current) {
|
||||||
|
closePicker();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", onEscape);
|
||||||
|
return () => window.removeEventListener("keydown", onEscape);
|
||||||
|
}, [closePicker]);
|
||||||
|
|
||||||
const openPicker = useCallback(async () => {
|
const openPicker = useCallback(async () => {
|
||||||
if (openingRef.current) return;
|
if (openingRef.current) return;
|
||||||
|
|
@ -87,15 +108,18 @@ export function useGooglePicker({ connectorId, onPicked }: UseGooglePickerOption
|
||||||
.setIncludeFolders(true)
|
.setIncludeFolders(true)
|
||||||
.setSelectFolderEnabled(true);
|
.setSelectFolderEnabled(true);
|
||||||
|
|
||||||
let pickerInstance: google.picker.Picker | null = null;
|
const builder = new google.picker.PickerBuilder()
|
||||||
|
|
||||||
const picker = new google.picker.PickerBuilder()
|
|
||||||
.addView(docsView)
|
.addView(docsView)
|
||||||
.enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
|
.enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
|
||||||
.setOAuthToken(access_token)
|
.setOAuthToken(access_token)
|
||||||
.setDeveloperKey(picker_api_key)
|
|
||||||
.setOrigin(window.location.protocol + "//" + window.location.host)
|
.setOrigin(window.location.protocol + "//" + window.location.host)
|
||||||
.setTitle("Select files and folders to index")
|
.setTitle("Select files and folders to index");
|
||||||
|
|
||||||
|
if (picker_api_key) {
|
||||||
|
builder.setDeveloperKey(picker_api_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const picker = builder
|
||||||
.setCallback((data: google.picker.ResponseObject) => {
|
.setCallback((data: google.picker.ResponseObject) => {
|
||||||
const action = data[google.picker.Response.ACTION];
|
const action = data[google.picker.Response.ACTION];
|
||||||
|
|
||||||
|
|
@ -128,16 +152,16 @@ export function useGooglePicker({ connectorId, onPicked }: UseGooglePickerOption
|
||||||
action === google.picker.Action.CANCEL ||
|
action === google.picker.Action.CANCEL ||
|
||||||
action === google.picker.Action.ERROR
|
action === google.picker.Action.ERROR
|
||||||
) {
|
) {
|
||||||
pickerInstance?.dispose();
|
closePicker();
|
||||||
pickerInstance = null;
|
|
||||||
openingRef.current = false;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
pickerInstance = picker;
|
pickerRef.current = picker;
|
||||||
|
window.dispatchEvent(new Event(PICKER_OPEN_EVENT));
|
||||||
picker.setVisible(true);
|
picker.setVisible(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
window.dispatchEvent(new Event(PICKER_CLOSE_EVENT));
|
||||||
openingRef.current = false;
|
openingRef.current = false;
|
||||||
const msg = err instanceof Error ? err.message : "Failed to open Google Picker";
|
const msg = err instanceof Error ? err.message : "Failed to open Google Picker";
|
||||||
setError(msg);
|
setError(msg);
|
||||||
|
|
@ -145,7 +169,7 @@ export function useGooglePicker({ connectorId, onPicked }: UseGooglePickerOption
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [connectorId]);
|
}, [connectorId, closePicker]);
|
||||||
|
|
||||||
return { openPicker, loading, error };
|
return { openPicker, closePicker, loading, error };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -273,7 +273,7 @@ class ConnectorsApiService {
|
||||||
return baseApiService.get<{
|
return baseApiService.get<{
|
||||||
access_token: string;
|
access_token: string;
|
||||||
client_id: string;
|
client_id: string;
|
||||||
picker_api_key: string;
|
picker_api_key: string | null;
|
||||||
}>(`/api/v1/connectors/${connectorId}/drive-picker-token`);
|
}>(`/api/v1/connectors/${connectorId}/drive-picker-token`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue