This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
import {
memo,
useCallback,
useRef,
} from 'react'
import { useTranslation } from 'react-i18next'
import Item from './item'
import Configure from './configure'
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'
import { useGetDataSourceOAuthUrl } from '@/service/use-datasource'
import { openOAuthPopup } from '@/hooks/use-oauth'
import { CollectionType } from '@/app/components/tools/types'
type CardProps = {
item: DataSourceAuth
disabled?: boolean
}
const Card = ({
item,
disabled,
}: CardProps) => {
const { t } = useTranslation()
const renderI18nObject = useRenderI18nObject()
const {
icon,
label,
author,
name,
credentials_list,
credential_schema,
} = item
const pluginPayload = {
category: AuthCategory.datasource,
provider: `${item.plugin_id}/${item.name}`,
providerType: CollectionType.datasource,
}
const { handleAuthUpdate } = useDataSourceAuthUpdate({
pluginId: item.plugin_id,
provider: item.name,
})
const {
deleteCredentialId,
doingAction,
handleConfirm,
handleEdit,
handleRemove,
handleRename,
handleSetDefault,
editValues,
setEditValues,
openConfirm,
closeConfirm,
pendingOperationCredentialId,
} = usePluginAuthAction(pluginPayload, handleAuthUpdate)
const changeCredentialIdRef = useRef<string | undefined>(undefined)
const {
mutateAsync: getPluginOAuthUrl,
} = useGetDataSourceOAuthUrl(pluginPayload.provider)
const handleOAuth = useCallback(async () => {
const { authorization_url } = await getPluginOAuthUrl(changeCredentialIdRef.current)
if (authorization_url) {
openOAuthPopup(
authorization_url,
handleAuthUpdate,
)
}
}, [getPluginOAuthUrl, 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)
if (action === 'change') {
changeCredentialIdRef.current = credentialItem.id
handleOAuth()
}
}, [
openConfirm,
handleEdit,
handleSetDefault,
handleRename,
])
return (
<div className='rounded-xl bg-background-section-burn'>
<div className='flex items-center p-3 pb-2'>
<img
src={icon}
className='mr-3 flex h-10 w-10 shrink-0 items-center justify-center'
/>
<div className='grow'>
<div className='system-md-semibold text-text-primary'>
{renderI18nObject(label)}
</div>
<div className='system-xs-regular flex h-4 items-center text-text-tertiary'>
{author}
<div className='mx-0.5 text-text-quaternary'>/</div>
{name}
</div>
</div>
<Configure
pluginPayload={pluginPayload}
item={item}
onUpdate={handleAuthUpdate}
/>
</div>
<div className='system-xs-medium flex h-4 items-center pl-3 text-text-tertiary'>
{t('plugin.auth.connectedWorkspace')}
<div className='ml-3 h-[1px] grow bg-divider-subtle'></div>
</div>
{
!!credentials_list.length && (
<div className='space-y-1 p-3 pt-2'>
{
credentials_list.map(credentialItem => (
<Item
key={credentialItem.id}
credentialItem={credentialItem}
onAction={handleAction}
/>
))
}
</div>
)
}
{
!credentials_list.length && (
<div className='p-3 pt-1'>
<div className='system-xs-regular flex h-10 items-center justify-center rounded-[10px] bg-background-section text-text-tertiary'>
{t('plugin.auth.emptyAuth')}
</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>
)
}
export default memo(Card)

View File

@@ -0,0 +1,131 @@
import {
memo,
useCallback,
useMemo,
useState,
} from 'react'
import {
RiAddLine,
} 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 [open, setOpen] = useState(false)
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 handleToggle = useCallback(() => {
setOpen(v => !v)
}, [])
const handleUpdate = useCallback(() => {
setOpen(false)
onUpdate?.()
}, [onUpdate])
return (
<>
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: -4,
}}
>
<PortalToFollowElemTrigger onClick={handleToggle}>
<Button
variant='secondary-accent'
>
<RiAddLine className='h-4 w-4' />
{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'>
{
!!canOAuth && (
<AddOAuthButton
{...oAuthButtonProps}
onUpdate={handleUpdate}
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={handleUpdate}
disabled={disabled}
/>
)
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</>
)
}
export default memo(Configure)

View File

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

View File

@@ -0,0 +1,30 @@
import { useCallback } from 'react'
import { useInvalidDataSourceAuth, useInvalidDataSourceListAuth } from '@/service/use-datasource'
import { useInvalidDefaultDataSourceListAuth } from '@/service/use-datasource'
import { useInvalidDataSourceList } from '@/service/use-pipeline'
export const useDataSourceAuthUpdate = ({
pluginId,
provider,
}: {
pluginId: string
provider: string
}) => {
const invalidateDataSourceListAuth = useInvalidDataSourceListAuth()
const invalidDefaultDataSourceListAuth = useInvalidDefaultDataSourceListAuth()
const invalidateDataSourceList = useInvalidDataSourceList()
const invalidateDataSourceAuth = useInvalidDataSourceAuth({
pluginId,
provider,
})
const handleAuthUpdate = useCallback(() => {
invalidateDataSourceListAuth()
invalidDefaultDataSourceListAuth()
invalidateDataSourceList()
invalidateDataSourceAuth()
}, [invalidateDataSourceListAuth, invalidateDataSourceList, invalidateDataSourceAuth, invalidDefaultDataSourceListAuth])
return {
handleAuthUpdate,
}
}

View File

@@ -0,0 +1,80 @@
import {
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import {
useMarketplacePlugins,
} from '@/app/components/plugins/marketplace/hooks'
import type { Plugin } from '@/app/components/plugins/types'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import { getMarketplacePluginsByCollectionId } from '@/app/components/plugins/marketplace/utils'
export const useMarketplaceAllPlugins = (providers: any[], searchText: string) => {
const exclude = useMemo(() => {
return providers.map(provider => provider.plugin_id)
}, [providers])
const [collectionPlugins, setCollectionPlugins] = useState<Plugin[]>([])
const {
plugins,
queryPlugins,
queryPluginsWithDebounced,
isLoading,
} = useMarketplacePlugins()
const getCollectionPlugins = useCallback(async () => {
const collectionPlugins = await getMarketplacePluginsByCollectionId('__datasource-settings-pinned-datasources')
setCollectionPlugins(collectionPlugins)
}, [])
useEffect(() => {
getCollectionPlugins()
}, [getCollectionPlugins])
useEffect(() => {
if (searchText) {
queryPluginsWithDebounced({
query: searchText,
category: PluginCategoryEnum.datasource,
exclude,
type: 'plugin',
sortBy: 'install_count',
sortOrder: 'DESC',
})
}
else {
queryPlugins({
query: '',
category: PluginCategoryEnum.datasource,
type: 'plugin',
pageSize: 1000,
exclude,
sortBy: 'install_count',
sortOrder: 'DESC',
})
}
}, [queryPlugins, queryPluginsWithDebounced, searchText, exclude])
const allPlugins = useMemo(() => {
const allPlugins = collectionPlugins.filter(plugin => !exclude.includes(plugin.plugin_id))
if (plugins?.length) {
for (let i = 0; i < plugins.length; i++) {
const plugin = plugins[i]
if (plugin.type !== 'bundle' && !allPlugins.find(p => p.plugin_id === plugin.plugin_id))
allPlugins.push(plugin)
}
}
return allPlugins
}, [plugins, collectionPlugins, exclude])
return {
plugins: allPlugins,
isLoading,
}
}

View File

@@ -0,0 +1,35 @@
import { memo } from 'react'
import Card from './card'
import InstallFromMarketplace from './install-from-marketplace'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useGetDataSourceListAuth } from '@/service/use-datasource'
const DataSourcePage = () => {
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { data } = useGetDataSourceListAuth()
return (
<div>
<div className='space-y-2'>
{
data?.result.map(item => (
<Card
key={item.plugin_unique_identifier}
item={item}
/>
))
}
</div>
{
enable_marketplace && (
<InstallFromMarketplace
providers={data?.result || []}
searchText={''}
/>
)
}
</div>
)
}
export default memo(DataSourcePage)

View File

@@ -0,0 +1,84 @@
import {
memo,
useCallback,
useState,
} from 'react'
import { useTheme } from 'next-themes'
import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import {
RiArrowDownSLine,
RiArrowRightUpLine,
} from '@remixicon/react'
import {
useMarketplaceAllPlugins,
} from './hooks'
import Divider from '@/app/components/base/divider'
import Loading from '@/app/components/base/loading'
import ProviderCard from '@/app/components/plugins/provider-card'
import List from '@/app/components/plugins/marketplace/list'
import type { Plugin } from '@/app/components/plugins/types'
import cn from '@/utils/classnames'
import { getLocaleOnClient } from '@/i18n-config'
import { getMarketplaceUrl } from '@/utils/var'
type InstallFromMarketplaceProps = {
providers: any[]
searchText: string
}
const InstallFromMarketplace = ({
providers,
searchText,
}: InstallFromMarketplaceProps) => {
const { t } = useTranslation()
const { theme } = useTheme()
const [collapse, setCollapse] = useState(false)
const locale = getLocaleOnClient()
const {
plugins: allPlugins,
isLoading: isAllPluginsLoading,
} = useMarketplaceAllPlugins(providers, searchText)
const cardRender = useCallback((plugin: Plugin) => {
if (plugin.type === 'bundle')
return null
return <ProviderCard key={plugin.plugin_id} payload={plugin} />
}, [])
return (
<div className='mb-2'>
<Divider className='!mt-4 h-px' />
<div className='flex items-center justify-between'>
<div className='system-md-semibold flex cursor-pointer items-center gap-1 text-text-primary' onClick={() => setCollapse(!collapse)}>
<RiArrowDownSLine className={cn('h-4 w-4', collapse && '-rotate-90')} />
{t('common.modelProvider.installDataSourceProvider')}
</div>
<div className='mb-2 flex items-center pt-2'>
<span className='system-sm-regular pr-1 text-text-tertiary'>{t('common.modelProvider.discoverMore')}</span>
<Link target="_blank" href={getMarketplaceUrl('', { theme })} className='system-sm-medium inline-flex items-center text-text-accent'>
{t('plugin.marketplace.difyMarketplace')}
<RiArrowRightUpLine className='h-4 w-4' />
</Link>
</div>
</div>
{!collapse && isAllPluginsLoading && <Loading type='area' />}
{
!isAllPluginsLoading && !collapse && (
<List
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
plugins={allPlugins}
showInstallButton
locale={locale}
cardContainerClassName='grid grid-cols-2 gap-2'
cardRender={cardRender}
emptyClassName='h-auto'
/>
)
}
</div>
)
}
export default memo(InstallFromMarketplace)

View File

@@ -0,0 +1,95 @@
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)
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> */}
{
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' />
</div>
<div className='system-xs-semibold-uppercase text-util-colors-green-green-600'>
connected
</div>
</div>
<div className='ml-3 mr-2 h-3 w-[1px] bg-divider-regular'></div>
<Operator
credentialItem={credentialItem}
onAction={onAction}
onRename={() => setRenaming(true)}
/>
</div>
)
}
export default memo(Item)

View File

@@ -0,0 +1,135 @@
import {
memo,
useCallback,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiDeleteBinLine,
RiEditLine,
RiEqualizer2Line,
RiHome9Line,
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'
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 commonItems = [
{
value: 'setDefault',
text: (
<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>
),
},
...(
type === CredentialTypeEnum.OAUTH2
? [
{
value: 'rename',
text: (
<div className='flex items-center'>
<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>
),
},
]
: []
),
...(
type === CredentialTypeEnum.API_KEY
? [
{
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 items-center'>
<RiStickyNoteAddLine className='mr-2 h-4 w-4 text-text-tertiary' />
<div className='system-sm-semibold mb-1 text-text-secondary'>{t('common.dataSource.notion.changeAuthorizedPages')}</div>
</div>
),
},
]
commonItems.push(...oAuthItems)
}
return commonItems
}, [t, type])
const secondItems = useMemo(() => {
return [
{
value: 'delete',
text: (
<div className='flex items-center'>
<RiDeleteBinLine className='mr-2 h-4 w-4 text-text-tertiary' />
<div className='system-sm-semibold text-text-secondary'>
{t('common.operation.remove')}
</div>
</div>
),
},
]
}, [])
const handleSelect = useCallback((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'
/>
)
}
export default memo(Operator)

View File

@@ -0,0 +1,34 @@
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: CredentialTypeEnum
name: string
id: string
is_default: boolean
avatar_url: string
}
export type DataSourceAuth = {
author: string
provider: string
plugin_id: string
plugin_unique_identifier: string
icon: string
name: string
label: TypeWithI18N
description: TypeWithI18N
credential_schema?: FormSchema[]
oauth_schema?: {
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[]
}