datasource oauth

feat/rag-2
zxhlyh 9 months ago
parent 039a053027
commit caa2de3344

@ -1,15 +1,32 @@
import { memo } from 'react' import {
memo,
useCallback,
} from 'react'
import { useTranslation } from 'react-i18next'
import Item from './item' import Item from './item'
import Configure from './configure' import Configure from './configure'
import type { DataSourceAuth } from './types' import type {
DataSourceAuth,
DataSourceCredential,
} from './types'
import { useRenderI18nObject } from '@/hooks/use-i18n' 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 = { type CardProps = {
item: DataSourceAuth item: DataSourceAuth
disabled?: boolean
} }
const Card = ({ const Card = ({
item, item,
disabled,
}: CardProps) => { }: CardProps) => {
const { t } = useTranslation()
const renderI18nObject = useRenderI18nObject() const renderI18nObject = useRenderI18nObject()
const { const {
icon, icon,
@ -17,7 +34,56 @@ const Card = ({
author, author,
provider, provider,
credentials_list, credentials_list,
credential_schema,
} = item } = 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 ( return (
<div className='rounded-xl bg-background-section-burn'> <div className='rounded-xl bg-background-section-burn'>
@ -36,7 +102,11 @@ const Card = ({
{provider} {provider}
</div> </div>
</div> </div>
<Configure /> <Configure
pluginPayload={pluginPayload}
item={item}
onUpdate={handleAuthUpdate}
/>
</div> </div>
<div className='system-xs-medium flex h-4 items-center pl-3 text-text-tertiary'> <div className='system-xs-medium flex h-4 items-center pl-3 text-text-tertiary'>
Connected workspace Connected workspace
@ -45,9 +115,15 @@ const Card = ({
{ {
!!credentials_list.length && ( !!credentials_list.length && (
<div className='space-y-1 p-3 pt-2'> <div className='space-y-1 p-3 pt-2'>
<Item /> {
<Item /> credentials_list.map(credentialItem => (
<Item /> <Item
key={credentialItem.id}
credentialItem={credentialItem}
onAction={handleAction}
/>
))
}
</div> </div>
) )
} }
@ -60,6 +136,33 @@ const Card = ({
</div> </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> </div>
) )
} }

@ -1,16 +1,58 @@
import { memo } from 'react' import {
memo,
useMemo,
} from 'react'
import { import {
RiAddLine, RiAddLine,
RiEqualizer2Line,
} from '@remixicon/react' } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { import {
PortalToFollowElem, PortalToFollowElem,
PortalToFollowElemContent, PortalToFollowElemContent,
PortalToFollowElemTrigger, PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem' } from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button' 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 ( return (
<> <>
<PortalToFollowElem <PortalToFollowElem
@ -25,34 +67,46 @@ const Configure = () => {
variant='secondary-accent' variant='secondary-accent'
> >
<RiAddLine className='h-4 w-4' /> <RiAddLine className='h-4 w-4' />
Configure {t('common.dataSource.configure')}
</Button> </Button>
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[61]'> <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'> <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' canOAuth && (
className='w-full px-0' <AddOAuthButton
> {...oAuthButtonProps}
<div className='grow'> onUpdate={onUpdate}
use oauth oAuthData={{
</div> schema: oAuthData.client_schema || [],
<div className='h-4 w-[1px] bg-text-primary-on-surface opacity-[0.15]'></div> is_oauth_custom_client_enabled: oAuthData.is_oauth_custom_client_enabled,
<div className='flex h-8 w-8 shrink-0 items-center justify-center'> is_system_oauth_params_exists: oAuthData.is_system_oauth_params_exists,
<RiEqualizer2Line className='h-4 w-4' /> client_params: oAuthData.oauth_custom_client_params,
</div> redirect_uri: oAuthData.redirect_uri,
</Button> }}
<div className='system-2xs-medium-uppercase flex h-4 items-center p-2 text-text-quaternary'> disabled={disabled}
<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 canApiKey && canOAuth && (
className='w-full' <div className='system-2xs-medium-uppercase flex h-4 items-center p-2 text-text-quaternary'>
variant='secondary-accent' <div className='mr-2 h-[1px] grow bg-gradient-to-l from-[rgba(16,24,40,0.08)]' />
> OR
Use API Key <div className='ml-2 h-[1px] grow bg-gradient-to-r from-[rgba(16,24,40,0.08)]' />
</Button> </div>
)
}
{
canApiKey && (
<AddApiKeyButton
{...apiKeyButtonProps}
formSchemas={item.credential_schema}
onUpdate={onUpdate}
disabled={disabled}
/>
)
}
</div> </div>
</PortalToFollowElemContent> </PortalToFollowElemContent>
</PortalToFollowElem> </PortalToFollowElem>

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

@ -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,
}
}

@ -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 Indicator from '@/app/components/header/indicator'
import Operator from './operator' 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 ( return (
<div className='flex h-10 items-center rounded-lg bg-components-panel-on-panel-item-bg pl-3 pr-1'> <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='mr-2 h-5 w-5 shrink-0'></div> */}
<div className='system-sm-medium grow text-text-secondary'> {
Evans Notion renaming && (
</div> <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='flex shrink-0 items-center'>
<div className='mr-1 flex h-3 w-3 items-center justify-center'> <div className='mr-1 flex h-3 w-3 items-center justify-center'>
<Indicator color='green' /> <Indicator color='green' />
@ -18,7 +83,10 @@ const Item = () => {
</div> </div>
</div> </div>
<div className='ml-3 mr-2 h-3 w-[1px] bg-divider-regular'></div> <div className='ml-3 mr-2 h-3 w-[1px] bg-divider-regular'></div>
<Operator /> <Operator
credentialItem={credentialItem}
onAction={onAction}
/>
</div> </div>
) )
} }

@ -3,40 +3,94 @@ import {
useCallback, useCallback,
useMemo, useMemo,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next'
import { import {
RiDeleteBinLine, RiDeleteBinLine,
RiEditLine,
RiEqualizer2Line,
RiHome9Line,
RiLoopLeftLine, RiLoopLeftLine,
RiStickyNoteAddLine, RiStickyNoteAddLine,
} from '@remixicon/react' } from '@remixicon/react'
import Dropdown from '@/app/components/base/dropdown' import Dropdown from '@/app/components/base/dropdown'
import type { Item } 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(() => { const items = useMemo(() => {
return [ const commonItems = [
{ {
value: 'change', value: 'setDefault',
text: ( text: (
<div className='flex'> <div className='flex items-center'>
<RiStickyNoteAddLine className='mr-2 h-4 w-4 text-text-tertiary' /> <RiHome9Line className='mr-2 h-4 w-4 text-text-tertiary' />
<div> <div className='system-sm-semibold text-text-secondary'>{t('plugin.auth.setDefault')}</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> </div>
), ),
}, },
{ {
value: 'sync', value: 'rename',
text: ( text: (
<div className='flex items-center'> <div className='flex items-center'>
<RiLoopLeftLine className='mr-2 h-4 w-4 text-text-tertiary' /> <RiEditLine className='mr-2 h-4 w-4 text-text-tertiary' />
<div className='system-sm-semibold text-text-secondary'>Sync</div> <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> </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(() => { const secondItems = useMemo(() => {
return [ return [
@ -51,23 +105,29 @@ const Operator = () => {
}, },
] ]
}, []) }, [])
const handleSelect = useCallback((item: Item) => { 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 ( return (
<Dropdown <Dropdown
items={items} items={items}
secondItems={secondItems} secondItems={secondItems}
onSelect={handleSelect} onSelect={handleSelect}
popupClassName='z-[61]' popupClassName='z-[61]'
triggerProps={{ triggerProps={{
size: 'l', size: 'l',
}} }}
itemClassName='py-2 h-auto hover:bg-state-base-hover' itemClassName='py-2 h-auto hover:bg-state-base-hover'
secondItemClassName='py-2 h-auto hover:bg-state-base-hover' secondItemClassName='py-2 h-auto hover:bg-state-base-hover'
/> />
) )
} }

@ -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 = { export type DataSourceCredential = {
credential: Record<string, any> credential: Record<string, any>
type: string type: CredentialTypeEnum
name: string name: string
id: string id: string
} }
@ -9,14 +15,18 @@ export type DataSourceAuth = {
provider: string provider: string
plugin_id: string plugin_id: string
plugin_unique_identifier: string plugin_unique_identifier: string
icon: any icon: string
name: string name: string
label: any label: TypeWithI18N
description: any description: TypeWithI18N
credential_schema?: any[] credential_schema?: FormSchema[]
oauth_schema?: { oauth_schema?: {
client_schema?: any[] client_schema?: FormSchema[]
credentials_schema?: any[] 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[] credentials_list: DataSourceCredential[]
} }

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

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

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

@ -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)

@ -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 { return {
getCredentialInfo: '', getCredentialInfo: '',
setDefaultCredential: '', setDefaultCredential: '',

@ -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,
}
}

@ -3,4 +3,10 @@ export { default as Authorized } from './authorized'
export { default as AuthorizedInNode } from './authorized-in-node' export { default as AuthorizedInNode } from './authorized-in-node'
export { default as PluginAuthInAgent } from './plugin-auth-in-agent' export { default as PluginAuthInAgent } from './plugin-auth-in-agent'
export { usePluginAuth } from './hooks/use-plugin-auth' 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' export * from './types'

@ -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)

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

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

@ -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)

@ -6,13 +6,11 @@ import {
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { memo } from 'react' import { memo } from 'react'
import { useBoolean } from 'ahooks'
import type { DataSourceNodeType } from './types' import type { DataSourceNodeType } from './types'
import { DataSourceClassification } from './types' import { DataSourceClassification } from './types'
import type { NodePanelProps } from '@/app/components/workflow/types' import type { NodePanelProps } from '@/app/components/workflow/types'
import { import {
BoxGroupField, BoxGroupField,
Group,
} from '@/app/components/workflow/nodes/_base/components/layout' } from '@/app/components/workflow/nodes/_base/components/layout'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import TagInput from '@/app/components/base/tag-input' import TagInput from '@/app/components/base/tag-input'
@ -26,18 +24,13 @@ import {
WEBSITE_CRAWL_OUTPUT, WEBSITE_CRAWL_OUTPUT,
} from './constants' } from './constants'
import { useStore } from '@/app/components/workflow/store' 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 InputVarList from '@/app/components/workflow/nodes/tool/components/input-var-list'
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import type { Var } from '@/app/components/workflow/types' import type { Var } from '@/app/components/workflow/types'
import { VarType } 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 Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext()
const { nodesReadOnly } = useNodesReadOnly() const { nodesReadOnly } = useNodesReadOnly()
const dataSourceList = useStore(s => s.dataSourceList) const dataSourceList = useStore(s => s.dataSourceList)
const { const {
@ -55,11 +48,6 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
const isOnlineDocument = provider_type === DataSourceClassification.onlineDocument const isOnlineDocument = provider_type === DataSourceClassification.onlineDocument
const isOnlineDrive = provider_type === DataSourceClassification.onlineDrive const isOnlineDrive = provider_type === DataSourceClassification.onlineDrive
const currentDataSource = dataSourceList?.find(ds => ds.plugin_id === plugin_id) 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 currentDataSourceItem: any = currentDataSource?.tools.find(tool => tool.name === data.datasource_name)
const formSchemas = useMemo(() => { const formSchemas = useMemo(() => {
return currentDataSourceItem ? toolParametersToFormSchemas(currentDataSourceItem.parameters) : [] return currentDataSourceItem ? toolParametersToFormSchemas(currentDataSourceItem.parameters) : []
@ -77,40 +65,10 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
return varPayload.type !== VarType.arrayFile return varPayload.type !== VarType.arrayFile
}, [currVarType]) }, [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 ( return (
<div > <div >
{ {
!isAuthorized && !showAuthModal && !isLocalFile && currentDataSource && ( currentDataSource?.is_authorized && !isLocalFile && !!formSchemas?.length && (
<Group>
<Button
variant='primary'
className='w-full'
onClick={openAuthModal}
disabled={nodesReadOnly}
>
{t('workflow.nodes.tool.authorize')}
</Button>
</Group>
)
}
{
isAuthorized && !isLocalFile && !!formSchemas?.length && (
<BoxGroupField <BoxGroupField
boxGroupProps={{ boxGroupProps={{
boxProps: { withBorderBottom: true }, boxProps: { withBorderBottom: true },
@ -222,16 +180,6 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
)) ))
} }
</OutputVars> </OutputVars>
{
showAuthModal && !isLocalFile && (
<ConfigCredential
dataSourceItem={currentDataSource!}
onCancel={hideAuthModal}
onSaved={handleAuth}
isHideRemoveBtn
/>
)
}
</div> </div>
) )
} }

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

@ -32,6 +32,7 @@ import type {
import type { DataSourceItem } from '@/app/components/workflow/block-selector/types' import type { DataSourceItem } from '@/app/components/workflow/block-selector/types'
import type { ToolCredential } from '@/app/components/tools/types' import type { ToolCredential } from '@/app/components/tools/types'
import type { IconInfo } from '@/models/datasets' import type { IconInfo } from '@/models/datasets'
import { useInvalid } from './use-base'
const NAME_SPACE = 'pipeline' 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 publishedPipelineInfoQueryKeyPrefix = [NAME_SPACE, 'published-pipeline']
export const usePublishedPipelineInfo = (pipelineId: string) => { export const usePublishedPipelineInfo = (pipelineId: string) => {

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

Loading…
Cancel
Save