mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-09 07:42:39 +02:00
feat: Remove multiple connector components and streamline source addition process, updating UI elements for improved user experience and consistency.
This commit is contained in:
parent
3a6749e86f
commit
63968e6455
32 changed files with 14 additions and 8331 deletions
|
|
@ -1,336 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ArrowLeft, Check, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { EditConnectorLoadingSkeleton } from "@/components/editConnector/EditConnectorLoadingSkeleton";
|
|
||||||
import { EditConnectorNameForm } from "@/components/editConnector/EditConnectorNameForm";
|
|
||||||
import { EditGitHubConnectorConfig } from "@/components/editConnector/EditGitHubConnectorConfig";
|
|
||||||
import { EditSimpleTokenForm } from "@/components/editConnector/EditSimpleTokenForm";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
import { useConnectorEditPage } from "@/hooks/use-connector-edit-page";
|
|
||||||
// Import Utils, Types, Hook, and Components
|
|
||||||
import { getConnectorTypeDisplay } from "@/lib/connectors/utils";
|
|
||||||
|
|
||||||
export default function EditConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
// Ensure connectorId is parsed safely
|
|
||||||
const connectorIdParam = params.connector_id as string;
|
|
||||||
const connectorId = connectorIdParam ? parseInt(connectorIdParam, 10) : NaN;
|
|
||||||
|
|
||||||
// Use the custom hook to manage state and logic
|
|
||||||
const {
|
|
||||||
connectorsLoading,
|
|
||||||
connector,
|
|
||||||
isSaving,
|
|
||||||
editForm,
|
|
||||||
patForm, // Needed for GitHub child component
|
|
||||||
handleSaveChanges,
|
|
||||||
// GitHub specific props for the child component
|
|
||||||
editMode,
|
|
||||||
setEditMode, // Pass down if needed by GitHub component
|
|
||||||
originalPat,
|
|
||||||
currentSelectedRepos,
|
|
||||||
fetchedRepos,
|
|
||||||
setFetchedRepos,
|
|
||||||
newSelectedRepos,
|
|
||||||
setNewSelectedRepos,
|
|
||||||
isFetchingRepos,
|
|
||||||
handleFetchRepositories,
|
|
||||||
handleRepoSelectionChange,
|
|
||||||
} = useConnectorEditPage(connectorId, searchSpaceId);
|
|
||||||
|
|
||||||
// Redirect if connectorId is not a valid number after parsing
|
|
||||||
useEffect(() => {
|
|
||||||
if (Number.isNaN(connectorId)) {
|
|
||||||
toast.error("Invalid Connector ID.");
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
}
|
|
||||||
}, [connectorId, router, searchSpaceId]);
|
|
||||||
|
|
||||||
// Loading State
|
|
||||||
if (connectorsLoading || !connector) {
|
|
||||||
// Handle NaN case before showing skeleton
|
|
||||||
if (Number.isNaN(connectorId)) return null;
|
|
||||||
return <EditConnectorLoadingSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main Render using data/handlers from the hook
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors`)}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" /> Back to Connectors
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
|
||||||
{getConnectorIcon(connector.connector_type)}
|
|
||||||
Edit {getConnectorTypeDisplay(connector.connector_type)} Connector
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>Modify connector name and configuration.</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<Form {...editForm}>
|
|
||||||
{/* Pass hook's handleSaveChanges */}
|
|
||||||
<form onSubmit={editForm.handleSubmit(handleSaveChanges)} className="space-y-6">
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
{/* Pass form control from hook */}
|
|
||||||
<EditConnectorNameForm control={editForm.control} />
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<h3 className="text-lg font-semibold">Configuration</h3>
|
|
||||||
|
|
||||||
{/* == GitHub == */}
|
|
||||||
{connector.connector_type === "GITHUB_CONNECTOR" && (
|
|
||||||
<EditGitHubConnectorConfig
|
|
||||||
// Pass relevant state and handlers from hook
|
|
||||||
editMode={editMode}
|
|
||||||
setEditMode={setEditMode} // Pass setter if child manages mode
|
|
||||||
originalPat={originalPat}
|
|
||||||
currentSelectedRepos={currentSelectedRepos}
|
|
||||||
fetchedRepos={fetchedRepos}
|
|
||||||
newSelectedRepos={newSelectedRepos}
|
|
||||||
isFetchingRepos={isFetchingRepos}
|
|
||||||
patForm={patForm}
|
|
||||||
handleFetchRepositories={handleFetchRepositories}
|
|
||||||
handleRepoSelectionChange={handleRepoSelectionChange}
|
|
||||||
setNewSelectedRepos={setNewSelectedRepos}
|
|
||||||
setFetchedRepos={setFetchedRepos}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* == Slack == */}
|
|
||||||
{connector.connector_type === "SLACK_CONNECTOR" && (
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="SLACK_BOT_TOKEN"
|
|
||||||
fieldLabel="Slack Bot Token"
|
|
||||||
fieldDescription="Update the Slack Bot Token if needed."
|
|
||||||
placeholder="Begins with xoxb-..."
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{/* == Notion == */}
|
|
||||||
{connector.connector_type === "NOTION_CONNECTOR" && (
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="NOTION_INTEGRATION_TOKEN"
|
|
||||||
fieldLabel="Notion Integration Token"
|
|
||||||
fieldDescription="Update the Notion Integration Token if needed."
|
|
||||||
placeholder="Begins with secret_..."
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{/* == Tavily == */}
|
|
||||||
{connector.connector_type === "TAVILY_API" && (
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="TAVILY_API_KEY"
|
|
||||||
fieldLabel="Tavily API Key"
|
|
||||||
fieldDescription="Update the Tavily API Key if needed."
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* == Linear == */}
|
|
||||||
{connector.connector_type === "LINEAR_CONNECTOR" && (
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="LINEAR_API_KEY"
|
|
||||||
fieldLabel="Linear API Key"
|
|
||||||
fieldDescription="Update your Linear API Key if needed."
|
|
||||||
placeholder="Begins with lin_api_..."
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* == Jira == */}
|
|
||||||
{connector.connector_type === "JIRA_CONNECTOR" && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="JIRA_BASE_URL"
|
|
||||||
fieldLabel="Jira Base URL"
|
|
||||||
fieldDescription="Update your Jira instance URL if needed."
|
|
||||||
placeholder="https://yourcompany.atlassian.net"
|
|
||||||
/>
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="JIRA_EMAIL"
|
|
||||||
fieldLabel="Jira Email"
|
|
||||||
fieldDescription="Update your Atlassian account email if needed."
|
|
||||||
placeholder="your.email@company.com"
|
|
||||||
/>
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="JIRA_API_TOKEN"
|
|
||||||
fieldLabel="Jira API Token"
|
|
||||||
fieldDescription="Update your Jira API Token if needed."
|
|
||||||
placeholder="Your Jira API Token"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* == Confluence == */}
|
|
||||||
{connector.connector_type === "CONFLUENCE_CONNECTOR" && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="CONFLUENCE_BASE_URL"
|
|
||||||
fieldLabel="Confluence Base URL"
|
|
||||||
fieldDescription="Update your Confluence instance URL if needed."
|
|
||||||
placeholder="https://yourcompany.atlassian.net"
|
|
||||||
/>
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="CONFLUENCE_EMAIL"
|
|
||||||
fieldLabel="Confluence Email"
|
|
||||||
fieldDescription="Update your Atlassian account email if needed."
|
|
||||||
placeholder="your.email@company.com"
|
|
||||||
/>
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="CONFLUENCE_API_TOKEN"
|
|
||||||
fieldLabel="Confluence API Token"
|
|
||||||
fieldDescription="Update your Confluence API Token if needed."
|
|
||||||
placeholder="Your Confluence API Token"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* == ClickUp == */}
|
|
||||||
{connector.connector_type === "CLICKUP_CONNECTOR" && (
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="CLICKUP_API_TOKEN"
|
|
||||||
fieldLabel="ClickUp API Token"
|
|
||||||
fieldDescription="Update your ClickUp API Token if needed."
|
|
||||||
placeholder="pk_..."
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* == Linkup == */}
|
|
||||||
{connector.connector_type === "LINKUP_API" && (
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="LINKUP_API_KEY"
|
|
||||||
fieldLabel="Linkup API Key"
|
|
||||||
fieldDescription="Update your Linkup API Key if needed."
|
|
||||||
placeholder="Begins with linkup_..."
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* == Discord == */}
|
|
||||||
{connector.connector_type === "DISCORD_CONNECTOR" && (
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="DISCORD_BOT_TOKEN"
|
|
||||||
fieldLabel="Discord Bot Token"
|
|
||||||
fieldDescription="Update the Discord Bot Token if needed."
|
|
||||||
placeholder="Bot token..."
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* == Luma == */}
|
|
||||||
{connector.connector_type === "LUMA_CONNECTOR" && (
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="LUMA_API_KEY"
|
|
||||||
fieldLabel="Luma API Key"
|
|
||||||
fieldDescription="Update the Luma API Key if needed."
|
|
||||||
placeholder="API Key..."
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* == Elasticsearch == */}
|
|
||||||
{connector.connector_type === "ELASTICSEARCH_CONNECTOR" && (
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="ELASTICSEARCH_API_KEY"
|
|
||||||
fieldLabel="Elasticsearch API Key"
|
|
||||||
fieldDescription="Update your Elasticsearch API Key if needed."
|
|
||||||
placeholder="Your Elasticsearch API Key"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* == Webcrawler == */}
|
|
||||||
{connector.connector_type === "WEBCRAWLER_CONNECTOR" && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<EditSimpleTokenForm
|
|
||||||
control={editForm.control}
|
|
||||||
fieldName="FIRECRAWL_API_KEY"
|
|
||||||
fieldLabel="Firecrawl API Key (Optional)"
|
|
||||||
fieldDescription="Add a Firecrawl API key for enhanced crawling capabilities. If not provided, will use AsyncChromiumLoader as fallback."
|
|
||||||
placeholder="fc-xxxxxxxxxxxxx"
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={editForm.control}
|
|
||||||
name="INITIAL_URLS"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>URLs to Crawl</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
placeholder="https://example.com https://docs.example.com https://blog.example.com"
|
|
||||||
className="min-h-[150px] font-mono text-sm"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Enter URLs to crawl (one per line). These URLs will be indexed when you
|
|
||||||
trigger indexing.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="border-t pt-6">
|
|
||||||
<Button type="submit" disabled={isSaving} className="w-full sm:w-auto">
|
|
||||||
{isSaving ? (
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Check className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,301 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { updateConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import type { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import type { SearchSourceConnector } from "@/hooks/use-search-source-connectors";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
|
||||||
const apiConnectorFormSchema = z.object({
|
|
||||||
name: z.string().min(3, {
|
|
||||||
message: "Connector name must be at least 3 characters.",
|
|
||||||
}),
|
|
||||||
api_key: z.string().min(10, {
|
|
||||||
message: "API key is required and must be valid.",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper function to get connector type display name
|
|
||||||
const getConnectorTypeDisplay = (type: string): string => {
|
|
||||||
const typeMap: Record<string, string> = {
|
|
||||||
TAVILY_API: "Tavily API",
|
|
||||||
SLACK_CONNECTOR: "Slack Connector",
|
|
||||||
NOTION_CONNECTOR: "Notion Connector",
|
|
||||||
GITHUB_CONNECTOR: "GitHub Connector",
|
|
||||||
LINEAR_CONNECTOR: "Linear Connector",
|
|
||||||
JIRA_CONNECTOR: "Jira Connector",
|
|
||||||
DISCORD_CONNECTOR: "Discord Connector",
|
|
||||||
LINKUP_API: "Linkup",
|
|
||||||
CONFLUENCE_CONNECTOR: "Confluence Connector",
|
|
||||||
CLICKUP_CONNECTOR: "ClickUp Connector",
|
|
||||||
GOOGLE_CALENDAR_CONNECTOR: "Google Calendar Connector",
|
|
||||||
GOOGLE_GMAIL_CONNECTOR: "Google Gmail Connector",
|
|
||||||
AIRTABLE_CONNECTOR: "Airtable Connector",
|
|
||||||
LUMA_CONNECTOR: "Luma Connector",
|
|
||||||
ELASTICSEARCH_CONNECTOR: "Elasticsearch Connector",
|
|
||||||
WEBCRAWLER_CONNECTOR: "Web Page Connector",
|
|
||||||
// Add other connector types here as needed
|
|
||||||
};
|
|
||||||
return typeMap[type] || type;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
type ApiConnectorFormValues = z.infer<typeof apiConnectorFormSchema>;
|
|
||||||
|
|
||||||
// Get API key field name based on connector type
|
|
||||||
const getApiKeyFieldName = (connectorType: string): string => {
|
|
||||||
const fieldMap: Record<string, string> = {
|
|
||||||
TAVILY_API: "TAVILY_API_KEY",
|
|
||||||
SLACK_CONNECTOR: "SLACK_BOT_TOKEN",
|
|
||||||
NOTION_CONNECTOR: "NOTION_INTEGRATION_TOKEN",
|
|
||||||
GITHUB_CONNECTOR: "GITHUB_PAT",
|
|
||||||
DISCORD_CONNECTOR: "DISCORD_BOT_TOKEN",
|
|
||||||
LINKUP_API: "LINKUP_API_KEY",
|
|
||||||
LUMA_CONNECTOR: "LUMA_API_KEY",
|
|
||||||
ELASTICSEARCH_CONNECTOR: "ELASTICSEARCH_API_KEY",
|
|
||||||
WEBCRAWLER_CONNECTOR: "FIRECRAWL_API_KEY",
|
|
||||||
};
|
|
||||||
return fieldMap[connectorType] || "";
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function EditConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const connectorId = parseInt(params.connector_id as string, 10);
|
|
||||||
|
|
||||||
const { data: connectors = [] } = useAtomValue(connectorsAtom);
|
|
||||||
const { mutateAsync: updateConnector } = useAtomValue(updateConnectorMutationAtom);
|
|
||||||
const [connector, setConnector] = useState<SearchSourceConnector | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
// console.log("connector", connector);
|
|
||||||
// Initialize the form
|
|
||||||
const form = useForm<ApiConnectorFormValues>({
|
|
||||||
resolver: zodResolver(apiConnectorFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "",
|
|
||||||
api_key: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const currentConnector = connectors.find((c) => c.id === connectorId);
|
|
||||||
|
|
||||||
if (currentConnector) {
|
|
||||||
setConnector(currentConnector);
|
|
||||||
|
|
||||||
const apiKeyField = getApiKeyFieldName(currentConnector.connector_type);
|
|
||||||
if (apiKeyField) {
|
|
||||||
form.reset({
|
|
||||||
name: currentConnector.name,
|
|
||||||
api_key: currentConnector.config[apiKeyField] || "",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast.error("This connector type is not supported for editing");
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(false);
|
|
||||||
} else if (!isLoading && connectors.length > 0) {
|
|
||||||
toast.error("Connector not found");
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
}
|
|
||||||
}, [connectors, connectorId, form, router, searchSpaceId, isLoading]);
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = async (values: ApiConnectorFormValues) => {
|
|
||||||
if (!connector) return;
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
const apiKeyField = getApiKeyFieldName(connector.connector_type);
|
|
||||||
|
|
||||||
const updatedConfig = { ...connector.config };
|
|
||||||
if (values.api_key) {
|
|
||||||
updatedConfig[apiKeyField] = values.api_key;
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateConnector({
|
|
||||||
id: connectorId,
|
|
||||||
data: {
|
|
||||||
name: values.name,
|
|
||||||
connector_type: connector.connector_type as EnumConnectorName,
|
|
||||||
config: updatedConfig,
|
|
||||||
is_indexable: connector.is_indexable,
|
|
||||||
last_indexed_at: connector.last_indexed_at,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("Connector updated successfully!");
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error updating connector:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to update connector");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl flex justify-center items-center min-h-[60vh]">
|
|
||||||
<div className="animate-pulse text-center">
|
|
||||||
<div className="h-8 w-48 bg-muted rounded mx-auto mb-4"></div>
|
|
||||||
<div className="h-4 w-64 bg-muted rounded mx-auto"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors`)}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to Connectors
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">
|
|
||||||
Edit {connector ? getConnectorTypeDisplay(connector.connector_type) : ""} Connector
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>Update your connector settings.</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Alert className="mb-6 bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>API Key Security</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Your API key is stored securely. For security reasons, we don't display your
|
|
||||||
existing API key. If you don't update the API key field, your existing key will be
|
|
||||||
preserved.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My API Connector" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>A friendly name to identify this connector.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="api_key"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{connector?.connector_type === "SLACK_CONNECTOR"
|
|
||||||
? "Slack Bot Token"
|
|
||||||
: connector?.connector_type === "NOTION_CONNECTOR"
|
|
||||||
? "Notion Integration Token"
|
|
||||||
: connector?.connector_type === "GITHUB_CONNECTOR"
|
|
||||||
? "GitHub Personal Access Token (PAT)"
|
|
||||||
: connector?.connector_type === "LINKUP_API"
|
|
||||||
? "Linkup API Key"
|
|
||||||
: connector?.connector_type === "ELASTICSEARCH_CONNECTOR"
|
|
||||||
? "Elasticsearch API Key"
|
|
||||||
: "API Key"}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder={
|
|
||||||
connector?.connector_type === "SLACK_CONNECTOR"
|
|
||||||
? "Enter new Slack Bot Token (optional)"
|
|
||||||
: connector?.connector_type === "NOTION_CONNECTOR"
|
|
||||||
? "Enter new Notion Token (optional)"
|
|
||||||
: connector?.connector_type === "GITHUB_CONNECTOR"
|
|
||||||
? "Enter new GitHub PAT (optional)"
|
|
||||||
: connector?.connector_type === "LINKUP_API"
|
|
||||||
? "Enter new Linkup API Key (optional)"
|
|
||||||
: connector?.connector_type === "ELASTICSEARCH_CONNECTOR"
|
|
||||||
? "Enter new Elasticsearch API Key (optional)"
|
|
||||||
: "Enter new API key (optional)"
|
|
||||||
}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{connector?.connector_type === "SLACK_CONNECTOR"
|
|
||||||
? "Enter a new Slack Bot Token or leave blank to keep your existing token."
|
|
||||||
: connector?.connector_type === "NOTION_CONNECTOR"
|
|
||||||
? "Enter a new Notion Integration Token or leave blank to keep your existing token."
|
|
||||||
: connector?.connector_type === "GITHUB_CONNECTOR"
|
|
||||||
? "Enter a new GitHub PAT or leave blank to keep your existing token."
|
|
||||||
: connector?.connector_type === "LINKUP_API"
|
|
||||||
? "Enter a new Linkup API Key or leave blank to keep your existing key."
|
|
||||||
: connector?.connector_type === "ELASTICSEARCH_CONNECTOR"
|
|
||||||
? "Enter a new Elasticsearch API Key or leave blank to keep your existing key."
|
|
||||||
: "Enter a new API key or leave blank to keep your existing key."}
|
|
||||||
</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" />
|
|
||||||
Updating...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Check className="mr-2 h-4 w-4" />
|
|
||||||
Update Connector
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, ExternalLink, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
// import { IconBrandAirtable } from "@tabler/icons-react";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
|
||||||
|
|
||||||
export default function AirtableConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
|
||||||
const [doesConnectorExist, setDoesConnectorExist] = useState(false);
|
|
||||||
|
|
||||||
const { refetch: fetchConnectors } = useAtomValue(connectorsAtom);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchConnectors().then((data) => {
|
|
||||||
const connectors = data.data || [];
|
|
||||||
const connector = connectors.find(
|
|
||||||
(c: SearchSourceConnector) => c.connector_type === EnumConnectorName.AIRTABLE_CONNECTOR
|
|
||||||
);
|
|
||||||
if (connector) {
|
|
||||||
setDoesConnectorExist(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleConnectAirtable = async () => {
|
|
||||||
setIsConnecting(true);
|
|
||||||
try {
|
|
||||||
const response = await authenticatedFetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/auth/airtable/connector/add/?space_id=${searchSpaceId}`,
|
|
||||||
{ method: "GET" }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to initiate Airtable OAuth");
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Redirect to Airtable for authentication
|
|
||||||
window.location.href = data.auth_url;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error connecting to Airtable:", error);
|
|
||||||
toast.error("Failed to connect to Airtable");
|
|
||||||
} finally {
|
|
||||||
setIsConnecting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-2xl">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<Link
|
|
||||||
href={`/dashboard/${searchSpaceId}/connectors/add`}
|
|
||||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground mb-4"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to connectors
|
|
||||||
</Link>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.AIRTABLE_CONNECTOR, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Airtable</h1>
|
|
||||||
<p className="text-muted-foreground">Connect your Airtable to search records.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* OAuth Connection Card */}
|
|
||||||
{!doesConnectorExist ? (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Connect Your Airtable Account</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Connect your Airtable account to access your records. We'll only request read-only
|
|
||||||
access to your records.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Read-only access to your records</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Access works even when you're offline</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>You can disconnect anytime</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex justify-between">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleConnectAirtable} disabled={isConnecting}>
|
|
||||||
{isConnecting ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
Connecting...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ExternalLink className="mr-2 h-4 w-4" />
|
|
||||||
Connect Your Airtable Account
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
/* Configuration Form Card */
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>✅ Your Airtable is successfully connected!</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Help Section */}
|
|
||||||
{!doesConnectorExist && (
|
|
||||||
<Card className="mt-6">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-lg">How It Works</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">1. Connect Your Account</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Click "Connect Your Airtable Account" to start the secure OAuth process. You'll be
|
|
||||||
redirected to Airtable to sign in.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">2. Grant Permissions</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Airtable will ask for permission to read your records. We only request read-only
|
|
||||||
access to keep your data safe.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,323 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
|
||||||
const baiduSearchApiFormSchema = z.object({
|
|
||||||
name: z.string().min(3, {
|
|
||||||
message: "Connector name must be at least 3 characters.",
|
|
||||||
}),
|
|
||||||
api_key: z.string().min(10, {
|
|
||||||
message: "API key is required and must be valid.",
|
|
||||||
}),
|
|
||||||
model: z.string().optional(),
|
|
||||||
search_source: z.enum(["baidu_search_v1", "baidu_search_v2"]).optional(),
|
|
||||||
enable_deep_search: z.boolean().default(false),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
type BaiduSearchApiFormValues = z.infer<typeof baiduSearchApiFormSchema>;
|
|
||||||
|
|
||||||
export default function BaiduSearchApiPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
|
|
||||||
// Initialize the form
|
|
||||||
const form = useForm<BaiduSearchApiFormValues>({
|
|
||||||
resolver: zodResolver(baiduSearchApiFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "Baidu Search Connector",
|
|
||||||
api_key: "",
|
|
||||||
model: "ernie-3.5-8k",
|
|
||||||
search_source: "baidu_search_v2",
|
|
||||||
enable_deep_search: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = async (values: BaiduSearchApiFormValues) => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
// Build config object
|
|
||||||
const config: Record<string, unknown> = {
|
|
||||||
BAIDU_API_KEY: values.api_key,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add optional parameters if provided
|
|
||||||
if (values.model) {
|
|
||||||
config.BAIDU_MODEL = values.model;
|
|
||||||
}
|
|
||||||
if (values.search_source) {
|
|
||||||
config.BAIDU_SEARCH_SOURCE = values.search_source;
|
|
||||||
}
|
|
||||||
if (values.enable_deep_search !== undefined) {
|
|
||||||
config.BAIDU_ENABLE_DEEP_SEARCH = values.enable_deep_search;
|
|
||||||
}
|
|
||||||
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
name: values.name,
|
|
||||||
connector_type: EnumConnectorName.BAIDU_SEARCH_API,
|
|
||||||
config,
|
|
||||||
is_indexable: false,
|
|
||||||
last_indexed_at: null,
|
|
||||||
periodic_indexing_enabled: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("Baidu Search connector created successfully!");
|
|
||||||
|
|
||||||
// Navigate back to connectors page
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating connector:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to Connectors
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.BAIDU_SEARCH_API, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Baidu Search</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Connect Baidu AI Search for intelligent Chinese web search capabilities.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">Connect Baidu Search</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Integrate with Baidu AI Search to enhance your search capabilities with intelligent
|
|
||||||
Chinese web search results.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Alert className="mb-6 bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>API Key Required</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
You'll need a Baidu AppBuilder API key to use this connector. You can get one by
|
|
||||||
signing up at{" "}
|
|
||||||
<a
|
|
||||||
href="https://qianfan.cloud.baidu.com/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
qianfan.cloud.baidu.com
|
|
||||||
</a>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My Baidu Search Connector" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>A friendly name to identify this connector.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="api_key"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Baidu AppBuilder API Key</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="password" placeholder="Enter your Baidu API key" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your API key will be encrypted and stored securely.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="model"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Model (Optional)</FormLabel>
|
|
||||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a model" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="ernie-3.5-8k">ERNIE 3.5 8K</SelectItem>
|
|
||||||
<SelectItem value="ernie-4.5-turbo-32k">ERNIE 4.5 Turbo 32K</SelectItem>
|
|
||||||
<SelectItem value="ernie-4.5-turbo-128k">ERNIE 4.5 Turbo 128K</SelectItem>
|
|
||||||
<SelectItem value="deepseek-v3">DeepSeek V3</SelectItem>
|
|
||||||
<SelectItem value="qwen3-235b-a22b-instruct-2507">Qwen3 235B</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormDescription>
|
|
||||||
The language model used for search summarization. Default: ERNIE 3.5 8K.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="search_source"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Search Source (Optional)</FormLabel>
|
|
||||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select search source" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="baidu_search_v1">Baidu Search V1</SelectItem>
|
|
||||||
<SelectItem value="baidu_search_v2">
|
|
||||||
Baidu Search V2 (Recommended)
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormDescription>
|
|
||||||
V2 provides better performance and richer content. Default: V2.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="enable_deep_search"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel className="text-base">Enable Deep Search</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Deep search retrieves up to 100 results per type (may incur additional
|
|
||||||
costs).
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
|
||||||
</FormControl>
|
|
||||||
</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 Baidu Search
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
<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 Baidu Search:</h4>
|
|
||||||
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
|
||||||
<li>Intelligent search tailored for Chinese web content</li>
|
|
||||||
<li>Real-time information from Baidu's search index</li>
|
|
||||||
<li>AI-powered summarization with source references</li>
|
|
||||||
<li>Support for web, video, and image search results</li>
|
|
||||||
</ul>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,306 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
|
||||||
const bookstackConnectorFormSchema = z.object({
|
|
||||||
name: z.string().min(3, {
|
|
||||||
message: "Connector name must be at least 3 characters.",
|
|
||||||
}),
|
|
||||||
base_url: z.string().url({
|
|
||||||
message: "Please enter a valid BookStack URL (e.g., https://docs.example.com)",
|
|
||||||
}),
|
|
||||||
token_id: z.string().min(10, {
|
|
||||||
message: "BookStack Token ID is required.",
|
|
||||||
}),
|
|
||||||
token_secret: z.string().min(10, {
|
|
||||||
message: "BookStack Token Secret is required.",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
type BookStackConnectorFormValues = z.infer<typeof bookstackConnectorFormSchema>;
|
|
||||||
|
|
||||||
export default function BookStackConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
|
|
||||||
// Initialize the form
|
|
||||||
const form = useForm<BookStackConnectorFormValues>({
|
|
||||||
resolver: zodResolver(bookstackConnectorFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "BookStack Connector",
|
|
||||||
base_url: "",
|
|
||||||
token_id: "",
|
|
||||||
token_secret: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = async (values: BookStackConnectorFormValues) => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
name: values.name,
|
|
||||||
connector_type: EnumConnectorName.BOOKSTACK_CONNECTOR,
|
|
||||||
config: {
|
|
||||||
BOOKSTACK_BASE_URL: values.base_url,
|
|
||||||
BOOKSTACK_TOKEN_ID: values.token_id,
|
|
||||||
BOOKSTACK_TOKEN_SECRET: values.token_secret,
|
|
||||||
},
|
|
||||||
is_indexable: true,
|
|
||||||
last_indexed_at: null,
|
|
||||||
periodic_indexing_enabled: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("BookStack connector created successfully!");
|
|
||||||
|
|
||||||
// Navigate back to connectors page
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating connector:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to Connectors
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.BOOKSTACK_CONNECTOR, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect BookStack</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Connect your BookStack instance to search wiki pages.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Tabs defaultValue="connect" className="w-full">
|
|
||||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
|
||||||
<TabsTrigger value="connect">Connect</TabsTrigger>
|
|
||||||
<TabsTrigger value="documentation">Documentation</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="connect">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Connect to BookStack</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Connect your BookStack instance to index pages from your wiki.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<Alert>
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertDescription>
|
|
||||||
You'll need to create an API token from your BookStack instance. Go to{" "}
|
|
||||||
<strong>Edit Profile → API Tokens → Create Token</strong>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My BookStack Connector" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
A friendly name to identify this connector.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="base_url"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>BookStack Instance URL</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="https://docs.example.com" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your BookStack instance URL (e.g., https://wiki.yourcompany.com)
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="token_id"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Token ID</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Your BookStack Token ID" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
The Token ID from your BookStack API token.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="token_secret"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Token Secret</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="Your BookStack Token Secret"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your Token Secret will be encrypted and stored securely.
|
|
||||||
</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 BookStack
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="documentation">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>BookStack Integration Guide</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Learn how to set up and use the BookStack connector.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-3">What gets indexed?</h3>
|
|
||||||
<ul className="list-disc list-inside space-y-2 text-sm text-muted-foreground">
|
|
||||||
<li>All pages from your BookStack instance</li>
|
|
||||||
<li>Page content in Markdown format</li>
|
|
||||||
<li>Page titles and metadata</li>
|
|
||||||
<li>Book and chapter hierarchy information</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-3">Setup Instructions</h3>
|
|
||||||
<ol className="list-decimal list-inside space-y-2 text-sm text-muted-foreground">
|
|
||||||
<li>Log in to your BookStack instance</li>
|
|
||||||
<li>Click on your profile icon → Edit Profile</li>
|
|
||||||
<li>Navigate to the "API Tokens" tab</li>
|
|
||||||
<li>Click "Create Token" and give it a name</li>
|
|
||||||
<li>Copy both the Token ID and Token Secret</li>
|
|
||||||
<li>Paste them in the form above</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-3">Permissions Required</h3>
|
|
||||||
<ul className="list-disc list-inside space-y-2 text-sm text-muted-foreground">
|
|
||||||
<li>Your user account must have "Access System API" permission</li>
|
|
||||||
<li>Read access to books and pages you want to index</li>
|
|
||||||
<li>The connector will only index content your account can view</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Alert>
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertDescription>
|
|
||||||
BookStack API has a rate limit of 180 requests per minute. The connector
|
|
||||||
automatically handles rate limiting to ensure reliable indexing.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,363 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, Copy, ExternalLink, Loader2, Webhook } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
|
||||||
const circlebackConnectorFormSchema = z.object({
|
|
||||||
name: z.string().min(3, {
|
|
||||||
message: "Connector name must be at least 3 characters.",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
type CirclebackConnectorFormValues = z.infer<typeof circlebackConnectorFormSchema>;
|
|
||||||
|
|
||||||
export default function CirclebackConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [doesConnectorExist, setDoesConnectorExist] = useState(false);
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
|
|
||||||
const { data: connectors } = useAtomValue(connectorsAtom);
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
|
|
||||||
// Construct the webhook URL
|
|
||||||
const apiBaseUrl = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000";
|
|
||||||
const webhookUrl = `${apiBaseUrl}/api/v1/webhooks/circleback/${searchSpaceId}`;
|
|
||||||
|
|
||||||
// Initialize the form
|
|
||||||
const form = useForm<CirclebackConnectorFormValues>({
|
|
||||||
resolver: zodResolver(circlebackConnectorFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "Circleback Meetings",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { refetch: fetchConnectors } = useAtomValue(connectorsAtom);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchConnectors().then((data) => {
|
|
||||||
const connectors = data.data || [];
|
|
||||||
const connector = connectors.find(
|
|
||||||
(c: SearchSourceConnector) => c.connector_type === EnumConnectorName.CIRCLEBACK_CONNECTOR
|
|
||||||
);
|
|
||||||
if (connector) {
|
|
||||||
setDoesConnectorExist(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Copy webhook URL to clipboard
|
|
||||||
const copyToClipboard = async () => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(webhookUrl);
|
|
||||||
setCopied(true);
|
|
||||||
toast.success("Webhook URL copied to clipboard!");
|
|
||||||
setTimeout(() => setCopied(false), 2000);
|
|
||||||
} catch {
|
|
||||||
toast.error("Failed to copy to clipboard");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = async (values: CirclebackConnectorFormValues) => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
name: values.name,
|
|
||||||
connector_type: EnumConnectorName.CIRCLEBACK_CONNECTOR,
|
|
||||||
config: {
|
|
||||||
webhook_url: webhookUrl,
|
|
||||||
},
|
|
||||||
is_indexable: false, // Webhooks push data, not indexed
|
|
||||||
last_indexed_at: null,
|
|
||||||
periodic_indexing_enabled: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("Circleback connector created successfully!");
|
|
||||||
|
|
||||||
// Navigate back to connectors page
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating connector:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-2xl">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<Link
|
|
||||||
href={`/dashboard/${searchSpaceId}/connectors/add`}
|
|
||||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground mb-4"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to connectors
|
|
||||||
</Link>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.CIRCLEBACK_CONNECTOR, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Circleback</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Receive meeting notes and transcripts via webhook.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Connection Card */}
|
|
||||||
{!doesConnectorExist ? (
|
|
||||||
<>
|
|
||||||
<Card className="mb-6">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Webhook Configuration</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Use this webhook URL in your Circleback automation to send meeting data to
|
|
||||||
SurfSense.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Webhook URL</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input value={webhookUrl} readOnly className="font-mono text-sm" />
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
className="shrink-0"
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Copy this URL and paste it in your Circleback automation settings.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Alert>
|
|
||||||
<Webhook className="h-4 w-4" />
|
|
||||||
<AlertTitle>How it works</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
When you configure this webhook in Circleback, it will automatically send
|
|
||||||
meeting notes, transcripts, and action items to SurfSense after each meeting.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Create Connector</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Register the Circleback connector to track incoming meeting data.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My Circleback Meetings" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
A friendly name to identify this connector.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="space-y-2 pt-2">
|
|
||||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Automatic meeting notes import</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Full transcripts with speaker identification</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Action items and insights extraction</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex justify-between">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
Creating...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Webhook className="mr-2 h-4 w-4" />
|
|
||||||
Create Connector
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
/* Success Card */
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>✅ Circleback connector is active!</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Your Circleback meetings will be automatically imported to this search space.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Webhook URL</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input value={webhookUrl} readOnly className="font-mono text-sm" />
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
className="shrink-0"
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Help Section */}
|
|
||||||
<Card className="mt-6">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-lg">Setup Instructions</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">1. Copy the Webhook URL</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Copy the webhook URL shown above. You'll need this for the next step.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">2. Open Circleback Automations</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Go to{" "}
|
|
||||||
<a
|
|
||||||
href="https://app.circleback.ai/automations"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
|
||||||
>
|
|
||||||
Circleback Automations
|
|
||||||
<ExternalLink className="h-3 w-3" />
|
|
||||||
</a>{" "}
|
|
||||||
and click "Create automation".
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">3. Configure the Webhook</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Set your automation conditions, then select "Send webhook request" and paste the
|
|
||||||
webhook URL.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">4. Select Meeting Outcomes</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Choose which meeting data to include: notes, transcript, action items, and
|
|
||||||
insights.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">5. Create & Test</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Give your automation a name and create it. You can send a test request to verify
|
|
||||||
the integration works.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,241 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, ExternalLink, Eye, EyeOff } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
|
||||||
const clickupConnectorFormSchema = z.object({
|
|
||||||
name: z.string().min(3, {
|
|
||||||
message: "Connector name must be at least 3 characters.",
|
|
||||||
}),
|
|
||||||
api_token: z.string().min(10, {
|
|
||||||
message: "ClickUp API Token is required and must be valid.",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
type ClickUpConnectorFormValues = z.infer<typeof clickupConnectorFormSchema>;
|
|
||||||
|
|
||||||
export default function ClickUpConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [showApiToken, setShowApiToken] = useState(false);
|
|
||||||
|
|
||||||
// Initialize the form with react-hook-form and zod validation
|
|
||||||
const form = useForm<ClickUpConnectorFormValues>({
|
|
||||||
resolver: zodResolver(clickupConnectorFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "ClickUp Connector",
|
|
||||||
api_token: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
async function onSubmit(values: ClickUpConnectorFormValues) {
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
name: values.name,
|
|
||||||
connector_type: EnumConnectorName.CLICKUP_CONNECTOR,
|
|
||||||
is_indexable: true,
|
|
||||||
config: {
|
|
||||||
CLICKUP_API_TOKEN: values.api_token,
|
|
||||||
},
|
|
||||||
last_indexed_at: null,
|
|
||||||
periodic_indexing_enabled: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("ClickUp connector created successfully!");
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating ClickUp connector:", error);
|
|
||||||
toast.error("Failed to create ClickUp connector. Please try again.");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-6 max-w-2xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to Connectors
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.CLICKUP_CONNECTOR, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect ClickUp</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Connect your ClickUp workspace to search tasks and projects.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>ClickUp Configuration</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Enter your ClickUp API token to connect your workspace. You can generate a personal API
|
|
||||||
token from your ClickUp settings.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="ClickUp Connector" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
A friendly name to identify this ClickUp connector.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="api_token"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>ClickUp API Token</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
type={showApiToken ? "text" : "password"}
|
|
||||||
placeholder="pk_..."
|
|
||||||
{...field}
|
|
||||||
className="pr-10"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
|
||||||
onClick={() => setShowApiToken(!showApiToken)}
|
|
||||||
>
|
|
||||||
{showApiToken ? (
|
|
||||||
<EyeOff className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your ClickUp personal API token. You can generate one in your{" "}
|
|
||||||
<Link
|
|
||||||
href="https://app.clickup.com/settings/apps"
|
|
||||||
target="_blank"
|
|
||||||
className="text-primary hover:underline inline-flex items-center"
|
|
||||||
>
|
|
||||||
ClickUp settings
|
|
||||||
<ExternalLink className="ml-1 h-3 w-3" />
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex justify-end space-x-4">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => router.back()}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isLoading}>
|
|
||||||
{isLoading ? "Creating..." : "Create Connector"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="mt-6">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-lg">How to get your ClickUp API Token</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-sm text-muted-foreground">1. Log in to your ClickUp account</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
2. Click your avatar in the upper-right corner and select "Settings"
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground">3. In the sidebar, click "Apps"</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
4. Under "API Token", click "Generate" or "Regenerate"
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
5. Copy the generated token and paste it above
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4">
|
|
||||||
<Link
|
|
||||||
href="https://app.clickup.com/settings/apps"
|
|
||||||
target="_blank"
|
|
||||||
className="inline-flex items-center text-sm text-primary hover:underline"
|
|
||||||
>
|
|
||||||
Go to ClickUp API Settings
|
|
||||||
<ExternalLink className="ml-1 h-3 w-3" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,322 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
|
||||||
const confluenceConnectorFormSchema = z.object({
|
|
||||||
name: z.string().min(3, {
|
|
||||||
message: "Connector name must be at least 3 characters.",
|
|
||||||
}),
|
|
||||||
base_url: z
|
|
||||||
.string()
|
|
||||||
.url({
|
|
||||||
message: "Please enter a valid Confluence URL (e.g., https://yourcompany.atlassian.net)",
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(url) => {
|
|
||||||
return url.includes("atlassian.net") || url.includes("confluence");
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "Please enter a valid Confluence instance URL",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
email: z.string().email({
|
|
||||||
message: "Please enter a valid email address.",
|
|
||||||
}),
|
|
||||||
api_token: z.string().min(10, {
|
|
||||||
message: "Confluence API Token is required and must be valid.",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
type ConfluenceConnectorFormValues = z.infer<typeof confluenceConnectorFormSchema>;
|
|
||||||
|
|
||||||
export default function ConfluenceConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
|
|
||||||
// Initialize the form
|
|
||||||
const form = useForm<ConfluenceConnectorFormValues>({
|
|
||||||
resolver: zodResolver(confluenceConnectorFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "Confluence Connector",
|
|
||||||
base_url: "",
|
|
||||||
email: "",
|
|
||||||
api_token: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = async (values: ConfluenceConnectorFormValues) => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
name: values.name,
|
|
||||||
connector_type: EnumConnectorName.CONFLUENCE_CONNECTOR,
|
|
||||||
config: {
|
|
||||||
CONFLUENCE_BASE_URL: values.base_url,
|
|
||||||
CONFLUENCE_EMAIL: values.email,
|
|
||||||
CONFLUENCE_API_TOKEN: values.api_token,
|
|
||||||
},
|
|
||||||
is_indexable: true,
|
|
||||||
last_indexed_at: null,
|
|
||||||
periodic_indexing_enabled: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("Confluence connector created successfully!");
|
|
||||||
|
|
||||||
// Navigate back to connectors page
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating connector:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to Connectors
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.CONFLUENCE_CONNECTOR, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Confluence</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Connect your Confluence instance to search pages and spaces.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Tabs defaultValue="connect" className="w-full">
|
|
||||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
|
||||||
<TabsTrigger value="connect">Connect</TabsTrigger>
|
|
||||||
<TabsTrigger value="documentation">Documentation</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="connect">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Connect to Confluence</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Connect your Confluence instance to index pages and comments from your spaces.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<Alert>
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertDescription>
|
|
||||||
You'll need to create an API token from your{" "}
|
|
||||||
<a
|
|
||||||
href="https://id.atlassian.com/manage-profile/security/api-tokens"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
Atlassian Account Settings
|
|
||||||
</a>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My Confluence Connector" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
A friendly name to identify this connector.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="base_url"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Confluence Instance URL</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="https://yourcompany.atlassian.net" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your Confluence instance URL. For Atlassian Cloud, this is typically
|
|
||||||
https://yourcompany.atlassian.net
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Email Address</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="email" placeholder="your.email@company.com" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Your Atlassian account email address.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="api_token"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>API Token</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="Your Confluence API Token"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your Confluence API Token will be encrypted and stored securely.
|
|
||||||
</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 Confluence
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="documentation">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Confluence Integration Guide</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Learn how to set up and use the Confluence connector.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-3">What gets indexed?</h3>
|
|
||||||
<ul className="list-disc list-inside space-y-2 text-sm text-muted-foreground">
|
|
||||||
<li>All pages from accessible spaces</li>
|
|
||||||
<li>Page content and metadata</li>
|
|
||||||
<li>Comments on pages (both footer and inline comments)</li>
|
|
||||||
<li>Page titles and descriptions</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-3">Setup Instructions</h3>
|
|
||||||
<ol className="list-decimal list-inside space-y-2 text-sm text-muted-foreground">
|
|
||||||
<li>Go to your Atlassian Account Settings</li>
|
|
||||||
<li>Navigate to Security → API tokens</li>
|
|
||||||
<li>Create a new API token with appropriate permissions</li>
|
|
||||||
<li>Copy the token and paste it in the form above</li>
|
|
||||||
<li>Ensure your account has read access to the spaces you want to index</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-3">Permissions Required</h3>
|
|
||||||
<ul className="list-disc list-inside space-y-2 text-sm text-muted-foreground">
|
|
||||||
<li>Read access to Confluence spaces</li>
|
|
||||||
<li>View pages and comments</li>
|
|
||||||
<li>Access to space metadata</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Alert>
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertDescription>
|
|
||||||
The connector will only index content that your account has permission to view.
|
|
||||||
Make sure your API token has the necessary permissions for the spaces you want
|
|
||||||
to index.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,371 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "@/components/ui/accordion";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
|
||||||
const discordConnectorFormSchema = z.object({
|
|
||||||
name: z.string().min(3, {
|
|
||||||
message: "Connector name must be at least 3 characters.",
|
|
||||||
}),
|
|
||||||
bot_token: z
|
|
||||||
.string()
|
|
||||||
.min(50, { message: "Discord Bot Token appears to be too short." })
|
|
||||||
.regex(/^[A-Za-z0-9._-]+$/, { message: "Discord Bot Token contains invalid characters." }),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
type DiscordConnectorFormValues = z.infer<typeof discordConnectorFormSchema>;
|
|
||||||
|
|
||||||
export default function DiscordConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
|
|
||||||
// Initialize the form
|
|
||||||
const form = useForm<DiscordConnectorFormValues>({
|
|
||||||
resolver: zodResolver(discordConnectorFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "Discord Connector",
|
|
||||||
bot_token: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = async (values: DiscordConnectorFormValues) => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
name: values.name,
|
|
||||||
connector_type: EnumConnectorName.DISCORD_CONNECTOR,
|
|
||||||
config: {
|
|
||||||
DISCORD_BOT_TOKEN: values.bot_token,
|
|
||||||
},
|
|
||||||
is_indexable: true,
|
|
||||||
last_indexed_at: null,
|
|
||||||
periodic_indexing_enabled: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("Discord connector created successfully!");
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating connector:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to Connectors
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg ">
|
|
||||||
{getConnectorIcon(EnumConnectorName.DISCORD_CONNECTOR, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Discord</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Connect your Discord server to search messages and channels.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Tabs defaultValue="connect" className="w-full">
|
|
||||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
|
||||||
<TabsTrigger value="connect">Connect</TabsTrigger>
|
|
||||||
<TabsTrigger value="documentation">Documentation</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="connect">
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">Connect Discord Server</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Integrate with Discord to search and retrieve information from your servers and
|
|
||||||
channels. This connector can index your Discord messages for search.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Alert className="mb-6 bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Bot Token Required</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
You'll need a Discord Bot Token to use this connector. You can create a Discord
|
|
||||||
bot and get the token from the{" "}
|
|
||||||
<a
|
|
||||||
href="https://discord.com/developers/applications"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
Discord Developer Portal
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My Discord Connector" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
A friendly name to identify this connector.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="bot_token"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Discord Bot Token</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="password" placeholder="Bot Token..." {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your Discord Bot Token will be encrypted and stored securely. You can
|
|
||||||
find it in the Bot section of your application in the Discord Developer
|
|
||||||
Portal.
|
|
||||||
</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 Discord
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
<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 Discord integration:</h4>
|
|
||||||
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
|
||||||
<li>Search through your Discord servers and channels</li>
|
|
||||||
<li>Access historical messages and shared files</li>
|
|
||||||
<li>Connect your team's knowledge directly to your search space</li>
|
|
||||||
<li>Keep your search results up-to-date with latest communications</li>
|
|
||||||
<li>Index your Discord messages for enhanced search capabilities</li>
|
|
||||||
</ul>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="documentation">
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">
|
|
||||||
Discord Connector Documentation
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Learn how to set up and use the Discord connector to index your server data.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-semibold mb-2">How it works</h3>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
The Discord connector indexes all accessible channels for a given bot in your
|
|
||||||
servers.
|
|
||||||
</p>
|
|
||||||
<ul className="mt-2 list-disc pl-5 text-muted-foreground">
|
|
||||||
<li>Upcoming: Support for private channels by granting the bot access.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
|
||||||
<AccordionItem value="authorization">
|
|
||||||
<AccordionTrigger className="text-lg font-medium">
|
|
||||||
Authorization
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-4">
|
|
||||||
<Alert className="bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Bot Setup Required</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
You must create a Discord bot and add it to your server with the correct
|
|
||||||
permissions.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<ol className="list-decimal pl-5 space-y-3">
|
|
||||||
<li>
|
|
||||||
Go to{" "}
|
|
||||||
<a
|
|
||||||
href="https://discord.com/developers/applications"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
https://discord.com/developers/applications
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</li>
|
|
||||||
<li>Create a new application and add a bot to it.</li>
|
|
||||||
<li>Copy the Bot Token from the Bot section.</li>
|
|
||||||
<li>
|
|
||||||
Invite the bot to your server with the following OAuth2 scopes and
|
|
||||||
permissions:
|
|
||||||
<ul className="list-disc pl-5 mt-1">
|
|
||||||
<li>
|
|
||||||
Scopes: <code>bot</code>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Bot Permissions: <code>Read Messages/View Channels</code>,{" "}
|
|
||||||
<code>Read Message History</code>, <code>Send Messages</code>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>Paste the Bot Token above to connect.</li>
|
|
||||||
</ol>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="indexing">
|
|
||||||
<AccordionTrigger className="text-lg font-medium">Indexing</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-4">
|
|
||||||
<ol className="list-decimal pl-5 space-y-3">
|
|
||||||
<li>
|
|
||||||
Navigate to the Connector Dashboard and select the{" "}
|
|
||||||
<strong>Discord</strong> Connector.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Place the <strong>Bot Token</strong> under{" "}
|
|
||||||
<strong>Step 1 Provide Credentials</strong>.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Click <strong>Connect</strong> to establish the connection.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<Alert className="bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Important: Bot Channel Access</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
After connecting, ensure the bot has access to all channels you want to
|
|
||||||
index. You may need to adjust channel permissions in Discord.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Alert className="bg-muted mt-4">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>First Indexing</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
The first indexing pulls all accessible channels and may take longer than
|
|
||||||
future updates. Only channels where the bot has access will be indexed.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<h4 className="font-medium mb-2">Troubleshooting:</h4>
|
|
||||||
<ul className="list-disc pl-5 space-y-2 text-muted-foreground">
|
|
||||||
<li>
|
|
||||||
<strong>Missing messages:</strong> If you don't see messages from a
|
|
||||||
channel, check the bot's permissions for that channel.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Bot not responding:</strong> Make sure the bot is online and the
|
|
||||||
token is correct.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Private channels:</strong> The bot must be explicitly granted
|
|
||||||
access to private channels.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,755 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
|
||||||
import { useId, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "@/components/ui/accordion";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
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 { Separator } from "@/components/ui/separator";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
|
||||||
const elasticsearchConnectorFormSchema = z
|
|
||||||
.object({
|
|
||||||
name: z.string().min(3, {
|
|
||||||
message: "Connector name must be at least 3 characters.",
|
|
||||||
}),
|
|
||||||
endpoint_url: z.string().url({ message: "Please enter a valid Elasticsearch endpoint URL." }),
|
|
||||||
auth_method: z.enum(["basic", "api_key"]).default("api_key"),
|
|
||||||
username: z.string().optional(),
|
|
||||||
password: z.string().optional(),
|
|
||||||
ELASTICSEARCH_API_KEY: z.string().optional(),
|
|
||||||
indices: z.string().optional(),
|
|
||||||
query: z.string().default("*"),
|
|
||||||
search_fields: z.string().optional(),
|
|
||||||
max_documents: z.number().min(1).max(10000).optional(),
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
if (data.auth_method === "basic") {
|
|
||||||
return Boolean(data.username?.trim() && data.password?.trim());
|
|
||||||
}
|
|
||||||
if (data.auth_method === "api_key") {
|
|
||||||
return Boolean(data.ELASTICSEARCH_API_KEY?.trim());
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "Authentication credentials are required for the selected method.",
|
|
||||||
path: ["auth_method"],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
type ElasticsearchConnectorFormValues = z.infer<typeof elasticsearchConnectorFormSchema>;
|
|
||||||
|
|
||||||
export default function ElasticsearchConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
// match pattern used in other connector pages: prefer route param, fallback to query param
|
|
||||||
const searchSpaceId = (params.search_space_id ?? searchParams?.get("search_space_id")) as string;
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
|
|
||||||
const authBasicId = useId();
|
|
||||||
const authApiKeyId = useId();
|
|
||||||
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
|
|
||||||
// Initialize the form
|
|
||||||
const form = useForm<ElasticsearchConnectorFormValues>({
|
|
||||||
resolver: zodResolver(elasticsearchConnectorFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "Elasticsearch Connector",
|
|
||||||
endpoint_url: "",
|
|
||||||
auth_method: "api_key",
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
ELASTICSEARCH_API_KEY: "",
|
|
||||||
indices: "",
|
|
||||||
query: "*",
|
|
||||||
search_fields: "",
|
|
||||||
max_documents: undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const stringToArray = (str: string): string[] => {
|
|
||||||
const items = str
|
|
||||||
.split(",")
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter((item) => item.length > 0);
|
|
||||||
return Array.from(new Set(items));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = async (values: ElasticsearchConnectorFormValues) => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
if (!searchSpaceId) {
|
|
||||||
toast.error(
|
|
||||||
"Missing search_space_id (route or ?search_space_id=). Provide it in the URL or pick a search space."
|
|
||||||
);
|
|
||||||
setIsSubmitting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const searchSpaceIdNum = Number(searchSpaceId);
|
|
||||||
if (!Number.isInteger(searchSpaceIdNum) || searchSpaceIdNum <= 0) {
|
|
||||||
toast.error("Invalid search_space_id. It must be a positive integer.");
|
|
||||||
setIsSubmitting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Send full URL to backend (backend expects ELASTICSEARCH_URL)
|
|
||||||
const config: Record<string, string | number | boolean | string[]> = {
|
|
||||||
ELASTICSEARCH_URL: values.endpoint_url,
|
|
||||||
// default to verifying certs; expose fields for CA/verify if UI added later
|
|
||||||
ELASTICSEARCH_VERIFY_CERTS: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (values.auth_method === "basic") {
|
|
||||||
if (values.username) config.ELASTICSEARCH_USERNAME = values.username;
|
|
||||||
if (values.password) config.ELASTICSEARCH_PASSWORD = values.password;
|
|
||||||
} else if (values.auth_method === "api_key") {
|
|
||||||
if (values.ELASTICSEARCH_API_KEY)
|
|
||||||
config.ELASTICSEARCH_API_KEY = values.ELASTICSEARCH_API_KEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
const indicesInput = values.indices?.trim() ?? "";
|
|
||||||
const indicesArr = stringToArray(indicesInput);
|
|
||||||
config.ELASTICSEARCH_INDEX =
|
|
||||||
indicesArr.length === 0 ? "*" : indicesArr.length === 1 ? indicesArr[0] : indicesArr;
|
|
||||||
|
|
||||||
if (values.query && values.query !== "*") {
|
|
||||||
config.ELASTICSEARCH_QUERY = values.query;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.search_fields?.trim()) {
|
|
||||||
// config.ELASTICSEARCH_FIELDS = stringToArray(values.search_fields);
|
|
||||||
const fields = stringToArray(values.search_fields);
|
|
||||||
config.ELASTICSEARCH_FIELDS = fields;
|
|
||||||
config.ELASTICSEARCH_CONTENT_FIELDS = fields;
|
|
||||||
if (fields.includes("title")) {
|
|
||||||
config.ELASTICSEARCH_TITLE_FIELD = "title";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.max_documents !== undefined && values.max_documents > 0) {
|
|
||||||
config.ELASTICSEARCH_MAX_DOCUMENTS = values.max_documents;
|
|
||||||
}
|
|
||||||
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
name: values.name,
|
|
||||||
connector_type: EnumConnectorName.ELASTICSEARCH_CONNECTOR,
|
|
||||||
is_indexable: true,
|
|
||||||
last_indexed_at: null,
|
|
||||||
periodic_indexing_enabled: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
config,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("Elasticsearch connector created successfully!");
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating connector:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to Connectors
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.ELASTICSEARCH_CONNECTOR, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Elasticsearch</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Connect to your Elasticsearch cluster to search and index documents.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Tabs defaultValue="connect" className="w-full">
|
|
||||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
|
||||||
<TabsTrigger value="connect">Connect</TabsTrigger>
|
|
||||||
<TabsTrigger value="documentation">Documentation</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="connect">
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">Connect Elasticsearch Cluster</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Connect to your Elasticsearch instance to search and index documents for enhanced
|
|
||||||
search capabilities.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
{/* Connector Name */}
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My Elasticsearch Connector" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
A friendly name to identify this connector.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Connection Details */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-medium">Connection Details</h3>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="endpoint_url"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Elasticsearch Endpoint URL</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="url"
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder="https://your-cluster.es.region.aws.com:443"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Enter the complete Elasticsearch endpoint URL. We'll automatically
|
|
||||||
extract the hostname, port, and SSL settings.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Show parsed URL details */}
|
|
||||||
{form.watch("endpoint_url") && (
|
|
||||||
<div className="rounded-lg border bg-muted/50 p-3">
|
|
||||||
<h4 className="text-sm font-medium mb-2">Parsed Connection Details:</h4>
|
|
||||||
<div className="text-sm text-muted-foreground space-y-1">
|
|
||||||
{(() => {
|
|
||||||
try {
|
|
||||||
const url = new URL(form.watch("endpoint_url"));
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<strong>Hostname:</strong> {url.hostname}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>Port:</strong>{" "}
|
|
||||||
{url.port || (url.protocol === "https:" ? "443" : "80")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>SSL/TLS:</strong>{" "}
|
|
||||||
{url.protocol === "https:" ? "Enabled" : "Disabled"}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
return <div className="text-destructive">Invalid URL format</div>;
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Authentication */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-medium">Authentication</h3>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="auth_method"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="space-y-3">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroup.Root
|
|
||||||
onValueChange={(value) => {
|
|
||||||
field.onChange(value);
|
|
||||||
// Clear auth fields when method changes
|
|
||||||
if (value !== "basic") {
|
|
||||||
form.setValue("username", "");
|
|
||||||
form.setValue("password", "");
|
|
||||||
}
|
|
||||||
if (value !== "api_key") {
|
|
||||||
form.setValue("ELASTICSEARCH_API_KEY", "");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
value={field.value}
|
|
||||||
className="flex flex-col space-y-2"
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroup.Item
|
|
||||||
value="api_key"
|
|
||||||
id={authApiKeyId}
|
|
||||||
className="aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
|
|
||||||
>
|
|
||||||
<RadioGroup.Indicator className="flex items-center justify-center">
|
|
||||||
<div className="h-2.5 w-2.5 rounded-full bg-current" />
|
|
||||||
</RadioGroup.Indicator>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
<Label htmlFor={authApiKeyId}>API Key</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroup.Item
|
|
||||||
value="basic"
|
|
||||||
id={authBasicId}
|
|
||||||
className="aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
|
|
||||||
>
|
|
||||||
<RadioGroup.Indicator className="flex items-center justify-center">
|
|
||||||
<div className="h-2.5 w-2.5 rounded-full bg-current" />
|
|
||||||
</RadioGroup.Indicator>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
<Label htmlFor={authBasicId}>Username & Password</Label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup.Root>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Basic Auth Fields */}
|
|
||||||
{form.watch("auth_method") === "basic" && (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="username"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Username</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="elastic" autoComplete="username" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="Password"
|
|
||||||
autoComplete="current-password"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* API Key Field */}
|
|
||||||
{form.watch("auth_method") === "api_key" && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="ELASTICSEARCH_API_KEY"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>API Key</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="Your API Key Here"
|
|
||||||
autoComplete="off"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Enter your Elasticsearch API key (base64 encoded). This will be
|
|
||||||
stored securely.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Index Selection */}
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="indices"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Index Selection </FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="logs-*, documents-*, app-logs" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Comma-separated indices to search (e.g., "logs-*, documents-*").
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Show parsed indices as badges */}
|
|
||||||
{form.watch("indices")?.trim() && (
|
|
||||||
<div className="rounded-lg border bg-muted/50 p-3">
|
|
||||||
<h4 className="text-sm font-medium mb-2">Selected Indices:</h4>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{stringToArray(form.watch("indices") ?? "").map((index) => (
|
|
||||||
<Badge key={index} variant="secondary" className="text-xs">
|
|
||||||
{index}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Alert className="bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Index Selection Tips</AlertTitle>
|
|
||||||
<AlertDescription className="mt-2">
|
|
||||||
<ul className="list-disc pl-4 space-y-1 text-sm">
|
|
||||||
<li>Use wildcards like "logs-*" to match multiple indices</li>
|
|
||||||
<li>Separate multiple indices with commas</li>
|
|
||||||
<li>
|
|
||||||
Leave empty to search all accessible indices including internal ones
|
|
||||||
</li>
|
|
||||||
<li>Choosing specific indices improves search performance</li>
|
|
||||||
</ul>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Advanced Configuration */}
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
|
||||||
<AccordionItem value="advanced">
|
|
||||||
<AccordionTrigger>Advanced Configuration</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-4">
|
|
||||||
{/* Default Search Query */}
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="query"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
Default Search Query{" "}
|
|
||||||
<span className="text-muted-foreground">(Optional)</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="*" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Default Elasticsearch query to use for searches. Use "*" to match
|
|
||||||
all documents.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Form Fields */}
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="search_fields"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
Search Fields{" "}
|
|
||||||
<span className="text-muted-foreground">(Optional)</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="title, content, description" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Comma-separated list of specific fields to search in (e.g.,
|
|
||||||
"title, content, description"). Leave empty to search all fields.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Show parsed search fields as badges */}
|
|
||||||
{form.watch("search_fields")?.trim() && (
|
|
||||||
<div className="rounded-lg border bg-muted/50 p-3">
|
|
||||||
<h4 className="text-sm font-medium mb-2">Search Fields:</h4>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{stringToArray(form.watch("search_fields") ?? "").map((field) => (
|
|
||||||
<Badge key={field} variant="outline" className="text-xs">
|
|
||||||
{field}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="max_documents"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
Maximum Documents{" "}
|
|
||||||
<span className="text-muted-foreground">(Optional)</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
placeholder="1000"
|
|
||||||
min="1"
|
|
||||||
max="10000"
|
|
||||||
{...field}
|
|
||||||
onChange={(e) =>
|
|
||||||
field.onChange(
|
|
||||||
e.target.value === ""
|
|
||||||
? undefined
|
|
||||||
: parseInt(e.target.value, 10)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Maximum number of documents to retrieve per search (1-10,000).
|
|
||||||
Leave empty to use Elasticsearch's default limit.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<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 Elasticsearch
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
<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 Elasticsearch integration:
|
|
||||||
</h4>
|
|
||||||
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
|
||||||
<li>Search across your indexed documents and logs</li>
|
|
||||||
<li>Access structured and unstructured data from your cluster</li>
|
|
||||||
<li>Leverage existing Elasticsearch indices for enhanced search</li>
|
|
||||||
<li>Real-time search capabilities with powerful query features</li>
|
|
||||||
<li>Integration with your existing Elasticsearch infrastructure</li>
|
|
||||||
</ul>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="documentation">
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">
|
|
||||||
Elasticsearch Connector Documentation
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Learn how to set up and use the Elasticsearch connector to search your data.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-semibold mb-2">How it works</h3>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
The Elasticsearch connector allows you to search and retrieve documents from
|
|
||||||
your Elasticsearch cluster. Configure connection details, select specific
|
|
||||||
indices, and set search parameters to make your existing data searchable within
|
|
||||||
SurfSense.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
|
||||||
<AccordionItem value="connection">
|
|
||||||
<AccordionTrigger className="text-lg font-medium">
|
|
||||||
Connection Setup
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-4">
|
|
||||||
<ol className="list-decimal pl-5 space-y-3">
|
|
||||||
<li>
|
|
||||||
<strong>Endpoint URL:</strong> Enter the complete Elasticsearch endpoint
|
|
||||||
URL (e.g., https://your-cluster.es.region.aws.com:443). We'll
|
|
||||||
automatically extract hostname, port, and SSL settings.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Authentication:</strong> Choose the appropriate method:
|
|
||||||
<ul className="list-disc pl-5 mt-1">
|
|
||||||
<li>
|
|
||||||
<strong>API Key:</strong> Base64 encoded API key (recommended for
|
|
||||||
security)
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Username/Password:</strong> Basic authentication credentials
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Index Selection:</strong> Specify which indices to search using
|
|
||||||
comma-separated patterns (e.g., "logs-*, documents-*")
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="advanced">
|
|
||||||
<AccordionTrigger className="text-lg font-medium">
|
|
||||||
Advanced Configuration
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-4">
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Fine-tune your Elasticsearch connector with these optional settings:
|
|
||||||
</p>
|
|
||||||
<ul className="list-disc pl-5 space-y-2">
|
|
||||||
<li>
|
|
||||||
<strong>Search Fields:</strong> Limit searches to specific fields (e.g.,
|
|
||||||
"title, content") for better relevance
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Default Query:</strong> Set a default Elasticsearch query pattern
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Max Documents:</strong> Limit the number of documents returned per
|
|
||||||
search (1-10,000)
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="troubleshooting">
|
|
||||||
<AccordionTrigger className="text-lg font-medium">
|
|
||||||
Troubleshooting
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-4">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">Common Connection Issues:</h4>
|
|
||||||
<ul className="list-disc pl-5 space-y-2 text-muted-foreground">
|
|
||||||
<li>
|
|
||||||
<strong>Connection Refused:</strong> Check hostname and port. Ensure
|
|
||||||
Elasticsearch is running.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Authentication Failed:</strong> Verify credentials. For API
|
|
||||||
keys, ensure they have proper permissions.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>SSL Errors:</strong> Try disabling SSL for local development
|
|
||||||
or check certificate validity.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>No Indices Found:</strong> Ensure your credentials have
|
|
||||||
permission to list and read indices.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Alert className="bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Security Note</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
For production environments, use API keys with minimal required
|
|
||||||
permissions: cluster monitoring and read access to specific indices.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,531 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, CircleAlert, Github, Info, ListChecks, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "@/components/ui/accordion";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
import { authenticatedFetch, redirectToLogin } from "@/lib/auth-utils";
|
|
||||||
|
|
||||||
// Define the form schema with Zod for GitHub PAT entry step
|
|
||||||
const githubPatFormSchema = z.object({
|
|
||||||
name: z.string().min(3, {
|
|
||||||
message: "Connector name must be at least 3 characters.",
|
|
||||||
}),
|
|
||||||
github_pat: z
|
|
||||||
.string()
|
|
||||||
.min(20, {
|
|
||||||
// Apply min length first
|
|
||||||
message: "GitHub Personal Access Token seems too short.",
|
|
||||||
})
|
|
||||||
.refine((pat) => pat.startsWith("ghp_") || pat.startsWith("github_pat_"), {
|
|
||||||
// Then refine the pattern
|
|
||||||
message: "GitHub PAT should start with 'ghp_' or 'github_pat_'",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
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() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [step, setStep] = useState<"enter_pat" | "select_repos">("enter_pat");
|
|
||||||
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
|
|
||||||
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
|
|
||||||
// Initialize the form for PAT entry
|
|
||||||
const form = useForm<GithubPatFormValues>({
|
|
||||||
resolver: zodResolver(githubPatFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: connectorName,
|
|
||||||
github_pat: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Function to fetch repositories using the new backend endpoint
|
|
||||||
const fetchRepositories = async (values: GithubPatFormValues) => {
|
|
||||||
setIsFetchingRepos(true);
|
|
||||||
setConnectorName(values.name); // Store the name
|
|
||||||
setValidatedPat(values.github_pat); // Store the PAT temporarily
|
|
||||||
try {
|
|
||||||
const response = await authenticatedFetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/github/repositories`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
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 {
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
name: connectorName, // Use the stored name
|
|
||||||
connector_type: EnumConnectorName.GITHUB_CONNECTOR,
|
|
||||||
config: {
|
|
||||||
GITHUB_PAT: validatedPat, // Use the stored validated PAT
|
|
||||||
repo_full_names: selectedRepos, // Add the selected repo names
|
|
||||||
},
|
|
||||||
is_indexable: true,
|
|
||||||
last_indexed_at: null,
|
|
||||||
periodic_indexing_enabled: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("GitHub connector created successfully!");
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating GitHub connector:", error);
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : "Failed to create GitHub connector.";
|
|
||||||
toast.error(errorMessage);
|
|
||||||
} finally {
|
|
||||||
setIsCreatingConnector(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle checkbox changes
|
|
||||||
const handleRepoSelection = (repoFullName: string, checked: boolean) => {
|
|
||||||
setSelectedRepos((prev) =>
|
|
||||||
checked ? [...prev, repoFullName] : prev.filter((name) => name !== repoFullName)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
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" />
|
|
||||||
{step === "select_repos" ? "Back to PAT Entry" : "Back to Add Connectors"}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Tabs defaultValue="connect" className="w-full">
|
|
||||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
|
||||||
<TabsTrigger value="connect">Connect GitHub</TabsTrigger>
|
|
||||||
<TabsTrigger value="documentation">Setup Guide</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="connect">
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
|
||||||
{step === "enter_pat" ? (
|
|
||||||
getConnectorIcon(EnumConnectorName.GITHUB_CONNECTOR, "h-6 w-6")
|
|
||||||
) : (
|
|
||||||
<ListChecks className="h-6 w-6" />
|
|
||||||
)}
|
|
||||||
{step === "enter_pat" ? "Connect GitHub Account" : "Select Repositories to Index"}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{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>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
{step === "enter_pat" && (
|
|
||||||
<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 fetch
|
|
||||||
repositories. 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"
|
|
||||||
>
|
|
||||||
GitHub Developer Settings
|
|
||||||
</a>
|
|
||||||
. The PAT will be used to fetch repositories and then stored securely to
|
|
||||||
enable indexing.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<form onSubmit={form.handleSubmit(fetchRepositories)} className="space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My GitHub Connector" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
A friendly name to identify this GitHub connection.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
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">
|
|
||||||
<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">
|
|
||||||
<li>Search through code and documentation in your selected repositories</li>
|
|
||||||
<li>Access READMEs, Markdown files, and common code files</li>
|
|
||||||
<li>Connect your project knowledge directly to your search space</li>
|
|
||||||
<li>Index your selected repositories for enhanced search capabilities</li>
|
|
||||||
</ul>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="documentation">
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">GitHub Connector Setup Guide</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Learn how to generate a Personal Access Token (PAT) and connect your GitHub
|
|
||||||
account.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-semibold mb-2">How it works</h3>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
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>
|
|
||||||
<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>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>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
|
||||||
<AccordionItem value="create_pat">
|
|
||||||
<AccordionTrigger className="text-lg font-medium">
|
|
||||||
Step 1: Generate GitHub PAT
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">Generating a Token:</h4>
|
|
||||||
<ol className="list-decimal pl-5 space-y-3">
|
|
||||||
<li>
|
|
||||||
Go to your GitHub{" "}
|
|
||||||
<a
|
|
||||||
href="https://github.com/settings/tokens"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
Developer settings
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Click on <strong>Personal access tokens</strong>, then choose{" "}
|
|
||||||
<strong>Tokens (classic)</strong> or{" "}
|
|
||||||
<strong>Fine-grained tokens</strong> (recommended if available and
|
|
||||||
suitable).
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Click <strong>Generate new token</strong> (and choose the appropriate
|
|
||||||
type).
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Give your token a descriptive name (e.g., "SurfSense Connector").
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Set an expiration date for the token (recommended for security).
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Under <strong>Select scopes</strong> (for classic tokens) or{" "}
|
|
||||||
<strong>Repository access</strong> (for fine-grained), grant the
|
|
||||||
necessary permissions. At minimum, the <strong>`repo`</strong> scope
|
|
||||||
(or equivalent read access to repositories for fine-grained tokens) is
|
|
||||||
required to read repository content.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Click <strong>Generate token</strong>.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Important:</strong> Copy your new PAT immediately. You won't
|
|
||||||
be able to see it again after leaving the page.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="connect_app">
|
|
||||||
<AccordionTrigger className="text-lg font-medium">
|
|
||||||
Step 2: Connect in SurfSense
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-4">
|
|
||||||
<ol className="list-decimal pl-5 space-y-3">
|
|
||||||
<li>Navigate to the "Connect GitHub" tab.</li>
|
|
||||||
<li>Enter a name for your connector.</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>
|
|
||||||
</ol>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,188 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, ExternalLink, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
|
||||||
|
|
||||||
export default function GoogleCalendarConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
|
||||||
const [doesConnectorExist, setDoesConnectorExist] = useState(false);
|
|
||||||
|
|
||||||
const { refetch: fetchConnectors } = useAtomValue(connectorsAtom);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchConnectors().then((data) => {
|
|
||||||
const connectors = data.data || [];
|
|
||||||
const connector = connectors.find(
|
|
||||||
(c: SearchSourceConnector) =>
|
|
||||||
c.connector_type === EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR
|
|
||||||
);
|
|
||||||
if (connector) {
|
|
||||||
setDoesConnectorExist(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle Google OAuth connection
|
|
||||||
const handleConnectGoogle = async () => {
|
|
||||||
try {
|
|
||||||
setIsConnecting(true);
|
|
||||||
// Call backend to initiate authorization flow
|
|
||||||
const response = await authenticatedFetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/auth/google/calendar/connector/add/?space_id=${searchSpaceId}`,
|
|
||||||
{ method: "GET" }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to initiate Google OAuth");
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Redirect to Google for authentication
|
|
||||||
window.location.href = data.auth_url;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error connecting to Google:", error);
|
|
||||||
toast.error("Failed to connect to Google Calendar");
|
|
||||||
} finally {
|
|
||||||
setIsConnecting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-2xl">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<Link
|
|
||||||
href={`/dashboard/${searchSpaceId}/connectors/add`}
|
|
||||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground mb-4"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to connectors
|
|
||||||
</Link>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Google Calendar</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Connect your Google Calendar to search events.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* OAuth Connection Card */}
|
|
||||||
{!doesConnectorExist ? (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Connect Your Google Account</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Connect your Google account to access your calendar events. We'll only request
|
|
||||||
read-only access to your calendars.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Read-only access to your calendar events</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Access works even when you're offline</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>You can disconnect anytime</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex justify-between">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleConnectGoogle} disabled={isConnecting}>
|
|
||||||
{isConnecting ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
Connecting...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ExternalLink className="mr-2 h-4 w-4" />
|
|
||||||
Connect Your Google Account
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
/* Configuration Form Card */
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>✅ Your Google calendar is successfully connected!</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Help Section */}
|
|
||||||
{!doesConnectorExist && (
|
|
||||||
<Card className="mt-6">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-lg">How It Works</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">1. Connect Your Account</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Click "Connect Your Google Account" to start the secure OAuth process. You'll be
|
|
||||||
redirected to Google to sign in.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">2. Grant Permissions</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Google will ask for permission to read your calendar events. We only request
|
|
||||||
read-only access to keep your data safe.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, ExternalLink, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
|
||||||
|
|
||||||
export default function GoogleDriveConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
|
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
|
||||||
const [doesConnectorExist, setDoesConnectorExist] = useState(false);
|
|
||||||
|
|
||||||
const { refetch: fetchConnectors } = useAtomValue(connectorsAtom);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchConnectors().then((data) => {
|
|
||||||
const connectors = data.data || [];
|
|
||||||
const connector = connectors.find(
|
|
||||||
(c: SearchSourceConnector) => c.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR
|
|
||||||
);
|
|
||||||
if (connector) {
|
|
||||||
setDoesConnectorExist(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleConnectGoogle = async () => {
|
|
||||||
try {
|
|
||||||
setIsConnecting(true);
|
|
||||||
const response = await authenticatedFetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/auth/google/drive/connector/add/?space_id=${searchSpaceId}`,
|
|
||||||
{ method: "GET" }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to initiate Google OAuth");
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
window.location.href = data.auth_url;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error connecting to Google:", error);
|
|
||||||
toast.error("Failed to connect to Google Drive");
|
|
||||||
} finally {
|
|
||||||
setIsConnecting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-2xl">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<Link
|
|
||||||
href={`/dashboard/${searchSpaceId}/connectors/add`}
|
|
||||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground mb-4"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to connectors
|
|
||||||
</Link>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.GOOGLE_DRIVE_CONNECTOR, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Google Drive</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Securely connect your Google Drive account
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Connection Card */}
|
|
||||||
{!doesConnectorExist ? (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Connect Your Google Account</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Authorize read-only access to your Google Drive. You'll select which folder to
|
|
||||||
index when you start indexing.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Read-only access to your Drive files</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Index documents, spreadsheets, presentations, PDFs & more</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Automatic updates with change tracking</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Secure OAuth 2.0 authentication</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex justify-between">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleConnectGoogle} disabled={isConnecting}>
|
|
||||||
{isConnecting ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
Connecting...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ExternalLink className="mr-2 h-4 w-4" />
|
|
||||||
Connect Google Drive
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>✅ Already Connected</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Your Google Drive connector is already set up. Go to the connectors page to
|
|
||||||
start indexing.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardFooter>
|
|
||||||
<Button onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors`)}>
|
|
||||||
Go to Connectors
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Information Card */}
|
|
||||||
<Card className="mt-6">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>How Google Drive Integration Works</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-medium">1️⃣ Connect Your Account</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
First, securely connect your Google Drive account using OAuth 2.0. We only
|
|
||||||
request read-only access.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-medium">2️⃣ Select Folder to Index</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
When you're ready to index, go to the connectors page and click "Index". You'll
|
|
||||||
choose which folder to process.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-medium">3️⃣ Automatic Change Detection</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
We use Google Drive's change tracking API to detect when files are modified,
|
|
||||||
added, or deleted. Only changed files are re-indexed.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-medium">📄 Comprehensive File Support</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Supports Google Workspace files (Docs, Sheets, Slides), Microsoft Office
|
|
||||||
documents, PDFs, text files, images (with OCR), and more.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,196 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, ExternalLink, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
|
||||||
|
|
||||||
export default function GoogleGmailConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
|
||||||
const [doesConnectorExist, setDoesConnectorExist] = useState(false);
|
|
||||||
|
|
||||||
const { refetch: fetchConnectors } = useAtomValue(connectorsAtom);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchConnectors().then((data) => {
|
|
||||||
const connectors = data.data || [];
|
|
||||||
const connector = connectors.find(
|
|
||||||
(c: SearchSourceConnector) => c.connector_type === EnumConnectorName.GOOGLE_GMAIL_CONNECTOR
|
|
||||||
);
|
|
||||||
if (connector) {
|
|
||||||
setDoesConnectorExist(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle Google OAuth connection
|
|
||||||
const handleConnectGoogle = async () => {
|
|
||||||
try {
|
|
||||||
setIsConnecting(true);
|
|
||||||
// Call backend to initiate authorization flow
|
|
||||||
const response = await authenticatedFetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/auth/google/gmail/connector/add/?space_id=${searchSpaceId}`,
|
|
||||||
{ method: "GET" }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to initiate Google OAuth");
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Redirect to Google for authentication
|
|
||||||
window.location.href = data.auth_url;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error connecting to Google:", error);
|
|
||||||
toast.error("Failed to connect to Google Gmail");
|
|
||||||
} finally {
|
|
||||||
setIsConnecting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-2xl">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<Link
|
|
||||||
href={`/dashboard/${searchSpaceId}/connectors/add`}
|
|
||||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground mb-4"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to connectors
|
|
||||||
</Link>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.GOOGLE_GMAIL_CONNECTOR, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Google Gmail</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Connect your Gmail account to search through your emails
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Connection Card */}
|
|
||||||
{!doesConnectorExist ? (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Connect Your Gmail Account</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Securely connect your Gmail account to enable email search within SurfSense. We'll
|
|
||||||
only access your emails with read-only permissions.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Read-only access to your emails</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Search through email content and metadata</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Secure OAuth 2.0 authentication</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>You can disconnect anytime</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex justify-between">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleConnectGoogle} disabled={isConnecting}>
|
|
||||||
{isConnecting ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
Connecting...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ExternalLink className="mr-2 h-4 w-4" />
|
|
||||||
Connect Your Google Account
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
/* Configuration Form Card */
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>✅ Your Gmail is successfully connected!</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Information Card */}
|
|
||||||
<Card className="mt-6">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>What data will be indexed?</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-medium">Email Content</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
We'll index the content of your emails including subject lines, sender information,
|
|
||||||
and message body text to make them searchable.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-medium">Email Metadata</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Information like sender, recipient, date, and labels will be indexed to provide
|
|
||||||
better search context and filtering options.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-medium">Privacy & Security</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Your emails are processed securely and stored with encryption. We only access emails
|
|
||||||
with read-only permissions and never modify or send emails on your behalf.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,427 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "@/components/ui/accordion";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
|
||||||
const jiraConnectorFormSchema = z.object({
|
|
||||||
name: z.string().min(3, {
|
|
||||||
message: "Connector name must be at least 3 characters.",
|
|
||||||
}),
|
|
||||||
base_url: z
|
|
||||||
.string()
|
|
||||||
.url({
|
|
||||||
message: "Please enter a valid Jira URL (e.g., https://yourcompany.atlassian.net)",
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(url) => {
|
|
||||||
return url.includes("atlassian.net") || url.includes("jira");
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "Please enter a valid Jira instance URL",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
email: z.string().email({
|
|
||||||
message: "Please enter a valid email address.",
|
|
||||||
}),
|
|
||||||
api_token: z.string().min(10, {
|
|
||||||
message: "Jira API Token is required and must be valid.",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
type JiraConnectorFormValues = z.infer<typeof jiraConnectorFormSchema>;
|
|
||||||
|
|
||||||
export default function JiraConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
|
|
||||||
// Initialize the form
|
|
||||||
const form = useForm<JiraConnectorFormValues>({
|
|
||||||
resolver: zodResolver(jiraConnectorFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "Jira Connector",
|
|
||||||
base_url: "",
|
|
||||||
email: "",
|
|
||||||
api_token: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = async (values: JiraConnectorFormValues) => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
name: values.name,
|
|
||||||
connector_type: EnumConnectorName.JIRA_CONNECTOR,
|
|
||||||
config: {
|
|
||||||
JIRA_BASE_URL: values.base_url,
|
|
||||||
JIRA_EMAIL: values.email,
|
|
||||||
JIRA_API_TOKEN: values.api_token,
|
|
||||||
},
|
|
||||||
is_indexable: true,
|
|
||||||
last_indexed_at: null,
|
|
||||||
periodic_indexing_enabled: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("Jira connector created successfully!");
|
|
||||||
|
|
||||||
// Navigate back to connectors page
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating connector:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to Connectors
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.JIRA_CONNECTOR, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Jira</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Connect your Jira instance to search issues and tickets.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Tabs defaultValue="connect" className="w-full">
|
|
||||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
|
||||||
<TabsTrigger value="connect">Connect</TabsTrigger>
|
|
||||||
<TabsTrigger value="documentation">Documentation</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="connect">
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">Connect Jira Instance</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Integrate with Jira to search and retrieve information from your issues, tickets,
|
|
||||||
and comments. This connector can index your Jira content for search.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Alert className="mb-6 bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Jira Personal Access Token Required</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
You'll need a Jira Personal Access Token to use this connector. You can create
|
|
||||||
one from{" "}
|
|
||||||
<a
|
|
||||||
href="https://id.atlassian.com/manage-profile/security/api-tokens"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
Atlassian Account Settings
|
|
||||||
</a>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My Jira Connector" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
A friendly name to identify this connector.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="base_url"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Jira Instance URL</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="https://yourcompany.atlassian.net" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your Jira instance URL. For Atlassian Cloud, this is typically
|
|
||||||
https://yourcompany.atlassian.net
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Email Address</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="email" placeholder="your.email@company.com" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>Your Atlassian account email address.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="api_token"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>API Token</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="password" placeholder="Your Jira API Token" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your Jira API Token will be encrypted and stored securely.
|
|
||||||
</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 Jira
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
<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 Jira integration:</h4>
|
|
||||||
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
|
||||||
<li>Search through all your Jira issues and tickets</li>
|
|
||||||
<li>Access issue descriptions, comments, and full discussion threads</li>
|
|
||||||
<li>Connect your team's project management directly to your search space</li>
|
|
||||||
<li>Keep your search results up-to-date with latest Jira content</li>
|
|
||||||
<li>Index your Jira issues for enhanced search capabilities</li>
|
|
||||||
<li>Search by issue keys, status, priority, and assignee information</li>
|
|
||||||
</ul>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="documentation">
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">Jira Connector Documentation</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Learn how to set up and use the Jira connector to index your project management
|
|
||||||
data.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-semibold mb-2">How it works</h3>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
The Jira connector uses the Jira REST API with Basic Authentication to fetch all
|
|
||||||
issues and comments that your account has access to within your Jira instance.
|
|
||||||
</p>
|
|
||||||
<ul className="mt-2 list-disc pl-5 text-muted-foreground">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
|
||||||
<AccordionItem value="authorization">
|
|
||||||
<AccordionTrigger className="text-lg font-medium">
|
|
||||||
Authorization
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-4">
|
|
||||||
<Alert className="bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Read-Only Access is Sufficient</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
You only need read access for this connector to work. The API Token will
|
|
||||||
only be used to read your Jira data.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">Step 1: Create an API Token</h4>
|
|
||||||
<ol className="list-decimal pl-5 space-y-3">
|
|
||||||
<li>Log in to your Atlassian account</li>
|
|
||||||
<li>
|
|
||||||
Navigate to{" "}
|
|
||||||
<a
|
|
||||||
href="https://id.atlassian.com/manage-profile/security/api-tokens"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
https://id.atlassian.com/manage-profile/security/api-tokens
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Click <strong>Create API token</strong>
|
|
||||||
</li>
|
|
||||||
<li>Enter a label for your token (like "SurfSense Connector")</li>
|
|
||||||
<li>
|
|
||||||
Click <strong>Create</strong>
|
|
||||||
</li>
|
|
||||||
<li>Copy the generated token as it will only be shown once</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">Step 2: Grant necessary access</h4>
|
|
||||||
<p className="text-muted-foreground mb-3">
|
|
||||||
The API Token will have access to all projects and issues that your user
|
|
||||||
account can see. Make sure your account has appropriate permissions for
|
|
||||||
the projects you want to index.
|
|
||||||
</p>
|
|
||||||
<Alert className="bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Data Privacy</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Only issues, comments, and basic metadata will be indexed. Jira
|
|
||||||
attachments and linked files are not indexed by this connector.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="indexing">
|
|
||||||
<AccordionTrigger className="text-lg font-medium">Indexing</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-4">
|
|
||||||
<ol className="list-decimal pl-5 space-y-3">
|
|
||||||
<li>
|
|
||||||
Navigate to the Connector Dashboard and select the <strong>Jira</strong>{" "}
|
|
||||||
Connector.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Enter your <strong>Jira Instance URL</strong> (e.g.,
|
|
||||||
https://yourcompany.atlassian.net)
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Place your <strong>Personal Access Token</strong> in the form field.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Click <strong>Connect</strong> to establish the connection.
|
|
||||||
</li>
|
|
||||||
<li>Once connected, your Jira issues will be indexed automatically.</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<Alert className="bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>What Gets Indexed</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
<p className="mb-2">The Jira connector indexes the following data:</p>
|
|
||||||
<ul className="list-disc pl-5">
|
|
||||||
<li>Issue keys and summaries (e.g., PROJ-123)</li>
|
|
||||||
<li>Issue descriptions</li>
|
|
||||||
<li>Issue comments and discussion threads</li>
|
|
||||||
<li>Issue status, priority, and type information</li>
|
|
||||||
<li>Assignee and reporter information</li>
|
|
||||||
<li>Project information</li>
|
|
||||||
</ul>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,379 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "@/components/ui/accordion";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
|
||||||
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_'",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
type LinearConnectorFormValues = z.infer<typeof linearConnectorFormSchema>;
|
|
||||||
|
|
||||||
export default function LinearConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
|
|
||||||
// Initialize the form
|
|
||||||
const form = useForm<LinearConnectorFormValues>({
|
|
||||||
resolver: zodResolver(linearConnectorFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "Linear Connector",
|
|
||||||
api_key: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = async (values: LinearConnectorFormValues) => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
name: values.name,
|
|
||||||
connector_type: EnumConnectorName.LINEAR_CONNECTOR,
|
|
||||||
config: {
|
|
||||||
LINEAR_API_KEY: values.api_key,
|
|
||||||
},
|
|
||||||
is_indexable: true,
|
|
||||||
last_indexed_at: null,
|
|
||||||
periodic_indexing_enabled: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("Linear connector created successfully!");
|
|
||||||
|
|
||||||
// Navigate back to connectors page
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating connector:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to Connectors
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.LINEAR_CONNECTOR, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Linear</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Connect your Linear workspace to search issues and projects.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Tabs defaultValue="connect" className="w-full">
|
|
||||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
|
||||||
<TabsTrigger value="connect">Connect</TabsTrigger>
|
|
||||||
<TabsTrigger value="documentation">Documentation</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="connect">
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">Connect Linear Workspace</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Integrate with Linear to search and retrieve information from your issues and
|
|
||||||
comments. This connector can index your Linear content for search.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Alert className="mb-6 bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Linear API Key Required</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
You'll need a Linear API Key to use this connector. You can create a Linear API
|
|
||||||
key 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>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My Linear Connector" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
A friendly name to identify this connector.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="api_key"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Linear API Key</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="password" placeholder="lin_api_..." {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your Linear API Key will be encrypted and stored securely. It typically
|
|
||||||
starts with "lin_api_".
|
|
||||||
</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 Linear
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
<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 Linear integration:</h4>
|
|
||||||
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
|
||||||
<li>Search through all your Linear issues and comments</li>
|
|
||||||
<li>Access issue titles, descriptions, and full discussion threads</li>
|
|
||||||
<li>Connect your team's project management directly to your search space</li>
|
|
||||||
<li>Keep your search results up-to-date with latest Linear content</li>
|
|
||||||
<li>Index your Linear issues for enhanced search capabilities</li>
|
|
||||||
</ul>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="documentation">
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">Linear Connector Documentation</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Learn how to set up and use the Linear connector to index your project management
|
|
||||||
data.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-semibold mb-2">How it works</h3>
|
|
||||||
<p className="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-muted-foreground">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
|
||||||
<AccordionItem value="authorization">
|
|
||||||
<AccordionTrigger className="text-lg font-medium">
|
|
||||||
Authorization
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-4">
|
|
||||||
<Alert className="bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Read-Only Access is Sufficient</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
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-6">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">Step 1: Create an API key</h4>
|
|
||||||
<ol className="list-decimal pl-5 space-y-3">
|
|
||||||
<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="font-medium mb-2">Step 2: Grant necessary access</h4>
|
|
||||||
<p className="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-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Data Privacy</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Only issues and comments will be indexed. Linear attachments and
|
|
||||||
linked files are not indexed by this connector.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="indexing">
|
|
||||||
<AccordionTrigger className="text-lg font-medium">Indexing</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-4">
|
|
||||||
<ol className="list-decimal pl-5 space-y-3">
|
|
||||||
<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-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>What Gets Indexed</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
<p className="mb-2">The Linear connector indexes the following data:</p>
|
|
||||||
<ul className="list-disc pl-5">
|
|
||||||
<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>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,219 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
|
||||||
const linkupApiFormSchema = z.object({
|
|
||||||
name: z.string().min(3, {
|
|
||||||
message: "Connector name must be at least 3 characters.",
|
|
||||||
}),
|
|
||||||
api_key: z.string().min(10, {
|
|
||||||
message: "API key is required and must be valid.",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
type LinkupApiFormValues = z.infer<typeof linkupApiFormSchema>;
|
|
||||||
|
|
||||||
export default function LinkupApiPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
|
|
||||||
// Initialize the form
|
|
||||||
const form = useForm<LinkupApiFormValues>({
|
|
||||||
resolver: zodResolver(linkupApiFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "Linkup API Connector",
|
|
||||||
api_key: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = async (values: LinkupApiFormValues) => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
name: values.name,
|
|
||||||
connector_type: EnumConnectorName.LINKUP_API,
|
|
||||||
config: {
|
|
||||||
LINKUP_API_KEY: values.api_key,
|
|
||||||
},
|
|
||||||
is_indexable: false,
|
|
||||||
last_indexed_at: null,
|
|
||||||
periodic_indexing_enabled: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("Linkup API connector created successfully!");
|
|
||||||
|
|
||||||
// Navigate back to connectors page
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating connector:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to Connectors
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.LINKUP_API, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Linkup API</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Connect Linkup API for enhanced search capabilities.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">Connect Linkup API</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Integrate with Linkup API to enhance your search capabilities with AI-powered search
|
|
||||||
results.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Alert className="mb-6 bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>API Key Required</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
You'll need a Linkup API key to use this connector. You can get one by signing up at{" "}
|
|
||||||
<a
|
|
||||||
href="https://linkup.so"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
linkup.so
|
|
||||||
</a>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My Linkup API Connector" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>A friendly name to identify this connector.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="api_key"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Linkup API Key</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="password" placeholder="Enter your Linkup API key" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your API key will be encrypted and stored securely.
|
|
||||||
</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 Linkup API
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
<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 Linkup API:</h4>
|
|
||||||
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
|
||||||
<li>AI-powered search results tailored to your queries</li>
|
|
||||||
<li>Real-time information from the web</li>
|
|
||||||
<li>Enhanced search capabilities for your projects</li>
|
|
||||||
</ul>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,268 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, Key, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
|
||||||
const lumaConnectorFormSchema = z.object({
|
|
||||||
name: z.string().min(3, {
|
|
||||||
message: "Connector name must be at least 3 characters.",
|
|
||||||
}),
|
|
||||||
api_key: z.string().min(10, {
|
|
||||||
message: "API key is required and must be valid.",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
type LumaConnectorFormValues = z.infer<typeof lumaConnectorFormSchema>;
|
|
||||||
|
|
||||||
export default function LumaConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [doesConnectorExist, setDoesConnectorExist] = useState(false);
|
|
||||||
|
|
||||||
const { data: connectors } = useAtomValue(connectorsAtom);
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
|
|
||||||
// Initialize the form
|
|
||||||
const form = useForm<LumaConnectorFormValues>({
|
|
||||||
resolver: zodResolver(lumaConnectorFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "Luma Events",
|
|
||||||
api_key: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { refetch: fetchConnectors } = useAtomValue(connectorsAtom);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchConnectors().then((data) => {
|
|
||||||
const connectors = data.data || [];
|
|
||||||
const connector = connectors.find(
|
|
||||||
(c: SearchSourceConnector) => c.connector_type === EnumConnectorName.LUMA_CONNECTOR
|
|
||||||
);
|
|
||||||
if (connector) {
|
|
||||||
setDoesConnectorExist(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = async (values: LumaConnectorFormValues) => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
name: values.name,
|
|
||||||
connector_type: EnumConnectorName.LUMA_CONNECTOR,
|
|
||||||
config: {
|
|
||||||
LUMA_API_KEY: values.api_key,
|
|
||||||
},
|
|
||||||
is_indexable: true,
|
|
||||||
last_indexed_at: null,
|
|
||||||
periodic_indexing_enabled: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("Luma connector created successfully!");
|
|
||||||
|
|
||||||
// Navigate back to connectors page
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating connector:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-2xl">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<Link
|
|
||||||
href={`/dashboard/${searchSpaceId}/connectors/add`}
|
|
||||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground mb-4"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to connectors
|
|
||||||
</Link>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.LUMA_CONNECTOR, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Luma</h1>
|
|
||||||
<p className="text-muted-foreground">Connect your Luma account to search events.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Connection Card */}
|
|
||||||
{!doesConnectorExist ? (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Connect Your Luma Account</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Enter your Luma API key to connect your account. We'll use this to access your
|
|
||||||
events in read-only mode.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My Luma Events" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
A friendly name to identify this connector.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="api_key"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>API Key</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="password" placeholder="Enter your Luma API key" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your API key will be encrypted and stored securely.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="space-y-2 pt-2">
|
|
||||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Read-only access to your Luma events</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>Access works even when you're offline</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
|
||||||
<Check className="h-4 w-4 text-green-500" />
|
|
||||||
<span>You can disconnect anytime</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex justify-between">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
Connecting...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Key className="mr-2 h-4 w-4" />
|
|
||||||
Connect Luma
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
/* Success Card */
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>✅ Your Luma account is successfully connected!</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Help Section */}
|
|
||||||
{!doesConnectorExist && (
|
|
||||||
<Card className="mt-6">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-lg">How It Works</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">1. Get Your API Key</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Log into your Luma account and navigate to your account settings to generate an
|
|
||||||
API key.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">2. Enter Your API Key</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Paste your API key in the field above. We'll use this to securely access your
|
|
||||||
events with read-only permissions.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,390 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "@/components/ui/accordion";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
|
||||||
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.",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
type NotionConnectorFormValues = z.infer<typeof notionConnectorFormSchema>;
|
|
||||||
|
|
||||||
export default function NotionConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
|
|
||||||
// Initialize the form
|
|
||||||
const form = useForm<NotionConnectorFormValues>({
|
|
||||||
resolver: zodResolver(notionConnectorFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "Notion Connector",
|
|
||||||
integration_token: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = async (values: NotionConnectorFormValues) => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
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: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("Notion connector created successfully!");
|
|
||||||
|
|
||||||
// Navigate back to connectors page
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating connector:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to Connectors
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.NOTION_CONNECTOR, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Notion</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Connect your Notion workspace to search pages and databases.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Tabs defaultValue="connect" className="w-full">
|
|
||||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
|
||||||
<TabsTrigger value="connect">Connect</TabsTrigger>
|
|
||||||
<TabsTrigger value="documentation">Documentation</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="connect">
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">Connect Notion Workspace</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Integrate with Notion to search and retrieve information from your workspace pages
|
|
||||||
and databases. This connector can index your Notion content for search.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Alert className="mb-6 bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Notion Integration Token Required</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
You'll need a Notion Integration Token to use this connector. You can create a
|
|
||||||
Notion integration and get the token from{" "}
|
|
||||||
<a
|
|
||||||
href="https://www.notion.so/my-integrations"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
Notion Integrations Dashboard
|
|
||||||
</a>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My Notion Connector" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
A friendly name to identify this connector.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="integration_token"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Notion Integration Token</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="password" placeholder="ntn_.." {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your Notion Integration Token will be encrypted and stored securely. It
|
|
||||||
typically starts with "ntn_".
|
|
||||||
</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 Notion
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
<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 Notion integration:</h4>
|
|
||||||
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
|
||||||
<li>Search through your Notion pages and databases</li>
|
|
||||||
<li>Access documents, wikis, and knowledge bases</li>
|
|
||||||
<li>Connect your team's knowledge directly to your search space</li>
|
|
||||||
<li>Keep your search results up-to-date with latest Notion content</li>
|
|
||||||
<li>Index your Notion documents for enhanced search capabilities</li>
|
|
||||||
</ul>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="documentation">
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">Notion Connector Documentation</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Learn how to set up and use the Notion connector to index your workspace data.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-semibold mb-2">How it works</h3>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
The Notion connector uses the Notion search API to fetch all pages that the
|
|
||||||
connector has access to within a workspace.
|
|
||||||
</p>
|
|
||||||
<ul className="mt-2 list-disc pl-5 text-muted-foreground">
|
|
||||||
<li>
|
|
||||||
For follow up indexing runs, the connector only retrieves pages that have been
|
|
||||||
updated since the last indexing attempt.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Indexing is configured to run every <strong>10 minutes</strong>, so page
|
|
||||||
updates should appear within 10 minutes.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
|
||||||
<AccordionItem value="authorization">
|
|
||||||
<AccordionTrigger className="text-lg font-medium">
|
|
||||||
Authorization
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-4">
|
|
||||||
<Alert className="bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>No Admin Access Required</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
There's no requirement to be an Admin to share information with an
|
|
||||||
integration. Any member can share pages and databases with it.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">Step 1: Create an integration</h4>
|
|
||||||
<ol className="list-decimal pl-5 space-y-3">
|
|
||||||
<li>
|
|
||||||
Visit{" "}
|
|
||||||
<a
|
|
||||||
href="https://www.notion.com/my-integrations"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
https://www.notion.com/my-integrations
|
|
||||||
</a>{" "}
|
|
||||||
in your browser.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Click the <strong>+ New integration</strong> button.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Name the integration (something like "Search Connector" could work).
|
|
||||||
</li>
|
|
||||||
<li>Select "Read content" as the only capability required.</li>
|
|
||||||
<li>
|
|
||||||
Click <strong>Submit</strong> to create the integration.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
On the next page, you'll find your Notion integration token. Make a
|
|
||||||
copy of it as you'll need it to configure the connector.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">
|
|
||||||
Step 2: Share pages/databases with your integration
|
|
||||||
</h4>
|
|
||||||
<p className="text-muted-foreground mb-3">
|
|
||||||
To keep your information secure, integrations don't have access to any
|
|
||||||
pages or databases in the workspace at first. You must share specific
|
|
||||||
pages with an integration in order for the connector to access those
|
|
||||||
pages.
|
|
||||||
</p>
|
|
||||||
<ol className="list-decimal pl-5 space-y-3">
|
|
||||||
<li>Go to the page/database in your workspace.</li>
|
|
||||||
<li>
|
|
||||||
Click the <code>•••</code> on the top right corner of the page.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Scroll to the bottom of the pop-up and click{" "}
|
|
||||||
<strong>Add connections</strong>.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Search for and select the new integration in the{" "}
|
|
||||||
<code>Search for connections...</code> menu.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Important:</strong>
|
|
||||||
<ul className="list-disc pl-5 mt-1">
|
|
||||||
<li>
|
|
||||||
If you've added a page, all child pages also become accessible.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
If you've added a database, all rows (and their children) become
|
|
||||||
accessible.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="indexing">
|
|
||||||
<AccordionTrigger className="text-lg font-medium">Indexing</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-4">
|
|
||||||
<ol className="list-decimal pl-5 space-y-3">
|
|
||||||
<li>
|
|
||||||
Navigate to the Connector Dashboard and select the <strong>Notion</strong>{" "}
|
|
||||||
Connector.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Place the <strong>Integration Token</strong> under{" "}
|
|
||||||
<strong>Step 1 Provide Credentials</strong>.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Click <strong>Connect</strong> to establish the connection.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<Alert className="bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Indexing Behavior</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
The Notion connector currently indexes everything it has access to. If you
|
|
||||||
want to limit specific content being indexed, simply unshare the database
|
|
||||||
from Notion with the integration.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export default function AddConnectorRedirect() {
|
|
||||||
const params = useParams();
|
|
||||||
const router = useRouter();
|
|
||||||
const search_space_id = params.search_space_id as string;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
router.replace(`/dashboard/${search_space_id}/sources/add?tab=connectors`);
|
|
||||||
}, [search_space_id, router]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
@ -1,370 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
|
|
||||||
const searxngFormSchema = z.object({
|
|
||||||
name: z.string().min(3, {
|
|
||||||
message: "Connector name must be at least 3 characters.",
|
|
||||||
}),
|
|
||||||
host: z
|
|
||||||
.string({ required_error: "Host is required." })
|
|
||||||
.url({ message: "Enter a valid SearxNG host URL (e.g. https://searxng.example.org)." }),
|
|
||||||
api_key: z.string().optional(),
|
|
||||||
engines: z.string().optional(),
|
|
||||||
categories: z.string().optional(),
|
|
||||||
language: z.string().optional(),
|
|
||||||
safesearch: z
|
|
||||||
.string()
|
|
||||||
.regex(/^[0-2]?$/, { message: "SafeSearch must be 0, 1, or 2." })
|
|
||||||
.optional(),
|
|
||||||
verify_ssl: z.boolean().default(true),
|
|
||||||
});
|
|
||||||
|
|
||||||
type SearxngFormValues = z.infer<typeof searxngFormSchema>;
|
|
||||||
|
|
||||||
const parseCommaSeparated = (value?: string | null) => {
|
|
||||||
if (!value) return undefined;
|
|
||||||
const items = value
|
|
||||||
.split(",")
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter((item) => item.length > 0);
|
|
||||||
return items.length > 0 ? items : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SearxngConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
|
|
||||||
const form = useForm<SearxngFormValues>({
|
|
||||||
resolver: zodResolver(searxngFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "SearxNG Connector",
|
|
||||||
host: "",
|
|
||||||
api_key: "",
|
|
||||||
engines: "",
|
|
||||||
categories: "",
|
|
||||||
language: "",
|
|
||||||
safesearch: "",
|
|
||||||
verify_ssl: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (values: SearxngFormValues) => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
const config: Record<string, unknown> = {
|
|
||||||
SEARXNG_HOST: values.host.trim(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const apiKey = values.api_key?.trim();
|
|
||||||
if (apiKey) config.SEARXNG_API_KEY = apiKey;
|
|
||||||
|
|
||||||
const engines = parseCommaSeparated(values.engines);
|
|
||||||
if (engines) config.SEARXNG_ENGINES = engines;
|
|
||||||
|
|
||||||
const categories = parseCommaSeparated(values.categories);
|
|
||||||
if (categories) config.SEARXNG_CATEGORIES = categories;
|
|
||||||
|
|
||||||
const language = values.language?.trim();
|
|
||||||
if (language) config.SEARXNG_LANGUAGE = language;
|
|
||||||
|
|
||||||
const safesearch = values.safesearch?.trim();
|
|
||||||
if (safesearch) {
|
|
||||||
const parsed = Number(safesearch);
|
|
||||||
if (!Number.isNaN(parsed)) {
|
|
||||||
config.SEARXNG_SAFESEARCH = parsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include verify flag only when disabled to keep config minimal
|
|
||||||
if (values.verify_ssl === false) {
|
|
||||||
config.SEARXNG_VERIFY_SSL = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
name: values.name,
|
|
||||||
connector_type: EnumConnectorName.SEARXNG_API,
|
|
||||||
config,
|
|
||||||
is_indexable: false,
|
|
||||||
last_indexed_at: null,
|
|
||||||
periodic_indexing_enabled: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("SearxNG connector created successfully!");
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating SearxNG connector:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to Connectors
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.SEARXNG_API, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect SearxNG</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Bring your self-hosted SearxNG meta-search engine into SurfSense.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">Connect SearxNG</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Integrate SurfSense with any SearxNG instance to broaden your search coverage while
|
|
||||||
preserving privacy and control.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Alert className="mb-6 bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>SearxNG Instance Required</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
You need access to a running SearxNG instance. Refer to the{" "}
|
|
||||||
<a
|
|
||||||
href="https://docs.searxng.org/admin/installation-docker.html"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
SearxNG installation guide
|
|
||||||
</a>{" "}
|
|
||||||
for setup instructions. If your instance requires an API key, include it below.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My SearxNG Connector" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>A friendly name to identify this connector.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="host"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>SearxNG Host</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="https://searxng.example.org" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Provide the full base URL to your SearxNG instance. Include the protocol
|
|
||||||
(http/https).
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="api_key"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>API Key (optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="Enter API key if your instance requires one"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Leave empty if your SearxNG instance does not enforce API keys.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="engines"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Engines (optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="google,bing,duckduckgo" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Comma-separated list to target specific engines.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="categories"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Categories (optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="general,it,science" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Comma-separated list of SearxNG categories.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="language"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Preferred Language (optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="en-US" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
IETF language tag (e.g. en, en-US). Leave blank to inherit defaults.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="safesearch"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>SafeSearch Level (optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="0 (off), 1 (moderate), 2 (strict)" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Set 0, 1, or 2 to adjust SafeSearch filtering. Leave blank to use the
|
|
||||||
instance default.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="verify_ssl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
|
||||||
<div>
|
|
||||||
<FormLabel>Verify SSL Certificates</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Disable only when connecting to instances with self-signed certificates.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CardFooter className="flex justify-end px-0">
|
|
||||||
<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 SearxNG
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,421 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "@/components/ui/accordion";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
|
||||||
const slackConnectorFormSchema = z.object({
|
|
||||||
name: z.string().min(3, {
|
|
||||||
message: "Connector name must be at least 3 characters.",
|
|
||||||
}),
|
|
||||||
bot_token: z.string().min(10, {
|
|
||||||
message: "Bot User OAuth Token is required and must be valid.",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
type SlackConnectorFormValues = z.infer<typeof slackConnectorFormSchema>;
|
|
||||||
|
|
||||||
export default function SlackConnectorPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
|
|
||||||
// Initialize the form
|
|
||||||
const form = useForm<SlackConnectorFormValues>({
|
|
||||||
resolver: zodResolver(slackConnectorFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "Slack Connector",
|
|
||||||
bot_token: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = async (values: SlackConnectorFormValues) => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
name: values.name,
|
|
||||||
connector_type: EnumConnectorName.SLACK_CONNECTOR,
|
|
||||||
config: {
|
|
||||||
SLACK_BOT_TOKEN: values.bot_token,
|
|
||||||
},
|
|
||||||
is_indexable: true,
|
|
||||||
last_indexed_at: null,
|
|
||||||
periodic_indexing_enabled: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("Slack connector created successfully!");
|
|
||||||
|
|
||||||
// Navigate back to connectors page
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating connector:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to Connectors
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.SLACK_CONNECTOR, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Slack</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Connect your Slack workspace to search messages and channels.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Tabs defaultValue="connect" className="w-full">
|
|
||||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
|
||||||
<TabsTrigger value="connect">Connect</TabsTrigger>
|
|
||||||
<TabsTrigger value="documentation">Documentation</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="connect">
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">Connect Slack Workspace</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Integrate with Slack to search and retrieve information from your workspace
|
|
||||||
channels and conversations. This connector can index your Slack messages for
|
|
||||||
search.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Alert className="mb-6 bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Bot User OAuth Token Required</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
You'll need a Slack Bot User OAuth Token to use this connector. You can create a
|
|
||||||
Slack app and get the token from{" "}
|
|
||||||
<a
|
|
||||||
href="https://api.slack.com/apps"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
Slack API Dashboard
|
|
||||||
</a>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My Slack Connector" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
A friendly name to identify this connector.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="bot_token"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Slack Bot User OAuth Token</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="password" placeholder="xoxb-..." {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your Bot User OAuth Token will be encrypted and stored securely. It
|
|
||||||
typically starts with "xoxb-".
|
|
||||||
</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 Slack
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
<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 Slack integration:</h4>
|
|
||||||
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
|
||||||
<li>Search through your Slack channels and conversations</li>
|
|
||||||
<li>Access historical messages and shared files</li>
|
|
||||||
<li>Connect your team's knowledge directly to your search space</li>
|
|
||||||
<li>Keep your search results up-to-date with latest communications</li>
|
|
||||||
<li>Index your Slack messages for enhanced search capabilities</li>
|
|
||||||
</ul>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="documentation">
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">Slack Connector Documentation</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Learn how to set up and use the Slack connector to index your workspace data.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-semibold mb-2">How it works</h3>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
The Slack connector indexes all public channels for a given workspace.
|
|
||||||
</p>
|
|
||||||
<ul className="mt-2 list-disc pl-5 text-muted-foreground">
|
|
||||||
<li>
|
|
||||||
Upcoming: Support for private channels by tagging/adding the Slack Bot to
|
|
||||||
private channels.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
|
||||||
<AccordionItem value="authorization">
|
|
||||||
<AccordionTrigger className="text-lg font-medium">
|
|
||||||
Authorization
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-4">
|
|
||||||
<Alert className="bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Admin Access Required</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
You must be an admin of the Slack workspace to set up the connector.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<ol className="list-decimal pl-5 space-y-3">
|
|
||||||
<li>
|
|
||||||
Navigate and sign in to{" "}
|
|
||||||
<a
|
|
||||||
href="https://api.slack.com/apps"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
https://api.slack.com/apps
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Create a new Slack app:
|
|
||||||
<ul className="list-disc pl-5 mt-1">
|
|
||||||
<li>
|
|
||||||
Click the <strong>Create New App</strong> button in the top right.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Select <strong>From an app manifest</strong> option.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Select the relevant workspace from the dropdown and click{" "}
|
|
||||||
<strong>Next</strong>.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Select the "YAML" tab, paste the following manifest into the text box, and
|
|
||||||
click <strong>Next</strong>:
|
|
||||||
<div className="bg-muted p-4 rounded-md mt-2 overflow-x-auto">
|
|
||||||
<pre className="text-xs">
|
|
||||||
{`display_information:
|
|
||||||
name: SlackConnector
|
|
||||||
description: ReadOnly Connector for indexing
|
|
||||||
features:
|
|
||||||
bot_user:
|
|
||||||
display_name: SlackConnector
|
|
||||||
always_online: false
|
|
||||||
oauth_config:
|
|
||||||
scopes:
|
|
||||||
bot:
|
|
||||||
- channels:history
|
|
||||||
- channels:read
|
|
||||||
- groups:history
|
|
||||||
- groups:read
|
|
||||||
- channels:join
|
|
||||||
- im:history
|
|
||||||
- users:read
|
|
||||||
- users:read.email
|
|
||||||
- usergroups:read
|
|
||||||
settings:
|
|
||||||
org_deploy_enabled: false
|
|
||||||
socket_mode_enabled: false
|
|
||||||
token_rotation_enabled: false`}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Click the <strong>Create</strong> button.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
In the app page, navigate to the <strong>OAuth & Permissions</strong> tab
|
|
||||||
under the <strong>Features</strong> header.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Copy the <strong>Bot User OAuth Token</strong>, this will be used to
|
|
||||||
access Slack.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem value="indexing">
|
|
||||||
<AccordionTrigger className="text-lg font-medium">Indexing</AccordionTrigger>
|
|
||||||
<AccordionContent className="space-y-4">
|
|
||||||
<ol className="list-decimal pl-5 space-y-3">
|
|
||||||
<li>
|
|
||||||
Navigate to the Connector Dashboard and select the <strong>Slack</strong>{" "}
|
|
||||||
Connector.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Place the <strong>Bot User OAuth Token</strong> under{" "}
|
|
||||||
<strong>Step 1 Provide Credentials</strong>.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Click <strong>Connect</strong> to establish the connection.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<Alert className="bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>Important: Invite Bot to Channels</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
After connecting, you must invite the bot to each channel you want to
|
|
||||||
index. In each Slack channel, type:
|
|
||||||
<pre className="mt-2 bg-background p-2 rounded-md text-xs">
|
|
||||||
/invite @YourBotName
|
|
||||||
</pre>
|
|
||||||
<p className="mt-2">
|
|
||||||
Without this step, you'll get a "not_in_channel" error when the
|
|
||||||
connector tries to access channel messages.
|
|
||||||
</p>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Alert className="bg-muted mt-4">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>First Indexing</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
The first indexing pulls all of the public channels and takes longer than
|
|
||||||
future updates. Only channels where the bot has been invited will be fully
|
|
||||||
indexed.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<h4 className="font-medium mb-2">Troubleshooting:</h4>
|
|
||||||
<ul className="list-disc pl-5 space-y-2 text-muted-foreground">
|
|
||||||
<li>
|
|
||||||
<strong>not_in_channel error:</strong> If you see this error in logs, it
|
|
||||||
means the bot hasn't been invited to a channel it's trying to access.
|
|
||||||
Use the <code>/invite @YourBotName</code> command in that channel.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Alternative approach:</strong> You can add the{" "}
|
|
||||||
<code>chat:write.public</code> scope to your Slack app to allow it to
|
|
||||||
access public channels without an explicit invitation.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>For private channels:</strong> The bot must always be invited
|
|
||||||
using the <code>/invite</code> command.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,219 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { createConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
|
|
||||||
// Define the form schema with Zod
|
|
||||||
const tavilyApiFormSchema = z.object({
|
|
||||||
name: z.string().min(3, {
|
|
||||||
message: "Connector name must be at least 3 characters.",
|
|
||||||
}),
|
|
||||||
api_key: z.string().min(10, {
|
|
||||||
message: "API key is required and must be valid.",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the type for the form values
|
|
||||||
type TavilyApiFormValues = z.infer<typeof tavilyApiFormSchema>;
|
|
||||||
|
|
||||||
export default function TavilyApiPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const { mutateAsync: createConnector } = useAtomValue(createConnectorMutationAtom);
|
|
||||||
|
|
||||||
// Initialize the form
|
|
||||||
const form = useForm<TavilyApiFormValues>({
|
|
||||||
resolver: zodResolver(tavilyApiFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "Tavily API Connector",
|
|
||||||
api_key: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = async (values: TavilyApiFormValues) => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
await createConnector({
|
|
||||||
data: {
|
|
||||||
name: values.name,
|
|
||||||
connector_type: EnumConnectorName.TAVILY_API,
|
|
||||||
config: {
|
|
||||||
TAVILY_API_KEY: values.api_key,
|
|
||||||
},
|
|
||||||
is_indexable: false,
|
|
||||||
last_indexed_at: null,
|
|
||||||
periodic_indexing_enabled: false,
|
|
||||||
indexing_frequency_minutes: null,
|
|
||||||
next_scheduled_at: null,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("Tavily API connector created successfully!");
|
|
||||||
|
|
||||||
// Navigate back to connectors page
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating connector:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="mb-6"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back to Connectors
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
|
||||||
{getConnectorIcon(EnumConnectorName.TAVILY_API, "h-6 w-6")}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Connect Tavily API</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Connect Tavily API for AI-powered search capabilities.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Card className="border-2 border-border">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl font-bold">Connect Tavily API</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Integrate with Tavily API to enhance your search capabilities with AI-powered search
|
|
||||||
results.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Alert className="mb-6 bg-muted">
|
|
||||||
<Info className="h-4 w-4" />
|
|
||||||
<AlertTitle>API Key Required</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
You'll need a Tavily API key to use this connector. You can get one by signing up at{" "}
|
|
||||||
<a
|
|
||||||
href="https://tavily.com"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
tavily.com
|
|
||||||
</a>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Connector Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My Tavily API Connector" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>A friendly name to identify this connector.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="api_key"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Tavily API Key</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="password" placeholder="Enter your Tavily API key" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your API key will be encrypted and stored securely.
|
|
||||||
</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 Tavily API
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
<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 Tavily API:</h4>
|
|
||||||
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
|
||||||
<li>AI-powered search results tailored to your queries</li>
|
|
||||||
<li>Real-time information from the web</li>
|
|
||||||
<li>Enhanced search capabilities for your projects</li>
|
|
||||||
</ul>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -140,15 +140,15 @@ export function DocumentsTableShell({
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-lg font-semibold">{t("no_documents")}</h3>
|
<h3 className="text-lg font-semibold">{t("no_documents")}</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Get started by adding your first data source.
|
Get started by uploading your first document.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/sources/add`)}
|
onClick={() => router.push(`/dashboard/${searchSpaceId}/documents/upload`)}
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
>
|
>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Add Sources
|
Upload Documents
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,520 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { AlertCircle, ArrowLeft, FileText, Loader2, Save } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { hasUnsavedEditorChangesAtom, pendingEditorNavigationAtom } from "@/atoms/editor/ui.atoms";
|
|
||||||
import { BlockNoteEditor } from "@/components/DynamicBlockNoteEditor";
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { notesApiService } from "@/lib/apis/notes-api.service";
|
|
||||||
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
|
||||||
|
|
||||||
// BlockNote types
|
|
||||||
type BlockNoteInlineContent =
|
|
||||||
| string
|
|
||||||
| { text?: string; type?: string; styles?: Record<string, unknown> };
|
|
||||||
|
|
||||||
interface BlockNoteBlock {
|
|
||||||
type: string;
|
|
||||||
content?: BlockNoteInlineContent[];
|
|
||||||
children?: BlockNoteBlock[];
|
|
||||||
props?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type BlockNoteDocument = BlockNoteBlock[] | null | undefined;
|
|
||||||
|
|
||||||
interface EditorContent {
|
|
||||||
document_id: number;
|
|
||||||
title: string;
|
|
||||||
document_type?: string;
|
|
||||||
blocknote_document: BlockNoteDocument;
|
|
||||||
updated_at: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to extract title from BlockNote document
|
|
||||||
// Takes the text content from the first block (should be a heading for notes)
|
|
||||||
function extractTitleFromBlockNote(blocknoteDocument: BlockNoteDocument): string {
|
|
||||||
if (!blocknoteDocument || !Array.isArray(blocknoteDocument) || blocknoteDocument.length === 0) {
|
|
||||||
return "Untitled";
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstBlock = blocknoteDocument[0];
|
|
||||||
if (!firstBlock) {
|
|
||||||
return "Untitled";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract text from block content
|
|
||||||
// BlockNote blocks have a content array with inline content
|
|
||||||
if (firstBlock.content && Array.isArray(firstBlock.content)) {
|
|
||||||
const textContent = firstBlock.content
|
|
||||||
.map((item: BlockNoteInlineContent) => {
|
|
||||||
if (typeof item === "string") return item;
|
|
||||||
if (typeof item === "object" && item?.text) return item.text;
|
|
||||||
return "";
|
|
||||||
})
|
|
||||||
.join("")
|
|
||||||
.trim();
|
|
||||||
return textContent || "Untitled";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Untitled";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function EditorPage() {
|
|
||||||
const params = useParams();
|
|
||||||
const router = useRouter();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const documentId = params.documentId as string;
|
|
||||||
const searchSpaceId = Number(params.search_space_id);
|
|
||||||
const isNewNote = documentId === "new";
|
|
||||||
|
|
||||||
const [document, setDocument] = useState<EditorContent | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [editorContent, setEditorContent] = useState<BlockNoteDocument>(null);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
||||||
const [showUnsavedDialog, setShowUnsavedDialog] = useState(false);
|
|
||||||
|
|
||||||
// Global state for cross-component communication
|
|
||||||
const [, setGlobalHasUnsavedChanges] = useAtom(hasUnsavedEditorChangesAtom);
|
|
||||||
const [pendingNavigation, setPendingNavigation] = useAtom(pendingEditorNavigationAtom);
|
|
||||||
|
|
||||||
// Sync local unsaved changes state with global atom
|
|
||||||
useEffect(() => {
|
|
||||||
setGlobalHasUnsavedChanges(hasUnsavedChanges);
|
|
||||||
}, [hasUnsavedChanges, setGlobalHasUnsavedChanges]);
|
|
||||||
|
|
||||||
// Cleanup global state when component unmounts
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
setGlobalHasUnsavedChanges(false);
|
|
||||||
setPendingNavigation(null);
|
|
||||||
};
|
|
||||||
}, [setGlobalHasUnsavedChanges, setPendingNavigation]);
|
|
||||||
|
|
||||||
// Handle pending navigation from sidebar (e.g., when user clicks "+" to create new note)
|
|
||||||
useEffect(() => {
|
|
||||||
if (pendingNavigation) {
|
|
||||||
if (hasUnsavedChanges) {
|
|
||||||
// Show dialog to confirm navigation
|
|
||||||
setShowUnsavedDialog(true);
|
|
||||||
} else {
|
|
||||||
// No unsaved changes, navigate immediately
|
|
||||||
router.push(pendingNavigation);
|
|
||||||
setPendingNavigation(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [pendingNavigation, hasUnsavedChanges, router, setPendingNavigation]);
|
|
||||||
|
|
||||||
// Reset state when documentId changes (e.g., navigating from existing note to new note)
|
|
||||||
useEffect(() => {
|
|
||||||
setDocument(null);
|
|
||||||
setEditorContent(null);
|
|
||||||
setError(null);
|
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
setLoading(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Fetch document content - DIRECT CALL TO FASTAPI
|
|
||||||
// Skip fetching if this is a new note
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchDocument() {
|
|
||||||
// For new notes, initialize with empty state
|
|
||||||
if (isNewNote) {
|
|
||||||
setDocument({
|
|
||||||
document_id: 0,
|
|
||||||
title: "Untitled",
|
|
||||||
document_type: "NOTE",
|
|
||||||
blocknote_document: null,
|
|
||||||
updated_at: null,
|
|
||||||
});
|
|
||||||
setEditorContent(null);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = getBearerToken();
|
|
||||||
if (!token) {
|
|
||||||
console.error("No auth token found");
|
|
||||||
// Redirect to login with current path saved
|
|
||||||
redirectToLogin();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await authenticatedFetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${params.search_space_id}/documents/${documentId}/editor-content`,
|
|
||||||
{ method: "GET" }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response
|
|
||||||
.json()
|
|
||||||
.catch(() => ({ detail: "Failed to fetch document" }));
|
|
||||||
const errorMessage = errorData.detail || "Failed to fetch document";
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Check if blocknote_document exists
|
|
||||||
if (!data.blocknote_document) {
|
|
||||||
const errorMsg =
|
|
||||||
"This document does not have BlockNote content. Please re-upload the document to enable editing.";
|
|
||||||
setError(errorMsg);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDocument(data);
|
|
||||||
setEditorContent(data.blocknote_document);
|
|
||||||
setError(null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching document:", error);
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : "Failed to fetch document. Please try again.";
|
|
||||||
setError(errorMessage);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (documentId) {
|
|
||||||
fetchDocument();
|
|
||||||
}
|
|
||||||
}, [documentId, params.search_space_id, isNewNote]);
|
|
||||||
|
|
||||||
// Track changes to mark as unsaved
|
|
||||||
useEffect(() => {
|
|
||||||
if (editorContent && document) {
|
|
||||||
setHasUnsavedChanges(true);
|
|
||||||
}
|
|
||||||
}, [editorContent, document]);
|
|
||||||
|
|
||||||
// Check if this is a NOTE type document
|
|
||||||
const isNote = isNewNote || document?.document_type === "NOTE";
|
|
||||||
|
|
||||||
// Extract title dynamically from editor content for notes, otherwise use document title
|
|
||||||
const displayTitle = useMemo(() => {
|
|
||||||
if (isNote && editorContent) {
|
|
||||||
return extractTitleFromBlockNote(editorContent);
|
|
||||||
}
|
|
||||||
return document?.title || "Untitled";
|
|
||||||
}, [isNote, editorContent, document?.title]);
|
|
||||||
|
|
||||||
// TODO: Maybe add Auto-save every 30 seconds - DIRECT CALL TO FASTAPI
|
|
||||||
|
|
||||||
// Save and exit - DIRECT CALL TO FASTAPI
|
|
||||||
// For new notes, create the note first, then save
|
|
||||||
const handleSave = async () => {
|
|
||||||
const token = getBearerToken();
|
|
||||||
if (!token) {
|
|
||||||
toast.error("Please login to save");
|
|
||||||
redirectToLogin();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSaving(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// If this is a new note, create it first
|
|
||||||
if (isNewNote) {
|
|
||||||
const title = extractTitleFromBlockNote(editorContent);
|
|
||||||
|
|
||||||
// Create the note first
|
|
||||||
const note = await notesApiService.createNote({
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
title: title,
|
|
||||||
blocknote_document: editorContent || undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If there's content, save it properly and trigger reindexing
|
|
||||||
if (editorContent) {
|
|
||||||
const response = await authenticatedFetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${note.id}/save`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ blocknote_document: editorContent }),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response
|
|
||||||
.json()
|
|
||||||
.catch(() => ({ detail: "Failed to save document" }));
|
|
||||||
throw new Error(errorData.detail || "Failed to save document");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
toast.success("Note created successfully! Reindexing in background...");
|
|
||||||
|
|
||||||
// Invalidate notes query to refresh the sidebar
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ["notes", String(searchSpaceId)],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update URL to reflect the new document ID without navigation
|
|
||||||
window.history.replaceState({}, "", `/dashboard/${searchSpaceId}/editor/${note.id}`);
|
|
||||||
// Update document state to reflect the new ID
|
|
||||||
setDocument({
|
|
||||||
document_id: note.id,
|
|
||||||
title: title,
|
|
||||||
document_type: "NOTE",
|
|
||||||
blocknote_document: editorContent,
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Existing document - save normally
|
|
||||||
if (!editorContent) {
|
|
||||||
toast.error("No content to save");
|
|
||||||
setSaving(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save blocknote_document and trigger reindexing in background
|
|
||||||
const response = await authenticatedFetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${params.search_space_id}/documents/${documentId}/save`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ blocknote_document: editorContent }),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response
|
|
||||||
.json()
|
|
||||||
.catch(() => ({ detail: "Failed to save document" }));
|
|
||||||
throw new Error(errorData.detail || "Failed to save document");
|
|
||||||
}
|
|
||||||
|
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
toast.success("Document saved! Reindexing in background...");
|
|
||||||
|
|
||||||
// Invalidate notes query when updating notes to refresh the sidebar
|
|
||||||
if (isNote) {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ["notes", String(searchSpaceId)],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error saving document:", error);
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: isNewNote
|
|
||||||
? "Failed to create note. Please try again."
|
|
||||||
: "Failed to save document. Please try again.";
|
|
||||||
setError(errorMessage);
|
|
||||||
toast.error(errorMessage);
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
if (hasUnsavedChanges) {
|
|
||||||
setShowUnsavedDialog(true);
|
|
||||||
} else {
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirmLeave = () => {
|
|
||||||
setShowUnsavedDialog(false);
|
|
||||||
// Clear global unsaved state
|
|
||||||
setGlobalHasUnsavedChanges(false);
|
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
|
|
||||||
// If there's a pending navigation (from sidebar), use that; otherwise go back to chat
|
|
||||||
if (pendingNavigation) {
|
|
||||||
router.push(pendingNavigation);
|
|
||||||
setPendingNavigation(null);
|
|
||||||
} else {
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/new-chat`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancelLeave = () => {
|
|
||||||
setShowUnsavedDialog(false);
|
|
||||||
// Clear pending navigation if user cancels
|
|
||||||
setPendingNavigation(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-[400px] p-6">
|
|
||||||
<Card className="w-full max-w-md">
|
|
||||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
|
||||||
<Loader2 className="h-12 w-12 text-primary animate-spin mb-4" />
|
|
||||||
<p className="text-muted-foreground">Loading editor...</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-[400px] p-6">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="w-full max-w-md"
|
|
||||||
>
|
|
||||||
<Card className="border-destructive/50">
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<AlertCircle className="h-5 w-5 text-destructive" />
|
|
||||||
<CardTitle className="text-destructive">Error</CardTitle>
|
|
||||||
</div>
|
|
||||||
<CardDescription>{error}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Button
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/new-chat`)}
|
|
||||||
variant="outline"
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-4 w-4" />
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!document && !isNewNote) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-[400px] p-6">
|
|
||||||
<Card className="w-full max-w-md">
|
|
||||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
|
||||||
<FileText className="h-12 w-12 text-muted-foreground mb-4" />
|
|
||||||
<p className="text-muted-foreground">Document not found</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
className="flex flex-col min-h-screen w-full"
|
|
||||||
>
|
|
||||||
{/* Toolbar */}
|
|
||||||
<div className="sticky top-0 z-40 flex h-14 md:h-16 shrink-0 items-center gap-2 md:gap-4 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-3 md:px-6">
|
|
||||||
<div className="flex items-center gap-2 md:gap-3 flex-1 min-w-0">
|
|
||||||
<FileText className="h-4 w-4 md:h-5 md:w-5 text-muted-foreground shrink-0" />
|
|
||||||
<div className="flex flex-col min-w-0">
|
|
||||||
<h1 className="text-base md:text-lg font-semibold truncate">{displayTitle}</h1>
|
|
||||||
{hasUnsavedChanges && (
|
|
||||||
<p className="text-[10px] md:text-xs text-muted-foreground">Unsaved changes</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleBack}
|
|
||||||
disabled={saving}
|
|
||||||
className="gap-1 md:gap-2 px-2 md:px-4 h-8 md:h-10"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-3.5 w-3.5 md:h-4 md:w-4" />
|
|
||||||
<span className="text-xs md:text-sm">Back</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={saving}
|
|
||||||
className="gap-1 md:gap-2 px-2 md:px-4 h-8 md:h-10"
|
|
||||||
>
|
|
||||||
{saving ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="h-3.5 w-3.5 md:h-4 md:w-4 animate-spin" />
|
|
||||||
<span className="text-xs md:text-sm">
|
|
||||||
{isNewNote ? "Creating..." : "Saving..."}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Save className="h-3.5 w-3.5 md:h-4 md:w-4" />
|
|
||||||
<span className="text-xs md:text-sm">Save</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Editor Container */}
|
|
||||||
<div className="flex-1 min-h-0 overflow-hidden relative">
|
|
||||||
<div className="h-full w-full overflow-auto p-3 md:p-6">
|
|
||||||
{error && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="mb-6 max-w-4xl mx-auto"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 p-4 rounded-lg border border-destructive/50 bg-destructive/10 text-destructive">
|
|
||||||
<AlertCircle className="h-5 w-5 shrink-0" />
|
|
||||||
<p className="text-sm">{error}</p>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<BlockNoteEditor
|
|
||||||
key={documentId} // Force re-mount when document changes
|
|
||||||
initialContent={isNewNote ? undefined : editorContent}
|
|
||||||
onChange={setEditorContent}
|
|
||||||
useTitleBlock={isNote}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Unsaved Changes Dialog */}
|
|
||||||
<AlertDialog
|
|
||||||
open={showUnsavedDialog}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) handleCancelLeave();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Unsaved Changes</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
You have unsaved changes. Are you sure you want to leave?
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel onClick={handleCancelLeave}>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={handleConfirmLeave}>OK</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -38,10 +38,6 @@ export default function DashboardLayout({
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: "Database",
|
icon: "Database",
|
||||||
items: [
|
items: [
|
||||||
{
|
|
||||||
title: "Add Sources",
|
|
||||||
url: `/dashboard/${search_space_id}/sources/add`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "Manage Documents",
|
title: "Manage Documents",
|
||||||
url: `/dashboard/${search_space_id}/documents`,
|
url: `/dashboard/${search_space_id}/documents`,
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { Database } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { ConnectorsTab } from "@/components/sources/ConnectorsTab";
|
|
||||||
|
|
||||||
export default function AddSourcesPage() {
|
|
||||||
const params = useParams();
|
|
||||||
const search_space_id = params.search_space_id as string;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 px-4 min-h-[calc(100vh-64px)]">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="text-center space-y-2">
|
|
||||||
<h1 className="text-2xl sm:text-4xl font-bold tracking-tight flex items-center justify-center gap-3">
|
|
||||||
<Database className="h-6 w-6 sm:h-8 sm:w-8" />
|
|
||||||
Add Sources
|
|
||||||
</h1>
|
|
||||||
<p className="text-muted-foreground text-sm sm:text-lg">
|
|
||||||
Add your sources to your search space
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Connectors */}
|
|
||||||
<div className="mt-8">
|
|
||||||
<ConnectorsTab searchSpaceId={search_space_id} />
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -98,7 +98,6 @@ export function DashboardBreadcrumb() {
|
||||||
"new-chat": t("chat") || "Chat",
|
"new-chat": t("chat") || "Chat",
|
||||||
documents: t("documents"),
|
documents: t("documents"),
|
||||||
connectors: t("connectors"),
|
connectors: t("connectors"),
|
||||||
sources: "Sources",
|
|
||||||
logs: t("logs"),
|
logs: t("logs"),
|
||||||
settings: t("settings"),
|
settings: t("settings"),
|
||||||
editor: t("editor"),
|
editor: t("editor"),
|
||||||
|
|
@ -132,21 +131,6 @@ export function DashboardBreadcrumb() {
|
||||||
return breadcrumbs;
|
return breadcrumbs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle sources sub-sections
|
|
||||||
if (section === "sources") {
|
|
||||||
const sourceLabels: Record<string, string> = {
|
|
||||||
add: "Add Sources",
|
|
||||||
};
|
|
||||||
|
|
||||||
const sourceLabel = sourceLabels[subSection] || subSection;
|
|
||||||
breadcrumbs.push({
|
|
||||||
label: "Sources",
|
|
||||||
href: `/dashboard/${segments[1]}/sources`,
|
|
||||||
});
|
|
||||||
breadcrumbs.push({ label: sourceLabel });
|
|
||||||
return breadcrumbs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle documents sub-sections
|
// Handle documents sub-sections
|
||||||
if (section === "documents") {
|
if (section === "documents") {
|
||||||
const documentLabels: Record<string, string> = {
|
const documentLabels: Record<string, string> = {
|
||||||
|
|
@ -257,7 +241,7 @@ export function DashboardBreadcrumb() {
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
{breadcrumbs.map((item, index) => (
|
{breadcrumbs.map((item, index) => (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={item.href || item.label}>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
{index === breadcrumbs.length - 1 ? (
|
{index === breadcrumbs.length - 1 ? (
|
||||||
<BreadcrumbPage>{item.label}</BreadcrumbPage>
|
<BreadcrumbPage>{item.label}</BreadcrumbPage>
|
||||||
|
|
|
||||||
|
|
@ -169,10 +169,6 @@ const defaultData = {
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: "Database",
|
icon: "Database",
|
||||||
items: [
|
items: [
|
||||||
{
|
|
||||||
title: "Add Sources",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "Manage Documents",
|
title: "Manage Documents",
|
||||||
url: "#",
|
url: "#",
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ export function NavMain({ items, onSourcesExpandedChange }: NavMainProps) {
|
||||||
Researcher: "researcher",
|
Researcher: "researcher",
|
||||||
"Manage LLMs": "manage_llms",
|
"Manage LLMs": "manage_llms",
|
||||||
Sources: "sources",
|
Sources: "sources",
|
||||||
"Add Sources": "add_sources",
|
|
||||||
"Manage Documents": "manage_documents",
|
"Manage Documents": "manage_documents",
|
||||||
"Manage Connectors": "manage_connectors",
|
"Manage Connectors": "manage_connectors",
|
||||||
Podcasts: "podcasts",
|
Podcasts: "podcasts",
|
||||||
|
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { IconChevronDown, IconChevronRight } from "@tabler/icons-react";
|
|
||||||
import { AnimatePresence, motion, type Variants } from "motion/react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
|
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
||||||
import { connectorCategories } from "./connector-data";
|
|
||||||
|
|
||||||
interface ConnectorsTabProps {
|
|
||||||
searchSpaceId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ConnectorsTab({ searchSpaceId }: ConnectorsTabProps) {
|
|
||||||
const t = useTranslations("add_connector");
|
|
||||||
const [expandedCategories, setExpandedCategories] = useState<string[]>([
|
|
||||||
"web-search",
|
|
||||||
"messaging",
|
|
||||||
"project-management",
|
|
||||||
"documentation",
|
|
||||||
"development",
|
|
||||||
"databases",
|
|
||||||
"productivity",
|
|
||||||
"web-crawling",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const toggleCategory = (categoryId: string) => {
|
|
||||||
setExpandedCategories((prev) =>
|
|
||||||
prev.includes(categoryId) ? prev.filter((id) => id !== categoryId) : [...prev, categoryId]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cardVariants: Variants = {
|
|
||||||
hidden: { opacity: 0, y: 20 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
transition: {
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 260,
|
|
||||||
damping: 20,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hover: {
|
|
||||||
scale: 1.02,
|
|
||||||
transition: {
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 400,
|
|
||||||
damping: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const staggerContainer = {
|
|
||||||
hidden: { opacity: 0 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
transition: {
|
|
||||||
staggerChildren: 0.1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
|
||||||
{connectorCategories.map((category) => (
|
|
||||||
<div key={category.id} className="rounded-lg border bg-card text-card-foreground shadow-sm">
|
|
||||||
<Collapsible
|
|
||||||
open={expandedCategories.includes(category.id)}
|
|
||||||
onOpenChange={() => toggleCategory(category.id)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between space-x-4 p-4">
|
|
||||||
<h3 className="text-lg sm:text-xl font-semibold">{t(category.title)}</h3>
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
<Button variant="ghost" size="sm" className="w-9 p-0 hover:bg-muted">
|
|
||||||
<motion.div
|
|
||||||
animate={{
|
|
||||||
rotate: expandedCategories.includes(category.id) ? 180 : 0,
|
|
||||||
}}
|
|
||||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
|
||||||
>
|
|
||||||
<IconChevronDown className="h-5 w-5" />
|
|
||||||
</motion.div>
|
|
||||||
<span className="sr-only">Toggle</span>
|
|
||||||
</Button>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CollapsibleContent>
|
|
||||||
<AnimatePresence>
|
|
||||||
<motion.div
|
|
||||||
className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 p-4"
|
|
||||||
variants={staggerContainer}
|
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
exit="hidden"
|
|
||||||
>
|
|
||||||
{category.connectors.map((connector) => (
|
|
||||||
<motion.div
|
|
||||||
key={connector.id}
|
|
||||||
variants={cardVariants}
|
|
||||||
whileHover="hover"
|
|
||||||
className="col-span-1"
|
|
||||||
>
|
|
||||||
<Card className="h-full flex flex-col overflow-hidden border-transparent transition-all duration-200 hover:border-primary/50">
|
|
||||||
<CardHeader className="flex-row items-center gap-4 pb-2">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10 dark:bg-primary/20">
|
|
||||||
<motion.div
|
|
||||||
whileHover={{ rotate: 5, scale: 1.1 }}
|
|
||||||
className="text-primary"
|
|
||||||
>
|
|
||||||
{connector.icon}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-medium">{connector.title}</h3>
|
|
||||||
{connector.status === "coming-soon" && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs bg-amber-100 dark:bg-amber-950 text-amber-800 dark:text-amber-300 border-amber-200 dark:border-amber-800"
|
|
||||||
>
|
|
||||||
{t("coming_soon")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{connector.status === "connected" && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs bg-green-100 dark:bg-green-950 text-green-800 dark:text-green-300 border-green-200 dark:border-green-800"
|
|
||||||
>
|
|
||||||
{t("connected")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="pb-4">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t(connector.description)}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
|
|
||||||
<CardFooter className="mt-auto pt-2">
|
|
||||||
{connector.status === "available" && (
|
|
||||||
<Link
|
|
||||||
href={`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<Button variant="default" className="w-full group">
|
|
||||||
<span>{t("connect")}</span>
|
|
||||||
<motion.div
|
|
||||||
className="ml-1"
|
|
||||||
initial={{ x: 0 }}
|
|
||||||
whileHover={{ x: 3 }}
|
|
||||||
transition={{
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 400,
|
|
||||||
damping: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconChevronRight className="h-4 w-4" />
|
|
||||||
</motion.div>
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{connector.status === "coming-soon" && (
|
|
||||||
<Button variant="outline" disabled className="w-full opacity-70">
|
|
||||||
{t("coming_soon")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{connector.status === "connected" && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full border-green-500 text-green-600 hover:bg-green-50 dark:hover:bg-green-950"
|
|
||||||
>
|
|
||||||
{t("manage")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
import { IconBrandWindows, IconBrandZoom } from "@tabler/icons-react";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
import type { ConnectorCategory } from "./types";
|
|
||||||
|
|
||||||
export const connectorCategories: ConnectorCategory[] = [
|
|
||||||
{
|
|
||||||
id: "web-search",
|
|
||||||
title: "web_search",
|
|
||||||
connectors: [
|
|
||||||
{
|
|
||||||
id: "tavily-api",
|
|
||||||
title: "Tavily API",
|
|
||||||
description: "tavily_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.TAVILY_API, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "searxng",
|
|
||||||
title: "SearxNG",
|
|
||||||
description: "searxng_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.SEARXNG_API, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "linkup-api",
|
|
||||||
title: "Linkup API",
|
|
||||||
description: "linkup_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.LINKUP_API, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "baidu-search-api",
|
|
||||||
title: "Baidu Search",
|
|
||||||
description: "baidu_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.BAIDU_SEARCH_API, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "messaging",
|
|
||||||
title: "messaging",
|
|
||||||
connectors: [
|
|
||||||
{
|
|
||||||
id: "slack-connector",
|
|
||||||
title: "Slack",
|
|
||||||
description: "slack_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.SLACK_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "discord-connector",
|
|
||||||
title: "Discord",
|
|
||||||
description: "discord_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.DISCORD_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "ms-teams",
|
|
||||||
title: "Microsoft Teams",
|
|
||||||
description: "teams_desc",
|
|
||||||
icon: <IconBrandWindows className="h-6 w-6" />,
|
|
||||||
status: "coming-soon",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "project-management",
|
|
||||||
title: "project_management",
|
|
||||||
connectors: [
|
|
||||||
{
|
|
||||||
id: "linear-connector",
|
|
||||||
title: "Linear",
|
|
||||||
description: "linear_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.LINEAR_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "jira-connector",
|
|
||||||
title: "Jira",
|
|
||||||
description: "jira_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.JIRA_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "clickup-connector",
|
|
||||||
title: "ClickUp",
|
|
||||||
description: "clickup_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.CLICKUP_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "documentation",
|
|
||||||
title: "documentation",
|
|
||||||
connectors: [
|
|
||||||
{
|
|
||||||
id: "notion-connector",
|
|
||||||
title: "Notion",
|
|
||||||
description: "notion_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.NOTION_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "confluence-connector",
|
|
||||||
title: "Confluence",
|
|
||||||
description: "confluence_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.CONFLUENCE_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "bookstack-connector",
|
|
||||||
title: "BookStack",
|
|
||||||
description: "bookstack_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.BOOKSTACK_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "development",
|
|
||||||
title: "development",
|
|
||||||
connectors: [
|
|
||||||
{
|
|
||||||
id: "github-connector",
|
|
||||||
title: "GitHub",
|
|
||||||
description: "github_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.GITHUB_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "databases",
|
|
||||||
title: "databases",
|
|
||||||
connectors: [
|
|
||||||
{
|
|
||||||
id: "elasticsearch-connector",
|
|
||||||
title: "Elasticsearch",
|
|
||||||
description: "elasticsearch_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.ELASTICSEARCH_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "airtable-connector",
|
|
||||||
title: "Airtable",
|
|
||||||
description: "airtable_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.AIRTABLE_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "productivity",
|
|
||||||
title: "productivity",
|
|
||||||
connectors: [
|
|
||||||
{
|
|
||||||
id: "google-calendar-connector",
|
|
||||||
title: "Google Calendar",
|
|
||||||
description: "calendar_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "google-gmail-connector",
|
|
||||||
title: "Gmail",
|
|
||||||
description: "gmail_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.GOOGLE_GMAIL_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "google-drive-connector",
|
|
||||||
title: "Google Drive",
|
|
||||||
description: "google_drive_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.GOOGLE_DRIVE_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "luma-connector",
|
|
||||||
title: "Luma",
|
|
||||||
description: "luma_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.LUMA_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "circleback-connector",
|
|
||||||
title: "Circleback",
|
|
||||||
description: "circleback_desc",
|
|
||||||
icon: getConnectorIcon(EnumConnectorName.CIRCLEBACK_CONNECTOR, "h-6 w-6"),
|
|
||||||
status: "available",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "zoom",
|
|
||||||
title: "Zoom",
|
|
||||||
description: "zoom_desc",
|
|
||||||
icon: <IconBrandZoom className="h-6 w-6" />,
|
|
||||||
status: "coming-soon",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue