您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import {
  2. memo,
  3. useCallback,
  4. useRef,
  5. useState,
  6. } from 'react'
  7. import {
  8. RiArrowDownSLine,
  9. } from '@remixicon/react'
  10. import { useTranslation } from 'react-i18next'
  11. import {
  12. PortalToFollowElem,
  13. PortalToFollowElemContent,
  14. PortalToFollowElemTrigger,
  15. } from '@/app/components/base/portal-to-follow-elem'
  16. import type {
  17. PortalToFollowElemOptions,
  18. } from '@/app/components/base/portal-to-follow-elem'
  19. import Button from '@/app/components/base/button'
  20. import Indicator from '@/app/components/header/indicator'
  21. import cn from '@/utils/classnames'
  22. import Confirm from '@/app/components/base/confirm'
  23. import Authorize from '../authorize'
  24. import type { Credential } from '../types'
  25. import { CredentialTypeEnum } from '../types'
  26. import ApiKeyModal from '../authorize/api-key-modal'
  27. import Item from './item'
  28. import { useToastContext } from '@/app/components/base/toast'
  29. import type { PluginPayload } from '../types'
  30. import {
  31. useDeletePluginCredentialHook,
  32. useSetPluginDefaultCredentialHook,
  33. useUpdatePluginCredentialHook,
  34. } from '../hooks/use-credential'
  35. type AuthorizedProps = {
  36. pluginPayload: PluginPayload
  37. credentials: Credential[]
  38. canOAuth?: boolean
  39. canApiKey?: boolean
  40. disabled?: boolean
  41. renderTrigger?: (open?: boolean) => React.ReactNode
  42. isOpen?: boolean
  43. onOpenChange?: (open: boolean) => void
  44. offset?: PortalToFollowElemOptions['offset']
  45. placement?: PortalToFollowElemOptions['placement']
  46. triggerPopupSameWidth?: boolean
  47. popupClassName?: string
  48. disableSetDefault?: boolean
  49. onItemClick?: (id: string) => void
  50. extraAuthorizationItems?: Credential[]
  51. showItemSelectedIcon?: boolean
  52. selectedCredentialId?: string
  53. onUpdate?: () => void
  54. }
  55. const Authorized = ({
  56. pluginPayload,
  57. credentials,
  58. canOAuth,
  59. canApiKey,
  60. disabled,
  61. renderTrigger,
  62. isOpen,
  63. onOpenChange,
  64. offset = 8,
  65. placement = 'bottom-start',
  66. triggerPopupSameWidth = true,
  67. popupClassName,
  68. disableSetDefault,
  69. onItemClick,
  70. extraAuthorizationItems,
  71. showItemSelectedIcon,
  72. selectedCredentialId,
  73. onUpdate,
  74. }: AuthorizedProps) => {
  75. const { t } = useTranslation()
  76. const { notify } = useToastContext()
  77. const [isLocalOpen, setIsLocalOpen] = useState(false)
  78. const mergedIsOpen = isOpen ?? isLocalOpen
  79. const setMergedIsOpen = useCallback((open: boolean) => {
  80. if (onOpenChange)
  81. onOpenChange(open)
  82. setIsLocalOpen(open)
  83. }, [onOpenChange])
  84. const oAuthCredentials = credentials.filter(credential => credential.credential_type === CredentialTypeEnum.OAUTH2)
  85. const apiKeyCredentials = credentials.filter(credential => credential.credential_type === CredentialTypeEnum.API_KEY)
  86. const pendingOperationCredentialId = useRef<string | null>(null)
  87. const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
  88. const { mutateAsync: deletePluginCredential } = useDeletePluginCredentialHook(pluginPayload)
  89. const openConfirm = useCallback((credentialId?: string) => {
  90. if (credentialId)
  91. pendingOperationCredentialId.current = credentialId
  92. setDeleteCredentialId(pendingOperationCredentialId.current)
  93. }, [])
  94. const closeConfirm = useCallback(() => {
  95. setDeleteCredentialId(null)
  96. pendingOperationCredentialId.current = null
  97. }, [])
  98. const [doingAction, setDoingAction] = useState(false)
  99. const doingActionRef = useRef(doingAction)
  100. const handleSetDoingAction = useCallback((doing: boolean) => {
  101. doingActionRef.current = doing
  102. setDoingAction(doing)
  103. }, [])
  104. const handleConfirm = useCallback(async () => {
  105. if (doingActionRef.current)
  106. return
  107. if (!pendingOperationCredentialId.current) {
  108. setDeleteCredentialId(null)
  109. return
  110. }
  111. try {
  112. handleSetDoingAction(true)
  113. await deletePluginCredential({ credential_id: pendingOperationCredentialId.current })
  114. notify({
  115. type: 'success',
  116. message: t('common.api.actionSuccess'),
  117. })
  118. onUpdate?.()
  119. setDeleteCredentialId(null)
  120. pendingOperationCredentialId.current = null
  121. }
  122. finally {
  123. handleSetDoingAction(false)
  124. }
  125. }, [deletePluginCredential, onUpdate, notify, t, handleSetDoingAction])
  126. const [editValues, setEditValues] = useState<Record<string, any> | null>(null)
  127. const handleEdit = useCallback((id: string, values: Record<string, any>) => {
  128. pendingOperationCredentialId.current = id
  129. setEditValues(values)
  130. }, [])
  131. const handleRemove = useCallback(() => {
  132. setDeleteCredentialId(pendingOperationCredentialId.current)
  133. }, [])
  134. const { mutateAsync: setPluginDefaultCredential } = useSetPluginDefaultCredentialHook(pluginPayload)
  135. const handleSetDefault = useCallback(async (id: string) => {
  136. if (doingActionRef.current)
  137. return
  138. try {
  139. handleSetDoingAction(true)
  140. await setPluginDefaultCredential(id)
  141. notify({
  142. type: 'success',
  143. message: t('common.api.actionSuccess'),
  144. })
  145. onUpdate?.()
  146. }
  147. finally {
  148. handleSetDoingAction(false)
  149. }
  150. }, [setPluginDefaultCredential, onUpdate, notify, t, handleSetDoingAction])
  151. const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
  152. const handleRename = useCallback(async (payload: {
  153. credential_id: string
  154. name: string
  155. }) => {
  156. if (doingActionRef.current)
  157. return
  158. try {
  159. handleSetDoingAction(true)
  160. await updatePluginCredential(payload)
  161. notify({
  162. type: 'success',
  163. message: t('common.api.actionSuccess'),
  164. })
  165. onUpdate?.()
  166. }
  167. finally {
  168. handleSetDoingAction(false)
  169. }
  170. }, [updatePluginCredential, notify, t, handleSetDoingAction, onUpdate])
  171. return (
  172. <>
  173. <PortalToFollowElem
  174. open={mergedIsOpen}
  175. onOpenChange={setMergedIsOpen}
  176. placement={placement}
  177. offset={offset}
  178. triggerPopupSameWidth={triggerPopupSameWidth}
  179. >
  180. <PortalToFollowElemTrigger
  181. onClick={() => setMergedIsOpen(!mergedIsOpen)}
  182. asChild
  183. >
  184. {
  185. renderTrigger
  186. ? renderTrigger(mergedIsOpen)
  187. : (
  188. <Button
  189. className={cn(
  190. 'w-full',
  191. isOpen && 'bg-components-button-secondary-bg-hover',
  192. )}>
  193. <Indicator className='mr-2' />
  194. {credentials.length}&nbsp;
  195. {
  196. credentials.length > 1
  197. ? t('plugin.auth.authorizations')
  198. : t('plugin.auth.authorization')
  199. }
  200. <RiArrowDownSLine className='ml-0.5 h-4 w-4' />
  201. </Button>
  202. )
  203. }
  204. </PortalToFollowElemTrigger>
  205. <PortalToFollowElemContent className='z-[100]'>
  206. <div className={cn(
  207. 'max-h-[360px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg',
  208. popupClassName,
  209. )}>
  210. <div className='py-1'>
  211. {
  212. !!extraAuthorizationItems?.length && (
  213. <div className='p-1'>
  214. {
  215. extraAuthorizationItems.map(credential => (
  216. <Item
  217. key={credential.id}
  218. credential={credential}
  219. disabled={disabled}
  220. onItemClick={onItemClick}
  221. disableRename
  222. disableEdit
  223. disableDelete
  224. disableSetDefault
  225. showSelectedIcon={showItemSelectedIcon}
  226. selectedCredentialId={selectedCredentialId}
  227. />
  228. ))
  229. }
  230. </div>
  231. )
  232. }
  233. {
  234. !!oAuthCredentials.length && (
  235. <div className='p-1'>
  236. <div className={cn(
  237. 'system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary',
  238. showItemSelectedIcon && 'pl-7',
  239. )}>
  240. OAuth
  241. </div>
  242. {
  243. oAuthCredentials.map(credential => (
  244. <Item
  245. key={credential.id}
  246. credential={credential}
  247. disabled={disabled}
  248. disableEdit
  249. onDelete={openConfirm}
  250. onSetDefault={handleSetDefault}
  251. onRename={handleRename}
  252. disableSetDefault={disableSetDefault}
  253. onItemClick={onItemClick}
  254. showSelectedIcon={showItemSelectedIcon}
  255. selectedCredentialId={selectedCredentialId}
  256. />
  257. ))
  258. }
  259. </div>
  260. )
  261. }
  262. {
  263. !!apiKeyCredentials.length && (
  264. <div className='p-1'>
  265. <div className={cn(
  266. 'system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary',
  267. showItemSelectedIcon && 'pl-7',
  268. )}>
  269. API Keys
  270. </div>
  271. {
  272. apiKeyCredentials.map(credential => (
  273. <Item
  274. key={credential.id}
  275. credential={credential}
  276. disabled={disabled}
  277. onDelete={openConfirm}
  278. onEdit={handleEdit}
  279. onSetDefault={handleSetDefault}
  280. disableSetDefault={disableSetDefault}
  281. disableRename
  282. onItemClick={onItemClick}
  283. onRename={handleRename}
  284. showSelectedIcon={showItemSelectedIcon}
  285. selectedCredentialId={selectedCredentialId}
  286. />
  287. ))
  288. }
  289. </div>
  290. )
  291. }
  292. </div>
  293. <div className='h-[1px] bg-divider-subtle'></div>
  294. <div className='p-2'>
  295. <Authorize
  296. pluginPayload={pluginPayload}
  297. theme='secondary'
  298. showDivider={false}
  299. canOAuth={canOAuth}
  300. canApiKey={canApiKey}
  301. disabled={disabled}
  302. onUpdate={onUpdate}
  303. />
  304. </div>
  305. </div>
  306. </PortalToFollowElemContent>
  307. </PortalToFollowElem>
  308. {
  309. deleteCredentialId && (
  310. <Confirm
  311. isShow
  312. title={t('datasetDocuments.list.delete.title')}
  313. isDisabled={doingAction}
  314. onCancel={closeConfirm}
  315. onConfirm={handleConfirm}
  316. />
  317. )
  318. }
  319. {
  320. !!editValues && (
  321. <ApiKeyModal
  322. pluginPayload={pluginPayload}
  323. editValues={editValues}
  324. onClose={() => {
  325. setEditValues(null)
  326. pendingOperationCredentialId.current = null
  327. }}
  328. onRemove={handleRemove}
  329. disabled={disabled || doingAction}
  330. onUpdate={onUpdate}
  331. />
  332. )
  333. }
  334. </>
  335. )
  336. }
  337. export default memo(Authorized)