feat: streamline Composio connector configurations and enhance UI interactions

- Refactored Composio connector configuration components to improve modularity and maintainability.
- Simplified the ComposioCalendarConfig, ComposioGmailConfig, and ComposioDriveConfig components by removing unnecessary state management and UI elements.
- Added functionality to remove selected folders and files in the Google Drive and Composio Drive configurations, enhancing user experience.
- Updated connector display names for better clarity in the UI.
- Improved the overall structure of the connector edit view for better readability and usability.
This commit is contained in:
Anish Sarkar 2026-01-23 20:19:04 +05:30
parent 1343fabeee
commit 12f45e1bd3
8 changed files with 88 additions and 390 deletions

View file

@ -7,7 +7,7 @@ import type { FC } from "react";
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { Tabs, TabsContent } from "@/components/ui/tabs";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { useConnectorsElectric } from "@/hooks/use-connectors-electric";
@ -185,6 +185,7 @@ export const ConnectorIndicator: FC = () => {
</TooltipIconButton>
<DialogContent className="max-w-3xl w-[95vw] sm:w-full h-[75vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border bg-muted text-foreground [&>button]:right-4 sm:[&>button]:right-12 [&>button]:top-6 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button_svg]:size-5">
<DialogTitle className="sr-only">Manage Connectors</DialogTitle>
{/* YouTube Crawler View - shown when adding YouTube videos */}
{isYouTubeView && searchSpaceId ? (
<YouTubeCrawlerView searchSpaceId={searchSpaceId} onBack={handleBackFromYouTube} />

View file

@ -1,17 +1,6 @@
"use client";
import { Calendar, Clock } from "lucide-react";
import type { FC } from "react";
import { useEffect, useState } from "react";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
interface ComposioCalendarConfigProps {
@ -20,201 +9,7 @@ interface ComposioCalendarConfigProps {
onNameChange?: (name: string) => void;
}
interface CalendarIndexingOptions {
max_events: number;
include_recurring: boolean;
include_past_events: boolean;
days_ahead: number;
}
const DEFAULT_CALENDAR_OPTIONS: CalendarIndexingOptions = {
max_events: 500,
include_recurring: true,
include_past_events: true,
days_ahead: 365,
};
export const ComposioCalendarConfig: FC<ComposioCalendarConfigProps> = ({ connector, onConfigChange }) => {
const isIndexable = connector.config?.is_indexable as boolean;
// Initialize with existing options from connector config
const existingOptions =
(connector.config?.calendar_options as CalendarIndexingOptions | undefined) || DEFAULT_CALENDAR_OPTIONS;
const [calendarOptions, setCalendarOptions] = useState<CalendarIndexingOptions>(existingOptions);
// Update options when connector config changes
useEffect(() => {
const options =
(connector.config?.calendar_options as CalendarIndexingOptions | undefined) ||
DEFAULT_CALENDAR_OPTIONS;
setCalendarOptions(options);
}, [connector.config]);
const updateConfig = (options: CalendarIndexingOptions) => {
if (onConfigChange) {
onConfigChange({
...connector.config,
calendar_options: options,
});
}
};
const handleOptionChange = (key: keyof CalendarIndexingOptions, value: number | boolean) => {
const newOptions = { ...calendarOptions, [key]: value };
setCalendarOptions(newOptions);
updateConfig(newOptions);
};
// Only show configuration if the connector is indexable
if (!isIndexable) {
return <div className="space-y-6" />;
}
return (
<div className="space-y-6">
{/* Calendar Indexing Options */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-4">
<div className="space-y-1 sm:space-y-2">
<div className="flex items-center gap-2">
<Calendar className="size-4 text-blue-500" />
<h3 className="font-medium text-sm sm:text-base">Calendar Indexing Options</h3>
</div>
<p className="text-xs sm:text-sm text-muted-foreground">
Configure how events are indexed from your Google Calendar.
</p>
</div>
{/* Max events to index */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="max-events" className="text-sm font-medium">
Max events to index
</Label>
<p className="text-xs text-muted-foreground">
Maximum number of events to index per sync
</p>
</div>
<Select
value={calendarOptions.max_events.toString()}
onValueChange={(value) =>
handleOptionChange("max_events", parseInt(value, 10))
}
>
<SelectTrigger
id="max-events"
className="w-[140px] bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
>
<SelectValue placeholder="Select limit" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="100" className="text-xs sm:text-sm">
100 events
</SelectItem>
<SelectItem value="250" className="text-xs sm:text-sm">
250 events
</SelectItem>
<SelectItem value="500" className="text-xs sm:text-sm">
500 events
</SelectItem>
<SelectItem value="1000" className="text-xs sm:text-sm">
1000 events
</SelectItem>
<SelectItem value="2500" className="text-xs sm:text-sm">
2500 events
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Days ahead */}
<div className="space-y-2 pt-2 border-t border-slate-400/20">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<div className="flex items-center gap-1.5">
<Clock className="size-3.5 text-muted-foreground" />
<Label htmlFor="days-ahead" className="text-sm font-medium">
Future events range
</Label>
</div>
<p className="text-xs text-muted-foreground">
How far ahead to index future events
</p>
</div>
<Select
value={calendarOptions.days_ahead.toString()}
onValueChange={(value) =>
handleOptionChange("days_ahead", parseInt(value, 10))
}
>
<SelectTrigger
id="days-ahead"
className="w-[140px] bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
>
<SelectValue placeholder="Select range" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="30" className="text-xs sm:text-sm">
30 days
</SelectItem>
<SelectItem value="90" className="text-xs sm:text-sm">
90 days
</SelectItem>
<SelectItem value="180" className="text-xs sm:text-sm">
180 days
</SelectItem>
<SelectItem value="365" className="text-xs sm:text-sm">
1 year
</SelectItem>
<SelectItem value="730" className="text-xs sm:text-sm">
2 years
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Include recurring events toggle */}
<div className="flex items-center justify-between pt-2 border-t border-slate-400/20">
<div className="space-y-0.5">
<Label htmlFor="include-recurring" className="text-sm font-medium">
Include recurring events
</Label>
<p className="text-xs text-muted-foreground">
Index individual instances of recurring events
</p>
</div>
<Switch
id="include-recurring"
checked={calendarOptions.include_recurring}
onCheckedChange={(checked) =>
handleOptionChange("include_recurring", checked)
}
/>
</div>
{/* Include past events toggle */}
<div className="flex items-center justify-between pt-2 border-t border-slate-400/20">
<div className="space-y-0.5">
<Label htmlFor="include-past" className="text-sm font-medium">
Include past events
</Label>
<p className="text-xs text-muted-foreground">
Index events from before the selected date range
</p>
</div>
<Switch
id="include-past"
checked={calendarOptions.include_past_events}
onCheckedChange={(checked) =>
handleOptionChange("include_past_events", checked)
}
/>
</div>
</div>
</div>
);
export const ComposioCalendarConfig: FC<ComposioCalendarConfigProps> = () => {
return <div className="space-y-6" />;
};

View file

@ -1,6 +1,6 @@
"use client";
import { File, FileSpreadsheet, FileText, FolderClosed, Image, Presentation } from "lucide-react";
import { File, FileSpreadsheet, FileText, FolderClosed, Image, Presentation, X } from "lucide-react";
import type { FC } from "react";
import { useEffect, useState } from "react";
import { ComposioDriveFolderTree } from "@/components/connectors/composio-drive-folder-tree";
@ -143,6 +143,18 @@ export const ComposioDriveConfig: FC<ComposioDriveConfigProps> = ({ connector, o
updateConfig(selectedFolders, selectedFiles, newOptions);
};
const handleRemoveFolder = (folderId: string) => {
const newFolders = selectedFolders.filter((folder) => folder.id !== folderId);
setSelectedFolders(newFolders);
updateConfig(newFolders, selectedFiles, indexingOptions);
};
const handleRemoveFile = (fileId: string) => {
const newFiles = selectedFiles.filter((file) => file.id !== fileId);
setSelectedFiles(newFiles);
updateConfig(selectedFolders, newFiles, indexingOptions);
};
const totalSelected = selectedFolders.length + selectedFiles.length;
// Only show configuration if the connector is indexable
@ -176,29 +188,45 @@ export const ComposioDriveConfig: FC<ComposioDriveConfigProps> = ({ connector, o
`${selectedFiles.length} file${selectedFiles.length > 1 ? "s" : ""}`
);
}
return parts.length > 0 ? `(${parts.join(" ")})` : "";
return parts.length > 0 ? `(${parts.join(", ")})` : "";
})()}
</p>
<div className="max-h-20 sm:max-h-24 overflow-y-auto space-y-1">
{selectedFolders.map((folder) => (
<p
<div
key={folder.id}
className="text-xs sm:text-sm text-muted-foreground truncate flex items-center gap-1.5"
title={folder.name}
>
<FolderClosed className="size-3.5 shrink-0 text-gray-500" />
{folder.name}
</p>
<span className="flex-1 truncate">{folder.name}</span>
<button
type="button"
onClick={() => handleRemoveFolder(folder.id)}
className="shrink-0 p-0.5 hover:bg-muted-foreground/20 rounded transition-colors"
aria-label={`Remove ${folder.name}`}
>
<X className="size-3.5" />
</button>
</div>
))}
{selectedFiles.map((file) => (
<p
<div
key={file.id}
className="text-xs sm:text-sm text-muted-foreground truncate flex items-center gap-1.5"
title={file.name}
>
{getFileIconFromName(file.name)}
{file.name}
</p>
<span className="flex-1 truncate">{file.name}</span>
<button
type="button"
onClick={() => handleRemoveFile(file.id)}
className="shrink-0 p-0.5 hover:bg-muted-foreground/20 rounded transition-colors"
aria-label={`Remove ${file.name}`}
>
<X className="size-3.5" />
</button>
</div>
))}
</div>
</div>

View file

@ -1,17 +1,6 @@
"use client";
import { Mail, Tag } from "lucide-react";
import type { FC } from "react";
import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
interface ComposioGmailConfigProps {
@ -20,155 +9,7 @@ interface ComposioGmailConfigProps {
onNameChange?: (name: string) => void;
}
interface GmailIndexingOptions {
max_emails: number;
label_filter: string;
search_query: string;
}
const DEFAULT_GMAIL_OPTIONS: GmailIndexingOptions = {
max_emails: 500,
label_filter: "",
search_query: "",
};
export const ComposioGmailConfig: FC<ComposioGmailConfigProps> = ({ connector, onConfigChange }) => {
const isIndexable = connector.config?.is_indexable as boolean;
// Initialize with existing options from connector config
const existingOptions =
(connector.config?.gmail_options as GmailIndexingOptions | undefined) || DEFAULT_GMAIL_OPTIONS;
const [gmailOptions, setGmailOptions] = useState<GmailIndexingOptions>(existingOptions);
// Update options when connector config changes
useEffect(() => {
const options =
(connector.config?.gmail_options as GmailIndexingOptions | undefined) ||
DEFAULT_GMAIL_OPTIONS;
setGmailOptions(options);
}, [connector.config]);
const updateConfig = (options: GmailIndexingOptions) => {
if (onConfigChange) {
onConfigChange({
...connector.config,
gmail_options: options,
});
}
};
const handleOptionChange = (key: keyof GmailIndexingOptions, value: number | string) => {
const newOptions = { ...gmailOptions, [key]: value };
setGmailOptions(newOptions);
updateConfig(newOptions);
};
// Only show configuration if the connector is indexable
if (!isIndexable) {
return <div className="space-y-6" />;
}
return (
<div className="space-y-6">
{/* Gmail Indexing Options */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-4">
<div className="space-y-1 sm:space-y-2">
<div className="flex items-center gap-2">
<Mail className="size-4 text-red-500" />
<h3 className="font-medium text-sm sm:text-base">Gmail Indexing Options</h3>
</div>
<p className="text-xs sm:text-sm text-muted-foreground">
Configure how emails are indexed from your Gmail account.
</p>
</div>
{/* Max emails to index */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="max-emails" className="text-sm font-medium">
Max emails to index
</Label>
<p className="text-xs text-muted-foreground">
Maximum number of emails to index per sync
</p>
</div>
<Select
value={gmailOptions.max_emails.toString()}
onValueChange={(value) =>
handleOptionChange("max_emails", parseInt(value, 10))
}
>
<SelectTrigger
id="max-emails"
className="w-[140px] bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
>
<SelectValue placeholder="Select limit" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="100" className="text-xs sm:text-sm">
100 emails
</SelectItem>
<SelectItem value="250" className="text-xs sm:text-sm">
250 emails
</SelectItem>
<SelectItem value="500" className="text-xs sm:text-sm">
500 emails
</SelectItem>
<SelectItem value="1000" className="text-xs sm:text-sm">
1000 emails
</SelectItem>
<SelectItem value="2500" className="text-xs sm:text-sm">
2500 emails
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Label filter */}
<div className="space-y-2 pt-2 border-t border-slate-400/20">
<div className="space-y-0.5">
<div className="flex items-center gap-1.5">
<Tag className="size-3.5 text-muted-foreground" />
<Label htmlFor="label-filter" className="text-sm font-medium">
Label filter (optional)
</Label>
</div>
<p className="text-xs text-muted-foreground">
Only index emails with this label (e.g., "INBOX", "IMPORTANT", "work")
</p>
</div>
<Input
id="label-filter"
value={gmailOptions.label_filter}
onChange={(e) => handleOptionChange("label_filter", e.target.value)}
placeholder="Enter label name..."
className="bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
/>
</div>
{/* Search query */}
<div className="space-y-2 pt-2 border-t border-slate-400/20">
<div className="space-y-0.5">
<Label htmlFor="search-query" className="text-sm font-medium">
Search query (optional)
</Label>
<p className="text-xs text-muted-foreground">
Gmail search query to filter emails (e.g., "from:boss@company.com", "has:attachment")
</p>
</div>
<Input
id="search-query"
value={gmailOptions.search_query}
onChange={(e) => handleOptionChange("search_query", e.target.value)}
placeholder="Enter Gmail search query..."
className="bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
/>
</div>
</div>
</div>
);
export const ComposioGmailConfig: FC<ComposioGmailConfigProps> = () => {
return <div className="space-y-6" />;
};

View file

@ -1,6 +1,6 @@
"use client";
import { File, FileSpreadsheet, FileText, FolderClosed, Image, Presentation } from "lucide-react";
import { File, FileSpreadsheet, FileText, FolderClosed, Image, Presentation, X } from "lucide-react";
import type { FC } from "react";
import { useEffect, useState } from "react";
import { GoogleDriveFolderTree } from "@/components/connectors/google-drive-folder-tree";
@ -135,6 +135,18 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfi
updateConfig(selectedFolders, selectedFiles, newOptions);
};
const handleRemoveFolder = (folderId: string) => {
const newFolders = selectedFolders.filter((folder) => folder.id !== folderId);
setSelectedFolders(newFolders);
updateConfig(newFolders, selectedFiles, indexingOptions);
};
const handleRemoveFile = (fileId: string) => {
const newFiles = selectedFiles.filter((file) => file.id !== fileId);
setSelectedFiles(newFiles);
updateConfig(selectedFolders, newFiles, indexingOptions);
};
const totalSelected = selectedFolders.length + selectedFiles.length;
return (
@ -161,29 +173,45 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfi
if (selectedFiles.length > 0) {
parts.push(`${selectedFiles.length} file${selectedFiles.length > 1 ? "s" : ""}`);
}
return parts.length > 0 ? `(${parts.join(" ")})` : "";
return parts.length > 0 ? `(${parts.join(", ")})` : "";
})()}
</p>
<div className="max-h-20 sm:max-h-24 overflow-y-auto space-y-1">
{selectedFolders.map((folder) => (
<p
<div
key={folder.id}
className="text-xs sm:text-sm text-muted-foreground truncate flex items-center gap-1.5"
title={folder.name}
>
<FolderClosed className="size-3.5 shrink-0 text-gray-500" />
{folder.name}
</p>
<span className="flex-1 truncate">{folder.name}</span>
<button
type="button"
onClick={() => handleRemoveFolder(folder.id)}
className="shrink-0 p-0.5 hover:bg-muted-foreground/20 rounded transition-colors"
aria-label={`Remove ${folder.name}`}
>
<X className="size-3.5" />
</button>
</div>
))}
{selectedFiles.map((file) => (
<p
<div
key={file.id}
className="text-xs sm:text-sm text-muted-foreground truncate flex items-center gap-1.5"
title={file.name}
>
{getFileIconFromName(file.name)}
{file.name}
</p>
<span className="flex-1 truncate">{file.name}</span>
<button
type="button"
onClick={() => handleRemoveFile(file.id)}
className="shrink-0 p-0.5 hover:bg-muted-foreground/20 rounded transition-colors"
aria-label={`Remove ${file.name}`}
>
<X className="size-3.5" />
</button>
</div>
))}
</div>
</div>

View file

@ -9,6 +9,7 @@ import { cn } from "@/lib/utils";
import { DateRangeSelector } from "../../components/date-range-selector";
import { PeriodicSyncConfig } from "../../components/periodic-sync-config";
import { getConnectorConfigComponent } from "../index";
import { getConnectorDisplayName } from "../../tabs/all-connectors-tab";
interface ConnectorEditViewProps {
connector: SearchSourceConnector;
@ -151,7 +152,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
</div>
<div className="flex-1 min-w-0">
<h2 className="text-xl sm:text-2xl font-semibold tracking-tight text-wrap whitespace-normal wrap-break-word">
{connector.name}
{getConnectorDisplayName(connector.name)}
</h2>
<p className="text-xs sm:text-base text-muted-foreground mt-1">
Manage your connector settings and sync configuration

View file

@ -15,6 +15,7 @@ import { connectorsApiService } from "@/lib/apis/connectors-api.service";
import { cn } from "@/lib/utils";
import { COMPOSIO_CONNECTORS, OAUTH_CONNECTORS } from "../constants/connector-constants";
import { getDocumentCountForConnector } from "../utils/connector-document-mapping";
import { getConnectorDisplayName } from "./all-connectors-tab";
interface ActiveConnectorsTabProps {
searchQuery: string;
@ -263,8 +264,8 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className="text-[14px] font-semibold leading-tight">
{connector.name}
<p className="text-[14px] font-semibold leading-tight truncate">
{getConnectorDisplayName(connector.name)}
</p>
</div>
{isIndexing ? (