mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-02 20:32:39 +02:00
resolve conflicts
This commit is contained in:
commit
11b160cee9
85 changed files with 8411 additions and 4637 deletions
|
|
@ -1,12 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import { IconBrandYoutube } from "@tabler/icons-react";
|
||||
import { differenceInDays, differenceInMinutes, format, isToday, isYesterday } from "date-fns";
|
||||
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";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useConnectorStatus } from "../hooks/use-connector-status";
|
||||
import { ConnectorStatusBadge } from "./connector-status-badge";
|
||||
|
|
@ -20,34 +18,11 @@ interface ConnectorCardProps {
|
|||
isConnecting?: boolean;
|
||||
documentCount?: number;
|
||||
accountCount?: number;
|
||||
lastIndexedAt?: string | null;
|
||||
isIndexing?: boolean;
|
||||
activeTask?: LogActiveTask;
|
||||
onConnect?: () => void;
|
||||
onManage?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a connector type is indexable (has documents)
|
||||
* MCP connectors are tools only and don't have indexable content
|
||||
*/
|
||||
function isIndexableConnector(connectorType?: string): boolean {
|
||||
if (!connectorType) return true; // Default to true for unknown types
|
||||
const nonIndexableTypes = ["MCP_CONNECTOR"];
|
||||
return !nonIndexableTypes.includes(connectorType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format document count (e.g., "1.2k docs", "500 docs", "1.5M docs")
|
||||
*/
|
||||
|
|
@ -62,45 +37,6 @@ function formatDocumentCount(count: number | undefined): string {
|
|||
return `${m.replace(/\.0$/, "")}M docs`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format last indexed date with contextual messages
|
||||
* Examples: "Just now", "10 minutes ago", "Today at 2:30 PM", "Yesterday at 3:45 PM", "3 days ago", "Jan 15, 2026"
|
||||
*/
|
||||
function formatLastIndexedDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const minutesAgo = differenceInMinutes(now, date);
|
||||
const daysAgo = differenceInDays(now, date);
|
||||
|
||||
// Just now (within last minute)
|
||||
if (minutesAgo < 1) {
|
||||
return "Just now";
|
||||
}
|
||||
|
||||
// X minutes ago (less than 1 hour)
|
||||
if (minutesAgo < 60) {
|
||||
return `${minutesAgo} ${minutesAgo === 1 ? "minute" : "minutes"} ago`;
|
||||
}
|
||||
|
||||
// Today at [time]
|
||||
if (isToday(date)) {
|
||||
return `Today at ${format(date, "h:mm a")}`;
|
||||
}
|
||||
|
||||
// Yesterday at [time]
|
||||
if (isYesterday(date)) {
|
||||
return `Yesterday at ${format(date, "h:mm a")}`;
|
||||
}
|
||||
|
||||
// X days ago (less than 7 days)
|
||||
if (daysAgo < 7) {
|
||||
return `${daysAgo} ${daysAgo === 1 ? "day" : "days"} ago`;
|
||||
}
|
||||
|
||||
// Full date for older entries
|
||||
return format(date, "MMM d, yyyy");
|
||||
}
|
||||
|
||||
export const ConnectorCard: FC<ConnectorCardProps> = ({
|
||||
id,
|
||||
title,
|
||||
|
|
@ -110,9 +46,7 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
|
|||
isConnecting = false,
|
||||
documentCount,
|
||||
accountCount,
|
||||
lastIndexedAt,
|
||||
isIndexing = false,
|
||||
activeTask,
|
||||
onConnect,
|
||||
onManage,
|
||||
}) => {
|
||||
|
|
@ -125,41 +59,11 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
|
|||
const statusMessage = getConnectorStatusMessage(connectorType);
|
||||
const showWarnings = shouldShowWarnings();
|
||||
|
||||
// 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) {
|
||||
// For non-indexable connectors (like MCP), show description instead of index status
|
||||
if (!isIndexableConnector(connectorType)) {
|
||||
return description;
|
||||
}
|
||||
|
||||
// Show last indexed date for connected indexable connectors
|
||||
if (lastIndexedAt) {
|
||||
return (
|
||||
<span className="whitespace-nowrap text-[10px]">
|
||||
Last indexed: {formatLastIndexedDate(lastIndexedAt)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
// Fallback for connected but never indexed
|
||||
return <span className="whitespace-nowrap text-[10px]">Never indexed</span>;
|
||||
// Don't show last indexed in overview tabs - only show in accounts list view
|
||||
return null;
|
||||
}
|
||||
|
||||
return description;
|
||||
|
|
@ -201,9 +105,13 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground mt-1">{getStatusContent()}</div>
|
||||
{isConnected && documentCount !== undefined && (
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5 flex items-center gap-1.5">
|
||||
{isIndexing ? (
|
||||
<p className="text-[11px] text-primary mt-1 flex items-center gap-1.5">
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
Syncing
|
||||
</p>
|
||||
) : isConnected ? (
|
||||
<p className="text-[10px] text-muted-foreground mt-1 flex items-center gap-1.5">
|
||||
<span>{formatDocumentCount(documentCount)}</span>
|
||||
{accountCount !== undefined && accountCount > 0 && (
|
||||
<>
|
||||
|
|
@ -214,6 +122,8 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
|
|||
</>
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<div className="text-[10px] text-muted-foreground mt-1">{getStatusContent()}</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export const ConnectorStatusBadge: FC<ConnectorStatusBadgeProps> = ({
|
|||
case "deprecated":
|
||||
return {
|
||||
icon: AlertTriangle,
|
||||
className: "ext-slate-500 dark:text-slate-400",
|
||||
className: "text-slate-500 dark:text-slate-400",
|
||||
defaultTitle: "Deprecated",
|
||||
};
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ export const ConnectorConnectView: FC<ConnectorConnectViewProps> = ({
|
|||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Connecting...
|
||||
Connecting
|
||||
</>
|
||||
) : (
|
||||
<>{connectorType === "MCP_CONNECTOR" ? "Connect" : `Connect ${getConnectorTypeDisplay(connectorType)}`}</>
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
|||
{isQuickIndexing || isIndexing ? (
|
||||
<>
|
||||
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||||
Indexing...
|
||||
Syncing
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -281,7 +281,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
|||
{isDisconnecting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Disconnecting...
|
||||
Disconnecting
|
||||
</>
|
||||
) : (
|
||||
"Confirm Disconnect"
|
||||
|
|
|
|||
|
|
@ -508,20 +508,23 @@ export const useConnectorDialog = () => {
|
|||
|
||||
// Handle submitting connect form
|
||||
const handleSubmitConnectForm = useCallback(
|
||||
async (formData: {
|
||||
name: string;
|
||||
connector_type: string;
|
||||
config: Record<string, unknown>;
|
||||
is_indexable: boolean;
|
||||
last_indexed_at: null;
|
||||
periodic_indexing_enabled: boolean;
|
||||
indexing_frequency_minutes: number | null;
|
||||
next_scheduled_at: null;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
periodicEnabled?: boolean;
|
||||
frequencyMinutes?: string;
|
||||
}) => {
|
||||
async (
|
||||
formData: {
|
||||
name: string;
|
||||
connector_type: string;
|
||||
config: Record<string, unknown>;
|
||||
is_indexable: boolean;
|
||||
last_indexed_at: null;
|
||||
periodic_indexing_enabled: boolean;
|
||||
indexing_frequency_minutes: number | null;
|
||||
next_scheduled_at: null;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
periodicEnabled?: boolean;
|
||||
frequencyMinutes?: string;
|
||||
},
|
||||
onIndexingStart?: (connectorId: number) => void
|
||||
) => {
|
||||
if (!searchSpaceId || !connectingConnectorType) return;
|
||||
|
||||
// Prevent multiple submissions using ref for immediate check
|
||||
|
|
@ -621,6 +624,11 @@ export const useConnectorDialog = () => {
|
|||
});
|
||||
}
|
||||
|
||||
// Notify caller that indexing is starting (for UI syncing state)
|
||||
if (onIndexingStart) {
|
||||
onIndexingStart(connector.id);
|
||||
}
|
||||
|
||||
// Start indexing (backend will use defaults if dates are undefined)
|
||||
const startDateStr = startDateForIndexing
|
||||
? format(startDateForIndexing, "yyyy-MM-dd")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import {
|
||||
type ConnectorStatusConfig,
|
||||
connectorStatusConfig,
|
||||
|
|
@ -14,34 +14,43 @@ export function useConnectorStatus() {
|
|||
/**
|
||||
* Get status configuration for a specific connector type
|
||||
*/
|
||||
const getConnectorStatus = (connectorType: string | undefined): ConnectorStatusConfig => {
|
||||
if (!connectorType) {
|
||||
return getDefaultConnectorStatus();
|
||||
}
|
||||
const getConnectorStatus = useCallback(
|
||||
(connectorType: string | undefined): ConnectorStatusConfig => {
|
||||
if (!connectorType) {
|
||||
return getDefaultConnectorStatus();
|
||||
}
|
||||
|
||||
return connectorStatusConfig.connectorStatuses[connectorType] || getDefaultConnectorStatus();
|
||||
};
|
||||
return connectorStatusConfig.connectorStatuses[connectorType] || getDefaultConnectorStatus();
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Check if a connector is enabled
|
||||
*/
|
||||
const isConnectorEnabled = (connectorType: string | undefined): boolean => {
|
||||
return getConnectorStatus(connectorType).enabled;
|
||||
};
|
||||
const isConnectorEnabled = useCallback(
|
||||
(connectorType: string | undefined): boolean => {
|
||||
return getConnectorStatus(connectorType).enabled;
|
||||
},
|
||||
[getConnectorStatus]
|
||||
);
|
||||
|
||||
/**
|
||||
* Get status message for a connector
|
||||
*/
|
||||
const getConnectorStatusMessage = (connectorType: string | undefined): string | null => {
|
||||
return getConnectorStatus(connectorType).statusMessage || null;
|
||||
};
|
||||
const getConnectorStatusMessage = useCallback(
|
||||
(connectorType: string | undefined): string | null => {
|
||||
return getConnectorStatus(connectorType).statusMessage || null;
|
||||
},
|
||||
[getConnectorStatus]
|
||||
);
|
||||
|
||||
/**
|
||||
* Check if warnings should be shown globally
|
||||
*/
|
||||
const shouldShowWarnings = (): boolean => {
|
||||
const shouldShowWarnings = useCallback((): boolean => {
|
||||
return connectorStatusConfig.globalSettings.showWarnings;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
|
|
@ -50,6 +59,6 @@ export function useConnectorStatus() {
|
|||
getConnectorStatusMessage,
|
||||
shouldShowWarnings,
|
||||
}),
|
||||
[]
|
||||
[getConnectorStatus, isConnectorEnabled, getConnectorStatusMessage, shouldShowWarnings]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||
|
||||
/**
|
||||
* Hook to track which connectors are currently indexing using local state.
|
||||
*
|
||||
* This provides a better UX than polling by:
|
||||
* 1. Setting indexing state immediately when user triggers indexing (optimistic)
|
||||
* 2. Clearing indexing state when Electric SQL detects last_indexed_at changed
|
||||
*
|
||||
* The actual `last_indexed_at` value comes from Electric SQL/PGlite, not local state.
|
||||
*/
|
||||
export function useIndexingConnectors(connectors: SearchSourceConnector[]) {
|
||||
// Set of connector IDs that are currently indexing
|
||||
const [indexingConnectorIds, setIndexingConnectorIds] = useState<Set<number>>(new Set());
|
||||
|
||||
// Track previous last_indexed_at values to detect changes
|
||||
const previousLastIndexedAtRef = useRef<Map<number, string | null>>(new Map());
|
||||
|
||||
// Detect when last_indexed_at changes (indexing completed) via Electric SQL
|
||||
useEffect(() => {
|
||||
const previousValues = previousLastIndexedAtRef.current;
|
||||
const newIndexingIds = new Set(indexingConnectorIds);
|
||||
let hasChanges = false;
|
||||
|
||||
for (const connector of connectors) {
|
||||
const previousValue = previousValues.get(connector.id);
|
||||
const currentValue = connector.last_indexed_at;
|
||||
|
||||
// If last_indexed_at changed and connector was in indexing state, clear it
|
||||
if (
|
||||
previousValue !== undefined && // We've seen this connector before
|
||||
previousValue !== currentValue && // Value changed
|
||||
indexingConnectorIds.has(connector.id) // It was marked as indexing
|
||||
) {
|
||||
newIndexingIds.delete(connector.id);
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
// Update previous value tracking
|
||||
previousValues.set(connector.id, currentValue);
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
setIndexingConnectorIds(newIndexingIds);
|
||||
}
|
||||
}, [connectors, indexingConnectorIds]);
|
||||
|
||||
// Add a connector to the indexing set (called when indexing starts)
|
||||
const startIndexing = useCallback((connectorId: number) => {
|
||||
setIndexingConnectorIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(connectorId);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Remove a connector from the indexing set (called manually if needed)
|
||||
const stopIndexing = useCallback((connectorId: number) => {
|
||||
setIndexingConnectorIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(connectorId);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Check if a connector is currently indexing
|
||||
const isIndexing = useCallback(
|
||||
(connectorId: number) => indexingConnectorIds.has(connectorId),
|
||||
[indexingConnectorIds]
|
||||
);
|
||||
|
||||
return {
|
||||
indexingConnectorIds,
|
||||
startIndexing,
|
||||
stopIndexing,
|
||||
isIndexing,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { differenceInDays, differenceInMinutes, format, isToday, isYesterday } from "date-fns";
|
||||
import { ArrowRight, Cable, Loader2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { FC } from "react";
|
||||
|
|
@ -24,7 +23,6 @@ interface ActiveConnectorsTabProps {
|
|||
activeDocumentTypes: Array<[string, number]>;
|
||||
connectors: SearchSourceConnector[];
|
||||
indexingConnectorIds: Set<number>;
|
||||
logsSummary: LogSummary | undefined;
|
||||
searchSpaceId: string;
|
||||
onTabChange: (value: string) => void;
|
||||
onManage?: (connector: SearchSourceConnector) => void;
|
||||
|
|
@ -45,7 +43,6 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
|
|||
activeDocumentTypes,
|
||||
connectors,
|
||||
indexingConnectorIds,
|
||||
logsSummary,
|
||||
searchSpaceId,
|
||||
onTabChange,
|
||||
onManage,
|
||||
|
|
@ -78,32 +75,6 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
|
|||
return `${m.replace(/\.0$/, "")}M docs`;
|
||||
};
|
||||
|
||||
// Format last indexed date with contextual messages
|
||||
const formatLastIndexedDate = (dateString: string): string => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const minutesAgo = differenceInMinutes(now, date);
|
||||
const daysAgo = differenceInDays(now, date);
|
||||
|
||||
if (minutesAgo < 1) return "Just now";
|
||||
if (minutesAgo < 60) return `${minutesAgo} ${minutesAgo === 1 ? "minute" : "minutes"} ago`;
|
||||
if (isToday(date)) return `Today at ${format(date, "h:mm a")}`;
|
||||
if (isYesterday(date)) return `Yesterday at ${format(date, "h:mm a")}`;
|
||||
if (daysAgo < 7) return `${daysAgo} ${daysAgo === 1 ? "day" : "days"} ago`;
|
||||
return format(date, "MMM d, yyyy");
|
||||
};
|
||||
|
||||
// Get most recent last indexed date from a list of connectors
|
||||
const getMostRecentLastIndexed = (
|
||||
connectorsList: SearchSourceConnector[]
|
||||
): string | undefined => {
|
||||
return connectorsList.reduce<string | undefined>((latest, c) => {
|
||||
if (!c.last_indexed_at) return latest;
|
||||
if (!latest) return c.last_indexed_at;
|
||||
return new Date(c.last_indexed_at) > new Date(latest) ? c.last_indexed_at : latest;
|
||||
}, undefined);
|
||||
};
|
||||
|
||||
// Document types that should be shown as standalone cards (not from connectors)
|
||||
const standaloneDocumentTypes = ["EXTENSION", "FILE", "NOTE", "YOUTUBE_VIDEO", "CRAWLED_URL"];
|
||||
|
||||
|
|
@ -202,24 +173,25 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
|
|||
const documentCount = getDocumentCountForConnector(
|
||||
connectorType,
|
||||
documentTypeCounts
|
||||
);
|
||||
const accountCount = typeConnectors.length;
|
||||
const mostRecentLastIndexed = getMostRecentLastIndexed(typeConnectors);
|
||||
const handleManageClick = () => {
|
||||
if (onViewAccountsList) {
|
||||
onViewAccountsList(connectorType, title);
|
||||
} else if (onManage && typeConnectors[0]) {
|
||||
onManage(typeConnectors[0]);
|
||||
}
|
||||
};
|
||||
);
|
||||
const accountCount = typeConnectors.length;
|
||||
|
||||
const handleManageClick = () => {
|
||||
if (onViewAccountsList) {
|
||||
onViewAccountsList(connectorType, title);
|
||||
} else if (onManage && typeConnectors[0]) {
|
||||
onManage(typeConnectors[0]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`oauth-type-${connectorType}`}
|
||||
className={cn(
|
||||
"relative flex items-center gap-4 p-4 rounded-xl border border-border transition-all",
|
||||
"relative flex items-center gap-4 p-4 rounded-xl transition-all",
|
||||
isAnyIndexing
|
||||
? "bg-primary/5 border-primary/20"
|
||||
: "bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10"
|
||||
? "bg-primary/5 border-0"
|
||||
: "bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10 border border-border"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
|
@ -237,30 +209,17 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
|
|||
{isAnyIndexing ? (
|
||||
<p className="text-[11px] text-primary mt-1 flex items-center gap-1.5">
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
Indexing...
|
||||
Syncing
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{isIndexableConnector(connectorType) && (
|
||||
<p className="text-[10px] text-muted-foreground mt-1 whitespace-nowrap">
|
||||
{mostRecentLastIndexed
|
||||
? `Last indexed: ${formatLastIndexedDate(mostRecentLastIndexed)}`
|
||||
: "Never indexed"}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
<p className="text-[10px] text-muted-foreground mt-1 flex items-center gap-1.5">
|
||||
<span>{formatDocumentCount(documentCount)}</span>
|
||||
<span className="text-muted-foreground/50">•</span>
|
||||
<span>
|
||||
{accountCount} {accountCount === 1 ? "Account" : "Accounts"}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5 flex items-center gap-1.5">
|
||||
{isIndexableConnector(connectorType) && (
|
||||
<>
|
||||
<span>{formatDocumentCount(documentCount)}</span>
|
||||
<span className="text-muted-foreground/50">•</span>
|
||||
</>
|
||||
)}
|
||||
<span>
|
||||
{accountCount} {accountCount === 1 ? "Account" : "Accounts"}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
|
@ -277,9 +236,6 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
|
|||
{/* Non-OAuth Connectors - Individual Cards */}
|
||||
{filteredNonOAuthConnectors.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
|
||||
|
|
@ -288,10 +244,10 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
|
|||
<div
|
||||
key={`connector-${connector.id}`}
|
||||
className={cn(
|
||||
"flex items-center gap-4 p-4 rounded-xl border border-border transition-all",
|
||||
"flex items-center gap-4 p-4 rounded-xl transition-all",
|
||||
isIndexing
|
||||
? "bg-primary/5 border-primary/20"
|
||||
: "bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10"
|
||||
? "bg-primary/5 border-0"
|
||||
: "bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10 border border-border"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
|
@ -313,26 +269,10 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
|
|||
{isIndexing ? (
|
||||
<p className="text-[11px] text-primary mt-1 flex items-center gap-1.5">
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
Indexing...
|
||||
{activeTask?.message && (
|
||||
<span className="text-muted-foreground truncate max-w-[150px]">
|
||||
• {activeTask.message}
|
||||
</span>
|
||||
)}
|
||||
Syncing
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-[10px] text-muted-foreground mt-1 whitespace-nowrap">
|
||||
{isIndexableConnector(connector.connector_type)
|
||||
? connector.last_indexed_at
|
||||
? `Last indexed: ${formatLastIndexedDate(connector.last_indexed_at)}`
|
||||
: "Never indexed"
|
||||
: connector.connector_type === "MCP_CONNECTOR"
|
||||
? ""
|
||||
: "Active"}
|
||||
</p>
|
||||
)}
|
||||
{isIndexableConnector(connector.connector_type) && (
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">
|
||||
<p className="text-[10px] text-muted-foreground mt-1">
|
||||
{formatDocumentCount(documentCount)}
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -389,19 +329,12 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
|
|||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted mb-4">
|
||||
<Cable className="size-8 text-muted-foreground/50" />
|
||||
<Cable className="size-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h4 className="text-lg font-semibold">No active sources</h4>
|
||||
<p className="text-sm text-muted-foreground mt-1 max-w-[280px]">
|
||||
Connect your first service to start searching across all your data.
|
||||
</p>
|
||||
<Button
|
||||
variant="link"
|
||||
className="mt-6 text-primary hover:underline"
|
||||
onClick={() => onTabChange("all")}
|
||||
>
|
||||
Browse available connectors
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { Plus } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||
import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types";
|
||||
import { ConnectorCard } from "../components/connector-card";
|
||||
import { CRAWLERS, OAUTH_CONNECTORS, OTHER_CONNECTORS } from "../constants/connector-constants";
|
||||
import { getDocumentCountForConnector } from "../utils/connector-document-mapping";
|
||||
|
|
@ -30,7 +27,6 @@ interface AllConnectorsTabProps {
|
|||
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;
|
||||
|
|
@ -41,13 +37,11 @@ interface AllConnectorsTabProps {
|
|||
|
||||
export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
||||
searchQuery,
|
||||
searchSpaceId,
|
||||
connectedTypes,
|
||||
connectingId,
|
||||
allConnectors,
|
||||
documentTypeCounts,
|
||||
indexingConnectorIds,
|
||||
logsSummary,
|
||||
onConnectOAuth,
|
||||
onConnectNonOAuth,
|
||||
onCreateWebcrawler,
|
||||
|
|
@ -55,14 +49,6 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
|||
onManage,
|
||||
onViewAccountsList,
|
||||
}) => {
|
||||
// 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) =>
|
||||
|
|
@ -125,11 +111,6 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
|||
// Check if any account is currently indexing
|
||||
const isIndexing = typeConnectors.some((c) => indexingConnectorIds?.has(c.id));
|
||||
|
||||
// Get active task from any indexing account
|
||||
const activeTask = typeConnectors
|
||||
.map((c) => getActiveTaskForConnector(c.id))
|
||||
.find((task) => task !== undefined);
|
||||
|
||||
return (
|
||||
<ConnectorCard
|
||||
key={connector.id}
|
||||
|
|
@ -143,7 +124,6 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
|||
accountCount={accountCount}
|
||||
lastIndexedAt={mostRecentLastIndexed}
|
||||
isIndexing={isIndexing}
|
||||
activeTask={activeTask}
|
||||
onConnect={() => onConnectOAuth(connector)}
|
||||
onManage={
|
||||
isConnected && onViewAccountsList
|
||||
|
|
@ -181,9 +161,6 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
|||
documentTypeCounts
|
||||
);
|
||||
const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id);
|
||||
const activeTask = actualConnector
|
||||
? getActiveTaskForConnector(actualConnector.id)
|
||||
: undefined;
|
||||
|
||||
const handleConnect = onConnectNonOAuth
|
||||
? () => onConnectNonOAuth(connector.connectorType)
|
||||
|
|
@ -199,9 +176,7 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
|||
isConnected={isConnected}
|
||||
isConnecting={isConnecting}
|
||||
documentCount={documentCount}
|
||||
lastIndexedAt={actualConnector?.last_indexed_at}
|
||||
isIndexing={isIndexing}
|
||||
activeTask={activeTask}
|
||||
onConnect={handleConnect}
|
||||
onManage={
|
||||
actualConnector && onManage ? () => onManage(actualConnector) : undefined
|
||||
|
|
@ -242,9 +217,6 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
|||
? getDocumentCountForConnector(crawler.connectorType, documentTypeCounts)
|
||||
: undefined;
|
||||
const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id);
|
||||
const activeTask = actualConnector
|
||||
? getActiveTaskForConnector(actualConnector.id)
|
||||
: undefined;
|
||||
|
||||
const handleConnect =
|
||||
isYouTube && onCreateYouTubeCrawler
|
||||
|
|
@ -269,9 +241,7 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
|||
isConnected={isConnected}
|
||||
isConnecting={isConnecting}
|
||||
documentCount={documentCount}
|
||||
lastIndexedAt={actualConnector?.last_indexed_at}
|
||||
isIndexing={isIndexing}
|
||||
activeTask={activeTask}
|
||||
onConnect={handleConnect}
|
||||
onManage={
|
||||
actualConnector && onManage ? () => onManage(actualConnector) : undefined
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
import type { MCPServerConfig, MCPToolDefinition } from "@/contracts/types/mcp.types";
|
||||
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
|
||||
|
||||
/**
|
||||
* Shared MCP configuration validation result
|
||||
*/
|
||||
export interface MCPConfigValidationResult {
|
||||
config: MCPServerConfig | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared MCP connection test result
|
||||
*/
|
||||
export interface MCPConnectionTestResult {
|
||||
status: "success" | "error";
|
||||
message: string;
|
||||
tools: MCPToolDefinition[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate MCP server configuration from JSON string
|
||||
* @param configJson - JSON string containing MCP server configuration
|
||||
* @returns Validation result with parsed config or error message
|
||||
*/
|
||||
export const parseMCPConfig = (configJson: string): MCPConfigValidationResult => {
|
||||
try {
|
||||
const parsed = JSON.parse(configJson);
|
||||
|
||||
// Validate that it's an object, not an array
|
||||
if (Array.isArray(parsed)) {
|
||||
return {
|
||||
config: null,
|
||||
error: "Please provide a single server configuration object, not an array",
|
||||
};
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!parsed.command || typeof parsed.command !== "string") {
|
||||
return {
|
||||
config: null,
|
||||
error: "'command' field is required and must be a string",
|
||||
};
|
||||
}
|
||||
|
||||
const config: MCPServerConfig = {
|
||||
command: parsed.command,
|
||||
args: parsed.args || [],
|
||||
env: parsed.env || {},
|
||||
transport: parsed.transport || "stdio",
|
||||
};
|
||||
|
||||
return {
|
||||
config,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
config: null,
|
||||
error: error instanceof Error ? error.message : "Invalid JSON",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Test connection to MCP server
|
||||
* @param serverConfig - MCP server configuration to test
|
||||
* @returns Connection test result with status, message, and available tools
|
||||
*/
|
||||
export const testMCPConnection = async (
|
||||
serverConfig: MCPServerConfig
|
||||
): Promise<MCPConnectionTestResult> => {
|
||||
try {
|
||||
const result = await connectorsApiService.testMCPConnection(serverConfig);
|
||||
|
||||
if (result.status === "success") {
|
||||
return {
|
||||
status: "success",
|
||||
message: `Successfully connected. Found ${result.tools.length} tool${result.tools.length !== 1 ? "s" : ""}.`,
|
||||
tools: result.tools,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "error",
|
||||
message: result.message || "Failed to connect",
|
||||
tools: [],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "error",
|
||||
message: error instanceof Error ? error.message : "Failed to connect",
|
||||
tools: [],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract server name from MCP config JSON
|
||||
* @param configJson - JSON string containing MCP server configuration
|
||||
* @returns Server name if found, otherwise default name
|
||||
*/
|
||||
export const extractServerName = (configJson: string): string => {
|
||||
try {
|
||||
const parsed = JSON.parse(configJson);
|
||||
if (parsed.name && typeof parsed.name === "string") {
|
||||
return parsed.name;
|
||||
}
|
||||
} catch {
|
||||
// Return default if parsing fails
|
||||
}
|
||||
return "MCP Server";
|
||||
};
|
||||
|
|
@ -6,7 +6,6 @@ import type { FC } from "react";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||
import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useConnectorStatus } from "../hooks/use-connector-status";
|
||||
import { getConnectorDisplayName } from "../tabs/all-connectors-tab";
|
||||
|
|
@ -16,7 +15,6 @@ interface ConnectorAccountsListViewProps {
|
|||
connectorTitle: string;
|
||||
connectors: SearchSourceConnector[];
|
||||
indexingConnectorIds: Set<number>;
|
||||
logsSummary: LogSummary | undefined;
|
||||
onBack: () => void;
|
||||
onManage: (connector: SearchSourceConnector) => void;
|
||||
onAddAccount: () => void;
|
||||
|
|
@ -68,7 +66,6 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
connectorTitle,
|
||||
connectors,
|
||||
indexingConnectorIds,
|
||||
logsSummary,
|
||||
onBack,
|
||||
onManage,
|
||||
onAddAccount,
|
||||
|
|
@ -133,7 +130,7 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
)}
|
||||
</div>
|
||||
<span className="text-[11px] sm:text-[12px] font-medium">
|
||||
{isConnecting ? "Connecting..." : "Add Account"}
|
||||
{isConnecting ? "Connecting" : "Add Account"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -145,18 +142,15 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{typeConnectors.map((connector) => {
|
||||
const isIndexing = indexingConnectorIds.has(connector.id);
|
||||
const activeTask = logsSummary?.active_tasks?.find(
|
||||
(task: LogActiveTask) => task.connector_id === connector.id
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={connector.id}
|
||||
className={cn(
|
||||
"flex items-center gap-4 p-4 rounded-xl border border-border transition-all",
|
||||
"flex items-center gap-4 p-4 rounded-xl transition-all",
|
||||
isIndexing
|
||||
? "bg-primary/5 border-primary/20"
|
||||
: "bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10"
|
||||
? "bg-primary/5 border-0"
|
||||
: "bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10 border border-border"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
|
@ -176,12 +170,7 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
{isIndexing ? (
|
||||
<p className="text-[11px] text-primary mt-1 flex items-center gap-1.5">
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
Indexing...
|
||||
{activeTask?.message && (
|
||||
<span className="text-muted-foreground truncate max-w-[100px]">
|
||||
• {activeTask.message}
|
||||
</span>
|
||||
)}
|
||||
Syncing
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-[10px] text-muted-foreground mt-1 whitespace-nowrap truncate">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue