feat: Add Tavily API connector support, including configuration forms and connection handling, enhance connector dialog with non-OAuth connection logic, and improve state management for connector creation.

This commit is contained in:
Anish Sarkar 2025-12-31 12:35:43 +05:30
parent 5d1859db17
commit 880b3cc4bf
11 changed files with 711 additions and 86 deletions

View file

@ -2,11 +2,13 @@
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { Cable, Loader2 } from "lucide-react"; import { Cable, Loader2 } from "lucide-react";
import { type FC, useMemo } from "react"; import { type FC, useMemo, useEffect } from "react";
import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms"; import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms";
import { useLogsSummary } from "@/hooks/use-logs"; import { useLogsSummary } from "@/hooks/use-logs";
import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -21,16 +23,13 @@ import { AllConnectorsTab } from "./connector-popup/tabs/all-connectors-tab";
import { ActiveConnectorsTab } from "./connector-popup/tabs/active-connectors-tab"; import { ActiveConnectorsTab } from "./connector-popup/tabs/active-connectors-tab";
import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header"; import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header";
import { ConnectorEditView } from "./connector-popup/connector-configs/views/connector-edit-view"; import { ConnectorEditView } from "./connector-popup/connector-configs/views/connector-edit-view";
import { ConnectorConnectView } from "./connector-popup/connector-configs/views/connector-connect-view";
import { IndexingConfigurationView } from "./connector-popup/connector-configs/views/indexing-configuration-view"; import { IndexingConfigurationView } from "./connector-popup/connector-configs/views/indexing-configuration-view";
import { useConnectorDialog } from "./connector-popup/hooks/use-connector-dialog"; import { useConnectorDialog } from "./connector-popup/hooks/use-connector-dialog";
import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { SearchSourceConnector } from "@/contracts/types/connector.types";
export const ConnectorIndicator: FC = () => { export const ConnectorIndicator: FC = () => {
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
const { connectors, isLoading: connectorsLoading, refreshConnectors } = useSearchSourceConnectors(
false,
searchSpaceId ? Number(searchSpaceId) : undefined
);
const { data: documentTypeCounts, isLoading: documentTypesLoading } = const { data: documentTypeCounts, isLoading: documentTypesLoading } =
useAtomValue(documentTypeCountsAtom); useAtomValue(documentTypeCountsAtom);
@ -44,22 +43,6 @@ export const ConnectorIndicator: FC = () => {
} }
); );
// Get connector IDs that are currently being indexed
const indexingConnectorIds = useMemo(() => {
if (!logsSummary?.active_tasks) return new Set<number>();
return new Set(
logsSummary.active_tasks
.filter((task) => task.source?.includes("connector_indexing"))
.map((task) => {
const match = task.source?.match(/connector[_-]?(\d+)/i);
return match ? parseInt(match[1], 10) : null;
})
.filter((id): id is number => id !== null)
);
}, [logsSummary?.active_tasks]);
const isLoading = connectorsLoading || documentTypesLoading;
// Use the custom hook for dialog state management // Use the custom hook for dialog state management
const { const {
isOpen, isOpen,
@ -71,6 +54,8 @@ export const ConnectorIndicator: FC = () => {
indexingConnector, indexingConnector,
indexingConnectorConfig, indexingConnectorConfig,
editingConnector, editingConnector,
connectingConnectorType,
isCreatingConnector,
startDate, startDate,
endDate, endDate,
isStartingIndexing, isStartingIndexing,
@ -88,19 +73,81 @@ export const ConnectorIndicator: FC = () => {
handleTabChange, handleTabChange,
handleScroll, handleScroll,
handleConnectOAuth, handleConnectOAuth,
handleConnectNonOAuth,
handleCreateWebcrawler, handleCreateWebcrawler,
handleCreateYouTube, handleCreateYouTube,
handleSubmitConnectForm,
handleStartIndexing, handleStartIndexing,
handleSkipIndexing, handleSkipIndexing,
handleStartEdit, handleStartEdit,
handleSaveConnector, handleSaveConnector,
handleDisconnectConnector, handleDisconnectConnector,
handleBackFromEdit, handleBackFromEdit,
handleBackFromConnect,
connectorConfig, connectorConfig,
setConnectorConfig, setConnectorConfig,
setIndexingConnectorConfig, setIndexingConnectorConfig,
setConnectorName,
} = useConnectorDialog(); } = useConnectorDialog();
// Fetch connectors using React Query with conditional refetchInterval
// This automatically refetches when mutations invalidate the cache (event-driven)
// and also polls when dialog is open to catch external changes
const {
data: connectors = [],
isLoading: connectorsLoading,
refetch: refreshConnectors,
} = useQuery({
queryKey: cacheKeys.connectors.all(searchSpaceId || ""),
queryFn: () =>
connectorsApiService.getConnectors({
queryParams: {
search_space_id: searchSpaceId ? Number(searchSpaceId) : undefined,
},
}),
enabled: !!searchSpaceId,
staleTime: 5 * 60 * 1000, // 5 minutes (same as connectorsAtom)
// Poll when dialog is open to catch external changes
refetchInterval: isOpen ? 5000 : false, // 5 seconds when open, no polling when closed
});
const queryClient = useQueryClient();
// Also refresh document type counts when dialog is open
useEffect(() => {
if (!isOpen || !searchSpaceId) return;
const POLL_INTERVAL = 5000; // 5 seconds, same as connectors
const intervalId = setInterval(() => {
// Invalidate document type counts to refresh active document types
queryClient.invalidateQueries({
queryKey: cacheKeys.documents.typeCounts(searchSpaceId),
});
}, POLL_INTERVAL);
// Cleanup interval on unmount or when dialog closes
return () => {
clearInterval(intervalId);
};
}, [isOpen, searchSpaceId, queryClient]);
// Get connector IDs that are currently being indexed
const indexingConnectorIds = useMemo(() => {
if (!logsSummary?.active_tasks) return new Set<number>();
return new Set(
logsSummary.active_tasks
.filter((task) => task.source?.includes("connector_indexing"))
.map((task) => {
const match = task.source?.match(/connector[_-]?(\d+)/i);
return match ? parseInt(match[1], 10) : null;
})
.filter((id): id is number => id !== null)
);
}, [logsSummary?.active_tasks]);
const isLoading = connectorsLoading || documentTypesLoading;
// Get document types that have documents in the search space // Get document types that have documents in the search space
const activeDocumentTypes = documentTypeCounts const activeDocumentTypes = documentTypeCounts
? Object.entries(documentTypeCounts).filter(([, count]) => count > 0) ? Object.entries(documentTypeCounts).filter(([, count]) => count > 0)
@ -148,12 +195,20 @@ export const ConnectorIndicator: FC = () => {
</TooltipIconButton> </TooltipIconButton>
<DialogContent className="max-w-3xl w-[95vw] sm:w-full h-[90vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border bg-muted text-foreground [&>button]:right-6 sm:[&>button]:right-12 [&>button]:top-8 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button_svg]:size-5"> <DialogContent className="max-w-3xl w-[95vw] sm:w-full h-[90vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border bg-muted text-foreground [&>button]:right-6 sm:[&>button]:right-12 [&>button]:top-8 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button_svg]:size-5">
{/* Connector Edit View - shown when editing existing connector */} {/* Connector Connect View - shown when connecting non-OAuth connectors */}
{editingConnector ? ( {connectingConnectorType ? (
<ConnectorConnectView
connectorType={connectingConnectorType}
onSubmit={handleSubmitConnectForm}
onBack={handleBackFromConnect}
isSubmitting={isCreatingConnector}
/>
) : editingConnector ? (
<ConnectorEditView <ConnectorEditView
connector={{ connector={{
...editingConnector, ...editingConnector,
config: connectorConfig || editingConnector.config, config: connectorConfig || editingConnector.config,
name: editingConnector.name,
}} }}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
@ -165,10 +220,11 @@ export const ConnectorIndicator: FC = () => {
onEndDateChange={setEndDate} onEndDateChange={setEndDate}
onPeriodicEnabledChange={setPeriodicEnabled} onPeriodicEnabledChange={setPeriodicEnabled}
onFrequencyChange={setFrequencyMinutes} onFrequencyChange={setFrequencyMinutes}
onSave={() => handleSaveConnector(refreshConnectors)} onSave={() => handleSaveConnector(() => refreshConnectors())}
onDisconnect={() => handleDisconnectConnector(refreshConnectors)} onDisconnect={() => handleDisconnectConnector(() => refreshConnectors())}
onBack={handleBackFromEdit} onBack={handleBackFromEdit}
onConfigChange={setConnectorConfig} onConfigChange={setConnectorConfig}
onNameChange={setConnectorName}
/> />
) : indexingConfig ? ( ) : indexingConfig ? (
<IndexingConfigurationView <IndexingConfigurationView
@ -187,7 +243,7 @@ export const ConnectorIndicator: FC = () => {
onPeriodicEnabledChange={setPeriodicEnabled} onPeriodicEnabledChange={setPeriodicEnabled}
onFrequencyChange={setFrequencyMinutes} onFrequencyChange={setFrequencyMinutes}
onConfigChange={setIndexingConnectorConfig} onConfigChange={setIndexingConnectorConfig}
onStartIndexing={() => handleStartIndexing(refreshConnectors)} onStartIndexing={() => handleStartIndexing(() => refreshConnectors())}
onSkip={handleSkipIndexing} onSkip={handleSkipIndexing}
/> />
) : ( ) : (
@ -207,17 +263,18 @@ export const ConnectorIndicator: FC = () => {
<div className="h-full overflow-y-auto" onScroll={handleScroll}> <div className="h-full overflow-y-auto" onScroll={handleScroll}>
<div className="px-6 sm:px-12 py-6 sm:py-8 pb-16 sm:pb-16"> <div className="px-6 sm:px-12 py-6 sm:py-8 pb-16 sm:pb-16">
<TabsContent value="all" className="m-0"> <TabsContent value="all" className="m-0">
<AllConnectorsTab <AllConnectorsTab
searchQuery={searchQuery} searchQuery={searchQuery}
searchSpaceId={searchSpaceId} searchSpaceId={searchSpaceId}
connectedTypes={connectedTypes} connectedTypes={connectedTypes}
connectingId={connectingId} connectingId={connectingId}
allConnectors={allConnectors} allConnectors={allConnectors}
onConnectOAuth={handleConnectOAuth} onConnectOAuth={handleConnectOAuth}
onCreateWebcrawler={handleCreateWebcrawler} onConnectNonOAuth={handleConnectNonOAuth}
onCreateYouTube={handleCreateYouTube} onCreateWebcrawler={handleCreateWebcrawler}
onManage={handleStartEdit} onCreateYouTube={handleCreateYouTube}
/> onManage={handleStartEdit}
/>
</TabsContent> </TabsContent>
<ActiveConnectorsTab <ActiveConnectorsTab

View file

@ -0,0 +1,92 @@
"use client";
import { KeyRound } from "lucide-react";
import { useState, useEffect } from "react";
import type { FC } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import type { ConnectorConfigProps } from "../index";
export interface TavilyApiConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
export const TavilyApiConfig: FC<TavilyApiConfigProps> = ({
connector,
onConfigChange,
onNameChange,
}) => {
const [apiKey, setApiKey] = useState<string>(
(connector.config?.TAVILY_API_KEY as string) || ""
);
const [name, setName] = useState<string>(connector.name || "");
// Update API key and name when connector changes
useEffect(() => {
const key = (connector.config?.TAVILY_API_KEY as string) || "";
setApiKey(key);
setName(connector.name || "");
}, [connector.config, connector.name]);
const handleApiKeyChange = (value: string) => {
setApiKey(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
TAVILY_API_KEY: value,
});
}
};
const handleNameChange = (value: string) => {
setName(value);
if (onNameChange) {
onNameChange(value);
}
};
return (
<div className="space-y-6">
{/* Connector Name */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Connector Name</Label>
<Input
value={name}
onChange={(e) => handleNameChange(e.target.value)}
placeholder="My Tavily API Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
A friendly name to identify this connector.
</p>
</div>
</div>
{/* Configuration */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-1 sm:space-y-2">
<h3 className="font-medium text-sm sm:text-base">Configuration</h3>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2 text-xs sm:text-sm">
<KeyRound className="h-4 w-4" />
Tavily API Key
</Label>
<Input
type="password"
value={apiKey}
onChange={(e) => handleApiKeyChange(e.target.value)}
placeholder="Enter your Tavily API key"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Update the Tavily API Key if needed.
</p>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,36 @@
import type { FC } from "react";
import { TavilyApiConnectForm } from "./tavily-api-connect-form";
export interface ConnectFormProps {
onSubmit: (data: {
name: string;
connector_type: string;
config: Record<string, unknown>;
is_indexable: boolean;
last_indexed_at: null;
periodic_indexing_enabled: boolean;
indexing_frequency_minutes: null;
next_scheduled_at: null;
}) => Promise<void>;
onBack: () => void;
isSubmitting: boolean;
onFormSubmit?: () => void;
}
export type ConnectFormComponent = FC<ConnectFormProps>;
/**
* Factory function to get the appropriate connect form component for a connector type
*/
export function getConnectFormComponent(
connectorType: string
): ConnectFormComponent | null {
switch (connectorType) {
case "TAVILY_API":
return TavilyApiConnectForm;
// Add other connector types here as needed
default:
return null;
}
}

View file

@ -0,0 +1,145 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Info } from "lucide-react";
import type { FC } from "react";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { EnumConnectorName } from "@/contracts/enums/connector";
import type { ConnectFormProps } from "./index";
const tavilyApiFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
api_key: z.string().min(10, {
message: "API key is required and must be valid.",
}),
});
type TavilyApiFormValues = z.infer<typeof tavilyApiFormSchema>;
export const TavilyApiConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
const isSubmittingRef = useRef(false);
const form = useForm<TavilyApiFormValues>({
resolver: zodResolver(tavilyApiFormSchema),
defaultValues: {
name: "Tavily API Connector",
api_key: "",
},
});
const handleSubmit = async (values: TavilyApiFormValues) => {
// Prevent multiple submissions
if (isSubmittingRef.current || isSubmitting) {
return;
}
isSubmittingRef.current = true;
try {
await onSubmit({
name: values.name,
connector_type: EnumConnectorName.TAVILY_API,
config: {
TAVILY_API_KEY: values.api_key,
},
is_indexable: false,
last_indexed_at: null,
periodic_indexing_enabled: false,
indexing_frequency_minutes: null,
next_scheduled_at: null,
});
} finally {
isSubmittingRef.current = false;
}
};
return (
<div className="space-y-6 pb-6">
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 p-2 sm:p-3 flex items-center [&>svg]:relative [&>svg]:left-0 [&>svg]:top-0 [&>svg+div]:translate-y-0">
<Info className="h-3 w-3 sm:h-4 sm:w-4 shrink-0 ml-1" />
<div className="-ml-1">
<AlertTitle className="text-xs sm:text-sm">API Key Required</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
You'll need a Tavily API key to use this connector. You can get one by signing up at{" "}
<a
href="https://tavily.com"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
tavily.com
</a>
</AlertDescription>
</div>
</Alert>
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<Form {...form}>
<form id="tavily-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My Tavily API Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
A friendly name to identify this connector.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="api_key"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Tavily API Key</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your Tavily API key"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Your API key will be encrypted and stored securely.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
</div>
);
};

View file

@ -3,12 +3,14 @@
import type { FC } from "react"; import type { FC } from "react";
import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { GoogleDriveConfig } from "./components/google-drive-config"; import { GoogleDriveConfig } from "./components/google-drive-config";
import { TavilyApiConfig } from "./components/tavily-api-config";
import { WebcrawlerConfig } from "./components/webcrawler-config"; import { WebcrawlerConfig } from "./components/webcrawler-config";
import { YouTubeConfig } from "./components/youtube-config"; import { YouTubeConfig } from "./components/youtube-config";
export interface ConnectorConfigProps { export interface ConnectorConfigProps {
connector: SearchSourceConnector; connector: SearchSourceConnector;
onConfigChange?: (config: Record<string, unknown>) => void; onConfigChange?: (config: Record<string, unknown>) => void;
onNameChange?: (name: string) => void;
} }
export type ConnectorConfigComponent = FC<ConnectorConfigProps>; export type ConnectorConfigComponent = FC<ConnectorConfigProps>;
@ -22,6 +24,8 @@ export function getConnectorConfigComponent(
switch (connectorType) { switch (connectorType) {
case "GOOGLE_DRIVE_CONNECTOR": case "GOOGLE_DRIVE_CONNECTOR":
return GoogleDriveConfig; return GoogleDriveConfig;
case "TAVILY_API":
return TavilyApiConfig;
case "WEBCRAWLER_CONNECTOR": case "WEBCRAWLER_CONNECTOR":
return WebcrawlerConfig; return WebcrawlerConfig;
case "YOUTUBE_CONNECTOR": case "YOUTUBE_CONNECTOR":

View file

@ -0,0 +1,131 @@
"use client";
import { ArrowLeft, Loader2 } from "lucide-react";
import { type FC, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorTypeDisplay } from "@/lib/connectors/utils";
import { getConnectFormComponent } from "../connect-forms";
interface ConnectorConnectViewProps {
connectorType: string;
onSubmit: (data: {
name: string;
connector_type: string;
config: Record<string, unknown>;
is_indexable: boolean;
last_indexed_at: null;
periodic_indexing_enabled: boolean;
indexing_frequency_minutes: null;
next_scheduled_at: null;
}) => Promise<void>;
onBack: () => void;
isSubmitting: boolean;
}
export const ConnectorConnectView: FC<ConnectorConnectViewProps> = ({
connectorType,
onSubmit,
onBack,
isSubmitting,
}) => {
// Get connector-specific form component
const ConnectFormComponent = useMemo(
() => getConnectFormComponent(connectorType),
[connectorType]
);
const handleFormSubmit = () => {
// Prevent multiple submissions
if (isSubmitting) {
return;
}
const form = document.getElementById("tavily-connect-form") as HTMLFormElement;
if (form) {
form.requestSubmit();
}
};
if (!ConnectFormComponent) {
return (
<div className="flex-1 flex flex-col min-h-0 overflow-hidden p-6">
<p className="text-sm text-muted-foreground mb-4">
Connector form not found for type: {connectorType}
</p>
<Button onClick={onBack} variant="ghost">
Back
</Button>
</div>
);
}
return (
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
{/* Header */}
<div className="flex-shrink-0 px-6 sm:px-12 pt-8 sm:pt-10">
<button
type="button"
onClick={onBack}
className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground hover:text-foreground mb-6 w-fit"
>
<ArrowLeft className="size-4" />
Back to connectors
</button>
<div className="flex items-center gap-4 mb-6">
<div className="flex h-14 w-14 items-center justify-center rounded-xl border border-slate-400/30">
{getConnectorIcon(connectorType as EnumConnectorName, "h-7 w-7")}
</div>
<div>
<h2 className="text-xl sm:text-2xl font-semibold tracking-tight">
Connect {connectorType === "TAVILY_API" ? "Tavily API" : connectorType}
</h2>
<p className="text-xs sm:text-base text-muted-foreground mt-1">
Enter your connection details
</p>
</div>
</div>
</div>
{/* Form Content - Scrollable */}
<div className="flex-1 min-h-0 overflow-y-auto px-6 sm:px-12">
<ConnectFormComponent
onSubmit={onSubmit}
onBack={onBack}
isSubmitting={isSubmitting}
onFormSubmit={handleFormSubmit}
/>
</div>
{/* Fixed Footer - Action buttons */}
<div className="flex-shrink-0 flex items-center justify-between px-6 sm:px-12 py-6 bg-muted border-t border-border">
<Button
variant="ghost"
onClick={onBack}
disabled={isSubmitting}
className="text-xs sm:text-sm"
>
Cancel
</Button>
<Button
onClick={handleFormSubmit}
disabled={isSubmitting}
className="text-xs sm:text-sm min-w-[140px] disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none"
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Connecting...
</>
) : (
<>
Connect {getConnectorTypeDisplay(connectorType)}
</>
)}
</Button>
</div>
</div>
);
};

View file

@ -26,6 +26,7 @@ interface ConnectorEditViewProps {
onDisconnect: () => void; onDisconnect: () => void;
onBack: () => void; onBack: () => void;
onConfigChange?: (config: Record<string, any>) => void; onConfigChange?: (config: Record<string, any>) => void;
onNameChange?: (name: string) => void;
} }
export const ConnectorEditView: FC<ConnectorEditViewProps> = ({ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
@ -44,6 +45,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
onDisconnect, onDisconnect,
onBack, onBack,
onConfigChange, onConfigChange,
onNameChange,
}) => { }) => {
// Get connector-specific config component // Get connector-specific config component
const ConnectorConfigComponent = useMemo( const ConnectorConfigComponent = useMemo(
@ -146,25 +148,31 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
<ConnectorConfigComponent <ConnectorConfigComponent
connector={connector} connector={connector}
onConfigChange={onConfigChange} onConfigChange={onConfigChange}
onNameChange={onNameChange}
/> />
)} )}
{/* Date range selector - not shown for Google Drive (uses folder selection), Webcrawler (uses config), or YouTube (uses URL selection) */} {/* Date range selector and periodic sync - only shown for indexable connectors */}
{connector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && connector.connector_type !== "WEBCRAWLER_CONNECTOR" && connector.connector_type !== "YOUTUBE_CONNECTOR" && ( {connector.is_indexable && (
<DateRangeSelector <>
startDate={startDate} {/* Date range selector - not shown for Google Drive (uses folder selection), Webcrawler (uses config), or YouTube (uses URL selection) */}
endDate={endDate} {connector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && connector.connector_type !== "WEBCRAWLER_CONNECTOR" && connector.connector_type !== "YOUTUBE_CONNECTOR" && (
onStartDateChange={onStartDateChange} <DateRangeSelector
onEndDateChange={onEndDateChange} startDate={startDate}
/> endDate={endDate}
)} onStartDateChange={onStartDateChange}
onEndDateChange={onEndDateChange}
/>
)}
<PeriodicSyncConfig <PeriodicSyncConfig
enabled={periodicEnabled} enabled={periodicEnabled}
frequencyMinutes={frequencyMinutes} frequencyMinutes={frequencyMinutes}
onEnabledChange={onPeriodicEnabledChange} onEnabledChange={onPeriodicEnabledChange}
onFrequencyChange={onFrequencyChange} onFrequencyChange={onFrequencyChange}
/> />
</>
)}
{/* Info box */} {/* Info box */}
<div className="rounded-xl border border-border bg-primary/5 p-4 flex items-start gap-3"> <div className="rounded-xl border border-border bg-primary/5 p-4 flex items-start gap-3">

View file

@ -3,7 +3,6 @@
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
import { type FC, useState, useCallback, useRef, useEffect, useMemo } from "react"; import { type FC, useState, useCallback, useRef, useEffect, useMemo } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { IndexingConfigState } from "../../constants/connector-constants"; import type { IndexingConfigState } from "../../constants/connector-constants";
@ -134,22 +133,27 @@ export const IndexingConfigurationView: FC<IndexingConfigurationViewProps> = ({
/> />
)} )}
{/* Date range selector - not shown for Google Drive (uses folder selection), Webcrawler (uses config), or YouTube (uses URL selection) */} {/* Date range selector and periodic sync - only shown for indexable connectors */}
{config.connectorType !== "GOOGLE_DRIVE_CONNECTOR" && config.connectorType !== "WEBCRAWLER_CONNECTOR" && config.connectorType !== "YOUTUBE_CONNECTOR" && ( {connector?.is_indexable && (
<DateRangeSelector <>
startDate={startDate} {/* Date range selector - not shown for Google Drive (uses folder selection), Webcrawler (uses config), or YouTube (uses URL selection) */}
endDate={endDate} {config.connectorType !== "GOOGLE_DRIVE_CONNECTOR" && config.connectorType !== "WEBCRAWLER_CONNECTOR" && config.connectorType !== "YOUTUBE_CONNECTOR" && (
onStartDateChange={onStartDateChange} <DateRangeSelector
onEndDateChange={onEndDateChange} startDate={startDate}
/> endDate={endDate}
)} onStartDateChange={onStartDateChange}
onEndDateChange={onEndDateChange}
/>
)}
<PeriodicSyncConfig <PeriodicSyncConfig
enabled={periodicEnabled} enabled={periodicEnabled}
frequencyMinutes={frequencyMinutes} frequencyMinutes={frequencyMinutes}
onEnabledChange={onPeriodicEnabledChange} onEnabledChange={onPeriodicEnabledChange}
onFrequencyChange={onFrequencyChange} onFrequencyChange={onFrequencyChange}
/> />
</>
)}
{/* Info box */} {/* Info box */}
<div className="rounded-xl border border-border bg-primary/5 p-4 flex items-start gap-3"> <div className="rounded-xl border border-border bg-primary/5 p-4 flex items-start gap-3">

View file

@ -7,9 +7,10 @@ import { searchSourceConnectorTypeEnum } from "@/contracts/types/connector.types
export const connectorPopupQueryParamsSchema = z.object({ export const connectorPopupQueryParamsSchema = z.object({
modal: z.enum(["connectors"]).optional(), modal: z.enum(["connectors"]).optional(),
tab: z.enum(["all", "active"]).optional(), tab: z.enum(["all", "active"]).optional(),
view: z.enum(["configure", "edit"]).optional(), view: z.enum(["configure", "edit", "connect"]).optional(),
connector: z.string().optional(), connector: z.string().optional(),
connectorId: z.string().optional(), connectorId: z.string().optional(),
connectorType: z.string().optional(),
success: z.enum(["true", "false"]).optional(), success: z.enum(["true", "false"]).optional(),
}); });

View file

@ -1,6 +1,6 @@
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { createConnectorMutationAtom, deleteConnectorMutationAtom, indexConnectorMutationAtom, updateConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms"; import { createConnectorMutationAtom, deleteConnectorMutationAtom, indexConnectorMutationAtom, updateConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
@ -51,6 +51,12 @@ export const useConnectorDialog = () => {
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [isDisconnecting, setIsDisconnecting] = useState(false); const [isDisconnecting, setIsDisconnecting] = useState(false);
const [connectorConfig, setConnectorConfig] = useState<Record<string, unknown> | null>(null); const [connectorConfig, setConnectorConfig] = useState<Record<string, unknown> | null>(null);
const [connectorName, setConnectorName] = useState<string | null>(null);
// Connect mode state (for non-OAuth connectors)
const [connectingConnectorType, setConnectingConnectorType] = useState<string | null>(null);
const [isCreatingConnector, setIsCreatingConnector] = useState(false);
const isCreatingConnectorRef = useRef(false);
// Helper function to get frequency label // Helper function to get frequency label
const getFrequencyLabel = useCallback((minutes: string): string => { const getFrequencyLabel = useCallback((minutes: string): string => {
@ -85,6 +91,17 @@ export const useConnectorDialog = () => {
// Clear editing connector if view is not "edit" anymore // Clear editing connector if view is not "edit" anymore
if (params.view !== "edit" && editingConnector) { if (params.view !== "edit" && editingConnector) {
setEditingConnector(null); setEditingConnector(null);
setConnectorName(null);
}
// Clear connecting connector type if view is not "connect" anymore
if (params.view !== "connect" && connectingConnectorType) {
setConnectingConnectorType(null);
}
// Handle connect view
if (params.view === "connect" && params.connectorType && !connectingConnectorType) {
setConnectingConnectorType(params.connectorType);
} }
if (params.view === "configure" && params.connector && !indexingConfig) { if (params.view === "configure" && params.connector && !indexingConfig) {
@ -147,6 +164,7 @@ export const useConnectorDialog = () => {
// Clear editing connector when modal is closed // Clear editing connector when modal is closed
if (editingConnector) { if (editingConnector) {
setEditingConnector(null); setEditingConnector(null);
setConnectorName(null);
setConnectorConfig(null); setConnectorConfig(null);
setStartDate(undefined); setStartDate(undefined);
setEndDate(undefined); setEndDate(undefined);
@ -155,13 +173,17 @@ export const useConnectorDialog = () => {
setIsScrolled(false); setIsScrolled(false);
setSearchQuery(""); setSearchQuery("");
} }
// Clear connecting connector type when modal is closed
if (connectingConnectorType) {
setConnectingConnectorType(null);
}
} }
} catch (error) { } catch (error) {
// Invalid query params - log but don't crash // Invalid query params - log but don't crash
console.warn("Invalid connector popup query params:", error); console.warn("Invalid connector popup query params:", error);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams, allConnectors, editingConnector, indexingConfig]); }, [searchParams, allConnectors, editingConnector, indexingConfig, connectingConnectorType]);
// Detect OAuth success and transition to config view // Detect OAuth success and transition to config view
useEffect(() => { useEffect(() => {
@ -358,6 +380,106 @@ export const useConnectorDialog = () => {
} }
}, [searchSpaceId, createConnector, refetchAllConnectors]); }, [searchSpaceId, createConnector, refetchAllConnectors]);
// Handle connecting non-OAuth connectors (like Tavily API)
const handleConnectNonOAuth = useCallback((connectorType: string) => {
if (!searchSpaceId) return;
// Set connecting state
setConnectingConnectorType(connectorType);
// Update URL to show connect view
const url = new URL(window.location.href);
url.searchParams.set("modal", "connectors");
url.searchParams.set("view", "connect");
url.searchParams.set("connectorType", connectorType);
window.history.pushState({ modal: true }, "", url.toString());
}, [searchSpaceId]);
// Handle submitting connect form
const handleSubmitConnectForm = useCallback(async (
formData: {
name: string;
connector_type: string;
config: Record<string, unknown>;
is_indexable: boolean;
last_indexed_at: null;
periodic_indexing_enabled: boolean;
indexing_frequency_minutes: null;
next_scheduled_at: null;
}
) => {
if (!searchSpaceId || !connectingConnectorType) return;
// Prevent multiple submissions using ref for immediate check
if (isCreatingConnectorRef.current) return;
isCreatingConnectorRef.current = true;
setIsCreatingConnector(true);
try {
// Create connector
const newConnector = await createConnector({
data: formData,
queryParams: {
search_space_id: searchSpaceId,
},
});
// Refetch connectors to get the new one
const result = await refetchAllConnectors();
if (result.data) {
const connector = result.data.find(
(c: SearchSourceConnector) => c.id === newConnector.id
);
if (connector) {
// Validate connector data
const connectorValidation = searchSourceConnector.safeParse(connector);
if (connectorValidation.success) {
// Find connector title from constants
const connectorInfo = OTHER_CONNECTORS.find(
c => c.connectorType === connectingConnectorType
);
const connectorTitle = connectorInfo?.title || connector.name;
// Set up indexing config
const config = validateIndexingConfigState({
connectorType: connectingConnectorType as EnumConnectorName,
connectorId: connector.id,
connectorTitle,
});
setIndexingConfig(config);
setIndexingConnector(connector);
setIndexingConnectorConfig(connector.config || {});
// Transition to configure view
const url = new URL(window.location.href);
url.searchParams.set("view", "configure");
url.searchParams.delete("connectorType");
window.history.replaceState({}, "", url.toString());
toast.success(`${connectorTitle} connected successfully!`);
}
}
}
} catch (error) {
console.error("Error creating connector:", error);
toast.error(error instanceof Error ? error.message : "Failed to create connector");
} finally {
isCreatingConnectorRef.current = false;
setIsCreatingConnector(false);
setConnectingConnectorType(null);
}
}, [connectingConnectorType, searchSpaceId, createConnector, refetchAllConnectors]);
// Handle going back from connect view
const handleBackFromConnect = useCallback(() => {
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 starting indexing // Handle starting indexing
const handleStartIndexing = useCallback(async (refreshConnectors: () => void) => { const handleStartIndexing = useCallback(async (refreshConnectors: () => void) => {
if (!indexingConfig || !searchSpaceId) return; if (!indexingConfig || !searchSpaceId) return;
@ -494,11 +616,12 @@ export const useConnectorDialog = () => {
(oauthConnector) => oauthConnector.connectorType === connector.connector_type (oauthConnector) => oauthConnector.connectorType === connector.connector_type
); );
// Check if this is webcrawler (can be managed in popup) // Check if this is webcrawler or Tavily API (can be managed in popup)
const isWebcrawler = connector.connector_type === EnumConnectorName.WEBCRAWLER_CONNECTOR; const isWebcrawler = connector.connector_type === EnumConnectorName.WEBCRAWLER_CONNECTOR;
const isTavilyApi = connector.connector_type === EnumConnectorName.TAVILY_API;
// If not OAuth and not webcrawler, redirect to old connector edit page // If not OAuth, not webcrawler, and not Tavily API, redirect to old connector edit page
if (!isOAuthConnector && !isWebcrawler) { if (!isOAuthConnector && !isWebcrawler && !isTavilyApi) {
router.push(`/dashboard/${searchSpaceId}/connectors/${connector.id}/edit`); router.push(`/dashboard/${searchSpaceId}/connectors/${connector.id}/edit`);
return; return;
} }
@ -511,6 +634,7 @@ export const useConnectorDialog = () => {
} }
setEditingConnector(connector); setEditingConnector(connector);
setConnectorName(connector.name);
// Load existing periodic sync settings // Load existing periodic sync settings
setPeriodicEnabled(connector.periodic_indexing_enabled); setPeriodicEnabled(connector.periodic_indexing_enabled);
setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440"); setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440");
@ -530,8 +654,8 @@ export const useConnectorDialog = () => {
const handleSaveConnector = useCallback(async (refreshConnectors: () => void) => { const handleSaveConnector = useCallback(async (refreshConnectors: () => void) => {
if (!editingConnector || !searchSpaceId) return; if (!editingConnector || !searchSpaceId) return;
// Validate date range (skip for Google Drive which uses folder selection, and Webcrawler which uses config) // Validate date range (skip for Google Drive which uses folder selection, Webcrawler which uses config, and non-indexable connectors)
if (editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && editingConnector.connector_type !== "WEBCRAWLER_CONNECTOR") { if (editingConnector.is_indexable && editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && editingConnector.connector_type !== "WEBCRAWLER_CONNECTOR") {
const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate }); const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate });
if (!dateRangeValidation.success) { if (!dateRangeValidation.success) {
toast.error(dateRangeValidation.error.issues[0]?.message || "Invalid date range"); toast.error(dateRangeValidation.error.issues[0]?.message || "Invalid date range");
@ -539,8 +663,14 @@ export const useConnectorDialog = () => {
} }
} }
// Validate frequency minutes if periodic is enabled // Prevent periodic indexing for non-indexable connectors
if (periodicEnabled) { if (periodicEnabled && !editingConnector.is_indexable) {
toast.error("Periodic indexing is not available for this connector type");
return;
}
// Validate frequency minutes if periodic is enabled (only for indexable connectors)
if (periodicEnabled && editingConnector.is_indexable) {
const frequencyValidation = frequencyMinutesSchema.safeParse(frequencyMinutes); const frequencyValidation = frequencyMinutesSchema.safeParse(frequencyMinutes);
if (!frequencyValidation.success) { if (!frequencyValidation.success) {
toast.error("Invalid frequency value"); toast.error("Invalid frequency value");
@ -553,20 +683,24 @@ export const useConnectorDialog = () => {
const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined; const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined;
const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined; const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined;
// Update connector with periodic sync settings and config changes // Update connector with periodic sync settings, config changes, and name
const frequency = periodicEnabled ? parseInt(frequencyMinutes, 10) : null; const frequency = periodicEnabled ? parseInt(frequencyMinutes, 10) : null;
await updateConnector({ await updateConnector({
id: editingConnector.id, id: editingConnector.id,
data: { data: {
name: connectorName || editingConnector.name,
periodic_indexing_enabled: periodicEnabled, periodic_indexing_enabled: periodicEnabled,
indexing_frequency_minutes: frequency, indexing_frequency_minutes: frequency,
config: connectorConfig || editingConnector.config, config: connectorConfig || editingConnector.config,
}, },
}); });
// Re-index based on connector type // Re-index based on connector type (only for indexable connectors)
let indexingDescription = "Settings saved."; let indexingDescription = "Settings saved.";
if (editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR") { if (!editingConnector.is_indexable) {
// Non-indexable connectors (like Tavily API) don't need re-indexing
indexingDescription = "Settings saved.";
} else if (editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR") {
// Google Drive uses folder selection from config, not date ranges // Google Drive uses folder selection from config, not date ranges
const selectedFolders = (connectorConfig || editingConnector.config)?.selected_folders as Array<{ id: string; name: string }> | undefined; const selectedFolders = (connectorConfig || editingConnector.config)?.selected_folders as Array<{ id: string; name: string }> | undefined;
if (selectedFolders && selectedFolders.length > 0) { if (selectedFolders && selectedFolders.length > 0) {
@ -628,7 +762,7 @@ export const useConnectorDialog = () => {
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
}, [editingConnector, searchSpaceId, startDate, endDate, indexConnector, updateConnector, periodicEnabled, frequencyMinutes, getFrequencyLabel, router, connectorConfig]); }, [editingConnector, searchSpaceId, startDate, endDate, indexConnector, updateConnector, periodicEnabled, frequencyMinutes, getFrequencyLabel, router, connectorConfig, connectorName]);
// Handle disconnecting connector // Handle disconnecting connector
const handleDisconnectConnector = useCallback(async (refreshConnectors: () => void) => { const handleDisconnectConnector = useCallback(async (refreshConnectors: () => void) => {
@ -692,12 +826,14 @@ export const useConnectorDialog = () => {
window.history.pushState({ modal: false }, "", url.toString()); window.history.pushState({ modal: false }, "", url.toString());
setIsScrolled(false); setIsScrolled(false);
setSearchQuery(""); setSearchQuery("");
if (!isStartingIndexing && !isSaving && !isDisconnecting) { if (!isStartingIndexing && !isSaving && !isDisconnecting && !isCreatingConnector) {
setIndexingConfig(null); setIndexingConfig(null);
setIndexingConnector(null); setIndexingConnector(null);
setIndexingConnectorConfig(null); setIndexingConnectorConfig(null);
setEditingConnector(null); setEditingConnector(null);
setConnectorName(null);
setConnectorConfig(null); setConnectorConfig(null);
setConnectingConnectorType(null);
setStartDate(undefined); setStartDate(undefined);
setEndDate(undefined); setEndDate(undefined);
setPeriodicEnabled(false); setPeriodicEnabled(false);
@ -705,7 +841,7 @@ export const useConnectorDialog = () => {
} }
} }
}, },
[activeTab, isStartingIndexing, isDisconnecting, isSaving] [activeTab, isStartingIndexing, isDisconnecting, isSaving, isCreatingConnector]
); );
// Handle tab change // Handle tab change
@ -735,6 +871,8 @@ export const useConnectorDialog = () => {
indexingConnector, indexingConnector,
indexingConnectorConfig, indexingConnectorConfig,
editingConnector, editingConnector,
connectingConnectorType,
isCreatingConnector,
startDate, startDate,
endDate, endDate,
isStartingIndexing, isStartingIndexing,
@ -751,20 +889,24 @@ export const useConnectorDialog = () => {
setEndDate, setEndDate,
setPeriodicEnabled, setPeriodicEnabled,
setFrequencyMinutes, setFrequencyMinutes,
setConnectorName,
// Handlers // Handlers
handleOpenChange, handleOpenChange,
handleTabChange, handleTabChange,
handleScroll, handleScroll,
handleConnectOAuth, handleConnectOAuth,
handleConnectNonOAuth,
handleCreateWebcrawler, handleCreateWebcrawler,
handleCreateYouTube, handleCreateYouTube,
handleSubmitConnectForm,
handleStartIndexing, handleStartIndexing,
handleSkipIndexing, handleSkipIndexing,
handleStartEdit, handleStartEdit,
handleSaveConnector, handleSaveConnector,
handleDisconnectConnector, handleDisconnectConnector,
handleBackFromEdit, handleBackFromEdit,
handleBackFromConnect,
connectorConfig, connectorConfig,
setConnectorConfig, setConnectorConfig,
setIndexingConnectorConfig, setIndexingConnectorConfig,

View file

@ -13,6 +13,7 @@ interface AllConnectorsTabProps {
connectingId: string | null; connectingId: string | null;
allConnectors: SearchSourceConnector[] | undefined; allConnectors: SearchSourceConnector[] | undefined;
onConnectOAuth: (connector: (typeof OAUTH_CONNECTORS)[0]) => void; onConnectOAuth: (connector: (typeof OAUTH_CONNECTORS)[0]) => void;
onConnectNonOAuth?: (connectorType: string) => void;
onCreateWebcrawler?: () => void; onCreateWebcrawler?: () => void;
onCreateYouTube?: () => void; onCreateYouTube?: () => void;
onManage?: (connector: SearchSourceConnector) => void; onManage?: (connector: SearchSourceConnector) => void;
@ -25,6 +26,7 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
connectingId, connectingId,
allConnectors, allConnectors,
onConnectOAuth, onConnectOAuth,
onConnectNonOAuth,
onCreateWebcrawler, onCreateWebcrawler,
onCreateYouTube, onCreateYouTube,
onManage, onManage,
@ -98,13 +100,16 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
? allConnectors.find((c: SearchSourceConnector) => c.connector_type === connector.connectorType) ? allConnectors.find((c: SearchSourceConnector) => c.connector_type === connector.connectorType)
: undefined; : undefined;
// Special handling for webcrawler and YouTube - create in popup // Special handling for connectors that can be created in popup
const isWebcrawler = connector.id === "webcrawler-connector"; const isWebcrawler = connector.id === "webcrawler-connector";
const isYouTube = connector.id === "youtube-connector"; const isYouTube = connector.id === "youtube-connector";
const isTavily = connector.id === "tavily-api";
const handleConnect = isWebcrawler && onCreateWebcrawler const handleConnect = isWebcrawler && onCreateWebcrawler
? onCreateWebcrawler ? onCreateWebcrawler
: isYouTube && onCreateYouTube : isYouTube && onCreateYouTube
? onCreateYouTube ? onCreateYouTube
: isTavily && onConnectNonOAuth
? () => onConnectNonOAuth(connector.connectorType)
: () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`); : () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`);
return ( return (