Compare commits
32 Commits
main
...
feat/tool-
| Author | SHA1 | Date |
|---|---|---|
|
|
364e0f1450 | 7 months ago |
|
|
25e0013db9 | 7 months ago |
|
|
75d8cb4978 | 7 months ago |
|
|
52e7bb1848 | 7 months ago |
|
|
686b4b8e0e | 7 months ago |
|
|
3f65117007 | 7 months ago |
|
|
0dcdfd6de3 | 7 months ago |
|
|
e1bb6c00df | 7 months ago |
|
|
67b5d73399 | 7 months ago |
|
|
e4cf6a497b | 7 months ago |
|
|
498d8ab33c | 7 months ago |
|
|
a923087b57 | 7 months ago |
|
|
3f273cae73 | 7 months ago |
|
|
964e29b27f | 7 months ago |
|
|
d0ba7adf33 | 7 months ago |
|
|
2572e99a4b | 7 months ago |
|
|
fd0a8d5834 | 7 months ago |
|
|
29035d333d | 7 months ago |
|
|
3b7df2f9b6 | 7 months ago |
|
|
580c9a668f | 7 months ago |
|
|
83ab69d2eb | 7 months ago |
|
|
119d41099d | 7 months ago |
|
|
cb0082c0b8 | 7 months ago |
|
|
90f800408d | 7 months ago |
|
|
5869d6aacc | 7 months ago |
|
|
18699f8671 | 7 months ago |
|
|
bdf5af7a6f | 7 months ago |
|
|
bda76080a9 | 7 months ago |
|
|
8968a3e254 | 7 months ago |
|
|
ce8bf7b5a2 | 7 months ago |
|
|
0f1be60daa | 7 months ago |
|
|
c53d5c105b | 7 months ago |
@ -0,0 +1,177 @@
|
|||||||
|
import {
|
||||||
|
isValidElement,
|
||||||
|
memo,
|
||||||
|
useMemo,
|
||||||
|
} from 'react'
|
||||||
|
import type { AnyFieldApi } from '@tanstack/react-form'
|
||||||
|
import { useStore } from '@tanstack/react-form'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
|
import PureSelect from '@/app/components/base/select/pure'
|
||||||
|
import type { FormSchema } from '@/app/components/base/form/types'
|
||||||
|
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||||
|
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||||
|
|
||||||
|
export type BaseFieldProps = {
|
||||||
|
fieldClassName?: string
|
||||||
|
labelClassName?: string
|
||||||
|
inputContainerClassName?: string
|
||||||
|
inputClassName?: string
|
||||||
|
formSchema: FormSchema
|
||||||
|
field: AnyFieldApi
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
const BaseField = ({
|
||||||
|
fieldClassName,
|
||||||
|
labelClassName,
|
||||||
|
inputContainerClassName,
|
||||||
|
inputClassName,
|
||||||
|
formSchema,
|
||||||
|
field,
|
||||||
|
disabled,
|
||||||
|
}: BaseFieldProps) => {
|
||||||
|
const renderI18nObject = useRenderI18nObject()
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
placeholder,
|
||||||
|
options,
|
||||||
|
labelClassName: formLabelClassName,
|
||||||
|
show_on = [],
|
||||||
|
} = formSchema
|
||||||
|
|
||||||
|
const memorizedLabel = useMemo(() => {
|
||||||
|
if (isValidElement(label))
|
||||||
|
return label
|
||||||
|
|
||||||
|
if (typeof label === 'string')
|
||||||
|
return label
|
||||||
|
|
||||||
|
if (typeof label === 'object' && label !== null)
|
||||||
|
return renderI18nObject(label as Record<string, string>)
|
||||||
|
}, [label, renderI18nObject])
|
||||||
|
const memorizedPlaceholder = useMemo(() => {
|
||||||
|
if (typeof placeholder === 'string')
|
||||||
|
return placeholder
|
||||||
|
|
||||||
|
if (typeof placeholder === 'object' && placeholder !== null)
|
||||||
|
return renderI18nObject(placeholder as Record<string, string>)
|
||||||
|
}, [placeholder, renderI18nObject])
|
||||||
|
const memorizedOptions = useMemo(() => {
|
||||||
|
return options?.map((option) => {
|
||||||
|
return {
|
||||||
|
label: typeof option.label === 'string' ? option.label : renderI18nObject(option.label),
|
||||||
|
value: option.value,
|
||||||
|
}
|
||||||
|
}) || []
|
||||||
|
}, [options, renderI18nObject])
|
||||||
|
const value = useStore(field.form.store, s => s.values[field.name])
|
||||||
|
const values = useStore(field.form.store, (s) => {
|
||||||
|
return show_on.reduce((acc, condition) => {
|
||||||
|
acc[condition.variable] = s.values[condition.variable]
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, any>)
|
||||||
|
})
|
||||||
|
const show = useMemo(() => {
|
||||||
|
return show_on.every((condition) => {
|
||||||
|
const conditionValue = values[condition.variable]
|
||||||
|
return conditionValue === condition.value
|
||||||
|
})
|
||||||
|
}, [values, show_on])
|
||||||
|
|
||||||
|
if (!show)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(fieldClassName)}>
|
||||||
|
<div className={cn(labelClassName, formLabelClassName)}>
|
||||||
|
{memorizedLabel}
|
||||||
|
{
|
||||||
|
required && !isValidElement(label) && (
|
||||||
|
<span className='ml-1 text-text-destructive-secondary'>*</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className={cn(inputContainerClassName)}>
|
||||||
|
{
|
||||||
|
formSchema.type === FormTypeEnum.textInput && (
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
className={cn(inputClassName)}
|
||||||
|
value={value || ''}
|
||||||
|
onChange={e => field.handleChange(e.target.value)}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={memorizedPlaceholder}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
formSchema.type === FormTypeEnum.secretInput && (
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
type='password'
|
||||||
|
className={cn(inputClassName)}
|
||||||
|
value={value || ''}
|
||||||
|
onChange={e => field.handleChange(e.target.value)}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={memorizedPlaceholder}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
formSchema.type === FormTypeEnum.textNumber && (
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
type='number'
|
||||||
|
className={cn(inputClassName)}
|
||||||
|
value={value || ''}
|
||||||
|
onChange={e => field.handleChange(e.target.value)}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={memorizedPlaceholder}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
formSchema.type === FormTypeEnum.select && (
|
||||||
|
<PureSelect
|
||||||
|
value={value}
|
||||||
|
onChange={v => field.handleChange(v)}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={memorizedPlaceholder}
|
||||||
|
options={memorizedOptions}
|
||||||
|
triggerPopupSameWidth
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
formSchema.type === FormTypeEnum.radio && (
|
||||||
|
<div className='flex items-center space-x-2'>
|
||||||
|
{
|
||||||
|
memorizedOptions.map(option => (
|
||||||
|
<div
|
||||||
|
key={option.value}
|
||||||
|
className={cn(
|
||||||
|
'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 grow cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary',
|
||||||
|
value === option.value && 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary shadow-xs',
|
||||||
|
)}
|
||||||
|
onClick={() => field.handleChange(option.value)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(BaseField)
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
import {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from 'react'
|
||||||
|
import type {
|
||||||
|
AnyFieldApi,
|
||||||
|
AnyFormApi,
|
||||||
|
} from '@tanstack/react-form'
|
||||||
|
import { useForm } from '@tanstack/react-form'
|
||||||
|
import type {
|
||||||
|
FormRef,
|
||||||
|
FormSchema,
|
||||||
|
} from '@/app/components/base/form/types'
|
||||||
|
import {
|
||||||
|
BaseField,
|
||||||
|
} from '.'
|
||||||
|
import type {
|
||||||
|
BaseFieldProps,
|
||||||
|
} from '.'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import {
|
||||||
|
useGetFormValues,
|
||||||
|
useGetValidators,
|
||||||
|
} from '@/app/components/base/form/hooks'
|
||||||
|
|
||||||
|
export type BaseFormProps = {
|
||||||
|
formSchemas?: FormSchema[]
|
||||||
|
defaultValues?: Record<string, any>
|
||||||
|
formClassName?: string
|
||||||
|
ref?: FormRef
|
||||||
|
disabled?: boolean
|
||||||
|
formFromProps?: AnyFormApi
|
||||||
|
} & Pick<BaseFieldProps, 'fieldClassName' | 'labelClassName' | 'inputContainerClassName' | 'inputClassName'>
|
||||||
|
|
||||||
|
const BaseForm = ({
|
||||||
|
formSchemas = [],
|
||||||
|
defaultValues,
|
||||||
|
formClassName,
|
||||||
|
fieldClassName,
|
||||||
|
labelClassName,
|
||||||
|
inputContainerClassName,
|
||||||
|
inputClassName,
|
||||||
|
ref,
|
||||||
|
disabled,
|
||||||
|
formFromProps,
|
||||||
|
}: BaseFormProps) => {
|
||||||
|
const formFromHook = useForm({
|
||||||
|
defaultValues,
|
||||||
|
})
|
||||||
|
const form: any = formFromProps || formFromHook
|
||||||
|
const { getFormValues } = useGetFormValues(form, formSchemas)
|
||||||
|
const { getValidators } = useGetValidators()
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => {
|
||||||
|
return {
|
||||||
|
getForm() {
|
||||||
|
return form
|
||||||
|
},
|
||||||
|
getFormValues: (option) => {
|
||||||
|
return getFormValues(option)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, [form, getFormValues])
|
||||||
|
|
||||||
|
const renderField = useCallback((field: AnyFieldApi) => {
|
||||||
|
const formSchema = formSchemas?.find(schema => schema.name === field.name)
|
||||||
|
|
||||||
|
if (formSchema) {
|
||||||
|
return (
|
||||||
|
<BaseField
|
||||||
|
field={field}
|
||||||
|
formSchema={formSchema}
|
||||||
|
fieldClassName={fieldClassName}
|
||||||
|
labelClassName={labelClassName}
|
||||||
|
inputContainerClassName={inputContainerClassName}
|
||||||
|
inputClassName={inputClassName}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled])
|
||||||
|
|
||||||
|
const renderFieldWrapper = useCallback((formSchema: FormSchema) => {
|
||||||
|
const validators = getValidators(formSchema)
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
} = formSchema
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form.Field
|
||||||
|
key={name}
|
||||||
|
name={name}
|
||||||
|
validators={validators}
|
||||||
|
>
|
||||||
|
{renderField}
|
||||||
|
</form.Field>
|
||||||
|
)
|
||||||
|
}, [renderField, form, getValidators])
|
||||||
|
|
||||||
|
if (!formSchemas?.length)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className={cn(formClassName)}
|
||||||
|
>
|
||||||
|
{formSchemas.map(renderFieldWrapper)}
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(BaseForm)
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export { default as BaseForm, type BaseFormProps } from './base-form'
|
||||||
|
export { default as BaseField, type BaseFieldProps } from './base-field'
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import { BaseForm } from '../../components/base'
|
||||||
|
import type { BaseFormProps } from '../../components/base'
|
||||||
|
|
||||||
|
const AuthForm = ({
|
||||||
|
formSchemas = [],
|
||||||
|
defaultValues,
|
||||||
|
ref,
|
||||||
|
formFromProps,
|
||||||
|
}: BaseFormProps) => {
|
||||||
|
return (
|
||||||
|
<BaseForm
|
||||||
|
ref={ref}
|
||||||
|
formSchemas={formSchemas}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
formClassName='space-y-4'
|
||||||
|
labelClassName='h-6 flex items-center mb-1 system-sm-medium text-text-secondary'
|
||||||
|
formFromProps={formFromProps}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(AuthForm)
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export * from './use-check-validated'
|
||||||
|
export * from './use-get-form-values'
|
||||||
|
export * from './use-get-validators'
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import type { AnyFormApi } from '@tanstack/react-form'
|
||||||
|
import { useToastContext } from '@/app/components/base/toast'
|
||||||
|
import type { FormSchema } from '@/app/components/base/form/types'
|
||||||
|
|
||||||
|
export const useCheckValidated = (form: AnyFormApi, FormSchemas: FormSchema[]) => {
|
||||||
|
const { notify } = useToastContext()
|
||||||
|
|
||||||
|
const checkValidated = useCallback(() => {
|
||||||
|
const allError = form?.getAllErrors()
|
||||||
|
const values = form.state.values
|
||||||
|
|
||||||
|
if (allError) {
|
||||||
|
const fields = allError.fields
|
||||||
|
const errorArray = Object.keys(fields).reduce((acc: string[], key: string) => {
|
||||||
|
const currentSchema = FormSchemas.find(schema => schema.name === key)
|
||||||
|
const { show_on = [] } = currentSchema || {}
|
||||||
|
const showOnValues = show_on.reduce((acc, condition) => {
|
||||||
|
acc[condition.variable] = values[condition.variable]
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, any>)
|
||||||
|
const show = show_on?.every((condition) => {
|
||||||
|
const conditionValue = showOnValues[condition.variable]
|
||||||
|
return conditionValue === condition.value
|
||||||
|
})
|
||||||
|
const errors: any[] = show ? fields[key].errors : []
|
||||||
|
|
||||||
|
return [...acc, ...errors]
|
||||||
|
}, [] as string[])
|
||||||
|
|
||||||
|
if (errorArray.length) {
|
||||||
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: errorArray[0],
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, [form, notify, FormSchemas])
|
||||||
|
|
||||||
|
return {
|
||||||
|
checkValidated,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import type { AnyFormApi } from '@tanstack/react-form'
|
||||||
|
import { useCheckValidated } from './use-check-validated'
|
||||||
|
import type {
|
||||||
|
FormSchema,
|
||||||
|
GetValuesOptions,
|
||||||
|
} from '../types'
|
||||||
|
import { getTransformedValuesWhenSecretInputPristine } from '../utils'
|
||||||
|
|
||||||
|
export const useGetFormValues = (form: AnyFormApi, formSchemas: FormSchema[]) => {
|
||||||
|
const { checkValidated } = useCheckValidated(form, formSchemas)
|
||||||
|
|
||||||
|
const getFormValues = useCallback((
|
||||||
|
{
|
||||||
|
needCheckValidatedValues,
|
||||||
|
needTransformWhenSecretFieldIsPristine,
|
||||||
|
}: GetValuesOptions,
|
||||||
|
) => {
|
||||||
|
const values = form?.store.state.values || {}
|
||||||
|
if (!needCheckValidatedValues) {
|
||||||
|
return {
|
||||||
|
values,
|
||||||
|
isCheckValidated: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkValidated()) {
|
||||||
|
return {
|
||||||
|
values: needTransformWhenSecretFieldIsPristine ? getTransformedValuesWhenSecretInputPristine(formSchemas, form) : values,
|
||||||
|
isCheckValidated: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {
|
||||||
|
values: {},
|
||||||
|
isCheckValidated: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [form, checkValidated, formSchemas])
|
||||||
|
|
||||||
|
return {
|
||||||
|
getFormValues,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import type { FormSchema } from '../types'
|
||||||
|
|
||||||
|
export const useGetValidators = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const getValidators = useCallback((formSchema: FormSchema) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
validators,
|
||||||
|
required,
|
||||||
|
} = formSchema
|
||||||
|
let mergedValidators = validators
|
||||||
|
if (required && !validators) {
|
||||||
|
mergedValidators = {
|
||||||
|
onMount: ({ value }: any) => {
|
||||||
|
if (!value)
|
||||||
|
return t('common.errorMsg.fieldRequired', { field: name })
|
||||||
|
},
|
||||||
|
onChange: ({ value }: any) => {
|
||||||
|
if (!value)
|
||||||
|
return t('common.errorMsg.fieldRequired', { field: name })
|
||||||
|
},
|
||||||
|
onBlur: ({ value }: any) => {
|
||||||
|
if (!value)
|
||||||
|
return t('common.errorMsg.fieldRequired', { field: name })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mergedValidators
|
||||||
|
}, [t])
|
||||||
|
|
||||||
|
return {
|
||||||
|
getValidators,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
import type {
|
||||||
|
ForwardedRef,
|
||||||
|
ReactNode,
|
||||||
|
} from 'react'
|
||||||
|
import type {
|
||||||
|
AnyFormApi,
|
||||||
|
FieldValidators,
|
||||||
|
} from '@tanstack/react-form'
|
||||||
|
|
||||||
|
export type TypeWithI18N<T = string> = {
|
||||||
|
en_US: T
|
||||||
|
zh_Hans: T
|
||||||
|
[key: string]: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormShowOnObject = {
|
||||||
|
variable: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FormTypeEnum {
|
||||||
|
textInput = 'text-input',
|
||||||
|
textNumber = 'number-input',
|
||||||
|
secretInput = 'secret-input',
|
||||||
|
select = 'select',
|
||||||
|
radio = 'radio',
|
||||||
|
boolean = 'boolean',
|
||||||
|
files = 'files',
|
||||||
|
file = 'file',
|
||||||
|
modelSelector = 'model-selector',
|
||||||
|
toolSelector = 'tool-selector',
|
||||||
|
multiToolSelector = 'array[tools]',
|
||||||
|
appSelector = 'app-selector',
|
||||||
|
dynamicSelect = 'dynamic-select',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormOption = {
|
||||||
|
label: TypeWithI18N | string
|
||||||
|
value: string
|
||||||
|
show_on?: FormShowOnObject[]
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnyValidators = FieldValidators<any, any, any, any, any, any, any, any, any, any>
|
||||||
|
|
||||||
|
export type FormSchema = {
|
||||||
|
type: FormTypeEnum
|
||||||
|
name: string
|
||||||
|
label: string | ReactNode | TypeWithI18N
|
||||||
|
required: boolean
|
||||||
|
default?: any
|
||||||
|
tooltip?: string | TypeWithI18N
|
||||||
|
show_on?: FormShowOnObject[]
|
||||||
|
url?: string
|
||||||
|
scope?: string
|
||||||
|
help?: string | TypeWithI18N
|
||||||
|
placeholder?: string | TypeWithI18N
|
||||||
|
options?: FormOption[]
|
||||||
|
labelClassName?: string
|
||||||
|
validators?: AnyValidators
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormValues = Record<string, any>
|
||||||
|
|
||||||
|
export type GetValuesOptions = {
|
||||||
|
needTransformWhenSecretFieldIsPristine?: boolean
|
||||||
|
needCheckValidatedValues?: boolean
|
||||||
|
}
|
||||||
|
export type FormRefObject = {
|
||||||
|
getForm: () => AnyFormApi
|
||||||
|
getFormValues: (obj: GetValuesOptions) => {
|
||||||
|
values: Record<string, any>
|
||||||
|
isCheckValidated: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export type FormRef = ForwardedRef<FormRefObject>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './secret-input'
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import type { AnyFormApi } from '@tanstack/react-form'
|
||||||
|
import type { FormSchema } from '@/app/components/base/form/types'
|
||||||
|
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||||
|
|
||||||
|
export const transformFormSchemasSecretInput = (isPristineSecretInputNames: string[], values: Record<string, any>) => {
|
||||||
|
const transformedValues: Record<string, any> = { ...values }
|
||||||
|
|
||||||
|
isPristineSecretInputNames.forEach((name) => {
|
||||||
|
if (transformedValues[name])
|
||||||
|
transformedValues[name] = '[__HIDDEN__]'
|
||||||
|
})
|
||||||
|
|
||||||
|
return transformedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTransformedValuesWhenSecretInputPristine = (formSchemas: FormSchema[], form: AnyFormApi) => {
|
||||||
|
const values = form?.store.state.values || {}
|
||||||
|
const isPristineSecretInputNames: string[] = []
|
||||||
|
for (let i = 0; i < formSchemas.length; i++) {
|
||||||
|
const schema = formSchemas[i]
|
||||||
|
if (schema.type === FormTypeEnum.secretInput) {
|
||||||
|
const fieldMeta = form?.getFieldMeta(schema.name)
|
||||||
|
if (fieldMeta?.isPristine)
|
||||||
|
isPristineSecretInputNames.push(schema.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformFormSchemasSecretInput(isPristineSecretInputNames, values)
|
||||||
|
}
|
||||||
@ -0,0 +1,127 @@
|
|||||||
|
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
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
const Modal = ({
|
||||||
|
onClose,
|
||||||
|
size = 'sm',
|
||||||
|
title,
|
||||||
|
subTitle,
|
||||||
|
children,
|
||||||
|
confirmButtonText,
|
||||||
|
onConfirm,
|
||||||
|
cancelButtonText,
|
||||||
|
onCancel,
|
||||||
|
showExtraButton,
|
||||||
|
extraButtonVariant = 'warning',
|
||||||
|
extraButtonText,
|
||||||
|
onExtraButtonClick,
|
||||||
|
footerSlot,
|
||||||
|
bottomSlot,
|
||||||
|
disabled,
|
||||||
|
}: ModalProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PortalToFollowElem open>
|
||||||
|
<PortalToFollowElemContent
|
||||||
|
className='z-[9998] 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 justify-between p-6 pt-5'>
|
||||||
|
<div>
|
||||||
|
{footerSlot}
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
{
|
||||||
|
showExtraButton && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant={extraButtonVariant}
|
||||||
|
onClick={onExtraButtonClick}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{extraButtonText || t('common.operation.remove')}
|
||||||
|
</Button>
|
||||||
|
<div className='mx-3 h-4 w-[1px] bg-divider-regular'></div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<Button
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{cancelButtonText || t('common.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className='ml-2'
|
||||||
|
variant='primary'
|
||||||
|
onClick={onConfirm}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{confirmButtonText || t('common.operation.save')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{bottomSlot}
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</PortalToFollowElem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(Modal)
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
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'
|
||||||
|
import type { PluginPayload } from '../types'
|
||||||
|
|
||||||
|
export type AddApiKeyButtonProps = {
|
||||||
|
pluginPayload: PluginPayload
|
||||||
|
buttonVariant?: ButtonProps['variant']
|
||||||
|
buttonText?: string
|
||||||
|
disabled?: boolean
|
||||||
|
onUpdate?: () => void
|
||||||
|
}
|
||||||
|
const AddApiKeyButton = ({
|
||||||
|
pluginPayload,
|
||||||
|
buttonVariant = 'secondary-accent',
|
||||||
|
buttonText = 'use api key',
|
||||||
|
disabled,
|
||||||
|
onUpdate,
|
||||||
|
}: AddApiKeyButtonProps) => {
|
||||||
|
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
className='w-full'
|
||||||
|
variant={buttonVariant}
|
||||||
|
onClick={() => setIsApiKeyModalOpen(true)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
|
{
|
||||||
|
isApiKeyModalOpen && (
|
||||||
|
<ApiKeyModal
|
||||||
|
pluginPayload={pluginPayload}
|
||||||
|
onClose={() => setIsApiKeyModalOpen(false)}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(AddApiKeyButton)
|
||||||
@ -0,0 +1,259 @@
|
|||||||
|
import {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import {
|
||||||
|
RiClipboardLine,
|
||||||
|
RiEqualizer2Line,
|
||||||
|
RiInformation2Fill,
|
||||||
|
} 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'
|
||||||
|
import type { PluginPayload } from '../types'
|
||||||
|
import { openOAuthPopup } from '@/hooks/use-oauth'
|
||||||
|
import Badge from '@/app/components/base/badge'
|
||||||
|
import {
|
||||||
|
useGetPluginOAuthClientSchemaHook,
|
||||||
|
useGetPluginOAuthUrlHook,
|
||||||
|
} from '../hooks/use-credential'
|
||||||
|
import type { FormSchema } from '@/app/components/base/form/types'
|
||||||
|
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||||
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
|
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||||
|
|
||||||
|
export type AddOAuthButtonProps = {
|
||||||
|
pluginPayload: PluginPayload
|
||||||
|
buttonVariant?: ButtonProps['variant']
|
||||||
|
buttonText?: string
|
||||||
|
className?: string
|
||||||
|
buttonLeftClassName?: string
|
||||||
|
buttonRightClassName?: string
|
||||||
|
dividerClassName?: string
|
||||||
|
disabled?: boolean
|
||||||
|
onUpdate?: () => void
|
||||||
|
}
|
||||||
|
const AddOAuthButton = ({
|
||||||
|
pluginPayload,
|
||||||
|
buttonVariant = 'primary',
|
||||||
|
buttonText = 'use oauth',
|
||||||
|
className,
|
||||||
|
buttonLeftClassName,
|
||||||
|
buttonRightClassName,
|
||||||
|
dividerClassName,
|
||||||
|
disabled,
|
||||||
|
onUpdate,
|
||||||
|
}: AddOAuthButtonProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const renderI18nObject = useRenderI18nObject()
|
||||||
|
const [isOAuthSettingsOpen, setIsOAuthSettingsOpen] = useState(false)
|
||||||
|
const { mutateAsync: getPluginOAuthUrl } = useGetPluginOAuthUrlHook(pluginPayload)
|
||||||
|
const { data, isLoading } = useGetPluginOAuthClientSchemaHook(pluginPayload)
|
||||||
|
const {
|
||||||
|
schema = [],
|
||||||
|
is_oauth_custom_client_enabled,
|
||||||
|
is_system_oauth_params_exists,
|
||||||
|
client_params,
|
||||||
|
redirect_uri,
|
||||||
|
} = data || {}
|
||||||
|
const isConfigured = is_system_oauth_params_exists || is_oauth_custom_client_enabled
|
||||||
|
const handleOAuth = useCallback(async () => {
|
||||||
|
const { authorization_url } = await getPluginOAuthUrl()
|
||||||
|
|
||||||
|
if (authorization_url) {
|
||||||
|
openOAuthPopup(
|
||||||
|
authorization_url,
|
||||||
|
() => onUpdate?.(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [getPluginOAuthUrl, onUpdate])
|
||||||
|
|
||||||
|
const renderCustomLabel = useCallback((item: FormSchema) => {
|
||||||
|
return (
|
||||||
|
<div className='w-full'>
|
||||||
|
<div className='mb-4 flex rounded-xl bg-background-section-burn p-4'>
|
||||||
|
<div className='mr-3 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg'>
|
||||||
|
<RiInformation2Fill className='h-5 w-5 text-text-accent' />
|
||||||
|
</div>
|
||||||
|
<div className='w-0 grow'>
|
||||||
|
<div className='system-sm-regular mb-1.5'>
|
||||||
|
{t('plugin.auth.clientInfo')}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
redirect_uri && (
|
||||||
|
<div className='system-sm-medium flex w-full py-0.5'>
|
||||||
|
<div className='w-0 grow break-words'>{redirect_uri}</div>
|
||||||
|
<ActionButton
|
||||||
|
className='shrink-0'
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(redirect_uri || '')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RiClipboardLine className='h-4 w-4' />
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='system-sm-medium flex h-6 items-center text-text-secondary'>
|
||||||
|
{renderI18nObject(item.label as Record<string, string>)}
|
||||||
|
{
|
||||||
|
item.required && (
|
||||||
|
<span className='ml-1 text-text-destructive-secondary'>*</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}, [t, redirect_uri, renderI18nObject])
|
||||||
|
const memorizedSchemas = useMemo(() => {
|
||||||
|
const result: FormSchema[] = schema.map((item, index) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
label: index === 0 ? renderCustomLabel(item) : item.label,
|
||||||
|
labelClassName: index === 0 ? 'h-auto' : undefined,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (is_system_oauth_params_exists) {
|
||||||
|
result.unshift({
|
||||||
|
name: '__oauth_client__',
|
||||||
|
label: t('plugin.auth.oauthClient'),
|
||||||
|
type: FormTypeEnum.radio,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: t('plugin.auth.default'),
|
||||||
|
value: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('plugin.auth.custom'),
|
||||||
|
value: 'custom',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
required: false,
|
||||||
|
default: is_oauth_custom_client_enabled ? 'custom' : 'default',
|
||||||
|
} as FormSchema)
|
||||||
|
result.forEach((item, index) => {
|
||||||
|
if (index > 0) {
|
||||||
|
item.show_on = [
|
||||||
|
{
|
||||||
|
variable: '__oauth_client__',
|
||||||
|
value: 'custom',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
if (client_params)
|
||||||
|
item.default = client_params[item.name] || item.default
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}, [schema, renderCustomLabel, t, is_system_oauth_params_exists, is_oauth_custom_client_enabled, client_params])
|
||||||
|
|
||||||
|
const __auth_client__ = useMemo(() => {
|
||||||
|
if (isConfigured) {
|
||||||
|
if (is_oauth_custom_client_enabled)
|
||||||
|
return 'custom'
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (is_system_oauth_params_exists)
|
||||||
|
return 'default'
|
||||||
|
return 'custom'
|
||||||
|
}
|
||||||
|
}, [isConfigured, is_oauth_custom_client_enabled, is_system_oauth_params_exists])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
isConfigured && (
|
||||||
|
<Button
|
||||||
|
variant={buttonVariant}
|
||||||
|
className={cn(
|
||||||
|
'w-full px-0 py-0 hover:bg-components-button-primary-bg',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={handleOAuth}
|
||||||
|
>
|
||||||
|
<div className={cn(
|
||||||
|
'flex h-full w-0 grow items-center justify-center rounded-l-lg pl-0.5 hover:bg-components-button-primary-bg-hover',
|
||||||
|
buttonLeftClassName,
|
||||||
|
)}>
|
||||||
|
<div
|
||||||
|
className='truncate'
|
||||||
|
title={buttonText}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
is_oauth_custom_client_enabled && (
|
||||||
|
<Badge
|
||||||
|
className={cn(
|
||||||
|
'ml-1 mr-0.5',
|
||||||
|
buttonVariant === 'primary' && 'border-text-primary-on-surface bg-components-badge-bg-dimm text-text-primary-on-surface',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t('plugin.auth.custom')}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className={cn(
|
||||||
|
'h-4 w-[1px] shrink-0 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={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setIsOAuthSettingsOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RiEqualizer2Line className='h-4 w-4' />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!isConfigured && (
|
||||||
|
<Button
|
||||||
|
variant={buttonVariant}
|
||||||
|
onClick={() => setIsOAuthSettingsOpen(true)}
|
||||||
|
disabled={disabled}
|
||||||
|
className='w-full'
|
||||||
|
>
|
||||||
|
<RiEqualizer2Line className='mr-0.5 h-4 w-4' />
|
||||||
|
{t('plugin.auth.setupOAuth')}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
isOAuthSettingsOpen && (
|
||||||
|
<OAuthClientSettings
|
||||||
|
pluginPayload={pluginPayload}
|
||||||
|
onClose={() => setIsOAuthSettingsOpen(false)}
|
||||||
|
disabled={disabled || isLoading}
|
||||||
|
schemas={memorizedSchemas}
|
||||||
|
onAuth={handleOAuth}
|
||||||
|
editValues={{
|
||||||
|
...client_params,
|
||||||
|
__oauth_client__: __auth_client__,
|
||||||
|
}}
|
||||||
|
hasOriginalClientParams={Object.keys(client_params || {}).length > 0}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(AddOAuthButton)
|
||||||
@ -0,0 +1,181 @@
|
|||||||
|
import {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} 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'
|
||||||
|
import { CredentialTypeEnum } from '../types'
|
||||||
|
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
|
||||||
|
import type { FormRefObject } 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'
|
||||||
|
import type { PluginPayload } from '../types'
|
||||||
|
import {
|
||||||
|
useAddPluginCredentialHook,
|
||||||
|
useGetPluginCredentialSchemaHook,
|
||||||
|
useUpdatePluginCredentialHook,
|
||||||
|
} from '../hooks/use-credential'
|
||||||
|
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||||
|
|
||||||
|
export type ApiKeyModalProps = {
|
||||||
|
pluginPayload: PluginPayload
|
||||||
|
onClose?: () => void
|
||||||
|
editValues?: Record<string, any>
|
||||||
|
onRemove?: () => void
|
||||||
|
disabled?: boolean
|
||||||
|
onUpdate?: () => void
|
||||||
|
}
|
||||||
|
const ApiKeyModal = ({
|
||||||
|
pluginPayload,
|
||||||
|
onClose,
|
||||||
|
editValues,
|
||||||
|
onRemove,
|
||||||
|
disabled,
|
||||||
|
onUpdate,
|
||||||
|
}: ApiKeyModalProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { notify } = useToastContext()
|
||||||
|
const [doingAction, setDoingAction] = useState(false)
|
||||||
|
const doingActionRef = useRef(doingAction)
|
||||||
|
const handleSetDoingAction = useCallback((value: boolean) => {
|
||||||
|
doingActionRef.current = value
|
||||||
|
setDoingAction(value)
|
||||||
|
}, [])
|
||||||
|
const { data = [], isLoading } = useGetPluginCredentialSchemaHook(pluginPayload, CredentialTypeEnum.API_KEY)
|
||||||
|
const formSchemas = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: FormTypeEnum.textInput,
|
||||||
|
name: '__name__',
|
||||||
|
label: t('plugin.auth.authorizationName'),
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
...data,
|
||||||
|
]
|
||||||
|
}, [data, t])
|
||||||
|
const defaultValues = formSchemas.reduce((acc, schema) => {
|
||||||
|
if (schema.default)
|
||||||
|
acc[schema.name] = schema.default
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, any>)
|
||||||
|
const helpField = formSchemas.find(schema => schema.url && schema.help)
|
||||||
|
const renderI18nObject = useRenderI18nObject()
|
||||||
|
const { mutateAsync: addPluginCredential } = useAddPluginCredentialHook(pluginPayload)
|
||||||
|
const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
|
||||||
|
const formRef = useRef<FormRefObject>(null)
|
||||||
|
const handleConfirm = useCallback(async () => {
|
||||||
|
if (doingActionRef.current)
|
||||||
|
return
|
||||||
|
const {
|
||||||
|
isCheckValidated,
|
||||||
|
values,
|
||||||
|
} = formRef.current?.getFormValues({
|
||||||
|
needCheckValidatedValues: true,
|
||||||
|
needTransformWhenSecretFieldIsPristine: true,
|
||||||
|
}) || { isCheckValidated: false, values: {} }
|
||||||
|
if (!isCheckValidated)
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
__name__,
|
||||||
|
__credential_id__,
|
||||||
|
...restValues
|
||||||
|
} = values
|
||||||
|
|
||||||
|
handleSetDoingAction(true)
|
||||||
|
if (editValues) {
|
||||||
|
await updatePluginCredential({
|
||||||
|
credentials: restValues,
|
||||||
|
credential_id: __credential_id__,
|
||||||
|
name: __name__ || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await addPluginCredential({
|
||||||
|
credentials: restValues,
|
||||||
|
type: CredentialTypeEnum.API_KEY,
|
||||||
|
name: __name__ || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
notify({
|
||||||
|
type: 'success',
|
||||||
|
message: t('common.api.actionSuccess'),
|
||||||
|
})
|
||||||
|
|
||||||
|
onClose?.()
|
||||||
|
onUpdate?.()
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
handleSetDoingAction(false)
|
||||||
|
}
|
||||||
|
}, [addPluginCredential, onClose, onUpdate, updatePluginCredential, notify, t, editValues, handleSetDoingAction])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
size='md'
|
||||||
|
title={t('plugin.auth.useApiAuth')}
|
||||||
|
subTitle={t('plugin.auth.useApiAuthDesc')}
|
||||||
|
onClose={onClose}
|
||||||
|
onCancel={onClose}
|
||||||
|
footerSlot={
|
||||||
|
helpField && (
|
||||||
|
<a
|
||||||
|
className='system-xs-regular mr-2 flex items-center py-2 text-text-accent'
|
||||||
|
href={helpField?.url}
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
<span className='break-all'>
|
||||||
|
{renderI18nObject(helpField?.help as any)}
|
||||||
|
</span>
|
||||||
|
<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>
|
||||||
|
}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
showExtraButton={!!editValues}
|
||||||
|
onExtraButtonClick={onRemove}
|
||||||
|
disabled={disabled || isLoading || doingAction}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
isLoading && (
|
||||||
|
<div className='flex h-40 items-center justify-center'>
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!isLoading && !!data.length && (
|
||||||
|
<AuthForm
|
||||||
|
ref={formRef}
|
||||||
|
formSchemas={formSchemas}
|
||||||
|
defaultValues={editValues || defaultValues}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ApiKeyModal)
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
import {
|
||||||
|
memo,
|
||||||
|
useMemo,
|
||||||
|
} from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import AddOAuthButton from './add-oauth-button'
|
||||||
|
import type { AddOAuthButtonProps } from './add-oauth-button'
|
||||||
|
import AddApiKeyButton from './add-api-key-button'
|
||||||
|
import type { AddApiKeyButtonProps } from './add-api-key-button'
|
||||||
|
import type { PluginPayload } from '../types'
|
||||||
|
|
||||||
|
type AuthorizeProps = {
|
||||||
|
pluginPayload: PluginPayload
|
||||||
|
theme?: 'primary' | 'secondary'
|
||||||
|
showDivider?: boolean
|
||||||
|
canOAuth?: boolean
|
||||||
|
canApiKey?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
onUpdate?: () => void
|
||||||
|
}
|
||||||
|
const Authorize = ({
|
||||||
|
pluginPayload,
|
||||||
|
theme = 'primary',
|
||||||
|
showDivider = true,
|
||||||
|
canOAuth,
|
||||||
|
canApiKey,
|
||||||
|
disabled,
|
||||||
|
onUpdate,
|
||||||
|
}: AuthorizeProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const oAuthButtonProps: AddOAuthButtonProps = useMemo(() => {
|
||||||
|
if (theme === 'secondary') {
|
||||||
|
return {
|
||||||
|
buttonText: !canApiKey ? t('plugin.auth.useOAuthAuth') : t('plugin.auth.addOAuth'),
|
||||||
|
buttonVariant: 'secondary',
|
||||||
|
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',
|
||||||
|
pluginPayload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buttonText: !canApiKey ? t('plugin.auth.useOAuthAuth') : t('plugin.auth.addOAuth'),
|
||||||
|
pluginPayload,
|
||||||
|
}
|
||||||
|
}, [canApiKey, theme, pluginPayload, t])
|
||||||
|
|
||||||
|
const apiKeyButtonProps: AddApiKeyButtonProps = useMemo(() => {
|
||||||
|
if (theme === 'secondary') {
|
||||||
|
return {
|
||||||
|
pluginPayload,
|
||||||
|
buttonVariant: 'secondary',
|
||||||
|
buttonText: !canOAuth ? t('plugin.auth.useApiAuth') : t('plugin.auth.addApi'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
pluginPayload,
|
||||||
|
buttonText: !canOAuth ? t('plugin.auth.useApiAuth') : t('plugin.auth.addApi'),
|
||||||
|
buttonVariant: !canOAuth ? 'primary' : 'secondary-accent',
|
||||||
|
}
|
||||||
|
}, [canOAuth, theme, pluginPayload, t])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='flex items-center space-x-1.5'>
|
||||||
|
{
|
||||||
|
canOAuth && (
|
||||||
|
<div className='min-w-0 flex-[1]'>
|
||||||
|
<AddOAuthButton
|
||||||
|
{...oAuthButtonProps}
|
||||||
|
disabled={disabled}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
showDivider && canOAuth && canApiKey && (
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
canApiKey && (
|
||||||
|
<div className='min-w-0 flex-[1]'>
|
||||||
|
<AddApiKeyButton
|
||||||
|
{...apiKeyButtonProps}
|
||||||
|
disabled={disabled}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(Authorize)
|
||||||
@ -0,0 +1,188 @@
|
|||||||
|
import {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import { RiExternalLinkLine } from '@remixicon/react'
|
||||||
|
import {
|
||||||
|
useForm,
|
||||||
|
useStore,
|
||||||
|
} from '@tanstack/react-form'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Modal from '@/app/components/base/modal/modal'
|
||||||
|
import {
|
||||||
|
useDeletePluginOAuthCustomClientHook,
|
||||||
|
useInvalidPluginOAuthClientSchemaHook,
|
||||||
|
useSetPluginOAuthCustomClientHook,
|
||||||
|
} from '../hooks/use-credential'
|
||||||
|
import type { PluginPayload } from '../types'
|
||||||
|
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
|
||||||
|
import type {
|
||||||
|
FormRefObject,
|
||||||
|
FormSchema,
|
||||||
|
} from '@/app/components/base/form/types'
|
||||||
|
import { useToastContext } from '@/app/components/base/toast'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||||
|
|
||||||
|
type OAuthClientSettingsProps = {
|
||||||
|
pluginPayload: PluginPayload
|
||||||
|
onClose?: () => void
|
||||||
|
editValues?: Record<string, any>
|
||||||
|
disabled?: boolean
|
||||||
|
schemas: FormSchema[]
|
||||||
|
onAuth?: () => Promise<void>
|
||||||
|
hasOriginalClientParams?: boolean
|
||||||
|
onUpdate?: () => void
|
||||||
|
}
|
||||||
|
const OAuthClientSettings = ({
|
||||||
|
pluginPayload,
|
||||||
|
onClose,
|
||||||
|
editValues,
|
||||||
|
disabled,
|
||||||
|
schemas,
|
||||||
|
onAuth,
|
||||||
|
hasOriginalClientParams,
|
||||||
|
onUpdate,
|
||||||
|
}: OAuthClientSettingsProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { notify } = useToastContext()
|
||||||
|
const [doingAction, setDoingAction] = useState(false)
|
||||||
|
const doingActionRef = useRef(doingAction)
|
||||||
|
const handleSetDoingAction = useCallback((value: boolean) => {
|
||||||
|
doingActionRef.current = value
|
||||||
|
setDoingAction(value)
|
||||||
|
}, [])
|
||||||
|
const defaultValues = schemas.reduce((acc, schema) => {
|
||||||
|
if (schema.default)
|
||||||
|
acc[schema.name] = schema.default
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, any>)
|
||||||
|
const { mutateAsync: setPluginOAuthCustomClient } = useSetPluginOAuthCustomClientHook(pluginPayload)
|
||||||
|
const invalidPluginOAuthClientSchema = useInvalidPluginOAuthClientSchemaHook(pluginPayload)
|
||||||
|
const formRef = useRef<FormRefObject>(null)
|
||||||
|
const handleConfirm = useCallback(async () => {
|
||||||
|
if (doingActionRef.current)
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
isCheckValidated,
|
||||||
|
values,
|
||||||
|
} = formRef.current?.getFormValues({
|
||||||
|
needCheckValidatedValues: true,
|
||||||
|
needTransformWhenSecretFieldIsPristine: true,
|
||||||
|
}) || { isCheckValidated: false, values: {} }
|
||||||
|
if (!isCheckValidated)
|
||||||
|
throw new Error('error')
|
||||||
|
const {
|
||||||
|
__oauth_client__,
|
||||||
|
...restValues
|
||||||
|
} = values
|
||||||
|
|
||||||
|
handleSetDoingAction(true)
|
||||||
|
await setPluginOAuthCustomClient({
|
||||||
|
client_params: restValues,
|
||||||
|
enable_oauth_custom_client: __oauth_client__ === 'custom',
|
||||||
|
})
|
||||||
|
notify({
|
||||||
|
type: 'success',
|
||||||
|
message: t('common.api.actionSuccess'),
|
||||||
|
})
|
||||||
|
|
||||||
|
onClose?.()
|
||||||
|
onUpdate?.()
|
||||||
|
invalidPluginOAuthClientSchema()
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
handleSetDoingAction(false)
|
||||||
|
}
|
||||||
|
}, [onClose, onUpdate, invalidPluginOAuthClientSchema, setPluginOAuthCustomClient, notify, t, handleSetDoingAction])
|
||||||
|
|
||||||
|
const handleConfirmAndAuthorize = useCallback(async () => {
|
||||||
|
await handleConfirm()
|
||||||
|
if (onAuth)
|
||||||
|
await onAuth()
|
||||||
|
}, [handleConfirm, onAuth])
|
||||||
|
const { mutateAsync: deletePluginOAuthCustomClient } = useDeletePluginOAuthCustomClientHook(pluginPayload)
|
||||||
|
const handleRemove = useCallback(async () => {
|
||||||
|
if (doingActionRef.current)
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
handleSetDoingAction(true)
|
||||||
|
await deletePluginOAuthCustomClient()
|
||||||
|
notify({
|
||||||
|
type: 'success',
|
||||||
|
message: t('common.api.actionSuccess'),
|
||||||
|
})
|
||||||
|
onClose?.()
|
||||||
|
onUpdate?.()
|
||||||
|
invalidPluginOAuthClientSchema()
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
handleSetDoingAction(false)
|
||||||
|
}
|
||||||
|
}, [onUpdate, invalidPluginOAuthClientSchema, deletePluginOAuthCustomClient, notify, t, handleSetDoingAction, onClose])
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: editValues || defaultValues,
|
||||||
|
})
|
||||||
|
const __oauth_client__ = useStore(form.store, s => s.values.__oauth_client__)
|
||||||
|
const helpField = schemas.find(schema => schema.url && schema.help)
|
||||||
|
const renderI18nObject = useRenderI18nObject()
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('plugin.auth.oauthClientSettings')}
|
||||||
|
confirmButtonText={t('plugin.auth.saveAndAuth')}
|
||||||
|
cancelButtonText={t('plugin.auth.saveOnly')}
|
||||||
|
extraButtonText={t('common.operation.cancel')}
|
||||||
|
showExtraButton
|
||||||
|
extraButtonVariant='secondary'
|
||||||
|
onExtraButtonClick={onClose}
|
||||||
|
onClose={onClose}
|
||||||
|
onCancel={handleConfirm}
|
||||||
|
onConfirm={handleConfirmAndAuthorize}
|
||||||
|
disabled={disabled || doingAction}
|
||||||
|
footerSlot={
|
||||||
|
__oauth_client__ === 'custom' && hasOriginalClientParams && (
|
||||||
|
<div className='grow'>
|
||||||
|
<Button
|
||||||
|
variant='secondary'
|
||||||
|
className='text-components-button-destructive-secondary-text'
|
||||||
|
disabled={disabled || doingAction || !editValues}
|
||||||
|
onClick={handleRemove}
|
||||||
|
>
|
||||||
|
{t('common.operation.remove')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<AuthForm
|
||||||
|
formFromProps={form}
|
||||||
|
ref={formRef}
|
||||||
|
formSchemas={schemas}
|
||||||
|
defaultValues={editValues || defaultValues}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
helpField && __oauth_client__ === 'custom' && (
|
||||||
|
<a
|
||||||
|
className='system-xs-regular mt-4 flex items-center text-text-accent'
|
||||||
|
href={helpField?.url}
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
<span className='break-all'>
|
||||||
|
{renderI18nObject(helpField?.help as any)}
|
||||||
|
</span>
|
||||||
|
<RiExternalLinkLine className='ml-1 h-3 w-3' />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(OAuthClientSettings)
|
||||||
@ -0,0 +1,113 @@
|
|||||||
|
import {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { RiArrowDownSLine } from '@remixicon/react'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import Indicator from '@/app/components/header/indicator'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import type {
|
||||||
|
Credential,
|
||||||
|
PluginPayload,
|
||||||
|
} from './types'
|
||||||
|
import {
|
||||||
|
Authorized,
|
||||||
|
usePluginAuth,
|
||||||
|
} from '.'
|
||||||
|
|
||||||
|
type AuthorizedInNodeProps = {
|
||||||
|
pluginPayload: PluginPayload
|
||||||
|
onAuthorizationItemClick: (id: string) => void
|
||||||
|
credentialId?: string
|
||||||
|
}
|
||||||
|
const AuthorizedInNode = ({
|
||||||
|
pluginPayload,
|
||||||
|
onAuthorizationItemClick,
|
||||||
|
credentialId,
|
||||||
|
}: AuthorizedInNodeProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const {
|
||||||
|
canApiKey,
|
||||||
|
canOAuth,
|
||||||
|
credentials,
|
||||||
|
disabled,
|
||||||
|
invalidPluginCredentialInfo,
|
||||||
|
} = usePluginAuth(pluginPayload, isOpen || !!credentialId)
|
||||||
|
const renderTrigger = useCallback((open?: boolean) => {
|
||||||
|
let label = ''
|
||||||
|
let removed = false
|
||||||
|
if (!credentialId) {
|
||||||
|
label = t('plugin.auth.workspaceDefault')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const credential = credentials.find(c => c.id === credentialId)
|
||||||
|
label = credential ? credential.name : t('plugin.auth.authRemoved')
|
||||||
|
removed = !credential
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
className={cn(
|
||||||
|
open && !removed && 'bg-components-button-ghost-bg-hover',
|
||||||
|
removed && 'bg-transparent text-text-destructive',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Indicator
|
||||||
|
className='mr-1.5'
|
||||||
|
color={removed ? 'red' : 'green'}
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
<RiArrowDownSLine
|
||||||
|
className={cn(
|
||||||
|
'h-3.5 w-3.5 text-components-button-ghost-text',
|
||||||
|
removed && 'text-text-destructive',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}, [credentialId, credentials, t])
|
||||||
|
const extraAuthorizationItems: Credential[] = [
|
||||||
|
{
|
||||||
|
id: '__workspace_default__',
|
||||||
|
name: t('plugin.auth.workspaceDefault'),
|
||||||
|
provider: '',
|
||||||
|
is_default: !credentialId,
|
||||||
|
isWorkspaceDefault: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const handleAuthorizationItemClick = useCallback((id: string) => {
|
||||||
|
onAuthorizationItemClick(id)
|
||||||
|
setIsOpen(false)
|
||||||
|
}, [
|
||||||
|
onAuthorizationItemClick,
|
||||||
|
setIsOpen,
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Authorized
|
||||||
|
pluginPayload={pluginPayload}
|
||||||
|
credentials={credentials}
|
||||||
|
canOAuth={canOAuth}
|
||||||
|
canApiKey={canApiKey}
|
||||||
|
renderTrigger={renderTrigger}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onOpenChange={setIsOpen}
|
||||||
|
offset={4}
|
||||||
|
placement='bottom-end'
|
||||||
|
triggerPopupSameWidth={false}
|
||||||
|
popupClassName='w-[360px]'
|
||||||
|
disabled={disabled}
|
||||||
|
disableSetDefault
|
||||||
|
onItemClick={handleAuthorizationItemClick}
|
||||||
|
extraAuthorizationItems={extraAuthorizationItems}
|
||||||
|
showItemSelectedIcon
|
||||||
|
selectedCredentialId={credentialId || '__workspace_default__'}
|
||||||
|
onUpdate={invalidPluginCredentialInfo}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(AuthorizedInNode)
|
||||||
@ -0,0 +1,342 @@
|
|||||||
|
import {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import {
|
||||||
|
RiArrowDownSLine,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import {
|
||||||
|
PortalToFollowElem,
|
||||||
|
PortalToFollowElemContent,
|
||||||
|
PortalToFollowElemTrigger,
|
||||||
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import type {
|
||||||
|
PortalToFollowElemOptions,
|
||||||
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import Indicator from '@/app/components/header/indicator'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import Confirm from '@/app/components/base/confirm'
|
||||||
|
import Authorize from '../authorize'
|
||||||
|
import type { Credential } from '../types'
|
||||||
|
import { CredentialTypeEnum } from '../types'
|
||||||
|
import ApiKeyModal from '../authorize/api-key-modal'
|
||||||
|
import Item from './item'
|
||||||
|
import { useToastContext } from '@/app/components/base/toast'
|
||||||
|
import type { PluginPayload } from '../types'
|
||||||
|
import {
|
||||||
|
useDeletePluginCredentialHook,
|
||||||
|
useSetPluginDefaultCredentialHook,
|
||||||
|
useUpdatePluginCredentialHook,
|
||||||
|
} from '../hooks/use-credential'
|
||||||
|
|
||||||
|
type AuthorizedProps = {
|
||||||
|
pluginPayload: PluginPayload
|
||||||
|
credentials: Credential[]
|
||||||
|
canOAuth?: boolean
|
||||||
|
canApiKey?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
renderTrigger?: (open?: boolean) => React.ReactNode
|
||||||
|
isOpen?: boolean
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
|
offset?: PortalToFollowElemOptions['offset']
|
||||||
|
placement?: PortalToFollowElemOptions['placement']
|
||||||
|
triggerPopupSameWidth?: boolean
|
||||||
|
popupClassName?: string
|
||||||
|
disableSetDefault?: boolean
|
||||||
|
onItemClick?: (id: string) => void
|
||||||
|
extraAuthorizationItems?: Credential[]
|
||||||
|
showItemSelectedIcon?: boolean
|
||||||
|
selectedCredentialId?: string
|
||||||
|
onUpdate?: () => void
|
||||||
|
}
|
||||||
|
const Authorized = ({
|
||||||
|
pluginPayload,
|
||||||
|
credentials,
|
||||||
|
canOAuth,
|
||||||
|
canApiKey,
|
||||||
|
disabled,
|
||||||
|
renderTrigger,
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
offset = 8,
|
||||||
|
placement = 'bottom-start',
|
||||||
|
triggerPopupSameWidth = true,
|
||||||
|
popupClassName,
|
||||||
|
disableSetDefault,
|
||||||
|
onItemClick,
|
||||||
|
extraAuthorizationItems,
|
||||||
|
showItemSelectedIcon,
|
||||||
|
selectedCredentialId,
|
||||||
|
onUpdate,
|
||||||
|
}: AuthorizedProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { notify } = useToastContext()
|
||||||
|
const [isLocalOpen, setIsLocalOpen] = useState(false)
|
||||||
|
const mergedIsOpen = isOpen ?? isLocalOpen
|
||||||
|
const setMergedIsOpen = useCallback((open: boolean) => {
|
||||||
|
if (onOpenChange)
|
||||||
|
onOpenChange(open)
|
||||||
|
|
||||||
|
setIsLocalOpen(open)
|
||||||
|
}, [onOpenChange])
|
||||||
|
const oAuthCredentials = credentials.filter(credential => credential.credential_type === CredentialTypeEnum.OAUTH2)
|
||||||
|
const apiKeyCredentials = credentials.filter(credential => credential.credential_type === CredentialTypeEnum.API_KEY)
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<PortalToFollowElem
|
||||||
|
open={mergedIsOpen}
|
||||||
|
onOpenChange={setMergedIsOpen}
|
||||||
|
placement={placement}
|
||||||
|
offset={offset}
|
||||||
|
triggerPopupSameWidth={triggerPopupSameWidth}
|
||||||
|
>
|
||||||
|
<PortalToFollowElemTrigger
|
||||||
|
onClick={() => setMergedIsOpen(!mergedIsOpen)}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
{
|
||||||
|
renderTrigger
|
||||||
|
? renderTrigger(mergedIsOpen)
|
||||||
|
: (
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
'w-full',
|
||||||
|
isOpen && 'bg-components-button-secondary-bg-hover',
|
||||||
|
)}>
|
||||||
|
<Indicator className='mr-2' />
|
||||||
|
{credentials.length}
|
||||||
|
{
|
||||||
|
credentials.length > 1
|
||||||
|
? t('plugin.auth.authorizations')
|
||||||
|
: t('plugin.auth.authorization')
|
||||||
|
}
|
||||||
|
<RiArrowDownSLine className='ml-0.5 h-4 w-4' />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</PortalToFollowElemTrigger>
|
||||||
|
<PortalToFollowElemContent className='z-[100]'>
|
||||||
|
<div className={cn(
|
||||||
|
'max-h-[360px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg',
|
||||||
|
popupClassName,
|
||||||
|
)}>
|
||||||
|
<div className='py-1'>
|
||||||
|
{
|
||||||
|
!!extraAuthorizationItems?.length && (
|
||||||
|
<div className='p-1'>
|
||||||
|
{
|
||||||
|
extraAuthorizationItems.map(credential => (
|
||||||
|
<Item
|
||||||
|
key={credential.id}
|
||||||
|
credential={credential}
|
||||||
|
disabled={disabled}
|
||||||
|
onItemClick={onItemClick}
|
||||||
|
disableRename
|
||||||
|
disableEdit
|
||||||
|
disableDelete
|
||||||
|
disableSetDefault
|
||||||
|
showSelectedIcon={showItemSelectedIcon}
|
||||||
|
selectedCredentialId={selectedCredentialId}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!!oAuthCredentials.length && (
|
||||||
|
<div className='p-1'>
|
||||||
|
<div className={cn(
|
||||||
|
'system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary',
|
||||||
|
showItemSelectedIcon && 'pl-7',
|
||||||
|
)}>
|
||||||
|
OAuth
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
oAuthCredentials.map(credential => (
|
||||||
|
<Item
|
||||||
|
key={credential.id}
|
||||||
|
credential={credential}
|
||||||
|
disabled={disabled}
|
||||||
|
disableEdit
|
||||||
|
onDelete={openConfirm}
|
||||||
|
onSetDefault={handleSetDefault}
|
||||||
|
onRename={handleRename}
|
||||||
|
disableSetDefault={disableSetDefault}
|
||||||
|
onItemClick={onItemClick}
|
||||||
|
showSelectedIcon={showItemSelectedIcon}
|
||||||
|
selectedCredentialId={selectedCredentialId}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!!apiKeyCredentials.length && (
|
||||||
|
<div className='p-1'>
|
||||||
|
<div className={cn(
|
||||||
|
'system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary',
|
||||||
|
showItemSelectedIcon && 'pl-7',
|
||||||
|
)}>
|
||||||
|
API Keys
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
apiKeyCredentials.map(credential => (
|
||||||
|
<Item
|
||||||
|
key={credential.id}
|
||||||
|
credential={credential}
|
||||||
|
disabled={disabled}
|
||||||
|
onDelete={openConfirm}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onSetDefault={handleSetDefault}
|
||||||
|
disableSetDefault={disableSetDefault}
|
||||||
|
disableRename
|
||||||
|
onItemClick={onItemClick}
|
||||||
|
onRename={handleRename}
|
||||||
|
showSelectedIcon={showItemSelectedIcon}
|
||||||
|
selectedCredentialId={selectedCredentialId}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className='h-[1px] bg-divider-subtle'></div>
|
||||||
|
<div className='p-2'>
|
||||||
|
<Authorize
|
||||||
|
pluginPayload={pluginPayload}
|
||||||
|
theme='secondary'
|
||||||
|
showDivider={false}
|
||||||
|
canOAuth={canOAuth}
|
||||||
|
canApiKey={canApiKey}
|
||||||
|
disabled={disabled}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</PortalToFollowElem>
|
||||||
|
{
|
||||||
|
deleteCredentialId && (
|
||||||
|
<Confirm
|
||||||
|
isShow
|
||||||
|
title={t('datasetDocuments.list.delete.title')}
|
||||||
|
isDisabled={doingAction}
|
||||||
|
onCancel={closeConfirm}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!!editValues && (
|
||||||
|
<ApiKeyModal
|
||||||
|
pluginPayload={pluginPayload}
|
||||||
|
editValues={editValues}
|
||||||
|
onClose={() => {
|
||||||
|
setEditValues(null)
|
||||||
|
pendingOperationCredentialId.current = null
|
||||||
|
}}
|
||||||
|
onRemove={handleRemove}
|
||||||
|
disabled={disabled || doingAction}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(Authorized)
|
||||||
@ -0,0 +1,219 @@
|
|||||||
|
import {
|
||||||
|
memo,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import {
|
||||||
|
RiCheckLine,
|
||||||
|
RiDeleteBinLine,
|
||||||
|
RiEditLine,
|
||||||
|
RiEqualizer2Line,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import Indicator from '@/app/components/header/indicator'
|
||||||
|
import Badge from '@/app/components/base/badge'
|
||||||
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import type { Credential } from '../types'
|
||||||
|
import { CredentialTypeEnum } from '../types'
|
||||||
|
|
||||||
|
type ItemProps = {
|
||||||
|
credential: Credential
|
||||||
|
disabled?: boolean
|
||||||
|
onDelete?: (id: string) => void
|
||||||
|
onEdit?: (id: string, values: Record<string, any>) => void
|
||||||
|
onSetDefault?: (id: string) => void
|
||||||
|
onRename?: (payload: {
|
||||||
|
credential_id: string
|
||||||
|
name: string
|
||||||
|
}) => void
|
||||||
|
disableRename?: boolean
|
||||||
|
disableEdit?: boolean
|
||||||
|
disableDelete?: boolean
|
||||||
|
disableSetDefault?: boolean
|
||||||
|
onItemClick?: (id: string) => void
|
||||||
|
showSelectedIcon?: boolean
|
||||||
|
selectedCredentialId?: string
|
||||||
|
}
|
||||||
|
const Item = ({
|
||||||
|
credential,
|
||||||
|
disabled,
|
||||||
|
onDelete,
|
||||||
|
onEdit,
|
||||||
|
onSetDefault,
|
||||||
|
onRename,
|
||||||
|
disableRename,
|
||||||
|
disableEdit,
|
||||||
|
disableDelete,
|
||||||
|
disableSetDefault,
|
||||||
|
onItemClick,
|
||||||
|
showSelectedIcon,
|
||||||
|
selectedCredentialId,
|
||||||
|
}: ItemProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [renaming, setRenaming] = useState(false)
|
||||||
|
const [renameValue, setRenameValue] = useState(credential.name)
|
||||||
|
const isOAuth = credential.credential_type === CredentialTypeEnum.OAUTH2
|
||||||
|
const showAction = useMemo(() => {
|
||||||
|
return !(disableRename && disableEdit && disableDelete && disableSetDefault)
|
||||||
|
}, [disableRename, disableEdit, disableDelete, disableSetDefault])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={credential.id}
|
||||||
|
className={cn(
|
||||||
|
'group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover',
|
||||||
|
renaming && 'bg-state-base-hover',
|
||||||
|
)}
|
||||||
|
onClick={() => onItemClick?.(credential.id === '__workspace_default__' ? '' : credential.id)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
onRename?.({
|
||||||
|
credential_id: credential.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='flex w-0 grow items-center space-x-1.5'>
|
||||||
|
{
|
||||||
|
showSelectedIcon && (
|
||||||
|
<div className='h-4 w-4'>
|
||||||
|
{
|
||||||
|
selectedCredentialId === credential.id && (
|
||||||
|
<RiCheckLine className='h-4 w-4 text-text-accent' />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<Indicator className='ml-2 mr-1.5 shrink-0' />
|
||||||
|
<div
|
||||||
|
className='system-md-regular truncate text-text-secondary'
|
||||||
|
title={credential.name}
|
||||||
|
>
|
||||||
|
{credential.name}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
credential.is_default && (
|
||||||
|
<Badge className='shrink-0'>
|
||||||
|
{t('plugin.auth.default')}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
showAction && !renaming && (
|
||||||
|
<div className='ml-2 hidden shrink-0 items-center group-hover:flex'>
|
||||||
|
{
|
||||||
|
!credential.is_default && !disableSetDefault && (
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onSetDefault?.(credential.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('plugin.auth.setDefault')}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!disableRename && (
|
||||||
|
<Tooltip popupContent={t('common.operation.rename')}>
|
||||||
|
<ActionButton
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setRenaming(true)
|
||||||
|
setRenameValue(credential.name)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RiEditLine className='h-4 w-4 text-text-tertiary' />
|
||||||
|
</ActionButton>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!isOAuth && !disableEdit && (
|
||||||
|
<Tooltip popupContent={t('common.operation.edit')}>
|
||||||
|
<ActionButton
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onEdit?.(
|
||||||
|
credential.id,
|
||||||
|
{
|
||||||
|
...credential.credentials,
|
||||||
|
__name__: credential.name,
|
||||||
|
__credential_id__: credential.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
|
||||||
|
</ActionButton>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!disableDelete && (
|
||||||
|
<Tooltip popupContent={t('common.operation.delete')}>
|
||||||
|
<ActionButton
|
||||||
|
className='hover:bg-transparent'
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onDelete?.(credential.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary hover:text-text-destructive' />
|
||||||
|
</ActionButton>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(Item)
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
import {
|
||||||
|
useAddPluginCredential,
|
||||||
|
useDeletePluginCredential,
|
||||||
|
useDeletePluginOAuthCustomClient,
|
||||||
|
useGetPluginCredentialInfo,
|
||||||
|
useGetPluginCredentialSchema,
|
||||||
|
useGetPluginOAuthClientSchema,
|
||||||
|
useGetPluginOAuthUrl,
|
||||||
|
useInvalidPluginCredentialInfo,
|
||||||
|
useInvalidPluginOAuthClientSchema,
|
||||||
|
useSetPluginDefaultCredential,
|
||||||
|
useSetPluginOAuthCustomClient,
|
||||||
|
useUpdatePluginCredential,
|
||||||
|
} from '@/service/use-plugins-auth'
|
||||||
|
import { useGetApi } from './use-get-api'
|
||||||
|
import type { PluginPayload } from '../types'
|
||||||
|
import type { CredentialTypeEnum } from '../types'
|
||||||
|
|
||||||
|
export const useGetPluginCredentialInfoHook = (pluginPayload: PluginPayload, enable?: boolean) => {
|
||||||
|
const apiMap = useGetApi(pluginPayload)
|
||||||
|
return useGetPluginCredentialInfo(enable ? apiMap.getCredentialInfo : '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeletePluginCredentialHook = (pluginPayload: PluginPayload) => {
|
||||||
|
const apiMap = useGetApi(pluginPayload)
|
||||||
|
|
||||||
|
return useDeletePluginCredential(apiMap.deleteCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInvalidPluginCredentialInfoHook = (pluginPayload: PluginPayload) => {
|
||||||
|
const apiMap = useGetApi(pluginPayload)
|
||||||
|
|
||||||
|
return useInvalidPluginCredentialInfo(apiMap.getCredentialInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSetPluginDefaultCredentialHook = (pluginPayload: PluginPayload) => {
|
||||||
|
const apiMap = useGetApi(pluginPayload)
|
||||||
|
|
||||||
|
return useSetPluginDefaultCredential(apiMap.setDefaultCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetPluginCredentialSchemaHook = (pluginPayload: PluginPayload, credentialType: CredentialTypeEnum) => {
|
||||||
|
const apiMap = useGetApi(pluginPayload)
|
||||||
|
|
||||||
|
return useGetPluginCredentialSchema(apiMap.getCredentialSchema(credentialType))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAddPluginCredentialHook = (pluginPayload: PluginPayload) => {
|
||||||
|
const apiMap = useGetApi(pluginPayload)
|
||||||
|
|
||||||
|
return useAddPluginCredential(apiMap.addCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdatePluginCredentialHook = (pluginPayload: PluginPayload) => {
|
||||||
|
const apiMap = useGetApi(pluginPayload)
|
||||||
|
|
||||||
|
return useUpdatePluginCredential(apiMap.updateCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetPluginOAuthUrlHook = (pluginPayload: PluginPayload) => {
|
||||||
|
const apiMap = useGetApi(pluginPayload)
|
||||||
|
|
||||||
|
return useGetPluginOAuthUrl(apiMap.getOauthUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetPluginOAuthClientSchemaHook = (pluginPayload: PluginPayload) => {
|
||||||
|
const apiMap = useGetApi(pluginPayload)
|
||||||
|
|
||||||
|
return useGetPluginOAuthClientSchema(apiMap.getOauthClientSchema)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInvalidPluginOAuthClientSchemaHook = (pluginPayload: PluginPayload) => {
|
||||||
|
const apiMap = useGetApi(pluginPayload)
|
||||||
|
|
||||||
|
return useInvalidPluginOAuthClientSchema(apiMap.getOauthClientSchema)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSetPluginOAuthCustomClientHook = (pluginPayload: PluginPayload) => {
|
||||||
|
const apiMap = useGetApi(pluginPayload)
|
||||||
|
|
||||||
|
return useSetPluginOAuthCustomClient(apiMap.setCustomOauthClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeletePluginOAuthCustomClientHook = (pluginPayload: PluginPayload) => {
|
||||||
|
const apiMap = useGetApi(pluginPayload)
|
||||||
|
|
||||||
|
return useDeletePluginOAuthCustomClient(apiMap.deleteCustomOAuthClient)
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
AuthCategory,
|
||||||
|
} from '../types'
|
||||||
|
import type {
|
||||||
|
CredentialTypeEnum,
|
||||||
|
PluginPayload,
|
||||||
|
} from '../types'
|
||||||
|
|
||||||
|
export const useGetApi = ({ category = AuthCategory.tool, provider }: PluginPayload) => {
|
||||||
|
if (category === AuthCategory.tool) {
|
||||||
|
return {
|
||||||
|
getCredentialInfo: `/workspaces/current/tool-provider/builtin/${provider}/credential/info`,
|
||||||
|
setDefaultCredential: `/workspaces/current/tool-provider/builtin/${provider}/default-credential`,
|
||||||
|
getCredentials: `/workspaces/current/tool-provider/builtin/${provider}/credentials`,
|
||||||
|
addCredential: `/workspaces/current/tool-provider/builtin/${provider}/add`,
|
||||||
|
updateCredential: `/workspaces/current/tool-provider/builtin/${provider}/update`,
|
||||||
|
deleteCredential: `/workspaces/current/tool-provider/builtin/${provider}/delete`,
|
||||||
|
getCredentialSchema: (credential_type: CredentialTypeEnum) => `/workspaces/current/tool-provider/builtin/${provider}/credential/schema/${credential_type}`,
|
||||||
|
getOauthUrl: `/oauth/plugin/${provider}/tool/authorization-url`,
|
||||||
|
getOauthClientSchema: `/workspaces/current/tool-provider/builtin/${provider}/oauth/client-schema`,
|
||||||
|
setCustomOauthClient: `/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`,
|
||||||
|
getCustomOAuthClientValues: `/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`,
|
||||||
|
deleteCustomOAuthClient: `/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCredentialInfo: '',
|
||||||
|
setDefaultCredential: '',
|
||||||
|
getCredentials: '',
|
||||||
|
addCredential: '',
|
||||||
|
updateCredential: '',
|
||||||
|
deleteCredential: '',
|
||||||
|
getCredentialSchema: () => '',
|
||||||
|
getOauthUrl: '',
|
||||||
|
getOauthClientSchema: '',
|
||||||
|
setCustomOauthClient: '',
|
||||||
|
getCustomOAuthClientValues: '',
|
||||||
|
deleteCustomOAuthClient: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import {
|
||||||
|
useGetPluginCredentialInfoHook,
|
||||||
|
useInvalidPluginCredentialInfoHook,
|
||||||
|
} from './use-credential'
|
||||||
|
import { CredentialTypeEnum } from '../types'
|
||||||
|
import type { PluginPayload } from '../types'
|
||||||
|
|
||||||
|
export const usePluginAuth = (pluginPayload: PluginPayload, enable?: boolean) => {
|
||||||
|
const { data } = useGetPluginCredentialInfoHook(pluginPayload, enable)
|
||||||
|
const { isCurrentWorkspaceManager } = useAppContext()
|
||||||
|
const isAuthorized = !!data?.credentials.length
|
||||||
|
const canOAuth = data?.supported_credential_types.includes(CredentialTypeEnum.OAUTH2)
|
||||||
|
const canApiKey = data?.supported_credential_types.includes(CredentialTypeEnum.API_KEY)
|
||||||
|
const invalidPluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload)
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAuthorized,
|
||||||
|
canOAuth,
|
||||||
|
canApiKey,
|
||||||
|
credentials: data?.credentials || [],
|
||||||
|
disabled: !isCurrentWorkspaceManager,
|
||||||
|
invalidPluginCredentialInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
export { default as PluginAuth } from './plugin-auth'
|
||||||
|
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 * from './types'
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
import {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import { RiArrowDownSLine } from '@remixicon/react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Authorize from './authorize'
|
||||||
|
import Authorized from './authorized'
|
||||||
|
import type {
|
||||||
|
Credential,
|
||||||
|
PluginPayload,
|
||||||
|
} from './types'
|
||||||
|
import { usePluginAuth } from './hooks/use-plugin-auth'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import Indicator from '@/app/components/header/indicator'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
type PluginAuthInAgentProps = {
|
||||||
|
pluginPayload: PluginPayload
|
||||||
|
credentialId?: string
|
||||||
|
onAuthorizationItemClick?: (id: string) => void
|
||||||
|
}
|
||||||
|
const PluginAuthInAgent = ({
|
||||||
|
pluginPayload,
|
||||||
|
credentialId,
|
||||||
|
onAuthorizationItemClick,
|
||||||
|
}: PluginAuthInAgentProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const {
|
||||||
|
isAuthorized,
|
||||||
|
canOAuth,
|
||||||
|
canApiKey,
|
||||||
|
credentials,
|
||||||
|
disabled,
|
||||||
|
invalidPluginCredentialInfo,
|
||||||
|
} = usePluginAuth(pluginPayload, true)
|
||||||
|
|
||||||
|
const extraAuthorizationItems: Credential[] = [
|
||||||
|
{
|
||||||
|
id: '__workspace_default__',
|
||||||
|
name: t('plugin.auth.workspaceDefault'),
|
||||||
|
provider: '',
|
||||||
|
is_default: !credentialId,
|
||||||
|
isWorkspaceDefault: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleAuthorizationItemClick = useCallback((id: string) => {
|
||||||
|
onAuthorizationItemClick?.(id)
|
||||||
|
setIsOpen(false)
|
||||||
|
}, [
|
||||||
|
onAuthorizationItemClick,
|
||||||
|
setIsOpen,
|
||||||
|
])
|
||||||
|
|
||||||
|
const renderTrigger = useCallback((isOpen?: boolean) => {
|
||||||
|
let label = ''
|
||||||
|
let removed = false
|
||||||
|
if (!credentialId) {
|
||||||
|
label = t('plugin.auth.workspaceDefault')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const credential = credentials.find(c => c.id === credentialId)
|
||||||
|
label = credential ? credential.name : t('plugin.auth.authRemoved')
|
||||||
|
removed = !credential
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
'w-full',
|
||||||
|
isOpen && 'bg-components-button-secondary-bg-hover',
|
||||||
|
removed && 'text-text-destructive',
|
||||||
|
)}>
|
||||||
|
<Indicator
|
||||||
|
className='mr-2'
|
||||||
|
color={removed ? 'red' : 'green'}
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
<RiArrowDownSLine className='ml-0.5 h-4 w-4' />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}, [credentialId, credentials, t])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
!isAuthorized && (
|
||||||
|
<Authorize
|
||||||
|
pluginPayload={pluginPayload}
|
||||||
|
canOAuth={canOAuth}
|
||||||
|
canApiKey={canApiKey}
|
||||||
|
disabled={disabled}
|
||||||
|
onUpdate={invalidPluginCredentialInfo}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
isAuthorized && (
|
||||||
|
<Authorized
|
||||||
|
pluginPayload={pluginPayload}
|
||||||
|
credentials={credentials}
|
||||||
|
canOAuth={canOAuth}
|
||||||
|
canApiKey={canApiKey}
|
||||||
|
disabled={disabled}
|
||||||
|
disableSetDefault
|
||||||
|
onItemClick={handleAuthorizationItemClick}
|
||||||
|
extraAuthorizationItems={extraAuthorizationItems}
|
||||||
|
showItemSelectedIcon
|
||||||
|
renderTrigger={renderTrigger}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onOpenChange={setIsOpen}
|
||||||
|
selectedCredentialId={credentialId || '__workspace_default__'}
|
||||||
|
onUpdate={invalidPluginCredentialInfo}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(PluginAuthInAgent)
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import Authorize from './authorize'
|
||||||
|
import Authorized from './authorized'
|
||||||
|
import type { PluginPayload } from './types'
|
||||||
|
import { usePluginAuth } from './hooks/use-plugin-auth'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
type PluginAuthProps = {
|
||||||
|
pluginPayload: PluginPayload
|
||||||
|
children?: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
const PluginAuth = ({
|
||||||
|
pluginPayload,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: PluginAuthProps) => {
|
||||||
|
const {
|
||||||
|
isAuthorized,
|
||||||
|
canOAuth,
|
||||||
|
canApiKey,
|
||||||
|
credentials,
|
||||||
|
disabled,
|
||||||
|
invalidPluginCredentialInfo,
|
||||||
|
} = usePluginAuth(pluginPayload, !!pluginPayload.provider)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(!isAuthorized && className)}>
|
||||||
|
{
|
||||||
|
!isAuthorized && (
|
||||||
|
<Authorize
|
||||||
|
pluginPayload={pluginPayload}
|
||||||
|
canOAuth={canOAuth}
|
||||||
|
canApiKey={canApiKey}
|
||||||
|
disabled={disabled}
|
||||||
|
onUpdate={invalidPluginCredentialInfo}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
isAuthorized && !children && (
|
||||||
|
<Authorized
|
||||||
|
pluginPayload={pluginPayload}
|
||||||
|
credentials={credentials}
|
||||||
|
canOAuth={canOAuth}
|
||||||
|
canApiKey={canApiKey}
|
||||||
|
disabled={disabled}
|
||||||
|
onUpdate={invalidPluginCredentialInfo}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
isAuthorized && children
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(PluginAuth)
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
export enum AuthCategory {
|
||||||
|
tool = 'tool',
|
||||||
|
datasource = 'datasource',
|
||||||
|
model = 'model',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginPayload = {
|
||||||
|
category: AuthCategory
|
||||||
|
provider: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CredentialTypeEnum {
|
||||||
|
OAUTH2 = 'oauth2',
|
||||||
|
API_KEY = 'api-key',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Credential = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
provider: string
|
||||||
|
credential_type?: CredentialTypeEnum
|
||||||
|
is_default: boolean
|
||||||
|
credentials?: Record<string, any>
|
||||||
|
isWorkspaceDefault?: boolean
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
export const transformFormSchemasSecretInput = (isPristineSecretInputNames: string[], values: Record<string, any>) => {
|
||||||
|
const transformedValues: Record<string, any> = { ...values }
|
||||||
|
|
||||||
|
isPristineSecretInputNames.forEach((name) => {
|
||||||
|
if (transformedValues[name])
|
||||||
|
transformedValues[name] = '[__HIDDEN__]'
|
||||||
|
})
|
||||||
|
|
||||||
|
return transformedValues
|
||||||
|
}
|
||||||
@ -0,0 +1,161 @@
|
|||||||
|
import {
|
||||||
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
} from '@tanstack/react-query'
|
||||||
|
import { del, get, post } from './base'
|
||||||
|
import { useInvalid } from './use-base'
|
||||||
|
import type {
|
||||||
|
Credential,
|
||||||
|
CredentialTypeEnum,
|
||||||
|
} from '@/app/components/plugins/plugin-auth/types'
|
||||||
|
import type { FormSchema } from '@/app/components/base/form/types'
|
||||||
|
|
||||||
|
const NAME_SPACE = 'plugins-auth'
|
||||||
|
|
||||||
|
export const useGetPluginCredentialInfo = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
enabled: !!url,
|
||||||
|
queryKey: [NAME_SPACE, 'credential-info', url],
|
||||||
|
queryFn: () => get<{
|
||||||
|
supported_credential_types: string[]
|
||||||
|
credentials: Credential[]
|
||||||
|
is_oauth_custom_client_enabled: boolean
|
||||||
|
}>(url),
|
||||||
|
staleTime: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInvalidPluginCredentialInfo = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useInvalid([NAME_SPACE, 'credential-info', url])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSetPluginDefaultCredential = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => {
|
||||||
|
return post(url, { body: { id } })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetPluginCredentialList = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [NAME_SPACE, 'credential-list', url],
|
||||||
|
queryFn: () => get(url),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAddPluginCredential = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: {
|
||||||
|
credentials: Record<string, any>
|
||||||
|
type: CredentialTypeEnum
|
||||||
|
name?: string
|
||||||
|
}) => {
|
||||||
|
return post(url, { body: params })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdatePluginCredential = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: {
|
||||||
|
credential_id: string
|
||||||
|
credentials?: Record<string, any>
|
||||||
|
name?: string
|
||||||
|
}) => {
|
||||||
|
return post(url, { body: params })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeletePluginCredential = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: { credential_id: string }) => {
|
||||||
|
return post(url, { body: params })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetPluginCredentialSchema = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [NAME_SPACE, 'credential-schema', url],
|
||||||
|
queryFn: () => get<FormSchema[]>(url),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetPluginOAuthUrl = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: [NAME_SPACE, 'oauth-url', url],
|
||||||
|
mutationFn: () => {
|
||||||
|
return get<
|
||||||
|
{
|
||||||
|
authorization_url: string
|
||||||
|
state: string
|
||||||
|
context_id: string
|
||||||
|
}>(url)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetPluginOAuthClientSchema = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [NAME_SPACE, 'oauth-client-schema', url],
|
||||||
|
queryFn: () => get<{
|
||||||
|
schema: FormSchema[]
|
||||||
|
is_oauth_custom_client_enabled: boolean
|
||||||
|
is_system_oauth_params_exists?: boolean
|
||||||
|
client_params?: Record<string, any>
|
||||||
|
redirect_uri?: string
|
||||||
|
}>(url),
|
||||||
|
staleTime: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInvalidPluginOAuthClientSchema = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useInvalid([NAME_SPACE, 'oauth-client-schema', url])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSetPluginOAuthCustomClient = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: {
|
||||||
|
client_params: Record<string, any>
|
||||||
|
enable_oauth_custom_client: boolean
|
||||||
|
}) => {
|
||||||
|
return post<{ result: string }>(url, { body: params })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeletePluginOAuthCustomClient = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => {
|
||||||
|
return del<{ result: string }>(url)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue