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
6 changes: 5 additions & 1 deletion apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -900,7 +900,11 @@ export function KnowledgeBase({
onClick={() => setShowConnectorsModal(true)}
className='flex shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption shadow-[inset_0_0_0_1px_var(--border)] transition-colors hover-hover:bg-[var(--surface-3)]'
>
{ConnectorIcon && <ConnectorIcon className='h-[14px] w-[14px]' />}
{connector.status === 'syncing' ? (
<Loader2 className='h-[14px] w-[14px] animate-spin' />
) : (
ConnectorIcon && <ConnectorIcon className='h-[14px] w-[14px]' />
)}
{def?.name || connector.connectorType}
</button>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,17 @@ import {
ModalHeader,
Tooltip,
} from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client'
import { consumeOAuthReturnContext, writeOAuthReturnContext } from '@/lib/credentials/client-state'
import {
getCanonicalScopesForProvider,
getProviderIdFromServiceId,
type OAuthProvider,
} from '@/lib/oauth'
import { consumeOAuthReturnContext } from '@/lib/credentials/client-state'
import { getProviderIdFromServiceId, type OAuthProvider } from '@/lib/oauth'
import { ConnectorSelectorField } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/components/connector-selector-field'
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
import { ConnectCredentialModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/connect-credential-modal'
import { getDependsOnFields } from '@/blocks/utils'
import { CONNECTOR_REGISTRY } from '@/connectors/registry'
import type { ConnectorConfig, ConnectorConfigField } from '@/connectors/types'
import { useCreateConnector } from '@/hooks/queries/kb/connectors'
import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials'
import type { SelectorKey } from '@/hooks/selectors/types'
import { useCredentialRefreshTriggers } from '@/hooks/use-credential-refresh-triggers'

const SYNC_INTERVALS = [
{ label: 'Every hour', value: 60 },
Expand Down Expand Up @@ -69,7 +65,6 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
const [searchTerm, setSearchTerm] = useState('')

const { workspaceId } = useParams<{ workspaceId: string }>()
const { data: session } = useSession()
const { mutate: createConnector, isPending: isCreating } = useCreateConnector()

const connectorConfig = selectedType ? CONNECTOR_REGISTRY[selectedType] : null
Expand All @@ -82,10 +77,16 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
[connectorConfig]
)

const { data: credentials = [], isLoading: credentialsLoading } = useOAuthCredentials(
connectorProviderId ?? undefined,
{ enabled: Boolean(connectorConfig) && !isApiKeyMode, workspaceId }
)
const {
data: credentials = [],
isLoading: credentialsLoading,
refetch: refetchCredentials,
} = useOAuthCredentials(connectorProviderId ?? undefined, {
enabled: Boolean(connectorConfig) && !isApiKeyMode,
workspaceId,
})

useCredentialRefreshTriggers(refetchCredentials, connectorProviderId ?? '', workspaceId)

const effectiveCredentialId =
selectedCredentialId ?? (credentials.length === 1 ? credentials[0].id : null)
Expand Down Expand Up @@ -263,51 +264,9 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
)
}

const handleConnectNewAccount = useCallback(async () => {
if (!connectorConfig || !connectorProviderId || !workspaceId) return

const userName = session?.user?.name
const integrationName = connectorConfig.name
const displayName = userName ? `${userName}'s ${integrationName}` : integrationName

try {
const res = await fetch('/api/credentials/draft', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
workspaceId,
providerId: connectorProviderId,
displayName,
}),
})
if (!res.ok) {
setError('Failed to prepare credential. Please try again.')
return
}
} catch {
setError('Failed to prepare credential. Please try again.')
return
}

writeOAuthReturnContext({
origin: 'kb-connectors',
knowledgeBaseId,
displayName,
providerId: connectorProviderId,
preCount: credentials.length,
workspaceId,
requestedAt: Date.now(),
})

const handleConnectNewAccount = useCallback(() => {
setShowOAuthModal(true)
}, [
connectorConfig,
connectorProviderId,
workspaceId,
session?.user?.name,
knowledgeBaseId,
credentials.length,
])
}, [])

const filteredEntries = useMemo(() => {
const term = searchTerm.toLowerCase().trim()
Expand Down Expand Up @@ -396,40 +355,40 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
) : (
<div className='flex flex-col gap-2'>
<Label>Account</Label>
{credentialsLoading ? (
<div className='flex items-center gap-2 text-[var(--text-muted)] text-small'>
<Loader2 className='h-4 w-4 animate-spin' />
Loading credentials...
</div>
) : (
<Combobox
size='sm'
options={[
...credentials.map(
(cred): ComboboxOption => ({
label: cred.name || cred.provider,
value: cred.id,
icon: connectorConfig.icon,
})
),
{
label: 'Connect new account',
value: '__connect_new__',
icon: Plus,
onSelect: () => {
void handleConnectNewAccount()
},
<Combobox
size='sm'
options={[
...credentials.map(
(cred): ComboboxOption => ({
label: cred.name || cred.provider,
value: cred.id,
icon: connectorConfig.icon,
})
),
{
label:
credentials.length > 0
? `Connect another ${connectorConfig.name} account`
: `Connect ${connectorConfig.name} account`,
value: '__connect_new__',
icon: Plus,
onSelect: () => {
void handleConnectNewAccount()
},
]}
value={effectiveCredentialId ?? undefined}
onChange={(value) => setSelectedCredentialId(value)}
placeholder={
credentials.length === 0
? `No ${connectorConfig.name} accounts`
: 'Select account'
}
/>
)}
},
]}
value={effectiveCredentialId ?? undefined}
onChange={(value) => setSelectedCredentialId(value)}
onOpenChange={(isOpen) => {
if (isOpen) void refetchCredentials()
}}
placeholder={
credentials.length === 0
? `No ${connectorConfig.name} accounts`
: 'Select account'
}
isLoading={credentialsLoading}
/>
</div>
)}

Expand Down Expand Up @@ -590,20 +549,23 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
)}
</ModalContent>
</Modal>
{connectorConfig && connectorConfig.auth.mode === 'oauth' && connectorProviderId && (
<OAuthRequiredModal
isOpen={showOAuthModal}
onClose={() => {
consumeOAuthReturnContext()
setShowOAuthModal(false)
}}
provider={connectorProviderId}
toolName={connectorConfig.name}
requiredScopes={getCanonicalScopesForProvider(connectorProviderId)}
newScopes={[]}
serviceId={connectorConfig.auth.provider}
/>
)}
{showOAuthModal &&
connectorConfig &&
connectorConfig.auth.mode === 'oauth' &&
connectorProviderId && (
<ConnectCredentialModal
isOpen={showOAuthModal}
onClose={() => {
consumeOAuthReturnContext()
setShowOAuthModal(false)
}}
provider={connectorProviderId}
serviceId={connectorConfig.auth.provider}
workspaceId={workspaceId}
knowledgeBaseId={knowledgeBaseId}
credentialCount={credentials.length}
/>
)}
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
} from '@/lib/oauth'
import { getMissingRequiredScopes } from '@/lib/oauth/utils'
import { EditConnectorModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal'
import { ConnectCredentialModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/connect-credential-modal'
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
import { CONNECTOR_REGISTRY } from '@/connectors/registry'
import type { ConnectorData, SyncLogData } from '@/hooks/queries/kb/connectors'
Expand All @@ -46,6 +47,7 @@ import {
useUpdateConnector,
} from '@/hooks/queries/kb/connectors'
import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials'
import { useCredentialRefreshTriggers } from '@/hooks/use-credential-refresh-triggers'

const logger = createLogger('ConnectorsSection')

Expand Down Expand Up @@ -328,11 +330,16 @@ function ConnectorCard({
const requiredScopes =
connectorDef?.auth.mode === 'oauth' ? (connectorDef.auth.requiredScopes ?? []) : []

const { data: credentials } = useOAuthCredentials(providerId, { workspaceId })
const { data: credentials, refetch: refetchCredentials } = useOAuthCredentials(providerId, {
workspaceId,
})

useCredentialRefreshTriggers(refetchCredentials, providerId ?? '', workspaceId)

const missingScopes = useMemo(() => {
if (!credentials || !connector.credentialId) return []
const credential = credentials.find((c) => c.id === connector.credentialId)
if (!credential) return []
return getMissingRequiredScopes(credential, requiredScopes)
}, [credentials, connector.credentialId, requiredScopes])

Expand Down Expand Up @@ -484,15 +491,17 @@ function ConnectorCard({
<Button
variant='active'
onClick={() => {
writeOAuthReturnContext({
origin: 'kb-connectors',
knowledgeBaseId,
displayName: connectorDef?.name ?? connector.connectorType,
providerId: providerId!,
preCount: credentials?.length ?? 0,
workspaceId,
requestedAt: Date.now(),
})
if (connector.credentialId) {
writeOAuthReturnContext({
origin: 'kb-connectors',
knowledgeBaseId,
displayName: connectorDef?.name ?? connector.connectorType,
providerId: providerId!,
preCount: credentials?.length ?? 0,
workspaceId,
requestedAt: Date.now(),
})
}
setShowOAuthModal(true)
}}
className='w-full px-2 py-1 font-medium text-caption'
Expand All @@ -510,7 +519,22 @@ function ConnectorCard({
</div>
)}

{showOAuthModal && serviceId && providerId && (
{showOAuthModal && serviceId && providerId && !connector.credentialId && (
<ConnectCredentialModal
isOpen={showOAuthModal}
onClose={() => {
consumeOAuthReturnContext()
setShowOAuthModal(false)
}}
provider={providerId as OAuthProvider}
serviceId={serviceId}
workspaceId={workspaceId}
knowledgeBaseId={knowledgeBaseId}
credentialCount={credentials?.length ?? 0}
/>
)}

{showOAuthModal && serviceId && providerId && connector.credentialId && (
<OAuthRequiredModal
isOpen={showOAuthModal}
onClose={() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ModalHeader,
} from '@/components/emcn'
import { client } from '@/lib/auth/auth-client'
import type { OAuthReturnContext } from '@/lib/credentials/client-state'
import { writeOAuthReturnContext } from '@/lib/credentials/client-state'
import {
getCanonicalScopesForProvider,
Expand All @@ -27,24 +28,30 @@ import { useCreateCredentialDraft } from '@/hooks/queries/credentials'

const logger = createLogger('ConnectCredentialModal')

export interface ConnectCredentialModalProps {
interface ConnectCredentialModalBaseProps {
isOpen: boolean
onClose: () => void
provider: OAuthProvider
serviceId: string
workspaceId: string
workflowId: string
/** Number of existing credentials for this provider — used to detect a successful new connection. */
credentialCount: number
}

export type ConnectCredentialModalProps = ConnectCredentialModalBaseProps &
(
| { workflowId: string; knowledgeBaseId?: never }
| { workflowId?: never; knowledgeBaseId: string }
)

export function ConnectCredentialModal({
isOpen,
onClose,
provider,
serviceId,
workspaceId,
workflowId,
knowledgeBaseId,
credentialCount,
}: ConnectCredentialModalProps) {
const [displayName, setDisplayName] = useState('')
Expand Down Expand Up @@ -97,15 +104,19 @@ export function ConnectCredentialModal({
try {
await createDraft.mutateAsync({ workspaceId, providerId, displayName: trimmedName })

writeOAuthReturnContext({
origin: 'workflow',
workflowId,
const baseContext = {
displayName: trimmedName,
providerId,
preCount: credentialCount,
workspaceId,
requestedAt: Date.now(),
})
}

const returnContext: OAuthReturnContext = knowledgeBaseId
? { ...baseContext, origin: 'kb-connectors' as const, knowledgeBaseId }
: { ...baseContext, origin: 'workflow' as const, workflowId: workflowId! }

writeOAuthReturnContext(returnContext)

if (providerId === 'trello') {
window.location.href = '/api/auth/trello/authorize'
Expand Down
Loading
Loading