Skip to content

Commit 45ab0f2

Browse files
authored
fix(knowledge): simplify connector management (#7788)
* fix(knowledge): simplify connector management * fix(knowledge): keep history collapse accessible
1 parent 64bce6a commit 45ab0f2

19 files changed

Lines changed: 518 additions & 320 deletions

File tree

‎apps/docs/content/docs/search/confluence.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ Central sources combine space permissions, page and ancestor restrictions, and g
132132

133133
Open **Settings → Sources → Confluence**, then a source's **Documents**, **Settings**, or **Sync history**. Invite teammates through **Settings → Members → Invite** or SSO, then have them connect through **Integrations**. **People → Request connections** only requests a provider connection; it does not invite people to the organization.
134134

135-
Syncing runs automatically. Admins can use **Sync now** for an immediate update, **Pause syncing** to stop scheduled syncs, or **Resume syncing** to restart them. **Full resync**, available for service-account connections, fetches unchanged content again and asks for confirmation. Successful manual syncs have a one-minute cooldown; failed syncs can be retried immediately.
135+
Syncing runs automatically. Admins can use **Sync now** for an immediate update, **Pause syncing** to stop scheduled syncs, or **Resume syncing** to restart them. Successful manual syncs have a one-minute cooldown; failed syncs can be retried immediately.
136136

137137
## Troubleshooting
138138

‎apps/docs/content/docs/search/connect-your-account.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ On the main **Integrations** page, select **Reconnect** beside the integration i
7878

7979
Admins manage setup from **Settings → Sources**. Open an integration, then its connection to see **Documents**, **Settings**, and **Sync history**. **People** shows account contributors across integrations and supports filtering by integration. This does not grant the admin access to every document.
8080

81-
Syncing runs automatically. Admins can use **Sync now** when they need an update; another manual run is available 60 seconds after a successful sync finishes. Failed or partial runs can be retried immediately. The connection header also offers **Pause syncing** or **Resume syncing**, and **Remove connection**. Where supported, **Full resync** fetches and reindexes all content and asks for confirmation first.
81+
Syncing runs automatically. Admins can use **Sync now** when they need an update; another manual run is available 60 seconds after a successful sync finishes. Failed or partial runs can be retried immediately. The connection header also offers **Pause syncing** or **Resume syncing**, and **Remove connection**.
8282

8383
Removing a Search connection also removes its indexed documents from Sim. The originals remain in the connected app.
8484

‎apps/docs/content/docs/search/gitlab.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ Open a project to use these administrator actions:
146146
| **Settings** | Change the token, project, filters, or CSV permissions. |
147147
| **Remove connection** | Confirm removal of the connection and its indexed documents. Documents cannot be retained without the connection that maintains their permissions. |
148148

149-
GitLab does not expose a separate **Full resync** action. Each sync checks the selected content. CSV grants change only when you replace the files.
149+
Each sync checks the selected content. CSV grants change only when you replace the files.
150150

151151
## Troubleshooting
152152

‎apps/docs/content/docs/search/index.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Edit **Settings** to change an existing connection's filters. Adding another con
104104

105105
**Sync using** shows the method selected when the source was created. Add a new connection to change that method. To replace a supported indexing credential, select its replacement and use **Change service account** or **Change account**, as shown.
106106

107-
The connection header offers **Sync now**, **Pause syncing** or **Resume syncing**, **Remove connection**, and, where supported, **Full resync**. Full resync fetches all content again and requires confirmation. Manual runs have a 60-second cooldown after a successful sync finishes; failed or partial runs can be retried immediately. **Pause syncing** becomes available when the current sync finishes.
107+
The connection header offers **Sync now**, **Pause syncing** or **Resume syncing**, and **Remove connection**. Manual runs have a 60-second cooldown after a successful sync finishes; failed or partial runs can be retried immediately. **Pause syncing** becomes available when the current sync finishes.
108108

109109
To deactivate an entire integration, open it from **Settings → Sources**, select **Deactivate**, and confirm. Its content becomes unavailable in Search, Assistant, and MCP; saved connections remain. Select **Activate** on that integration to enable it again.
110110

‎apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.test.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,8 @@ describe('ConnectOAuthModal reauthorization', () => {
347347
},
348348
})
349349

350+
expect(container.querySelector('header')).toHaveTextContent('Reconnect Slack')
351+
expect(container).not.toHaveTextContent('tool requires access')
350352
await clickConnect()
351353

352354
expect(mocks.writeOAuthReturnContext).toHaveBeenCalledExactlyOnceWith(
@@ -424,6 +426,8 @@ describe('ConnectOAuthModal reauthorization', () => {
424426
},
425427
})
426428

429+
expect(container.querySelector('header')).toHaveTextContent('Connect Slack')
430+
expect(container).toHaveTextContent('tool requires access')
427431
await clickConnect()
428432

429433
expect(mocks.writeOAuthReturnContext).toHaveBeenCalledOnce()

‎apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,16 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
506506
? `An integration named "${existingCredential.displayName}" already exists.`
507507
: undefined)
508508

509-
const title = `Connect ${providerName}`
509+
const isConnectorReconnect = !isConnect && props.returnContext?.origin === 'kb-connectors'
510+
const connectLabel = isConnectorReconnect
511+
? newScopes.length > 0
512+
? 'Update access'
513+
: 'Reconnect'
514+
: 'Connect'
515+
const title =
516+
isConnectorReconnect && newScopes.length > 0
517+
? `Update ${providerName} access`
518+
: `${connectLabel} ${providerName}`
510519

511520
return (
512521
<ChipModal open={open} onOpenChange={onOpenChange} srTitle={title}>
@@ -519,7 +528,11 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
519528
<ChipModalBody>
520529
{!isConnect && (
521530
<p className='text-[var(--text-tertiary)] text-caption'>
522-
The "{props.toolName}" tool requires access to your account.
531+
{isConnectorReconnect
532+
? newScopes.length > 0
533+
? 'Approve the requested permissions to continue syncing.'
534+
: `Continue to ${providerName} to restore this connection.`
535+
: `The "${props.toolName}" tool requires access to your account.`}
523536
</p>
524537
)}
525538

@@ -651,7 +664,7 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
651664
: undefined
652665
}
653666
primaryAction={{
654-
label: isPending ? 'Connecting...' : 'Connect',
667+
label: isPending ? 'Connecting...' : connectLabel,
655668
onClick: handleConnect,
656669
disabled: isDisabled,
657670
}}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use client'
2+
3+
import { ChipDropdown } from '@sim/emcn'
4+
import type {
5+
ConnectorDocumentFilter,
6+
ConnectorDocumentsData,
7+
} from '@/lib/api/contracts/knowledge/connectors'
8+
9+
interface ConnectorDocumentStatusFilterProps {
10+
filter: ConnectorDocumentFilter
11+
onFilterChange: (filter: ConnectorDocumentFilter) => void
12+
counts?: ConnectorDocumentsData['counts']
13+
isLoading: boolean
14+
}
15+
16+
export function ConnectorDocumentStatusFilter({
17+
filter,
18+
onFilterChange,
19+
counts = { active: 0, excluded: 0, failed: 0, skipped: 0 },
20+
isLoading,
21+
}: ConnectorDocumentStatusFilterProps) {
22+
return (
23+
<ChipDropdown
24+
aria-label='Document status'
25+
value={filter}
26+
onChange={(value) => {
27+
if (value === 'active' || value === 'excluded' || value === 'failed' || value === 'skipped')
28+
onFilterChange(value)
29+
}}
30+
matchTriggerWidth={false}
31+
options={[
32+
{ value: 'active', label: isLoading ? 'Included' : `Included (${counts.active})` },
33+
{ value: 'excluded', label: isLoading ? 'Excluded' : `Excluded (${counts.excluded})` },
34+
{ value: 'failed', label: isLoading ? 'Failed' : `Failed (${counts.failed})` },
35+
{ value: 'skipped', label: isLoading ? 'Skipped' : `Skipped (${counts.skipped})` },
36+
]}
37+
/>
38+
)
39+
}

‎apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-documents.tsx‎

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
'use client'
22

3-
import { Chip, ChipDropdown, ChipInput, ChipLink, Skeleton } from '@sim/emcn'
3+
import { Chip, ChipInput, ChipLink, Skeleton } from '@sim/emcn'
44
import { RefreshCw, Search, SquareArrowUpRight } from '@sim/emcn/icons'
55
import type { ConnectorDocumentFilter } from '@/lib/api/contracts/knowledge/connectors'
66
import type { ResourceScope } from '@/lib/core/resource-scope'
77
import { getDocumentIndexingStatus } from '@/lib/knowledge/documents/types'
8+
import { ConnectorDocumentStatusFilter } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-document-status-filter'
89
import {
910
SettingsEmptyState,
1011
SettingsQueryErrorState,
@@ -25,6 +26,7 @@ interface ConnectorDocumentsProps {
2526
connectorId: string
2627
search?: string
2728
searchControl?: { value: string; onChange: (value: string) => void }
29+
showToolbar?: boolean
2830
progressScope?: ResourceScope
2931
isSearchIndex?: boolean
3032
syncing?: boolean
@@ -38,6 +40,7 @@ export function ConnectorDocuments({
3840
filter,
3941
search,
4042
searchControl,
43+
showToolbar = true,
4144
progressScope,
4245
isSearchIndex = false,
4346
syncing,
@@ -79,41 +82,26 @@ export function ConnectorDocuments({
7982
{isSearchIndex && (
8083
<p className='text-[var(--text-body)] text-sm'>Documents you can access</p>
8184
)}
82-
<div className='flex items-center gap-2'>
83-
{searchControl && (
84-
<ChipInput
85-
icon={Search}
86-
placeholder='Search documents...'
87-
value={searchControl.value}
88-
onChange={(event) => searchControl.onChange(event.target.value)}
89-
autoComplete='off'
90-
className='min-w-0 flex-1'
85+
{showToolbar && (
86+
<div className='flex items-center gap-2'>
87+
{searchControl && (
88+
<ChipInput
89+
icon={Search}
90+
placeholder='Search documents...'
91+
value={searchControl.value}
92+
onChange={(event) => searchControl.onChange(event.target.value)}
93+
autoComplete='off'
94+
className='min-w-0 flex-1'
95+
/>
96+
)}
97+
<ConnectorDocumentStatusFilter
98+
filter={filter}
99+
onFilterChange={onFilterChange}
100+
counts={counts}
101+
isLoading={isLoading}
91102
/>
92-
)}
93-
<ChipDropdown
94-
aria-label='Document status'
95-
value={filter}
96-
onChange={(value) => {
97-
if (
98-
value === 'active' ||
99-
value === 'excluded' ||
100-
value === 'failed' ||
101-
value === 'skipped'
102-
)
103-
onFilterChange(value)
104-
}}
105-
matchTriggerWidth={false}
106-
options={[
107-
{ value: 'active', label: isLoading ? 'Included' : `Included (${counts.active})` },
108-
{
109-
value: 'excluded',
110-
label: isLoading ? 'Excluded' : `Excluded (${counts.excluded})`,
111-
},
112-
{ value: 'failed', label: isLoading ? 'Failed' : `Failed (${counts.failed})` },
113-
{ value: 'skipped', label: isLoading ? 'Skipped' : `Skipped (${counts.skipped})` },
114-
]}
115-
/>
116-
</div>
103+
</div>
104+
)}
117105
<div className={RESOURCE_LIST_STACK}>
118106
{query.isError && !query.isFetchNextPageError ? (
119107
<SettingsQueryErrorState

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

Lines changed: 88 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,96 @@
11
'use client'
22

3-
import { useId } from 'react'
4-
import { Checkbox, ChipConfirmModal, ChipModalError, ChipModalField } from '@sim/emcn'
5-
import { SettingsActionChips } from '@/components/settings/settings-header'
3+
import { Fragment, type Ref, useId } from 'react'
64
import {
7-
type ConnectorActionState,
8-
type ConnectorActionsOptions,
9-
useConnectorActions,
10-
} from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/use-connector-actions'
5+
Checkbox,
6+
Chip,
7+
ChipConfirmModal,
8+
ChipModalError,
9+
ChipModalField,
10+
DropdownMenu,
11+
DropdownMenuContent,
12+
DropdownMenuItem,
13+
DropdownMenuSeparator,
14+
DropdownMenuTrigger,
15+
Tooltip,
16+
} from '@sim/emcn'
17+
import { MoreHorizontal } from '@sim/emcn/icons'
18+
import { orderHeaderActions, type SettingsAction } from '@/components/settings/settings-header'
19+
import type { ConnectorActionState } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/use-connector-actions'
1120
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
1221

13-
export function ConnectorActions(props: ConnectorActionsOptions) {
14-
const state = useConnectorActions(props)
15-
if (!state.canEdit) return null
22+
interface ConnectorActionsProps {
23+
state: ConnectorActionState
24+
triggerRef?: Ref<HTMLButtonElement>
25+
history?: {
26+
expanded: boolean
27+
contentId: string
28+
onToggle: () => void
29+
}
30+
}
31+
32+
export function ConnectorActions({ state, triggerRef, history }: ConnectorActionsProps) {
33+
if (!state.canEdit && !history) return null
34+
const actions = orderHeaderActions([
35+
...state.actions,
36+
...(history
37+
? [
38+
{
39+
id: 'history',
40+
text: history.expanded ? 'Hide history' : 'Sync history',
41+
onSelect: history.onToggle,
42+
},
43+
]
44+
: []),
45+
])
1646
return (
17-
<div className='flex flex-col gap-2'>
18-
<div className='flex flex-wrap items-center gap-1'>
19-
<SettingsActionChips actions={state.actions} />
20-
</div>
21-
<ConnectorActionFeedback state={state} />
22-
</div>
47+
<DropdownMenu>
48+
<DropdownMenuTrigger asChild>
49+
<Chip ref={triggerRef} aria-label='Connection actions' leftIcon={MoreHorizontal} />
50+
</DropdownMenuTrigger>
51+
<DropdownMenuContent align='end'>
52+
{actions.map(({ action }, index) => (
53+
<Fragment key={action.id}>
54+
{action.id === 'delete' && index > 0 && <DropdownMenuSeparator />}
55+
<ConnectorActionMenuItem
56+
action={action}
57+
history={action.id === 'history' ? history : undefined}
58+
/>
59+
</Fragment>
60+
))}
61+
</DropdownMenuContent>
62+
</DropdownMenu>
63+
)
64+
}
65+
66+
interface ConnectorActionMenuItemProps {
67+
action: SettingsAction
68+
history?: ConnectorActionsProps['history']
69+
}
70+
71+
function ConnectorActionMenuItem({ action, history }: ConnectorActionMenuItemProps) {
72+
const item = (
73+
<DropdownMenuItem
74+
onSelect={action.onSelect}
75+
disabled={action.disabled}
76+
aria-label={
77+
action.disabled && action.tooltip ? `${action.text} — ${action.tooltip}` : action.text
78+
}
79+
aria-expanded={history?.expanded}
80+
aria-controls={history?.contentId}
81+
>
82+
{action.text}
83+
</DropdownMenuItem>
84+
)
85+
return action.tooltip ? (
86+
<Tooltip.Root>
87+
<Tooltip.Trigger asChild>
88+
<span className='block'>{item}</span>
89+
</Tooltip.Trigger>
90+
<Tooltip.Content>{action.tooltip}</Tooltip.Content>
91+
</Tooltip.Root>
92+
) : (
93+
item
2394
)
2495
}
2596

@@ -30,30 +101,14 @@ interface ConnectorActionFeedbackProps {
30101
export function ConnectorActionFeedback({ state }: ConnectorActionFeedbackProps) {
31102
const deleteDocumentsId = useId()
32103
if (!state.canEdit) return null
33-
const { removal, fullResync } = state
104+
const { removal } = state
34105
return (
35106
<>
36107
{state.error && (
37108
<SettingsEmptyState variant='inline' tone='error'>
38109
{state.error.message}
39110
</SettingsEmptyState>
40111
)}
41-
<ChipConfirmModal
42-
open={fullResync.open}
43-
onOpenChange={fullResync.onOpenChange}
44-
title='Full resync?'
45-
text='Fetch all content again for this connection, including unchanged documents. This can take longer than a regular sync.'
46-
confirm={{
47-
label: 'Full resync',
48-
variant: 'primary',
49-
pending: fullResync.pending,
50-
disabled: fullResync.disabled,
51-
pendingLabel: 'Queuing…',
52-
onClick: fullResync.onConfirm,
53-
}}
54-
>
55-
<ChipModalError>{fullResync.error?.message}</ChipModalError>
56-
</ChipConfirmModal>
57112
<ChipConfirmModal
58113
open={removal.open}
59114
onOpenChange={removal.onOpenChange}

0 commit comments

Comments
 (0)