feat: Enhance connector management UI with improved loading states, add document count display for connectors, and implement indexing progress indicators for better user feedback.

This commit is contained in:
Anish Sarkar 2025-12-31 17:21:26 +05:30
parent 3ac806dcdf
commit 163df8fda7
10 changed files with 287 additions and 95 deletions

View file

@ -136,12 +136,8 @@ export const ConnectorIndicator: FC = () => {
if (!logsSummary?.active_tasks) return new Set<number>();
return new Set(
logsSummary.active_tasks
.filter((task) => task.source?.includes("connector_indexing"))
.map((task) => {
const match = task.source?.match(/connector[_-]?(\d+)/i);
return match ? parseInt(match[1], 10) : null;
})
.filter((id): id is number => id !== null)
.filter((task) => task.source?.includes("connector_indexing") && task.connector_id != null)
.map((task) => task.connector_id as number)
);
}, [logsSummary?.active_tasks]);
@ -261,19 +257,22 @@ export const ConnectorIndicator: FC = () => {
<div className="flex-1 min-h-0 relative overflow-hidden">
<div className="h-full overflow-y-auto" onScroll={handleScroll}>
<div className="px-6 sm:px-12 py-6 sm:py-8 pb-16 sm:pb-16">
<TabsContent value="all" className="m-0">
<AllConnectorsTab
searchQuery={searchQuery}
searchSpaceId={searchSpaceId}
connectedTypes={connectedTypes}
connectingId={connectingId}
allConnectors={allConnectors}
onConnectOAuth={handleConnectOAuth}
onConnectNonOAuth={handleConnectNonOAuth}
onCreateWebcrawler={handleCreateWebcrawler}
onManage={handleStartEdit}
/>
</TabsContent>
<TabsContent value="all" className="m-0">
<AllConnectorsTab
searchQuery={searchQuery}
searchSpaceId={searchSpaceId}
connectedTypes={connectedTypes}
connectingId={connectingId}
allConnectors={allConnectors}
documentTypeCounts={documentTypeCounts}
indexingConnectorIds={indexingConnectorIds}
logsSummary={logsSummary}
onConnectOAuth={handleConnectOAuth}
onConnectNonOAuth={handleConnectNonOAuth}
onCreateWebcrawler={handleCreateWebcrawler}
onManage={handleStartEdit}
/>
</TabsContent>
<ActiveConnectorsTab
hasSources={hasSources}

View file

@ -1,9 +1,10 @@
"use client";
import { Loader2 } from "lucide-react";
import { FileText, Loader2 } from "lucide-react";
import { type FC } from "react";
import { Button } from "@/components/ui/button";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { LogActiveTask } from "@/contracts/types/log.types";
interface ConnectorCardProps {
id: string;
@ -12,10 +13,24 @@ interface ConnectorCardProps {
connectorType: string;
isConnected?: boolean;
isConnecting?: boolean;
documentCount?: number;
isIndexing?: boolean;
activeTask?: LogActiveTask;
onConnect?: () => void;
onManage?: () => void;
}
/**
* Extract a number from the active task message for display
* Looks for patterns like "45 indexed", "Processing 123", etc.
*/
function extractIndexedCount(message: string | undefined): number | null {
if (!message) return null;
// Try to find a number in the message
const match = message.match(/(\d+)/);
return match ? parseInt(match[1], 10) : null;
}
export const ConnectorCard: FC<ConnectorCardProps> = ({
id,
title,
@ -23,9 +38,53 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
connectorType,
isConnected = false,
isConnecting = false,
documentCount,
isIndexing = false,
activeTask,
onConnect,
onManage,
}) => {
// Extract count from active task message during indexing
const indexingCount = extractIndexedCount(activeTask?.message);
// Determine the status content to display
const getStatusContent = () => {
if (isIndexing) {
return (
<div className="flex items-center gap-2 w-full max-w-[200px]">
<span className="text-[11px] text-primary font-medium whitespace-nowrap">
{indexingCount !== null ? (
<>{indexingCount.toLocaleString()} indexed</>
) : (
"Syncing..."
)}
</span>
{/* Indeterminate progress bar with animation */}
<div className="relative flex-1 h-1 overflow-hidden rounded-full bg-primary/20">
<div className="absolute h-full bg-primary rounded-full animate-progress-indeterminate" />
</div>
</div>
);
}
if (isConnected) {
if (documentCount !== undefined && documentCount > 0) {
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>
);
}
// Fallback for connected but no documents yet
return <span className="whitespace-nowrap">No documents indexed</span>;
}
return description;
};
return (
<div className="group relative flex items-center gap-4 p-4 rounded-xl text-left transition-all duration-200 w-full border border-border bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10">
<div className="flex h-12 w-12 items-center justify-center rounded-lg transition-colors flex-shrink-0 bg-slate-400/5 dark:bg-white/5 border border-slate-400/5 dark:border-white/5">
@ -35,19 +94,21 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
<div className="flex items-center gap-2">
<span className="text-[14px] font-semibold leading-tight">{title}</span>
</div>
<p className="text-[11px] text-muted-foreground truncate mt-1">
{isConnected ? "Connected" : description}
</p>
<div className="text-[11px] text-muted-foreground mt-1">
{getStatusContent()}
</div>
</div>
<Button
size="sm"
variant={isConnected ? "outline" : "default"}
className="h-8 text-[11px] px-3 rounded-lg flex-shrink-0 font-medium"
onClick={isConnected ? onManage : onConnect}
disabled={isConnecting}
disabled={isConnecting || isIndexing}
>
{isConnecting ? (
<Loader2 className="size-3 animate-spin" />
) : isIndexing ? (
"Syncing..."
) : isConnected ? (
"Manage"
) : (

View file

@ -1,8 +1,7 @@
"use client";
import { format } from "date-fns";
import { Cable, Loader2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { Cable, FileText, Loader2 } from "lucide-react";
import type { FC } from "react";
import { Button } from "@/components/ui/button";
import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon";
@ -32,11 +31,9 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
connectors,
indexingConnectorIds,
logsSummary,
searchSpaceId,
onTabChange,
onManage,
}) => {
const router = useRouter();
return (
<TabsContent value="active" className="m-0">
@ -60,8 +57,11 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
<p className="text-[14px] font-semibold leading-tight">
{getDocumentTypeLabel(docType)}
</p>
<p className="text-[11px] text-muted-foreground mt-1">
{count as number} documents indexed
<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>
</div>
@ -69,9 +69,7 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
{connectors.map((connector) => {
const isIndexing = indexingConnectorIds.has(connector.id);
const activeTask = logsSummary?.active_tasks?.find(
(task: LogActiveTask) =>
task.source?.includes(`connector_${connector.id}`) ||
task.source?.includes(`connector-${connector.id}`)
(task: LogActiveTask) => task.connector_id === connector.id
);
return (

View file

@ -3,8 +3,10 @@
import { useRouter } from "next/navigation";
import { type FC } from "react";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types";
import { OAUTH_CONNECTORS, OTHER_CONNECTORS } from "../constants/connector-constants";
import { ConnectorCard } from "../components/connector-card";
import { getDocumentCountForConnector } from "../utils/connector-document-mapping";
interface AllConnectorsTabProps {
searchQuery: string;
@ -12,6 +14,9 @@ interface AllConnectorsTabProps {
connectedTypes: Set<string>;
connectingId: string | null;
allConnectors: SearchSourceConnector[] | undefined;
documentTypeCounts?: Record<string, number>;
indexingConnectorIds?: Set<number>;
logsSummary?: LogSummary;
onConnectOAuth: (connector: (typeof OAUTH_CONNECTORS)[number]) => void;
onConnectNonOAuth?: (connectorType: string) => void;
onCreateWebcrawler?: () => void;
@ -24,6 +29,9 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
connectedTypes,
connectingId,
allConnectors,
documentTypeCounts,
indexingConnectorIds,
logsSummary,
onConnectOAuth,
onConnectNonOAuth,
onCreateWebcrawler,
@ -31,6 +39,14 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
}) => {
const router = useRouter();
// Helper to find active task for a connector
const getActiveTaskForConnector = (connectorId: number): LogActiveTask | undefined => {
if (!logsSummary?.active_tasks) return undefined;
return logsSummary.active_tasks.find(
(task: LogActiveTask) => task.connector_id === connectorId
);
};
// Filter connectors based on search
const filteredOAuth = OAUTH_CONNECTORS.filter(
(c) =>
@ -55,28 +71,35 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{filteredOAuth.map((connector) => {
const isConnected = connectedTypes.has(connector.connectorType);
const isConnecting = connectingId === connector.id;
// Find the actual connector object if connected
const actualConnector = isConnected && allConnectors
? allConnectors.find((c: SearchSourceConnector) => c.connector_type === connector.connectorType)
: undefined;
{filteredOAuth.map((connector) => {
const isConnected = connectedTypes.has(connector.connectorType);
const isConnecting = connectingId === connector.id;
// Find the actual connector object if connected
const actualConnector = isConnected && allConnectors
? allConnectors.find((c: SearchSourceConnector) => c.connector_type === connector.connectorType)
: undefined;
const documentCount = getDocumentCountForConnector(connector.connectorType, documentTypeCounts);
const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id);
const activeTask = actualConnector ? getActiveTaskForConnector(actualConnector.id) : undefined;
return (
<ConnectorCard
key={connector.id}
id={connector.id}
title={connector.title}
description={connector.description}
connectorType={connector.connectorType}
isConnected={isConnected}
isConnecting={isConnecting}
onConnect={() => onConnectOAuth(connector)}
onManage={actualConnector && onManage ? () => onManage(actualConnector) : undefined}
/>
);
})}
return (
<ConnectorCard
key={connector.id}
id={connector.id}
title={connector.title}
description={connector.description}
connectorType={connector.connectorType}
isConnected={isConnected}
isConnecting={isConnecting}
documentCount={documentCount}
isIndexing={isIndexing}
activeTask={activeTask}
onConnect={() => onConnectOAuth(connector)}
onManage={actualConnector && onManage ? () => onManage(actualConnector) : undefined}
/>
);
})}
</div>
</section>
)}
@ -109,34 +132,41 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
const isClickUp = connector.id === "clickup-connector";
const isLuma = connector.id === "luma-connector";
const isConnected = connectedTypes.has(connector.connectorType);
const isConnecting = connectingId === connector.id;
// Find the actual connector object if connected
const actualConnector = isConnected && allConnectors
? allConnectors.find((c: SearchSourceConnector) => c.connector_type === connector.connectorType)
: undefined;
const isConnected = connectedTypes.has(connector.connectorType);
const isConnecting = connectingId === connector.id;
// Find the actual connector object if connected
const actualConnector = isConnected && allConnectors
? allConnectors.find((c: SearchSourceConnector) => c.connector_type === connector.connectorType)
: undefined;
const handleConnect = isWebcrawler && onCreateWebcrawler
? onCreateWebcrawler
: (isTavily || isSearxng || isLinkup || isBaidu || isLinear || isElasticsearch || isSlack || isDiscord || isNotion || isConfluence || isBookStack || isGithub || isJira || isClickUp || isLuma) && onConnectNonOAuth
? () => onConnectNonOAuth(connector.connectorType)
: () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`);
const documentCount = getDocumentCountForConnector(connector.connectorType, documentTypeCounts);
const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id);
const activeTask = actualConnector ? getActiveTaskForConnector(actualConnector.id) : undefined;
return (
<ConnectorCard
key={connector.id}
id={connector.id}
title={connector.title}
description={connector.description}
connectorType={connector.connectorType}
isConnected={isConnected}
isConnecting={isConnecting}
onConnect={handleConnect}
onManage={actualConnector && onManage ? () => onManage(actualConnector) : undefined}
/>
);
})}
const handleConnect = isWebcrawler && onCreateWebcrawler
? onCreateWebcrawler
: (isTavily || isSearxng || isLinkup || isBaidu || isLinear || isElasticsearch || isSlack || isDiscord || isNotion || isConfluence || isBookStack || isGithub || isJira || isClickUp || isLuma) && onConnectNonOAuth
? () => onConnectNonOAuth(connector.connectorType)
: () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`);
return (
<ConnectorCard
key={connector.id}
id={connector.id}
title={connector.title}
description={connector.description}
connectorType={connector.connectorType}
isConnected={isConnected}
isConnecting={isConnecting}
documentCount={documentCount}
isIndexing={isIndexing}
activeTask={activeTask}
onConnect={handleConnect}
onManage={actualConnector && onManage ? () => onManage(actualConnector) : undefined}
/>
);
})}
</div>
</section>
)}

View file

@ -0,0 +1,64 @@
"use client";
/**
* Maps SearchSourceConnectorType to DocumentType for fetching document counts
*
* Note: Some connectors don't have a direct 1:1 mapping to document types:
* - Search API connectors (TAVILY_API, SEARXNG_API, etc.) don't index documents
* - WEBCRAWLER_CONNECTOR maps to CRAWLED_URL document type
* - GOOGLE_DRIVE_CONNECTOR maps to GOOGLE_DRIVE_FILE document type
*/
export const CONNECTOR_TO_DOCUMENT_TYPE: Record<string, string> = {
// Direct mappings (connector type matches document type)
SLACK_CONNECTOR: "SLACK_CONNECTOR",
NOTION_CONNECTOR: "NOTION_CONNECTOR",
GITHUB_CONNECTOR: "GITHUB_CONNECTOR",
LINEAR_CONNECTOR: "LINEAR_CONNECTOR",
DISCORD_CONNECTOR: "DISCORD_CONNECTOR",
JIRA_CONNECTOR: "JIRA_CONNECTOR",
CONFLUENCE_CONNECTOR: "CONFLUENCE_CONNECTOR",
CLICKUP_CONNECTOR: "CLICKUP_CONNECTOR",
GOOGLE_CALENDAR_CONNECTOR: "GOOGLE_CALENDAR_CONNECTOR",
GOOGLE_GMAIL_CONNECTOR: "GOOGLE_GMAIL_CONNECTOR",
AIRTABLE_CONNECTOR: "AIRTABLE_CONNECTOR",
LUMA_CONNECTOR: "LUMA_CONNECTOR",
ELASTICSEARCH_CONNECTOR: "ELASTICSEARCH_CONNECTOR",
BOOKSTACK_CONNECTOR: "BOOKSTACK_CONNECTOR",
// Special mappings (connector type differs from document type)
GOOGLE_DRIVE_CONNECTOR: "GOOGLE_DRIVE_FILE",
WEBCRAWLER_CONNECTOR: "CRAWLED_URL",
};
/**
* Get the document type for a given connector type
* Returns undefined if the connector doesn't index documents (e.g., search APIs)
*/
export function getDocumentTypeForConnector(
connectorType: string
): string | undefined {
return CONNECTOR_TO_DOCUMENT_TYPE[connectorType];
}
/**
* Get document count for a specific connector type from document type counts
*/
export function getDocumentCountForConnector(
connectorType: string,
documentTypeCounts: Record<string, number> | undefined
): number | undefined {
if (!documentTypeCounts) return undefined;
const documentType = getDocumentTypeForConnector(connectorType);
if (!documentType) return undefined;
return documentTypeCounts[documentType];
}
/**
* Check if a connector type is indexable (produces documents)
*/
export function isIndexableConnectorType(connectorType: string): boolean {
return connectorType in CONNECTOR_TO_DOCUMENT_TYPE;
}