feat: add Linear OAuth integration and connector routes

- Introduced Linear OAuth support with new environment variables for client ID, client secret, and redirect URI.
- Implemented Linear connector routes for OAuth flow, including authorization and callback handling.
- Updated existing components to accommodate Linear integration, including validation changes and connector configuration.
- Enhanced the Linear indexer to utilize OAuth access tokens instead of API keys.
- Adjusted UI components to reflect the new Linear connector without requiring special configuration.
This commit is contained in:
Anish Sarkar 2026-01-02 21:24:28 +05:30
parent c5b184d475
commit b81af397c0
19 changed files with 309 additions and 1035 deletions

View file

@ -39,16 +39,21 @@ GOOGLE_CALENDAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/calendar/c
GOOGLE_GMAIL_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/gmail/connector/callback
GOOGLE_DRIVE_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/drive/connector/callback
# Notion OAuth for Notion Connector
NOTION_CLIENT_ID=your_notion_client_id
NOTION_CLIENT_SECRET=your_notion_client_secret
NOTION_REDIRECT_URI=http://localhost:8000/api/v1/auth/notion/connector/callback
# Airtable OAuth for Aitable Connector
# OAuth for Aitable Connector
AIRTABLE_CLIENT_ID=your_airtable_client_id
AIRTABLE_CLIENT_SECRET=your_airtable_client_secret
AIRTABLE_REDIRECT_URI=http://localhost:8000/api/v1/auth/airtable/connector/callback
# OAuth for Linear Connector
LINEAR_CLIENT_ID=your_linear_client_id
LINEAR_CLIENT_SECRET=your_linear_client_secret
LINEAR_REDIRECT_URI=http://localhost:8000/api/v1/auth/linear/connector/callback
# OAuth for Notion Connector
NOTION_CLIENT_ID=your_notion_client_id
NOTION_CLIENT_SECRET=your_notion_client_secret
NOTION_REDIRECT_URI=http://localhost:8000/api/v1/auth/notion/connector/callback
# Embedding Model
# Examples:
# # Get sentence transformers embeddings

View file

@ -95,6 +95,11 @@ class Config:
NOTION_CLIENT_SECRET = os.getenv("NOTION_CLIENT_SECRET")
NOTION_REDIRECT_URI = os.getenv("NOTION_REDIRECT_URI")
# Linear OAuth
LINEAR_CLIENT_ID = os.getenv("LINEAR_CLIENT_ID")
LINEAR_CLIENT_SECRET = os.getenv("LINEAR_CLIENT_SECRET")
LINEAR_REDIRECT_URI = os.getenv("LINEAR_REDIRECT_URI")
# LLM instances are now managed per-user through the LLMConfig system
# Legacy environment variables removed in favor of user-specific configurations

View file

@ -14,24 +14,24 @@ import requests
class LinearConnector:
"""Class for retrieving issues and comments from Linear."""
def __init__(self, token: str | None = None):
def __init__(self, access_token: str | None = None):
"""
Initialize the LinearConnector class.
Args:
token: Linear API token (optional, can be set later with set_token)
access_token: Linear OAuth access token (optional, can be set later with set_token)
"""
self.token = token
self.access_token = access_token
self.api_url = "https://api.linear.app/graphql"
def set_token(self, token: str) -> None:
def set_token(self, access_token: str) -> None:
"""
Set the Linear API token.
Set the Linear OAuth access token.
Args:
token: Linear API token
access_token: Linear OAuth access token
"""
self.token = token
self.access_token = access_token
def get_headers(self) -> dict[str, str]:
"""
@ -41,12 +41,12 @@ class LinearConnector:
Dictionary of headers
Raises:
ValueError: If no Linear token has been set
ValueError: If no Linear access token has been set
"""
if not self.token:
raise ValueError("Linear token not initialized. Call set_token() first.")
if not self.access_token:
raise ValueError("Linear access token not initialized. Call set_token() first.")
return {"Content-Type": "application/json", "Authorization": self.token}
return {"Content-Type": "application/json", "Authorization": f"Bearer {self.access_token}"}
def execute_graphql_query(
self, query: str, variables: dict[str, Any] | None = None
@ -62,11 +62,11 @@ class LinearConnector:
Response data from the API
Raises:
ValueError: If no Linear token has been set
ValueError: If no Linear access token has been set
Exception: If the API request fails
"""
if not self.token:
raise ValueError("Linear token not initialized. Call set_token() first.")
if not self.access_token:
raise ValueError("Linear access token not initialized. Call set_token() first.")
headers = self.get_headers()
payload = {"query": query}
@ -94,7 +94,7 @@ class LinearConnector:
List of issue objects
Raises:
ValueError: If no Linear token has been set
ValueError: If no Linear access token has been set
Exception: If the API request fails
"""
comments_query = ""
@ -451,10 +451,10 @@ class LinearConnector:
# Example usage (uncomment to use):
"""
if __name__ == "__main__":
# Set your token here
token = "YOUR_LINEAR_API_KEY"
# Set your OAuth access token here
access_token = "YOUR_LINEAR_ACCESS_TOKEN"
linear = LinearConnector(token)
linear = LinearConnector(access_token=access_token)
try:
# Get all issues with comments

View file

@ -18,6 +18,7 @@ from .google_gmail_add_connector_route import (
from .logs_routes import router as logs_router
from .luma_add_connector_route import router as luma_add_connector_router
from .new_chat_routes import router as new_chat_router
from .linear_add_connector_route import router as linear_add_connector_router
from .notion_add_connector_route import router as notion_add_connector_router
from .new_llm_config_routes import router as new_llm_config_router
from .notes_routes import router as notes_router
@ -40,6 +41,7 @@ router.include_router(google_calendar_add_connector_router)
router.include_router(google_gmail_add_connector_router)
router.include_router(google_drive_add_connector_router)
router.include_router(airtable_add_connector_router)
router.include_router(linear_add_connector_router)
router.include_router(luma_add_connector_router)
router.include_router(notion_add_connector_router)
router.include_router(new_llm_config_router) # LLM configs with prompt configuration

View file

@ -0,0 +1,256 @@
"""
Linear Connector OAuth Routes.
Handles OAuth 2.0 authentication flow for Linear connector.
"""
import base64
import json
import logging
from datetime import UTC, datetime, timedelta
from uuid import UUID
import httpx
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import RedirectResponse
from pydantic import ValidationError
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from app.config import config
from app.db import (
SearchSourceConnector,
SearchSourceConnectorType,
User,
get_async_session,
)
from app.users import current_active_user
logger = logging.getLogger(__name__)
router = APIRouter()
# Linear OAuth endpoints
AUTHORIZATION_URL = "https://linear.app/oauth/authorize"
TOKEN_URL = "https://api.linear.app/oauth/token"
# OAuth scopes for Linear
SCOPES = ["read", "write"]
def make_basic_auth_header(client_id: str, client_secret: str) -> str:
"""Create Basic Auth header for Linear OAuth."""
credentials = f"{client_id}:{client_secret}".encode()
b64 = base64.b64encode(credentials).decode("ascii")
return f"Basic {b64}"
@router.get("/auth/linear/connector/add")
async def connect_linear(space_id: int, user: User = Depends(current_active_user)):
"""
Initiate Linear OAuth flow.
Args:
space_id: The search space ID
user: Current authenticated user
Returns:
Authorization URL for redirect
"""
try:
if not space_id:
raise HTTPException(status_code=400, detail="space_id is required")
if not config.LINEAR_CLIENT_ID:
raise HTTPException(
status_code=500, detail="Linear OAuth not configured."
)
# Generate state parameter
state_payload = json.dumps(
{
"space_id": space_id,
"user_id": str(user.id),
}
)
state_encoded = base64.urlsafe_b64encode(state_payload.encode()).decode()
# Build authorization URL
from urllib.parse import urlencode
auth_params = {
"client_id": config.LINEAR_CLIENT_ID,
"response_type": "code",
"redirect_uri": config.LINEAR_REDIRECT_URI,
"scope": " ".join(SCOPES),
"state": state_encoded,
}
auth_url = f"{AUTHORIZATION_URL}?{urlencode(auth_params)}"
logger.info(
f"Generated Linear OAuth URL for user {user.id}, space {space_id}"
)
return {"auth_url": auth_url}
except Exception as e:
logger.error(f"Failed to initiate Linear OAuth: {e!s}", exc_info=True)
raise HTTPException(
status_code=500, detail=f"Failed to initiate Linear OAuth: {e!s}"
) from e
@router.get("/auth/linear/connector/callback")
async def linear_callback(
request: Request,
code: str,
state: str,
session: AsyncSession = Depends(get_async_session),
):
"""
Handle Linear OAuth callback.
Args:
request: FastAPI request object
code: Authorization code from Linear
state: State parameter containing user/space info
session: Database session
Returns:
Redirect response to frontend
"""
try:
# Decode and parse the state
try:
decoded_state = base64.urlsafe_b64decode(state.encode()).decode()
data = json.loads(decoded_state)
except Exception as e:
raise HTTPException(
status_code=400, detail=f"Invalid state parameter: {e!s}"
) from e
user_id = UUID(data["user_id"])
space_id = data["space_id"]
# Exchange authorization code for access token
auth_header = make_basic_auth_header(
config.LINEAR_CLIENT_ID, config.LINEAR_CLIENT_SECRET
)
token_data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": config.LINEAR_REDIRECT_URI,
}
async with httpx.AsyncClient() as client:
token_response = await client.post(
TOKEN_URL,
data=token_data,
headers={
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": auth_header,
},
timeout=30.0,
)
if token_response.status_code != 200:
error_detail = token_response.text
try:
error_json = token_response.json()
error_detail = error_json.get("error_description", error_detail)
except Exception:
pass
raise HTTPException(
status_code=400, detail=f"Token exchange failed: {error_detail}"
)
token_json = token_response.json()
# Calculate expiration time (UTC, tz-aware)
expires_at = None
if token_json.get("expires_in"):
now_utc = datetime.now(UTC)
expires_at = now_utc + timedelta(seconds=int(token_json["expires_in"]))
# Store the access token and refresh token in connector config
connector_config = {
"access_token": token_json["access_token"],
"refresh_token": token_json.get("refresh_token"),
"token_type": token_json.get("token_type", "Bearer"),
"expires_in": token_json.get("expires_in"),
"expires_at": expires_at.isoformat() if expires_at else None,
"scope": token_json.get("scope"),
}
# Check if connector already exists for this search space and user
existing_connector_result = await session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == space_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.LINEAR_CONNECTOR,
)
)
existing_connector = existing_connector_result.scalars().first()
if existing_connector:
# Update existing connector
existing_connector.config = connector_config
existing_connector.name = "Linear Connector"
existing_connector.is_indexable = True
logger.info(
f"Updated existing Linear connector for user {user_id} in space {space_id}"
)
else:
# Create new connector
new_connector = SearchSourceConnector(
name="Linear Connector",
connector_type=SearchSourceConnectorType.LINEAR_CONNECTOR,
is_indexable=True,
config=connector_config,
search_space_id=space_id,
user_id=user_id,
)
session.add(new_connector)
logger.info(
f"Created new Linear connector for user {user_id} in space {space_id}"
)
try:
await session.commit()
logger.info(f"Successfully saved Linear connector for user {user_id}")
# Redirect to the frontend with success params
return RedirectResponse(
url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=linear-connector"
)
except ValidationError as e:
await session.rollback()
raise HTTPException(
status_code=422, detail=f"Validation error: {e!s}"
) from e
except IntegrityError as e:
await session.rollback()
raise HTTPException(
status_code=409,
detail=f"Integrity error: A connector with this type already exists. {e!s}",
) from e
except Exception as e:
logger.error(f"Failed to create search source connector: {e!s}")
await session.rollback()
raise HTTPException(
status_code=500,
detail=f"Failed to create search source connector: {e!s}",
) from e
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to complete Linear OAuth: {e!s}", exc_info=True)
raise HTTPException(
status_code=500, detail=f"Failed to complete Linear OAuth: {e!s}"
) from e

View file

@ -92,16 +92,16 @@ async def index_linear_issues(
f"Connector with ID {connector_id} not found or is not a Linear connector",
)
# Get the Linear token from the connector config
linear_token = connector.config.get("LINEAR_API_KEY")
if not linear_token:
# Get the Linear access token from the connector config
linear_access_token = connector.config.get("access_token")
if not linear_access_token:
await task_logger.log_task_failure(
log_entry,
f"Linear API token not found in connector config for connector {connector_id}",
"Missing Linear token",
f"Linear access token not found in connector config for connector {connector_id}",
"Missing Linear access token",
{"error_type": "MissingToken"},
)
return 0, "Linear API token not found in connector config"
return 0, "Linear access token not found in connector config"
# Initialize Linear client
await task_logger.log_task_progress(
@ -110,7 +110,7 @@ async def index_linear_issues(
{"stage": "client_initialization"},
)
linear_client = LinearConnector(token=linear_token)
linear_client = LinearConnector(access_token=linear_access_token)
# Calculate date range
start_date_str, end_date_str = calculate_date_range(

View file

@ -532,7 +532,6 @@ def validate_connector_config(
)
},
},
"LINEAR_CONNECTOR": {"required": ["LINEAR_API_KEY"], "validators": {}},
"DISCORD_CONNECTOR": {"required": ["DISCORD_BOT_TOKEN"], "validators": {}},
"JIRA_CONNECTOR": {
"required": ["JIRA_EMAIL", "JIRA_API_TOKEN", "JIRA_BASE_URL"],

View file

@ -63,7 +63,7 @@ export const ConnectorDialogHeader: FC<ConnectorDialogHeaderProps> = ({
<div className="w-full sm:w-72 sm:pb-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground/60" />
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-gray-500 dark:text-gray-500" />
<input
type="text"
placeholder="Search"
@ -78,7 +78,7 @@ export const ConnectorDialogHeader: FC<ConnectorDialogHeaderProps> = ({
<button
type="button"
onClick={() => onSearchChange("")}
className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-gray-500 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
aria-label="Clear search"
>
<X className="size-4" />

View file

@ -1,396 +0,0 @@
"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 {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { DateRangeSelector } from "../../components/date-range-selector";
import { getConnectorBenefits } from "../connector-benefits";
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<typeof linearConnectorFormSchema>;
export const LinearConnectForm: 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<LinearConnectorFormValues>({
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: 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 Linear API Key to use this connector. You can create one from{" "}
<a
href="https://linear.app/settings/api"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
Linear 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="linear-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 Linear 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">Linear API Key</FormLabel>
<FormControl>
<Input
type="password"
placeholder="lin_api_..."
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 Linear API Key will be encrypted and stored securely. It typically starts
with "lin_api_".
</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.LINEAR_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 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) => (
<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 Linear connector uses the Linear GraphQL API to fetch all issues and comments
that the API key has access to within a 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 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 a read-only API key for this connector to work. This limits the
permissions to just reading your Linear 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 key
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Log in to your Linear account</li>
<li>
Navigate to{" "}
<a
href="https://linear.app/settings/api"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
https://linear.app/settings/api
</a>{" "}
in your browser.
</li>
<li>Alternatively, click on your profile picture Settings API</li>
<li>
Click the <strong>+ New API key</strong> button.
</li>
<li>Enter a description for your key (like "Search Connector").</li>
<li>Select "Read-only" as the permission.</li>
<li>
Click <strong>Create</strong> to generate the API key.
</li>
<li>
Copy the generated API key that starts with 'lin_api_' 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 key will have access to all issues and comments that your user account
can see. If you're creating the key as an admin, it will have access to all
issues in the workspace.
</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 and comments will be indexed. Linear 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>Linear</strong>{" "}
Connector.
</li>
<li>
Place the <strong>API Key</strong> in the form field.
</li>
<li>
Click <strong>Connect</strong> to establish the connection.
</li>
<li>Once connected, your Linear 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 Linear connector indexes the following data:</p>
<ul className="list-disc pl-5 space-y-1">
<li>Issue titles and identifiers (e.g., PROJ-123)</li>
<li>Issue descriptions</li>
<li>Issue comments</li>
<li>Issue status and metadata</li>
</ul>
</AlertDescription>
</Alert>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};

View file

@ -1,396 +0,0 @@
"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 {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { DateRangeSelector } from "../../components/date-range-selector";
import { getConnectorBenefits } from "../connector-benefits";
import type { ConnectFormProps } from "../index";
const notionConnectorFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
integration_token: z.string().min(10, {
message: "Notion Integration Token is required and must be valid.",
}),
});
type NotionConnectorFormValues = z.infer<typeof notionConnectorFormSchema>;
export const NotionConnectForm: 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<NotionConnectorFormValues>({
resolver: zodResolver(notionConnectorFormSchema),
defaultValues: {
name: "Notion Connector",
integration_token: "",
},
});
const handleSubmit = async (values: NotionConnectorFormValues) => {
// Prevent multiple submissions
if (isSubmittingRef.current || isSubmitting) {
return;
}
isSubmittingRef.current = true;
try {
await onSubmit({
name: values.name,
connector_type: EnumConnectorName.NOTION_CONNECTOR,
config: {
NOTION_INTEGRATION_TOKEN: values.integration_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">Integration Token Required</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
You'll need a Notion Integration Token to use this connector. You can create one from{" "}
<a
href="https://www.notion.so/my-integrations"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
Notion Integrations
</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="notion-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 Notion 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="integration_token"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Notion Integration Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="ntn_..."
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 Notion Integration Token will be encrypted and stored securely. It
typically starts with "ntn_".
</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.NOTION_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 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) => (
<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 Notion connector uses the Notion API to fetch pages from all accessible
workspaces that the integration token 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">
Integration Token Required
</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You need to create a Notion integration and share pages with it to get access.
The integration needs read access to pages.
</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 a Notion Integration
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>
Go to{" "}
<a
href="https://www.notion.so/my-integrations"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
https://www.notion.so/my-integrations
</a>
</li>
<li>
Click <strong>+ New integration</strong>
</li>
<li>Enter a name for your integration (e.g., "Search Connector")</li>
<li>Select your workspace</li>
<li>
Under <strong>Capabilities</strong>, enable <strong>Read content</strong>
</li>
<li>
Click <strong>Submit</strong> to create the integration
</li>
<li>
Copy the <strong>Internal Integration Token</strong> (starts with "ntn_")
</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 2: Share Pages with Integration
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Open the Notion pages or databases you want to index</li>
<li>
Click the <strong></strong> (three dots) menu in the top right
</li>
<li>
Select <strong>Add connections</strong> or <strong>Connections</strong>
</li>
<li>Search for and select your integration</li>
<li>Repeat for all pages you want to index</li>
</ol>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mt-3">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Important</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
The integration can only access pages that have been explicitly shared with
it. Make sure to share all pages you want to index.
</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>Notion</strong>{" "}
Connector.
</li>
<li>
Place the <strong>Integration Token</strong> in the form field.
</li>
<li>
Click <strong>Connect</strong> to establish the connection.
</li>
<li>Once connected, your Notion 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 Notion connector indexes the following data:</p>
<ul className="list-disc pl-5 space-y-1">
<li>Page titles and content</li>
<li>Database entries and properties</li>
<li>Page metadata and properties</li>
<li>Nested pages and sub-pages</li>
</ul>
</AlertDescription>
</Alert>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};

View file

@ -8,7 +8,6 @@ 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 { SearxngConnectForm } from "./components/searxng-connect-form";
@ -50,8 +49,6 @@ export function getConnectFormComponent(connectorType: string): ConnectFormCompo
return LinkupApiConnectForm;
case "BAIDU_SEARCH_API":
return BaiduSearchApiConnectForm;
case "LINEAR_CONNECTOR":
return LinearConnectForm;
case "ELASTICSEARCH_CONNECTOR":
return ElasticsearchConnectForm;
case "SLACK_CONNECTOR":

View file

@ -1,89 +0,0 @@
"use client";
import { KeyRound } from "lucide-react";
import type { FC } from "react";
import { useEffect, useState } 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<LinearConfigProps> = ({
connector,
onConfigChange,
onNameChange,
}) => {
const [apiKey, setApiKey] = useState<string>((connector.config?.LINEAR_API_KEY as string) || "");
const [name, setName] = useState<string>(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 (
<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 Linear 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" />
Linear API Key
</Label>
<Input
type="password"
value={apiKey}
onChange={(e) => handleApiKeyChange(e.target.value)}
placeholder="Begins with lin_api_..."
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Update your Linear API Key if needed.
</p>
</div>
</div>
</div>
);
};

View file

@ -1,91 +0,0 @@
"use client";
import { KeyRound } from "lucide-react";
import type { FC } from "react";
import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import type { ConnectorConfigProps } from "../index";
export interface NotionConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
export const NotionConfig: FC<NotionConfigProps> = ({
connector,
onConfigChange,
onNameChange,
}) => {
const [integrationToken, setIntegrationToken] = useState<string>(
(connector.config?.NOTION_INTEGRATION_TOKEN as string) || ""
);
const [name, setName] = useState<string>(connector.name || "");
// Update integration token and name when connector changes
useEffect(() => {
const token = (connector.config?.NOTION_INTEGRATION_TOKEN as string) || "";
setIntegrationToken(token);
setName(connector.name || "");
}, [connector.config, connector.name]);
const handleIntegrationTokenChange = (value: string) => {
setIntegrationToken(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
NOTION_INTEGRATION_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 Notion 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" />
Notion Integration Token
</Label>
<Input
type="password"
value={integrationToken}
onChange={(e) => handleIntegrationTokenChange(e.target.value)}
placeholder="Begins with secret_..."
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Update your Notion Integration Token if needed.
</p>
</div>
</div>
</div>
);
};

View file

@ -12,7 +12,6 @@ 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 { SearxngConfig } from "./components/searxng-config";
@ -45,8 +44,6 @@ export function getConnectorConfigComponent(
return LinkupApiConfig;
case "BAIDU_SEARCH_API":
return BaiduSearchApiConfig;
case "LINEAR_CONNECTOR":
return LinearConfig;
case "WEBCRAWLER_CONNECTOR":
return WebcrawlerConfig;
case "ELASTICSEARCH_CONNECTOR":

View file

@ -51,7 +51,6 @@ export const ConnectorConnectView: FC<ConnectorConnectViewProps> = ({
SEARXNG_API: "searxng-connect-form",
LINKUP_API: "linkup-api-connect-form",
BAIDU_SEARCH_API: "baidu-search-api-connect-form",
LINEAR_CONNECTOR: "linear-connect-form",
ELASTICSEARCH_CONNECTOR: "elasticsearch-connect-form",
SLACK_CONNECTOR: "slack-connect-form",
DISCORD_CONNECTOR: "discord-connect-form",

View file

@ -37,6 +37,13 @@ export const OAUTH_CONNECTORS = [
connectorType: EnumConnectorName.NOTION_CONNECTOR,
authEndpoint: "/api/v1/auth/notion/connector/add/",
},
{
id: "linear-connector",
title: "Linear",
description: "Search issues & projects",
connectorType: EnumConnectorName.LINEAR_CONNECTOR,
authEndpoint: "/api/v1/auth/linear/connector/add/",
},
] as const;
// Content Sources (tools that extract and import content from external sources)
@ -87,12 +94,6 @@ export const OTHER_CONNECTORS = [
description: "Search repositories",
connectorType: EnumConnectorName.GITHUB_CONNECTOR,
},
{
id: "linear-connector",
title: "Linear",
description: "Search issues & projects",
connectorType: EnumConnectorName.LINEAR_CONNECTOR,
},
{
id: "jira-connector",
title: "Jira",

View file

@ -472,7 +472,7 @@ export const useConnectorDialog = () => {
}
// Auto-start indexing for non-OAuth reindexable connectors
// This only applies to non-OAuth reindexable connectors (e.g., Elasticsearch, Linear)
// This only applies to non-OAuth reindexable connectors (e.g., Elasticsearch)
// Non-reindexable connectors (e.g., Tavily) have is_indexable: false, so they won't trigger this
// Backend will use default date ranges (365 days ago to today) if dates are not provided
if (connector.is_indexable) {

View file

@ -36,7 +36,6 @@ export const editConnectorSchema = z.object({
SEARXNG_LANGUAGE: z.string().optional(),
SEARXNG_SAFESEARCH: z.string().optional(),
SEARXNG_VERIFY_SSL: z.string().optional(),
LINEAR_API_KEY: z.string().optional(),
LINKUP_API_KEY: z.string().optional(),
DISCORD_BOT_TOKEN: z.string().optional(),
CONFLUENCE_BASE_URL: z.string().optional(),

View file

@ -86,7 +86,6 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
SEARXNG_LANGUAGE: "",
SEARXNG_SAFESEARCH: "",
SEARXNG_VERIFY_SSL: "",
LINEAR_API_KEY: "",
DISCORD_BOT_TOKEN: "",
CONFLUENCE_BASE_URL: "",
CONFLUENCE_EMAIL: "",
@ -134,7 +133,6 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
config.SEARXNG_VERIFY_SSL !== undefined && config.SEARXNG_VERIFY_SSL !== null
? String(config.SEARXNG_VERIFY_SSL)
: "",
LINEAR_API_KEY: config.LINEAR_API_KEY || "",
LINKUP_API_KEY: config.LINKUP_API_KEY || "",
DISCORD_BOT_TOKEN: config.DISCORD_BOT_TOKEN || "",
CONFLUENCE_BASE_URL: config.CONFLUENCE_BASE_URL || "",
@ -384,16 +382,6 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
break;
}
case "LINEAR_CONNECTOR":
if (formData.LINEAR_API_KEY !== originalConfig.LINEAR_API_KEY) {
if (!formData.LINEAR_API_KEY) {
toast.error("Linear API Key cannot be empty.");
setIsSaving(false);
return;
}
newConfig = { LINEAR_API_KEY: formData.LINEAR_API_KEY };
}
break;
case "LINKUP_API":
if (formData.LINKUP_API_KEY !== originalConfig.LINKUP_API_KEY) {
if (!formData.LINKUP_API_KEY) {
@ -599,8 +587,6 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
"SEARXNG_VERIFY_SSL",
verifyValue === null ? "" : String(verifyValue)
);
} else if (connector.connector_type === "LINEAR_CONNECTOR") {
editForm.setValue("LINEAR_API_KEY", newlySavedConfig.LINEAR_API_KEY || "");
} else if (connector.connector_type === "LINKUP_API") {
editForm.setValue("LINKUP_API_KEY", newlySavedConfig.LINKUP_API_KEY || "");
} else if (connector.connector_type === "DISCORD_CONNECTOR") {