mirror of
https://github.com/langgenius/dify.git
synced 2026-11-14 07:11:06 +08:00
274 lines
9.2 KiB
TypeScript
274 lines
9.2 KiB
TypeScript
import type { ExternalKnowledgeApiResponse } from '@dify/contracts/api/console/datasets/types.gen'
|
|
import type { FC } from 'react'
|
|
import type { CreateExternalAPIReq, FormSchema } from '../declarations'
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogCancelButton,
|
|
AlertDialogConfirmButton,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from '@langgenius/dify-ui/alert-dialog'
|
|
import { Button } from '@langgenius/dify-ui/button'
|
|
import {
|
|
Dialog,
|
|
DialogBody,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogHeaderCloseButton,
|
|
DialogTitle,
|
|
} from '@langgenius/dify-ui/dialog'
|
|
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
|
import { RiBook2Line, RiInformation2Line } from '@remixicon/react'
|
|
import { memo, useId, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { EncryptedBottom } from '@/app/components/base/encrypted-bottom'
|
|
import { toast } from '@/app/notifications'
|
|
import { createExternalAPI } from '@/service/datasets'
|
|
import ExternalAPIForm from './Form'
|
|
|
|
type AddExternalAPIModalProps = {
|
|
data?: CreateExternalAPIReq
|
|
onSave: (api: ExternalKnowledgeApiResponse) => void
|
|
onCancel: () => void
|
|
onEdit?: (formValue: CreateExternalAPIReq) => Promise<void>
|
|
datasetBindings?: {
|
|
id: string
|
|
name: string
|
|
}[]
|
|
isEditMode: boolean
|
|
}
|
|
const formSchemas: FormSchema[] = [
|
|
{
|
|
variable: 'name',
|
|
type: 'text',
|
|
label: {
|
|
en_US: 'Name',
|
|
},
|
|
required: true,
|
|
},
|
|
{
|
|
variable: 'endpoint',
|
|
type: 'text',
|
|
label: {
|
|
en_US: 'API Endpoint',
|
|
},
|
|
required: true,
|
|
},
|
|
{
|
|
variable: 'api_key',
|
|
type: 'secret',
|
|
label: {
|
|
en_US: 'API Key',
|
|
},
|
|
required: true,
|
|
},
|
|
]
|
|
|
|
const emptyExternalAPIFormData: CreateExternalAPIReq = {
|
|
name: '',
|
|
settings: {
|
|
endpoint: '',
|
|
api_key: '',
|
|
},
|
|
}
|
|
|
|
function isValidHttpEndpoint(value: string) {
|
|
try {
|
|
const endpoint = new URL(value)
|
|
return (
|
|
(endpoint.protocol === 'http:' || endpoint.protocol === 'https:') && Boolean(endpoint.host)
|
|
)
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
const AddExternalAPIModal: FC<AddExternalAPIModalProps> = ({
|
|
data,
|
|
onSave,
|
|
onCancel,
|
|
datasetBindings,
|
|
isEditMode,
|
|
onEdit,
|
|
}) => {
|
|
const { t } = useTranslation(['common', 'dataset'])
|
|
const formId = useId()
|
|
const [loading, setLoading] = useState(false)
|
|
const [showConfirm, setShowConfirm] = useState(false)
|
|
const [endpointError, setEndpointError] = useState<string>()
|
|
const [formData, setFormData] = useState<CreateExternalAPIReq>(() =>
|
|
isEditMode && data ? data : emptyExternalAPIFormData,
|
|
)
|
|
const hasEmptyInputs = Object.values(formData).some((value) =>
|
|
typeof value === 'string'
|
|
? value.trim() === ''
|
|
: Object.values(value).some((v) => v.trim() === ''),
|
|
)
|
|
const handleDataChange = (val: CreateExternalAPIReq) => {
|
|
if (val.settings.endpoint !== formData.settings.endpoint) setEndpointError(undefined)
|
|
setFormData(val)
|
|
}
|
|
const handleSave = async () => {
|
|
const endpoint = formData.settings.endpoint.trim()
|
|
if (!isValidHttpEndpoint(endpoint)) {
|
|
setEndpointError(t(($) => $['validation.invalidHttpUrl'], { ns: 'dataset' }))
|
|
return
|
|
}
|
|
if (formData && formData.settings.api_key && formData.settings.api_key?.length < 5) {
|
|
toast.error(t(($) => $['apiBasedExtension.modal.apiKey.lengthError'], { ns: 'common' }))
|
|
setLoading(false)
|
|
return
|
|
}
|
|
try {
|
|
setLoading(true)
|
|
if (isEditMode && onEdit) {
|
|
// Only send [__HIDDEN__] when the user has not changed the key, otherwise
|
|
// send the actual api_key so updated tokens are persisted.
|
|
const apiKeyToSend =
|
|
formData.settings.api_key === '[__HIDDEN__]' ? '[__HIDDEN__]' : formData.settings.api_key
|
|
await onEdit({
|
|
...formData,
|
|
settings: { ...formData.settings, api_key: apiKeyToSend, endpoint },
|
|
})
|
|
} else {
|
|
const res = await createExternalAPI({
|
|
body: { ...formData, settings: { ...formData.settings, endpoint } },
|
|
})
|
|
if (res && res.id) onSave(res)
|
|
}
|
|
onCancel()
|
|
} catch (error) {
|
|
console.error('Error saving/updating external API:', error)
|
|
toast.error(t(($) => $['api.actionFailed'], { ns: 'common' }))
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleSubmit = () => {
|
|
if (hasEmptyInputs || loading) return
|
|
|
|
if (isEditMode && (datasetBindings?.length ?? 0) > 0) setShowConfirm(true)
|
|
else void handleSave()
|
|
}
|
|
|
|
return (
|
|
<Dialog
|
|
open
|
|
disablePointerDismissal
|
|
onOpenChange={(open) => {
|
|
if (!open) onCancel()
|
|
}}
|
|
>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{isEditMode
|
|
? t(($) => $.editExternalAPIFormTitle, { ns: 'dataset' })
|
|
: t(($) => $.createExternalAPI, { ns: 'dataset' })}
|
|
</DialogTitle>
|
|
{isEditMode && (datasetBindings?.length ?? 0) > 0 && (
|
|
<div className="flex items-center system-xs-regular text-text-tertiary">
|
|
{t(($) => $['editExternalAPIFormWarning.front'], { ns: 'dataset' })}
|
|
<span className="flex cursor-pointer items-center text-text-accent">
|
|
|
|
{datasetBindings?.length}{' '}
|
|
{t(($) => $['editExternalAPIFormWarning.end'], { ns: 'dataset' })}
|
|
|
|
<Popover>
|
|
<PopoverTrigger
|
|
openOnHover
|
|
aria-label={t(($) => $['editExternalAPIFormWarning.end'], { ns: 'dataset' })}
|
|
render={
|
|
<button
|
|
type="button"
|
|
className="flex size-3.5 items-center justify-center rounded-sm hover:bg-state-base-hover"
|
|
>
|
|
<RiInformation2Line className="size-3.5" />
|
|
</button>
|
|
}
|
|
/>
|
|
<PopoverContent className="flex w-[320px] items-center self-stretch px-3 py-2">
|
|
<div className="p-1">
|
|
<div className="flex items-start self-stretch pt-1 pr-3 pb-0.5 pl-2">
|
|
<div className="system-xs-medium-uppercase text-text-tertiary">{`${datasetBindings?.length} ${t(($) => $.editExternalAPITooltipTitle, { ns: 'dataset' })}`}</div>
|
|
</div>
|
|
{datasetBindings?.map((binding) => (
|
|
<div
|
|
key={binding.id}
|
|
className="flex items-center gap-1 self-stretch px-2 py-1"
|
|
>
|
|
<RiBook2Line className="size-4 text-text-secondary" />
|
|
<div className="system-sm-medium text-text-secondary">{binding.name}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</span>
|
|
</div>
|
|
)}
|
|
<DialogHeaderCloseButton aria-label={t(($) => $['operation.close'], { ns: 'common' })} />
|
|
</DialogHeader>
|
|
<DialogBody>
|
|
<ExternalAPIForm
|
|
id={formId}
|
|
value={formData}
|
|
onChange={handleDataChange}
|
|
onSubmit={(event) => {
|
|
event.preventDefault()
|
|
handleSubmit()
|
|
}}
|
|
formSchemas={formSchemas}
|
|
errors={{ endpoint: endpointError }}
|
|
/>
|
|
</DialogBody>
|
|
<DialogFooter>
|
|
<Button type="button" variant="secondary" onClick={onCancel}>
|
|
{t(($) => $['externalAPIForm.cancel'], { ns: 'dataset' })}
|
|
</Button>
|
|
<Button
|
|
form={formId}
|
|
type="submit"
|
|
variant="primary"
|
|
loading={loading}
|
|
disabled={hasEmptyInputs}
|
|
>
|
|
{t(($) => $['externalAPIForm.save'], { ns: 'dataset' })}
|
|
</Button>
|
|
</DialogFooter>
|
|
<EncryptedBottom
|
|
front={t(($) => $['externalAPIForm.encrypted.front'], { ns: 'dataset' })}
|
|
back={t(($) => $['externalAPIForm.encrypted.end'], { ns: 'dataset' })}
|
|
/>
|
|
<AlertDialog
|
|
open={showConfirm && (datasetBindings?.length ?? 0) > 0}
|
|
onOpenChange={(open) => !open && setShowConfirm(false)}
|
|
>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Warning</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{`${t(($) => $['editExternalAPIConfirmWarningContent.front'], { ns: 'dataset' })} ${datasetBindings?.length} ${t(($) => $['editExternalAPIConfirmWarningContent.end'], { ns: 'dataset' })}`}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancelButton>
|
|
{t(($) => $['operation.cancel'], { ns: 'common' })}
|
|
</AlertDialogCancelButton>
|
|
<AlertDialogConfirmButton onClick={handleSave}>
|
|
{t(($) => $['operation.confirm'], { ns: 'common' })}
|
|
</AlertDialogConfirmButton>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
export default memo(AddExternalAPIModal)
|