mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-25 19:15:18 +02:00
select repos when adding gh connector
This commit is contained in:
parent
2b7a1b1082
commit
ae8c74a5aa
4 changed files with 330 additions and 138 deletions
|
|
@ -9,7 +9,7 @@ POST /search-source-connectors/{connector_id}/index - Index content from a conne
|
||||||
|
|
||||||
Note: Each user can have only one connector of each type (SERPER_API, TAVILY_API, SLACK_CONNECTOR, NOTION_CONNECTOR).
|
Note: Each user can have only one connector of each type (SERPER_API, TAVILY_API, SLACK_CONNECTOR, NOTION_CONNECTOR).
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
|
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks, Body
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.future import select
|
from sqlalchemy.future import select
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
@ -18,8 +18,9 @@ from app.db import get_async_session, User, SearchSourceConnector, SearchSourceC
|
||||||
from app.schemas import SearchSourceConnectorCreate, SearchSourceConnectorUpdate, SearchSourceConnectorRead
|
from app.schemas import SearchSourceConnectorCreate, SearchSourceConnectorUpdate, SearchSourceConnectorRead
|
||||||
from app.users import current_active_user
|
from app.users import current_active_user
|
||||||
from app.utils.check_ownership import check_ownership
|
from app.utils.check_ownership import check_ownership
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError, BaseModel, Field
|
||||||
from app.tasks.connectors_indexing_tasks import index_slack_messages, index_notion_pages, index_github_repos
|
from app.tasks.connectors_indexing_tasks import index_slack_messages, index_notion_pages, index_github_repos
|
||||||
|
from app.connectors.github_connector import GitHubConnector
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
@ -28,6 +29,34 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
# --- New Schema for GitHub PAT ---
|
||||||
|
class GitHubPATRequest(BaseModel):
|
||||||
|
github_pat: str = Field(..., description="GitHub Personal Access Token")
|
||||||
|
|
||||||
|
# --- New Endpoint to list GitHub Repositories ---
|
||||||
|
@router.post("/github/repositories/", response_model=List[Dict[str, Any]])
|
||||||
|
async def list_github_repositories(
|
||||||
|
pat_request: GitHubPATRequest,
|
||||||
|
user: User = Depends(current_active_user) # Ensure the user is logged in
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Fetches a list of repositories accessible by the provided GitHub PAT.
|
||||||
|
The PAT is used for this request only and is not stored.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Initialize GitHubConnector with the provided PAT
|
||||||
|
github_client = GitHubConnector(token=pat_request.github_pat)
|
||||||
|
# Fetch repositories
|
||||||
|
repositories = github_client.get_user_repositories()
|
||||||
|
return repositories
|
||||||
|
except ValueError as e:
|
||||||
|
# Handle invalid token error specifically
|
||||||
|
logger.error(f"GitHub PAT validation failed for user {user.id}: {str(e)}")
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid GitHub PAT: {str(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to fetch GitHub repositories for user {user.id}: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to fetch GitHub repositories.")
|
||||||
|
|
||||||
@router.post("/search-source-connectors/", response_model=SearchSourceConnectorRead)
|
@router.post("/search-source-connectors/", response_model=SearchSourceConnectorRead)
|
||||||
async def create_search_source_connector(
|
async def create_search_source_connector(
|
||||||
connector: SearchSourceConnectorCreate,
|
connector: SearchSourceConnectorCreate,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ from typing import Dict, Any
|
||||||
from pydantic import BaseModel, field_validator
|
from pydantic import BaseModel, field_validator
|
||||||
from .base import IDModel, TimestampModel
|
from .base import IDModel, TimestampModel
|
||||||
from app.db import SearchSourceConnectorType
|
from app.db import SearchSourceConnectorType
|
||||||
from fastapi import HTTPException
|
|
||||||
|
|
||||||
class SearchSourceConnectorBase(BaseModel):
|
class SearchSourceConnectorBase(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
|
|
@ -59,8 +58,8 @@ class SearchSourceConnectorBase(BaseModel):
|
||||||
raise ValueError("NOTION_INTEGRATION_TOKEN cannot be empty")
|
raise ValueError("NOTION_INTEGRATION_TOKEN cannot be empty")
|
||||||
|
|
||||||
elif connector_type == SearchSourceConnectorType.GITHUB_CONNECTOR:
|
elif connector_type == SearchSourceConnectorType.GITHUB_CONNECTOR:
|
||||||
# For GITHUB_CONNECTOR, only allow GITHUB_PAT
|
# For GITHUB_CONNECTOR, only allow GITHUB_PAT and repo_full_names
|
||||||
allowed_keys = ["GITHUB_PAT"]
|
allowed_keys = ["GITHUB_PAT", "repo_full_names"]
|
||||||
if set(config.keys()) != set(allowed_keys):
|
if set(config.keys()) != set(allowed_keys):
|
||||||
raise ValueError(f"For GITHUB_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
|
raise ValueError(f"For GITHUB_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
|
||||||
|
|
||||||
|
|
@ -68,6 +67,11 @@ class SearchSourceConnectorBase(BaseModel):
|
||||||
if not config.get("GITHUB_PAT"):
|
if not config.get("GITHUB_PAT"):
|
||||||
raise ValueError("GITHUB_PAT cannot be empty")
|
raise ValueError("GITHUB_PAT cannot be empty")
|
||||||
|
|
||||||
|
# Ensure the repo_full_names is present and is a non-empty list
|
||||||
|
repo_full_names = config.get("repo_full_names")
|
||||||
|
if not isinstance(repo_full_names, list) or not repo_full_names:
|
||||||
|
raise ValueError("repo_full_names must be a non-empty list of strings")
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
class SearchSourceConnectorCreate(SearchSourceConnectorBase):
|
class SearchSourceConnectorCreate(SearchSourceConnectorBase):
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Optional, List, Dict, Any, Tuple
|
from typing import Optional, Tuple
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy.future import select
|
from sqlalchemy.future import select
|
||||||
|
|
@ -626,24 +626,26 @@ async def index_github_repos(
|
||||||
if not connector:
|
if not connector:
|
||||||
return 0, f"Connector with ID {connector_id} not found or is not a GitHub connector"
|
return 0, f"Connector with ID {connector_id} not found or is not a GitHub connector"
|
||||||
|
|
||||||
# 2. Get the GitHub PAT from the connector config
|
# 2. Get the GitHub PAT and selected repositories from the connector config
|
||||||
github_pat = connector.config.get("GITHUB_PAT")
|
github_pat = connector.config.get("GITHUB_PAT")
|
||||||
|
repo_full_names_to_index = connector.config.get("repo_full_names")
|
||||||
|
|
||||||
if not github_pat:
|
if not github_pat:
|
||||||
return 0, "GitHub Personal Access Token (PAT) not found in connector config"
|
return 0, "GitHub Personal Access Token (PAT) not found in connector config"
|
||||||
|
|
||||||
|
if not repo_full_names_to_index or not isinstance(repo_full_names_to_index, list):
|
||||||
|
return 0, "'repo_full_names' not found or is not a list in connector config"
|
||||||
|
|
||||||
# 3. Initialize GitHub connector client
|
# 3. Initialize GitHub connector client
|
||||||
try:
|
try:
|
||||||
github_client = GitHubConnector(token=github_pat)
|
github_client = GitHubConnector(token=github_pat)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return 0, f"Failed to initialize GitHub client: {str(e)}"
|
return 0, f"Failed to initialize GitHub client: {str(e)}"
|
||||||
|
|
||||||
# 4. Get list of accessible repositories
|
# 4. Validate selected repositories
|
||||||
repositories = github_client.get_user_repositories()
|
# For simplicity, we'll proceed with the list provided.
|
||||||
if not repositories:
|
# If a repo is inaccessible, get_repository_files will likely fail gracefully later.
|
||||||
logger.info("No accessible GitHub repositories found for the provided token.")
|
logger.info(f"Starting indexing for {len(repo_full_names_to_index)} selected repositories.")
|
||||||
return 0, "No accessible GitHub repositories found."
|
|
||||||
|
|
||||||
logger.info(f"Found {len(repositories)} repositories to potentially index.")
|
|
||||||
|
|
||||||
# 5. Get existing documents for this search space and connector type to prevent duplicates
|
# 5. Get existing documents for this search space and connector type to prevent duplicates
|
||||||
existing_docs_result = await session.execute(
|
existing_docs_result = await session.execute(
|
||||||
|
|
@ -658,11 +660,10 @@ async def index_github_repos(
|
||||||
existing_docs_lookup = {doc.document_metadata.get("full_path"): doc for doc in existing_docs if doc.document_metadata.get("full_path")}
|
existing_docs_lookup = {doc.document_metadata.get("full_path"): doc for doc in existing_docs if doc.document_metadata.get("full_path")}
|
||||||
logger.info(f"Found {len(existing_docs_lookup)} existing GitHub documents in database for search space {search_space_id}")
|
logger.info(f"Found {len(existing_docs_lookup)} existing GitHub documents in database for search space {search_space_id}")
|
||||||
|
|
||||||
# 6. Iterate through repositories and index files
|
# 6. Iterate through selected repositories and index files
|
||||||
for repo_info in repositories:
|
for repo_full_name in repo_full_names_to_index:
|
||||||
repo_full_name = repo_info.get("full_name")
|
if not repo_full_name or not isinstance(repo_full_name, str):
|
||||||
if not repo_full_name:
|
logger.warning(f"Skipping invalid repository entry: {repo_full_name}")
|
||||||
logger.warning(f"Skipping repository with missing full_name: {repo_info.get('name')}")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.info(f"Processing repository: {repo_full_name}")
|
logger.info(f"Processing repository: {repo_full_name}")
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ArrowLeft, Check, Info, Loader2, Github } from "lucide-react";
|
import { ArrowLeft, Check, Info, Loader2, Github, CircleAlert, ListChecks } from "lucide-react";
|
||||||
|
|
||||||
// Assuming useSearchSourceConnectors hook exists and works similarly
|
// Assuming useSearchSourceConnectors hook exists and works similarly
|
||||||
import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
|
import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
|
||||||
|
|
@ -42,9 +42,10 @@ import {
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
|
||||||
// Define the form schema with Zod for GitHub
|
// Define the form schema with Zod for GitHub PAT entry step
|
||||||
const githubConnectorFormSchema = z.object({
|
const githubPatFormSchema = z.object({
|
||||||
name: z.string().min(3, {
|
name: z.string().min(3, {
|
||||||
message: "Connector name must be at least 3 characters.",
|
message: "Connector name must be at least 3 characters.",
|
||||||
}),
|
}),
|
||||||
|
|
@ -58,61 +59,144 @@ const githubConnectorFormSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
// Define the type for the form values
|
// Define the type for the form values
|
||||||
type GithubConnectorFormValues = z.infer<typeof githubConnectorFormSchema>;
|
type GithubPatFormValues = z.infer<typeof githubPatFormSchema>;
|
||||||
|
|
||||||
|
// Type for fetched GitHub repositories
|
||||||
|
interface GithubRepo {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
full_name: string;
|
||||||
|
private: boolean;
|
||||||
|
url: string;
|
||||||
|
description: string | null;
|
||||||
|
last_updated: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export default function GithubConnectorPage() {
|
export default function GithubConnectorPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const searchSpaceId = params.search_space_id as string;
|
const searchSpaceId = params.search_space_id as string;
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [step, setStep] = useState<'enter_pat' | 'select_repos'>('enter_pat');
|
||||||
const { createConnector } = useSearchSourceConnectors(); // Assuming this hook exists
|
const [isFetchingRepos, setIsFetchingRepos] = useState(false);
|
||||||
|
const [isCreatingConnector, setIsCreatingConnector] = useState(false);
|
||||||
|
const [repositories, setRepositories] = useState<GithubRepo[]>([]);
|
||||||
|
const [selectedRepos, setSelectedRepos] = useState<string[]>([]);
|
||||||
|
const [connectorName, setConnectorName] = useState<string>("GitHub Connector");
|
||||||
|
const [validatedPat, setValidatedPat] = useState<string>(""); // Store the validated PAT
|
||||||
|
|
||||||
// Initialize the form
|
const { createConnector } = useSearchSourceConnectors();
|
||||||
const form = useForm<GithubConnectorFormValues>({
|
|
||||||
resolver: zodResolver(githubConnectorFormSchema),
|
// Initialize the form for PAT entry
|
||||||
|
const form = useForm<GithubPatFormValues>({
|
||||||
|
resolver: zodResolver(githubPatFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "GitHub Connector",
|
name: connectorName,
|
||||||
github_pat: "",
|
github_pat: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle form submission
|
// Function to fetch repositories using the new backend endpoint
|
||||||
const onSubmit = async (values: GithubConnectorFormValues) => {
|
const fetchRepositories = async (values: GithubPatFormValues) => {
|
||||||
setIsSubmitting(true);
|
setIsFetchingRepos(true);
|
||||||
|
setConnectorName(values.name); // Store the name
|
||||||
|
setValidatedPat(values.github_pat); // Store the PAT temporarily
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('surfsense_bearer_token');
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('No authentication token found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/github/repositories/`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ github_pat: values.github_pat })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.detail || `Failed to fetch repositories: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: GithubRepo[] = await response.json();
|
||||||
|
setRepositories(data);
|
||||||
|
setStep('select_repos'); // Move to the next step
|
||||||
|
toast.success(`Found ${data.length} repositories.`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching GitHub repositories:", error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Failed to fetch repositories. Please check the PAT and try again.";
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setIsFetchingRepos(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle final connector creation
|
||||||
|
const handleCreateConnector = async () => {
|
||||||
|
if (selectedRepos.length === 0) {
|
||||||
|
toast.warning("Please select at least one repository to index.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCreatingConnector(true);
|
||||||
try {
|
try {
|
||||||
await createConnector({
|
await createConnector({
|
||||||
name: values.name,
|
name: connectorName, // Use the stored name
|
||||||
connector_type: "GITHUB_CONNECTOR",
|
connector_type: "GITHUB_CONNECTOR",
|
||||||
config: {
|
config: {
|
||||||
GITHUB_PAT: values.github_pat,
|
GITHUB_PAT: validatedPat, // Use the stored validated PAT
|
||||||
|
repo_full_names: selectedRepos, // Add the selected repo names
|
||||||
},
|
},
|
||||||
is_indexable: true, // GitHub connector is indexable
|
is_indexable: true,
|
||||||
last_indexed_at: null, // New connector hasn't been indexed
|
last_indexed_at: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success("GitHub connector created successfully!");
|
toast.success("GitHub connector created successfully!");
|
||||||
|
|
||||||
// Navigate back to connectors management page (or the add page)
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
||||||
} catch (error) { // Added type check for error
|
} catch (error) {
|
||||||
console.error("Error creating GitHub connector:", error);
|
console.error("Error creating GitHub connector:", error);
|
||||||
// Display specific backend error message if available
|
const errorMessage = error instanceof Error ? error.message : "Failed to create GitHub connector.";
|
||||||
const errorMessage = error instanceof Error ? error.message : "Failed to create GitHub connector. Please check the PAT and permissions.";
|
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsCreatingConnector(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle checkbox changes
|
||||||
|
const handleRepoSelection = (repoFullName: string, checked: boolean) => {
|
||||||
|
setSelectedRepos(prev =>
|
||||||
|
checked
|
||||||
|
? [...prev, repoFullName]
|
||||||
|
: prev.filter(name => name !== repoFullName)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
<div className="container mx-auto py-8 max-w-3xl">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="mb-6"
|
className="mb-6"
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
onClick={() => {
|
||||||
|
if (step === 'select_repos') {
|
||||||
|
// Go back to PAT entry, clear sensitive/fetched data
|
||||||
|
setStep('enter_pat');
|
||||||
|
setRepositories([]);
|
||||||
|
setSelectedRepos([]);
|
||||||
|
setValidatedPat("");
|
||||||
|
// Reset form PAT field, keep name
|
||||||
|
form.reset({ name: connectorName, github_pat: "" });
|
||||||
|
} else {
|
||||||
|
router.push(`/dashboard/${searchSpaceId}/connectors/add`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
Back to Add Connectors
|
{step === 'select_repos' ? "Back to PAT Entry" : "Back to Add Connectors"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
@ -129,97 +213,174 @@ export default function GithubConnectorPage() {
|
||||||
<TabsContent value="connect">
|
<TabsContent value="connect">
|
||||||
<Card className="border-2 border-border">
|
<Card className="border-2 border-border">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl font-bold flex items-center gap-2"><Github className="h-6 w-6" /> Connect GitHub Account</CardTitle>
|
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
||||||
|
{step === 'enter_pat' ? <Github className="h-6 w-6" /> : <ListChecks className="h-6 w-6" />}
|
||||||
|
{step === 'enter_pat' ? "Connect GitHub Account" : "Select Repositories to Index"}
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Integrate with GitHub using a Personal Access Token (PAT) to search and retrieve information from accessible repositories. This connector can index your code and documentation.
|
{step === 'enter_pat'
|
||||||
|
? "Provide a name and GitHub Personal Access Token (PAT) to fetch accessible repositories."
|
||||||
|
: `Select which repositories you want SurfSense to index for search. Found ${repositories.length} repositories accessible via your PAT.`
|
||||||
|
}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
|
||||||
<Alert className="mb-6 bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>GitHub Personal Access Token (PAT) Required</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
You'll need a GitHub PAT with the appropriate scopes (e.g., 'repo') to use this connector. You can create one from your
|
|
||||||
<a
|
|
||||||
href="https://github.com/settings/personal-access-tokens"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4 ml-1"
|
|
||||||
>
|
|
||||||
GitHub Developer Settings
|
|
||||||
</a>.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
{step === 'enter_pat' && (
|
||||||
<FormField
|
<CardContent>
|
||||||
control={form.control}
|
<Alert className="mb-6 bg-muted">
|
||||||
name="name"
|
<Info className="h-4 w-4" />
|
||||||
render={({ field }) => (
|
<AlertTitle>GitHub Personal Access Token (PAT) Required</AlertTitle>
|
||||||
<FormItem>
|
<AlertDescription>
|
||||||
<FormLabel>Connector Name</FormLabel>
|
You'll need a GitHub PAT with the appropriate scopes (e.g., 'repo') to fetch repositories. You can create one from your{' '}
|
||||||
<FormControl>
|
<a
|
||||||
<Input placeholder="My GitHub Connector" {...field} />
|
href="https://github.com/settings/personal-access-tokens"
|
||||||
</FormControl>
|
target="_blank"
|
||||||
<FormDescription>
|
rel="noopener noreferrer"
|
||||||
A friendly name to identify this GitHub connection.
|
className="font-medium underline underline-offset-4"
|
||||||
</FormDescription>
|
>
|
||||||
<FormMessage />
|
GitHub Developer Settings
|
||||||
</FormItem>
|
</a>. The PAT will be used to fetch repositories and then stored securely to enable indexing.
|
||||||
)}
|
</AlertDescription>
|
||||||
/>
|
</Alert>
|
||||||
|
|
||||||
<FormField
|
<form onSubmit={form.handleSubmit(fetchRepositories)} className="space-y-6">
|
||||||
control={form.control}
|
<FormField
|
||||||
name="github_pat"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="name"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>GitHub Personal Access Token (PAT)</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>Connector Name</FormLabel>
|
||||||
<Input
|
<FormControl>
|
||||||
type="password"
|
<Input placeholder="My GitHub Connector" {...field} />
|
||||||
placeholder="ghp_... or github_pat_..."
|
</FormControl>
|
||||||
{...field}
|
<FormDescription>
|
||||||
/>
|
A friendly name to identify this GitHub connection.
|
||||||
</FormControl>
|
</FormDescription>
|
||||||
<FormDescription>
|
<FormMessage />
|
||||||
Your GitHub PAT will be encrypted and stored securely. Ensure it has the necessary 'repo' scopes.
|
</FormItem>
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="w-full sm:w-auto"
|
|
||||||
>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
Connecting...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Check className="mr-2 h-4 w-4" />
|
|
||||||
Connect GitHub
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Button>
|
/>
|
||||||
</div>
|
|
||||||
</form>
|
<FormField
|
||||||
</Form>
|
control={form.control}
|
||||||
</CardContent>
|
name="github_pat"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>GitHub Personal Access Token (PAT)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="ghp_... or github_pat_..."
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Enter your GitHub PAT here to fetch your repositories. It will be stored encrypted later.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isFetchingRepos}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
{isFetchingRepos ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Fetching Repositories...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Fetch Repositories"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 'select_repos' && (
|
||||||
|
<CardContent>
|
||||||
|
{repositories.length === 0 ? (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<CircleAlert className="h-4 w-4" />
|
||||||
|
<AlertTitle>No Repositories Found</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
No repositories were found or accessible with the provided PAT. Please check the token and its permissions, then go back and try again.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<FormLabel>Repositories ({selectedRepos.length} selected)</FormLabel>
|
||||||
|
<div className="h-64 w-full rounded-md border p-4 overflow-y-auto">
|
||||||
|
{repositories.map((repo) => (
|
||||||
|
<div key={repo.id} className="flex items-center space-x-2 mb-2 py-1">
|
||||||
|
<Checkbox
|
||||||
|
id={`repo-${repo.id}`}
|
||||||
|
checked={selectedRepos.includes(repo.full_name)}
|
||||||
|
onCheckedChange={(checked) => handleRepoSelection(repo.full_name, !!checked)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={`repo-${repo.id}`}
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
{repo.full_name} {repo.private && "(Private)"}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FormDescription>
|
||||||
|
Select the repositories you wish to index. Only checked repositories will be processed.
|
||||||
|
</FormDescription>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center pt-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setStep('enter_pat');
|
||||||
|
setRepositories([]);
|
||||||
|
setSelectedRepos([]);
|
||||||
|
setValidatedPat("");
|
||||||
|
form.reset({ name: connectorName, github_pat: "" });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateConnector}
|
||||||
|
disabled={isCreatingConnector || selectedRepos.length === 0}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
{isCreatingConnector ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Creating Connector...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Check className="mr-2 h-4 w-4" />
|
||||||
|
Create Connector
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
|
||||||
<CardFooter className="flex flex-col items-start border-t bg-muted/50 px-6 py-4">
|
<CardFooter className="flex flex-col items-start border-t bg-muted/50 px-6 py-4">
|
||||||
<h4 className="text-sm font-medium">What you get with GitHub integration:</h4>
|
<h4 className="text-sm font-medium">What you get with GitHub integration:</h4>
|
||||||
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
||||||
<li>Search through code and documentation in your repositories</li>
|
<li>Search through code and documentation in your selected repositories</li>
|
||||||
<li>Access READMEs, Markdown files, and common code files</li>
|
<li>Access READMEs, Markdown files, and common code files</li>
|
||||||
<li>Connect your project knowledge directly to your search space</li>
|
<li>Connect your project knowledge directly to your search space</li>
|
||||||
<li>Index your repositories for enhanced search capabilities</li>
|
<li>Index your selected repositories for enhanced search capabilities</li>
|
||||||
</ul>
|
</ul>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -237,27 +398,20 @@ export default function GithubConnectorPage() {
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-semibold mb-2">How it works</h3>
|
<h3 className="text-xl font-semibold mb-2">How it works</h3>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
The GitHub connector uses a Personal Access Token (PAT) to authenticate with the GitHub API. It fetches information about repositories accessible to the token and indexes relevant files (code, markdown, text).
|
The GitHub connector uses a Personal Access Token (PAT) to authenticate with the GitHub API. First, it fetches a list of repositories accessible to the token. You then select which repositories you want to index. The connector indexes relevant files (code, markdown, text) from only the selected repositories.
|
||||||
</p>
|
</p>
|
||||||
<ul className="mt-2 list-disc pl-5 text-muted-foreground">
|
<ul className="mt-2 list-disc pl-5 text-muted-foreground">
|
||||||
<li>The connector indexes files based on common code and documentation extensions.</li>
|
<li>The connector indexes files based on common code and documentation extensions.</li>
|
||||||
<li>Large files (over 1MB) are skipped during indexing.</li>
|
<li>Large files (over 1MB) are skipped during indexing.</li>
|
||||||
|
<li>Only selected repositories are indexed.</li>
|
||||||
<li>Indexing runs periodically (check connector settings for frequency) to keep content up-to-date.</li>
|
<li>Indexing runs periodically (check connector settings for frequency) to keep content up-to-date.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
<Accordion type="single" collapsible className="w-full">
|
||||||
<AccordionItem value="create_pat">
|
<AccordionItem value="create_pat">
|
||||||
<AccordionTrigger className="text-lg font-medium">Step 1: Create a GitHub PAT</AccordionTrigger>
|
<AccordionTrigger className="text-lg font-medium">Step 1: Generate GitHub PAT</AccordionTrigger>
|
||||||
<AccordionContent className="space-y-4">
|
<AccordionContent>
|
||||||
<Alert className="bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Token Security</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Treat your PAT like a password. Store it securely and consider using fine-grained tokens if possible.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium mb-2">Generating a Token:</h4>
|
<h4 className="font-medium mb-2">Generating a Token:</h4>
|
||||||
|
|
@ -280,9 +434,13 @@ export default function GithubConnectorPage() {
|
||||||
<AccordionTrigger className="text-lg font-medium">Step 2: Connect in SurfSense</AccordionTrigger>
|
<AccordionTrigger className="text-lg font-medium">Step 2: Connect in SurfSense</AccordionTrigger>
|
||||||
<AccordionContent className="space-y-4">
|
<AccordionContent className="space-y-4">
|
||||||
<ol className="list-decimal pl-5 space-y-3">
|
<ol className="list-decimal pl-5 space-y-3">
|
||||||
<li>Paste the copied GitHub PAT into the "GitHub Personal Access Token (PAT)" field on the "Connect GitHub" tab.</li>
|
<li>Navigate to the "Connect GitHub" tab.</li>
|
||||||
<li>Optionally, give the connector a custom name.</li>
|
<li>Enter a name for your connector.</li>
|
||||||
<li>Click the <strong>Connect GitHub</strong> button.</li>
|
<li>Paste the copied GitHub PAT into the "GitHub Personal Access Token (PAT)" field.</li>
|
||||||
|
<li>Click <strong>Fetch Repositories</strong>.</li>
|
||||||
|
<li>If the PAT is valid, you'll see a list of your accessible repositories.</li>
|
||||||
|
<li>Select the repositories you want SurfSense to index using the checkboxes.</li>
|
||||||
|
<li>Click the <strong>Create Connector</strong> button.</li>
|
||||||
<li>If the connection is successful, you will be redirected and can start indexing from the Connectors page.</li>
|
<li>If the connection is successful, you will be redirected and can start indexing from the Connectors page.</li>
|
||||||
</ol>
|
</ol>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue