11'use client'
22
33import { useMemo } from 'react'
4+ import { Chip } from '@sim/emcn'
45import type { ResourceScope } from '@/lib/core/resource-scope'
5- import { connectorDisplayName } from '@/lib/sim-search/connectors'
6+ import {
7+ connectorDisplayName ,
8+ getConnectorAccessAvailability ,
9+ SEARCH_CONNECTORS ,
10+ SEARCH_SOURCE_TYPES ,
11+ } from '@/lib/sim-search/connectors'
612import { OrganizationPage } from '@/app/o/[organizationId]/components/organization-page'
713import { useOrganizationPageFilters } from '@/app/o/[organizationId]/components/organization-page/use-organization-page-filters'
814import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
15+ import { SourceSetupModal } from '@/app/workspace/[workspaceId]/home/components/search-sources/source-setup-modal'
16+ import { IntegrationTile } from '@/app/workspace/[workspaceId]/integrations/components/integrations-showcase'
917import { SearchSourceRow } from '@/app/workspace/[workspaceId]/search/components/search-source-row'
1018import {
1119 SettingsEmptyState ,
1220 SettingsQueryErrorState ,
1321} from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
14- import { RESOURCE_LIST_STACK } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
22+ import {
23+ RESOURCE_LIST_STACK ,
24+ SettingsResourceRow ,
25+ } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
1526import { searchSourceKeys , useSearchSources } from '@/hooks/queries/kb/connectors'
27+ import { useSearchIntegrations } from '@/hooks/queries/search-integrations'
1628import { useMemberEnrollment } from '@/hooks/use-member-enrollment'
1729import { useDesktopOAuthConnectListener , useOAuthReturnRouter } from '@/hooks/use-oauth-return'
30+ import { usePermissionConfig } from '@/hooks/use-permission-config'
1831
1932/** Every source the organization searches, or only the ones the viewer has connected. */
2033const TABS = [
@@ -33,6 +46,8 @@ export function OrganizationIntegrations() {
3346 const { organization, searchAccess } = useOrganizationContext ( )
3447 const scope : ResourceScope = { kind : 'organization' , organizationId : organization . id }
3548 const sources = useSearchSources ( scope )
49+ const integrations = useSearchIntegrations ( organization . id )
50+ const availability = usePermissionConfig ( )
3651 const { tab, search } = useOrganizationPageFilters ( )
3752 const membershipQueryKeys = useMemo (
3853 ( ) => [ searchSourceKeys . list ( { kind : 'organization' , organizationId : organization . id } ) ] ,
@@ -59,40 +74,96 @@ export function OrganizationIntegrations() {
5974 . includes ( query )
6075 ) ?? [ ]
6176
77+ const approvedTypes = new Set (
78+ integrations . data
79+ ?. filter ( ( integration ) => integration . approved )
80+ . map ( ( integration ) => integration . connectorType )
81+ )
82+ const configuredTypes = new Set ( sources . data ?. map ( ( source ) => source . connectorType ) )
83+ const unconfigured = mineOnly
84+ ? [ ]
85+ : SEARCH_SOURCE_TYPES . filter (
86+ ( [ type , meta ] ) =>
87+ approvedTypes . has ( type ) &&
88+ ! configuredTypes . has ( type ) &&
89+ meta . name . toLowerCase ( ) . includes ( query )
90+ )
91+ const failedQuery = sources . isError ? sources : integrations . isError ? integrations : null
92+
6293 return (
6394 < OrganizationPage
6495 title = 'Integrations'
6596 description = 'Connect your tools for Sim Search'
6697 tabs = { TABS }
6798 >
6899 < div className = { RESOURCE_LIST_STACK } >
69- { sources . isError ? (
100+ { failedQuery ? (
70101 < SettingsQueryErrorState
71- error = { sources . error }
102+ error = { failedQuery . error }
72103 fallback = 'Could not load sources'
73- isRetrying = { sources . isFetching }
74- onRetry = { ( ) => void sources . refetch ( ) }
104+ isRetrying = { failedQuery . isFetching }
105+ onRetry = { ( ) => void failedQuery . refetch ( ) }
75106 variant = 'inline'
76107 />
77- ) : visibleSources . length > 0 ? (
78- visibleSources . map ( ( source ) => (
79- < SearchSourceRow
80- key = { source . connectorId }
81- source = { source }
82- scope = { scope }
83- canAdmin = { false }
84- available = {
85- source . accessMode === 'members'
86- ? searchAccess . memberScoped
87- : searchAccess . sourceMirrored &&
88- ( ! source . connectionRequired || searchAccess . memberScoped )
89- }
90- waiting = { enrollment . isAwaiting ( source . connectorId ) }
91- isPending = { enrollment . isPending }
92- onConnect = { ( ) => enrollment . connect ( source . knowledgeBaseId , source . connectorId ) }
93- />
94- ) )
95- ) : sources . isPending ? null : (
108+ ) : visibleSources . length > 0 || unconfigured . length > 0 ? (
109+ < >
110+ { unconfigured . map ( ( [ type , meta ] ) => {
111+ const connector = SEARCH_CONNECTORS . find ( ( item ) => item . type === type )
112+ const access = getConnectorAccessAvailability (
113+ meta ,
114+ availability . integrationAvailability ,
115+ {
116+ memberAccessAvailable : searchAccess . memberScoped ,
117+ mirroredAccessAvailable : searchAccess . sourceMirrored ,
118+ oauthServiceAvailability : availability . oauthServiceAvailability ,
119+ isIntegrationAvailabilityReady : availability . isIntegrationAvailabilityReady ,
120+ }
121+ )
122+ const canConnect = connector && access . members
123+ return (
124+ < SettingsResourceRow
125+ key = { type }
126+ iconVariant = 'custom'
127+ icon = { < IntegrationTile blockType = { type } icon = { meta . icon } /> }
128+ title = { meta . name }
129+ description = {
130+ canConnect
131+ ? 'Approved · Connect your account to search this source'
132+ : 'Approved · An admin needs to finish source setup'
133+ }
134+ trailing = {
135+ canConnect ? (
136+ < Chip
137+ variant = 'primary'
138+ disabled = { enrollment . isPending }
139+ onClick = { ( ) => enrollment . connectSearchSource ( scope , connector , undefined ) }
140+ >
141+ Connect account
142+ </ Chip >
143+ ) : undefined
144+ }
145+ />
146+ )
147+ } ) }
148+ { visibleSources . map ( ( source ) => (
149+ < SearchSourceRow
150+ key = { source . connectorId }
151+ source = { source }
152+ scope = { scope }
153+ canAdmin = { false }
154+ available = {
155+ source . accessMode === 'members'
156+ ? searchAccess . memberScoped
157+ : searchAccess . sourceMirrored &&
158+ ( ! source . connectionRequired || searchAccess . memberScoped )
159+ }
160+ waiting = { enrollment . isAwaiting ( source . connectorId ) }
161+ isPending = { enrollment . isPending }
162+ onConnect = { ( ) => enrollment . connect ( source . knowledgeBaseId , source . connectorId ) }
163+ />
164+ ) ) }
165+ </ >
166+ ) : sources . isPending || integrations . isPending ? null : (
96167 < SettingsEmptyState variant = 'inline' >
97168 { query
98169 ? 'No matching sources.'
@@ -105,6 +176,15 @@ export function OrganizationIntegrations() {
105176 < p className = 'text-[var(--text-error)] text-caption' > { enrollment . error } </ p >
106177 ) }
107178 </ div >
179+ { enrollment . setupConnector && (
180+ < SourceSetupModal
181+ connector = { enrollment . setupConnector }
182+ onClose = { enrollment . closeSetup }
183+ onConnect = { ( config ) =>
184+ enrollment . connectSource ( scope , enrollment . setupConnector ! . type , config )
185+ }
186+ />
187+ ) }
108188 </ OrganizationPage >
109189 )
110190}
0 commit comments