feat: Add support for Confluence, BookStack, GitHub, Jira, ClickUp, and Luma connectors, including configuration forms and benefits display, enhance connector dialog for new connectors, and improve overall connector management functionality.

This commit is contained in:
Anish Sarkar 2025-12-31 16:27:30 +05:30
parent aaf2861681
commit 21146485e6
22 changed files with 3185 additions and 11 deletions

View file

@ -0,0 +1,379 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Info } from "lucide-react";
import type { FC } from "react";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { 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 bookstackConnectorFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
base_url: z.string().url({ message: "Please enter a valid BookStack base URL." }),
token_id: z.string().min(1, {
message: "BookStack Token ID is required.",
}),
token_secret: z.string().min(1, {
message: "BookStack Token Secret is required.",
}),
});
type BookStackConnectorFormValues = z.infer<typeof bookstackConnectorFormSchema>;
export const BookStackConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
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<BookStackConnectorFormValues>({
resolver: zodResolver(bookstackConnectorFormSchema),
defaultValues: {
name: "BookStack Connector",
base_url: "",
token_id: "",
token_secret: "",
},
});
const handleSubmit = async (values: BookStackConnectorFormValues) => {
// Prevent multiple submissions
if (isSubmittingRef.current || isSubmitting) {
return;
}
isSubmittingRef.current = true;
try {
await onSubmit({
name: values.name,
connector_type: EnumConnectorName.BOOKSTACK_CONNECTOR,
config: {
BOOKSTACK_BASE_URL: values.base_url,
BOOKSTACK_TOKEN_ID: values.token_id,
BOOKSTACK_TOKEN_SECRET: values.token_secret,
},
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 Token Required</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
You'll need a BookStack API Token to use this connector. You can create one from your BookStack instance settings.
</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="bookstack-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 BookStack 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>
)}
/>
<FormField
control={form.control}
name="base_url"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">BookStack Base URL</FormLabel>
<FormControl>
<Input
type="url"
placeholder="https://your-bookstack-instance.com"
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">
The base URL of your BookStack instance (e.g., https://your-bookstack-instance.com).
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="token_id"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Token ID</FormLabel>
<FormControl>
<Input
placeholder="Your Token ID"
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">
Your BookStack API Token ID.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="token_secret"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Token Secret</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Your Token Secret"
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">
Your BookStack API Token Secret will be encrypted and stored securely.
</FormDescription>
<FormMessage />
</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>
</div>
{/* What you get section */}
{getConnectorBenefits(EnumConnectorName.BOOKSTACK_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 BookStack integration:</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.BOOKSTACK_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 BookStack connector uses the BookStack REST API to fetch all pages from your BookStack instance that your account has access to.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
For follow up indexing runs, the connector retrieves pages that have been updated since the last indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates should appear in your search results within minutes.
</li>
</ul>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mb-4">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">API Token Required</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You need to create an API token from your BookStack instance. The token requires "Access System API" permission.
</AlertDescription>
</Alert>
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Create an API Token</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Log in to your BookStack instance</li>
<li>Click on your profile icon Edit Profile</li>
<li>Navigate to the "API Tokens" tab</li>
<li>Click "Create Token" and give it a name</li>
<li>Copy both the Token ID and Token Secret</li>
<li>Paste them in the form above</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Grant necessary access</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
Your user account must have "Access System API" permission. The connector will only index content your account can view.
</p>
<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">Rate Limiting</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
BookStack API has a rate limit of 180 requests per minute. The connector automatically handles rate limiting to ensure reliable indexing.
</AlertDescription>
</Alert>
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li>
Navigate to the Connector Dashboard and select the <strong>BookStack</strong> Connector.
</li>
<li>
Enter your <strong>BookStack Instance URL</strong> (e.g., https://docs.example.com)
</li>
<li>
Enter your <strong>Token ID</strong> and <strong>Token Secret</strong> from your BookStack API token.
</li>
<li>
Click <strong>Connect</strong> to establish the connection.
</li>
<li>Once connected, your BookStack pages will be indexed automatically.</li>
</ol>
<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">What Gets Indexed</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
<p className="mb-2">The BookStack connector indexes the following data:</p>
<ul className="list-disc pl-5 space-y-1">
<li>All pages from your BookStack instance</li>
<li>Page content in Markdown format</li>
<li>Page titles and metadata</li>
<li>Book and chapter hierarchy information</li>
</ul>
</AlertDescription>
</Alert>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};

View file

@ -0,0 +1,344 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Info } from "lucide-react";
import type { FC } from "react";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { 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 clickupConnectorFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
api_token: z.string().min(10, {
message: "ClickUp API Token is required and must be valid.",
}),
});
type ClickUpConnectorFormValues = z.infer<typeof clickupConnectorFormSchema>;
export const ClickUpConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
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<ClickUpConnectorFormValues>({
resolver: zodResolver(clickupConnectorFormSchema),
defaultValues: {
name: "ClickUp Connector",
api_token: "",
},
});
const handleSubmit = async (values: ClickUpConnectorFormValues) => {
// Prevent multiple submissions
if (isSubmittingRef.current || isSubmitting) {
return;
}
isSubmittingRef.current = true;
try {
await onSubmit({
name: values.name,
connector_type: EnumConnectorName.CLICKUP_CONNECTOR,
config: {
CLICKUP_API_TOKEN: values.api_token,
},
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 Token Required</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
You'll need a ClickUp API Token to use this connector. You can create one from{" "}
<a
href="https://app.clickup.com/settings/apps"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
ClickUp Settings
</a>
</AlertDescription>
</div>
</Alert>
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<Form {...form}>
<form id="clickup-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 ClickUp 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>
)}
/>
<FormField
control={form.control}
name="api_token"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">ClickUp API Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="pk_..."
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">
Your ClickUp API Token will be encrypted and stored securely.
</FormDescription>
<FormMessage />
</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>
</div>
{/* What you get section */}
{getConnectorBenefits(EnumConnectorName.CLICKUP_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 ClickUp integration:</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.CLICKUP_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 ClickUp connector uses the ClickUp API to fetch all tasks and projects that your API token has access to within your workspace.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
For follow up indexing runs, the connector retrieves tasks that have been updated since the last indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates should appear in your search results within minutes.
</li>
</ul>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mb-4">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">API Token Required</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You need a ClickUp personal API token to use this connector. The token will be used to read your ClickUp data.
</AlertDescription>
</Alert>
<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 API Token</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Log in to your ClickUp account</li>
<li>Click your avatar in the upper-right corner and select "Settings"</li>
<li>In the sidebar, click "Apps"</li>
<li>
Under "API Token", click <strong>Generate</strong> or <strong>Regenerate</strong>
</li>
<li>Copy the generated token (it typically starts with "pk_")</li>
<li>
Paste it in the form above. You can also visit{" "}
<a
href="https://app.clickup.com/settings/apps"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
ClickUp API Settings
</a>{" "}
directly.
</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Grant necessary access</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
The API Token will have access to all tasks and projects that your user account can see. Make sure your account has appropriate permissions for the workspaces you want to index.
</p>
<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">Data Privacy</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
Only tasks, comments, and basic metadata will be indexed. ClickUp attachments and linked files are not indexed by this connector.
</AlertDescription>
</Alert>
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li>
Navigate to the Connector Dashboard and select the <strong>ClickUp</strong> Connector.
</li>
<li>
Place your <strong>API Token</strong> in the form field.
</li>
<li>
Click <strong>Connect</strong> to establish the connection.
</li>
<li>Once connected, your ClickUp tasks will be indexed automatically.</li>
</ol>
<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">What Gets Indexed</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
<p className="mb-2">The ClickUp connector indexes the following data:</p>
<ul className="list-disc pl-5 space-y-1">
<li>Task names and descriptions</li>
<li>Task comments and discussion threads</li>
<li>Task status, priority, and assignee information</li>
<li>Project and workspace information</li>
</ul>
</AlertDescription>
</Alert>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};

View file

@ -0,0 +1,405 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Info } from "lucide-react";
import type { FC } from "react";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { 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 confluenceConnectorFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
base_url: z.string().url({ message: "Please enter a valid Confluence base URL." }),
email: z.string().email({ message: "Please enter a valid email address." }),
api_token: z.string().min(10, {
message: "Confluence API Token is required and must be valid.",
}),
});
type ConfluenceConnectorFormValues = z.infer<typeof confluenceConnectorFormSchema>;
export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
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<ConfluenceConnectorFormValues>({
resolver: zodResolver(confluenceConnectorFormSchema),
defaultValues: {
name: "Confluence Connector",
base_url: "",
email: "",
api_token: "",
},
});
const handleSubmit = async (values: ConfluenceConnectorFormValues) => {
// Prevent multiple submissions
if (isSubmittingRef.current || isSubmitting) {
return;
}
isSubmittingRef.current = true;
try {
await onSubmit({
name: values.name,
connector_type: EnumConnectorName.CONFLUENCE_CONNECTOR,
config: {
CONFLUENCE_BASE_URL: values.base_url,
CONFLUENCE_EMAIL: values.email,
CONFLUENCE_API_TOKEN: values.api_token,
},
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 Token Required</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
You'll need a Confluence API Token to use this connector. You can create one from{" "}
<a
href="https://id.atlassian.com/manage-profile/security/api-tokens"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
Atlassian Account Settings
</a>
</AlertDescription>
</div>
</Alert>
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<Form {...form}>
<form id="confluence-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 Confluence 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>
)}
/>
<FormField
control={form.control}
name="base_url"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Confluence Base URL</FormLabel>
<FormControl>
<Input
type="url"
placeholder="https://your-domain.atlassian.net"
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">
The base URL of your Confluence instance (e.g., https://your-domain.atlassian.net).
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Email Address</FormLabel>
<FormControl>
<Input
type="email"
placeholder="your-email@example.com"
autoComplete="email"
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">
The email address associated with your Atlassian account.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="api_token"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">API Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Your API Token"
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">
Your Confluence API Token will be encrypted and stored securely.
</FormDescription>
<FormMessage />
</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>
</div>
{/* What you get section */}
{getConnectorBenefits(EnumConnectorName.CONFLUENCE_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 Confluence integration:</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.CONFLUENCE_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 Confluence connector uses the Confluence REST API to fetch all pages and comments that your account has access to within your Confluence instance.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
For follow up indexing runs, the connector retrieves pages and comments that have been updated since the last indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates should appear in your search results within minutes.
</li>
</ul>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mb-4">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Read-Only Access is Sufficient</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You only need read access for this connector to work. The API Token will only be used to read your Confluence data.
</AlertDescription>
</Alert>
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Create an API Token</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Log in to your Atlassian account</li>
<li>
Navigate to{" "}
<a
href="https://id.atlassian.com/manage-profile/security/api-tokens"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
https://id.atlassian.com/manage-profile/security/api-tokens
</a>{" "}
in your browser.
</li>
<li>
Click <strong>Create API token</strong>
</li>
<li>Enter a label for your token (like "SurfSense Connector")</li>
<li>
Click <strong>Create</strong>
</li>
<li>Copy the generated token as it will only be shown once</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Grant necessary access</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
The API Token will have access to all spaces and pages that your user account can see. Make sure your account has appropriate permissions for the spaces you want to index.
</p>
<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">Data Privacy</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
Only pages, comments, and basic metadata will be indexed. Confluence attachments and linked files are not indexed by this connector.
</AlertDescription>
</Alert>
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li>
Navigate to the Connector Dashboard and select the <strong>Confluence</strong> Connector.
</li>
<li>
Enter your <strong>Confluence Instance URL</strong> (e.g., https://yourcompany.atlassian.net)
</li>
<li>
Enter your <strong>Email Address</strong> associated with your Atlassian account
</li>
<li>
Place your <strong>API Token</strong> in the form field.
</li>
<li>
Click <strong>Connect</strong> to establish the connection.
</li>
<li>Once connected, your Confluence pages will be indexed automatically.</li>
</ol>
<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">What Gets Indexed</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
<p className="mb-2">The Confluence connector indexes the following data:</p>
<ul className="list-disc pl-5 space-y-1">
<li>All pages from accessible spaces</li>
<li>Page content and metadata</li>
<li>Comments on pages (both footer and inline comments)</li>
<li>Page titles and descriptions</li>
</ul>
</AlertDescription>
</Alert>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};

View file

@ -223,8 +223,8 @@ export const DiscordConnectForm: FC<ConnectFormProps> = ({
<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 Discord integration:</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.DISCORD_CONNECTOR)?.map((benefit, i) => (
<li key={i}>{benefit}</li>
{getConnectorBenefits(EnumConnectorName.DISCORD_CONNECTOR)?.map((benefit) => (
<li key={benefit}>{benefit}</li>
))}
</ul>
</div>

View file

@ -0,0 +1,419 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Info } from "lucide-react";
import type { FC } from "react";
import { useRef, useState } 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";
const githubConnectorFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
github_pat: z
.string()
.min(20, {
message: "GitHub Personal Access Token seems too short.",
})
.refine((pat) => pat.startsWith("ghp_") || pat.startsWith("github_pat_"), {
message: "GitHub PAT should start with 'ghp_' or 'github_pat_'",
}),
repo_full_names: z.string().min(1, {
message: "At least one repository is required.",
}),
});
type GithubConnectorFormValues = z.infer<typeof githubConnectorFormSchema>;
export const GithubConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
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<GithubConnectorFormValues>({
resolver: zodResolver(githubConnectorFormSchema),
defaultValues: {
name: "GitHub Connector",
github_pat: "",
repo_full_names: "",
},
});
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: GithubConnectorFormValues) => {
// Prevent multiple submissions
if (isSubmittingRef.current || isSubmitting) {
return;
}
isSubmittingRef.current = true;
try {
const repoList = stringToArray(values.repo_full_names);
await onSubmit({
name: values.name,
connector_type: EnumConnectorName.GITHUB_CONNECTOR,
config: {
GITHUB_PAT: values.github_pat,
repo_full_names: repoList,
},
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">Personal Access Token Required</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
You'll need a GitHub Personal Access Token to use this connector. You can create one from{" "}
<a
href="https://github.com/settings/tokens"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
GitHub Settings
</a>
</AlertDescription>
</div>
</Alert>
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<Form {...form}>
<form id="github-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 GitHub 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>
)}
/>
<FormField
control={form.control}
name="github_pat"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">GitHub Personal Access Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="ghp_..."
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">
Your GitHub PAT will be encrypted and stored securely. It typically starts with "ghp_" or "github_pat_".
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repo_full_names"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Repository Names</FormLabel>
<FormControl>
<Input
placeholder="owner/repo1, owner/repo2"
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 repository full names (e.g., "owner/repo1, owner/repo2").
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* Show parsed repositories as badges */}
{form.watch("repo_full_names")?.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 Repositories:</h4>
<div className="flex flex-wrap gap-2">
{stringToArray(form.watch("repo_full_names") ?? "").map((repo) => (
<Badge key={repo} variant="secondary" className="text-[10px]">
{repo}
</Badge>
))}
</div>
</div>
)}
{/* 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.GITHUB_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 GitHub integration:</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.GITHUB_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 GitHub connector uses a Personal Access Token (PAT) to authenticate with the GitHub API. You provide a comma-separated list of repository full names (e.g., "owner/repo1, owner/repo2") that you want to index. The connector indexes relevant files (code, markdown, text) from the selected repositories.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
The connector indexes files based on common code and documentation extensions.
</li>
<li>Large files (over 1MB) are skipped during indexing.</li>
<li>Only specified repositories are indexed.</li>
<li>
Indexing runs periodically (check connector settings for frequency) to keep content up-to-date.
</li>
</ul>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mb-4">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Personal Access Token Required</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You'll need a GitHub PAT with the appropriate scopes (e.g., 'repo') to fetch repositories. The PAT will be stored securely to enable indexing.
</AlertDescription>
</Alert>
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Generate GitHub PAT</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>
Go to your GitHub{" "}
<a
href="https://github.com/settings/tokens"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
Developer settings
</a>
</li>
<li>
Click on <strong>Personal access tokens</strong>, then choose{" "}
<strong>Tokens (classic)</strong> or <strong>Fine-grained tokens</strong> (recommended if available).
</li>
<li>
Click <strong>Generate new token</strong> (and choose the appropriate type).
</li>
<li>
Give your token a descriptive name (e.g., "SurfSense Connector").
</li>
<li>
Set an expiration date for the token (recommended for security).
</li>
<li>
Under <strong>Select scopes</strong> (for classic tokens) or <strong>Repository access</strong> (for fine-grained), grant the necessary permissions. At minimum, the <strong>`repo`</strong> scope (or equivalent read access to repositories for fine-grained tokens) is required to read repository content.
</li>
<li>
Click <strong>Generate token</strong>.
</li>
<li>
<strong>Important:</strong> Copy your new PAT immediately. You won't be able to see it again after leaving the page.
</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Specify repositories</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
Enter a comma-separated list of repository full names in the format "owner/repo1, owner/repo2". The connector will index files from only the specified repositories.
</p>
<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">Repository Access</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
Make sure your PAT has access to all repositories you want to index. Private repositories require appropriate permissions.
</AlertDescription>
</Alert>
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li>
Navigate to the Connector Dashboard and select the <strong>GitHub</strong> Connector.
</li>
<li>
Enter your <strong>GitHub Personal Access Token</strong> in the form field.
</li>
<li>
Enter a comma-separated list of <strong>Repository Names</strong> (e.g., "owner/repo1, owner/repo2").
</li>
<li>
Click <strong>Connect</strong> to establish the connection.
</li>
<li>Once connected, your GitHub repositories will be indexed automatically.</li>
</ol>
<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">What Gets Indexed</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
<p className="mb-2">The GitHub connector indexes the following data:</p>
<ul className="list-disc pl-5 space-y-1">
<li>Code files from selected repositories</li>
<li>README files and Markdown documentation</li>
<li>Common text-based file formats</li>
<li>Repository metadata and structure</li>
</ul>
</AlertDescription>
</Alert>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};

View file

@ -0,0 +1,407 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Info } from "lucide-react";
import type { FC } from "react";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { 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 jiraConnectorFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
base_url: z.string().url({ message: "Please enter a valid Jira base URL." }),
email: z.string().email({ message: "Please enter a valid email address." }),
api_token: z.string().min(10, {
message: "Jira API Token is required and must be valid.",
}),
});
type JiraConnectorFormValues = z.infer<typeof jiraConnectorFormSchema>;
export const JiraConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
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<JiraConnectorFormValues>({
resolver: zodResolver(jiraConnectorFormSchema),
defaultValues: {
name: "Jira Connector",
base_url: "",
email: "",
api_token: "",
},
});
const handleSubmit = async (values: JiraConnectorFormValues) => {
// Prevent multiple submissions
if (isSubmittingRef.current || isSubmitting) {
return;
}
isSubmittingRef.current = true;
try {
await onSubmit({
name: values.name,
connector_type: EnumConnectorName.JIRA_CONNECTOR,
config: {
JIRA_BASE_URL: values.base_url,
JIRA_EMAIL: values.email,
JIRA_API_TOKEN: values.api_token,
},
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 Token Required</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
You'll need a Jira API Token to use this connector. You can create one from{" "}
<a
href="https://id.atlassian.com/manage-profile/security/api-tokens"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
Atlassian Account Settings
</a>
</AlertDescription>
</div>
</Alert>
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<Form {...form}>
<form id="jira-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 Jira 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>
)}
/>
<FormField
control={form.control}
name="base_url"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Jira Base URL</FormLabel>
<FormControl>
<Input
type="url"
placeholder="https://your-domain.atlassian.net"
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">
The base URL of your Jira instance (e.g., https://your-domain.atlassian.net).
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Email Address</FormLabel>
<FormControl>
<Input
type="email"
placeholder="your-email@example.com"
autoComplete="email"
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">
The email address associated with your Atlassian account.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="api_token"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">API Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Your API Token"
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">
Your Jira API Token will be encrypted and stored securely.
</FormDescription>
<FormMessage />
</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>
</div>
{/* What you get section */}
{getConnectorBenefits(EnumConnectorName.JIRA_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 Jira integration:</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.JIRA_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 Jira connector uses the Jira REST API with Basic Authentication to fetch all issues and comments that your account has access to within your Jira instance.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
For follow up indexing runs, the connector retrieves issues and comments that have been updated since the last indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates should appear in your search results within minutes.
</li>
</ul>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mb-4">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Read-Only Access is Sufficient</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You only need read access for this connector to work. The API Token will only be used to read your Jira data.
</AlertDescription>
</Alert>
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Create an API Token</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Log in to your Atlassian account</li>
<li>
Navigate to{" "}
<a
href="https://id.atlassian.com/manage-profile/security/api-tokens"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
https://id.atlassian.com/manage-profile/security/api-tokens
</a>{" "}
in your browser.
</li>
<li>
Click <strong>Create API token</strong>
</li>
<li>Enter a label for your token (like "SurfSense Connector")</li>
<li>
Click <strong>Create</strong>
</li>
<li>Copy the generated token as it will only be shown once</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Grant necessary access</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
The API Token will have access to all projects and issues that your user account can see. Make sure your account has appropriate permissions for the projects you want to index.
</p>
<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">Data Privacy</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
Only issues, comments, and basic metadata will be indexed. Jira attachments and linked files are not indexed by this connector.
</AlertDescription>
</Alert>
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li>
Navigate to the Connector Dashboard and select the <strong>Jira</strong> Connector.
</li>
<li>
Enter your <strong>Jira Instance URL</strong> (e.g., https://yourcompany.atlassian.net)
</li>
<li>
Enter your <strong>Email Address</strong> associated with your Atlassian account
</li>
<li>
Place your <strong>API Token</strong> in the form field.
</li>
<li>
Click <strong>Connect</strong> to establish the connection.
</li>
<li>Once connected, your Jira issues will be indexed automatically.</li>
</ol>
<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">What Gets Indexed</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
<p className="mb-2">The Jira connector indexes the following data:</p>
<ul className="list-disc pl-5 space-y-1">
<li>Issue keys and summaries (e.g., PROJ-123)</li>
<li>Issue descriptions</li>
<li>Issue comments and discussion threads</li>
<li>Issue status, priority, and type information</li>
<li>Assignee and reporter information</li>
<li>Project information</li>
</ul>
</AlertDescription>
</Alert>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};

View file

@ -226,8 +226,8 @@ export const LinearConnectForm: FC<ConnectFormProps> = ({
<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 Linear integration:</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.LINEAR_CONNECTOR)?.map((benefit, i) => (
<li key={i}>{benefit}</li>
{getConnectorBenefits(EnumConnectorName.LINEAR_CONNECTOR)?.map((benefit) => (
<li key={benefit}>{benefit}</li>
))}
</ul>
</div>

View file

@ -0,0 +1,342 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Info } from "lucide-react";
import type { FC } from "react";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { 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 lumaConnectorFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
api_key: z.string().min(10, {
message: "Luma API Key is required and must be valid.",
}),
});
type LumaConnectorFormValues = z.infer<typeof lumaConnectorFormSchema>;
export const LumaConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
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<LumaConnectorFormValues>({
resolver: zodResolver(lumaConnectorFormSchema),
defaultValues: {
name: "Luma Connector",
api_key: "",
},
});
const handleSubmit = async (values: LumaConnectorFormValues) => {
// Prevent multiple submissions
if (isSubmittingRef.current || isSubmitting) {
return;
}
isSubmittingRef.current = true;
try {
await onSubmit({
name: values.name,
connector_type: EnumConnectorName.LUMA_CONNECTOR,
config: {
LUMA_API_KEY: values.api_key,
},
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">
You'll need a Luma API Key to use this connector. You can create one from{" "}
<a
href="https://lu.ma/api"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
Luma API Settings
</a>
</AlertDescription>
</div>
</Alert>
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<Form {...form}>
<form id="luma-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 Luma 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>
)}
/>
<FormField
control={form.control}
name="api_key"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Luma API Key</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Your API Key"
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">
Your Luma API Key will be encrypted and stored securely.
</FormDescription>
<FormMessage />
</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>
</div>
{/* What you get section */}
{getConnectorBenefits(EnumConnectorName.LUMA_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 Luma integration:</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.LUMA_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 Luma connector uses the Luma API to fetch all events that your API key has access to.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
For follow up indexing runs, the connector retrieves events that have been updated since the last indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates should appear in your search results within minutes.
</li>
</ul>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mb-4">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">API Key Required</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You need a Luma API key to use this connector. The key will be used to read your Luma events with read-only permissions.
</AlertDescription>
</Alert>
<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 API Key</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Log into your Luma account</li>
<li>Navigate to your account settings</li>
<li>Go to API settings or Developer settings</li>
<li>Generate a new API key</li>
<li>Copy the generated API key</li>
<li>
You can also visit{" "}
<a
href="https://lu.ma/api"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
Luma API Settings
</a>{" "}
for more information.
</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Grant necessary access</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
The API key will have access to all events that your user account can see. Make sure your account has appropriate permissions for the events you want to index.
</p>
<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">Data Privacy</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
Only event details, descriptions, and attendee information will be indexed. Event attachments and linked files are not indexed by this connector.
</AlertDescription>
</Alert>
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li>
Navigate to the Connector Dashboard and select the <strong>Luma</strong> Connector.
</li>
<li>
Place your <strong>API Key</strong> in the form field.
</li>
<li>
Click <strong>Connect</strong> to establish the connection.
</li>
<li>Once connected, your Luma events will be indexed automatically.</li>
</ol>
<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">What Gets Indexed</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
<p className="mb-2">The Luma connector indexes the following data:</p>
<ul className="list-disc pl-5 space-y-1">
<li>Event titles and descriptions</li>
<li>Event details and metadata</li>
<li>Attendee information</li>
<li>Event dates and locations</li>
</ul>
</AlertDescription>
</Alert>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};

View file

@ -223,8 +223,8 @@ export const NotionConnectForm: FC<ConnectFormProps> = ({
<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 Notion integration:</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.NOTION_CONNECTOR)?.map((benefit, i) => (
<li key={i}>{benefit}</li>
{getConnectorBenefits(EnumConnectorName.NOTION_CONNECTOR)?.map((benefit) => (
<li key={benefit}>{benefit}</li>
))}
</ul>
</div>

View file

@ -223,8 +223,8 @@ export const SlackConnectForm: FC<ConnectFormProps> = ({
<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 Slack integration:</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.SLACK_CONNECTOR)?.map((benefit, i) => (
<li key={i}>{benefit}</li>
{getConnectorBenefits(EnumConnectorName.SLACK_CONNECTOR)?.map((benefit) => (
<li key={benefit}>{benefit}</li>
))}
</ul>
</div>

View file

@ -59,8 +59,48 @@ export function getConnectorBenefits(connectorType: string): string[] | null {
"Keep your search results up-to-date with latest Notion content",
"Index your Notion workspace for enhanced search capabilities",
],
// Add other connectors as needed
// GITHUB_CONNECTOR: [...],
CONFLUENCE_CONNECTOR: [
"Search through all your Confluence pages and spaces",
"Access page content, comments, and attachments",
"Connect your team's documentation directly to your search space",
"Keep your search results up-to-date with latest Confluence content",
"Index your Confluence workspace for enhanced search capabilities",
],
BOOKSTACK_CONNECTOR: [
"Search through all your BookStack pages and books",
"Access page content, chapters, and documentation",
"Connect your documentation directly to your search space",
"Keep your search results up-to-date with latest BookStack content",
"Index your BookStack instance for enhanced search capabilities",
],
GITHUB_CONNECTOR: [
"Search through code, issues, and documentation from GitHub repositories",
"Access repository content, pull requests, and discussions",
"Connect your codebase directly to your search space",
"Keep your search results up-to-date with latest GitHub content",
"Index your GitHub repositories for enhanced search capabilities",
],
JIRA_CONNECTOR: [
"Search through all your Jira issues and tickets",
"Access issue descriptions, comments, and project data",
"Connect your project management directly to your search space",
"Keep your search results up-to-date with latest Jira content",
"Index your Jira projects for enhanced search capabilities",
],
CLICKUP_CONNECTOR: [
"Search through all your ClickUp tasks and projects",
"Access task descriptions, comments, and project data",
"Connect your task management directly to your search space",
"Keep your search results up-to-date with latest ClickUp content",
"Index your ClickUp workspace for enhanced search capabilities",
],
LUMA_CONNECTOR: [
"Search through all your Luma events",
"Access event details, descriptions, and attendee information",
"Connect your events directly to your search space",
"Keep your search results up-to-date with latest Luma content",
"Index your Luma events for enhanced search capabilities",
],
};
return benefits[connectorType] || null;

View file

@ -1,9 +1,15 @@
import type { FC } from "react";
import { BaiduSearchApiConnectForm } from "./components/baidu-search-api-connect-form";
import { BookStackConnectForm } from "./components/bookstack-connect-form";
import { ClickUpConnectForm } from "./components/clickup-connect-form";
import { ConfluenceConnectForm } from "./components/confluence-connect-form";
import { DiscordConnectForm } from "./components/discord-connect-form";
import { ElasticsearchConnectForm } from "./components/elasticsearch-connect-form";
import { GithubConnectForm } from "./components/github-connect-form";
import { JiraConnectForm } from "./components/jira-connect-form";
import { LinearConnectForm } from "./components/linear-connect-form";
import { LinkupApiConnectForm } from "./components/linkup-api-connect-form";
import { LumaConnectForm } from "./components/luma-connect-form";
import { NotionConnectForm } from "./components/notion-connect-form";
import { SearxngConnectForm } from "./components/searxng-connect-form";
import { SlackConnectForm } from "./components/slack-connect-form";
@ -56,6 +62,18 @@ export function getConnectFormComponent(
return DiscordConnectForm;
case "NOTION_CONNECTOR":
return NotionConnectForm;
case "CONFLUENCE_CONNECTOR":
return ConfluenceConnectForm;
case "BOOKSTACK_CONNECTOR":
return BookStackConnectForm;
case "GITHUB_CONNECTOR":
return GithubConnectForm;
case "JIRA_CONNECTOR":
return JiraConnectForm;
case "CLICKUP_CONNECTOR":
return ClickUpConnectForm;
case "LUMA_CONNECTOR":
return LumaConnectForm;
// Add other connector types here as needed
default:
return null;

View file

@ -0,0 +1,151 @@
"use client";
import { KeyRound } from "lucide-react";
import { useState, useEffect } from "react";
import type { FC } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import type { ConnectorConfigProps } from "../index";
export interface BookStackConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
export const BookStackConfig: FC<BookStackConfigProps> = ({
connector,
onConfigChange,
onNameChange,
}) => {
const [baseUrl, setBaseUrl] = useState<string>(
(connector.config?.BOOKSTACK_BASE_URL as string) || ""
);
const [tokenId, setTokenId] = useState<string>(
(connector.config?.BOOKSTACK_TOKEN_ID as string) || ""
);
const [tokenSecret, setTokenSecret] = useState<string>(
(connector.config?.BOOKSTACK_TOKEN_SECRET as string) || ""
);
const [name, setName] = useState<string>(connector.name || "");
// Update values when connector changes
useEffect(() => {
const url = (connector.config?.BOOKSTACK_BASE_URL as string) || "";
const id = (connector.config?.BOOKSTACK_TOKEN_ID as string) || "";
const secret = (connector.config?.BOOKSTACK_TOKEN_SECRET as string) || "";
setBaseUrl(url);
setTokenId(id);
setTokenSecret(secret);
setName(connector.name || "");
}, [connector.config, connector.name]);
const handleBaseUrlChange = (value: string) => {
setBaseUrl(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
BOOKSTACK_BASE_URL: value,
});
}
};
const handleTokenIdChange = (value: string) => {
setTokenId(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
BOOKSTACK_TOKEN_ID: value,
});
}
};
const handleTokenSecretChange = (value: string) => {
setTokenSecret(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
BOOKSTACK_TOKEN_SECRET: value,
});
}
};
const handleNameChange = (value: string) => {
setName(value);
if (onNameChange) {
onNameChange(value);
}
};
return (
<div className="space-y-6">
{/* Connector Name */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Connector Name</Label>
<Input
value={name}
onChange={(e) => handleNameChange(e.target.value)}
placeholder="My BookStack Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
A friendly name to identify this connector.
</p>
</div>
</div>
{/* Configuration */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-1 sm:space-y-2">
<h3 className="font-medium text-sm sm:text-base">Configuration</h3>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-xs sm:text-sm">BookStack Base URL</Label>
<Input
type="url"
value={baseUrl}
onChange={(e) => handleBaseUrlChange(e.target.value)}
placeholder="https://your-bookstack-instance.com"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The base URL of your BookStack instance.
</p>
</div>
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Token ID</Label>
<Input
value={tokenId}
onChange={(e) => handleTokenIdChange(e.target.value)}
placeholder="Your Token ID"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Your BookStack API Token ID.
</p>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2 text-xs sm:text-sm">
<KeyRound className="h-4 w-4" />
Token Secret
</Label>
<Input
type="password"
value={tokenSecret}
onChange={(e) => handleTokenSecretChange(e.target.value)}
placeholder="Your Token Secret"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Update your BookStack Token Secret if needed.
</p>
</div>
</div>
</div>
</div>
);
};

View file

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

View file

@ -0,0 +1,152 @@
"use client";
import { KeyRound } from "lucide-react";
import { useState, useEffect } from "react";
import type { FC } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import type { ConnectorConfigProps } from "../index";
export interface ConfluenceConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
export const ConfluenceConfig: FC<ConfluenceConfigProps> = ({
connector,
onConfigChange,
onNameChange,
}) => {
const [baseUrl, setBaseUrl] = useState<string>(
(connector.config?.CONFLUENCE_BASE_URL as string) || ""
);
const [email, setEmail] = useState<string>(
(connector.config?.CONFLUENCE_EMAIL as string) || ""
);
const [apiToken, setApiToken] = useState<string>(
(connector.config?.CONFLUENCE_API_TOKEN as string) || ""
);
const [name, setName] = useState<string>(connector.name || "");
// Update values when connector changes
useEffect(() => {
const url = (connector.config?.CONFLUENCE_BASE_URL as string) || "";
const emailVal = (connector.config?.CONFLUENCE_EMAIL as string) || "";
const token = (connector.config?.CONFLUENCE_API_TOKEN as string) || "";
setBaseUrl(url);
setEmail(emailVal);
setApiToken(token);
setName(connector.name || "");
}, [connector.config, connector.name]);
const handleBaseUrlChange = (value: string) => {
setBaseUrl(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
CONFLUENCE_BASE_URL: value,
});
}
};
const handleEmailChange = (value: string) => {
setEmail(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
CONFLUENCE_EMAIL: value,
});
}
};
const handleApiTokenChange = (value: string) => {
setApiToken(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
CONFLUENCE_API_TOKEN: value,
});
}
};
const handleNameChange = (value: string) => {
setName(value);
if (onNameChange) {
onNameChange(value);
}
};
return (
<div className="space-y-6">
{/* Connector Name */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Connector Name</Label>
<Input
value={name}
onChange={(e) => handleNameChange(e.target.value)}
placeholder="My Confluence Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
A friendly name to identify this connector.
</p>
</div>
</div>
{/* Configuration */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-1 sm:space-y-2">
<h3 className="font-medium text-sm sm:text-base">Configuration</h3>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Confluence Base URL</Label>
<Input
type="url"
value={baseUrl}
onChange={(e) => handleBaseUrlChange(e.target.value)}
placeholder="https://your-domain.atlassian.net"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The base URL of your Confluence instance.
</p>
</div>
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Email Address</Label>
<Input
type="email"
value={email}
onChange={(e) => handleEmailChange(e.target.value)}
placeholder="your-email@example.com"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The email address associated with your Atlassian account.
</p>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2 text-xs sm:text-sm">
<KeyRound className="h-4 w-4" />
API Token
</Label>
<Input
type="password"
value={apiToken}
onChange={(e) => handleApiTokenChange(e.target.value)}
placeholder="Your API Token"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Update your Confluence API Token if needed.
</p>
</div>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,150 @@
"use client";
import { KeyRound } from "lucide-react";
import { useState, useEffect } from "react";
import type { FC } 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 GithubConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
export const GithubConfig: FC<GithubConfigProps> = ({
connector,
onConfigChange,
onNameChange,
}) => {
const stringToArray = (arr: string[] | string | undefined): string[] => {
if (Array.isArray(arr)) return arr;
if (typeof arr === "string") {
return arr.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
}
return [];
};
const arrayToString = (arr: string[]): string => {
return arr.join(", ");
};
const [githubPat, setGithubPat] = useState<string>(
(connector.config?.GITHUB_PAT as string) || ""
);
const [repoFullNames, setRepoFullNames] = useState<string>(
arrayToString(stringToArray(connector.config?.repo_full_names))
);
const [name, setName] = useState<string>(connector.name || "");
// Update values when connector changes
useEffect(() => {
const pat = (connector.config?.GITHUB_PAT as string) || "";
const repos = arrayToString(stringToArray(connector.config?.repo_full_names));
setGithubPat(pat);
setRepoFullNames(repos);
setName(connector.name || "");
}, [connector.config, connector.name]);
const handleGithubPatChange = (value: string) => {
setGithubPat(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
GITHUB_PAT: value,
});
}
};
const handleRepoFullNamesChange = (value: string) => {
setRepoFullNames(value);
const repoList = stringToArray(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
repo_full_names: repoList,
});
}
};
const handleNameChange = (value: string) => {
setName(value);
if (onNameChange) {
onNameChange(value);
}
};
return (
<div className="space-y-6">
{/* Connector Name */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Connector Name</Label>
<Input
value={name}
onChange={(e) => handleNameChange(e.target.value)}
placeholder="My GitHub Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
A friendly name to identify this connector.
</p>
</div>
</div>
{/* Configuration */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-1 sm:space-y-2">
<h3 className="font-medium text-sm sm:text-base">Configuration</h3>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label className="flex items-center gap-2 text-xs sm:text-sm">
<KeyRound className="h-4 w-4" />
GitHub Personal Access Token
</Label>
<Input
type="password"
value={githubPat}
onChange={(e) => handleGithubPatChange(e.target.value)}
placeholder="ghp_..."
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Update your GitHub PAT if needed.
</p>
</div>
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Repository Names</Label>
<Input
value={repoFullNames}
onChange={(e) => handleRepoFullNamesChange(e.target.value)}
placeholder="owner/repo1, owner/repo2"
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 repository full names.
</p>
</div>
{/* Show parsed repositories as badges */}
{repoFullNames.trim() && (
<div className="rounded-lg border border-border bg-muted/50 p-3">
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Repositories:</h4>
<div className="flex flex-wrap gap-2">
{stringToArray(repoFullNames).map((repo) => (
<Badge key={repo} variant="secondary" className="text-[10px]">
{repo}
</Badge>
))}
</div>
</div>
)}
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,152 @@
"use client";
import { KeyRound } from "lucide-react";
import { useState, useEffect } from "react";
import type { FC } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import type { ConnectorConfigProps } from "../index";
export interface JiraConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
export const JiraConfig: FC<JiraConfigProps> = ({
connector,
onConfigChange,
onNameChange,
}) => {
const [baseUrl, setBaseUrl] = useState<string>(
(connector.config?.JIRA_BASE_URL as string) || ""
);
const [email, setEmail] = useState<string>(
(connector.config?.JIRA_EMAIL as string) || ""
);
const [apiToken, setApiToken] = useState<string>(
(connector.config?.JIRA_API_TOKEN as string) || ""
);
const [name, setName] = useState<string>(connector.name || "");
// Update values when connector changes
useEffect(() => {
const url = (connector.config?.JIRA_BASE_URL as string) || "";
const emailVal = (connector.config?.JIRA_EMAIL as string) || "";
const token = (connector.config?.JIRA_API_TOKEN as string) || "";
setBaseUrl(url);
setEmail(emailVal);
setApiToken(token);
setName(connector.name || "");
}, [connector.config, connector.name]);
const handleBaseUrlChange = (value: string) => {
setBaseUrl(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
JIRA_BASE_URL: value,
});
}
};
const handleEmailChange = (value: string) => {
setEmail(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
JIRA_EMAIL: value,
});
}
};
const handleApiTokenChange = (value: string) => {
setApiToken(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
JIRA_API_TOKEN: value,
});
}
};
const handleNameChange = (value: string) => {
setName(value);
if (onNameChange) {
onNameChange(value);
}
};
return (
<div className="space-y-6">
{/* Connector Name */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Connector Name</Label>
<Input
value={name}
onChange={(e) => handleNameChange(e.target.value)}
placeholder="My Jira Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
A friendly name to identify this connector.
</p>
</div>
</div>
{/* Configuration */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-1 sm:space-y-2">
<h3 className="font-medium text-sm sm:text-base">Configuration</h3>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Jira Base URL</Label>
<Input
type="url"
value={baseUrl}
onChange={(e) => handleBaseUrlChange(e.target.value)}
placeholder="https://your-domain.atlassian.net"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The base URL of your Jira instance.
</p>
</div>
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Email Address</Label>
<Input
type="email"
value={email}
onChange={(e) => handleEmailChange(e.target.value)}
placeholder="your-email@example.com"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The email address associated with your Atlassian account.
</p>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2 text-xs sm:text-sm">
<KeyRound className="h-4 w-4" />
API Token
</Label>
<Input
type="password"
value={apiToken}
onChange={(e) => handleApiTokenChange(e.target.value)}
placeholder="Your API Token"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Update your Jira API Token if needed.
</p>
</div>
</div>
</div>
</div>
);
};

View file

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

View file

@ -3,11 +3,17 @@
import type { FC } from "react";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { BaiduSearchApiConfig } from "./components/baidu-search-api-config";
import { BookStackConfig } from "./components/bookstack-config";
import { ClickUpConfig } from "./components/clickup-config";
import { ConfluenceConfig } from "./components/confluence-config";
import { DiscordConfig } from "./components/discord-config";
import { ElasticsearchConfig } from "./components/elasticsearch-config";
import { GithubConfig } from "./components/github-config";
import { GoogleDriveConfig } from "./components/google-drive-config";
import { JiraConfig } from "./components/jira-config";
import { LinearConfig } from "./components/linear-config";
import { LinkupApiConfig } from "./components/linkup-api-config";
import { LumaConfig } from "./components/luma-config";
import { NotionConfig } from "./components/notion-config";
import { SearxngConfig } from "./components/searxng-config";
import { SlackConfig } from "./components/slack-config";
@ -51,6 +57,18 @@ export function getConnectorConfigComponent(
return DiscordConfig;
case "NOTION_CONNECTOR":
return NotionConfig;
case "CONFLUENCE_CONNECTOR":
return ConfluenceConfig;
case "BOOKSTACK_CONNECTOR":
return BookStackConfig;
case "GITHUB_CONNECTOR":
return GithubConfig;
case "JIRA_CONNECTOR":
return JiraConfig;
case "CLICKUP_CONNECTOR":
return ClickUpConfig;
case "LUMA_CONNECTOR":
return LumaConfig;
// OAuth connectors (Gmail, Calendar, Airtable) and others don't need special config UI
default:
return null;

View file

@ -52,6 +52,12 @@ export const ConnectorConnectView: FC<ConnectorConnectViewProps> = ({
SLACK_CONNECTOR: "slack-connect-form",
DISCORD_CONNECTOR: "discord-connect-form",
NOTION_CONNECTOR: "notion-connect-form",
CONFLUENCE_CONNECTOR: "confluence-connect-form",
BOOKSTACK_CONNECTOR: "bookstack-connect-form",
GITHUB_CONNECTOR: "github-connect-form",
JIRA_CONNECTOR: "jira-connect-form",
CLICKUP_CONNECTOR: "clickup-connect-form",
LUMA_CONNECTOR: "luma-connect-form",
};
const formId = formIdMap[connectorType];
if (formId) {

View file

@ -102,6 +102,12 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
const isSlack = connector.id === "slack-connector";
const isDiscord = connector.id === "discord-connector";
const isNotion = connector.id === "notion-connector";
const isConfluence = connector.id === "confluence-connector";
const isBookStack = connector.id === "bookstack-connector";
const isGithub = connector.id === "github-connector";
const isJira = connector.id === "jira-connector";
const isClickUp = connector.id === "clickup-connector";
const isLuma = connector.id === "luma-connector";
const isConnected = connectedTypes.has(connector.connectorType);
const isConnecting = connectingId === connector.id;
@ -113,7 +119,7 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
const handleConnect = isWebcrawler && onCreateWebcrawler
? onCreateWebcrawler
: (isTavily || isSearxng || isLinkup || isBaidu || isLinear || isElasticsearch || isSlack || isDiscord || isNotion) && onConnectNonOAuth
: (isTavily || isSearxng || isLinkup || isBaidu || isLinear || isElasticsearch || isSlack || isDiscord || isNotion || isConfluence || isBookStack || isGithub || isJira || isClickUp || isLuma) && onConnectNonOAuth
? () => onConnectNonOAuth(connector.connectorType)
: () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`);