feat: tool oauth

This commit is contained in:
zxhlyh 2025-07-03 17:55:52 +08:00
parent 31eb8548ef
commit c53d5c105b
8 changed files with 458 additions and 0 deletions

View File

@ -0,0 +1,118 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import type { ButtonProps } from '@/app/components/base/button'
import cn from '@/utils/classnames'
type ModalProps = {
onClose?: () => void
size?: 'sm' | 'md'
title: string
subTitle?: string
children?: React.ReactNode
confirmButtonText?: string
onConfirm?: () => void
cancelButtonText?: string
onCancel?: () => void
showExtraButton?: boolean
extraButtonText?: string
extraButtonVariant?: ButtonProps['variant']
onExtraButtonClick?: () => void
footerSlot?: React.ReactNode
bottomSlot?: React.ReactNode
}
const Modal = ({
onClose,
size = 'sm',
title,
subTitle,
children,
confirmButtonText,
onConfirm,
cancelButtonText,
onCancel,
showExtraButton,
extraButtonVariant = 'warning',
extraButtonText,
onExtraButtonClick,
footerSlot,
bottomSlot,
}: ModalProps) => {
const { t } = useTranslation()
return (
<PortalToFollowElem open>
<PortalToFollowElemContent
className='z-[999999] flex h-full w-full items-center justify-center bg-background-overlay'
onClick={onClose}
>
<div
className={cn(
'max-h-[80%] w-[480px] overflow-y-auto rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xs',
size === 'sm' && 'w-[480px',
size === 'md' && 'w-[640px]',
)}
onClick={e => e.stopPropagation()}
>
<div className='title-2xl-semi-bold relative p-6 pb-3 pr-14 text-text-primary'>
{title}
{
subTitle && (
<div className='system-xs-regular mt-1 text-text-tertiary'>
{subTitle}
</div>
)
}
<div
className='absolute right-5 top-5 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg'
onClick={onClose}
>
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
</div>
</div>
{
children && (
<div className='px-6 py-3'>{children}</div>
)
}
<div className='flex items-center justify-end p-6 pt-5'>
{footerSlot}
{
showExtraButton && (
<>
<Button
variant={extraButtonVariant}
onClick={onExtraButtonClick}
>
{extraButtonText || t('common.operation.remove')}
</Button>
<div className='mx-3 h-4 w-[1px] bg-divider-regular'></div>
</>
)
}
<Button
onClick={onCancel}
>
{cancelButtonText || t('common.operation.cancel')}
</Button>
<Button
className='ml-2'
variant='primary'
onClick={onConfirm}
>
{confirmButtonText || t('common.operation.save')}
</Button>
</div>
{bottomSlot}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(Modal)

View File

@ -0,0 +1,40 @@
import {
memo,
useState,
} from 'react'
import Button from '@/app/components/base/button'
import type { ButtonProps } from '@/app/components/base/button'
import ApiKeyModal from './api-key-modal'
type AddApiKeyButtonProps = {
buttonVariant?: ButtonProps['variant']
buttonText?: string
}
const AddApiKeyButton = ({
buttonVariant = 'secondary-accent',
buttonText = 'use api key',
}: AddApiKeyButtonProps) => {
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false)
return (
<>
<Button
className='grow'
variant={buttonVariant}
onClick={() => setIsApiKeyModalOpen(true)}
>
{buttonText}
</Button>
{
isApiKeyModalOpen && (
<ApiKeyModal
onClose={() => setIsApiKeyModalOpen(false)}
/>
)
}
</>
)
}
export default memo(AddApiKeyButton)

View File

@ -0,0 +1,69 @@
import {
memo,
useState,
} from 'react'
import { RiEqualizer2Line } from '@remixicon/react'
import Button from '@/app/components/base/button'
import type { ButtonProps } from '@/app/components/base/button'
import OAuthClientSettings from './oauth-client-settings'
import cn from '@/utils/classnames'
type AddOAuthButtonProps = {
buttonVariant?: ButtonProps['variant']
buttonText?: string
className?: string
buttonLeftClassName?: string
buttonRightClassName?: string
dividerClassName?: string
}
const AddOAuthButton = ({
buttonVariant = 'primary',
buttonText = 'use oauth',
className,
buttonLeftClassName,
buttonRightClassName,
dividerClassName,
}: AddOAuthButtonProps) => {
const [isOAuthSettingsOpen, setIsOAuthSettingsOpen] = useState(false)
return (
<>
<Button
variant={buttonVariant}
className={cn(
'grow px-0 py-0 hover:bg-components-button-primary-bg',
className,
)}
>
<div className={cn(
'flex h-full grow items-center justify-center rounded-l-lg hover:bg-components-button-primary-bg-hover',
buttonLeftClassName,
)}>
{buttonText}
</div>
<div className={cn(
'h-4 w-[1px] bg-text-primary-on-surface opacity-[0.15]',
dividerClassName,
)}></div>
<div
className={cn(
'flex h-full w-8 shrink-0 items-center justify-center rounded-r-lg hover:bg-components-button-primary-bg-hover',
buttonRightClassName,
)}
onClick={() => setIsOAuthSettingsOpen(true)}
>
<RiEqualizer2Line className='h-4 w-4' />
</div>
</Button>
{
isOAuthSettingsOpen && (
<OAuthClientSettings
onClose={() => setIsOAuthSettingsOpen(false)}
/>
)
}
</>
)
}
export default memo(AddOAuthButton)

View File

@ -0,0 +1,52 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { RiExternalLinkLine } from '@remixicon/react'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import Modal from '@/app/components/base/modal/modal'
export type ApiKeyModalProps = {
onClose?: () => void
}
const ApiKeyModal = ({
onClose,
}: ApiKeyModalProps) => {
const { t } = useTranslation()
return (
<Modal
size='md'
title='API Key Authorization Configuration'
subTitle='After configuring credentials, all members within the workspace can use this tool when orchestrating applications.'
onClose={onClose}
onCancel={onClose}
footerSlot={
<a
className='system-xs-regular flex h-8 grow items-center text-text-accent'
href=''
target='_blank'
>
Get your API Key from OpenAI
<RiExternalLinkLine className='ml-1 h-3 w-3' />
</a>
}
bottomSlot={
<div className='flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary'>
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
{t('common.modelProvider.encrypted.front')}
<a
className='mx-1 text-text-accent'
target='_blank' rel='noopener noreferrer'
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
>
PKCS1_OAEP
</a>
{t('common.modelProvider.encrypted.back')}
</div>
}
>
<div>oauth</div>
</Modal>
)
}
export default memo(ApiKeyModal)

View File

@ -0,0 +1,124 @@
import {
memo,
useState,
} from 'react'
import {
RiArrowDownSLine,
RiDeleteBinLine,
RiEditLine,
} from '@remixicon/react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import Badge from '@/app/components/base/badge'
import ActionButton from '@/app/components/base/action-button'
import cn from '@/utils/classnames'
import AddOauthButton from './add-oauth-button'
import AddApiKeyButton from './add-api-key-button'
const Authorization = () => {
const [isOpen, setIsOpen] = useState(false)
return (
<PortalToFollowElem
open={isOpen}
onOpenChange={setIsOpen}
placement='bottom-start'
offset={8}
triggerPopupSameWidth
>
<PortalToFollowElemTrigger
onClick={() => setIsOpen(!isOpen)}
asChild
>
<Button
className={cn(
'w-full',
isOpen && 'bg-components-button-secondary-bg-hover',
)}>
<Indicator className='mr-2' />
4 Authorizations
<RiArrowDownSLine className='ml-0.5 h-4 w-4' />
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>
<div className='max-h-[360px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
<div className='py-1'>
<div className='p-1'>
<div className='system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary'>
OAuth
</div>
<div className='flex items-center rounded-lg p-1 hover:bg-state-base-hover'>
<div className='flex grow items-center space-x-1.5 pl-2'>
<Indicator className='mr-1.5' />
<div
className='system-md-regular truncate text-text-secondary'
title='Auth 1'
>
Auth 1
</div>
<Badge>
Default
</Badge>
</div>
<div className='ml-2 flex shrink-0 items-center'>
<ActionButton>
<RiEditLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
<ActionButton>
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
</div>
</div>
</div>
<div className='p-1'>
<div className='system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary'>
API Keys
</div>
<div className='flex items-center rounded-lg p-1 hover:bg-state-base-hover'>
<div className='flex grow items-center space-x-1.5 pl-2'>
<Indicator className='mr-1.5' />
<div
className='system-md-regular truncate text-text-secondary'
title='Production'
>
Production
</div>
</div>
<div className='ml-2 flex shrink-0 items-center space-x-1'>
<Badge>
0.2
</Badge>
<Badge>
ENTERPRISE
</Badge>
</div>
</div>
</div>
</div>
<div className='h-[1px] bg-divider-subtle'></div>
<div className='flex items-center space-x-1.5 p-2'>
<AddOauthButton
buttonVariant='secondary'
buttonText='Add OAuth'
className='hover:bg-components-button-secondary-bg'
buttonLeftClassName='hover:bg-components-button-secondary-bg-hover'
buttonRightClassName='hover:bg-components-button-secondary-bg-hover'
dividerClassName='bg-divider-regular opacity-100'
/>
<AddApiKeyButton
buttonVariant='secondary'
buttonText='Add API Key'
/>
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(Authorization)

View File

@ -0,0 +1,23 @@
import {
memo,
} from 'react'
import AddOAuthButton from './add-oauth-button'
import AddApiKeyButton from './add-api-key-button'
const ToolAuth = () => {
return (
<>
<div className='flex items-center space-x-1.5'>
<AddOAuthButton />
<div className='system-2xs-medium-uppercase flex shrink-0 flex-col items-center justify-between text-text-tertiary'>
<div className='h-2 w-[1px] bg-divider-subtle'></div>
or
<div className='h-2 w-[1px] bg-divider-subtle'></div>
</div>
<AddApiKeyButton />
</div>
</>
)
}
export default memo(ToolAuth)

View File

@ -0,0 +1,26 @@
import { memo } from 'react'
import Modal from '@/app/components/base/modal/modal'
type OAuthClientSettingsProps = {
onClose?: () => void
}
const OAuthClientSettings = ({
onClose,
}: OAuthClientSettingsProps) => {
return (
<Modal
title='Oauth client settings'
confirmButtonText='Save & Authorize'
cancelButtonText='Save only'
extraButtonText='Cancel'
showExtraButton
extraButtonVariant='secondary'
onExtraButtonClick={onClose}
onClose={onClose}
>
<div>oauth</div>
</Modal>
)
}
export default memo(OAuthClientSettings)

View File

@ -14,6 +14,8 @@ import Loading from '@/app/components/base/loading'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
import { Type } from '../llm/types'
import ToolAuth from '@/app/components/tools/tool-auth'
import Authorization from '@/app/components/tools/tool-auth/authorization'
const i18nPrefix = 'workflow.nodes.tool'
@ -53,6 +55,10 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
return (
<div className='pt-2'>
<div className='px-4 py-2'>
<ToolAuth />
<Authorization />
</div>
{!readOnly && isShowAuthBtn && (
<>
<div className='px-4'>