mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-09 07:42:39 +02:00
feat: Add Elasticsearch connector support with configuration forms, enhance connector dialog for indexing settings, and implement date range selection for periodic sync.
This commit is contained in:
parent
95f95558dc
commit
36d25e9505
14 changed files with 1521 additions and 44 deletions
|
|
@ -7,12 +7,19 @@ import {
|
||||||
useAssistantApi,
|
useAssistantApi,
|
||||||
useAssistantState,
|
useAssistantState,
|
||||||
} from "@assistant-ui/react";
|
} from "@assistant-ui/react";
|
||||||
import { FileText, Loader2, PlusIcon, XIcon } from "lucide-react";
|
import { FileText, Loader2, Paperclip, PlusIcon, Upload, XIcon } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { type FC, type PropsWithChildren, useEffect, useState } from "react";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { type FC, type PropsWithChildren, useRef, useEffect, useState } from "react";
|
||||||
import { useShallow } from "zustand/shallow";
|
import { useShallow } from "zustand/shallow";
|
||||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
@ -184,18 +191,18 @@ const AttachmentUI: FC = () => {
|
||||||
>
|
>
|
||||||
<AttachmentPreviewDialog>
|
<AttachmentPreviewDialog>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"aui-attachment-tile size-14 cursor-pointer overflow-hidden rounded-[14px] border bg-muted transition-opacity hover:opacity-75",
|
"aui-attachment-tile size-14 cursor-pointer overflow-hidden rounded-[14px] border bg-muted transition-opacity hover:opacity-75",
|
||||||
isComposer && "aui-attachment-tile-composer border-foreground/20",
|
isComposer && "aui-attachment-tile-composer border-foreground/20",
|
||||||
isProcessing && "animate-pulse"
|
isProcessing && "animate-pulse"
|
||||||
)}
|
)}
|
||||||
role="button"
|
|
||||||
id="attachment-tile"
|
id="attachment-tile"
|
||||||
aria-label={isProcessing ? "Processing attachment..." : `${typeLabel} attachment`}
|
aria-label={isProcessing ? "Processing attachment..." : `${typeLabel} attachment`}
|
||||||
>
|
>
|
||||||
<AttachmentThumb />
|
<AttachmentThumb />
|
||||||
</div>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
</AttachmentPreviewDialog>
|
</AttachmentPreviewDialog>
|
||||||
{isComposer && !isProcessing && <AttachmentRemove />}
|
{isComposer && !isProcessing && <AttachmentRemove />}
|
||||||
|
|
@ -312,18 +319,54 @@ export const ComposerAttachments: FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ComposerAddAttachment: FC = () => {
|
export const ComposerAddAttachment: FC = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const searchSpaceId = params.search_space_id as string;
|
||||||
|
const chatAttachmentInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleFileUpload = () => {
|
||||||
|
router.push(`/dashboard/${searchSpaceId}/sources/add?tab=documents`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChatAttachment = () => {
|
||||||
|
chatAttachmentInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ComposerPrimitive.AddAttachment asChild>
|
<>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
<TooltipIconButton
|
<TooltipIconButton
|
||||||
tooltip="Add Attachment"
|
tooltip="Upload documents or add attachment"
|
||||||
side="bottom"
|
side="bottom"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="aui-composer-add-attachment size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
|
className="aui-composer-add-attachment size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
|
||||||
aria-label="Add Attachment"
|
aria-label="Upload documents or add attachment"
|
||||||
>
|
>
|
||||||
<PlusIcon className="aui-attachment-add-icon size-5 stroke-[1.5px]" />
|
<PlusIcon className="aui-attachment-add-icon size-5 stroke-[1.5px]" />
|
||||||
</TooltipIconButton>
|
</TooltipIconButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="w-48 bg-background border-border">
|
||||||
|
<DropdownMenuItem onSelect={handleChatAttachment} className="cursor-pointer">
|
||||||
|
<Paperclip className="size-4" />
|
||||||
|
<span>Add attachment(s)</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={handleFileUpload} className="cursor-pointer">
|
||||||
|
<Upload className="size-4" />
|
||||||
|
<span>File upload</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<ComposerPrimitive.AddAttachment asChild>
|
||||||
|
<input
|
||||||
|
ref={chatAttachmentInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
className="hidden"
|
||||||
|
accept="image/*,application/pdf,.doc,.docx,.txt"
|
||||||
|
/>
|
||||||
</ComposerPrimitive.AddAttachment>
|
</ComposerPrimitive.AddAttachment>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@ export const DateRangeSelector: FC<DateRangeSelectorProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6">
|
<div className="rounded-xl bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6">
|
||||||
<h3 className="font-medium text-xs sm:text-base mb-4">Select Date Range</h3>
|
<h3 className="font-medium text-sm sm:text-base mb-4">Select Date Range</h3>
|
||||||
<p className="text-xs sm:text-sm text-muted-foreground mb-6">
|
<p className="text-xs sm:text-sm text-muted-foreground mb-6">
|
||||||
Choose how far back you want to sync your data. You can always re-index later with different dates.
|
Choose how far back you want to sync your data. You can always re-index later with different dates.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,10 @@ export const PeriodicSyncConfig: FC<PeriodicSyncConfigProps> = ({
|
||||||
onFrequencyChange,
|
onFrequencyChange,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6">
|
<div className="rounded-xl bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h3 className="font-medium text-xs sm:text-base">Enable Periodic Sync</h3>
|
<h3 className="font-medium text-sm sm:text-base">Enable Periodic Sync</h3>
|
||||||
<p className="text-xs sm:text-sm text-muted-foreground">
|
<p className="text-xs sm:text-sm text-muted-foreground">
|
||||||
Automatically re-index at regular intervals
|
Automatically re-index at regular intervals
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,791 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
|
import { Info } from "lucide-react";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import { useId, useRef } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import * as z from "zod";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/components/ui/accordion";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { EnumConnectorName } from "@/contracts/enums/connector";
|
||||||
|
import type { ConnectFormProps } from "../index";
|
||||||
|
import { getConnectorBenefits } from "../connector-benefits";
|
||||||
|
import { DateRangeSelector } from "../../components/date-range-selector";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const elasticsearchConnectorFormSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(3, {
|
||||||
|
message: "Connector name must be at least 3 characters.",
|
||||||
|
}),
|
||||||
|
endpoint_url: z.string().url({ message: "Please enter a valid Elasticsearch endpoint URL." }),
|
||||||
|
auth_method: z.enum(["basic", "api_key"]),
|
||||||
|
username: z.string().optional(),
|
||||||
|
password: z.string().optional(),
|
||||||
|
ELASTICSEARCH_API_KEY: z.string().optional(),
|
||||||
|
indices: z.string().optional(),
|
||||||
|
query: z.string(),
|
||||||
|
search_fields: z.string().optional(),
|
||||||
|
max_documents: z.number().min(1).max(10000).optional(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.auth_method === "basic") {
|
||||||
|
return Boolean(data.username?.trim() && data.password?.trim());
|
||||||
|
}
|
||||||
|
if (data.auth_method === "api_key") {
|
||||||
|
return Boolean(data.ELASTICSEARCH_API_KEY?.trim());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Authentication credentials are required for the selected method.",
|
||||||
|
path: ["auth_method"],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type ElasticsearchConnectorFormValues = z.infer<typeof elasticsearchConnectorFormSchema>;
|
||||||
|
|
||||||
|
export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
|
||||||
|
onSubmit,
|
||||||
|
isSubmitting,
|
||||||
|
}) => {
|
||||||
|
const isSubmittingRef = useRef(false);
|
||||||
|
const authBasicId = useId();
|
||||||
|
const authApiKeyId = useId();
|
||||||
|
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
|
||||||
|
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
|
||||||
|
const [periodicEnabled, setPeriodicEnabled] = useState(false);
|
||||||
|
const [frequencyMinutes, setFrequencyMinutes] = useState("1440");
|
||||||
|
|
||||||
|
const form = useForm<ElasticsearchConnectorFormValues>({
|
||||||
|
resolver: zodResolver(elasticsearchConnectorFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "Elasticsearch Connector",
|
||||||
|
endpoint_url: "",
|
||||||
|
auth_method: "api_key",
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
ELASTICSEARCH_API_KEY: "",
|
||||||
|
indices: "",
|
||||||
|
query: "*",
|
||||||
|
search_fields: "",
|
||||||
|
max_documents: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const stringToArray = (str: string): string[] => {
|
||||||
|
const items = str
|
||||||
|
.split(",")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
return Array.from(new Set(items));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (values: ElasticsearchConnectorFormValues) => {
|
||||||
|
// Prevent multiple submissions
|
||||||
|
if (isSubmittingRef.current || isSubmitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmittingRef.current = true;
|
||||||
|
try {
|
||||||
|
// Send full URL to backend (backend expects ELASTICSEARCH_URL)
|
||||||
|
const config: Record<string, string | number | boolean | string[]> = {
|
||||||
|
ELASTICSEARCH_URL: values.endpoint_url,
|
||||||
|
// default to verifying certs; expose fields for CA/verify if UI added later
|
||||||
|
ELASTICSEARCH_VERIFY_CERTS: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (values.auth_method === "basic") {
|
||||||
|
if (values.username) config.ELASTICSEARCH_USERNAME = values.username;
|
||||||
|
if (values.password) config.ELASTICSEARCH_PASSWORD = values.password;
|
||||||
|
} else if (values.auth_method === "api_key") {
|
||||||
|
if (values.ELASTICSEARCH_API_KEY)
|
||||||
|
config.ELASTICSEARCH_API_KEY = values.ELASTICSEARCH_API_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const indicesInput = values.indices?.trim() ?? "";
|
||||||
|
const indicesArr = stringToArray(indicesInput);
|
||||||
|
config.ELASTICSEARCH_INDEX =
|
||||||
|
indicesArr.length === 0 ? "*" : indicesArr.length === 1 ? indicesArr[0] : indicesArr;
|
||||||
|
|
||||||
|
if (values.query && values.query !== "*") {
|
||||||
|
config.ELASTICSEARCH_QUERY = values.query;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.search_fields?.trim()) {
|
||||||
|
const fields = stringToArray(values.search_fields);
|
||||||
|
config.ELASTICSEARCH_FIELDS = fields;
|
||||||
|
config.ELASTICSEARCH_CONTENT_FIELDS = fields;
|
||||||
|
if (fields.includes("title")) {
|
||||||
|
config.ELASTICSEARCH_TITLE_FIELD = "title";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.max_documents !== undefined && values.max_documents > 0) {
|
||||||
|
config.ELASTICSEARCH_MAX_DOCUMENTS = values.max_documents;
|
||||||
|
}
|
||||||
|
|
||||||
|
await onSubmit({
|
||||||
|
name: values.name,
|
||||||
|
connector_type: EnumConnectorName.ELASTICSEARCH_CONNECTOR,
|
||||||
|
config,
|
||||||
|
is_indexable: true,
|
||||||
|
last_indexed_at: null,
|
||||||
|
periodic_indexing_enabled: periodicEnabled,
|
||||||
|
indexing_frequency_minutes: periodicEnabled ? parseInt(frequencyMinutes, 10) : null,
|
||||||
|
next_scheduled_at: null,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
periodicEnabled,
|
||||||
|
frequencyMinutes,
|
||||||
|
});
|
||||||
|
} 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">
|
||||||
|
Enter your Elasticsearch cluster endpoint URL and authentication credentials to connect.
|
||||||
|
</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="elasticsearch-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 Elasticsearch Connector"
|
||||||
|
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm 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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Connection Details */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm sm:text-base font-medium">Connection Details</h3>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="endpoint_url"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="text-xs sm:text-sm">Elasticsearch Endpoint URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="url"
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="https://your-cluster.es.region.aws.com:443"
|
||||||
|
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-[10px] sm:text-xs">
|
||||||
|
Enter the complete Elasticsearch endpoint URL. We'll automatically extract the hostname, port, and SSL settings.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Show parsed URL details */}
|
||||||
|
{form.watch("endpoint_url") && (
|
||||||
|
<div className="rounded-lg border border-border bg-muted/50 p-3">
|
||||||
|
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Parsed Connection Details:</h4>
|
||||||
|
<div className="text-[10px] sm:text-xs text-muted-foreground space-y-1">
|
||||||
|
{(() => {
|
||||||
|
try {
|
||||||
|
const url = new URL(form.watch("endpoint_url"));
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<strong>Hostname:</strong> {url.hostname}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Port:</strong>{" "}
|
||||||
|
{url.port || (url.protocol === "https:" ? "443" : "80")}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>SSL/TLS:</strong>{" "}
|
||||||
|
{url.protocol === "https:" ? "Enabled" : "Disabled"}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return <div className="text-destructive">Invalid URL format</div>;
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Authentication */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm sm:text-base font-medium">Authentication</h3>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="auth_method"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="space-y-3">
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroup.Root
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
// Clear auth fields when method changes
|
||||||
|
if (value !== "basic") {
|
||||||
|
form.setValue("username", "");
|
||||||
|
form.setValue("password", "");
|
||||||
|
}
|
||||||
|
if (value !== "api_key") {
|
||||||
|
form.setValue("ELASTICSEARCH_API_KEY", "");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={field.value}
|
||||||
|
className="flex flex-col space-y-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item
|
||||||
|
value="api_key"
|
||||||
|
id={authApiKeyId}
|
||||||
|
className="aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
|
||||||
|
>
|
||||||
|
<RadioGroup.Indicator className="flex items-center justify-center">
|
||||||
|
<div className="h-2.5 w-2.5 rounded-full bg-current" />
|
||||||
|
</RadioGroup.Indicator>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
<Label htmlFor={authApiKeyId} className="text-xs sm:text-sm">API Key</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item
|
||||||
|
value="basic"
|
||||||
|
id={authBasicId}
|
||||||
|
className="aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
|
||||||
|
>
|
||||||
|
<RadioGroup.Indicator className="flex items-center justify-center">
|
||||||
|
<div className="h-2.5 w-2.5 rounded-full bg-current" />
|
||||||
|
</RadioGroup.Indicator>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
<Label htmlFor={authBasicId} className="text-xs sm:text-sm">Username & Password</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Root>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Basic Auth Fields */}
|
||||||
|
{form.watch("auth_method") === "basic" && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="text-xs sm:text-sm">Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="elastic"
|
||||||
|
autoComplete="username"
|
||||||
|
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="text-xs sm:text-sm">Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* API Key Field */}
|
||||||
|
{form.watch("auth_method") === "api_key" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="ELASTICSEARCH_API_KEY"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="text-xs sm:text-sm">API Key</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Your API Key Here"
|
||||||
|
autoComplete="off"
|
||||||
|
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-[10px] sm:text-xs">
|
||||||
|
Enter your Elasticsearch API key (base64 encoded). This will be stored securely.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Index Selection */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="indices"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="text-xs sm:text-sm">Index Selection</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="logs-*, documents-*, app-logs"
|
||||||
|
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-[10px] sm:text-xs">
|
||||||
|
Comma-separated indices to search (e.g., "logs-*, documents-*").
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Show parsed indices as badges */}
|
||||||
|
{form.watch("indices")?.trim() && (
|
||||||
|
<div className="rounded-lg border border-border bg-muted/50 p-3">
|
||||||
|
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Selected Indices:</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{stringToArray(form.watch("indices") ?? "").map((index) => (
|
||||||
|
<Badge key={index} variant="secondary" className="text-[10px]">
|
||||||
|
{index}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20">
|
||||||
|
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
|
<AlertTitle className="text-[10px] sm:text-xs">Index Selection Tips</AlertTitle>
|
||||||
|
<AlertDescription className="text-[9px] sm:text-[10px] mt-2">
|
||||||
|
<ul className="list-disc pl-4 space-y-1">
|
||||||
|
<li>Use wildcards like "logs-*" to match multiple indices</li>
|
||||||
|
<li>Separate multiple indices with commas</li>
|
||||||
|
<li>Leave empty to search all accessible indices including internal ones</li>
|
||||||
|
<li>Choosing specific indices improves search performance</li>
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{/* Advanced Configuration */}
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem value="advanced">
|
||||||
|
<AccordionTrigger className="text-xs sm:text-sm">Advanced Configuration</AccordionTrigger>
|
||||||
|
<AccordionContent className="space-y-4">
|
||||||
|
{/* Default Search Query */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="query"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="text-xs sm:text-sm">
|
||||||
|
Default Search Query{" "}
|
||||||
|
<span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="*"
|
||||||
|
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-[10px] sm:text-xs">
|
||||||
|
Default Elasticsearch query to use for searches. Use "*" to match all documents.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Form Fields */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="search_fields"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="text-xs sm:text-sm">
|
||||||
|
Search Fields{" "}
|
||||||
|
<span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="title, content, description"
|
||||||
|
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-[10px] sm:text-xs">
|
||||||
|
Comma-separated list of specific fields to search in (e.g., "title, content, description"). Leave empty to search all fields.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Show parsed search fields as badges */}
|
||||||
|
{form.watch("search_fields")?.trim() && (
|
||||||
|
<div className="rounded-lg border border-border bg-muted/50 p-3">
|
||||||
|
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Search Fields:</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{stringToArray(form.watch("search_fields") ?? "").map((field) => (
|
||||||
|
<Badge key={field} variant="outline" className="text-[10px]">
|
||||||
|
{field}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="max_documents"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="text-xs sm:text-sm">
|
||||||
|
Maximum Documents{" "}
|
||||||
|
<span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="1000"
|
||||||
|
min="1"
|
||||||
|
max="10000"
|
||||||
|
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
e.target.value === ""
|
||||||
|
? undefined
|
||||||
|
: parseInt(e.target.value, 10)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-[10px] sm:text-xs">
|
||||||
|
Maximum number of documents to retrieve per search (1-10,000). Leave empty to use Elasticsearch's default limit.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
{/* Indexing Configuration */}
|
||||||
|
<div className="space-y-4 pt-4 border-t border-slate-400/20">
|
||||||
|
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
|
||||||
|
|
||||||
|
{/* Date Range Selector */}
|
||||||
|
<DateRangeSelector
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onStartDateChange={setStartDate}
|
||||||
|
onEndDateChange={setEndDate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Periodic Sync Config */}
|
||||||
|
<div className="rounded-xl bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h3 className="font-medium text-sm sm:text-base">Enable Periodic Sync</h3>
|
||||||
|
<p className="text-xs sm:text-sm text-muted-foreground">
|
||||||
|
Automatically re-index at regular intervals
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{periodicEnabled && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="frequency" className="text-xs sm:text-sm">Sync Frequency</Label>
|
||||||
|
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}>
|
||||||
|
<SelectTrigger
|
||||||
|
id="frequency"
|
||||||
|
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Select frequency" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="z-[100]">
|
||||||
|
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem>
|
||||||
|
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem>
|
||||||
|
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem>
|
||||||
|
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem>
|
||||||
|
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem>
|
||||||
|
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* What you get section */}
|
||||||
|
{getConnectorBenefits(EnumConnectorName.ELASTICSEARCH_CONNECTOR) && (
|
||||||
|
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 px-3 sm:px-6 py-4 space-y-2">
|
||||||
|
<h4 className="text-xs sm:text-sm font-medium">What you get with Elasticsearch integration:</h4>
|
||||||
|
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
|
||||||
|
{getConnectorBenefits(EnumConnectorName.ELASTICSEARCH_CONNECTOR)?.map((benefit) => (
|
||||||
|
<li key={benefit}>{benefit}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Documentation Section */}
|
||||||
|
<Accordion type="single" collapsible className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5">
|
||||||
|
<AccordionItem value="documentation" className="border-0">
|
||||||
|
<AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
|
||||||
|
Documentation
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="px-3 sm:px-6 pb-3 sm:pb-6 space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3>
|
||||||
|
<p className="text-[10px] sm:text-xs text-muted-foreground">
|
||||||
|
The Elasticsearch connector allows you to search and retrieve documents from your Elasticsearch cluster. Configure connection details, select specific indices, and set search parameters to make your existing data searchable within SurfSense.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm sm:text-base font-semibold mb-2">Connection Setup</h3>
|
||||||
|
<div className="space-y-4 sm:space-y-6">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Get your Elasticsearch endpoint</h4>
|
||||||
|
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
|
||||||
|
You'll need the endpoint URL for your Elasticsearch cluster. This typically looks like:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1 text-[10px] sm:text-xs text-muted-foreground mb-4">
|
||||||
|
<li>Cloud: <code className="bg-muted px-1 py-0.5 rounded">https://your-cluster.es.region.aws.com:443</code></li>
|
||||||
|
<li>Self-hosted: <code className="bg-muted px-1 py-0.5 rounded">https://elasticsearch.example.com:9200</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Configure authentication</h4>
|
||||||
|
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
|
||||||
|
Elasticsearch requires authentication. You can use either:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
|
||||||
|
<li>
|
||||||
|
<strong>API Key:</strong> A base64-encoded API key. You can create one in Elasticsearch by running:
|
||||||
|
<pre className="bg-muted p-2 rounded mt-1 text-[9px] overflow-x-auto">
|
||||||
|
<code>POST /_security/api_key</code>
|
||||||
|
</pre>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Username & Password:</strong> Basic authentication using your Elasticsearch username and password.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 3: Select indices</h4>
|
||||||
|
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
|
||||||
|
Specify which indices to search. You can:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1 text-[10px] sm:text-xs text-muted-foreground">
|
||||||
|
<li>Use wildcards: <code className="bg-muted px-1 py-0.5 rounded">logs-*</code> to match multiple indices</li>
|
||||||
|
<li>List specific indices: <code className="bg-muted px-1 py-0.5 rounded">logs-2024, documents-2024</code></li>
|
||||||
|
<li>Leave empty to search all accessible indices (not recommended for performance)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm sm:text-base font-semibold mb-2">Advanced Configuration</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Search Query</h4>
|
||||||
|
<p className="text-[10px] sm:text-xs text-muted-foreground mb-2">
|
||||||
|
The default query used for searches. Use <code className="bg-muted px-1 py-0.5 rounded">*</code> to match all documents, or specify a more complex Elasticsearch query.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Search Fields</h4>
|
||||||
|
<p className="text-[10px] sm:text-xs text-muted-foreground mb-2">
|
||||||
|
Limit searches to specific fields for better performance. Common fields include:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1 text-[10px] sm:text-xs text-muted-foreground">
|
||||||
|
<li><code className="bg-muted px-1 py-0.5 rounded">title</code> - Document titles</li>
|
||||||
|
<li><code className="bg-muted px-1 py-0.5 rounded">content</code> - Main content</li>
|
||||||
|
<li><code className="bg-muted px-1 py-0.5 rounded">description</code> - Descriptions</li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-[10px] sm:text-xs text-muted-foreground mt-2">
|
||||||
|
Leave empty to search all fields in your documents.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Maximum Documents</h4>
|
||||||
|
<p className="text-[10px] sm:text-xs text-muted-foreground">
|
||||||
|
Set a limit on the number of documents retrieved per search (1-10,000). This helps control response times and resource usage. Leave empty to use Elasticsearch's default limit.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm sm:text-base font-semibold mb-2">Troubleshooting</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Connection Issues</h4>
|
||||||
|
<ul className="list-disc pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
|
||||||
|
<li>
|
||||||
|
<strong>Invalid URL:</strong> Ensure your endpoint URL includes the protocol (https://) and port number if required.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>SSL/TLS Errors:</strong> Verify that your cluster uses HTTPS and the certificate is valid. Self-signed certificates may require additional configuration.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Connection Timeout:</strong> Check your network connectivity and firewall settings. Ensure the Elasticsearch cluster is accessible from SurfSense servers.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Authentication Issues</h4>
|
||||||
|
<ul className="list-disc pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
|
||||||
|
<li>
|
||||||
|
<strong>Invalid Credentials:</strong> Double-check your username/password or API key. API keys must be base64-encoded.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Permission Denied:</strong> Ensure your API key or user account has read permissions for the indices you want to search.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>API Key Format:</strong> Elasticsearch API keys are typically base64-encoded strings. Make sure you're using the full key value.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Search Issues</h4>
|
||||||
|
<ul className="list-disc pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
|
||||||
|
<li>
|
||||||
|
<strong>No Results:</strong> Verify that your index selection matches existing indices. Use wildcards carefully.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Slow Searches:</strong> Limit the number of indices or use specific index names instead of wildcards. Reduce the maximum documents limit.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Field Not Found:</strong> Ensure the search fields you specify actually exist in your Elasticsearch documents.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mt-4">
|
||||||
|
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
|
<AlertTitle className="text-[10px] sm:text-xs">Need More Help?</AlertTitle>
|
||||||
|
<AlertDescription className="text-[9px] sm:text-[10px]">
|
||||||
|
If you continue to experience issues, check your Elasticsearch cluster logs and ensure your cluster version is compatible. For Elasticsearch Cloud deployments, verify your access policies and IP allowlists.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -23,9 +23,20 @@ import {
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
import { EnumConnectorName } from "@/contracts/enums/connector";
|
||||||
import type { ConnectFormProps } from "../index";
|
import type { ConnectFormProps } from "../index";
|
||||||
import { getConnectorBenefits } from "../connector-benefits";
|
import { getConnectorBenefits } from "../connector-benefits";
|
||||||
|
import { DateRangeSelector } from "../../components/date-range-selector";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
const linearConnectorFormSchema = z.object({
|
const linearConnectorFormSchema = z.object({
|
||||||
name: z.string().min(3, {
|
name: z.string().min(3, {
|
||||||
|
|
@ -48,6 +59,10 @@ export const LinearConnectForm: FC<ConnectFormProps> = ({
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
}) => {
|
}) => {
|
||||||
const isSubmittingRef = useRef(false);
|
const isSubmittingRef = useRef(false);
|
||||||
|
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
|
||||||
|
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
|
||||||
|
const [periodicEnabled, setPeriodicEnabled] = useState(false);
|
||||||
|
const [frequencyMinutes, setFrequencyMinutes] = useState("1440");
|
||||||
const form = useForm<LinearConnectorFormValues>({
|
const form = useForm<LinearConnectorFormValues>({
|
||||||
resolver: zodResolver(linearConnectorFormSchema),
|
resolver: zodResolver(linearConnectorFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
|
@ -72,9 +87,13 @@ export const LinearConnectForm: FC<ConnectFormProps> = ({
|
||||||
},
|
},
|
||||||
is_indexable: true,
|
is_indexable: true,
|
||||||
last_indexed_at: null,
|
last_indexed_at: null,
|
||||||
periodic_indexing_enabled: false,
|
periodic_indexing_enabled: periodicEnabled,
|
||||||
indexing_frequency_minutes: null,
|
indexing_frequency_minutes: periodicEnabled ? parseInt(frequencyMinutes, 10) : null,
|
||||||
next_scheduled_at: null,
|
next_scheduled_at: null,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
periodicEnabled,
|
||||||
|
frequencyMinutes,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
isSubmittingRef.current = false;
|
isSubmittingRef.current = false;
|
||||||
|
|
@ -148,6 +167,56 @@ export const LinearConnectForm: FC<ConnectFormProps> = ({
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Indexing Configuration */}
|
||||||
|
<div className="space-y-4 pt-4 border-t border-slate-400/20">
|
||||||
|
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
|
||||||
|
|
||||||
|
{/* Date Range Selector */}
|
||||||
|
<DateRangeSelector
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onStartDateChange={setStartDate}
|
||||||
|
onEndDateChange={setEndDate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Periodic Sync Config */}
|
||||||
|
<div className="rounded-xl bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h3 className="font-medium text-sm sm:text-base">Enable Periodic Sync</h3>
|
||||||
|
<p className="text-xs sm:text-sm text-muted-foreground">
|
||||||
|
Automatically re-index at regular intervals
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{periodicEnabled && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="frequency" className="text-xs sm:text-sm">Sync Frequency</Label>
|
||||||
|
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}>
|
||||||
|
<SelectTrigger
|
||||||
|
id="frequency"
|
||||||
|
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Select frequency" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="z-[100]">
|
||||||
|
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem>
|
||||||
|
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem>
|
||||||
|
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem>
|
||||||
|
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem>
|
||||||
|
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem>
|
||||||
|
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
import { EnumConnectorName } from "@/contracts/enums/connector";
|
||||||
import type { ConnectFormProps } from "../index";
|
import type { ConnectFormProps } from "../index";
|
||||||
|
import { getConnectorBenefits } from "../connector-benefits";
|
||||||
|
|
||||||
const tavilyApiFormSchema = z.object({
|
const tavilyApiFormSchema = z.object({
|
||||||
name: z.string().min(3, {
|
name: z.string().min(3, {
|
||||||
|
|
@ -139,6 +140,18 @@ export const TavilyApiConnectForm: FC<ConnectFormProps> = ({
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* What you get section */}
|
||||||
|
{getConnectorBenefits(EnumConnectorName.TAVILY_API) && (
|
||||||
|
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 px-3 sm:px-6 py-4 space-y-2">
|
||||||
|
<h4 className="text-xs sm:text-sm font-medium">What you get with Tavily API:</h4>
|
||||||
|
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
|
||||||
|
{getConnectorBenefits(EnumConnectorName.TAVILY_API)?.map((benefit) => (
|
||||||
|
<li key={benefit}>{benefit}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,19 @@ export function getConnectorBenefits(connectorType: string): string[] | null {
|
||||||
"Keep your search results up-to-date with latest Linear content",
|
"Keep your search results up-to-date with latest Linear content",
|
||||||
"Index your Linear issues for enhanced search capabilities",
|
"Index your Linear issues for enhanced search capabilities",
|
||||||
],
|
],
|
||||||
|
ELASTICSEARCH_CONNECTOR: [
|
||||||
|
"Search across your indexed documents and logs",
|
||||||
|
"Access structured and unstructured data from your cluster",
|
||||||
|
"Leverage existing Elasticsearch indices for enhanced search",
|
||||||
|
"Real-time search capabilities with powerful query features",
|
||||||
|
"Integration with your existing Elasticsearch infrastructure",
|
||||||
|
],
|
||||||
|
TAVILY_API: [
|
||||||
|
"AI-powered search results tailored to your queries",
|
||||||
|
"Real-time information from the web",
|
||||||
|
"Enhanced search capabilities for your projects",
|
||||||
|
],
|
||||||
// Add other connectors as needed
|
// Add other connectors as needed
|
||||||
// TAVILY_API: [...],
|
|
||||||
// GITHUB_CONNECTOR: [...],
|
// GITHUB_CONNECTOR: [...],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
|
import { ElasticsearchConnectForm } from "./components/elasticsearch-connect-form";
|
||||||
import { LinearConnectForm } from "./components/linear-connect-form";
|
import { LinearConnectForm } from "./components/linear-connect-form";
|
||||||
import { TavilyApiConnectForm } from "./components/tavily-api-connect-form";
|
import { TavilyApiConnectForm } from "./components/tavily-api-connect-form";
|
||||||
|
|
||||||
|
|
@ -10,8 +11,12 @@ export interface ConnectFormProps {
|
||||||
is_indexable: boolean;
|
is_indexable: boolean;
|
||||||
last_indexed_at: null;
|
last_indexed_at: null;
|
||||||
periodic_indexing_enabled: boolean;
|
periodic_indexing_enabled: boolean;
|
||||||
indexing_frequency_minutes: null;
|
indexing_frequency_minutes: number | null;
|
||||||
next_scheduled_at: null;
|
next_scheduled_at: null;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
periodicEnabled?: boolean;
|
||||||
|
frequencyMinutes?: string;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
|
|
@ -31,6 +36,8 @@ export function getConnectFormComponent(
|
||||||
return TavilyApiConnectForm;
|
return TavilyApiConnectForm;
|
||||||
case "LINEAR_CONNECTOR":
|
case "LINEAR_CONNECTOR":
|
||||||
return LinearConnectForm;
|
return LinearConnectForm;
|
||||||
|
case "ELASTICSEARCH_CONNECTOR":
|
||||||
|
return ElasticsearchConnectForm;
|
||||||
// Add other connector types here as needed
|
// Add other connector types here as needed
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,438 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { KeyRound, Server } from "lucide-react";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
|
import { useId } from "react";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import type { ConnectorConfigProps } from "../index";
|
||||||
|
|
||||||
|
export interface ElasticsearchConfigProps extends ConnectorConfigProps {
|
||||||
|
onNameChange?: (name: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ElasticsearchConfig: FC<ElasticsearchConfigProps> = ({
|
||||||
|
connector,
|
||||||
|
onConfigChange,
|
||||||
|
onNameChange,
|
||||||
|
}) => {
|
||||||
|
const authBasicId = useId();
|
||||||
|
const authApiKeyId = useId();
|
||||||
|
|
||||||
|
const [name, setName] = useState<string>(connector.name || "");
|
||||||
|
const [endpointUrl, setEndpointUrl] = useState<string>(
|
||||||
|
(connector.config?.ELASTICSEARCH_URL as string) || ""
|
||||||
|
);
|
||||||
|
const [authMethod, setAuthMethod] = useState<"basic" | "api_key">(
|
||||||
|
(connector.config?.ELASTICSEARCH_API_KEY ? "api_key" : "basic") as "basic" | "api_key"
|
||||||
|
);
|
||||||
|
const [username, setUsername] = useState<string>(
|
||||||
|
(connector.config?.ELASTICSEARCH_USERNAME as string) || ""
|
||||||
|
);
|
||||||
|
const [password, setPassword] = useState<string>(
|
||||||
|
(connector.config?.ELASTICSEARCH_PASSWORD as string) || ""
|
||||||
|
);
|
||||||
|
const [apiKey, setApiKey] = useState<string>(
|
||||||
|
(connector.config?.ELASTICSEARCH_API_KEY as string) || ""
|
||||||
|
);
|
||||||
|
const [indices, setIndices] = useState<string>(
|
||||||
|
Array.isArray(connector.config?.ELASTICSEARCH_INDEX)
|
||||||
|
? (connector.config?.ELASTICSEARCH_INDEX as string[]).join(", ")
|
||||||
|
: (connector.config?.ELASTICSEARCH_INDEX as string) || ""
|
||||||
|
);
|
||||||
|
const [query, setQuery] = useState<string>(
|
||||||
|
(connector.config?.ELASTICSEARCH_QUERY as string) || "*"
|
||||||
|
);
|
||||||
|
const [searchFields, setSearchFields] = useState<string>(
|
||||||
|
Array.isArray(connector.config?.ELASTICSEARCH_FIELDS)
|
||||||
|
? (connector.config?.ELASTICSEARCH_FIELDS as string[]).join(", ")
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
const [maxDocuments, setMaxDocuments] = useState<string>(
|
||||||
|
connector.config?.ELASTICSEARCH_MAX_DOCUMENTS
|
||||||
|
? String(connector.config.ELASTICSEARCH_MAX_DOCUMENTS)
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update values when connector changes
|
||||||
|
useEffect(() => {
|
||||||
|
setName(connector.name || "");
|
||||||
|
setEndpointUrl((connector.config?.ELASTICSEARCH_URL as string) || "");
|
||||||
|
setAuthMethod(
|
||||||
|
(connector.config?.ELASTICSEARCH_API_KEY ? "api_key" : "basic") as "basic" | "api_key"
|
||||||
|
);
|
||||||
|
setUsername((connector.config?.ELASTICSEARCH_USERNAME as string) || "");
|
||||||
|
setPassword((connector.config?.ELASTICSEARCH_PASSWORD as string) || "");
|
||||||
|
setApiKey((connector.config?.ELASTICSEARCH_API_KEY as string) || "");
|
||||||
|
setIndices(
|
||||||
|
Array.isArray(connector.config?.ELASTICSEARCH_INDEX)
|
||||||
|
? (connector.config?.ELASTICSEARCH_INDEX as string[]).join(", ")
|
||||||
|
: (connector.config?.ELASTICSEARCH_INDEX as string) || ""
|
||||||
|
);
|
||||||
|
setQuery((connector.config?.ELASTICSEARCH_QUERY as string) || "*");
|
||||||
|
setSearchFields(
|
||||||
|
Array.isArray(connector.config?.ELASTICSEARCH_FIELDS)
|
||||||
|
? (connector.config?.ELASTICSEARCH_FIELDS as string[]).join(", ")
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
setMaxDocuments(
|
||||||
|
connector.config?.ELASTICSEARCH_MAX_DOCUMENTS
|
||||||
|
? String(connector.config.ELASTICSEARCH_MAX_DOCUMENTS)
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
}, [connector.config, connector.name]);
|
||||||
|
|
||||||
|
const stringToArray = (str: string): string[] => {
|
||||||
|
const items = str
|
||||||
|
.split(",")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
return Array.from(new Set(items));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateConfig = (updates: Record<string, unknown>) => {
|
||||||
|
if (onConfigChange) {
|
||||||
|
// Filter out undefined values to remove keys
|
||||||
|
const filteredUpdates = Object.fromEntries(
|
||||||
|
Object.entries(updates).filter(([_, value]) => value !== undefined)
|
||||||
|
);
|
||||||
|
const newConfig = {
|
||||||
|
...connector.config,
|
||||||
|
...filteredUpdates,
|
||||||
|
};
|
||||||
|
// Remove keys that were set to undefined
|
||||||
|
Object.keys(updates).forEach((key) => {
|
||||||
|
if (updates[key] === undefined) {
|
||||||
|
delete newConfig[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onConfigChange(newConfig);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNameChange = (value: string) => {
|
||||||
|
setName(value);
|
||||||
|
if (onNameChange) {
|
||||||
|
onNameChange(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEndpointUrlChange = (value: string) => {
|
||||||
|
setEndpointUrl(value);
|
||||||
|
updateConfig({ ELASTICSEARCH_URL: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAuthMethodChange = (value: "basic" | "api_key") => {
|
||||||
|
setAuthMethod(value);
|
||||||
|
if (value === "basic") {
|
||||||
|
updateConfig({
|
||||||
|
ELASTICSEARCH_API_KEY: undefined,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateConfig({
|
||||||
|
ELASTICSEARCH_USERNAME: undefined,
|
||||||
|
ELASTICSEARCH_PASSWORD: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUsernameChange = (value: string) => {
|
||||||
|
setUsername(value);
|
||||||
|
updateConfig({ ELASTICSEARCH_USERNAME: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordChange = (value: string) => {
|
||||||
|
setPassword(value);
|
||||||
|
updateConfig({ ELASTICSEARCH_PASSWORD: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApiKeyChange = (value: string) => {
|
||||||
|
setApiKey(value);
|
||||||
|
updateConfig({ ELASTICSEARCH_API_KEY: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIndicesChange = (value: string) => {
|
||||||
|
setIndices(value);
|
||||||
|
const indicesArr = stringToArray(value);
|
||||||
|
const indexValue =
|
||||||
|
indicesArr.length === 0 ? "*" : indicesArr.length === 1 ? indicesArr[0] : indicesArr;
|
||||||
|
updateConfig({ ELASTICSEARCH_INDEX: indexValue });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQueryChange = (value: string) => {
|
||||||
|
setQuery(value);
|
||||||
|
if (value && value !== "*") {
|
||||||
|
updateConfig({ ELASTICSEARCH_QUERY: value });
|
||||||
|
} else {
|
||||||
|
// Remove the key by setting it to undefined
|
||||||
|
updateConfig({ ELASTICSEARCH_QUERY: undefined });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchFieldsChange = (value: string) => {
|
||||||
|
setSearchFields(value);
|
||||||
|
if (value.trim()) {
|
||||||
|
const fields = stringToArray(value);
|
||||||
|
updateConfig({
|
||||||
|
ELASTICSEARCH_FIELDS: fields,
|
||||||
|
ELASTICSEARCH_CONTENT_FIELDS: fields,
|
||||||
|
ELASTICSEARCH_TITLE_FIELD: fields.includes("title") ? "title" : undefined,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Remove the keys by setting them to undefined
|
||||||
|
updateConfig({
|
||||||
|
ELASTICSEARCH_FIELDS: undefined,
|
||||||
|
ELASTICSEARCH_CONTENT_FIELDS: undefined,
|
||||||
|
ELASTICSEARCH_TITLE_FIELD: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMaxDocumentsChange = (value: string) => {
|
||||||
|
setMaxDocuments(value);
|
||||||
|
if (value && value.trim()) {
|
||||||
|
const num = parseInt(value, 10);
|
||||||
|
if (!isNaN(num) && num > 0) {
|
||||||
|
updateConfig({ ELASTICSEARCH_MAX_DOCUMENTS: num });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove the key by setting it to undefined
|
||||||
|
updateConfig({ ELASTICSEARCH_MAX_DOCUMENTS: undefined });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 Elasticsearch 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>
|
||||||
|
|
||||||
|
{/* Connection Details */}
|
||||||
|
<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 flex items-center gap-2">
|
||||||
|
<Server className="h-4 w-4" />
|
||||||
|
Connection Details
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs sm:text-sm">Elasticsearch Endpoint URL</Label>
|
||||||
|
<Input
|
||||||
|
type="url"
|
||||||
|
value={endpointUrl}
|
||||||
|
onChange={(e) => handleEndpointUrlChange(e.target.value)}
|
||||||
|
placeholder="https://your-cluster.es.region.aws.com:443"
|
||||||
|
className="border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] sm:text-xs text-muted-foreground">
|
||||||
|
Update the Elasticsearch endpoint URL if needed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Authentication */}
|
||||||
|
<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 flex items-center gap-2">
|
||||||
|
<KeyRound className="h-4 w-4" />
|
||||||
|
Authentication
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<RadioGroup.Root
|
||||||
|
value={authMethod}
|
||||||
|
onValueChange={(value) => handleAuthMethodChange(value as "basic" | "api_key")}
|
||||||
|
className="flex flex-col space-y-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item
|
||||||
|
value="api_key"
|
||||||
|
id={authApiKeyId}
|
||||||
|
className="aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
|
||||||
|
>
|
||||||
|
<RadioGroup.Indicator className="flex items-center justify-center">
|
||||||
|
<div className="h-2.5 w-2.5 rounded-full bg-current" />
|
||||||
|
</RadioGroup.Indicator>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
<Label htmlFor={authApiKeyId} className="text-xs sm:text-sm">API Key</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item
|
||||||
|
value="basic"
|
||||||
|
id={authBasicId}
|
||||||
|
className="aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
|
||||||
|
>
|
||||||
|
<RadioGroup.Indicator className="flex items-center justify-center">
|
||||||
|
<div className="h-2.5 w-2.5 rounded-full bg-current" />
|
||||||
|
</RadioGroup.Indicator>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
<Label htmlFor={authBasicId} className="text-xs sm:text-sm">Username & Password</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Root>
|
||||||
|
|
||||||
|
{authMethod === "basic" && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs sm:text-sm">Username</Label>
|
||||||
|
<Input
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => handleUsernameChange(e.target.value)}
|
||||||
|
placeholder="elastic"
|
||||||
|
className="border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs sm:text-sm">Password</Label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => handlePasswordChange(e.target.value)}
|
||||||
|
placeholder="Password"
|
||||||
|
className="border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{authMethod === "api_key" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs sm:text-sm">API Key</Label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => handleApiKeyChange(e.target.value)}
|
||||||
|
placeholder="Your API Key Here"
|
||||||
|
className="border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] sm:text-xs text-muted-foreground">
|
||||||
|
Update the Elasticsearch API key if needed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Index Selection */}
|
||||||
|
<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">Index Selection</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs sm:text-sm">Indices</Label>
|
||||||
|
<Input
|
||||||
|
value={indices}
|
||||||
|
onChange={(e) => handleIndicesChange(e.target.value)}
|
||||||
|
placeholder="logs-*, documents-*, app-logs"
|
||||||
|
className="border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] sm:text-xs text-muted-foreground">
|
||||||
|
Comma-separated indices to search (e.g., "logs-*, documents-*").
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{indices.trim() && (
|
||||||
|
<div className="rounded-lg border border-border bg-muted/50 p-3">
|
||||||
|
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Selected Indices:</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{stringToArray(indices).map((index) => (
|
||||||
|
<Badge key={index} variant="secondary" className="text-[10px]">
|
||||||
|
{index}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Advanced 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">Advanced Configuration</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs sm:text-sm">
|
||||||
|
Default Search Query <span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => handleQueryChange(e.target.value)}
|
||||||
|
placeholder="*"
|
||||||
|
className="border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] sm:text-xs text-muted-foreground">
|
||||||
|
Default Elasticsearch query to use for searches. Use "*" to match all documents.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs sm:text-sm">
|
||||||
|
Search Fields <span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
value={searchFields}
|
||||||
|
onChange={(e) => handleSearchFieldsChange(e.target.value)}
|
||||||
|
placeholder="title, content, description"
|
||||||
|
className="border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] sm:text-xs text-muted-foreground">
|
||||||
|
Comma-separated list of specific fields to search in.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{searchFields.trim() && (
|
||||||
|
<div className="rounded-lg border border-border bg-muted/50 p-3">
|
||||||
|
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Search Fields:</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{stringToArray(searchFields).map((field) => (
|
||||||
|
<Badge key={field} variant="outline" className="text-[10px]">
|
||||||
|
{field}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs sm:text-sm">
|
||||||
|
Maximum Documents <span className="text-muted-foreground">(Optional)</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={maxDocuments}
|
||||||
|
onChange={(e) => handleMaxDocumentsChange(e.target.value)}
|
||||||
|
placeholder="1000"
|
||||||
|
min="1"
|
||||||
|
max="10000"
|
||||||
|
className="border-slate-400/20 focus-visible:border-slate-400/40"
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] sm:text-xs text-muted-foreground">
|
||||||
|
Maximum number of documents to retrieve per search (1-10,000).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
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 { ElasticsearchConfig } from "./components/elasticsearch-config";
|
||||||
import { GoogleDriveConfig } from "./components/google-drive-config";
|
import { GoogleDriveConfig } from "./components/google-drive-config";
|
||||||
import { LinearConfig } from "./components/linear-config";
|
import { LinearConfig } from "./components/linear-config";
|
||||||
import { TavilyApiConfig } from "./components/tavily-api-config";
|
import { TavilyApiConfig } from "./components/tavily-api-config";
|
||||||
|
|
@ -30,6 +31,8 @@ export function getConnectorConfigComponent(
|
||||||
return LinearConfig;
|
return LinearConfig;
|
||||||
case "WEBCRAWLER_CONNECTOR":
|
case "WEBCRAWLER_CONNECTOR":
|
||||||
return WebcrawlerConfig;
|
return WebcrawlerConfig;
|
||||||
|
case "ELASTICSEARCH_CONNECTOR":
|
||||||
|
return ElasticsearchConfig;
|
||||||
// OAuth connectors (Gmail, Calendar, Airtable) and others don't need special config UI
|
// OAuth connectors (Gmail, Calendar, Airtable) and others don't need special config UI
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ export const ConnectorConnectView: FC<ConnectorConnectViewProps> = ({
|
||||||
const formIdMap: Record<string, string> = {
|
const formIdMap: Record<string, string> = {
|
||||||
TAVILY_API: "tavily-connect-form",
|
TAVILY_API: "tavily-connect-form",
|
||||||
LINEAR_CONNECTOR: "linear-connect-form",
|
LINEAR_CONNECTOR: "linear-connect-form",
|
||||||
|
ELASTICSEARCH_CONNECTOR: "elasticsearch-connect-form",
|
||||||
};
|
};
|
||||||
const formId = formIdMap[connectorType];
|
const formId = formIdMap[connectorType];
|
||||||
if (formId) {
|
if (formId) {
|
||||||
|
|
|
||||||
|
|
@ -349,8 +349,12 @@ export const useConnectorDialog = () => {
|
||||||
is_indexable: boolean;
|
is_indexable: boolean;
|
||||||
last_indexed_at: null;
|
last_indexed_at: null;
|
||||||
periodic_indexing_enabled: boolean;
|
periodic_indexing_enabled: boolean;
|
||||||
indexing_frequency_minutes: null;
|
indexing_frequency_minutes: number | null;
|
||||||
next_scheduled_at: null;
|
next_scheduled_at: null;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
periodicEnabled?: boolean;
|
||||||
|
frequencyMinutes?: string;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
if (!searchSpaceId || !connectingConnectorType) return;
|
if (!searchSpaceId || !connectingConnectorType) return;
|
||||||
|
|
@ -361,9 +365,12 @@ export const useConnectorDialog = () => {
|
||||||
|
|
||||||
setIsCreatingConnector(true);
|
setIsCreatingConnector(true);
|
||||||
try {
|
try {
|
||||||
|
// Extract UI-only fields before sending to backend
|
||||||
|
const { startDate, endDate, periodicEnabled, frequencyMinutes, ...connectorData } = formData;
|
||||||
|
|
||||||
// Create connector
|
// Create connector
|
||||||
const newConnector = await createConnector({
|
const newConnector = await createConnector({
|
||||||
data: formData,
|
data: connectorData,
|
||||||
queryParams: {
|
queryParams: {
|
||||||
search_space_id: searchSpaceId,
|
search_space_id: searchSpaceId,
|
||||||
},
|
},
|
||||||
|
|
@ -379,29 +386,118 @@ export const useConnectorDialog = () => {
|
||||||
// Validate connector data
|
// Validate connector data
|
||||||
const connectorValidation = searchSourceConnector.safeParse(connector);
|
const connectorValidation = searchSourceConnector.safeParse(connector);
|
||||||
if (connectorValidation.success) {
|
if (connectorValidation.success) {
|
||||||
|
// Store connectingConnectorType before clearing it
|
||||||
|
const currentConnectorType = connectingConnectorType;
|
||||||
|
|
||||||
// Find connector title from constants
|
// Find connector title from constants
|
||||||
const connectorInfo = OTHER_CONNECTORS.find(
|
const connectorInfo = OTHER_CONNECTORS.find(
|
||||||
c => c.connectorType === connectingConnectorType
|
c => c.connectorType === currentConnectorType
|
||||||
);
|
);
|
||||||
const connectorTitle = connectorInfo?.title || connector.name;
|
const connectorTitle = connectorInfo?.title || connector.name;
|
||||||
|
|
||||||
// Set up indexing config
|
// Set up indexing config
|
||||||
const config = validateIndexingConfigState({
|
const config = validateIndexingConfigState({
|
||||||
connectorType: connectingConnectorType as EnumConnectorName,
|
connectorType: currentConnectorType as EnumConnectorName,
|
||||||
connectorId: connector.id,
|
connectorId: connector.id,
|
||||||
connectorTitle,
|
connectorTitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clear connecting state to allow view transition
|
||||||
|
setConnectingConnectorType(null);
|
||||||
|
|
||||||
|
// Set indexing config state
|
||||||
setIndexingConfig(config);
|
setIndexingConfig(config);
|
||||||
setIndexingConnector(connector);
|
setIndexingConnector(connector);
|
||||||
setIndexingConnectorConfig(connector.config || {});
|
setIndexingConnectorConfig(connector.config || {});
|
||||||
|
|
||||||
// Transition to configure view
|
// Pre-populate indexing configuration with values from form if provided
|
||||||
const url = new URL(window.location.href);
|
if (formData.startDate !== undefined) {
|
||||||
url.searchParams.set("view", "configure");
|
setStartDate(formData.startDate);
|
||||||
url.searchParams.delete("connectorType");
|
}
|
||||||
window.history.replaceState({}, "", url.toString());
|
if (formData.endDate !== undefined) {
|
||||||
|
setEndDate(formData.endDate);
|
||||||
|
}
|
||||||
|
if (formData.periodicEnabled !== undefined) {
|
||||||
|
setPeriodicEnabled(formData.periodicEnabled);
|
||||||
|
}
|
||||||
|
if (formData.frequencyMinutes !== undefined) {
|
||||||
|
setFrequencyMinutes(formData.frequencyMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-start indexing for non-OAuth reindexable connectors
|
||||||
|
// This only applies to non-OAuth reindexable connectors (e.g., Elasticsearch, Linear)
|
||||||
|
// Non-reindexable connectors (e.g., Tavily) have is_indexable: false, so they won't trigger this
|
||||||
|
// Backend will use default date ranges (365 days ago to today) if dates are not provided
|
||||||
|
if (connector.is_indexable) {
|
||||||
|
// Get indexing configuration from form (or use defaults)
|
||||||
|
const startDateForIndexing = formData.startDate;
|
||||||
|
const endDateForIndexing = formData.endDate;
|
||||||
|
const periodicEnabledForIndexing = formData.periodicEnabled || false;
|
||||||
|
const frequencyMinutesForIndexing = formData.frequencyMinutes || "1440";
|
||||||
|
|
||||||
|
// Update connector with periodic sync settings if enabled
|
||||||
|
if (periodicEnabledForIndexing) {
|
||||||
|
const frequency = parseInt(frequencyMinutesForIndexing, 10);
|
||||||
|
await updateConnector({
|
||||||
|
id: connector.id,
|
||||||
|
data: {
|
||||||
|
periodic_indexing_enabled: true,
|
||||||
|
indexing_frequency_minutes: frequency,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start indexing (backend will use defaults if dates are undefined)
|
||||||
|
const startDateStr = startDateForIndexing ? format(startDateForIndexing, "yyyy-MM-dd") : undefined;
|
||||||
|
const endDateStr = endDateForIndexing ? format(endDateForIndexing, "yyyy-MM-dd") : undefined;
|
||||||
|
|
||||||
|
await indexConnector({
|
||||||
|
connector_id: connector.id,
|
||||||
|
queryParams: {
|
||||||
|
search_space_id: searchSpaceId,
|
||||||
|
start_date: startDateStr,
|
||||||
|
end_date: endDateStr,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success(`${connectorTitle} connected and indexing started!`, {
|
||||||
|
description: periodicEnabledForIndexing
|
||||||
|
? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutesForIndexing)}.`
|
||||||
|
: "You can continue working while we sync your data.",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal and return to main view
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.delete("modal");
|
||||||
|
url.searchParams.delete("tab");
|
||||||
|
url.searchParams.delete("view");
|
||||||
|
url.searchParams.delete("connectorType");
|
||||||
|
router.replace(url.pathname + url.search, { scroll: false });
|
||||||
|
|
||||||
|
// Clear indexing config state since we're not showing the view
|
||||||
|
setIndexingConfig(null);
|
||||||
|
setIndexingConnector(null);
|
||||||
|
setIndexingConnectorConfig(null);
|
||||||
|
|
||||||
|
// Invalidate queries to refresh data
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh connectors list
|
||||||
|
await refetchAllConnectors();
|
||||||
|
} else {
|
||||||
|
// Non-indexable connector - just show success message
|
||||||
toast.success(`${connectorTitle} connected successfully!`);
|
toast.success(`${connectorTitle} connected successfully!`);
|
||||||
|
|
||||||
|
// Close modal and return to main view
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.delete("modal");
|
||||||
|
url.searchParams.delete("tab");
|
||||||
|
url.searchParams.delete("view");
|
||||||
|
url.searchParams.delete("connectorType");
|
||||||
|
router.replace(url.pathname + url.search, { scroll: false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -411,9 +507,9 @@ export const useConnectorDialog = () => {
|
||||||
} finally {
|
} finally {
|
||||||
isCreatingConnectorRef.current = false;
|
isCreatingConnectorRef.current = false;
|
||||||
setIsCreatingConnector(false);
|
setIsCreatingConnector(false);
|
||||||
setConnectingConnectorType(null);
|
// Don't clear connectingConnectorType here - it's cleared above when transitioning to config view
|
||||||
}
|
}
|
||||||
}, [connectingConnectorType, searchSpaceId, createConnector, refetchAllConnectors]);
|
}, [connectingConnectorType, searchSpaceId, createConnector, refetchAllConnectors, updateConnector, indexConnector, router, getFrequencyLabel, queryClient]);
|
||||||
|
|
||||||
// Handle going back from connect view
|
// Handle going back from connect view
|
||||||
const handleBackFromConnect = useCallback(() => {
|
const handleBackFromConnect = useCallback(() => {
|
||||||
|
|
@ -561,13 +657,14 @@ export const useConnectorDialog = () => {
|
||||||
(oauthConnector) => oauthConnector.connectorType === connector.connector_type
|
(oauthConnector) => oauthConnector.connectorType === connector.connector_type
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if this is webcrawler, Tavily API, or Linear (can be managed in popup)
|
// Check if this is webcrawler, Tavily API, Linear, or Elasticsearch (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;
|
const isTavilyApi = connector.connector_type === EnumConnectorName.TAVILY_API;
|
||||||
const isLinear = connector.connector_type === EnumConnectorName.LINEAR_CONNECTOR;
|
const isLinear = connector.connector_type === EnumConnectorName.LINEAR_CONNECTOR;
|
||||||
|
const isElasticsearch = connector.connector_type === EnumConnectorName.ELASTICSEARCH_CONNECTOR;
|
||||||
|
|
||||||
// If not OAuth, not webcrawler, not Tavily API, and not Linear, redirect to old connector edit page
|
// If not OAuth, not webcrawler, not Tavily API, not Linear, and not Elasticsearch, redirect to old connector edit page
|
||||||
if (!isOAuthConnector && !isWebcrawler && !isTavilyApi && !isLinear) {
|
if (!isOAuthConnector && !isWebcrawler && !isTavilyApi && !isLinear && !isElasticsearch) {
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors/${connector.id}/edit`);
|
router.push(`/dashboard/${searchSpaceId}/connectors/${connector.id}/edit`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import type { LogSummary, LogActiveTask } from "@/contracts/types/log.types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
TabsContent,
|
TabsContent,
|
||||||
TabsTrigger,
|
|
||||||
} from "@/components/ui/tabs";
|
} from "@/components/ui/tabs";
|
||||||
|
|
||||||
interface ActiveConnectorsTabProps {
|
interface ActiveConnectorsTabProps {
|
||||||
|
|
@ -140,9 +139,13 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
|
||||||
<p className="text-sm text-muted-foreground mt-1 max-w-[280px]">
|
<p className="text-sm text-muted-foreground mt-1 max-w-[280px]">
|
||||||
Connect your first service to start searching across all your data.
|
Connect your first service to start searching across all your data.
|
||||||
</p>
|
</p>
|
||||||
<TabsTrigger value="all" className="mt-6 text-primary hover:underline" onClick={() => onTabChange("all")}>
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="mt-6 text-primary hover:underline"
|
||||||
|
onClick={() => onTabChange("all")}
|
||||||
|
>
|
||||||
Browse available connectors
|
Browse available connectors
|
||||||
</TabsTrigger>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
||||||
const isWebcrawler = connector.id === "webcrawler-connector";
|
const isWebcrawler = connector.id === "webcrawler-connector";
|
||||||
const isTavily = connector.id === "tavily-api";
|
const isTavily = connector.id === "tavily-api";
|
||||||
const isLinear = connector.id === "linear-connector";
|
const isLinear = connector.id === "linear-connector";
|
||||||
|
const isElasticsearch = connector.id === "elasticsearch-connector";
|
||||||
|
|
||||||
const isConnected = connectedTypes.has(connector.connectorType);
|
const isConnected = connectedTypes.has(connector.connectorType);
|
||||||
const isConnecting = connectingId === connector.id;
|
const isConnecting = connectingId === connector.id;
|
||||||
|
|
@ -106,7 +107,7 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
||||||
|
|
||||||
const handleConnect = isWebcrawler && onCreateWebcrawler
|
const handleConnect = isWebcrawler && onCreateWebcrawler
|
||||||
? onCreateWebcrawler
|
? onCreateWebcrawler
|
||||||
: (isTavily || isLinear) && onConnectNonOAuth
|
: (isTavily || isLinear || isElasticsearch) && onConnectNonOAuth
|
||||||
? () => onConnectNonOAuth(connector.connectorType)
|
? () => onConnectNonOAuth(connector.connectorType)
|
||||||
: () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`);
|
: () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue