-
Enable Periodic Sync
+
Enable Periodic Sync
Automatically re-index at regular intervals
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/elasticsearch-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/elasticsearch-connect-form.tsx
new file mode 100644
index 000000000..5420d3bb1
--- /dev/null
+++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/elasticsearch-connect-form.tsx
@@ -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
;
+
+export const ElasticsearchConnectForm: FC = ({
+ onSubmit,
+ isSubmitting,
+}) => {
+ const isSubmittingRef = useRef(false);
+ const authBasicId = useId();
+ const authApiKeyId = useId();
+ const [startDate, setStartDate] = useState(undefined);
+ const [endDate, setEndDate] = useState(undefined);
+ const [periodicEnabled, setPeriodicEnabled] = useState(false);
+ const [frequencyMinutes, setFrequencyMinutes] = useState("1440");
+
+ const form = useForm({
+ 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 = {
+ 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 (
+
+
+
+
+
API Key Required
+
+ Enter your Elasticsearch cluster endpoint URL and authentication credentials to connect.
+
+
+
+
+
+
+ {/* What you get section */}
+ {getConnectorBenefits(EnumConnectorName.ELASTICSEARCH_CONNECTOR) && (
+
+
What you get with Elasticsearch integration:
+
+ {getConnectorBenefits(EnumConnectorName.ELASTICSEARCH_CONNECTOR)?.map((benefit) => (
+ - {benefit}
+ ))}
+
+
+ )}
+
+ {/* Documentation Section */}
+
+
+
+ Documentation
+
+
+
+
How it works
+
+ 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.
+
+
+
+
+
+
Connection Setup
+
+
+
Step 1: Get your Elasticsearch endpoint
+
+ You'll need the endpoint URL for your Elasticsearch cluster. This typically looks like:
+
+
+ - Cloud:
https://your-cluster.es.region.aws.com:443
+ - Self-hosted:
https://elasticsearch.example.com:9200
+
+
+
+
+
Step 2: Configure authentication
+
+ Elasticsearch requires authentication. You can use either:
+
+
+
+
+
+
Step 3: Select indices
+
+ Specify which indices to search. You can:
+
+
+ - Use wildcards:
logs-* to match multiple indices
+ - List specific indices:
logs-2024, documents-2024
+ - Leave empty to search all accessible indices (not recommended for performance)
+
+
+
+
+
+
+
+
+
Advanced Configuration
+
+
+
Search Query
+
+ The default query used for searches. Use * to match all documents, or specify a more complex Elasticsearch query.
+
+
+
+
+
Search Fields
+
+ Limit searches to specific fields for better performance. Common fields include:
+
+
+ title - Document titles
+ content - Main content
+ description - Descriptions
+
+
+ Leave empty to search all fields in your documents.
+
+
+
+
+
Maximum Documents
+
+ 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.
+
+
+
+
+
+
+
+
+
Troubleshooting
+
+
+
Connection Issues
+
+ -
+ Invalid URL: Ensure your endpoint URL includes the protocol (https://) and port number if required.
+
+ -
+ SSL/TLS Errors: Verify that your cluster uses HTTPS and the certificate is valid. Self-signed certificates may require additional configuration.
+
+ -
+ Connection Timeout: Check your network connectivity and firewall settings. Ensure the Elasticsearch cluster is accessible from SurfSense servers.
+
+
+
+
+
+
Authentication Issues
+
+ -
+ Invalid Credentials: Double-check your username/password or API key. API keys must be base64-encoded.
+
+ -
+ Permission Denied: Ensure your API key or user account has read permissions for the indices you want to search.
+
+ -
+ API Key Format: Elasticsearch API keys are typically base64-encoded strings. Make sure you're using the full key value.
+
+
+
+
+
+
Search Issues
+
+ -
+ No Results: Verify that your index selection matches existing indices. Use wildcards carefully.
+
+ -
+ Slow Searches: Limit the number of indices or use specific index names instead of wildcards. Reduce the maximum documents limit.
+
+ -
+ Field Not Found: Ensure the search fields you specify actually exist in your Elasticsearch documents.
+
+
+
+
+
+
+ Need More Help?
+
+ 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.
+
+
+
+
+
+
+
+
+
+ );
+};
+
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linear-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linear-connect-form.tsx
index 3e1bb370a..cc80b5dd3 100644
--- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linear-connect-form.tsx
+++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linear-connect-form.tsx
@@ -23,9 +23,20 @@ import {
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 linearConnectorFormSchema = z.object({
name: z.string().min(3, {
@@ -48,6 +59,10 @@ export const LinearConnectForm: FC = ({
isSubmitting,
}) => {
const isSubmittingRef = useRef(false);
+ const [startDate, setStartDate] = useState(undefined);
+ const [endDate, setEndDate] = useState(undefined);
+ const [periodicEnabled, setPeriodicEnabled] = useState(false);
+ const [frequencyMinutes, setFrequencyMinutes] = useState("1440");
const form = useForm({
resolver: zodResolver(linearConnectorFormSchema),
defaultValues: {
@@ -72,9 +87,13 @@ export const LinearConnectForm: FC = ({
},
is_indexable: true,
last_indexed_at: null,
- periodic_indexing_enabled: false,
- indexing_frequency_minutes: 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;
@@ -148,6 +167,56 @@ export const LinearConnectForm: FC = ({
)}
/>
+
+ {/* Indexing Configuration */}
+
+
Indexing Configuration
+
+ {/* Date Range Selector */}
+
+
+ {/* Periodic Sync Config */}
+
+
+
+
Enable Periodic Sync
+
+ Automatically re-index at regular intervals
+
+
+
+
+
+ {periodicEnabled && (
+
+
+
+
+
+
+ )}
+
+
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/tavily-api-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/tavily-api-connect-form.tsx
index 4ce3a751d..ae9ad4144 100644
--- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/tavily-api-connect-form.tsx
+++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/tavily-api-connect-form.tsx
@@ -19,6 +19,7 @@ import {
import { Input } from "@/components/ui/input";
import { EnumConnectorName } from "@/contracts/enums/connector";
import type { ConnectFormProps } from "../index";
+import { getConnectorBenefits } from "../connector-benefits";
const tavilyApiFormSchema = z.object({
name: z.string().min(3, {
@@ -139,6 +140,18 @@ export const TavilyApiConnectForm: FC