feat: Moved searchconnectors association from user to searchspace

- Need to move llm configs to searchspace
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2025-10-08 21:13:01 -07:00
parent b3d8279931
commit aea09a5dad
36 changed files with 578 additions and 223 deletions

View file

@ -65,7 +65,7 @@ export default function ConnectorsPage() {
const today = new Date();
const { connectors, isLoading, error, deleteConnector, indexConnector } =
useSearchSourceConnectors();
useSearchSourceConnectors(false, parseInt(searchSpaceId));
const [connectorToDelete, setConnectorToDelete] = useState<number | null>(null);
const [indexingConnectorId, setIndexingConnectorId] = useState<number | null>(null);
const [datePickerOpen, setDatePickerOpen] = useState(false);
@ -366,12 +366,7 @@ export default function ConnectorsPage() {
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={endDate}
onSelect={setEndDate}
initialFocus
/>
<Calendar mode="single" selected={endDate} onSelect={setEndDate} initialFocus />
</PopoverContent>
</Popover>
</div>

View file

@ -83,7 +83,7 @@ export default function EditConnectorPage() {
const searchSpaceId = params.search_space_id as string;
const connectorId = parseInt(params.connector_id as string, 10);
const { connectors, updateConnector } = useSearchSourceConnectors();
const { connectors, updateConnector } = useSearchSourceConnectors(false, parseInt(searchSpaceId));
const [connector, setConnector] = useState<SearchSourceConnector | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);

View file

@ -30,10 +30,10 @@ export default function AirtableConnectorPage() {
const [isConnecting, setIsConnecting] = useState(false);
const [doesConnectorExist, setDoesConnectorExist] = useState(false);
const { fetchConnectors } = useSearchSourceConnectors();
const { fetchConnectors } = useSearchSourceConnectors(true, parseInt(searchSpaceId));
useEffect(() => {
fetchConnectors().then((data) => {
fetchConnectors(parseInt(searchSpaceId)).then((data) => {
const connector = data.find(
(c: SearchSourceConnector) => c.connector_type === EnumConnectorName.AIRTABLE_CONNECTOR
);

View file

@ -69,7 +69,7 @@ export default function ClickUpConnectorPage() {
last_indexed_at: null,
};
await createConnector(connectorData);
await createConnector(connectorData, parseInt(searchSpaceId));
toast.success("ClickUp connector created successfully!");
router.push(`/dashboard/${searchSpaceId}/connectors`);

View file

@ -77,17 +77,20 @@ export default function ConfluenceConnectorPage() {
const onSubmit = async (values: ConfluenceConnectorFormValues) => {
setIsSubmitting(true);
try {
await createConnector({
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,
await createConnector(
{
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,
},
is_indexable: true,
last_indexed_at: null,
});
parseInt(searchSpaceId)
);
toast.success("Confluence connector created successfully!");

View file

@ -73,15 +73,18 @@ export default function DiscordConnectorPage() {
const onSubmit = async (values: DiscordConnectorFormValues) => {
setIsSubmitting(true);
try {
await createConnector({
name: values.name,
connector_type: EnumConnectorName.DISCORD_CONNECTOR,
config: {
DISCORD_BOT_TOKEN: values.bot_token,
await createConnector(
{
name: values.name,
connector_type: EnumConnectorName.DISCORD_CONNECTOR,
config: {
DISCORD_BOT_TOKEN: values.bot_token,
},
is_indexable: true,
last_indexed_at: null,
},
is_indexable: true,
last_indexed_at: null,
});
parseInt(searchSpaceId)
);
toast.success("Discord connector created successfully!");
router.push(`/dashboard/${searchSpaceId}/connectors`);

View file

@ -148,16 +148,19 @@ export default function GithubConnectorPage() {
setIsCreatingConnector(true);
try {
await createConnector({
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
await createConnector(
{
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,
},
is_indexable: true,
last_indexed_at: null,
});
parseInt(searchSpaceId)
);
toast.success("GitHub connector created successfully!");
router.push(`/dashboard/${searchSpaceId}/connectors`);

View file

@ -32,10 +32,10 @@ export default function GoogleCalendarConnectorPage() {
const [isConnecting, setIsConnecting] = useState(false);
const [doesConnectorExist, setDoesConnectorExist] = useState(false);
const { fetchConnectors } = useSearchSourceConnectors();
const { fetchConnectors } = useSearchSourceConnectors(true, parseInt(searchSpaceId));
useEffect(() => {
fetchConnectors().then((data) => {
fetchConnectors(parseInt(searchSpaceId)).then((data) => {
const connector = data.find(
(c: SearchSourceConnector) =>
c.connector_type === EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR

View file

@ -32,10 +32,10 @@ export default function GoogleGmailConnectorPage() {
const [isConnecting, setIsConnecting] = useState(false);
const [doesConnectorExist, setDoesConnectorExist] = useState(false);
const { fetchConnectors } = useSearchSourceConnectors();
const { fetchConnectors } = useSearchSourceConnectors(true, parseInt(searchSpaceId));
useEffect(() => {
fetchConnectors().then((data) => {
fetchConnectors(parseInt(searchSpaceId)).then((data) => {
const connector = data.find(
(c: SearchSourceConnector) => c.connector_type === EnumConnectorName.GOOGLE_GMAIL_CONNECTOR
);

View file

@ -90,17 +90,20 @@ export default function JiraConnectorPage() {
const onSubmit = async (values: JiraConnectorFormValues) => {
setIsSubmitting(true);
try {
await createConnector({
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,
await createConnector(
{
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,
},
is_indexable: true,
last_indexed_at: null,
});
parseInt(searchSpaceId)
);
toast.success("Jira connector created successfully!");

View file

@ -77,15 +77,18 @@ export default function LinearConnectorPage() {
const onSubmit = async (values: LinearConnectorFormValues) => {
setIsSubmitting(true);
try {
await createConnector({
name: values.name,
connector_type: EnumConnectorName.LINEAR_CONNECTOR,
config: {
LINEAR_API_KEY: values.api_key,
await createConnector(
{
name: values.name,
connector_type: EnumConnectorName.LINEAR_CONNECTOR,
config: {
LINEAR_API_KEY: values.api_key,
},
is_indexable: true,
last_indexed_at: null,
},
is_indexable: true,
last_indexed_at: null,
});
parseInt(searchSpaceId)
);
toast.success("Linear connector created successfully!");

View file

@ -65,15 +65,18 @@ export default function LinkupApiPage() {
const onSubmit = async (values: LinkupApiFormValues) => {
setIsSubmitting(true);
try {
await createConnector({
name: values.name,
connector_type: EnumConnectorName.LINKUP_API,
config: {
LINKUP_API_KEY: values.api_key,
await createConnector(
{
name: values.name,
connector_type: EnumConnectorName.LINKUP_API,
config: {
LINKUP_API_KEY: values.api_key,
},
is_indexable: false,
last_indexed_at: null,
},
is_indexable: false,
last_indexed_at: null,
});
parseInt(searchSpaceId)
);
toast.success("Linkup API connector created successfully!");

View file

@ -55,7 +55,10 @@ export default function LumaConnectorPage() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [doesConnectorExist, setDoesConnectorExist] = useState(false);
const { fetchConnectors, createConnector } = useSearchSourceConnectors();
const { fetchConnectors, createConnector } = useSearchSourceConnectors(
true,
parseInt(searchSpaceId)
);
// Initialize the form
const form = useForm<LumaConnectorFormValues>({
@ -67,7 +70,7 @@ export default function LumaConnectorPage() {
});
useEffect(() => {
fetchConnectors().then((data) => {
fetchConnectors(parseInt(searchSpaceId)).then((data) => {
const connector = data.find(
(c: SearchSourceConnector) => c.connector_type === EnumConnectorName.LUMA_CONNECTOR
);
@ -81,15 +84,18 @@ export default function LumaConnectorPage() {
const onSubmit = async (values: LumaConnectorFormValues) => {
setIsSubmitting(true);
try {
await createConnector({
name: values.name,
connector_type: EnumConnectorName.LUMA_CONNECTOR,
config: {
LUMA_API_KEY: values.api_key,
await createConnector(
{
name: values.name,
connector_type: EnumConnectorName.LUMA_CONNECTOR,
config: {
LUMA_API_KEY: values.api_key,
},
is_indexable: true,
last_indexed_at: null,
},
is_indexable: true,
last_indexed_at: null,
});
parseInt(searchSpaceId)
);
toast.success("Luma connector created successfully!");

View file

@ -72,15 +72,18 @@ export default function NotionConnectorPage() {
const onSubmit = async (values: NotionConnectorFormValues) => {
setIsSubmitting(true);
try {
await createConnector({
name: values.name,
connector_type: EnumConnectorName.NOTION_CONNECTOR,
config: {
NOTION_INTEGRATION_TOKEN: values.integration_token,
await createConnector(
{
name: values.name,
connector_type: EnumConnectorName.NOTION_CONNECTOR,
config: {
NOTION_INTEGRATION_TOKEN: values.integration_token,
},
is_indexable: true,
last_indexed_at: null,
},
is_indexable: true,
last_indexed_at: null,
});
parseInt(searchSpaceId)
);
toast.success("Notion connector created successfully!");

View file

@ -65,15 +65,18 @@ export default function SerperApiPage() {
const onSubmit = async (values: SerperApiFormValues) => {
setIsSubmitting(true);
try {
await createConnector({
name: values.name,
connector_type: EnumConnectorName.SERPER_API,
config: {
SERPER_API_KEY: values.api_key,
await createConnector(
{
name: values.name,
connector_type: EnumConnectorName.SERPER_API,
config: {
SERPER_API_KEY: values.api_key,
},
is_indexable: false,
last_indexed_at: null,
},
is_indexable: false,
last_indexed_at: null,
});
parseInt(searchSpaceId)
);
toast.success("Serper API connector created successfully!");

View file

@ -72,15 +72,18 @@ export default function SlackConnectorPage() {
const onSubmit = async (values: SlackConnectorFormValues) => {
setIsSubmitting(true);
try {
await createConnector({
name: values.name,
connector_type: EnumConnectorName.SLACK_CONNECTOR,
config: {
SLACK_BOT_TOKEN: values.bot_token,
await createConnector(
{
name: values.name,
connector_type: EnumConnectorName.SLACK_CONNECTOR,
config: {
SLACK_BOT_TOKEN: values.bot_token,
},
is_indexable: true,
last_indexed_at: null,
},
is_indexable: true,
last_indexed_at: null,
});
parseInt(searchSpaceId)
);
toast.success("Slack connector created successfully!");

View file

@ -65,15 +65,18 @@ export default function TavilyApiPage() {
const onSubmit = async (values: TavilyApiFormValues) => {
setIsSubmitting(true);
try {
await createConnector({
name: values.name,
connector_type: EnumConnectorName.TAVILY_API,
config: {
TAVILY_API_KEY: values.api_key,
await createConnector(
{
name: values.name,
connector_type: EnumConnectorName.TAVILY_API,
config: {
TAVILY_API_KEY: values.api_key,
},
is_indexable: false,
last_indexed_at: null,
},
is_indexable: false,
last_indexed_at: null,
});
parseInt(searchSpaceId)
);
toast.success("Tavily API connector created successfully!");

View file

@ -1,7 +1,6 @@
"use client";
import { format } from "date-fns";
import { AnimatePresence, motion, type Variants } from "framer-motion";
import {
Calendar,
MoreHorizontal,
@ -16,6 +15,7 @@ import {
VolumeX,
X,
} from "lucide-react";
import { AnimatePresence, motion, type Variants } from "motion/react";
import Image from "next/image";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";

View file

@ -124,19 +124,20 @@ const ConnectorSelector = React.memo(
onSelectionChange?: (connectorTypes: string[]) => void;
selectedConnectors?: string[];
}) => {
const { search_space_id } = useParams();
const [isOpen, setIsOpen] = useState(false);
const { connectorSourceItems, isLoading, isLoaded, fetchConnectors } =
useSearchSourceConnectors(true);
useSearchSourceConnectors(true, Number(search_space_id));
const handleOpenChange = useCallback(
(open: boolean) => {
setIsOpen(open);
if (open && !isLoaded) {
fetchConnectors();
fetchConnectors(Number(search_space_id));
}
},
[fetchConnectors, isLoaded]
[fetchConnectors, isLoaded, search_space_id]
);
const handleConnectorToggle = useCallback(

View file

@ -18,7 +18,11 @@ import {
export function useConnectorEditPage(connectorId: number, searchSpaceId: string) {
const router = useRouter();
const { connectors, updateConnector, isLoading: connectorsLoading } = useSearchSourceConnectors();
const {
connectors,
updateConnector,
isLoading: connectorsLoading,
} = useSearchSourceConnectors(false, parseInt(searchSpaceId));
// State managed by the hook
const [connector, setConnector] = useState<SearchSourceConnector | null>(null);

View file

@ -7,6 +7,7 @@ export interface SearchSourceConnector {
is_indexable: boolean;
last_indexed_at: string | null;
config: Record<string, any>;
search_space_id: number;
user_id?: string;
created_at?: string;
}
@ -20,8 +21,10 @@ export interface ConnectorSourceItem {
/**
* Hook to fetch search source connectors from the API
* @param lazy - If true, connectors won't be fetched on mount
* @param searchSpaceId - Optional search space ID to filter connectors
*/
export const useSearchSourceConnectors = (lazy: boolean = false) => {
export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?: number) => {
const [connectors, setConnectors] = useState<SearchSourceConnector[]>([]);
const [isLoading, setIsLoading] = useState(!lazy); // Don't show loading initially for lazy mode
const [isLoaded, setIsLoaded] = useState(false); // Memoization flag
@ -53,60 +56,71 @@ export const useSearchSourceConnectors = (lazy: boolean = false) => {
},
]);
const fetchConnectors = useCallback(async () => {
if (isLoaded && lazy) return; // Avoid redundant calls in lazy mode
const fetchConnectors = useCallback(
async (spaceId?: number) => {
if (isLoaded && lazy) return; // Avoid redundant calls in lazy mode
try {
setIsLoading(true);
setError(null);
const token = localStorage.getItem("surfsense_bearer_token");
try {
setIsLoading(true);
setError(null);
const token = localStorage.getItem("surfsense_bearer_token");
if (!token) {
throw new Error("No authentication token found");
}
if (!token) {
throw new Error("No authentication token found");
}
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/`,
{
// Build URL with optional search_space_id query parameter
const url = new URL(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/`
);
if (spaceId !== undefined) {
url.searchParams.append("search_space_id", spaceId.toString());
}
const response = await fetch(url.toString(), {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error(`Failed to fetch connectors: ${response.statusText}`);
}
);
if (!response.ok) {
throw new Error(`Failed to fetch connectors: ${response.statusText}`);
const data = await response.json();
setConnectors(data);
setIsLoaded(true);
// Update connector source items when connectors change
updateConnectorSourceItems(data);
return data;
} catch (err) {
setError(err instanceof Error ? err : new Error("An unknown error occurred"));
console.error("Error fetching search source connectors:", err);
} finally {
setIsLoading(false);
}
const data = await response.json();
setConnectors(data);
setIsLoaded(true);
// Update connector source items when connectors change
updateConnectorSourceItems(data);
return data;
} catch (err) {
setError(err instanceof Error ? err : new Error("An unknown error occurred"));
console.error("Error fetching search source connectors:", err);
} finally {
setIsLoading(false);
}
}, [isLoaded, lazy]);
},
[isLoaded, lazy]
);
useEffect(() => {
if (!lazy) {
fetchConnectors();
fetchConnectors(searchSpaceId);
}
}, [lazy, fetchConnectors]);
}, [lazy, fetchConnectors, searchSpaceId]);
// Function to refresh the connectors list
const refreshConnectors = useCallback(async () => {
setIsLoaded(false); // Reset memoization flag to allow refetch
await fetchConnectors();
}, [fetchConnectors]);
const refreshConnectors = useCallback(
async (spaceId?: number) => {
setIsLoaded(false); // Reset memoization flag to allow refetch
await fetchConnectors(spaceId !== undefined ? spaceId : searchSpaceId);
},
[fetchConnectors, searchSpaceId]
);
// Update connector source items when connectors change
const updateConnectorSourceItems = (currentConnectors: SearchSourceConnector[]) => {
@ -151,9 +165,12 @@ export const useSearchSourceConnectors = (lazy: boolean = false) => {
/**
* Create a new search source connector
* @param connectorData - The connector data (excluding search_space_id)
* @param spaceId - The search space ID to associate the connector with
*/
const createConnector = async (
connectorData: Omit<SearchSourceConnector, "id" | "user_id" | "created_at">
connectorData: Omit<SearchSourceConnector, "id" | "user_id" | "created_at" | "search_space_id">,
spaceId: number
) => {
try {
const token = localStorage.getItem("surfsense_bearer_token");
@ -162,17 +179,20 @@ export const useSearchSourceConnectors = (lazy: boolean = false) => {
throw new Error("No authentication token found");
}
const response = await fetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(connectorData),
}
// Add search_space_id as a query parameter
const url = new URL(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/`
);
url.searchParams.append("search_space_id", spaceId.toString());
const response = await fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(connectorData),
});
if (!response.ok) {
throw new Error(`Failed to create connector: ${response.statusText}`);
@ -194,7 +214,9 @@ export const useSearchSourceConnectors = (lazy: boolean = false) => {
*/
const updateConnector = async (
connectorId: number,
connectorData: Partial<Omit<SearchSourceConnector, "id" | "user_id" | "created_at">>
connectorData: Partial<
Omit<SearchSourceConnector, "id" | "user_id" | "created_at" | "search_space_id">
>
) => {
try {
const token = localStorage.getItem("surfsense_bearer_token");