mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
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:
parent
c5b184d475
commit
b81af397c0
19 changed files with 309 additions and 1035 deletions
|
|
@ -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_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
|
GOOGLE_DRIVE_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/drive/connector/callback
|
||||||
|
|
||||||
# Notion OAuth for Notion Connector
|
# OAuth for Aitable 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
|
|
||||||
AIRTABLE_CLIENT_ID=your_airtable_client_id
|
AIRTABLE_CLIENT_ID=your_airtable_client_id
|
||||||
AIRTABLE_CLIENT_SECRET=your_airtable_client_secret
|
AIRTABLE_CLIENT_SECRET=your_airtable_client_secret
|
||||||
AIRTABLE_REDIRECT_URI=http://localhost:8000/api/v1/auth/airtable/connector/callback
|
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
|
# Embedding Model
|
||||||
# Examples:
|
# Examples:
|
||||||
# # Get sentence transformers embeddings
|
# # Get sentence transformers embeddings
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,11 @@ class Config:
|
||||||
NOTION_CLIENT_SECRET = os.getenv("NOTION_CLIENT_SECRET")
|
NOTION_CLIENT_SECRET = os.getenv("NOTION_CLIENT_SECRET")
|
||||||
NOTION_REDIRECT_URI = os.getenv("NOTION_REDIRECT_URI")
|
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
|
# LLM instances are now managed per-user through the LLMConfig system
|
||||||
# Legacy environment variables removed in favor of user-specific configurations
|
# Legacy environment variables removed in favor of user-specific configurations
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,24 +14,24 @@ import requests
|
||||||
class LinearConnector:
|
class LinearConnector:
|
||||||
"""Class for retrieving issues and comments from Linear."""
|
"""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.
|
Initialize the LinearConnector class.
|
||||||
|
|
||||||
Args:
|
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"
|
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:
|
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]:
|
def get_headers(self) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -41,12 +41,12 @@ class LinearConnector:
|
||||||
Dictionary of headers
|
Dictionary of headers
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If no Linear token has been set
|
ValueError: If no Linear access token has been set
|
||||||
"""
|
"""
|
||||||
if not self.token:
|
if not self.access_token:
|
||||||
raise ValueError("Linear token not initialized. Call set_token() first.")
|
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(
|
def execute_graphql_query(
|
||||||
self, query: str, variables: dict[str, Any] | None = None
|
self, query: str, variables: dict[str, Any] | None = None
|
||||||
|
|
@ -62,11 +62,11 @@ class LinearConnector:
|
||||||
Response data from the API
|
Response data from the API
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If no Linear token has been set
|
ValueError: If no Linear access token has been set
|
||||||
Exception: If the API request fails
|
Exception: If the API request fails
|
||||||
"""
|
"""
|
||||||
if not self.token:
|
if not self.access_token:
|
||||||
raise ValueError("Linear token not initialized. Call set_token() first.")
|
raise ValueError("Linear access token not initialized. Call set_token() first.")
|
||||||
|
|
||||||
headers = self.get_headers()
|
headers = self.get_headers()
|
||||||
payload = {"query": query}
|
payload = {"query": query}
|
||||||
|
|
@ -94,7 +94,7 @@ class LinearConnector:
|
||||||
List of issue objects
|
List of issue objects
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If no Linear token has been set
|
ValueError: If no Linear access token has been set
|
||||||
Exception: If the API request fails
|
Exception: If the API request fails
|
||||||
"""
|
"""
|
||||||
comments_query = ""
|
comments_query = ""
|
||||||
|
|
@ -451,10 +451,10 @@ class LinearConnector:
|
||||||
# Example usage (uncomment to use):
|
# Example usage (uncomment to use):
|
||||||
"""
|
"""
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Set your token here
|
# Set your OAuth access token here
|
||||||
token = "YOUR_LINEAR_API_KEY"
|
access_token = "YOUR_LINEAR_ACCESS_TOKEN"
|
||||||
|
|
||||||
linear = LinearConnector(token)
|
linear = LinearConnector(access_token=access_token)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get all issues with comments
|
# Get all issues with comments
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ from .google_gmail_add_connector_route import (
|
||||||
from .logs_routes import router as logs_router
|
from .logs_routes import router as logs_router
|
||||||
from .luma_add_connector_route import router as luma_add_connector_router
|
from .luma_add_connector_route import router as luma_add_connector_router
|
||||||
from .new_chat_routes import router as new_chat_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 .notion_add_connector_route import router as notion_add_connector_router
|
||||||
from .new_llm_config_routes import router as new_llm_config_router
|
from .new_llm_config_routes import router as new_llm_config_router
|
||||||
from .notes_routes import router as notes_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_gmail_add_connector_router)
|
||||||
router.include_router(google_drive_add_connector_router)
|
router.include_router(google_drive_add_connector_router)
|
||||||
router.include_router(airtable_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(luma_add_connector_router)
|
||||||
router.include_router(notion_add_connector_router)
|
router.include_router(notion_add_connector_router)
|
||||||
router.include_router(new_llm_config_router) # LLM configs with prompt configuration
|
router.include_router(new_llm_config_router) # LLM configs with prompt configuration
|
||||||
|
|
|
||||||
256
surfsense_backend/app/routes/linear_add_connector_route.py
Normal file
256
surfsense_backend/app/routes/linear_add_connector_route.py
Normal 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
|
||||||
|
|
||||||
|
|
@ -92,16 +92,16 @@ async def index_linear_issues(
|
||||||
f"Connector with ID {connector_id} not found or is not a Linear connector",
|
f"Connector with ID {connector_id} not found or is not a Linear connector",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get the Linear token from the connector config
|
# Get the Linear access token from the connector config
|
||||||
linear_token = connector.config.get("LINEAR_API_KEY")
|
linear_access_token = connector.config.get("access_token")
|
||||||
if not linear_token:
|
if not linear_access_token:
|
||||||
await task_logger.log_task_failure(
|
await task_logger.log_task_failure(
|
||||||
log_entry,
|
log_entry,
|
||||||
f"Linear API token not found in connector config for connector {connector_id}",
|
f"Linear access token not found in connector config for connector {connector_id}",
|
||||||
"Missing Linear token",
|
"Missing Linear access token",
|
||||||
{"error_type": "MissingToken"},
|
{"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
|
# Initialize Linear client
|
||||||
await task_logger.log_task_progress(
|
await task_logger.log_task_progress(
|
||||||
|
|
@ -110,7 +110,7 @@ async def index_linear_issues(
|
||||||
{"stage": "client_initialization"},
|
{"stage": "client_initialization"},
|
||||||
)
|
)
|
||||||
|
|
||||||
linear_client = LinearConnector(token=linear_token)
|
linear_client = LinearConnector(access_token=linear_access_token)
|
||||||
|
|
||||||
# Calculate date range
|
# Calculate date range
|
||||||
start_date_str, end_date_str = calculate_date_range(
|
start_date_str, end_date_str = calculate_date_range(
|
||||||
|
|
|
||||||
|
|
@ -532,7 +532,6 @@ def validate_connector_config(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"LINEAR_CONNECTOR": {"required": ["LINEAR_API_KEY"], "validators": {}},
|
|
||||||
"DISCORD_CONNECTOR": {"required": ["DISCORD_BOT_TOKEN"], "validators": {}},
|
"DISCORD_CONNECTOR": {"required": ["DISCORD_BOT_TOKEN"], "validators": {}},
|
||||||
"JIRA_CONNECTOR": {
|
"JIRA_CONNECTOR": {
|
||||||
"required": ["JIRA_EMAIL", "JIRA_API_TOKEN", "JIRA_BASE_URL"],
|
"required": ["JIRA_EMAIL", "JIRA_API_TOKEN", "JIRA_BASE_URL"],
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export const ConnectorDialogHeader: FC<ConnectorDialogHeaderProps> = ({
|
||||||
|
|
||||||
<div className="w-full sm:w-72 sm:pb-1">
|
<div className="w-full sm:w-72 sm:pb-1">
|
||||||
<div className="relative">
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
|
|
@ -78,7 +78,7 @@ export const ConnectorDialogHeader: FC<ConnectorDialogHeaderProps> = ({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSearchChange("")}
|
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"
|
aria-label="Clear search"
|
||||||
>
|
>
|
||||||
<X className="size-4" />
|
<X className="size-4" />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { DiscordConnectForm } from "./components/discord-connect-form";
|
||||||
import { ElasticsearchConnectForm } from "./components/elasticsearch-connect-form";
|
import { ElasticsearchConnectForm } from "./components/elasticsearch-connect-form";
|
||||||
import { GithubConnectForm } from "./components/github-connect-form";
|
import { GithubConnectForm } from "./components/github-connect-form";
|
||||||
import { JiraConnectForm } from "./components/jira-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 { LinkupApiConnectForm } from "./components/linkup-api-connect-form";
|
||||||
import { LumaConnectForm } from "./components/luma-connect-form";
|
import { LumaConnectForm } from "./components/luma-connect-form";
|
||||||
import { SearxngConnectForm } from "./components/searxng-connect-form";
|
import { SearxngConnectForm } from "./components/searxng-connect-form";
|
||||||
|
|
@ -50,8 +49,6 @@ export function getConnectFormComponent(connectorType: string): ConnectFormCompo
|
||||||
return LinkupApiConnectForm;
|
return LinkupApiConnectForm;
|
||||||
case "BAIDU_SEARCH_API":
|
case "BAIDU_SEARCH_API":
|
||||||
return BaiduSearchApiConnectForm;
|
return BaiduSearchApiConnectForm;
|
||||||
case "LINEAR_CONNECTOR":
|
|
||||||
return LinearConnectForm;
|
|
||||||
case "ELASTICSEARCH_CONNECTOR":
|
case "ELASTICSEARCH_CONNECTOR":
|
||||||
return ElasticsearchConnectForm;
|
return ElasticsearchConnectForm;
|
||||||
case "SLACK_CONNECTOR":
|
case "SLACK_CONNECTOR":
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -12,7 +12,6 @@ import { ElasticsearchConfig } from "./components/elasticsearch-config";
|
||||||
import { GithubConfig } from "./components/github-config";
|
import { GithubConfig } from "./components/github-config";
|
||||||
import { GoogleDriveConfig } from "./components/google-drive-config";
|
import { GoogleDriveConfig } from "./components/google-drive-config";
|
||||||
import { JiraConfig } from "./components/jira-config";
|
import { JiraConfig } from "./components/jira-config";
|
||||||
import { LinearConfig } from "./components/linear-config";
|
|
||||||
import { LinkupApiConfig } from "./components/linkup-api-config";
|
import { LinkupApiConfig } from "./components/linkup-api-config";
|
||||||
import { LumaConfig } from "./components/luma-config";
|
import { LumaConfig } from "./components/luma-config";
|
||||||
import { SearxngConfig } from "./components/searxng-config";
|
import { SearxngConfig } from "./components/searxng-config";
|
||||||
|
|
@ -45,8 +44,6 @@ export function getConnectorConfigComponent(
|
||||||
return LinkupApiConfig;
|
return LinkupApiConfig;
|
||||||
case "BAIDU_SEARCH_API":
|
case "BAIDU_SEARCH_API":
|
||||||
return BaiduSearchApiConfig;
|
return BaiduSearchApiConfig;
|
||||||
case "LINEAR_CONNECTOR":
|
|
||||||
return LinearConfig;
|
|
||||||
case "WEBCRAWLER_CONNECTOR":
|
case "WEBCRAWLER_CONNECTOR":
|
||||||
return WebcrawlerConfig;
|
return WebcrawlerConfig;
|
||||||
case "ELASTICSEARCH_CONNECTOR":
|
case "ELASTICSEARCH_CONNECTOR":
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ export const ConnectorConnectView: FC<ConnectorConnectViewProps> = ({
|
||||||
SEARXNG_API: "searxng-connect-form",
|
SEARXNG_API: "searxng-connect-form",
|
||||||
LINKUP_API: "linkup-api-connect-form",
|
LINKUP_API: "linkup-api-connect-form",
|
||||||
BAIDU_SEARCH_API: "baidu-search-api-connect-form",
|
BAIDU_SEARCH_API: "baidu-search-api-connect-form",
|
||||||
LINEAR_CONNECTOR: "linear-connect-form",
|
|
||||||
ELASTICSEARCH_CONNECTOR: "elasticsearch-connect-form",
|
ELASTICSEARCH_CONNECTOR: "elasticsearch-connect-form",
|
||||||
SLACK_CONNECTOR: "slack-connect-form",
|
SLACK_CONNECTOR: "slack-connect-form",
|
||||||
DISCORD_CONNECTOR: "discord-connect-form",
|
DISCORD_CONNECTOR: "discord-connect-form",
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,13 @@ export const OAUTH_CONNECTORS = [
|
||||||
connectorType: EnumConnectorName.NOTION_CONNECTOR,
|
connectorType: EnumConnectorName.NOTION_CONNECTOR,
|
||||||
authEndpoint: "/api/v1/auth/notion/connector/add/",
|
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;
|
] as const;
|
||||||
|
|
||||||
// Content Sources (tools that extract and import content from external sources)
|
// Content Sources (tools that extract and import content from external sources)
|
||||||
|
|
@ -87,12 +94,6 @@ export const OTHER_CONNECTORS = [
|
||||||
description: "Search repositories",
|
description: "Search repositories",
|
||||||
connectorType: EnumConnectorName.GITHUB_CONNECTOR,
|
connectorType: EnumConnectorName.GITHUB_CONNECTOR,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "linear-connector",
|
|
||||||
title: "Linear",
|
|
||||||
description: "Search issues & projects",
|
|
||||||
connectorType: EnumConnectorName.LINEAR_CONNECTOR,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "jira-connector",
|
id: "jira-connector",
|
||||||
title: "Jira",
|
title: "Jira",
|
||||||
|
|
|
||||||
|
|
@ -472,7 +472,7 @@ export const useConnectorDialog = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-start indexing for non-OAuth reindexable connectors
|
// 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
|
// 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
|
// Backend will use default date ranges (365 days ago to today) if dates are not provided
|
||||||
if (connector.is_indexable) {
|
if (connector.is_indexable) {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ export const editConnectorSchema = z.object({
|
||||||
SEARXNG_LANGUAGE: z.string().optional(),
|
SEARXNG_LANGUAGE: z.string().optional(),
|
||||||
SEARXNG_SAFESEARCH: z.string().optional(),
|
SEARXNG_SAFESEARCH: z.string().optional(),
|
||||||
SEARXNG_VERIFY_SSL: z.string().optional(),
|
SEARXNG_VERIFY_SSL: z.string().optional(),
|
||||||
LINEAR_API_KEY: z.string().optional(),
|
|
||||||
LINKUP_API_KEY: z.string().optional(),
|
LINKUP_API_KEY: z.string().optional(),
|
||||||
DISCORD_BOT_TOKEN: z.string().optional(),
|
DISCORD_BOT_TOKEN: z.string().optional(),
|
||||||
CONFLUENCE_BASE_URL: z.string().optional(),
|
CONFLUENCE_BASE_URL: z.string().optional(),
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,6 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
SEARXNG_LANGUAGE: "",
|
SEARXNG_LANGUAGE: "",
|
||||||
SEARXNG_SAFESEARCH: "",
|
SEARXNG_SAFESEARCH: "",
|
||||||
SEARXNG_VERIFY_SSL: "",
|
SEARXNG_VERIFY_SSL: "",
|
||||||
LINEAR_API_KEY: "",
|
|
||||||
DISCORD_BOT_TOKEN: "",
|
DISCORD_BOT_TOKEN: "",
|
||||||
CONFLUENCE_BASE_URL: "",
|
CONFLUENCE_BASE_URL: "",
|
||||||
CONFLUENCE_EMAIL: "",
|
CONFLUENCE_EMAIL: "",
|
||||||
|
|
@ -134,7 +133,6 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
config.SEARXNG_VERIFY_SSL !== undefined && config.SEARXNG_VERIFY_SSL !== null
|
config.SEARXNG_VERIFY_SSL !== undefined && config.SEARXNG_VERIFY_SSL !== null
|
||||||
? String(config.SEARXNG_VERIFY_SSL)
|
? String(config.SEARXNG_VERIFY_SSL)
|
||||||
: "",
|
: "",
|
||||||
LINEAR_API_KEY: config.LINEAR_API_KEY || "",
|
|
||||||
LINKUP_API_KEY: config.LINKUP_API_KEY || "",
|
LINKUP_API_KEY: config.LINKUP_API_KEY || "",
|
||||||
DISCORD_BOT_TOKEN: config.DISCORD_BOT_TOKEN || "",
|
DISCORD_BOT_TOKEN: config.DISCORD_BOT_TOKEN || "",
|
||||||
CONFLUENCE_BASE_URL: config.CONFLUENCE_BASE_URL || "",
|
CONFLUENCE_BASE_URL: config.CONFLUENCE_BASE_URL || "",
|
||||||
|
|
@ -384,16 +382,6 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
break;
|
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":
|
case "LINKUP_API":
|
||||||
if (formData.LINKUP_API_KEY !== originalConfig.LINKUP_API_KEY) {
|
if (formData.LINKUP_API_KEY !== originalConfig.LINKUP_API_KEY) {
|
||||||
if (!formData.LINKUP_API_KEY) {
|
if (!formData.LINKUP_API_KEY) {
|
||||||
|
|
@ -599,8 +587,6 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
"SEARXNG_VERIFY_SSL",
|
"SEARXNG_VERIFY_SSL",
|
||||||
verifyValue === null ? "" : String(verifyValue)
|
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") {
|
} else if (connector.connector_type === "LINKUP_API") {
|
||||||
editForm.setValue("LINKUP_API_KEY", newlySavedConfig.LINKUP_API_KEY || "");
|
editForm.setValue("LINKUP_API_KEY", newlySavedConfig.LINKUP_API_KEY || "");
|
||||||
} else if (connector.connector_type === "DISCORD_CONNECTOR") {
|
} else if (connector.connector_type === "DISCORD_CONNECTOR") {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue