datasource oauth

This commit is contained in:
zxhlyh 2025-07-21 17:40:19 +08:00
parent 039a053027
commit caa2de3344
23 changed files with 701 additions and 272 deletions

View File

@ -1,15 +1,32 @@
import { memo } from 'react'
import {
memo,
useCallback,
} from 'react'
import { useTranslation } from 'react-i18next'
import Item from './item'
import Configure from './configure'
import type { DataSourceAuth } from './types'
import type {
DataSourceAuth,
DataSourceCredential,
} from './types'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import { AuthCategory } from '@/app/components/plugins/plugin-auth/types'
import {
ApiKeyModal,
usePluginAuthAction,
} from '@/app/components/plugins/plugin-auth'
import { useDataSourceAuthUpdate } from './hooks'
import Confirm from '@/app/components/base/confirm'
type CardProps = {
item: DataSourceAuth
disabled?: boolean
}
const Card = ({
item,
disabled,
}: CardProps) => {
const { t } = useTranslation()
const renderI18nObject = useRenderI18nObject()
const {
icon,
@ -17,7 +34,56 @@ const Card = ({
author,
provider,
credentials_list,
credential_schema,
} = item
const pluginPayload = {
category: AuthCategory.datasource,
provider: item.name,
}
const { handleAuthUpdate } = useDataSourceAuthUpdate()
const {
deleteCredentialId,
doingAction,
handleConfirm,
handleEdit,
handleRemove,
handleRename,
handleSetDefault,
editValues,
setEditValues,
openConfirm,
closeConfirm,
pendingOperationCredentialId,
} = usePluginAuthAction(pluginPayload, handleAuthUpdate)
const handleAction = useCallback((
action: string,
credentialItem: DataSourceCredential,
renamePayload?: Record<string, any>,
) => {
if (action === 'edit') {
handleEdit(
credentialItem.id,
{
...credentialItem.credential,
__name__: credentialItem.name,
__credential_id__: credentialItem.id,
},
)
}
if (action === 'delete')
openConfirm(credentialItem.id)
if (action === 'setDefault')
handleSetDefault(credentialItem.id)
if (action === 'rename')
handleRename(renamePayload as any)
}, [
openConfirm,
handleEdit,
handleSetDefault,
handleRename,
])
return (
<div className='rounded-xl bg-background-section-burn'>
@ -36,7 +102,11 @@ const Card = ({
{provider}
</div>
</div>
<Configure />
<Configure
pluginPayload={pluginPayload}
item={item}
onUpdate={handleAuthUpdate}
/>
</div>
<div className='system-xs-medium flex h-4 items-center pl-3 text-text-tertiary'>
Connected workspace
@ -45,9 +115,15 @@ const Card = ({
{
!!credentials_list.length && (
<div className='space-y-1 p-3 pt-2'>
<Item />
<Item />
<Item />
{
credentials_list.map(credentialItem => (
<Item
key={credentialItem.id}
credentialItem={credentialItem}
onAction={handleAction}
/>
))
}
</div>
)
}
@ -60,6 +136,33 @@ const Card = ({
</div>
)
}
{
deleteCredentialId && (
<Confirm
isShow
title={t('datasetDocuments.list.delete.title')}
isDisabled={doingAction}
onCancel={closeConfirm}
onConfirm={handleConfirm}
/>
)
}
{
!!editValues && (
<ApiKeyModal
pluginPayload={pluginPayload}
onClose={() => {
setEditValues(null)
pendingOperationCredentialId.current = null
}}
onUpdate={handleAuthUpdate}
formSchemas={credential_schema}
editValues={editValues}
onRemove={handleRemove}
disabled={disabled || doingAction}
/>
)
}
</div>
)
}

View File

@ -1,16 +1,58 @@
import { memo } from 'react'
import {
memo,
useMemo,
} from 'react'
import {
RiAddLine,
RiEqualizer2Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import {
AddApiKeyButton,
AddOAuthButton,
} from '@/app/components/plugins/plugin-auth'
import type { DataSourceAuth } from './types'
import type {
AddApiKeyButtonProps,
AddOAuthButtonProps,
PluginPayload,
} from '@/app/components/plugins/plugin-auth/types'
type ConfigureProps = {
item: DataSourceAuth
pluginPayload: PluginPayload
onUpdate?: () => void
disabled?: boolean
}
const Configure = ({
item,
pluginPayload,
onUpdate,
disabled,
}: ConfigureProps) => {
const { t } = useTranslation()
const canApiKey = item.credential_schema?.length
const oAuthData = item.oauth_schema || {}
const canOAuth = oAuthData.client_schema?.length
const oAuthButtonProps: AddOAuthButtonProps = useMemo(() => {
return {
buttonText: t('plugin.auth.addOAuth'),
pluginPayload,
}
}, [pluginPayload, t])
const apiKeyButtonProps: AddApiKeyButtonProps = useMemo(() => {
return {
pluginPayload,
buttonText: t('plugin.auth.addApi'),
}
}, [pluginPayload, t])
const Configure = () => {
return (
<>
<PortalToFollowElem
@ -25,34 +67,46 @@ const Configure = () => {
variant='secondary-accent'
>
<RiAddLine className='h-4 w-4' />
Configure
{t('common.dataSource.configure')}
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[61]'>
<div className='w-[240px] space-y-1.5 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-lg'>
<Button
variant='primary'
className='w-full px-0'
>
<div className='grow'>
use oauth
</div>
<div className='h-4 w-[1px] bg-text-primary-on-surface opacity-[0.15]'></div>
<div className='flex h-8 w-8 shrink-0 items-center justify-center'>
<RiEqualizer2Line className='h-4 w-4' />
</div>
</Button>
<div className='system-2xs-medium-uppercase flex h-4 items-center p-2 text-text-quaternary'>
<div className='mr-2 h-[1px] grow bg-gradient-to-l from-[rgba(16,24,40,0.08)]' />
OR
<div className='ml-2 h-[1px] grow bg-gradient-to-r from-[rgba(16,24,40,0.08)]' />
</div>
<Button
className='w-full'
variant='secondary-accent'
>
Use API Key
</Button>
{
canOAuth && (
<AddOAuthButton
{...oAuthButtonProps}
onUpdate={onUpdate}
oAuthData={{
schema: oAuthData.client_schema || [],
is_oauth_custom_client_enabled: oAuthData.is_oauth_custom_client_enabled,
is_system_oauth_params_exists: oAuthData.is_system_oauth_params_exists,
client_params: oAuthData.oauth_custom_client_params,
redirect_uri: oAuthData.redirect_uri,
}}
disabled={disabled}
/>
)
}
{
canApiKey && canOAuth && (
<div className='system-2xs-medium-uppercase flex h-4 items-center p-2 text-text-quaternary'>
<div className='mr-2 h-[1px] grow bg-gradient-to-l from-[rgba(16,24,40,0.08)]' />
OR
<div className='ml-2 h-[1px] grow bg-gradient-to-r from-[rgba(16,24,40,0.08)]' />
</div>
)
}
{
canApiKey && (
<AddApiKeyButton
{...apiKeyButtonProps}
formSchemas={item.credential_schema}
onUpdate={onUpdate}
disabled={disabled}
/>
)
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>

View File

@ -0,0 +1,2 @@
export * from './use-marketplace-all-plugins'
export * from './use-data-source-auth-update'

View File

@ -0,0 +1,16 @@
import { useCallback } from 'react'
import { useInvalidDataSourceListAuth } from '@/service/use-datasource'
import { useInvalidDataSourceList } from '@/service/use-pipeline'
export const useDataSourceAuthUpdate = () => {
const invalidateDataSourceListAuth = useInvalidDataSourceListAuth()
const invalidateDataSourceList = useInvalidDataSourceList()
const handleAuthUpdate = useCallback(() => {
invalidateDataSourceListAuth()
invalidateDataSourceList()
}, [invalidateDataSourceListAuth, invalidateDataSourceList])
return {
handleAuthUpdate,
}
}

View File

@ -1,14 +1,79 @@
import { memo } from 'react'
import {
memo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import Indicator from '@/app/components/header/indicator'
import Operator from './operator'
import type {
DataSourceCredential,
} from './types'
import Input from '@/app/components/base/input'
import Button from '@/app/components/base/button'
type ItemProps = {
credentialItem: DataSourceCredential
onAction: (action: string, credentialItem: DataSourceCredential, renamePayload?: Record<string, any>) => void
}
const Item = ({
credentialItem,
onAction,
}: ItemProps) => {
const { t } = useTranslation()
const [renaming, setRenaming] = useState(false)
const [renameValue, setRenameValue] = useState(credentialItem.name)
const Item = () => {
return (
<div className='flex h-10 items-center rounded-lg bg-components-panel-on-panel-item-bg pl-3 pr-1'>
<div className='mr-2 h-5 w-5 shrink-0'></div>
<div className='system-sm-medium grow text-text-secondary'>
Evans Notion
</div>
{/* <div className='mr-2 h-5 w-5 shrink-0'></div> */}
{
renaming && (
<div className='flex w-full items-center space-x-1'>
<Input
wrapperClassName='grow rounded-[6px]'
className='h-6'
value={renameValue}
onChange={e => setRenameValue(e.target.value)}
placeholder={t('common.placeholder.input')}
onClick={e => e.stopPropagation()}
/>
<Button
size='small'
variant='primary'
onClick={(e) => {
e.stopPropagation()
onAction?.(
'rename',
credentialItem,
{
credential_id: credentialItem.id,
name: renameValue,
},
)
setRenaming(false)
}}
>
{t('common.operation.save')}
</Button>
<Button
size='small'
onClick={(e) => {
e.stopPropagation()
setRenaming(false)
}}
>
{t('common.operation.cancel')}
</Button>
</div>
)
}
{
!renaming && (
<div className='system-sm-medium grow text-text-secondary'>
{credentialItem.name}
</div>
)
}
<div className='flex shrink-0 items-center'>
<div className='mr-1 flex h-3 w-3 items-center justify-center'>
<Indicator color='green' />
@ -18,7 +83,10 @@ const Item = () => {
</div>
</div>
<div className='ml-3 mr-2 h-3 w-[1px] bg-divider-regular'></div>
<Operator />
<Operator
credentialItem={credentialItem}
onAction={onAction}
/>
</div>
)
}

View File

@ -3,40 +3,94 @@ import {
useCallback,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiDeleteBinLine,
RiEditLine,
RiEqualizer2Line,
RiHome9Line,
RiLoopLeftLine,
RiStickyNoteAddLine,
} from '@remixicon/react'
import Dropdown from '@/app/components/base/dropdown'
import type { Item } from '@/app/components/base/dropdown'
import type {
DataSourceCredential,
} from './types'
import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types'
const Operator = () => {
type OperatorProps = {
credentialItem: DataSourceCredential
onAction: (action: string, credentialItem: DataSourceCredential) => void
onRename?: () => void
}
const Operator = ({
credentialItem,
onAction,
onRename,
}: OperatorProps) => {
const { t } = useTranslation()
const {
type,
} = credentialItem
const items = useMemo(() => {
return [
const commonItems = [
{
value: 'change',
value: 'setDefault',
text: (
<div className='flex'>
<RiStickyNoteAddLine className='mr-2 h-4 w-4 text-text-tertiary' />
<div>
<div className='system-sm-semibold mb-1 text-text-secondary'>Change authorized pages</div>
<div className='system-xs-regular text-text-tertiary'>18 Pages authorized</div>
</div>
<div className='flex items-center'>
<RiHome9Line className='mr-2 h-4 w-4 text-text-tertiary' />
<div className='system-sm-semibold text-text-secondary'>{t('plugin.auth.setDefault')}</div>
</div>
),
},
{
value: 'sync',
value: 'rename',
text: (
<div className='flex items-center'>
<RiLoopLeftLine className='mr-2 h-4 w-4 text-text-tertiary' />
<div className='system-sm-semibold text-text-secondary'>Sync</div>
<RiEditLine className='mr-2 h-4 w-4 text-text-tertiary' />
<div className='system-sm-semibold text-text-secondary'>{t('common.operation.rename')}</div>
</div>
),
},
{
value: 'edit',
text: (
<div className='flex items-center'>
<RiEqualizer2Line className='mr-2 h-4 w-4 text-text-tertiary' />
<div className='system-sm-semibold text-text-secondary'>{t('common.operation.edit')}</div>
</div>
),
},
]
}, [])
if (type === CredentialTypeEnum.OAUTH2) {
const oAuthItems = [
{
value: 'change',
text: (
<div className='flex'>
<RiStickyNoteAddLine className='mr-2 h-4 w-4 text-text-tertiary' />
<div>
<div className='system-sm-semibold mb-1 text-text-secondary'>{t('common.dataSource.notion.changeAuthorizedPages')}</div>
<div className='system-xs-regular text-text-tertiary'>18 {t('common.dataSource.notion.pagesAuthorized')}</div>
</div>
</div>
),
},
{
value: 'sync',
text: (
<div className='flex items-center'>
<RiLoopLeftLine className='mr-2 h-4 w-4 text-text-tertiary' />
<div className='system-sm-semibold text-text-secondary'>{t('common.dataSource.notion.sync')}</div>
</div>
),
},
]
commonItems.push(...oAuthItems)
}
return commonItems
}, [t, type])
const secondItems = useMemo(() => {
return [
@ -51,23 +105,29 @@ const Operator = () => {
},
]
}, [])
const handleSelect = useCallback((item: Item) => {
console.log('Selected item:', item)
}, [])
if (item.value === 'rename') {
onRename?.()
return
}
onAction(
item.value as string,
credentialItem,
)
}, [onAction, credentialItem, onRename])
return (
<Dropdown
items={items}
secondItems={secondItems}
onSelect={handleSelect}
popupClassName='z-[61]'
triggerProps={{
size: 'l',
}}
itemClassName='py-2 h-auto hover:bg-state-base-hover'
secondItemClassName='py-2 h-auto hover:bg-state-base-hover'
/>
<Dropdown
items={items}
secondItems={secondItems}
onSelect={handleSelect}
popupClassName='z-[61]'
triggerProps={{
size: 'l',
}}
itemClassName='py-2 h-auto hover:bg-state-base-hover'
secondItemClassName='py-2 h-auto hover:bg-state-base-hover'
/>
)
}

View File

@ -1,6 +1,12 @@
import type {
FormSchema,
TypeWithI18N,
} from '@/app/components/base/form/types'
import type { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types'
export type DataSourceCredential = {
credential: Record<string, any>
type: string
type: CredentialTypeEnum
name: string
id: string
}
@ -9,14 +15,18 @@ export type DataSourceAuth = {
provider: string
plugin_id: string
plugin_unique_identifier: string
icon: any
icon: string
name: string
label: any
description: any
credential_schema?: any[]
label: TypeWithI18N
description: TypeWithI18N
credential_schema?: FormSchema[]
oauth_schema?: {
client_schema?: any[]
credentials_schema?: any[]
client_schema?: FormSchema[]
credentials_schema?: FormSchema[]
is_oauth_custom_client_enabled?: boolean
is_system_oauth_params_exists?: boolean
oauth_custom_client_params?: Record<string, any>
redirect_uri?: string
}
credentials_list: DataSourceCredential[]
}

View File

@ -6,6 +6,7 @@ import Button from '@/app/components/base/button'
import type { ButtonProps } from '@/app/components/base/button'
import ApiKeyModal from './api-key-modal'
import type { PluginPayload } from '../types'
import type { FormSchema } from '@/app/components/base/form/types'
export type AddApiKeyButtonProps = {
pluginPayload: PluginPayload
@ -13,13 +14,15 @@ export type AddApiKeyButtonProps = {
buttonText?: string
disabled?: boolean
onUpdate?: () => void
formSchemas?: FormSchema[]
}
const AddApiKeyButton = ({
pluginPayload,
buttonVariant = 'secondary-accent',
buttonText = 'use api key',
buttonText = 'Use Api Key',
disabled,
onUpdate,
formSchemas = [],
}: AddApiKeyButtonProps) => {
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false)
@ -39,6 +42,7 @@ const AddApiKeyButton = ({
pluginPayload={pluginPayload}
onClose={() => setIsApiKeyModalOpen(false)}
onUpdate={onUpdate}
formSchemas={formSchemas}
/>
)
}

View File

@ -36,6 +36,13 @@ export type AddOAuthButtonProps = {
dividerClassName?: string
disabled?: boolean
onUpdate?: () => void
oAuthData?: {
schema?: FormSchema[]
is_oauth_custom_client_enabled?: boolean
is_system_oauth_params_exists?: boolean
client_params?: Record<string, any>
redirect_uri?: string
}
}
const AddOAuthButton = ({
pluginPayload,
@ -47,19 +54,26 @@ const AddOAuthButton = ({
dividerClassName,
disabled,
onUpdate,
oAuthData,
}: AddOAuthButtonProps) => {
const { t } = useTranslation()
const renderI18nObject = useRenderI18nObject()
const [isOAuthSettingsOpen, setIsOAuthSettingsOpen] = useState(false)
const { mutateAsync: getPluginOAuthUrl } = useGetPluginOAuthUrlHook(pluginPayload)
const { data, isLoading } = useGetPluginOAuthClientSchemaHook(pluginPayload)
const mergedOAuthData = useMemo(() => {
if (oAuthData)
return oAuthData
return data
}, [oAuthData, data])
const {
schema = [],
is_oauth_custom_client_enabled,
is_system_oauth_params_exists,
client_params,
redirect_uri,
} = data || {}
} = mergedOAuthData as any
const isConfigured = is_system_oauth_params_exists || is_oauth_custom_client_enabled
const handleOAuth = useCallback(async () => {
const { authorization_url } = await getPluginOAuthUrl()
@ -112,7 +126,7 @@ const AddOAuthButton = ({
)
}, [t, redirect_uri, renderI18nObject])
const memorizedSchemas = useMemo(() => {
const result: FormSchema[] = schema.map((item, index) => {
const result: FormSchema[] = (schema as FormSchema[]).map((item, index) => {
return {
...item,
label: index === 0 ? renderCustomLabel(item) : item.label,

View File

@ -11,7 +11,10 @@ import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import Modal from '@/app/components/base/modal/modal'
import { CredentialTypeEnum } from '../types'
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
import type { FormRefObject } from '@/app/components/base/form/types'
import type {
FormRefObject,
FormSchema,
} from '@/app/components/base/form/types'
import { FormTypeEnum } from '@/app/components/base/form/types'
import { useToastContext } from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading'
@ -30,6 +33,7 @@ export type ApiKeyModalProps = {
onRemove?: () => void
disabled?: boolean
onUpdate?: () => void
formSchemas?: FormSchema[]
}
const ApiKeyModal = ({
pluginPayload,
@ -38,6 +42,7 @@ const ApiKeyModal = ({
onRemove,
disabled,
onUpdate,
formSchemas: formSchemasFromProps = [],
}: ApiKeyModalProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
@ -48,6 +53,12 @@ const ApiKeyModal = ({
setDoingAction(value)
}, [])
const { data = [], isLoading } = useGetPluginCredentialSchemaHook(pluginPayload, CredentialTypeEnum.API_KEY)
const mergedData = useMemo(() => {
if (formSchemasFromProps?.length)
return formSchemasFromProps
return data
}, [formSchemasFromProps, data])
const formSchemas = useMemo(() => {
return [
{
@ -56,9 +67,9 @@ const ApiKeyModal = ({
label: t('plugin.auth.authorizationName'),
required: false,
},
...data,
...mergedData,
]
}, [data, t])
}, [mergedData, t])
const defaultValues = formSchemas.reduce((acc, schema) => {
if (schema.default)
acc[schema.name] = schema.default
@ -165,7 +176,7 @@ const ApiKeyModal = ({
)
}
{
!isLoading && !!data.length && (
!isLoading && !!mergedData.length && (
<AuthForm
ref={formRef}
formSchemas={formSchemas}

View File

@ -0,0 +1,43 @@
import {
memo,
} from 'react'
import { useTranslation } from 'react-i18next'
import { RiEqualizer2Line } from '@remixicon/react'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
type AuthorizedInDataSourceNodeProps = {
authorizationsNum: number
onJumpToDataSourcePage: () => void
}
const AuthorizedInDataSourceNode = ({
authorizationsNum,
onJumpToDataSourcePage,
}: AuthorizedInDataSourceNodeProps) => {
const { t } = useTranslation()
return (
<Button
size='small'
onClick={onJumpToDataSourcePage}
>
<Indicator
className='mr-1.5'
color='green'
/>
{
authorizationsNum > 1
? t('plugin.auth.authorizations')
: t('plugin.auth.authorization')
}
<RiEqualizer2Line
className={cn(
'h-3.5 w-3.5 text-components-button-ghost-text',
)}
/>
</Button>
)
}
export default memo(AuthorizedInDataSourceNode)

View File

@ -24,6 +24,22 @@ export const useGetApi = ({ category = AuthCategory.tool, provider }: PluginPayl
}
}
if (category === AuthCategory.datasource) {
return {
getCredentialInfo: '',
setDefaultCredential: `/auth/plugin/datasource/${provider}/default`,
getCredentials: `/auth/plugin/datasource/${provider}`,
addCredential: `/auth/plugin/datasource/${provider}`,
updateCredential: `/auth/plugin/datasource/${provider}/update`,
deleteCredential: `/auth/plugin/datasource/${provider}/delete`,
getCredentialSchema: () => '',
getOauthUrl: `/oauth/plugin/${provider}/datasource/get-authorization-url`,
getOauthClientSchema: '',
setCustomOauthClient: `/auth/plugin/datasource/${provider}/custom-client`,
deleteCustomOAuthClient: `/auth/plugin/datasource/${provider}/custom-client`,
}
}
return {
getCredentialInfo: '',
setDefaultCredential: '',

View File

@ -0,0 +1,124 @@
import {
useCallback,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useToastContext } from '@/app/components/base/toast'
import type { PluginPayload } from '../types'
import {
useDeletePluginCredentialHook,
useSetPluginDefaultCredentialHook,
useUpdatePluginCredentialHook,
} from '../hooks/use-credential'
export const usePluginAuthAction = (
pluginPayload: PluginPayload,
onUpdate?: () => void,
) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const pendingOperationCredentialId = useRef<string | null>(null)
const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
const { mutateAsync: deletePluginCredential } = useDeletePluginCredentialHook(pluginPayload)
const openConfirm = useCallback((credentialId?: string) => {
if (credentialId)
pendingOperationCredentialId.current = credentialId
setDeleteCredentialId(pendingOperationCredentialId.current)
}, [])
const closeConfirm = useCallback(() => {
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
}, [])
const [doingAction, setDoingAction] = useState(false)
const doingActionRef = useRef(doingAction)
const handleSetDoingAction = useCallback((doing: boolean) => {
doingActionRef.current = doing
setDoingAction(doing)
}, [])
const handleConfirm = useCallback(async () => {
if (doingActionRef.current)
return
if (!pendingOperationCredentialId.current) {
setDeleteCredentialId(null)
return
}
try {
handleSetDoingAction(true)
await deletePluginCredential({ credential_id: pendingOperationCredentialId.current })
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
}
finally {
handleSetDoingAction(false)
}
}, [deletePluginCredential, onUpdate, notify, t, handleSetDoingAction])
const [editValues, setEditValues] = useState<Record<string, any> | null>(null)
const handleEdit = useCallback((id: string, values: Record<string, any>) => {
pendingOperationCredentialId.current = id
setEditValues(values)
}, [])
const handleRemove = useCallback(() => {
setDeleteCredentialId(pendingOperationCredentialId.current)
}, [])
const { mutateAsync: setPluginDefaultCredential } = useSetPluginDefaultCredentialHook(pluginPayload)
const handleSetDefault = useCallback(async (id: string) => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
await setPluginDefaultCredential(id)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
}
finally {
handleSetDoingAction(false)
}
}, [setPluginDefaultCredential, onUpdate, notify, t, handleSetDoingAction])
const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
const handleRename = useCallback(async (payload: {
credential_id: string
name: string
}) => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
await updatePluginCredential(payload)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
}
finally {
handleSetDoingAction(false)
}
}, [updatePluginCredential, notify, t, handleSetDoingAction, onUpdate])
return {
doingAction,
handleSetDoingAction,
openConfirm,
closeConfirm,
deleteCredentialId,
setDeleteCredentialId,
handleConfirm,
editValues,
setEditValues,
handleEdit,
handleRemove,
handleSetDefault,
handleRename,
pendingOperationCredentialId,
}
}

View File

@ -3,4 +3,10 @@ export { default as Authorized } from './authorized'
export { default as AuthorizedInNode } from './authorized-in-node'
export { default as PluginAuthInAgent } from './plugin-auth-in-agent'
export { usePluginAuth } from './hooks/use-plugin-auth'
export { default as PluginAuthInDataSourceNode } from './plugin-auth-in-datasource-node'
export { default as AuthorizedInDataSourceNode } from './authorized-in-data-source-node'
export { default as AddOAuthButton } from './authorize/add-oauth-button'
export { default as AddApiKeyButton } from './authorize/add-api-key-button'
export { default as ApiKeyModal } from './authorize/api-key-modal'
export * from './hooks/use-plugin-auth-action'
export * from './types'

View File

@ -0,0 +1,39 @@
import { memo } from 'react'
import type { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { RiAddLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
type PluginAuthInDataSourceNodeProps = {
children?: ReactNode
isAuthorized?: boolean
onJumpToDataSourcePage: () => void
}
const PluginAuthInDataSourceNode = ({
children,
isAuthorized,
onJumpToDataSourcePage,
}: PluginAuthInDataSourceNodeProps) => {
const { t } = useTranslation()
return (
<>
{
!isAuthorized && (
<div className='px-4 pb-2'>
<Button
className='w-full'
variant='primary'
onClick={onJumpToDataSourcePage}
>
<RiAddLine className='mr-1 h-4 w-4' />
{t('common.integrations.connect')}
</Button>
</div>
)
}
{isAuthorized && children}
</>
)
}
export default memo(PluginAuthInDataSourceNode)

View File

@ -1,3 +1,6 @@
export type { AddApiKeyButtonProps } from './authorize/add-api-key-button'
export type { AddOAuthButtonProps } from './authorize/add-oauth-button'
export enum AuthCategory {
tool = 'tool',
datasource = 'datasource',

View File

@ -62,11 +62,15 @@ import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevice
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import { FlowType } from '@/types/common'
import {
AuthorizedInDataSourceNode,
AuthorizedInNode,
PluginAuth,
PluginAuthInDataSourceNode,
} from '@/app/components/plugins/plugin-auth'
import { AuthCategory } from '@/app/components/plugins/plugin-auth'
import { canFindTool } from '@/utils'
import { DataSourceClassification } from '@/app/components/workflow/nodes/data-source/types'
import { useModalContext } from '@/context/modal-context'
type BasePanelProps = {
children: ReactNode
@ -240,6 +244,11 @@ const BasePanel: FC<BasePanelProps> = ({
const showPluginAuth = useMemo(() => {
return data.type === BlockEnum.Tool && currCollection?.allow_delete
}, [currCollection, data.type])
const dataSourceList = useStore(s => s.dataSourceList)
const currentDataSource = useMemo(() => {
if (data.type === BlockEnum.DataSource && data.provider_type !== DataSourceClassification.localFile)
return dataSourceList?.find(item => item.plugin_id === data.plugin_id)
}, [dataSourceList, data.plugin_id, data.type, data.provider_type])
const handleAuthorizationItemClick = useCallback((credential_id: string) => {
handleNodeDataUpdateWithSyncDraft({
id,
@ -248,6 +257,10 @@ const BasePanel: FC<BasePanelProps> = ({
},
})
}, [handleNodeDataUpdateWithSyncDraft, id])
const { setShowAccountSettingModal } = useModalContext()
const handleJumpToDataSourcePage = useCallback(() => {
setShowAccountSettingModal({ payload: 'data-source' })
}, [setShowAccountSettingModal])
if (logParams.showSpecialResultPanel) {
return (
@ -413,7 +426,26 @@ const BasePanel: FC<BasePanelProps> = ({
)
}
{
!showPluginAuth && (
!!currentDataSource && (
<PluginAuthInDataSourceNode
onJumpToDataSourcePage={handleJumpToDataSourcePage}
isAuthorized={currentDataSource.is_authorized}
>
<div className='flex items-center justify-between pl-4 pr-3'>
<Tab
value={tabType}
onChange={setTabType}
/>
<AuthorizedInDataSourceNode
onJumpToDataSourcePage={handleJumpToDataSourcePage}
authorizationsNum={3}
/>
</div>
</PluginAuthInDataSourceNode>
)
}
{
!showPluginAuth && !currentDataSource && (
<div className='flex items-center justify-between pl-4 pr-3'>
<Tab
value={tabType}

View File

@ -1,137 +0,0 @@
'use client'
import type { FC } from 'react'
import {
memo,
useCallback,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import cn from '@/utils/classnames'
import Drawer from '@/app/components/base/drawer-plus'
import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { noop } from 'lodash-es'
import { useDataSourceCredentials } from '@/service/use-pipeline'
import type { ToolCredential } from '@/app/components/tools/types'
type Props = {
dataSourceItem: any
onCancel: () => void
onSaved: (value: Record<string, any>) => void
isHideRemoveBtn?: boolean
onRemove?: () => void
isSaving?: boolean
}
const ConfigCredential: FC<Props> = ({
dataSourceItem,
onCancel,
onSaved,
isHideRemoveBtn,
onRemove = noop,
isSaving,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const {
provider,
plugin_id,
credentialsSchema = [],
is_authorized,
} = dataSourceItem
const transformedCredentialsSchema = useMemo(() => {
return toolCredentialToFormSchemas(credentialsSchema)
}, [credentialsSchema])
const [isLoading, setIsLoading] = useState(false)
const [tempCredential, setTempCredential] = useState<any>({})
const handleUpdateCredentials = useCallback((credentialValue: ToolCredential[]) => {
const defaultCredentials = addDefaultValue(credentialValue, transformedCredentialsSchema)
setTempCredential(defaultCredentials)
}, [transformedCredentialsSchema])
useDataSourceCredentials(provider, plugin_id, handleUpdateCredentials)
const handleSave = async () => {
for (const field of transformedCredentialsSchema) {
if (field.required && !tempCredential[field.name]) {
Toast.notify({ type: 'error', message: t('common.errorMsg.fieldRequired', { field: field.label[language] || field.label.en_US }) })
return
}
}
setIsLoading(true)
try {
await onSaved(tempCredential)
setIsLoading(false)
}
finally {
setIsLoading(false)
}
}
return (
<Drawer
isShow
onHide={onCancel}
title={t('tools.auth.setupModalTitle') as string}
titleDescription={t('tools.auth.setupModalTitleDescription') as string}
panelClassName='mt-[64px] mb-2 !w-[420px] border-components-panel-border'
maxWidthClassName='!max-w-[420px]'
height='calc(100vh - 64px)'
contentClassName='!bg-components-panel-bg'
headerClassName='!border-b-divider-subtle'
body={
<div className='h-full px-6 py-3'>
{!transformedCredentialsSchema.length
? <Loading type='app' />
: (
<>
<Form
value={tempCredential}
onChange={(v) => {
setTempCredential(v)
}}
formSchemas={transformedCredentialsSchema as any}
isEditMode={true}
showOnVariableMap={{}}
validating={false}
inputClassName='!bg-components-input-bg-normal'
fieldMoreInfo={item => item.url
? (<a
href={item.url}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-text-accent'
>
{t('tools.howToGet')}
<LinkExternal02 className='ml-1 h-3 w-3' />
</a>)
: null}
/>
<div className={cn((is_authorized && !isHideRemoveBtn) ? 'justify-between' : 'justify-end', 'mt-2 flex ')} >
{
(is_authorized && !isHideRemoveBtn) && (
<Button onClick={onRemove}>{t('common.operation.remove')}</Button>
)
}
< div className='flex space-x-2'>
<Button onClick={onCancel}>{t('common.operation.cancel')}</Button>
<Button loading={isLoading || isSaving} disabled={isLoading || isSaving} variant='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
</div>
</div>
</>
)
}
</div >
}
isShowMask={true}
clickOutsideNotOpen={false}
/>
)
}
export default memo(ConfigCredential)

View File

@ -6,13 +6,11 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { memo } from 'react'
import { useBoolean } from 'ahooks'
import type { DataSourceNodeType } from './types'
import { DataSourceClassification } from './types'
import type { NodePanelProps } from '@/app/components/workflow/types'
import {
BoxGroupField,
Group,
} from '@/app/components/workflow/nodes/_base/components/layout'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import TagInput from '@/app/components/base/tag-input'
@ -26,18 +24,13 @@ import {
WEBSITE_CRAWL_OUTPUT,
} from './constants'
import { useStore } from '@/app/components/workflow/store'
import Button from '@/app/components/base/button'
import ConfigCredential from './components/config-credential'
import InputVarList from '@/app/components/workflow/nodes/tool/components/input-var-list'
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import type { Var } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
import { useToastContext } from '@/app/components/base/toast'
import { useUpdateDataSourceCredentials } from '@/service/use-pipeline'
const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const { nodesReadOnly } = useNodesReadOnly()
const dataSourceList = useStore(s => s.dataSourceList)
const {
@ -55,11 +48,6 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
const isOnlineDocument = provider_type === DataSourceClassification.onlineDocument
const isOnlineDrive = provider_type === DataSourceClassification.onlineDrive
const currentDataSource = dataSourceList?.find(ds => ds.plugin_id === plugin_id)
const isAuthorized = !!currentDataSource?.is_authorized
const [showAuthModal, {
setTrue: openAuthModal,
setFalse: hideAuthModal,
}] = useBoolean(false)
const currentDataSourceItem: any = currentDataSource?.tools.find(tool => tool.name === data.datasource_name)
const formSchemas = useMemo(() => {
return currentDataSourceItem ? toolParametersToFormSchemas(currentDataSourceItem.parameters) : []
@ -77,40 +65,10 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
return varPayload.type !== VarType.arrayFile
}, [currVarType])
const { mutateAsync } = useUpdateDataSourceCredentials()
const handleAuth = useCallback(async (value: any) => {
await mutateAsync({
provider: currentDataSource?.provider || '',
pluginId: currentDataSource?.plugin_id || '',
credentials: value,
name: 'd14249c6-abe3-47ad-b0f1-1e65a591e790', // todo: fake name field, need to be removed later
})
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
hideAuthModal()
}, [currentDataSource, mutateAsync, notify, t, hideAuthModal])
return (
<div >
{
!isAuthorized && !showAuthModal && !isLocalFile && currentDataSource && (
<Group>
<Button
variant='primary'
className='w-full'
onClick={openAuthModal}
disabled={nodesReadOnly}
>
{t('workflow.nodes.tool.authorize')}
</Button>
</Group>
)
}
{
isAuthorized && !isLocalFile && !!formSchemas?.length && (
currentDataSource?.is_authorized && !isLocalFile && !!formSchemas?.length && (
<BoxGroupField
boxGroupProps={{
boxProps: { withBorderBottom: true },
@ -222,16 +180,6 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
))
}
</OutputVars>
{
showAuthModal && !isLocalFile && (
<ConfigCredential
dataSourceItem={currentDataSource!}
onCancel={hideAuthModal}
onSaved={handleAuth}
isHideRemoveBtn
/>
)
}
</div>
)
}

View File

@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query'
import { get } from './base'
import { useInvalid } from './use-base'
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
const NAME_SPACE = 'data-source-auth'
@ -11,3 +12,8 @@ export const useGetDataSourceListAuth = () => {
retry: 0,
})
}
export const useInvalidDataSourceListAuth = (
) => {
return useInvalid([NAME_SPACE, 'list'])
}

View File

@ -32,6 +32,7 @@ import type {
import type { DataSourceItem } from '@/app/components/workflow/block-selector/types'
import type { ToolCredential } from '@/app/components/tools/types'
import type { IconInfo } from '@/models/datasets'
import { useInvalid } from './use-base'
const NAME_SPACE = 'pipeline'
@ -180,6 +181,10 @@ export const useDataSourceList = (enabled: boolean, onSuccess?: (v: DataSourceIt
})
}
export const useInvalidDataSourceList = () => {
return useInvalid([NAME_SPACE, 'datasource'])
}
export const publishedPipelineInfoQueryKeyPrefix = [NAME_SPACE, 'published-pipeline']
export const usePublishedPipelineInfo = (pipelineId: string) => {

View File

@ -94,6 +94,7 @@ export const useGetPluginCredentialSchema = (
url: string,
) => {
return useQuery({
enabled: !!url,
queryKey: [NAME_SPACE, 'credential-schema', url],
queryFn: () => get<FormSchema[]>(url),
})
@ -119,6 +120,7 @@ export const useGetPluginOAuthClientSchema = (
url: string,
) => {
return useQuery({
enabled: !!url,
queryKey: [NAME_SPACE, 'oauth-client-schema', url],
queryFn: () => get<{
schema: FormSchema[]