dify
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import type { ChangeEvent } from 'react'
|
||||
import {
|
||||
ValidatedErrorIcon,
|
||||
ValidatedErrorMessage,
|
||||
ValidatedSuccessIcon,
|
||||
ValidatingTip,
|
||||
} from './ValidateStatus'
|
||||
import { ValidatedStatus } from './declarations'
|
||||
import type { ValidatedStatusState } from './declarations'
|
||||
|
||||
type KeyInputProps = {
|
||||
value?: string
|
||||
name: string
|
||||
placeholder: string
|
||||
className?: string
|
||||
onChange: (v: string) => void
|
||||
onFocus?: () => void
|
||||
validating: boolean
|
||||
validatedStatusState: ValidatedStatusState
|
||||
}
|
||||
|
||||
const KeyInput = ({
|
||||
value,
|
||||
name,
|
||||
placeholder,
|
||||
className,
|
||||
onChange,
|
||||
onFocus,
|
||||
validating,
|
||||
validatedStatusState,
|
||||
}: KeyInputProps) => {
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const inputValue = e.target.value
|
||||
onChange(inputValue)
|
||||
}
|
||||
|
||||
const getValidatedIcon = () => {
|
||||
if (validatedStatusState.status === ValidatedStatus.Error || validatedStatusState.status === ValidatedStatus.Exceed)
|
||||
return <ValidatedErrorIcon />
|
||||
|
||||
if (validatedStatusState.status === ValidatedStatus.Success)
|
||||
return <ValidatedSuccessIcon />
|
||||
}
|
||||
const getValidatedTip = () => {
|
||||
if (validating)
|
||||
return <ValidatingTip />
|
||||
|
||||
if (validatedStatusState.status === ValidatedStatus.Error)
|
||||
return <ValidatedErrorMessage errorMessage={validatedStatusState.message ?? ''} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="mb-2 text-[13px] font-medium text-gray-800">{name}</div>
|
||||
<div className='
|
||||
flex items-center rounded-lg bg-white px-3
|
||||
shadow-xs
|
||||
'>
|
||||
<input
|
||||
className='
|
||||
mr-2 w-full appearance-none
|
||||
bg-transparent py-[9px] text-xs font-medium
|
||||
leading-[18px] text-gray-700 outline-none
|
||||
'
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
{getValidatedIcon()}
|
||||
</div>
|
||||
{getValidatedTip()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default KeyInput
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Indicator from '../../indicator'
|
||||
import type { Status } from './declarations'
|
||||
|
||||
type OperateProps = {
|
||||
isOpen: boolean
|
||||
status: Status
|
||||
disabled?: boolean
|
||||
onCancel: () => void
|
||||
onSave: () => void
|
||||
onAdd: () => void
|
||||
onEdit: () => void
|
||||
}
|
||||
|
||||
const Operate = ({
|
||||
isOpen,
|
||||
status,
|
||||
disabled,
|
||||
onCancel,
|
||||
onSave,
|
||||
onAdd,
|
||||
onEdit,
|
||||
}: OperateProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (isOpen) {
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
<div className='
|
||||
mr-[5px] flex
|
||||
h-7 cursor-pointer items-center rounded-md px-3
|
||||
text-xs font-medium text-gray-700
|
||||
' onClick={onCancel} >
|
||||
{t('common.operation.cancel')}
|
||||
</div>
|
||||
<div className='
|
||||
flex h-7
|
||||
cursor-pointer items-center rounded-md bg-primary-700 px-3
|
||||
text-xs font-medium text-white
|
||||
' onClick={onSave}>
|
||||
{t('common.operation.save')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'add') {
|
||||
return (
|
||||
<div className={
|
||||
`flex h-[28px] cursor-pointer items-center rounded-md border border-gray-200
|
||||
bg-white px-3 text-xs font-medium text-gray-700 ${disabled && 'cursor-default opacity-50'}}`
|
||||
} onClick={() => !disabled && onAdd()}>
|
||||
{t('common.provider.addKey')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'fail' || status === 'success') {
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
status === 'fail' && (
|
||||
<div className='mr-4 flex items-center'>
|
||||
<div className='text-xs text-[#D92D20]'>{t('common.provider.invalidApiKey')}</div>
|
||||
<Indicator color='red' className='ml-2' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
status === 'success' && (
|
||||
<Indicator color='green' className='mr-4' />
|
||||
)
|
||||
}
|
||||
<div className={
|
||||
`flex h-[28px] cursor-pointer items-center rounded-md border border-gray-200
|
||||
bg-white px-3 text-xs font-medium text-gray-700 ${disabled && 'cursor-default opacity-50'}}`
|
||||
} onClick={() => !disabled && onEdit()}>
|
||||
{t('common.provider.editKey')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default Operate
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiErrorWarningFill,
|
||||
} from '@remixicon/react'
|
||||
import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
|
||||
export const ValidatedErrorIcon = () => {
|
||||
return <RiErrorWarningFill className='h-4 w-4 text-[#D92D20]' />
|
||||
}
|
||||
|
||||
export const ValidatedSuccessIcon = () => {
|
||||
return <CheckCircle className='h-4 w-4 text-[#039855]' />
|
||||
}
|
||||
|
||||
export const ValidatingTip = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className={'mt-2 text-xs font-normal text-primary-600'}>
|
||||
{t('common.provider.validating')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ValidatedErrorMessage = ({ errorMessage }: { errorMessage: string }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={'mt-2 text-xs font-normal text-[#D92D20]'}>
|
||||
{t('common.provider.validatedError')}{errorMessage}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
|
||||
export enum ValidatedStatus {
|
||||
Success = 'success',
|
||||
Error = 'error',
|
||||
Exceed = 'exceed',
|
||||
}
|
||||
|
||||
export type ValidatedStatusState = {
|
||||
status?: ValidatedStatus
|
||||
message?: string
|
||||
}
|
||||
|
||||
export type Status = 'add' | 'fail' | 'success'
|
||||
|
||||
export type ValidateValue = Record<string, any>
|
||||
|
||||
export type ValidateCallback = {
|
||||
before: (v?: ValidateValue) => boolean | undefined
|
||||
run?: (v?: ValidateValue) => Promise<ValidatedStatusState>
|
||||
}
|
||||
|
||||
export type Form = {
|
||||
key: string
|
||||
title: string
|
||||
placeholder: string
|
||||
value?: string
|
||||
validate?: ValidateCallback
|
||||
handleFocus?: (v: ValidateValue, dispatch: Dispatch<SetStateAction<ValidateValue>>) => void
|
||||
}
|
||||
|
||||
export type KeyFrom = {
|
||||
text: string
|
||||
link: string
|
||||
}
|
||||
|
||||
export type KeyValidatorProps = {
|
||||
type: string
|
||||
title: React.ReactNode
|
||||
status: Status
|
||||
forms: Form[]
|
||||
keyFrom: KeyFrom
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useState } from 'react'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import type { DebouncedFunc } from 'lodash-es'
|
||||
import { ValidatedStatus } from './declarations'
|
||||
import type { ValidateCallback, ValidateValue, ValidatedStatusState } from './declarations'
|
||||
|
||||
export const useValidate: (value: ValidateValue) => [DebouncedFunc<(validateCallback: ValidateCallback) => Promise<void>>, boolean, ValidatedStatusState] = (value) => {
|
||||
const [validating, setValidating] = useState(false)
|
||||
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>({})
|
||||
|
||||
const { run } = useDebounceFn(async (validateCallback: ValidateCallback) => {
|
||||
if (!validateCallback.before(value)) {
|
||||
setValidating(false)
|
||||
setValidatedStatus({})
|
||||
return
|
||||
}
|
||||
setValidating(true)
|
||||
|
||||
if (validateCallback.run) {
|
||||
const res = await validateCallback?.run(value)
|
||||
setValidatedStatus(
|
||||
res.status === 'success'
|
||||
? { status: ValidatedStatus.Success }
|
||||
: { status: ValidatedStatus.Error, message: res.message })
|
||||
|
||||
setValidating(false)
|
||||
}
|
||||
}, { wait: 1000 })
|
||||
|
||||
return [run, validating, validatedStatus]
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useState } from 'react'
|
||||
import Operate from './Operate'
|
||||
import KeyInput from './KeyInput'
|
||||
import { useValidate } from './hooks'
|
||||
import type { Form, KeyFrom, Status, ValidateValue } from './declarations'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
export type KeyValidatorProps = {
|
||||
type: string
|
||||
title: React.ReactNode
|
||||
status: Status
|
||||
forms: Form[]
|
||||
keyFrom: KeyFrom
|
||||
onSave: (v: ValidateValue) => Promise<boolean | undefined>
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const KeyValidator = ({
|
||||
type,
|
||||
title,
|
||||
status,
|
||||
forms,
|
||||
keyFrom,
|
||||
onSave,
|
||||
disabled,
|
||||
}: KeyValidatorProps) => {
|
||||
const triggerKey = `plugins/${type}`
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const prevValue = forms.reduce((prev: ValidateValue, next: Form) => {
|
||||
prev[next.key] = next.value
|
||||
return prev
|
||||
}, {})
|
||||
const [value, setValue] = useState(prevValue)
|
||||
const [validate, validating, validatedStatusState] = useValidate(value)
|
||||
|
||||
eventEmitter?.useSubscription((v) => {
|
||||
if (v !== triggerKey) {
|
||||
setIsOpen(false)
|
||||
setValue(prevValue)
|
||||
validate({ before: () => false })
|
||||
}
|
||||
})
|
||||
|
||||
const handleCancel = () => {
|
||||
eventEmitter?.emit('')
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (await onSave(value))
|
||||
eventEmitter?.emit('')
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
setIsOpen(true)
|
||||
eventEmitter?.emit(triggerKey)
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
setIsOpen(true)
|
||||
eventEmitter?.emit(triggerKey)
|
||||
}
|
||||
|
||||
const handleChange = (form: Form, val: string) => {
|
||||
setValue({ ...value, [form.key]: val })
|
||||
|
||||
if (form.validate)
|
||||
validate(form.validate)
|
||||
}
|
||||
|
||||
const handleFocus = (form: Form) => {
|
||||
if (form.handleFocus)
|
||||
form.handleFocus(value, setValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mb-2 rounded-md border-[0.5px] border-gray-200 bg-gray-50'>
|
||||
<div className={
|
||||
`flex h-[52px] cursor-pointer items-center justify-between px-4 ${isOpen && 'border-b-[0.5px] border-b-gray-200'}`
|
||||
}>
|
||||
{title}
|
||||
<Operate
|
||||
isOpen={isOpen}
|
||||
status={status}
|
||||
onCancel={handleCancel}
|
||||
onSave={handleSave}
|
||||
onAdd={handleAdd}
|
||||
onEdit={handleEdit}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
isOpen && !disabled && (
|
||||
<div className='px-4 py-3'>
|
||||
{
|
||||
forms.map(form => (
|
||||
<KeyInput
|
||||
key={form.key}
|
||||
className='mb-4'
|
||||
name={form.title}
|
||||
placeholder={form.placeholder}
|
||||
value={value[form.key] as string || ''}
|
||||
onChange={v => handleChange(form, v)}
|
||||
onFocus={() => handleFocus(form)}
|
||||
validating={validating}
|
||||
validatedStatusState={validatedStatusState}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<a className="flex cursor-pointer items-center text-xs text-primary-600" href={keyFrom.link} target='_blank' rel='noopener noreferrer'>
|
||||
{keyFrom.text}
|
||||
<LinkExternal02 className='ml-1 h-3 w-3 text-primary-600' />
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default KeyValidator
|
||||
Reference in New Issue
Block a user