mirror of
https://github.com/langgenius/dify.git
synced 2026-04-09 18:08:09 +08:00
Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: lif <1835304752@qq.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: Stephen Zhou <hi@hyoban.cc> Co-authored-by: tmimmanuel <14046872+tmimmanuel@users.noreply.github.com> Co-authored-by: Desel72 <pedroluiscolmenares722@gmail.com> Co-authored-by: Renzo <170978465+RenzoMXD@users.noreply.github.com> Co-authored-by: Krishna Chaitanya <krishnabkc15@gmail.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
288 lines
9.9 KiB
TypeScript
288 lines
9.9 KiB
TypeScript
'use client'
|
|
|
|
import type { MouseEventHandler } from 'react'
|
|
import {
|
|
RiAlertFill,
|
|
RiCloseLine,
|
|
RiFileDownloadLine,
|
|
} from '@remixicon/react'
|
|
import {
|
|
memo,
|
|
useCallback,
|
|
useRef,
|
|
useState,
|
|
} from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import Uploader from '@/app/components/app/create-from-dsl-modal/uploader'
|
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
|
import Button from '@/app/components/base/button'
|
|
import Modal from '@/app/components/base/modal'
|
|
import { toast } from '@/app/components/base/ui/toast'
|
|
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
|
import {
|
|
DSLImportMode,
|
|
DSLImportStatus,
|
|
} from '@/models/app'
|
|
import {
|
|
importDSL,
|
|
importDSLConfirm,
|
|
} from '@/service/apps'
|
|
import { fetchWorkflowDraft } from '@/service/workflow'
|
|
import { WORKFLOW_DATA_UPDATE } from './constants'
|
|
import {
|
|
getImportNotificationPayload,
|
|
isImportCompleted,
|
|
normalizeWorkflowFeatures,
|
|
validateDSLContent,
|
|
} from './update-dsl-modal.helpers'
|
|
import {
|
|
initialEdges,
|
|
initialNodes,
|
|
} from './utils'
|
|
|
|
type UpdateDSLModalProps = {
|
|
onCancel: () => void
|
|
onBackup: () => void
|
|
onImport?: () => void
|
|
}
|
|
|
|
const UpdateDSLModal = ({
|
|
onCancel,
|
|
onBackup,
|
|
onImport,
|
|
}: UpdateDSLModalProps) => {
|
|
const { t } = useTranslation()
|
|
const appDetail = useAppStore(s => s.appDetail)
|
|
const [currentFile, setDSLFile] = useState<File>()
|
|
const [fileContent, setFileContent] = useState<string>()
|
|
const [loading, setLoading] = useState(false)
|
|
const { eventEmitter } = useEventEmitterContextContext()
|
|
const [show, setShow] = useState(true)
|
|
const [showErrorModal, setShowErrorModal] = useState(false)
|
|
const [versions, setVersions] = useState<{ importedVersion: string, systemVersion: string }>()
|
|
const [importId, setImportId] = useState<string>()
|
|
const { handleCheckPluginDependencies } = usePluginDependencies()
|
|
|
|
const readFile = (file: File) => {
|
|
const reader = new FileReader()
|
|
reader.onload = function (event) {
|
|
const content = event.target?.result
|
|
setFileContent(content as string)
|
|
}
|
|
reader.readAsText(file)
|
|
}
|
|
|
|
const handleFile = (file?: File) => {
|
|
setDSLFile(file)
|
|
if (file)
|
|
readFile(file)
|
|
if (!file)
|
|
setFileContent('')
|
|
}
|
|
|
|
const handleWorkflowUpdate = useCallback(async (app_id: string) => {
|
|
const {
|
|
graph,
|
|
features,
|
|
hash,
|
|
conversation_variables,
|
|
environment_variables,
|
|
} = await fetchWorkflowDraft(`/apps/${app_id}/workflows/draft`)
|
|
|
|
const { nodes, edges, viewport } = graph
|
|
eventEmitter?.emit({
|
|
type: WORKFLOW_DATA_UPDATE,
|
|
payload: {
|
|
nodes: initialNodes(nodes, edges),
|
|
edges: initialEdges(edges, nodes),
|
|
viewport,
|
|
features: normalizeWorkflowFeatures(features),
|
|
hash,
|
|
conversation_variables: conversation_variables || [],
|
|
environment_variables: environment_variables || [],
|
|
},
|
|
} as any)
|
|
}, [eventEmitter])
|
|
|
|
const isCreatingRef = useRef(false)
|
|
const handleCompletedImport = useCallback(async (status: DSLImportStatus, appId?: string) => {
|
|
if (!appId) {
|
|
toast.error(t('common.importFailure', { ns: 'workflow' }))
|
|
return
|
|
}
|
|
|
|
await handleWorkflowUpdate(appId)
|
|
onImport?.()
|
|
const payload = getImportNotificationPayload(status, t)
|
|
toast[payload.type](payload.message, payload.children ? { description: payload.children } : undefined)
|
|
await handleCheckPluginDependencies(appId)
|
|
setLoading(false)
|
|
onCancel()
|
|
}, [handleCheckPluginDependencies, handleWorkflowUpdate, onCancel, onImport, t])
|
|
|
|
const handlePendingImport = useCallback((id: string, importedVersion?: string | null, currentVersion?: string | null) => {
|
|
setShow(false)
|
|
setTimeout(() => {
|
|
setShowErrorModal(true)
|
|
}, 300)
|
|
setVersions({
|
|
importedVersion: importedVersion ?? '',
|
|
systemVersion: currentVersion ?? '',
|
|
})
|
|
setImportId(id)
|
|
}, [])
|
|
|
|
const handleImport: MouseEventHandler = useCallback(async () => {
|
|
if (isCreatingRef.current)
|
|
return
|
|
isCreatingRef.current = true
|
|
if (!currentFile) {
|
|
isCreatingRef.current = false
|
|
return
|
|
}
|
|
try {
|
|
if (appDetail && fileContent && validateDSLContent(fileContent, appDetail.mode)) {
|
|
setLoading(true)
|
|
const response = await importDSL({ mode: DSLImportMode.YAML_CONTENT, yaml_content: fileContent, app_id: appDetail.id })
|
|
const { id, status, app_id, imported_dsl_version, current_dsl_version } = response
|
|
|
|
if (isImportCompleted(status)) {
|
|
await handleCompletedImport(status, app_id)
|
|
}
|
|
else if (status === DSLImportStatus.PENDING) {
|
|
handlePendingImport(id, imported_dsl_version, current_dsl_version)
|
|
}
|
|
else {
|
|
setLoading(false)
|
|
toast.error(t('common.importFailure', { ns: 'workflow' }))
|
|
}
|
|
}
|
|
else if (fileContent) {
|
|
toast.error(t('common.importFailure', { ns: 'workflow' }))
|
|
}
|
|
}
|
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
catch (e) {
|
|
setLoading(false)
|
|
toast.error(t('common.importFailure', { ns: 'workflow' }))
|
|
}
|
|
isCreatingRef.current = false
|
|
}, [currentFile, fileContent, t, appDetail, handleCompletedImport, handlePendingImport])
|
|
|
|
const onUpdateDSLConfirm: MouseEventHandler = async () => {
|
|
try {
|
|
if (!importId)
|
|
return
|
|
const response = await importDSLConfirm({
|
|
import_id: importId,
|
|
})
|
|
|
|
const { status, app_id } = response
|
|
|
|
if (isImportCompleted(status)) {
|
|
await handleCompletedImport(status, app_id)
|
|
}
|
|
else if (status === DSLImportStatus.FAILED) {
|
|
setLoading(false)
|
|
toast.error(t('common.importFailure', { ns: 'workflow' }))
|
|
}
|
|
}
|
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
catch (e) {
|
|
setLoading(false)
|
|
toast.error(t('common.importFailure', { ns: 'workflow' }))
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Modal
|
|
className="w-[520px] rounded-2xl p-6"
|
|
isShow={show}
|
|
onClose={onCancel}
|
|
>
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<div className="text-text-primary title-2xl-semi-bold">{t('common.importDSL', { ns: 'workflow' })}</div>
|
|
<div className="flex h-[22px] w-[22px] cursor-pointer items-center justify-center" onClick={onCancel}>
|
|
<RiCloseLine className="h-[18px] w-[18px] text-text-tertiary" />
|
|
</div>
|
|
</div>
|
|
<div className="relative mb-2 flex grow gap-0.5 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-xs">
|
|
<div className="absolute left-0 top-0 h-full w-full bg-toast-warning-bg opacity-40" />
|
|
<div className="flex items-start justify-center p-1">
|
|
<RiAlertFill className="h-4 w-4 shrink-0 text-text-warning-secondary" />
|
|
</div>
|
|
<div className="flex grow flex-col items-start gap-0.5 py-1">
|
|
<div className="whitespace-pre-line text-text-primary system-xs-medium">{t('common.importDSLTip', { ns: 'workflow' })}</div>
|
|
<div className="flex items-start gap-1 self-stretch pb-0.5 pt-1">
|
|
<Button
|
|
size="small"
|
|
variant="secondary"
|
|
className="z-[1000]"
|
|
onClick={onBackup}
|
|
>
|
|
<RiFileDownloadLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
|
|
<div className="flex items-center justify-center gap-1 px-[3px]">
|
|
{t('common.backupCurrentDraft', { ns: 'workflow' })}
|
|
</div>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="pt-2 text-text-primary system-md-semibold">
|
|
{t('common.chooseDSL', { ns: 'workflow' })}
|
|
</div>
|
|
<div className="flex w-full flex-col items-start justify-center gap-4 self-stretch py-4">
|
|
<Uploader
|
|
file={currentFile}
|
|
updateFile={handleFile}
|
|
className="!mt-0 w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-end gap-2 self-stretch pt-5">
|
|
<Button onClick={onCancel}>{t('newApp.Cancel', { ns: 'app' })}</Button>
|
|
<Button
|
|
disabled={!currentFile || loading}
|
|
variant="warning"
|
|
onClick={handleImport}
|
|
loading={loading}
|
|
>
|
|
{t('common.overwriteAndImport', { ns: 'workflow' })}
|
|
</Button>
|
|
</div>
|
|
</Modal>
|
|
<Modal
|
|
isShow={showErrorModal}
|
|
onClose={() => setShowErrorModal(false)}
|
|
className="w-[480px]"
|
|
>
|
|
<div className="flex flex-col items-start gap-2 self-stretch pb-4">
|
|
<div className="text-text-primary title-2xl-semi-bold">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</div>
|
|
<div className="flex grow flex-col text-text-secondary system-md-regular">
|
|
<div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
|
|
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
|
|
<br />
|
|
<div>
|
|
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
|
|
<span className="system-md-medium">{versions?.importedVersion}</span>
|
|
</div>
|
|
<div>
|
|
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
|
|
<span className="system-md-medium">{versions?.systemVersion}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-start justify-end gap-2 self-stretch pt-6">
|
|
<Button variant="secondary" onClick={() => setShowErrorModal(false)}>{t('newApp.Cancel', { ns: 'app' })}</Button>
|
|
<Button variant="primary" destructive onClick={onUpdateDSLConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button>
|
|
</div>
|
|
</Modal>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default memo(UpdateDSLModal)
|