From 828b2733e5a2dc1a15523b0bd65007ddd1667ec1 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 24 Jul 2026 00:48:56 +0530 Subject: [PATCH] feat(connector-rows): implement useConnectorRows hook for managing connector health and indexing state --- .../hooks/use-connector-rows.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-rows.ts diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-rows.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-rows.ts new file mode 100644 index 000000000..e5b1052cf --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-rows.ts @@ -0,0 +1,78 @@ +"use client"; + +import { useAtomValue } from "jotai"; +import { useMemo } from "react"; +import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom"; +import type { SearchSourceConnector } from "@/contracts/types/connector.types"; +import { connectorIndexingMetadata } from "@/contracts/types/inbox.types"; +import { type ConnectorTypeRow, groupConnectorsByType } from "../constants/connector-constants"; +import { useIndexingConnectors } from "./use-indexing-connectors"; + +/** Health of a connected connector type, derived from indexing + inbox state. */ +export type ConnectorHealth = "syncing" | "failed" | "ok"; + +/** A grouped connector row enriched with live indexing health. */ +export interface ConnectorRow extends ConnectorTypeRow { + health: ConnectorHealth; + errorMessage?: string; +} + +/** + * Single source of truth for the "Your connectors" list: groups the connectors + * by type and derives each row's health (syncing > failed > ok) from the status + * inbox and optimistic indexing state. Shared by the connectors panel rail and + * the composer add-menu so both render identical status glyphs. + * + * Also returns the underlying indexing controls so the panel can reuse them for + * its flow views instead of instantiating `useIndexingConnectors` twice. + */ +export function useConnectorRows(connectors: SearchSourceConnector[]) { + const statusInboxItems = useAtomValue(statusInboxItemsAtom); + const inboxItems = useMemo( + () => statusInboxItems.filter((item) => item.type === "connector_indexing"), + [statusInboxItems] + ); + + const { indexingConnectorIds, startIndexing, stopIndexing } = useIndexingConnectors( + connectors, + inboxItems + ); + + // Latest indexing status per connector id, parsed from the status inbox. + const statusByConnectorId = useMemo(() => { + const map = new Map(); + for (const item of inboxItems) { + const parsed = connectorIndexingMetadata.safeParse(item.metadata); + if (!parsed.success) continue; + const at = item.updated_at ?? item.created_at; + const prev = map.get(parsed.data.connector_id); + if (!prev || at > prev.at) { + map.set(parsed.data.connector_id, { + status: parsed.data.status, + error: parsed.data.error_message ?? undefined, + at, + }); + } + } + return map; + }, [inboxItems]); + + const rows = useMemo(() => { + return groupConnectorsByType(connectors).map((row) => { + const ids = row.connectors.map((c) => c.id); + const syncing = ids.some( + (id) => indexingConnectorIds.has(id) || statusByConnectorId.get(id)?.status === "in_progress" + ); + const failedEntry = syncing + ? undefined + : ids.map((id) => statusByConnectorId.get(id)).find((s) => s?.status === "failed"); + return { + ...row, + health: syncing ? "syncing" : failedEntry ? "failed" : "ok", + errorMessage: failedEntry?.error, + } satisfies ConnectorRow; + }); + }, [connectors, indexingConnectorIds, statusByConnectorId]); + + return { rows, indexingConnectorIds, startIndexing, stopIndexing, inboxItems }; +}