From 6ca206f2fdf97b85cb69484e1158c87422cddc26 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 27 Mar 2026 12:41:23 -0700 Subject: [PATCH 1/4] fix(knowledge): scope sync/update state per-connector to prevent race conditions --- .../connectors-section/connectors-section.tsx | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx index 86e761b8a2..ca8fa62455 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx @@ -73,13 +73,14 @@ export function ConnectorsSection({ isLoading, canEdit, }: ConnectorsSectionProps) { - const { mutate: triggerSync, isPending: isSyncing } = useTriggerSync() - const { mutate: updateConnector, isPending: isUpdating } = useUpdateConnector() + const { mutate: triggerSync } = useTriggerSync() + const { mutate: updateConnector } = useUpdateConnector() const { mutate: deleteConnector, isPending: isDeleting } = useDeleteConnector() const [deleteTarget, setDeleteTarget] = useState(null) const [editingConnector, setEditingConnector] = useState(null) const [error, setError] = useState(null) - const [syncingConnectorId, setSyncingConnectorId] = useState(null) + const [syncingIds, setSyncingIds] = useState>(() => new Set()) + const [updatingIds, setUpdatingIds] = useState>(() => new Set()) const syncTriggeredAt = useRef>({}) const cooldownTimers = useRef>>(new Set()) @@ -104,14 +105,18 @@ export function ConnectorsSection({ if (isSyncOnCooldown(connectorId)) return syncTriggeredAt.current[connectorId] = Date.now() - setSyncingConnectorId(connectorId) + setSyncingIds((prev) => new Set(prev).add(connectorId)) triggerSync( { knowledgeBaseId, connectorId }, { onSuccess: () => { setError(null) - setSyncingConnectorId(null) + setSyncingIds((prev) => { + const next = new Set(prev) + next.delete(connectorId) + return next + }) const timer = setTimeout(() => { cooldownTimers.current.delete(timer) forceUpdate((n) => n + 1) @@ -121,7 +126,11 @@ export function ConnectorsSection({ onError: (err) => { logger.error('Sync trigger failed', { error: err.message }) setError(err.message) - setSyncingConnectorId(null) + setSyncingIds((prev) => { + const next = new Set(prev) + next.delete(connectorId) + return next + }) delete syncTriggeredAt.current[connectorId] forceUpdate((n) => n + 1) }, @@ -167,12 +176,12 @@ export function ConnectorsSection({ workspaceId={workspaceId} knowledgeBaseId={knowledgeBaseId} canEdit={canEdit} - isSyncing={isSyncing} - isSyncPending={syncingConnectorId === connector.id} - isUpdating={isUpdating} + isSyncPending={syncingIds.has(connector.id)} + isUpdating={updatingIds.has(connector.id)} syncCooldown={isSyncOnCooldown(connector.id)} onSync={() => handleSync(connector.id)} - onTogglePause={() => + onTogglePause={() => { + setUpdatingIds((prev) => new Set(prev).add(connector.id)) updateConnector( { knowledgeBaseId, @@ -182,6 +191,13 @@ export function ConnectorsSection({ }, }, { + onSettled: () => { + setUpdatingIds((prev) => { + const next = new Set(prev) + next.delete(connector.id) + return next + }) + }, onSuccess: () => setError(null), onError: (err) => { logger.error('Toggle pause failed', { error: err.message }) @@ -189,7 +205,7 @@ export function ConnectorsSection({ }, } ) - } + }} onEdit={() => setEditingConnector(connector)} onDelete={() => setDeleteTarget(connector.id)} /> @@ -260,7 +276,6 @@ interface ConnectorCardProps { workspaceId: string knowledgeBaseId: string canEdit: boolean - isSyncing: boolean isSyncPending: boolean isUpdating: boolean syncCooldown: boolean @@ -275,7 +290,6 @@ function ConnectorCard({ workspaceId, knowledgeBaseId, canEdit, - isSyncing, isSyncPending, isUpdating, syncCooldown, @@ -368,7 +382,7 @@ function ConnectorCard({ variant='ghost' className='h-7 w-7 p-0' onClick={onSync} - disabled={connector.status === 'syncing' || isSyncing || syncCooldown} + disabled={connector.status === 'syncing' || isSyncPending || syncCooldown} > Date: Fri, 27 Mar 2026 12:44:01 -0700 Subject: [PATCH 2/4] feat(knowledge): add connectors column to knowledge base list --- .../[workspaceId]/knowledge/knowledge.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index c5b35bc713..654fa2dd6e 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx @@ -3,10 +3,12 @@ import { useCallback, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { useParams, useRouter } from 'next/navigation' +import { Tooltip } from '@/components/emcn' import { Database } from '@/components/emcn/icons' import type { KnowledgeBaseData } from '@/lib/knowledge/types' import type { CreateAction, + ResourceCell, ResourceColumn, ResourceRow, SearchConfig, @@ -23,6 +25,7 @@ import { import { filterKnowledgeBases } from '@/app/workspace/[workspaceId]/knowledge/utils/sort' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' +import { CONNECTOR_REGISTRY } from '@/connectors/registry' import { useKnowledgeBasesList } from '@/hooks/kb/use-knowledge' import { useDeleteKnowledgeBase, useUpdateKnowledgeBase } from '@/hooks/queries/kb/knowledge' import { useWorkspaceMembersQuery } from '@/hooks/queries/workspace' @@ -37,6 +40,7 @@ const COLUMNS: ResourceColumn[] = [ { id: 'name', header: 'Name' }, { id: 'documents', header: 'Documents' }, { id: 'tokens', header: 'Tokens' }, + { id: 'connectors', header: 'Connectors' }, { id: 'created', header: 'Created' }, { id: 'owner', header: 'Owner' }, { id: 'updated', header: 'Last Updated' }, @@ -44,6 +48,34 @@ const COLUMNS: ResourceColumn[] = [ const DATABASE_ICON = +function connectorCell(connectorTypes?: string[]): ResourceCell { + if (!connectorTypes || connectorTypes.length === 0) { + return { label: '—' } + } + + return { + content: ( +
+ {connectorTypes.map((type) => { + const def = CONNECTOR_REGISTRY[type] + const Icon = def?.icon + if (!Icon) return null + return ( + + + + + + + {def.name} + + ) + })} +
+ ), + } +} + export function Knowledge() { const params = useParams() const router = useRouter() @@ -168,6 +200,7 @@ export function Knowledge() { tokens: { label: kb.tokenCount ? kb.tokenCount.toLocaleString() : '0', }, + connectors: connectorCell(kb.connectorTypes), created: timeCell(kb.createdAt), owner: ownerCell(kb.userId, members), updated: timeCell(kb.updatedAt), @@ -175,6 +208,7 @@ export function Knowledge() { sortValues: { documents: kbWithCount.docCount || 0, tokens: kb.tokenCount || 0, + connectors: kb.connectorTypes?.length || 0, created: -new Date(kb.createdAt).getTime(), updated: -new Date(kb.updatedAt).getTime(), }, From 9f3a125660fd006fcc45cbccefb28a75f0a8c10f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 27 Mar 2026 12:47:20 -0700 Subject: [PATCH 3/4] refactor(knowledge): extract set helpers, handleTogglePause, and filter-before-map --- .../connectors-section/connectors-section.tsx | 79 ++++++++++--------- .../[workspaceId]/knowledge/knowledge.tsx | 14 +++- 2 files changed, 51 insertions(+), 42 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx index ca8fa62455..988a870953 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx @@ -82,6 +82,18 @@ export function ConnectorsSection({ const [syncingIds, setSyncingIds] = useState>(() => new Set()) const [updatingIds, setUpdatingIds] = useState>(() => new Set()) + const addToSet = useCallback((setter: typeof setSyncingIds, id: string) => { + setter((prev) => new Set(prev).add(id)) + }, []) + + const removeFromSet = useCallback((setter: typeof setSyncingIds, id: string) => { + setter((prev) => { + const next = new Set(prev) + next.delete(id) + return next + }) + }, []) + const syncTriggeredAt = useRef>({}) const cooldownTimers = useRef>>(new Set()) const [, forceUpdate] = useState(0) @@ -105,18 +117,14 @@ export function ConnectorsSection({ if (isSyncOnCooldown(connectorId)) return syncTriggeredAt.current[connectorId] = Date.now() - setSyncingIds((prev) => new Set(prev).add(connectorId)) + addToSet(setSyncingIds, connectorId) triggerSync( { knowledgeBaseId, connectorId }, { onSuccess: () => { setError(null) - setSyncingIds((prev) => { - const next = new Set(prev) - next.delete(connectorId) - return next - }) + removeFromSet(setSyncingIds, connectorId) const timer = setTimeout(() => { cooldownTimers.current.delete(timer) forceUpdate((n) => n + 1) @@ -126,18 +134,38 @@ export function ConnectorsSection({ onError: (err) => { logger.error('Sync trigger failed', { error: err.message }) setError(err.message) - setSyncingIds((prev) => { - const next = new Set(prev) - next.delete(connectorId) - return next - }) + removeFromSet(setSyncingIds, connectorId) delete syncTriggeredAt.current[connectorId] forceUpdate((n) => n + 1) }, } ) }, - [knowledgeBaseId, triggerSync, isSyncOnCooldown] + [knowledgeBaseId, triggerSync, isSyncOnCooldown, addToSet, removeFromSet] + ) + + const handleTogglePause = useCallback( + (connector: ConnectorData) => { + addToSet(setUpdatingIds, connector.id) + updateConnector( + { + knowledgeBaseId, + connectorId: connector.id, + updates: { + status: connector.status === 'paused' ? 'active' : 'paused', + }, + }, + { + onSettled: () => removeFromSet(setUpdatingIds, connector.id), + onSuccess: () => setError(null), + onError: (err) => { + logger.error('Toggle pause failed', { error: err.message }) + setError(err.message) + }, + } + ) + }, + [knowledgeBaseId, updateConnector, addToSet, removeFromSet] ) if (connectors.length === 0 && !canEdit && !isLoading) return null @@ -180,32 +208,7 @@ export function ConnectorsSection({ isUpdating={updatingIds.has(connector.id)} syncCooldown={isSyncOnCooldown(connector.id)} onSync={() => handleSync(connector.id)} - onTogglePause={() => { - setUpdatingIds((prev) => new Set(prev).add(connector.id)) - updateConnector( - { - knowledgeBaseId, - connectorId: connector.id, - updates: { - status: connector.status === 'paused' ? 'active' : 'paused', - }, - }, - { - onSettled: () => { - setUpdatingIds((prev) => { - const next = new Set(prev) - next.delete(connector.id) - return next - }) - }, - onSuccess: () => setError(null), - onError: (err) => { - logger.error('Toggle pause failed', { error: err.message }) - setError(err.message) - }, - } - ) - }} + onTogglePause={() => handleTogglePause(connector)} onEdit={() => setEditingConnector(connector)} onDelete={() => setDeleteTarget(connector.id)} /> diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index 654fa2dd6e..46ee5efdbe 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx @@ -53,13 +53,19 @@ function connectorCell(connectorTypes?: string[]): ResourceCell { return { label: '—' } } + const entries = connectorTypes + .map((type) => ({ type, def: CONNECTOR_REGISTRY[type] })) + .filter((e): e is { type: string; def: NonNullable<(typeof CONNECTOR_REGISTRY)[string]> } => + Boolean(e.def?.icon) + ) + + if (entries.length === 0) return { label: '—' } + return { content: (
- {connectorTypes.map((type) => { - const def = CONNECTOR_REGISTRY[type] - const Icon = def?.icon - if (!Icon) return null + {entries.map(({ type, def }) => { + const Icon = def.icon return ( From 959b7a8b6b97d58eef805a095fcd3f58b599767c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 27 Mar 2026 12:47:59 -0700 Subject: [PATCH 4/4] refactor(knowledge): use onSettled for syncingIds cleanup, consistent with updatingIds --- .../[id]/components/connectors-section/connectors-section.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx index 988a870953..b8db5cffcc 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx @@ -124,7 +124,6 @@ export function ConnectorsSection({ { onSuccess: () => { setError(null) - removeFromSet(setSyncingIds, connectorId) const timer = setTimeout(() => { cooldownTimers.current.delete(timer) forceUpdate((n) => n + 1) @@ -134,10 +133,10 @@ export function ConnectorsSection({ onError: (err) => { logger.error('Sync trigger failed', { error: err.message }) setError(err.message) - removeFromSet(setSyncingIds, connectorId) delete syncTriggeredAt.current[connectorId] forceUpdate((n) => n + 1) }, + onSettled: () => removeFromSet(setSyncingIds, connectorId), } ) },