|
| 1 | +import { useEffect, useRef } from 'react' |
| 2 | +import { generateId } from '@sim/utils/id' |
1 | 3 | import { |
2 | 4 | keepPreviousData, |
3 | 5 | type QueryClient, |
@@ -48,6 +50,9 @@ import { |
48 | 50 | resourceScopeFromOwner, |
49 | 51 | resourceScopeKey, |
50 | 52 | } from '@/lib/core/resource-scope' |
| 53 | +import { connectCredentialGroupInPopup } from '@/lib/credential-groups/oauth-popup' |
| 54 | +import { isDesktopApp } from '@/lib/desktop' |
| 55 | +import { connectDesktopSource } from '@/lib/desktop/source-connect' |
51 | 56 | import { MAX_KNOWLEDGE_CONNECTOR_DOCUMENT_PAGE_SIZE } from '@/lib/knowledge/constants' |
52 | 57 | import { organizationAccountsKeys } from '@/hooks/queries/organization-accounts' |
53 | 58 | import { credentialGroupKeys } from '@/hooks/queries/utils/credential-group-queries' |
@@ -388,16 +393,19 @@ async function updateConnectorAccess({ |
388 | 393 | interface StartConnectorMemberEnrollmentParams extends SearchConnectionOAuthQuery { |
389 | 394 | knowledgeBaseId: string |
390 | 395 | connectorId: string |
| 396 | + signal?: AbortSignal |
391 | 397 | } |
392 | 398 |
|
393 | 399 | async function startConnectorMemberEnrollment({ |
394 | 400 | knowledgeBaseId, |
395 | 401 | connectorId, |
396 | 402 | oauthCompletionId, |
| 403 | + signal, |
397 | 404 | }: StartConnectorMemberEnrollmentParams): Promise<StartKnowledgeConnectorMemberEnrollmentData> { |
398 | 405 | const response = await requestJson(startKnowledgeConnectorMemberEnrollmentContract, { |
399 | 406 | params: { id: knowledgeBaseId, connectorId }, |
400 | 407 | query: { oauthCompletionId }, |
| 408 | + signal, |
401 | 409 | }) |
402 | 410 | return response.data |
403 | 411 | } |
@@ -464,17 +472,94 @@ export function useSearchSources( |
464 | 472 | }) |
465 | 473 | } |
466 | 474 |
|
467 | | -/** Mints the viewer's enrollment link for a per-member connector; the caller navigates to it. */ |
| 475 | +interface ConnectMemberAccountParams { |
| 476 | + knowledgeBaseId: string |
| 477 | + connectorId: string |
| 478 | + /** Search sources only: the server opens provider OAuth directly and reports its completion. */ |
| 479 | + directOAuth: boolean |
| 480 | +} |
| 481 | + |
| 482 | +/** |
| 483 | + * Connects the viewer's own account to a per-member connector. Desktop and |
| 484 | + * direct OAuth settle when the connection completes. An ordinary knowledge |
| 485 | + * base hands the viewer an invitation page in a new tab that reports nothing |
| 486 | + * back, so it refreshes each time the viewer returns to this tab until the |
| 487 | + * viewer's membership reads `connected`. The listener belongs to the attempt's |
| 488 | + * abort signal, so a newer attempt or unmount removes it. |
| 489 | + */ |
468 | 490 | export function useStartConnectorMemberEnrollment() { |
469 | 491 | const queryClient = useQueryClient() |
| 492 | + const pending = useRef<AbortController | null>(null) |
| 493 | + useEffect(() => () => pending.current?.abort(), []) |
| 494 | + const refresh = (knowledgeBaseId: string) => |
| 495 | + Promise.all([ |
| 496 | + queryClient.invalidateQueries({ queryKey: connectorKeys.lists(knowledgeBaseId) }), |
| 497 | + queryClient.invalidateQueries({ queryKey: credentialGroupKeys.details() }), |
| 498 | + queryClient.invalidateQueries({ queryKey: searchSourceKeys.lists() }), |
| 499 | + queryClient.invalidateQueries({ queryKey: searchIntegrationKeys.lists() }), |
| 500 | + ]) |
470 | 501 | return useMutation({ |
471 | | - mutationFn: startConnectorMemberEnrollment, |
472 | | - onSuccess: () => |
473 | | - Promise.all([ |
474 | | - queryClient.invalidateQueries({ queryKey: credentialGroupKeys.details() }), |
475 | | - queryClient.invalidateQueries({ queryKey: searchSourceKeys.lists() }), |
476 | | - queryClient.invalidateQueries({ queryKey: searchIntegrationKeys.lists() }), |
477 | | - ]), |
| 502 | + mutationFn: async ({ |
| 503 | + knowledgeBaseId, |
| 504 | + connectorId, |
| 505 | + directOAuth, |
| 506 | + }: ConnectMemberAccountParams) => { |
| 507 | + pending.current?.abort() |
| 508 | + const controller = new AbortController() |
| 509 | + pending.current = controller |
| 510 | + const params = { id: knowledgeBaseId, connectorId } |
| 511 | + const completionId = directOAuth ? generateId() : undefined |
| 512 | + if (isDesktopApp()) { |
| 513 | + await connectDesktopSource( |
| 514 | + { kind: 'member-enrollment', params, completionId }, |
| 515 | + controller.signal |
| 516 | + ) |
| 517 | + return true |
| 518 | + } |
| 519 | + if (completionId) { |
| 520 | + await connectCredentialGroupInPopup( |
| 521 | + completionId, |
| 522 | + async (signal) => ({ |
| 523 | + invitationLink: ( |
| 524 | + await startConnectorMemberEnrollment({ |
| 525 | + knowledgeBaseId, |
| 526 | + connectorId, |
| 527 | + oauthCompletionId: completionId, |
| 528 | + signal, |
| 529 | + }) |
| 530 | + ).url, |
| 531 | + }), |
| 532 | + controller.signal |
| 533 | + ) |
| 534 | + return true |
| 535 | + } |
| 536 | + const tab = window.open('about:blank', '_blank') |
| 537 | + if (tab) tab.opener = null |
| 538 | + try { |
| 539 | + const { url } = await startConnectorMemberEnrollment({ |
| 540 | + knowledgeBaseId, |
| 541 | + connectorId, |
| 542 | + signal: controller.signal, |
| 543 | + }) |
| 544 | + if (tab) tab.location.replace(url) |
| 545 | + else window.location.assign(url) |
| 546 | + } catch (error) { |
| 547 | + tab?.close() |
| 548 | + throw error |
| 549 | + } |
| 550 | + const refreshUntilConnected = async () => { |
| 551 | + if (document.visibilityState !== 'visible') return |
| 552 | + await refresh(knowledgeBaseId) |
| 553 | + const connector = getCachedConnector(queryClient, knowledgeBaseId, connectorId) |
| 554 | + if (connector?.viewerMembership === 'connected') controller.abort() |
| 555 | + } |
| 556 | + document.addEventListener('visibilitychange', () => void refreshUntilConnected(), { |
| 557 | + signal: controller.signal, |
| 558 | + }) |
| 559 | + return false |
| 560 | + }, |
| 561 | + onSettled: (completed, _error, { knowledgeBaseId }) => |
| 562 | + completed === false ? undefined : refresh(knowledgeBaseId), |
478 | 563 | }) |
479 | 564 | } |
480 | 565 |
|
|
0 commit comments