feat: improve Google Calendar and Gmail connectors with enhanced error handling

- Added user-friendly re-authentication messages for expired or revoked tokens in both Google Calendar and Gmail connectors.
- Updated error handling in indexing tasks to log specific authentication errors and provide clearer feedback to users.
- Enhanced the connector UI to handle indexing failures more effectively, improving overall user experience.
This commit is contained in:
Anish Sarkar 2026-01-23 18:57:10 +05:30
parent 29382070aa
commit 8d8f69545e
8 changed files with 113 additions and 12 deletions

View file

@ -5,12 +5,14 @@ import { Cable, Loader2 } from "lucide-react";
import { useSearchParams } from "next/navigation";
import type { FC } from "react";
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Tabs, TabsContent } from "@/components/ui/tabs";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { useConnectorsElectric } from "@/hooks/use-connectors-electric";
import { useDocumentsElectric } from "@/hooks/use-documents-electric";
import { useInbox } from "@/hooks/use-inbox";
import { cn } from "@/lib/utils";
import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header";
import { ConnectorConnectView } from "./connector-popup/connector-configs/views/connector-connect-view";
@ -27,10 +29,18 @@ import { YouTubeCrawlerView } from "./connector-popup/views/youtube-crawler-view
export const ConnectorIndicator: FC = () => {
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
const searchParams = useSearchParams();
const { data: currentUser } = useAtomValue(currentUserAtom);
// Fetch document type counts using Electric SQL + PGlite for real-time updates
const { documentTypeCounts, loading: documentTypesLoading } = useDocumentsElectric(searchSpaceId);
// Fetch notifications to detect indexing failures
const { inboxItems = [] } = useInbox(
currentUser?.id ?? null,
searchSpaceId ? Number(searchSpaceId) : null,
"connector_indexing"
);
// Check if YouTube view is active
const isYouTubeView = searchParams.get("view") === "youtube";
@ -116,8 +126,10 @@ export const ConnectorIndicator: FC = () => {
};
// Track indexing state locally - clears automatically when Electric SQL detects last_indexed_at changed
const { indexingConnectorIds, startIndexing } = useIndexingConnectors(
connectors as SearchSourceConnector[]
// Also clears when failed notifications are detected
const { indexingConnectorIds, startIndexing, stopIndexing } = useIndexingConnectors(
connectors as SearchSourceConnector[],
inboxItems
);
const isLoading = connectorsLoading || documentTypesLoading;
@ -246,7 +258,7 @@ export const ConnectorIndicator: FC = () => {
editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR"
? () => {
startIndexing(editingConnector.id);
handleQuickIndexConnector(editingConnector.id, editingConnector.connector_type);
handleQuickIndexConnector(editingConnector.id, editingConnector.connector_type, stopIndexing);
}
: undefined
}

View file

@ -97,7 +97,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
};
}, [checkScrollState]);
// Reset local quick indexing state when indexing completes
// Reset local quick indexing state when indexing completes or fails
useEffect(() => {
if (!isIndexing) {
setIsQuickIndexing(false);

View file

@ -1375,7 +1375,7 @@ export const useConnectorDialog = () => {
// Handle quick index (index without date picker, uses backend defaults)
const handleQuickIndexConnector = useCallback(
async (connectorId: number, connectorType?: string) => {
async (connectorId: number, connectorType?: string, stopIndexing?: (id: number) => void) => {
if (!searchSpaceId) return;
// Track quick index clicked event
@ -1401,6 +1401,10 @@ export const useConnectorDialog = () => {
} catch (error) {
console.error("Error indexing connector content:", error);
toast.error(error instanceof Error ? error.message : "Failed to start indexing");
// Stop indexing state on error
if (stopIndexing) {
stopIndexing(connectorId);
}
}
},
[searchSpaceId, indexConnector]

View file

@ -2,6 +2,8 @@
import { useCallback, useEffect, useRef, useState } from "react";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import type { InboxItem } from "@/contracts/types/inbox.types";
import { isConnectorIndexingMetadata } from "@/contracts/types/inbox.types";
/**
* Hook to track which connectors are currently indexing using local state.
@ -9,10 +11,14 @@ import type { SearchSourceConnector } from "@/contracts/types/connector.types";
* 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
* 3. Clearing indexing state when a failed notification is detected
*
* The actual `last_indexed_at` value comes from Electric SQL/PGlite, not local state.
*/
export function useIndexingConnectors(connectors: SearchSourceConnector[]) {
export function useIndexingConnectors(
connectors: SearchSourceConnector[],
inboxItems?: InboxItem[]
) {
// Set of connector IDs that are currently indexing
const [indexingConnectorIds, setIndexingConnectorIds] = useState<Set<number>>(new Set());
@ -48,6 +54,40 @@ export function useIndexingConnectors(connectors: SearchSourceConnector[]) {
}
}, [connectors, indexingConnectorIds]);
// Detect failed notifications and stop indexing state
useEffect(() => {
if (!inboxItems || inboxItems.length === 0) return;
const newIndexingIds = new Set(indexingConnectorIds);
let hasChanges = false;
for (const item of inboxItems) {
// Only check connector_indexing notifications
if (item.type !== "connector_indexing") continue;
// Check if this notification indicates a failure
const metadata = isConnectorIndexingMetadata(item.metadata)
? item.metadata
: null;
if (!metadata) continue;
// Check if status is "failed" or if there's an error_message
const isFailed =
metadata.status === "failed" ||
(metadata.error_message && metadata.error_message.trim().length > 0);
// If failed and connector is in indexing state, clear it
if (isFailed && indexingConnectorIds.has(metadata.connector_id)) {
newIndexingIds.delete(metadata.connector_id);
hasChanges = true;
}
}
if (hasChanges) {
setIndexingConnectorIds(newIndexingIds);
}
}, [inboxItems, indexingConnectorIds]);
// Add a connector to the indexing set (called when indexing starts)
const startIndexing = useCallback((connectorId: number) => {
setIndexingConnectorIds((prev) => {