mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-10 22:32:16 +02:00
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:
parent
5d1859db17
commit
880b3cc4bf
11 changed files with 711 additions and 86 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -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":
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue