Skip to content

Commit b90bb75

Browse files
authored
fix(knowledge): connector spinner race condition + connectors column (#3812)
* fix(knowledge): scope sync/update state per-connector to prevent race conditions * feat(knowledge): add connectors column to knowledge base list * refactor(knowledge): extract set helpers, handleTogglePause, and filter-before-map * refactor(knowledge): use onSettled for syncingIds cleanup, consistent with updatingIds
1 parent fb233d0 commit b90bb75

File tree

2 files changed

+87
-31
lines changed

2 files changed

+87
-31
lines changed

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,26 @@ export function ConnectorsSection({
7373
isLoading,
7474
canEdit,
7575
}: ConnectorsSectionProps) {
76-
const { mutate: triggerSync, isPending: isSyncing } = useTriggerSync()
77-
const { mutate: updateConnector, isPending: isUpdating } = useUpdateConnector()
76+
const { mutate: triggerSync } = useTriggerSync()
77+
const { mutate: updateConnector } = useUpdateConnector()
7878
const { mutate: deleteConnector, isPending: isDeleting } = useDeleteConnector()
7979
const [deleteTarget, setDeleteTarget] = useState<string | null>(null)
8080
const [editingConnector, setEditingConnector] = useState<ConnectorData | null>(null)
8181
const [error, setError] = useState<string | null>(null)
82-
const [syncingConnectorId, setSyncingConnectorId] = useState<string | null>(null)
82+
const [syncingIds, setSyncingIds] = useState<Set<string>>(() => new Set())
83+
const [updatingIds, setUpdatingIds] = useState<Set<string>>(() => new Set())
84+
85+
const addToSet = useCallback((setter: typeof setSyncingIds, id: string) => {
86+
setter((prev) => new Set(prev).add(id))
87+
}, [])
88+
89+
const removeFromSet = useCallback((setter: typeof setSyncingIds, id: string) => {
90+
setter((prev) => {
91+
const next = new Set(prev)
92+
next.delete(id)
93+
return next
94+
})
95+
}, [])
8396

8497
const syncTriggeredAt = useRef<Record<string, number>>({})
8598
const cooldownTimers = useRef<Set<ReturnType<typeof setTimeout>>>(new Set())
@@ -104,14 +117,13 @@ export function ConnectorsSection({
104117
if (isSyncOnCooldown(connectorId)) return
105118

106119
syncTriggeredAt.current[connectorId] = Date.now()
107-
setSyncingConnectorId(connectorId)
120+
addToSet(setSyncingIds, connectorId)
108121

109122
triggerSync(
110123
{ knowledgeBaseId, connectorId },
111124
{
112125
onSuccess: () => {
113126
setError(null)
114-
setSyncingConnectorId(null)
115127
const timer = setTimeout(() => {
116128
cooldownTimers.current.delete(timer)
117129
forceUpdate((n) => n + 1)
@@ -121,14 +133,38 @@ export function ConnectorsSection({
121133
onError: (err) => {
122134
logger.error('Sync trigger failed', { error: err.message })
123135
setError(err.message)
124-
setSyncingConnectorId(null)
125136
delete syncTriggeredAt.current[connectorId]
126137
forceUpdate((n) => n + 1)
127138
},
139+
onSettled: () => removeFromSet(setSyncingIds, connectorId),
140+
}
141+
)
142+
},
143+
[knowledgeBaseId, triggerSync, isSyncOnCooldown, addToSet, removeFromSet]
144+
)
145+
146+
const handleTogglePause = useCallback(
147+
(connector: ConnectorData) => {
148+
addToSet(setUpdatingIds, connector.id)
149+
updateConnector(
150+
{
151+
knowledgeBaseId,
152+
connectorId: connector.id,
153+
updates: {
154+
status: connector.status === 'paused' ? 'active' : 'paused',
155+
},
156+
},
157+
{
158+
onSettled: () => removeFromSet(setUpdatingIds, connector.id),
159+
onSuccess: () => setError(null),
160+
onError: (err) => {
161+
logger.error('Toggle pause failed', { error: err.message })
162+
setError(err.message)
163+
},
128164
}
129165
)
130166
},
131-
[knowledgeBaseId, triggerSync, isSyncOnCooldown]
167+
[knowledgeBaseId, updateConnector, addToSet, removeFromSet]
132168
)
133169

134170
if (connectors.length === 0 && !canEdit && !isLoading) return null
@@ -167,29 +203,11 @@ export function ConnectorsSection({
167203
workspaceId={workspaceId}
168204
knowledgeBaseId={knowledgeBaseId}
169205
canEdit={canEdit}
170-
isSyncing={isSyncing}
171-
isSyncPending={syncingConnectorId === connector.id}
172-
isUpdating={isUpdating}
206+
isSyncPending={syncingIds.has(connector.id)}
207+
isUpdating={updatingIds.has(connector.id)}
173208
syncCooldown={isSyncOnCooldown(connector.id)}
174209
onSync={() => handleSync(connector.id)}
175-
onTogglePause={() =>
176-
updateConnector(
177-
{
178-
knowledgeBaseId,
179-
connectorId: connector.id,
180-
updates: {
181-
status: connector.status === 'paused' ? 'active' : 'paused',
182-
},
183-
},
184-
{
185-
onSuccess: () => setError(null),
186-
onError: (err) => {
187-
logger.error('Toggle pause failed', { error: err.message })
188-
setError(err.message)
189-
},
190-
}
191-
)
192-
}
210+
onTogglePause={() => handleTogglePause(connector)}
193211
onEdit={() => setEditingConnector(connector)}
194212
onDelete={() => setDeleteTarget(connector.id)}
195213
/>
@@ -260,7 +278,6 @@ interface ConnectorCardProps {
260278
workspaceId: string
261279
knowledgeBaseId: string
262280
canEdit: boolean
263-
isSyncing: boolean
264281
isSyncPending: boolean
265282
isUpdating: boolean
266283
syncCooldown: boolean
@@ -275,7 +292,6 @@ function ConnectorCard({
275292
workspaceId,
276293
knowledgeBaseId,
277294
canEdit,
278-
isSyncing,
279295
isSyncPending,
280296
isUpdating,
281297
syncCooldown,
@@ -368,7 +384,7 @@ function ConnectorCard({
368384
variant='ghost'
369385
className='h-7 w-7 p-0'
370386
onClick={onSync}
371-
disabled={connector.status === 'syncing' || isSyncing || syncCooldown}
387+
disabled={connector.status === 'syncing' || isSyncPending || syncCooldown}
372388
>
373389
<RefreshCw
374390
className={cn(

apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import { useCallback, useMemo, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { useParams, useRouter } from 'next/navigation'
6+
import { Tooltip } from '@/components/emcn'
67
import { Database } from '@/components/emcn/icons'
78
import type { KnowledgeBaseData } from '@/lib/knowledge/types'
89
import type {
910
CreateAction,
11+
ResourceCell,
1012
ResourceColumn,
1113
ResourceRow,
1214
SearchConfig,
@@ -23,6 +25,7 @@ import {
2325
import { filterKnowledgeBases } from '@/app/workspace/[workspaceId]/knowledge/utils/sort'
2426
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
2527
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
28+
import { CONNECTOR_REGISTRY } from '@/connectors/registry'
2629
import { useKnowledgeBasesList } from '@/hooks/kb/use-knowledge'
2730
import { useDeleteKnowledgeBase, useUpdateKnowledgeBase } from '@/hooks/queries/kb/knowledge'
2831
import { useWorkspaceMembersQuery } from '@/hooks/queries/workspace'
@@ -37,13 +40,48 @@ const COLUMNS: ResourceColumn[] = [
3740
{ id: 'name', header: 'Name' },
3841
{ id: 'documents', header: 'Documents' },
3942
{ id: 'tokens', header: 'Tokens' },
43+
{ id: 'connectors', header: 'Connectors' },
4044
{ id: 'created', header: 'Created' },
4145
{ id: 'owner', header: 'Owner' },
4246
{ id: 'updated', header: 'Last Updated' },
4347
]
4448

4549
const DATABASE_ICON = <Database className='h-[14px] w-[14px]' />
4650

51+
function connectorCell(connectorTypes?: string[]): ResourceCell {
52+
if (!connectorTypes || connectorTypes.length === 0) {
53+
return { label: '—' }
54+
}
55+
56+
const entries = connectorTypes
57+
.map((type) => ({ type, def: CONNECTOR_REGISTRY[type] }))
58+
.filter((e): e is { type: string; def: NonNullable<(typeof CONNECTOR_REGISTRY)[string]> } =>
59+
Boolean(e.def?.icon)
60+
)
61+
62+
if (entries.length === 0) return { label: '—' }
63+
64+
return {
65+
content: (
66+
<div className='flex items-center gap-1'>
67+
{entries.map(({ type, def }) => {
68+
const Icon = def.icon
69+
return (
70+
<Tooltip.Root key={type}>
71+
<Tooltip.Trigger asChild>
72+
<span className='flex-shrink-0'>
73+
<Icon className='h-3.5 w-3.5' />
74+
</span>
75+
</Tooltip.Trigger>
76+
<Tooltip.Content>{def.name}</Tooltip.Content>
77+
</Tooltip.Root>
78+
)
79+
})}
80+
</div>
81+
),
82+
}
83+
}
84+
4785
export function Knowledge() {
4886
const params = useParams()
4987
const router = useRouter()
@@ -168,13 +206,15 @@ export function Knowledge() {
168206
tokens: {
169207
label: kb.tokenCount ? kb.tokenCount.toLocaleString() : '0',
170208
},
209+
connectors: connectorCell(kb.connectorTypes),
171210
created: timeCell(kb.createdAt),
172211
owner: ownerCell(kb.userId, members),
173212
updated: timeCell(kb.updatedAt),
174213
},
175214
sortValues: {
176215
documents: kbWithCount.docCount || 0,
177216
tokens: kb.tokenCount || 0,
217+
connectors: kb.connectorTypes?.length || 0,
178218
created: -new Date(kb.createdAt).getTime(),
179219
updated: -new Date(kb.updatedAt).getTime(),
180220
},

0 commit comments

Comments
 (0)