refactor: streamline connector management UI and enhance document handling

- Updated the ConnectorIndicator component to accurately reflect active connectors and their document counts.
- Improved the display of standalone document types in the ActiveConnectorsTab, allowing users to view all documents easily.
- Enhanced the ConnectorCard to show last indexed dates and formatted document counts for better clarity.
- Adjusted tooltip and aria-labels for accessibility and consistency across attachment upload components.
- Preserved newlines in URL input for webcrawler configuration to ensure proper backend handling.
This commit is contained in:
Anish Sarkar 2026-01-01 20:38:12 +05:30
parent 0e93d8420f
commit 543daa0434
12 changed files with 239 additions and 116 deletions

View file

@ -337,12 +337,12 @@ export const ComposerAddAttachment: FC = () => {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<TooltipIconButton
tooltip="Upload documents or add attachment"
tooltip="Upload"
side="bottom"
variant="ghost"
size="icon"
className="aui-composer-add-attachment size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
aria-label="Upload documents or add attachment"
aria-label="Upload"
>
<PlusIcon className="aui-attachment-add-icon size-5 stroke-[1.5px]" />
</TooltipIconButton>
@ -350,7 +350,7 @@ export const ComposerAddAttachment: FC = () => {
<DropdownMenuContent align="start" className="w-48 bg-background border-border">
<DropdownMenuItem onSelect={handleChatAttachment} className="cursor-pointer">
<Paperclip className="size-4" />
<span>Add attachment(s)</span>
<span>Add attachment</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={handleFileUpload} className="cursor-pointer">
<Upload className="size-4" />

View file

@ -38,11 +38,10 @@ const ConnectorIndicator: FC = () => {
? Object.entries(documentTypeCounts).filter(([_, count]) => count > 0)
: [];
const nonIndexableConnectors = connectors.filter((connector) => !connector.is_indexable);
const hasConnectors = nonIndexableConnectors.length > 0;
// Count only active connectors (matching what's shown in the Active tab)
const activeConnectorsCount = connectors.length;
const hasConnectors = activeConnectorsCount > 0;
const hasSources = hasConnectors || activeDocumentTypes.length > 0;
const totalSourceCount = nonIndexableConnectors.length + activeDocumentTypes.length;
const handleMouseEnter = useCallback(() => {
// Clear any pending close timeout
@ -76,7 +75,7 @@ const ConnectorIndicator: FC = () => {
"text-muted-foreground"
)}
aria-label={
hasSources ? `View ${totalSourceCount} connected sources` : "Add your first connector"
hasConnectors ? `View ${activeConnectorsCount} active connectors` : "Add your first connector"
}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
@ -86,9 +85,9 @@ const ConnectorIndicator: FC = () => {
) : (
<>
<Plug2 className="size-4" />
{totalSourceCount > 0 && (
{activeConnectorsCount > 0 && (
<span className="absolute -top-0.5 -right-0.5 flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-medium rounded-full bg-primary text-primary-foreground shadow-sm">
{totalSourceCount > 99 ? "99+" : totalSourceCount}
{activeConnectorsCount > 99 ? "99+" : activeConnectorsCount}
</span>
)}
</>
@ -104,35 +103,50 @@ const ConnectorIndicator: FC = () => {
>
{hasSources ? (
<div className="space-y-3">
<div className="flex items-center justify-between">
<p className="text-xs font-medium text-muted-foreground">Connected Sources</p>
<span className="text-xs font-medium bg-muted px-1.5 py-0.5 rounded">
{totalSourceCount}
</span>
</div>
<div className="flex flex-wrap gap-2">
{activeDocumentTypes.map(([docType, count]) => (
<div
key={docType}
className="flex items-center gap-1.5 rounded-md bg-muted/80 px-2.5 py-1.5 text-xs border border-border/50"
>
{getConnectorIcon(docType, "size-3.5")}
<span className="truncate max-w-[100px]">{getDocumentTypeLabel(docType)}</span>
<span className="flex items-center justify-center min-w-[18px] h-[18px] px-1 text-[10px] font-medium rounded-full bg-primary/10 text-primary">
{count > 999 ? "999+" : count}
</span>
{activeConnectorsCount > 0 && (
<div className="flex items-center justify-between">
<p className="text-xs font-medium text-muted-foreground">Active Connectors</p>
<span className="text-xs font-medium bg-muted px-1.5 py-0.5 rounded">
{activeConnectorsCount}
</span>
</div>
)}
{activeConnectorsCount > 0 && (
<div className="flex flex-wrap gap-2">
{connectors.map((connector) => (
<div
key={`connector-${connector.id}`}
className="flex items-center gap-1.5 rounded-md bg-muted/80 px-2.5 py-1.5 text-xs border border-border/50"
>
{getConnectorIcon(connector.connector_type, "size-3.5")}
<span className="truncate max-w-[100px]">{connector.name}</span>
</div>
))}
</div>
)}
{activeDocumentTypes.length > 0 && (
<>
{activeConnectorsCount > 0 && (
<div className="pt-2 border-t border-border/50">
<p className="text-xs font-medium text-muted-foreground mb-2">Documents</p>
</div>
)}
<div className="flex flex-wrap gap-2">
{activeDocumentTypes.map(([docType, count]) => (
<div
key={docType}
className="flex items-center gap-1.5 rounded-md bg-muted/80 px-2.5 py-1.5 text-xs border border-border/50"
>
{getConnectorIcon(docType, "size-3.5")}
<span className="truncate max-w-[100px]">{getDocumentTypeLabel(docType)}</span>
<span className="flex items-center justify-center min-w-[18px] h-[18px] px-1 text-[10px] font-medium rounded-full bg-primary/10 text-primary">
{count > 999 ? "999+" : count}
</span>
</div>
))}
</div>
))}
{nonIndexableConnectors.map((connector) => (
<div
key={`connector-${connector.id}`}
className="flex items-center gap-1.5 rounded-md bg-muted/80 px-2.5 py-1.5 text-xs border border-border/50"
>
{getConnectorIcon(connector.connector_type, "size-3.5")}
<span className="truncate max-w-[100px]">{connector.name}</span>
</div>
))}
</div>
</>
)}
<div className="pt-1 border-t border-border/50">
<Link
href={`/dashboard/${searchSpaceId}/connectors/add`}

View file

@ -159,6 +159,7 @@ export const ConnectorIndicator: FC = () => {
const hasConnectors = connectors.length > 0;
const hasSources = hasConnectors || activeDocumentTypes.length > 0;
const totalSourceCount = connectors.length + activeDocumentTypes.length;
const activeConnectorsCount = connectors.length; // Only actual connectors, not document types
// Check which connectors are already connected
const connectedTypes = new Set(
@ -170,7 +171,7 @@ export const ConnectorIndicator: FC = () => {
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<TooltipIconButton
tooltip={hasSources ? `Manage ${totalSourceCount} sources` : "Connect your data"}
tooltip={hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"}
side="bottom"
className={cn(
"size-[34px] rounded-full p-1 flex items-center justify-center transition-colors relative",
@ -179,7 +180,7 @@ export const ConnectorIndicator: FC = () => {
"border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none"
)}
aria-label={
hasSources ? `View ${totalSourceCount} connected sources` : "Add your first connector"
hasConnectors ? `View ${activeConnectorsCount} connectors` : "Add your first connector"
}
onClick={() => handleOpenChange(true)}
>
@ -188,9 +189,9 @@ export const ConnectorIndicator: FC = () => {
) : (
<>
<Cable className="size-4 stroke-[1.5px]" />
{totalSourceCount > 0 && (
{activeConnectorsCount > 0 && (
<span className="absolute -top-0.5 right-0 flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-medium rounded-full bg-primary text-primary-foreground shadow-sm">
{totalSourceCount > 99 ? "99+" : totalSourceCount}
{activeConnectorsCount > 99 ? "99+" : activeConnectorsCount}
</span>
)}
</>
@ -259,7 +260,7 @@ export const ConnectorIndicator: FC = () => {
{/* Header */}
<ConnectorDialogHeader
activeTab={activeTab}
totalSourceCount={totalSourceCount}
totalSourceCount={activeConnectorsCount}
searchQuery={searchQuery}
onTabChange={handleTabChange}
onSearchChange={setSearchQuery}

View file

@ -3,6 +3,7 @@
import { IconBrandYoutube } from "@tabler/icons-react";
import { FileText, Loader2 } from "lucide-react";
import { type FC } from "react";
import { format } from "date-fns";
import { Button } from "@/components/ui/button";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { LogActiveTask } from "@/contracts/types/log.types";
@ -16,6 +17,7 @@ interface ConnectorCardProps {
isConnected?: boolean;
isConnecting?: boolean;
documentCount?: number;
lastIndexedAt?: string | null;
isIndexing?: boolean;
activeTask?: LogActiveTask;
onConnect?: () => void;
@ -33,6 +35,20 @@ function extractIndexedCount(message: string | undefined): number | null {
return match ? parseInt(match[1], 10) : null;
}
/**
* Format document count (e.g., "1.2k docs", "500 docs", "1.5M docs")
*/
function formatDocumentCount(count: number | undefined): string {
if (count === undefined || count === 0) return "0 docs";
if (count < 1000) return `${count} docs`;
if (count < 1000000) {
const k = (count / 1000).toFixed(1);
return `${k.replace(/\.0$/, "")}k docs`;
}
const m = (count / 1000000).toFixed(1);
return `${m.replace(/\.0$/, "")}M docs`;
}
export const ConnectorCard: FC<ConnectorCardProps> = ({
id,
title,
@ -41,6 +57,7 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
isConnected = false,
isConnecting = false,
documentCount,
lastIndexedAt,
isIndexing = false,
activeTask,
onConnect,
@ -70,18 +87,16 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
}
if (isConnected) {
if (documentCount !== undefined && documentCount > 0) {
// Show last indexed date for connected connectors
if (lastIndexedAt) {
return (
<span className="inline-flex items-center gap-1.5">
<FileText className="size-3 flex-shrink-0" />
<span className="whitespace-nowrap">
{documentCount.toLocaleString()} document{documentCount !== 1 ? "s" : ""}
</span>
<span className="whitespace-nowrap">
Last indexed: {format(new Date(lastIndexedAt), "MMM d, yyyy")}
</span>
);
}
// Fallback for connected but no documents yet
return <span className="whitespace-nowrap">No documents indexed</span>;
// Fallback for connected but never indexed
return <span className="whitespace-nowrap">Never indexed</span>;
}
return description;
@ -105,6 +120,11 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
<div className="text-[11px] text-muted-foreground mt-1">
{getStatusContent()}
</div>
{isConnected && documentCount !== undefined && (
<p className="text-[11px] text-muted-foreground mt-0.5">
{formatDocumentCount(documentCount)}
</p>
)}
</div>
<Button
size="sm"
@ -123,6 +143,8 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
"Syncing..."
) : isConnected ? (
"Manage"
) : id === "youtube-crawler" ? (
"Add"
) : connectorType ? (
"Connect"
) : (

View file

@ -43,9 +43,11 @@ export const WebcrawlerConfig: FC<ConnectorConfigProps> = ({
const handleUrlsChange = (value: string) => {
setInitialUrls(value);
if (onConfigChange) {
// Preserve newlines for multi-line URL input
// Backend will handle trimming individual URLs when splitting by newline
onConfigChange({
...connector.config,
INITIAL_URLS: value.trim() || undefined,
INITIAL_URLS: value || undefined,
});
}
};

View file

@ -25,7 +25,7 @@ interface ConnectorEditViewProps {
onSave: () => void;
onDisconnect: () => void;
onBack: () => void;
onConfigChange?: (config: Record<string, any>) => void;
onConfigChange?: (config: Record<string, unknown>) => void;
onNameChange?: (name: string) => void;
}
@ -201,35 +201,37 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
</div>
{/* Fixed Footer - Action buttons */}
<div className="flex-shrink-0 flex items-center justify-between px-6 sm:px-12 py-6 bg-muted border-t border-border">
<div className="flex-shrink-0 flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3 sm:gap-0 px-6 sm:px-12 py-4 sm:py-6 bg-muted border-t border-border">
{showDisconnectConfirm ? (
<div className="flex items-center gap-3">
<span className="text-xs sm:text-sm text-muted-foreground">Are you sure?</span>
<Button
variant="destructive"
size="sm"
onClick={handleDisconnectConfirm}
disabled={isDisconnecting}
className="text-xs sm:text-sm"
>
{isDisconnecting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Disconnecting...
</>
) : (
"Confirm Disconnect"
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleDisconnectCancel}
disabled={isDisconnecting}
className="text-xs sm:text-sm"
>
Cancel
</Button>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 flex-1 sm:flex-initial">
<span className="text-xs sm:text-sm text-muted-foreground sm:whitespace-nowrap">Are you sure?</span>
<div className="flex items-center gap-2 sm:gap-3">
<Button
variant="destructive"
size="sm"
onClick={handleDisconnectConfirm}
disabled={isDisconnecting}
className="text-xs sm:text-sm flex-1 sm:flex-initial"
>
{isDisconnecting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Disconnecting...
</>
) : (
"Confirm Disconnect"
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleDisconnectCancel}
disabled={isDisconnecting}
className="text-xs sm:text-sm flex-1 sm:flex-initial"
>
Cancel
</Button>
</div>
</div>
) : (
<Button
@ -237,13 +239,17 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
size="sm"
onClick={handleDisconnectClick}
disabled={isSaving || isDisconnecting}
className="text-xs sm:text-sm"
className="text-xs sm:text-sm flex-1 sm:flex-initial"
>
<Trash2 className="mr-2 h-4 w-4" />
Disconnect
</Button>
)}
<Button onClick={onSave} disabled={isSaving || isDisconnecting} className="text-xs sm:text-sm">
<Button
onClick={onSave}
disabled={isSaving || isDisconnecting}
className="text-xs sm:text-sm flex-1 sm:flex-initial"
>
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />

View file

@ -1,14 +1,16 @@
"use client";
import { format } from "date-fns";
import { Cable, FileText, Loader2 } from "lucide-react";
import { ArrowRight, Cable, Loader2 } from "lucide-react";
import type { FC } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import type { LogSummary, LogActiveTask } from "@/contracts/types/log.types";
import { cn } from "@/lib/utils";
import { getDocumentCountForConnector } from "../utils/connector-document-mapping";
import {
TabsContent,
} from "@/components/ui/tabs";
@ -31,46 +33,75 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
connectors,
indexingConnectorIds,
logsSummary,
searchSpaceId,
onTabChange,
onManage,
}) => {
const router = useRouter();
const handleViewAllDocuments = () => {
router.push(`/dashboard/${searchSpaceId}/documents`);
};
// Convert activeDocumentTypes array to Record for utility function
const documentTypeCounts = activeDocumentTypes.reduce(
(acc, [docType, count]) => {
acc[docType] = count;
return acc;
},
{} as Record<string, number>
);
// Format document count (e.g., "1.2k docs", "500 docs", "1.5M docs")
const formatDocumentCount = (count: number | undefined): string => {
if (count === undefined || count === 0) return "0 docs";
if (count < 1000) return `${count} docs`;
if (count < 1000000) {
const k = (count / 1000).toFixed(1);
return `${k.replace(/\.0$/, "")}k docs`;
}
const m = (count / 1000000).toFixed(1);
return `${m.replace(/\.0$/, "")}M docs`;
};
// Document types that should be shown as cards (not from connectors)
// These are: EXTENSION (browser extension), FILE (uploaded files), NOTE (editor notes),
// YOUTUBE_VIDEO (YouTube videos), and CRAWLED_URL (web pages - shown separately even though it can come from WEBCRAWLER_CONNECTOR)
const standaloneDocumentTypes = ["EXTENSION", "FILE", "NOTE", "YOUTUBE_VIDEO", "CRAWLED_URL"];
// Filter to only show standalone document types that have documents (count > 0)
const standaloneDocuments = activeDocumentTypes
.filter(([docType, count]) =>
standaloneDocumentTypes.includes(docType) && count > 0
)
.map(([docType, count]) => ({
type: docType,
count,
label: getDocumentTypeLabel(docType),
}));
return (
<TabsContent value="active" className="m-0">
{hasSources ? (
<div className="space-y-6">
<div className="flex items-center gap-2 mb-4">
<h3 className="text-sm font-semibold text-muted-foreground">
Currently Active
</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{activeDocumentTypes.map(([docType, count]) => (
<div
key={docType}
className="flex items-center gap-4 p-4 rounded-xl bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10 border border-border transition-all"
>
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-slate-400/5 dark:bg-white/5 border border-slate-400/5 dark:border-white/5">
{getConnectorIcon(docType, "size-6")}
</div>
<div>
<p className="text-[14px] font-semibold leading-tight">
{getDocumentTypeLabel(docType)}
</p>
<p className="text-[11px] text-muted-foreground mt-1 inline-flex items-center gap-1.5">
<FileText className="size-3 flex-shrink-0" />
<span className="whitespace-nowrap">
{(count as number).toLocaleString()} document{count !== 1 ? "s" : ""}
</span>
</p>
</div>
{/* Active Connectors Section */}
{connectors.length > 0 && (
<div className="space-y-4">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold text-muted-foreground">
Active Connectors
</h3>
</div>
))}
{connectors.map((connector) => {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{connectors.map((connector) => {
const isIndexing = indexingConnectorIds.has(connector.id);
const activeTask = logsSummary?.active_tasks?.find(
(task: LogActiveTask) => task.connector_id === connector.id
);
const documentCount = getDocumentCountForConnector(
connector.connector_type,
documentTypeCounts
);
return (
<div
@ -113,6 +144,9 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
: "Never indexed"}
</p>
)}
<p className="text-[11px] text-muted-foreground mt-0.5">
{formatDocumentCount(documentCount)}
</p>
</div>
<Button
variant="secondary"
@ -126,7 +160,47 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
</div>
);
})}
</div>
</div>
</div>
)}
{/* Standalone Documents Section */}
{standaloneDocuments.length > 0 && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-muted-foreground">
Documents
</h3>
<Button
variant="ghost"
size="sm"
onClick={handleViewAllDocuments}
className="h-7 text-xs text-muted-foreground hover:text-foreground gap-1.5"
>
View all documents
<ArrowRight className="size-3" />
</Button>
</div>
<div className="flex flex-wrap items-center gap-2">
{standaloneDocuments.map((doc) => (
<div
key={doc.type}
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full border border-border bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10 transition-all"
>
<div className="flex items-center justify-center">
{getConnectorIcon(doc.type, "size-3.5")}
</div>
<span className="text-[12px] font-medium">
{doc.label}
</span>
<span className="text-[11px] text-muted-foreground">
{formatDocumentCount(doc.count)}
</span>
</div>
))}
</div>
</div>
)}
</div>
) : (
<div className="flex flex-col items-center justify-center py-20 text-center">

View file

@ -101,6 +101,7 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
isConnected={isConnected}
isConnecting={isConnecting}
documentCount={documentCount}
lastIndexedAt={actualConnector?.last_indexed_at}
isIndexing={isIndexing}
activeTask={activeTask}
onConnect={() => onConnectOAuth(connector)}
@ -162,6 +163,7 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
isConnected={isConnected}
isConnecting={isConnecting}
documentCount={documentCount}
lastIndexedAt={actualConnector?.last_indexed_at}
isIndexing={isIndexing}
activeTask={activeTask}
onConnect={handleConnect}
@ -230,6 +232,7 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
isConnected={isConnected}
isConnecting={isConnecting}
documentCount={documentCount}
lastIndexedAt={actualConnector?.last_indexed_at}
isIndexing={isIndexing}
activeTask={activeTask}
onConnect={handleConnect}