mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
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:
parent
3ac806dcdf
commit
163df8fda7
10 changed files with 287 additions and 95 deletions
|
|
@ -322,6 +322,9 @@ async def get_logs_summary(
|
|||
document_id = (
|
||||
log.log_metadata.get("document_id") if log.log_metadata else None
|
||||
)
|
||||
connector_id = (
|
||||
log.log_metadata.get("connector_id") if log.log_metadata else None
|
||||
)
|
||||
summary["active_tasks"].append(
|
||||
{
|
||||
"id": log.id,
|
||||
|
|
@ -330,6 +333,7 @@ async def get_logs_summary(
|
|||
"started_at": log.created_at,
|
||||
"source": log.source,
|
||||
"document_id": document_id,
|
||||
"connector_id": connector_id,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -125,14 +125,22 @@ export default function DocumentsTable() {
|
|||
setColumnVisibility((prev) => ({ ...prev, [id]: checked }));
|
||||
};
|
||||
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const refreshCurrentView = useCallback(async () => {
|
||||
if (debouncedSearch.trim()) {
|
||||
await refetchSearch();
|
||||
} else {
|
||||
await refetchDocuments();
|
||||
if (isRefreshing) return;
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
if (debouncedSearch.trim()) {
|
||||
await refetchSearch();
|
||||
} else {
|
||||
await refetchDocuments();
|
||||
}
|
||||
toast.success(t("refresh_success") || "Documents refreshed");
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
toast.success(t("refresh_success") || "Documents refreshed");
|
||||
}, [debouncedSearch, refetchSearch, refetchDocuments, t]);
|
||||
}, [debouncedSearch, refetchSearch, refetchDocuments, t, isRefreshing]);
|
||||
|
||||
// Set up smart polling for active tasks - only polls when tasks are in progress
|
||||
const { summary } = useLogsSummary(searchSpaceId, 24, {
|
||||
|
|
@ -230,8 +238,8 @@ export default function DocumentsTable() {
|
|||
<h2 className="text-xl md:text-2xl font-bold tracking-tight">{t("title")}</h2>
|
||||
<p className="text-xs md:text-sm text-muted-foreground">{t("subtitle")}</p>
|
||||
</div>
|
||||
<Button onClick={refreshCurrentView} variant="outline" size="sm">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
<Button onClick={refreshCurrentView} variant="outline" size="sm" disabled={isRefreshing}>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
{t("refresh")}
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -472,9 +472,18 @@ export default function LogsManagePage() {
|
|||
}
|
||||
};
|
||||
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [isSummaryRefreshing, setIsSummaryRefreshing] = useState(false);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await Promise.all([refreshLogs(), refreshSummary()]);
|
||||
toast.success("Logs refreshed");
|
||||
if (isRefreshing) return;
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await Promise.all([refreshLogs(), refreshSummary()]);
|
||||
toast.success("Logs refreshed");
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -495,7 +504,16 @@ export default function LogsManagePage() {
|
|||
summary={summary}
|
||||
loading={summaryLoading}
|
||||
error={summaryError?.message ?? null}
|
||||
onRefresh={refreshSummary}
|
||||
onRefresh={async () => {
|
||||
if (isSummaryRefreshing) return;
|
||||
setIsSummaryRefreshing(true);
|
||||
try {
|
||||
await refreshSummary();
|
||||
} finally {
|
||||
setIsSummaryRefreshing(false);
|
||||
}
|
||||
}}
|
||||
isRefreshing={isSummaryRefreshing}
|
||||
/>
|
||||
|
||||
{/* Logs Table Header */}
|
||||
|
|
@ -509,8 +527,8 @@ export default function LogsManagePage() {
|
|||
<h2 className="text-xl md:text-2xl font-bold tracking-tight">{t("title")}</h2>
|
||||
<p className="text-xs md:text-sm text-muted-foreground">{t("subtitle")}</p>
|
||||
</div>
|
||||
<Button onClick={handleRefresh} variant="outline" size="sm">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
<Button onClick={handleRefresh} variant="outline" size="sm" disabled={isRefreshing}>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
{t("refresh")}
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
|
@ -546,11 +564,13 @@ function LogsSummaryDashboard({
|
|||
loading,
|
||||
error,
|
||||
onRefresh,
|
||||
isRefreshing = false,
|
||||
}: {
|
||||
summary: any;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
onRefresh: () => void;
|
||||
onRefresh: () => void | Promise<void>;
|
||||
isRefreshing?: boolean;
|
||||
}) {
|
||||
const t = useTranslations("logs");
|
||||
if (loading) {
|
||||
|
|
@ -581,7 +601,8 @@ function LogsSummaryDashboard({
|
|||
<div className="flex flex-col items-center gap-2">
|
||||
<AlertCircle className="h-8 w-8 text-destructive" />
|
||||
<p className="text-sm text-destructive">{t("failed_load_summary")}</p>
|
||||
<Button variant="outline" size="sm" onClick={onRefresh}>
|
||||
<Button variant="outline" size="sm" onClick={onRefresh} disabled={isRefreshing}>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
{t("retry")}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -86,6 +86,7 @@ export const logActiveTask = z.object({
|
|||
started_at: z.string(),
|
||||
source: z.string().nullable().optional(),
|
||||
document_id: z.number().nullable().optional(),
|
||||
connector_id: z.number().nullable().optional(),
|
||||
});
|
||||
export const logFailure = z.object({
|
||||
id: z.number(),
|
||||
|
|
|
|||
|
|
@ -65,10 +65,16 @@ module.exports = {
|
|||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: 0 },
|
||||
},
|
||||
"progress-indeterminate": {
|
||||
"0%": { left: "-33%", width: "33%" },
|
||||
"50%": { width: "50%" },
|
||||
"100%": { left: "100%", width: "33%" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"progress-indeterminate": "progress-indeterminate 1.5s ease-in-out infinite",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue