Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use client'

import { Chip } from '@sim/emcn'
import type { ViewerConnectorMembership } from '@/lib/api/contracts/knowledge/connectors'
import { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
import { useStartConnectorMemberEnrollment } from '@/hooks/queries/kb/connectors'

type UnconnectedMembership = Exclude<ViewerConnectorMembership, 'connected'>

/** Revoked and unverified viewers get the server's refusal as the row, with no action to retry. */
const MEMBERSHIP_STATES: Record<UnconnectedMembership, { title: string; action?: string }> = {
not_enrolled: { title: 'Your account is not connected', action: 'Connect your account' },
invited: { title: 'Your account is not connected', action: 'Connect your account' },
needs_reauth: { title: 'Your account needs to be reconnected', action: 'Reconnect' },
revoked: { title: 'An admin removed your access to Connected accounts' },
unverified_email: { title: 'Verify your email address before connecting an account' },
}

interface ConnectorMemberEnrollmentProps {
knowledgeBaseId: string
connectorId: string
isSearchIndex: boolean
membership: UnconnectedMembership
}

export function ConnectorMemberEnrollment({
knowledgeBaseId,
connectorId,
isSearchIndex,
membership,
}: ConnectorMemberEnrollmentProps) {
const enrollment = useStartConnectorMemberEnrollment()
const { title, action } = MEMBERSHIP_STATES[membership]

return (
<>
<SettingsResourceRow
title={title}
trailing={
action ? (
<Chip
disabled={enrollment.isPending}
onClick={() =>
enrollment.mutate({ knowledgeBaseId, connectorId, directOAuth: isSearchIndex })
}
>
{action}
</Chip>
) : undefined
}
/>
{enrollment.error && (
<p role='alert' className='text-[var(--text-error)] text-caption'>
{enrollment.error.message}
</p>
)}
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ConnectorActionFeedback,
ConnectorActions,
} from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connector-actions'
import { ConnectorMemberEnrollment } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connector-member-enrollment'
import { ConnectorRecovery } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connector-recovery'
import { ConnectorSyncHistory } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connector-sync-history'
import { getConnectorSyncState } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connector-sync-state'
Expand Down Expand Up @@ -171,6 +172,14 @@ function ConnectorCard({
Updating access
</span>
)}
{connector.viewerMembership && connector.viewerMembership !== 'connected' && (
<ConnectorMemberEnrollment
knowledgeBaseId={knowledgeBaseId}
connectorId={connector.id}
isSearchIndex={isSearchIndex}
membership={connector.viewerMembership}
/>
)}
<ConnectorRecovery
connector={connector}
scope={scope}
Expand Down
101 changes: 93 additions & 8 deletions apps/sim/hooks/queries/kb/connectors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useEffect, useRef } from 'react'
import { generateId } from '@sim/utils/id'
import {
keepPreviousData,
type QueryClient,
Expand Down Expand Up @@ -48,6 +50,9 @@ import {
resourceScopeFromOwner,
resourceScopeKey,
} from '@/lib/core/resource-scope'
import { connectCredentialGroupInPopup } from '@/lib/credential-groups/oauth-popup'
import { isDesktopApp } from '@/lib/desktop'
import { connectDesktopSource } from '@/lib/desktop/source-connect'
import { MAX_KNOWLEDGE_CONNECTOR_DOCUMENT_PAGE_SIZE } from '@/lib/knowledge/constants'
import { organizationAccountsKeys } from '@/hooks/queries/organization-accounts'
import { credentialGroupKeys } from '@/hooks/queries/utils/credential-group-queries'
Expand Down Expand Up @@ -388,16 +393,19 @@ async function updateConnectorAccess({
interface StartConnectorMemberEnrollmentParams extends SearchConnectionOAuthQuery {
knowledgeBaseId: string
connectorId: string
signal?: AbortSignal
}

async function startConnectorMemberEnrollment({
knowledgeBaseId,
connectorId,
oauthCompletionId,
signal,
}: StartConnectorMemberEnrollmentParams): Promise<StartKnowledgeConnectorMemberEnrollmentData> {
const response = await requestJson(startKnowledgeConnectorMemberEnrollmentContract, {
params: { id: knowledgeBaseId, connectorId },
query: { oauthCompletionId },
signal,
})
return response.data
}
Expand Down Expand Up @@ -464,17 +472,94 @@ export function useSearchSources(
})
}

/** Mints the viewer's enrollment link for a per-member connector; the caller navigates to it. */
interface ConnectMemberAccountParams {
knowledgeBaseId: string
connectorId: string
/** Search sources only: the server opens provider OAuth directly and reports its completion. */
directOAuth: boolean
}

/**
* Connects the viewer's own account to a per-member connector. Desktop and
* direct OAuth settle when the connection completes. An ordinary knowledge
* base hands the viewer an invitation page in a new tab that reports nothing
* back, so it refreshes each time the viewer returns to this tab until the
* viewer's membership reads `connected`. The listener belongs to the attempt's
* abort signal, so a newer attempt or unmount removes it.
*/
export function useStartConnectorMemberEnrollment() {
const queryClient = useQueryClient()
const pending = useRef<AbortController | null>(null)
useEffect(() => () => pending.current?.abort(), [])
const refresh = (knowledgeBaseId: string) =>
Promise.all([
queryClient.invalidateQueries({ queryKey: connectorKeys.lists(knowledgeBaseId) }),
queryClient.invalidateQueries({ queryKey: credentialGroupKeys.details() }),
queryClient.invalidateQueries({ queryKey: searchSourceKeys.lists() }),
queryClient.invalidateQueries({ queryKey: searchIntegrationKeys.lists() }),
])
return useMutation({
mutationFn: startConnectorMemberEnrollment,
onSuccess: () =>
Promise.all([
queryClient.invalidateQueries({ queryKey: credentialGroupKeys.details() }),
queryClient.invalidateQueries({ queryKey: searchSourceKeys.lists() }),
queryClient.invalidateQueries({ queryKey: searchIntegrationKeys.lists() }),
]),
mutationFn: async ({
knowledgeBaseId,
connectorId,
directOAuth,
}: ConnectMemberAccountParams) => {
pending.current?.abort()
const controller = new AbortController()
pending.current = controller
const params = { id: knowledgeBaseId, connectorId }
const completionId = directOAuth ? generateId() : undefined
if (isDesktopApp()) {
await connectDesktopSource(
{ kind: 'member-enrollment', params, completionId },
controller.signal
)
return true
}
if (completionId) {
await connectCredentialGroupInPopup(
completionId,
async (signal) => ({
invitationLink: (
await startConnectorMemberEnrollment({
knowledgeBaseId,
connectorId,
oauthCompletionId: completionId,
signal,
})
).url,
}),
controller.signal
)
return true
}
const tab = window.open('about:blank', '_blank')
if (tab) tab.opener = null
try {
const { url } = await startConnectorMemberEnrollment({
knowledgeBaseId,
connectorId,
signal: controller.signal,
})
if (tab) tab.location.replace(url)
else window.location.assign(url)
} catch (error) {
tab?.close()
throw error
}
const refreshUntilConnected = async () => {
if (document.visibilityState !== 'visible') return
await refresh(knowledgeBaseId)
const connector = getCachedConnector(queryClient, knowledgeBaseId, connectorId)
if (connector?.viewerMembership === 'connected') controller.abort()
}
document.addEventListener('visibilitychange', () => void refreshUntilConnected(), {
signal: controller.signal,
})
return false
},
onSettled: (completed, _error, { knowledgeBaseId }) =>
completed === false ? undefined : refresh(knowledgeBaseId),
})
}

Expand Down
1 change: 0 additions & 1 deletion scripts/check-unused-exports.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,6 @@
"apps/sim/hooks/queries/kb/connectors.ts#ViewerConnectorMembership",
"apps/sim/hooks/queries/kb/connectors.ts#connectorDocumentKeys",
"apps/sim/hooks/queries/kb/connectors.ts#searchIndexKeys",
"apps/sim/hooks/queries/kb/connectors.ts#useStartConnectorMemberEnrollment",
"apps/sim/hooks/queries/kb/knowledge.ts#KNOWLEDGE_CHUNK_LIST_STALE_TIME",
"apps/sim/hooks/queries/kb/knowledge.ts#KNOWLEDGE_CHUNK_SEARCH_STALE_TIME",
"apps/sim/hooks/queries/kb/knowledge.ts#KNOWLEDGE_DOCUMENT_DETAIL_STALE_TIME",
Expand Down
Loading