diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linear-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linear-connect-form.tsx new file mode 100644 index 000000000..a87591ee1 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linear-connect-form.tsx @@ -0,0 +1,150 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Info } from "lucide-react"; +import type { FC } from "react"; +import { useRef } from "react"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { EnumConnectorName } from "@/contracts/enums/connector"; +import type { ConnectFormProps } from "../index"; + +const linearConnectorFormSchema = z.object({ + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + api_key: z + .string() + .min(10, { + message: "Linear API Key is required and must be valid.", + }) + .regex(/^lin_api_/, { + message: "Linear API Key should start with 'lin_api_'", + }), +}); + +type LinearConnectorFormValues = z.infer; + +export const LinearConnectForm: FC = ({ + onSubmit, + isSubmitting, +}) => { + const isSubmittingRef = useRef(false); + const form = useForm({ + resolver: zodResolver(linearConnectorFormSchema), + defaultValues: { + name: "Linear Connector", + api_key: "", + }, + }); + + const handleSubmit = async (values: LinearConnectorFormValues) => { + // Prevent multiple submissions + if (isSubmittingRef.current || isSubmitting) { + return; + } + + isSubmittingRef.current = true; + try { + await onSubmit({ + name: values.name, + connector_type: EnumConnectorName.LINEAR_CONNECTOR, + config: { + LINEAR_API_KEY: values.api_key, + }, + is_indexable: true, + last_indexed_at: null, + periodic_indexing_enabled: false, + indexing_frequency_minutes: null, + next_scheduled_at: null, + }); + } finally { + isSubmittingRef.current = false; + } + }; + + return ( +
+ + +
+ API Key Required + + You'll need a Linear API Key to use this connector. You can create one from{" "} + + Linear API Settings + + +
+
+ +
+
+ + ( + + Connector Name + + + + + A friendly name to identify this connector. + + + + )} + /> + + ( + + Linear API Key + + + + + Your Linear API Key will be encrypted and stored securely. It typically starts with "lin_api_". + + + + )} + /> + + +
+
+ ); +}; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/connect-forms/tavily-api-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/tavily-api-connect-form.tsx similarity index 98% rename from surfsense_web/components/assistant-ui/connector-popup/connector-configs/connect-forms/tavily-api-connect-form.tsx rename to surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/tavily-api-connect-form.tsx index ddccbf8c8..4ce3a751d 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/connect-forms/tavily-api-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/tavily-api-connect-form.tsx @@ -18,7 +18,7 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { EnumConnectorName } from "@/contracts/enums/connector"; -import type { ConnectFormProps } from "./index"; +import type { ConnectFormProps } from "../index"; const tavilyApiFormSchema = z.object({ name: z.string().min(3, { diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/connect-forms/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx similarity index 80% rename from surfsense_web/components/assistant-ui/connector-popup/connector-configs/connect-forms/index.tsx rename to surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx index fd0c4eced..4e48d7159 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/connect-forms/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx @@ -1,5 +1,6 @@ import type { FC } from "react"; -import { TavilyApiConnectForm } from "./tavily-api-connect-form"; +import { LinearConnectForm } from "./components/linear-connect-form"; +import { TavilyApiConnectForm } from "./components/tavily-api-connect-form"; export interface ConnectFormProps { onSubmit: (data: { @@ -28,6 +29,8 @@ export function getConnectFormComponent( switch (connectorType) { case "TAVILY_API": return TavilyApiConnectForm; + case "LINEAR_CONNECTOR": + return LinearConnectForm; // Add other connector types here as needed default: return null; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/linear-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/linear-config.tsx new file mode 100644 index 000000000..db08f12ff --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/linear-config.tsx @@ -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 LinearConfigProps extends ConnectorConfigProps { + onNameChange?: (name: string) => void; +} + +export const LinearConfig: FC = ({ + connector, + onConfigChange, + onNameChange, +}) => { + const [apiKey, setApiKey] = useState( + (connector.config?.LINEAR_API_KEY as string) || "" + ); + const [name, setName] = useState(connector.name || ""); + + // Update API key and name when connector changes + useEffect(() => { + const key = (connector.config?.LINEAR_API_KEY as string) || ""; + setApiKey(key); + setName(connector.name || ""); + }, [connector.config, connector.name]); + + const handleApiKeyChange = (value: string) => { + setApiKey(value); + if (onConfigChange) { + onConfigChange({ + ...connector.config, + LINEAR_API_KEY: value, + }); + } + }; + + const handleNameChange = (value: string) => { + setName(value); + if (onNameChange) { + onNameChange(value); + } + }; + + return ( +
+ {/* Connector Name */} +
+
+ + handleNameChange(e.target.value)} + placeholder="My Linear Connector" + className="border-slate-400/20 focus-visible:border-slate-400/40" + /> +

+ A friendly name to identify this connector. +

+
+
+ + {/* Configuration */} +
+
+

Configuration

+
+ +
+ + handleApiKeyChange(e.target.value)} + placeholder="Begins with lin_api_..." + className="border-slate-400/20 focus-visible:border-slate-400/40" + /> +

+ Update your Linear API Key if needed. +

+
+
+
+ ); +}; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx index 78efb60a9..199d191ea 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx @@ -3,6 +3,7 @@ import type { FC } from "react"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import { GoogleDriveConfig } from "./components/google-drive-config"; +import { LinearConfig } from "./components/linear-config"; import { TavilyApiConfig } from "./components/tavily-api-config"; import { WebcrawlerConfig } from "./components/webcrawler-config"; import { YouTubeConfig } from "./components/youtube-config"; @@ -26,6 +27,8 @@ export function getConnectorConfigComponent( return GoogleDriveConfig; case "TAVILY_API": return TavilyApiConfig; + case "LINEAR_CONNECTOR": + return LinearConfig; case "WEBCRAWLER_CONNECTOR": return WebcrawlerConfig; case "YOUTUBE_CONNECTOR": diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx index 9499606c9..664425010 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { EnumConnectorName } from "@/contracts/enums/connector"; import { getConnectorTypeDisplay } from "@/lib/connectors/utils"; -import { getConnectFormComponent } from "../connect-forms"; +import { getConnectFormComponent } from "../../connect-forms"; interface ConnectorConnectViewProps { connectorType: string; @@ -41,9 +41,17 @@ export const ConnectorConnectView: FC = ({ if (isSubmitting) { return; } - const form = document.getElementById("tavily-connect-form") as HTMLFormElement; - if (form) { - form.requestSubmit(); + // Map connector types to their form IDs + const formIdMap: Record = { + TAVILY_API: "tavily-connect-form", + LINEAR_CONNECTOR: "linear-connect-form", + }; + const formId = formIdMap[connectorType]; + if (formId) { + const form = document.getElementById(formId) as HTMLFormElement; + if (form) { + form.requestSubmit(); + } } }; @@ -79,7 +87,7 @@ export const ConnectorConnectView: FC = ({

- Connect {connectorType === "TAVILY_API" ? "Tavily API" : connectorType} + Connect {getConnectorTypeDisplay(connectorType)}

Enter your connection details diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx index 76380e74b..3928b2266 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx @@ -104,11 +104,12 @@ export const AllConnectorsTab: FC = ({ const isWebcrawler = connector.id === "webcrawler-connector"; const isYouTube = connector.id === "youtube-connector"; const isTavily = connector.id === "tavily-api"; + const isLinear = connector.id === "linear-connector"; const handleConnect = isWebcrawler && onCreateWebcrawler ? onCreateWebcrawler : isYouTube && onCreateYouTube ? onCreateYouTube - : isTavily && onConnectNonOAuth + : (isTavily || isLinear) && onConnectNonOAuth ? () => onConnectNonOAuth(connector.connectorType) : () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`);