mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-28 10:26:33 +02:00
refactor: simplify connector management and update dashboard layout
- Removed the Connectors management page and integrated its functionality into a popup for better user experience. - Updated the DashboardLayout to reflect changes in navigation, removing references to the Connectors page. - Streamlined the breadcrumb component by eliminating unnecessary connector-related sections. - Enhanced the ConnectorIndicator to facilitate easier access to connector management features. - Improved overall UI consistency and accessibility across the dashboard components.
This commit is contained in:
parent
b909032e32
commit
3ae8fe3a7e
7 changed files with 65 additions and 999 deletions
|
|
@ -1,857 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import { useAtomValue } from "jotai";
|
|
||||||
import {
|
|
||||||
Calendar as CalendarIcon,
|
|
||||||
Clock,
|
|
||||||
Edit,
|
|
||||||
Folder,
|
|
||||||
HardDrive,
|
|
||||||
Info,
|
|
||||||
Loader2,
|
|
||||||
Plus,
|
|
||||||
RefreshCw,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import {
|
|
||||||
deleteConnectorMutationAtom,
|
|
||||||
indexConnectorMutationAtom,
|
|
||||||
updateConnectorMutationAtom,
|
|
||||||
} from "@/atoms/connectors/connector-mutation.atoms";
|
|
||||||
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { GoogleDriveFolderTree } from "@/components/connectors/google-drive-folder-tree";
|
|
||||||
|
|
||||||
export default function ConnectorsPage() {
|
|
||||||
const t = useTranslations("connectors");
|
|
||||||
const tCommon = useTranslations("common");
|
|
||||||
|
|
||||||
// Helper function to format date with time
|
|
||||||
const formatDateTime = (dateString: string | null): string => {
|
|
||||||
if (!dateString) return t("never");
|
|
||||||
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return new Intl.DateTimeFormat("en-US", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
}).format(date);
|
|
||||||
};
|
|
||||||
const router = useRouter();
|
|
||||||
const params = useParams();
|
|
||||||
const searchSpaceId = params.search_space_id as string;
|
|
||||||
const today = new Date();
|
|
||||||
|
|
||||||
const { data: connectors = [], isLoading, error } = useAtomValue(connectorsAtom);
|
|
||||||
|
|
||||||
const { mutateAsync: deleteConnector } = useAtomValue(deleteConnectorMutationAtom);
|
|
||||||
const { mutateAsync: indexConnector } = useAtomValue(indexConnectorMutationAtom);
|
|
||||||
const { mutateAsync: updateConnector } = useAtomValue(updateConnectorMutationAtom);
|
|
||||||
|
|
||||||
const [connectorToDelete, setConnectorToDelete] = useState<number | null>(null);
|
|
||||||
const [indexingConnectorId, setIndexingConnectorId] = useState<number | null>(null);
|
|
||||||
const [datePickerOpen, setDatePickerOpen] = useState(false);
|
|
||||||
const [selectedConnectorForIndexing, setSelectedConnectorForIndexing] = useState<number | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
|
|
||||||
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
|
|
||||||
|
|
||||||
// Periodic indexing state
|
|
||||||
const [periodicDialogOpen, setPeriodicDialogOpen] = useState(false);
|
|
||||||
const [selectedConnectorForPeriodic, setSelectedConnectorForPeriodic] = useState<number | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [periodicEnabled, setPeriodicEnabled] = useState(false);
|
|
||||||
const [frequencyMinutes, setFrequencyMinutes] = useState<string>("1440");
|
|
||||||
const [customFrequency, setCustomFrequency] = useState<string>("");
|
|
||||||
const [isSavingPeriodic, setIsSavingPeriodic] = useState(false);
|
|
||||||
|
|
||||||
// Google Drive folder and file selection state
|
|
||||||
const [driveFolderDialogOpen, setDriveFolderDialogOpen] = useState(false);
|
|
||||||
const [selectedFolders, setSelectedFolders] = useState<Array<{ id: string; name: string }>>([]);
|
|
||||||
const [selectedFiles, setSelectedFiles] = useState<Array<{ id: string; name: string }>>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (error) {
|
|
||||||
toast.error(t("failed_load"));
|
|
||||||
console.error("Error fetching connectors:", error);
|
|
||||||
}
|
|
||||||
}, [error, t]);
|
|
||||||
|
|
||||||
// Handle connector deletion
|
|
||||||
const handleDeleteConnector = async () => {
|
|
||||||
if (connectorToDelete === null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await deleteConnector({ id: connectorToDelete });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting connector:", error);
|
|
||||||
} finally {
|
|
||||||
setConnectorToDelete(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle opening date picker for indexing
|
|
||||||
const handleOpenDatePicker = (connectorId: number) => {
|
|
||||||
// Check if this is a Google Drive connector
|
|
||||||
const connector = connectors.find((c) => c.id === connectorId);
|
|
||||||
if (connector?.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR) {
|
|
||||||
// Open folder selection dialog for Google Drive
|
|
||||||
handleOpenDriveFolderDialog(connectorId);
|
|
||||||
} else {
|
|
||||||
// Open date picker for other connectors
|
|
||||||
setSelectedConnectorForIndexing(connectorId);
|
|
||||||
setDatePickerOpen(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenDriveFolderDialog = (connectorId: number) => {
|
|
||||||
setSelectedConnectorForIndexing(connectorId);
|
|
||||||
setDriveFolderDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle Google Drive folder and file indexing
|
|
||||||
const handleIndexGoogleDrive = async () => {
|
|
||||||
if (selectedConnectorForIndexing === null || (selectedFolders.length === 0 && selectedFiles.length === 0)) {
|
|
||||||
toast.error("Please select at least one folder or file");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDriveFolderDialogOpen(false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIndexingConnectorId(selectedConnectorForIndexing);
|
|
||||||
|
|
||||||
await indexConnector({
|
|
||||||
connector_id: selectedConnectorForIndexing,
|
|
||||||
body: {
|
|
||||||
folders: selectedFolders,
|
|
||||||
files: selectedFiles,
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
toast.success(t("indexing_started"));
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error indexing connector content:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : t("indexing_failed"));
|
|
||||||
} finally {
|
|
||||||
setIndexingConnectorId(null);
|
|
||||||
setSelectedConnectorForIndexing(null);
|
|
||||||
setSelectedFolders([]);
|
|
||||||
setSelectedFiles([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle connector indexing with dates
|
|
||||||
const handleIndexConnector = async () => {
|
|
||||||
if (selectedConnectorForIndexing === null) return;
|
|
||||||
|
|
||||||
setDatePickerOpen(false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIndexingConnectorId(selectedConnectorForIndexing);
|
|
||||||
const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined;
|
|
||||||
const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined;
|
|
||||||
|
|
||||||
await indexConnector({
|
|
||||||
connector_id: selectedConnectorForIndexing,
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
start_date: startDateStr,
|
|
||||||
end_date: endDateStr,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
toast.success(t("indexing_started"));
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error indexing connector content:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : t("indexing_failed"));
|
|
||||||
} finally {
|
|
||||||
setIndexingConnectorId(null);
|
|
||||||
setSelectedConnectorForIndexing(null);
|
|
||||||
setStartDate(undefined);
|
|
||||||
setEndDate(undefined);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle indexing without date picker (for quick indexing)
|
|
||||||
const handleQuickIndexConnector = async (connectorId: number) => {
|
|
||||||
setIndexingConnectorId(connectorId);
|
|
||||||
try {
|
|
||||||
await indexConnector({
|
|
||||||
connector_id: connectorId,
|
|
||||||
queryParams: {
|
|
||||||
search_space_id: searchSpaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
toast.success(t("indexing_started"));
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error indexing connector content:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : t("indexing_failed"));
|
|
||||||
} finally {
|
|
||||||
setIndexingConnectorId(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle opening periodic indexing dialog
|
|
||||||
const handleOpenPeriodicDialog = (connectorId: number) => {
|
|
||||||
const connector = connectors.find((c) => c.id === connectorId);
|
|
||||||
if (!connector) return;
|
|
||||||
|
|
||||||
setSelectedConnectorForPeriodic(connectorId);
|
|
||||||
setPeriodicEnabled(connector.periodic_indexing_enabled);
|
|
||||||
|
|
||||||
if (connector.indexing_frequency_minutes) {
|
|
||||||
// Check if it's a preset value
|
|
||||||
const presetValues = ["15", "60", "360", "720", "1440", "10080"];
|
|
||||||
if (presetValues.includes(connector.indexing_frequency_minutes.toString())) {
|
|
||||||
setFrequencyMinutes(connector.indexing_frequency_minutes.toString());
|
|
||||||
setCustomFrequency("");
|
|
||||||
} else {
|
|
||||||
setFrequencyMinutes("custom");
|
|
||||||
setCustomFrequency(connector.indexing_frequency_minutes.toString());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setFrequencyMinutes("1440");
|
|
||||||
setCustomFrequency("");
|
|
||||||
}
|
|
||||||
|
|
||||||
setPeriodicDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle saving periodic indexing configuration
|
|
||||||
const handleSavePeriodicIndexing = async () => {
|
|
||||||
if (selectedConnectorForPeriodic === null) return;
|
|
||||||
|
|
||||||
const connector = connectors.find((c) => c.id === selectedConnectorForPeriodic);
|
|
||||||
if (!connector) return;
|
|
||||||
|
|
||||||
setIsSavingPeriodic(true);
|
|
||||||
try {
|
|
||||||
// Determine the frequency value
|
|
||||||
let frequency: number | null = null;
|
|
||||||
if (periodicEnabled) {
|
|
||||||
if (frequencyMinutes === "custom") {
|
|
||||||
frequency = parseInt(customFrequency, 10);
|
|
||||||
if (isNaN(frequency) || frequency <= 0) {
|
|
||||||
toast.error("Please enter a valid frequency in minutes");
|
|
||||||
setIsSavingPeriodic(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
frequency = parseInt(frequencyMinutes, 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateConnector({
|
|
||||||
id: selectedConnectorForPeriodic,
|
|
||||||
data: {
|
|
||||||
periodic_indexing_enabled: periodicEnabled,
|
|
||||||
indexing_frequency_minutes: frequency,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success(
|
|
||||||
periodicEnabled
|
|
||||||
? "Periodic indexing enabled successfully"
|
|
||||||
: "Periodic indexing disabled successfully"
|
|
||||||
);
|
|
||||||
setPeriodicDialogOpen(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error updating periodic indexing:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to update periodic indexing");
|
|
||||||
} finally {
|
|
||||||
setIsSavingPeriodic(false);
|
|
||||||
setSelectedConnectorForPeriodic(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format frequency for display
|
|
||||||
const formatFrequency = (minutes: number): string => {
|
|
||||||
if (minutes < 60) return `${minutes}m`;
|
|
||||||
if (minutes < 1440) return `${Math.floor(minutes / 60)}h`;
|
|
||||||
if (minutes < 10080) return `${Math.floor(minutes / 1440)}d`;
|
|
||||||
return `${Math.floor(minutes / 10080)}w`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-8 px-4 max-w-6xl min-h-[calc(100vh-64px)]">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="mb-8 flex items-center justify-between gap-2"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-xl md:text-3xl font-bold tracking-tight">{t("title")}</h1>
|
|
||||||
<p className="text-xs md:text-base text-muted-foreground mt-2">{t("subtitle")}</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
className="h-8 text-xs px-3 md:h-10 md:text-sm md:px-4"
|
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
|
||||||
>
|
|
||||||
<Plus className="mr-2 h-3 w-3 md:h-4 md:w-4" />
|
|
||||||
{t("add_connector")}
|
|
||||||
</Button>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex justify-center py-8">
|
|
||||||
<div className="animate-pulse text-center">
|
|
||||||
<div className="h-6 w-32 bg-muted rounded mx-auto mb-2"></div>
|
|
||||||
<div className="h-4 w-48 bg-muted rounded mx-auto"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : connectors.length === 0 ? (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<h3 className="text-lg font-medium mb-2">{t("no_connectors")}</h3>
|
|
||||||
<p className="text-muted-foreground mb-6">{t("no_connectors_desc")}</p>
|
|
||||||
<Button onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}>
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
|
||||||
{t("add_first")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="rounded-md border">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>{t("name")}</TableHead>
|
|
||||||
<TableHead>{t("type")}</TableHead>
|
|
||||||
<TableHead>{t("last_indexed")}</TableHead>
|
|
||||||
<TableHead>{t("periodic")}</TableHead>
|
|
||||||
<TableHead className="text-right">{t("actions")}</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{connectors.map((connector) => (
|
|
||||||
<TableRow key={connector.id}>
|
|
||||||
<TableCell className="font-medium">{connector.name}</TableCell>
|
|
||||||
<TableCell>{getConnectorIcon(connector.connector_type)}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{connector.is_indexable
|
|
||||||
? formatDateTime(connector.last_indexed_at)
|
|
||||||
: t("not_indexable")}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{connector.is_indexable ? (
|
|
||||||
connector.periodic_indexing_enabled ? (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center gap-1 text-green-600 dark:text-green-400">
|
|
||||||
<Clock className="h-4 w-4" />
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{connector.indexing_frequency_minutes
|
|
||||||
? formatFrequency(connector.indexing_frequency_minutes)
|
|
||||||
: "Enabled"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Runs every {connector.indexing_frequency_minutes} minutes
|
|
||||||
{connector.next_scheduled_at && (
|
|
||||||
<>
|
|
||||||
<br />
|
|
||||||
Next: {formatDateTime(connector.next_scheduled_at)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
) : (
|
|
||||||
<span className="text-sm text-muted-foreground">Disabled</span>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span className="text-sm text-muted-foreground">-</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
{connector.is_indexable && (
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleOpenDatePicker(connector.id)}
|
|
||||||
disabled={indexingConnectorId === connector.id}
|
|
||||||
>
|
|
||||||
{indexingConnectorId === connector.id ? (
|
|
||||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
|
||||||
) : connector.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR ? (
|
|
||||||
<Folder className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<CalendarIcon className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
<span className="sr-only">
|
|
||||||
{connector.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR
|
|
||||||
? "Select folder to index"
|
|
||||||
: t("index_date_range")}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
{connector.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR
|
|
||||||
? "Select folder to index"
|
|
||||||
: t("index_date_range")}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
{/* Hide quick index button for Google Drive (requires folder selection) */}
|
|
||||||
{connector.connector_type !== EnumConnectorName.GOOGLE_DRIVE_CONNECTOR && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleQuickIndexConnector(connector.id)}
|
|
||||||
disabled={indexingConnectorId === connector.id}
|
|
||||||
>
|
|
||||||
{indexingConnectorId === connector.id ? (
|
|
||||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<RefreshCw className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
<span className="sr-only">{t("quick_index")}</span>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{t("quick_index_auto")}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{connector.is_indexable && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleOpenPeriodicDialog(connector.id)}
|
|
||||||
>
|
|
||||||
<Clock className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Configure Periodic Indexing</span>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Configure Periodic Indexing</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
router.push(
|
|
||||||
`/dashboard/${searchSpaceId}/connectors/${connector.id}/edit`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Edit className="h-4 w-4" />
|
|
||||||
<span className="sr-only">{tCommon("edit")}</span>
|
|
||||||
</Button>
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="text-destructive-foreground hover:bg-destructive/10"
|
|
||||||
onClick={() => setConnectorToDelete(connector.id)}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
<span className="sr-only">{tCommon("delete")}</span>
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>{t("delete_connector")}</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
{t("delete_confirm")}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel onClick={() => setConnectorToDelete(null)}>
|
|
||||||
{tCommon("cancel")}
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
||||||
onClick={handleDeleteConnector}
|
|
||||||
>
|
|
||||||
{tCommon("delete")}
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Date Picker Dialog */}
|
|
||||||
<Dialog open={datePickerOpen} onOpenChange={setDatePickerOpen}>
|
|
||||||
<DialogContent className="sm:max-w-[500px]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t("select_date_range")}</DialogTitle>
|
|
||||||
<DialogDescription>{t("select_date_range_desc")}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4 py-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="start-date">{t("start_date")}</Label>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
id="start-date"
|
|
||||||
variant="outline"
|
|
||||||
className={cn(
|
|
||||||
"w-full justify-start text-left font-normal",
|
|
||||||
!startDate && "text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
||||||
{startDate ? format(startDate, "PPP") : t("pick_date")}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
|
||||||
<Calendar
|
|
||||||
mode="single"
|
|
||||||
selected={startDate}
|
|
||||||
onSelect={setStartDate}
|
|
||||||
initialFocus
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="end-date">{t("end_date")}</Label>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
id="end-date"
|
|
||||||
variant="outline"
|
|
||||||
className={cn(
|
|
||||||
"w-full justify-start text-left font-normal",
|
|
||||||
!endDate && "text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
||||||
{endDate ? format(endDate, "PPP") : t("pick_date")}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
|
||||||
<Calendar mode="single" selected={endDate} onSelect={setEndDate} initialFocus />
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setStartDate(undefined);
|
|
||||||
setEndDate(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("clear_dates")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
const thirtyDaysAgo = new Date(today);
|
|
||||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
|
||||||
setStartDate(thirtyDaysAgo);
|
|
||||||
setEndDate(today);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("last_30_days")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
const yearAgo = new Date(today);
|
|
||||||
yearAgo.setFullYear(today.getFullYear() - 1);
|
|
||||||
setStartDate(yearAgo);
|
|
||||||
setEndDate(today);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("last_year")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setDatePickerOpen(false);
|
|
||||||
setSelectedConnectorForIndexing(null);
|
|
||||||
setStartDate(undefined);
|
|
||||||
setEndDate(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tCommon("cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleIndexConnector}>{t("start_indexing")}</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Google Drive Folder Selection Dialog */}
|
|
||||||
<Dialog open={driveFolderDialogOpen} onOpenChange={setDriveFolderDialogOpen}>
|
|
||||||
<DialogContent className="w-auto max-w-full">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Select Google Drive Folders & Files</DialogTitle>
|
|
||||||
<DialogDescription className="flex items-start gap-2 text-sm p-2 border mt-1 rounded ">
|
|
||||||
<Info className="h-4 w-4 shrink-0 text-blue-500" />
|
|
||||||
<span>
|
|
||||||
Select folders and/or individual files to index. For folders, only files <strong>directly in each folder</strong> will be
|
|
||||||
processed—subfolders must be selected separately.
|
|
||||||
</span>
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4 overflow-hidden w-full">
|
|
||||||
<div className="space-y-3 w-full overflow-hidden">
|
|
||||||
<Label>Browse Folders</Label>
|
|
||||||
{selectedConnectorForIndexing && (
|
|
||||||
<GoogleDriveFolderTree
|
|
||||||
connectorId={selectedConnectorForIndexing}
|
|
||||||
selectedFolders={selectedFolders}
|
|
||||||
onSelectFolders={(folders) => {
|
|
||||||
setSelectedFolders(folders);
|
|
||||||
}}
|
|
||||||
selectedFiles={selectedFiles}
|
|
||||||
onSelectFiles={(files) => {
|
|
||||||
setSelectedFiles(files);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{(selectedFolders.length > 0 || selectedFiles.length > 0) && (
|
|
||||||
<div className="p-3 bg-muted rounded-lg text-sm space-y-2">
|
|
||||||
{selectedFolders.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<p className="font-medium mb-1">
|
|
||||||
Selected {selectedFolders.length} folder{selectedFolders.length > 1 ? "s" : ""}:
|
|
||||||
</p>
|
|
||||||
<div className="max-h-24 overflow-y-auto">
|
|
||||||
{selectedFolders.map((folder) => (
|
|
||||||
<p key={folder.id} className="text-sm text-muted-foreground truncate" title={folder.name}>
|
|
||||||
📁 {folder.name}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{selectedFiles.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<p className="font-medium mb-1">
|
|
||||||
Selected {selectedFiles.length} file{selectedFiles.length > 1 ? "s" : ""}:
|
|
||||||
</p>
|
|
||||||
<div className="max-h-24 overflow-y-auto">
|
|
||||||
{selectedFiles.map((file) => (
|
|
||||||
<p key={file.id} className="text-sm text-muted-foreground truncate" title={file.name}>
|
|
||||||
📄 {file.name}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setDriveFolderDialogOpen(false);
|
|
||||||
setSelectedConnectorForIndexing(null);
|
|
||||||
setSelectedFolders([]);
|
|
||||||
setSelectedFiles([]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tCommon("cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleIndexGoogleDrive} disabled={selectedFolders.length === 0 && selectedFiles.length === 0}>
|
|
||||||
{t("start_indexing")}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Periodic Indexing Configuration Dialog */}
|
|
||||||
<Dialog open={periodicDialogOpen} onOpenChange={setPeriodicDialogOpen}>
|
|
||||||
<DialogContent className="sm:max-w-[500px]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Configure Periodic Indexing</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Set up automatic indexing at regular intervals for this connector.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-6 py-4">
|
|
||||||
<div className="flex items-center justify-between space-x-2">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<Label htmlFor="periodic-enabled" className="text-base">
|
|
||||||
Enable Periodic Indexing
|
|
||||||
</Label>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Automatically index this connector at regular intervals
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id="periodic-enabled"
|
|
||||||
checked={periodicEnabled}
|
|
||||||
onCheckedChange={setPeriodicEnabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{periodicEnabled && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="frequency">Indexing Frequency</Label>
|
|
||||||
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes}>
|
|
||||||
<SelectTrigger id="frequency">
|
|
||||||
<SelectValue placeholder="Select frequency" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="15">Every 15 minutes</SelectItem>
|
|
||||||
<SelectItem value="60">Every hour</SelectItem>
|
|
||||||
<SelectItem value="360">Every 6 hours</SelectItem>
|
|
||||||
<SelectItem value="720">Every 12 hours</SelectItem>
|
|
||||||
<SelectItem value="1440">Daily (24 hours)</SelectItem>
|
|
||||||
<SelectItem value="10080">Weekly (7 days)</SelectItem>
|
|
||||||
<SelectItem value="custom">Custom</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{frequencyMinutes === "custom" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="custom-frequency">Custom Frequency (minutes)</Label>
|
|
||||||
<Input
|
|
||||||
id="custom-frequency"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
placeholder="Enter minutes"
|
|
||||||
value={customFrequency}
|
|
||||||
onChange={(e) => setCustomFrequency(e.target.value)}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Enter the number of minutes between each indexing run
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="rounded-lg bg-muted p-3 text-sm">
|
|
||||||
<p className="font-medium mb-1">Preview:</p>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
{frequencyMinutes === "custom" && customFrequency
|
|
||||||
? `Will run every ${customFrequency} minutes`
|
|
||||||
: frequencyMinutes === "15"
|
|
||||||
? "Will run every 15 minutes"
|
|
||||||
: frequencyMinutes === "60"
|
|
||||||
? "Will run every hour"
|
|
||||||
: frequencyMinutes === "360"
|
|
||||||
? "Will run every 6 hours"
|
|
||||||
: frequencyMinutes === "720"
|
|
||||||
? "Will run every 12 hours"
|
|
||||||
: frequencyMinutes === "1440"
|
|
||||||
? "Will run daily (every 24 hours)"
|
|
||||||
: frequencyMinutes === "10080"
|
|
||||||
? "Will run weekly (every 7 days)"
|
|
||||||
: "Select a frequency above"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setPeriodicDialogOpen(false);
|
|
||||||
setSelectedConnectorForPeriodic(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSavePeriodicIndexing} disabled={isSavingPeriodic}>
|
|
||||||
{isSavingPeriodic && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
||||||
Save Configuration
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -26,28 +26,24 @@ export default function DashboardLayout({
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const customNavMain = [
|
const customNavMain = [
|
||||||
{
|
{
|
||||||
title: "Chat",
|
title: "Chat",
|
||||||
url: `/dashboard/${search_space_id}/new-chat`,
|
url: `/dashboard/${search_space_id}/new-chat`,
|
||||||
icon: "SquareTerminal",
|
icon: "SquareTerminal",
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Sources",
|
title: "Sources",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: "Database",
|
icon: "Database",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Manage Documents",
|
title: "Manage Documents",
|
||||||
url: `/dashboard/${search_space_id}/documents`,
|
url: `/dashboard/${search_space_id}/documents`,
|
||||||
},
|
},
|
||||||
{
|
],
|
||||||
title: "Manage Connectors",
|
},
|
||||||
url: `/dashboard/${search_space_id}/connectors`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "Logs",
|
title: "Logs",
|
||||||
url: `/dashboard/${search_space_id}/logs`,
|
url: `/dashboard/${search_space_id}/logs`,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { AssistantIf, ComposerPrimitive, useAssistantState } from "@assistant-ui/react";
|
import { AssistantIf, ComposerPrimitive, useAssistantState } from "@assistant-ui/react";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { AlertCircle, ArrowUpIcon, Loader2, Plus, Plug2, SquareIcon } from "lucide-react";
|
import { AlertCircle, ArrowUpIcon, Loader2, Plus, Plug2, SquareIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon";
|
import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon";
|
||||||
|
|
@ -148,14 +147,15 @@ const ConnectorIndicator: FC = () => {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="pt-1 border-t border-border/50">
|
<div className="pt-1 border-t border-border/50">
|
||||||
<Link
|
<button
|
||||||
href={`/dashboard/${searchSpaceId}/connectors/add`}
|
type="button"
|
||||||
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
onClick={() => {/* Connector popup should be opened via the connector indicator button */}}
|
||||||
>
|
>
|
||||||
<Plus className="size-3" />
|
<Plus className="size-3" />
|
||||||
Add more sources
|
Add more sources
|
||||||
<ChevronRightIcon className="size-3" />
|
<ChevronRightIcon className="size-3" />
|
||||||
</Link>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -164,13 +164,14 @@ const ConnectorIndicator: FC = () => {
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Add documents or connect data sources to enhance search results.
|
Add documents or connect data sources to enhance search results.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<button
|
||||||
href={`/dashboard/${searchSpaceId}/connectors/add`}
|
type="button"
|
||||||
className="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 transition-colors mt-1"
|
className="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 transition-colors mt-1"
|
||||||
|
onClick={() => {/* Connector popup should be opened via the connector indicator button */}}
|
||||||
>
|
>
|
||||||
<Plus className="size-3" />
|
<Plus className="size-3" />
|
||||||
Add Connector
|
Add Connector
|
||||||
</Link>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
|
|
|
||||||
|
|
@ -688,29 +688,7 @@ export const useConnectorDialog = () => {
|
||||||
const handleStartEdit = useCallback((connector: SearchSourceConnector) => {
|
const handleStartEdit = useCallback((connector: SearchSourceConnector) => {
|
||||||
if (!searchSpaceId) return;
|
if (!searchSpaceId) return;
|
||||||
|
|
||||||
// Check if this is an OAuth connector
|
// All connector types should be handled in the popup edit view
|
||||||
const isOAuthConnector = OAUTH_CONNECTORS.some(
|
|
||||||
(oauthConnector) => oauthConnector.connectorType === connector.connector_type
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if this is webcrawler, Tavily API, SearxNG, Linkup, Baidu, Linear, Elasticsearch, Slack, Discord, or Notion (can be managed in popup)
|
|
||||||
const isWebcrawler = connector.connector_type === EnumConnectorName.WEBCRAWLER_CONNECTOR;
|
|
||||||
const isTavilyApi = connector.connector_type === EnumConnectorName.TAVILY_API;
|
|
||||||
const isSearxng = connector.connector_type === EnumConnectorName.SEARXNG_API;
|
|
||||||
const isLinkup = connector.connector_type === EnumConnectorName.LINKUP_API;
|
|
||||||
const isBaidu = connector.connector_type === EnumConnectorName.BAIDU_SEARCH_API;
|
|
||||||
const isLinear = connector.connector_type === EnumConnectorName.LINEAR_CONNECTOR;
|
|
||||||
const isElasticsearch = connector.connector_type === EnumConnectorName.ELASTICSEARCH_CONNECTOR;
|
|
||||||
const isSlack = connector.connector_type === EnumConnectorName.SLACK_CONNECTOR;
|
|
||||||
const isDiscord = connector.connector_type === EnumConnectorName.DISCORD_CONNECTOR;
|
|
||||||
const isNotion = connector.connector_type === EnumConnectorName.NOTION_CONNECTOR;
|
|
||||||
|
|
||||||
// If not OAuth, not webcrawler, not Tavily API, not SearxNG, not Linkup, not Baidu, not Linear, not Elasticsearch, not Slack, not Discord, and not Notion, redirect to old connector edit page
|
|
||||||
if (!isOAuthConnector && !isWebcrawler && !isTavilyApi && !isSearxng && !isLinkup && !isBaidu && !isLinear && !isElasticsearch && !isSlack && !isDiscord && !isNotion) {
|
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors/${connector.id}/edit`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate connector data
|
// Validate connector data
|
||||||
const connectorValidation = searchSourceConnector.safeParse(connector);
|
const connectorValidation = searchSourceConnector.safeParse(connector);
|
||||||
if (!connectorValidation.success) {
|
if (!connectorValidation.success) {
|
||||||
|
|
@ -733,7 +711,7 @@ export const useConnectorDialog = () => {
|
||||||
url.searchParams.set("view", "edit");
|
url.searchParams.set("view", "edit");
|
||||||
url.searchParams.set("connectorId", connector.id.toString());
|
url.searchParams.set("connectorId", connector.id.toString());
|
||||||
window.history.pushState({ modal: true }, "", url.toString());
|
window.history.pushState({ modal: true }, "", url.toString());
|
||||||
}, [searchSpaceId, router]);
|
}, [searchSpaceId]);
|
||||||
|
|
||||||
// Handle saving connector changes
|
// Handle saving connector changes
|
||||||
const handleSaveConnector = useCallback(async (refreshConnectors: () => void) => {
|
const handleSaveConnector = useCallback(async (refreshConnectors: () => void) => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import type { FC } from "react";
|
||||||
import { type FC } from "react";
|
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||||
import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types";
|
import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types";
|
||||||
import { OAUTH_CONNECTORS, CRAWLERS, OTHER_CONNECTORS } from "../constants/connector-constants";
|
import { OAUTH_CONNECTORS, CRAWLERS, OTHER_CONNECTORS } from "../constants/connector-constants";
|
||||||
|
|
@ -39,8 +38,6 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
||||||
onCreateYouTubeCrawler,
|
onCreateYouTubeCrawler,
|
||||||
onManage,
|
onManage,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// Helper to find active task for a connector
|
// Helper to find active task for a connector
|
||||||
const getActiveTaskForConnector = (connectorId: number): LogActiveTask | undefined => {
|
const getActiveTaskForConnector = (connectorId: number): LogActiveTask | undefined => {
|
||||||
if (!logsSummary?.active_tasks) return undefined;
|
if (!logsSummary?.active_tasks) return undefined;
|
||||||
|
|
@ -148,9 +145,11 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
||||||
: isWebcrawler && onCreateWebcrawler
|
: isWebcrawler && onCreateWebcrawler
|
||||||
? onCreateWebcrawler
|
? onCreateWebcrawler
|
||||||
: crawler.connectorType && onConnectNonOAuth
|
: crawler.connectorType && onConnectNonOAuth
|
||||||
? () => onConnectNonOAuth(crawler.connectorType!)
|
? () => {
|
||||||
: crawler.connectorType
|
if (crawler.connectorType) {
|
||||||
? () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${crawler.id}`)
|
onConnectNonOAuth(crawler.connectorType);
|
||||||
|
}
|
||||||
|
}
|
||||||
: () => {}; // Fallback for non-connector crawlers
|
: () => {}; // Fallback for non-connector crawlers
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -186,7 +185,6 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
{filteredOther.map((connector) => {
|
{filteredOther.map((connector) => {
|
||||||
// Special handling for connectors that can be created in popup
|
// Special handling for connectors that can be created in popup
|
||||||
const isWebcrawler = connector.id === "webcrawler-connector";
|
|
||||||
const isTavily = connector.id === "tavily-api";
|
const isTavily = connector.id === "tavily-api";
|
||||||
const isSearxng = connector.id === "searxng";
|
const isSearxng = connector.id === "searxng";
|
||||||
const isLinkup = connector.id === "linkup-api";
|
const isLinkup = connector.id === "linkup-api";
|
||||||
|
|
@ -216,11 +214,9 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
||||||
const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id);
|
const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id);
|
||||||
const activeTask = actualConnector ? getActiveTaskForConnector(actualConnector.id) : undefined;
|
const activeTask = actualConnector ? getActiveTaskForConnector(actualConnector.id) : undefined;
|
||||||
|
|
||||||
const handleConnect = isWebcrawler && onCreateWebcrawler
|
const handleConnect = (isTavily || isSearxng || isLinkup || isBaidu || isLinear || isElasticsearch || isSlack || isDiscord || isNotion || isConfluence || isBookStack || isGithub || isJira || isClickUp || isLuma || isCircleback) && onConnectNonOAuth
|
||||||
? onCreateWebcrawler
|
|
||||||
: (isTavily || isSearxng || isLinkup || isBaidu || isLinear || isElasticsearch || isSlack || isDiscord || isNotion || isConfluence || isBookStack || isGithub || isJira || isClickUp || isLuma || isCircleback) && onConnectNonOAuth
|
|
||||||
? () => onConnectNonOAuth(connector.connectorType)
|
? () => onConnectNonOAuth(connector.connectorType)
|
||||||
: () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`);
|
: () => {}; // Fallback - connector popup should handle all connector types
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConnectorCard
|
<ConnectorCard
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,6 @@ export function DashboardBreadcrumb() {
|
||||||
const sectionLabels: Record<string, string> = {
|
const sectionLabels: Record<string, string> = {
|
||||||
"new-chat": t("chat") || "Chat",
|
"new-chat": t("chat") || "Chat",
|
||||||
documents: t("documents"),
|
documents: t("documents"),
|
||||||
connectors: t("connectors"),
|
|
||||||
logs: t("logs"),
|
logs: t("logs"),
|
||||||
settings: t("settings"),
|
settings: t("settings"),
|
||||||
editor: t("editor"),
|
editor: t("editor"),
|
||||||
|
|
@ -156,53 +155,6 @@ export function DashboardBreadcrumb() {
|
||||||
return breadcrumbs;
|
return breadcrumbs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle connector sub-sections
|
|
||||||
if (section === "connectors") {
|
|
||||||
// Handle specific connector types
|
|
||||||
if (subSection === "add" && segments[4]) {
|
|
||||||
const connectorType = segments[4];
|
|
||||||
const connectorLabels: Record<string, string> = {
|
|
||||||
"github-connector": "GitHub",
|
|
||||||
"jira-connector": "Jira",
|
|
||||||
"confluence-connector": "Confluence",
|
|
||||||
"bookstack-connector": "BookStack",
|
|
||||||
"discord-connector": "Discord",
|
|
||||||
"linear-connector": "Linear",
|
|
||||||
"clickup-connector": "ClickUp",
|
|
||||||
"slack-connector": "Slack",
|
|
||||||
"notion-connector": "Notion",
|
|
||||||
"tavily-api": "Tavily API",
|
|
||||||
"linkup-api": "LinkUp API",
|
|
||||||
"luma-connector": "Luma",
|
|
||||||
"elasticsearch-connector": "Elasticsearch",
|
|
||||||
};
|
|
||||||
|
|
||||||
const connectorLabel = connectorLabels[connectorType] || connectorType;
|
|
||||||
breadcrumbs.push({
|
|
||||||
label: "Connectors",
|
|
||||||
href: `/dashboard/${segments[1]}/connectors`,
|
|
||||||
});
|
|
||||||
breadcrumbs.push({
|
|
||||||
label: "Add Connector",
|
|
||||||
href: `/dashboard/${segments[1]}/connectors/add`,
|
|
||||||
});
|
|
||||||
breadcrumbs.push({ label: connectorLabel });
|
|
||||||
return breadcrumbs;
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectorLabels: Record<string, string> = {
|
|
||||||
add: t("add_connector"),
|
|
||||||
manage: t("manage_connectors"),
|
|
||||||
};
|
|
||||||
|
|
||||||
const connectorLabel = connectorLabels[subSection] || subSection;
|
|
||||||
breadcrumbs.push({
|
|
||||||
label: t("connectors"),
|
|
||||||
href: `/dashboard/${segments[1]}/connectors`,
|
|
||||||
});
|
|
||||||
breadcrumbs.push({ label: connectorLabel });
|
|
||||||
return breadcrumbs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle other sub-sections
|
// Handle other sub-sections
|
||||||
let subSectionLabel = subSection.charAt(0).toUpperCase() + subSection.slice(1);
|
let subSectionLabel = subSection.charAt(0).toUpperCase() + subSection.slice(1);
|
||||||
|
|
@ -210,8 +162,6 @@ export function DashboardBreadcrumb() {
|
||||||
upload: t("upload_documents"),
|
upload: t("upload_documents"),
|
||||||
youtube: t("add_youtube"),
|
youtube: t("add_youtube"),
|
||||||
webpage: t("add_webpages"),
|
webpage: t("add_webpages"),
|
||||||
add: t("add_connector"),
|
|
||||||
edit: t("edit_connector"),
|
|
||||||
manage: t("manage"),
|
manage: t("manage"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
} from "@/components/editConnector/types";
|
} from "@/components/editConnector/types";
|
||||||
import type { EnumConnectorName } from "@/contracts/enums/connector";
|
import type { EnumConnectorName } from "@/contracts/enums/connector";
|
||||||
import type { SearchSourceConnector } from "@/hooks/use-search-source-connectors";
|
import type { SearchSourceConnector } from "@/hooks/use-search-source-connectors";
|
||||||
|
import type { UpdateConnectorResponse } from "@/contracts/types/connector.types";
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||||
|
|
||||||
const normalizeListInput = (value: unknown): string[] => {
|
const normalizeListInput = (value: unknown): string[] => {
|
||||||
|
|
@ -57,7 +58,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
|
|
||||||
// State managed by the hook
|
// State managed by the hook
|
||||||
const [connector, setConnector] = useState<SearchSourceConnector | null>(null);
|
const [connector, setConnector] = useState<SearchSourceConnector | null>(null);
|
||||||
const [originalConfig, setOriginalConfig] = useState<Record<string, any> | null>(null);
|
const [originalConfig, setOriginalConfig] = useState<Record<string, unknown> | null>(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [currentSelectedRepos, setCurrentSelectedRepos] = useState<string[]>([]);
|
const [currentSelectedRepos, setCurrentSelectedRepos] = useState<string[]>([]);
|
||||||
const [originalPat, setOriginalPat] = useState<string>("");
|
const [originalPat, setOriginalPat] = useState<string>("");
|
||||||
|
|
@ -161,7 +162,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error("Connector not found.");
|
toast.error("Connector not found.");
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
router.push(`/dashboard/${searchSpaceId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
|
@ -170,9 +171,9 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
connectorsLoading,
|
connectorsLoading,
|
||||||
router,
|
router,
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
connector,
|
connector, editForm.reset, patForm.reset
|
||||||
editForm,
|
// Note: editForm and patForm are intentionally excluded from dependencies
|
||||||
patForm,
|
// to prevent infinite loops. They are stable form objects from react-hook-form.
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Handlers managed by the hook
|
// Handlers managed by the hook
|
||||||
|
|
@ -219,7 +220,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
const updatePayload: Partial<SearchSourceConnector> = {};
|
const updatePayload: Partial<SearchSourceConnector> = {};
|
||||||
let configChanged = false;
|
let configChanged = false;
|
||||||
let newConfig: Record<string, any> | null = null;
|
let newConfig: Record<string, unknown> | null = null;
|
||||||
|
|
||||||
if (formData.name !== connector.name) {
|
if (formData.name !== connector.name) {
|
||||||
updatePayload.name = formData.name;
|
updatePayload.name = formData.name;
|
||||||
|
|
@ -296,12 +297,14 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const candidateConfig: Record<string, any> = { SEARXNG_HOST: host };
|
const candidateConfig: Record<string, unknown> = { SEARXNG_HOST: host };
|
||||||
let hasChanges = host !== (originalConfig.SEARXNG_HOST || "").trim();
|
const originalHost = typeof originalConfig.SEARXNG_HOST === "string" ? originalConfig.SEARXNG_HOST : "";
|
||||||
|
let hasChanges = host !== originalHost.trim();
|
||||||
|
|
||||||
const apiKey = (formData.SEARXNG_API_KEY || "").trim();
|
const apiKey = (formData.SEARXNG_API_KEY || "").trim();
|
||||||
const originalApiKey = (originalConfig.SEARXNG_API_KEY || "").trim();
|
const originalApiKey = typeof originalConfig.SEARXNG_API_KEY === "string" ? originalConfig.SEARXNG_API_KEY : "";
|
||||||
if (apiKey !== originalApiKey) {
|
const originalApiKeyTrimmed = originalApiKey.trim();
|
||||||
|
if (apiKey !== originalApiKeyTrimmed) {
|
||||||
candidateConfig.SEARXNG_API_KEY = apiKey || null;
|
candidateConfig.SEARXNG_API_KEY = apiKey || null;
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
}
|
}
|
||||||
|
|
@ -321,8 +324,9 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
}
|
}
|
||||||
|
|
||||||
const language = (formData.SEARXNG_LANGUAGE || "").trim();
|
const language = (formData.SEARXNG_LANGUAGE || "").trim();
|
||||||
const originalLanguage = (originalConfig.SEARXNG_LANGUAGE || "").trim();
|
const originalLanguage = typeof originalConfig.SEARXNG_LANGUAGE === "string" ? originalConfig.SEARXNG_LANGUAGE : "";
|
||||||
if (language !== originalLanguage) {
|
const originalLanguageTrimmed = originalLanguage.trim();
|
||||||
|
if (language !== originalLanguageTrimmed) {
|
||||||
candidateConfig.SEARXNG_LANGUAGE = language || null;
|
candidateConfig.SEARXNG_LANGUAGE = language || null;
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
}
|
}
|
||||||
|
|
@ -490,7 +494,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
) {
|
) {
|
||||||
newConfig = {};
|
newConfig = {};
|
||||||
|
|
||||||
if (formData.FIRECRAWL_API_KEY && formData.FIRECRAWL_API_KEY.trim()) {
|
if (formData.FIRECRAWL_API_KEY?.trim()) {
|
||||||
if (!formData.FIRECRAWL_API_KEY.startsWith("fc-")) {
|
if (!formData.FIRECRAWL_API_KEY.startsWith("fc-")) {
|
||||||
toast.warning(
|
toast.warning(
|
||||||
"Firecrawl API keys typically start with 'fc-'. Please verify your key."
|
"Firecrawl API keys typically start with 'fc-'. Please verify your key."
|
||||||
|
|
@ -504,7 +508,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formData.INITIAL_URLS !== undefined) {
|
if (formData.INITIAL_URLS !== undefined) {
|
||||||
if (formData.INITIAL_URLS && formData.INITIAL_URLS.trim()) {
|
if (formData.INITIAL_URLS?.trim()) {
|
||||||
newConfig.INITIAL_URLS = formData.INITIAL_URLS.trim();
|
newConfig.INITIAL_URLS = formData.INITIAL_URLS.trim();
|
||||||
} else if (originalConfig.INITIAL_URLS) {
|
} else if (originalConfig.INITIAL_URLS) {
|
||||||
toast.info("URLs removed from crawler configuration.");
|
toast.info("URLs removed from crawler configuration.");
|
||||||
|
|
@ -530,21 +534,19 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateConnector({
|
const updatedConnector = await updateConnector({
|
||||||
id: connectorId,
|
id: connectorId,
|
||||||
data: {
|
data: {
|
||||||
...updatePayload,
|
...updatePayload,
|
||||||
connector_type: connector.connector_type as EnumConnectorName,
|
connector_type: connector.connector_type as EnumConnectorName,
|
||||||
},
|
},
|
||||||
});
|
}) as UpdateConnectorResponse;
|
||||||
toast.success("Connector updated!");
|
toast.success("Connector updated!");
|
||||||
const newlySavedConfig = updatePayload.config || originalConfig;
|
// Use the response from the API which has the full merged config
|
||||||
|
const newlySavedConfig = updatedConnector.config || originalConfig;
|
||||||
setOriginalConfig(newlySavedConfig);
|
setOriginalConfig(newlySavedConfig);
|
||||||
if (updatePayload.name) {
|
// Update connector state with the full updated connector from the API
|
||||||
setConnector((prev) =>
|
setConnector(updatedConnector);
|
||||||
prev ? { ...prev, name: updatePayload.name!, config: newlySavedConfig } : null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (configChanged) {
|
if (configChanged) {
|
||||||
if (connector.connector_type === "GITHUB_CONNECTOR") {
|
if (connector.connector_type === "GITHUB_CONNECTOR") {
|
||||||
const savedGitHubConfig = newlySavedConfig as {
|
const savedGitHubConfig = newlySavedConfig as {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue