mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-09 10:56:52 +08:00
feat: Check and compare the DSL version before import an app (#10969)
Co-authored-by: Yi <yxiaoisme@gmail.com>
This commit is contained in:
@@ -12,9 +12,13 @@ import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import {
|
||||
importApp,
|
||||
importAppFromUrl,
|
||||
importDSL,
|
||||
importDSLConfirm,
|
||||
} from '@/service/apps'
|
||||
import {
|
||||
DSLImportMode,
|
||||
DSLImportStatus,
|
||||
} from '@/models/app'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||
@@ -43,6 +47,9 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
const [fileContent, setFileContent] = useState<string>()
|
||||
const [currentTab, setCurrentTab] = useState(activeTab)
|
||||
const [dslUrlValue, setDslUrlValue] = useState(dslUrl)
|
||||
const [showErrorModal, setShowErrorModal] = useState(false)
|
||||
const [versions, setVersions] = useState<{ importedVersion: string; systemVersion: string }>()
|
||||
const [importId, setImportId] = useState<string>()
|
||||
|
||||
const readFile = (file: File) => {
|
||||
const reader = new FileReader()
|
||||
@@ -66,6 +73,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
|
||||
|
||||
const isCreatingRef = useRef(false)
|
||||
|
||||
const onCreate: MouseEventHandler = async () => {
|
||||
if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile)
|
||||
return
|
||||
@@ -75,25 +83,54 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
return
|
||||
isCreatingRef.current = true
|
||||
try {
|
||||
let app
|
||||
let response
|
||||
|
||||
if (currentTab === CreateFromDSLModalTab.FROM_FILE) {
|
||||
app = await importApp({
|
||||
data: fileContent || '',
|
||||
response = await importDSL({
|
||||
mode: DSLImportMode.YAML_CONTENT,
|
||||
yaml_content: fileContent || '',
|
||||
})
|
||||
}
|
||||
if (currentTab === CreateFromDSLModalTab.FROM_URL) {
|
||||
app = await importAppFromUrl({
|
||||
url: dslUrlValue || '',
|
||||
response = await importDSL({
|
||||
mode: DSLImportMode.YAML_URL,
|
||||
yaml_url: dslUrlValue || '',
|
||||
})
|
||||
}
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
if (onClose)
|
||||
onClose()
|
||||
notify({ type: 'success', message: t('app.newApp.appCreated') })
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
getRedirection(isCurrentWorkspaceEditor, app, push)
|
||||
|
||||
if (!response)
|
||||
return
|
||||
|
||||
const { id, status, app_id, imported_dsl_version, current_dsl_version } = response
|
||||
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
if (onClose)
|
||||
onClose()
|
||||
|
||||
notify({
|
||||
type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
|
||||
message: t(status === DSLImportStatus.COMPLETED ? 'app.newApp.appCreated' : 'app.newApp.caution'),
|
||||
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('app.newApp.appCreateDSLWarning'),
|
||||
})
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
getRedirection(isCurrentWorkspaceEditor, { id: app_id }, push)
|
||||
}
|
||||
else if (status === DSLImportStatus.PENDING) {
|
||||
setVersions({
|
||||
importedVersion: imported_dsl_version ?? '',
|
||||
systemVersion: current_dsl_version ?? '',
|
||||
})
|
||||
if (onClose)
|
||||
onClose()
|
||||
setTimeout(() => {
|
||||
setShowErrorModal(true)
|
||||
}, 300)
|
||||
setImportId(id)
|
||||
}
|
||||
else {
|
||||
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
@@ -101,6 +138,38 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
isCreatingRef.current = false
|
||||
}
|
||||
|
||||
const onDSLConfirm: MouseEventHandler = async () => {
|
||||
try {
|
||||
if (!importId)
|
||||
return
|
||||
const response = await importDSLConfirm({
|
||||
import_id: importId,
|
||||
})
|
||||
|
||||
const { status, app_id } = response
|
||||
|
||||
if (status === DSLImportStatus.COMPLETED) {
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
if (onClose)
|
||||
onClose()
|
||||
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('app.newApp.appCreated'),
|
||||
})
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
getRedirection(isCurrentWorkspaceEditor, { id: app_id }, push)
|
||||
}
|
||||
else if (status === DSLImportStatus.FAILED) {
|
||||
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
}
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
key: CreateFromDSLModalTab.FROM_FILE,
|
||||
@@ -123,74 +192,96 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
}, [isAppsFull, currentTab, currentFile, dslUrlValue])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className='p-0 w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'
|
||||
isShow={show}
|
||||
onClose={() => { }}
|
||||
>
|
||||
<div className='flex items-center justify-between pt-6 pl-6 pr-5 pb-3 text-text-primary title-2xl-semi-bold'>
|
||||
{t('app.importFromDSL')}
|
||||
<div
|
||||
className='flex items-center w-8 h-8 cursor-pointer'
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
<RiCloseLine className='w-5 h-5 text-text-tertiary' />
|
||||
<>
|
||||
<Modal
|
||||
className='p-0 w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'
|
||||
isShow={show}
|
||||
onClose={() => { }}
|
||||
>
|
||||
<div className='flex items-center justify-between pt-6 pl-6 pr-5 pb-3 text-text-primary title-2xl-semi-bold'>
|
||||
{t('app.importFromDSL')}
|
||||
<div
|
||||
className='flex items-center w-8 h-8 cursor-pointer'
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
<RiCloseLine className='w-5 h-5 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center px-6 h-9 space-x-6 system-md-semibold text-text-tertiary border-b border-divider-subtle'>
|
||||
{
|
||||
tabs.map(tab => (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={cn(
|
||||
'relative flex items-center h-full cursor-pointer',
|
||||
currentTab === tab.key && 'text-text-primary',
|
||||
)}
|
||||
onClick={() => setCurrentTab(tab.key)}
|
||||
>
|
||||
{tab.label}
|
||||
{
|
||||
currentTab === tab.key && (
|
||||
<div className='absolute bottom-0 w-full h-[2px] bg-util-colors-blue-brand-blue-brand-600'></div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className='px-6 py-4'>
|
||||
{
|
||||
currentTab === CreateFromDSLModalTab.FROM_FILE && (
|
||||
<Uploader
|
||||
className='mt-0'
|
||||
file={currentFile}
|
||||
updateFile={handleFile}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
currentTab === CreateFromDSLModalTab.FROM_URL && (
|
||||
<div>
|
||||
<div className='mb-1 system-md-semibold leading6'>DSL URL</div>
|
||||
<Input
|
||||
placeholder={t('app.importFromDSLUrlPlaceholder') || ''}
|
||||
value={dslUrlValue}
|
||||
onChange={e => setDslUrlValue(e.target.value)}
|
||||
<div className='flex items-center px-6 h-9 space-x-6 system-md-semibold text-text-tertiary border-b border-divider-subtle'>
|
||||
{
|
||||
tabs.map(tab => (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={cn(
|
||||
'relative flex items-center h-full cursor-pointer',
|
||||
currentTab === tab.key && 'text-text-primary',
|
||||
)}
|
||||
onClick={() => setCurrentTab(tab.key)}
|
||||
>
|
||||
{tab.label}
|
||||
{
|
||||
currentTab === tab.key && (
|
||||
<div className='absolute bottom-0 w-full h-[2px] bg-util-colors-blue-brand-blue-brand-600'></div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className='px-6 py-4'>
|
||||
{
|
||||
currentTab === CreateFromDSLModalTab.FROM_FILE && (
|
||||
<Uploader
|
||||
className='mt-0'
|
||||
file={currentFile}
|
||||
updateFile={handleFile}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{isAppsFull && (
|
||||
<div className='px-6'>
|
||||
<AppsFull className='mt-0' loc='app-create-dsl' />
|
||||
)
|
||||
}
|
||||
{
|
||||
currentTab === CreateFromDSLModalTab.FROM_URL && (
|
||||
<div>
|
||||
<div className='mb-1 system-md-semibold leading6'>DSL URL</div>
|
||||
<Input
|
||||
placeholder={t('app.importFromDSLUrlPlaceholder') || ''}
|
||||
value={dslUrlValue}
|
||||
onChange={e => setDslUrlValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
<div className='flex justify-end px-6 py-5'>
|
||||
<Button className='mr-2' onClick={onClose}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button disabled={buttonDisabled} variant="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
{isAppsFull && (
|
||||
<div className='px-6'>
|
||||
<AppsFull className='mt-0' loc='app-create-dsl' />
|
||||
</div>
|
||||
)}
|
||||
<div className='flex justify-end px-6 py-5'>
|
||||
<Button className='mr-2' onClick={onClose}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button disabled={buttonDisabled} variant="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
isShow={showErrorModal}
|
||||
onClose={() => setShowErrorModal(false)}
|
||||
className='w-[480px]'
|
||||
>
|
||||
<div className='flex pb-4 flex-col items-start gap-2 self-stretch'>
|
||||
<div className='text-text-primary title-2xl-semi-bold'>{t('app.newApp.appCreateDSLErrorTitle')}</div>
|
||||
<div className='flex flex-grow flex-col text-text-secondary system-md-regular'>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart1')}</div>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart2')}</div>
|
||||
<br />
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart3')}<span className='system-md-medium'>{versions?.importedVersion}</span></div>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart4')}<span className='system-md-medium'>{versions?.systemVersion}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex pt-6 justify-end items-start gap-2 self-stretch'>
|
||||
<Button variant='secondary' onClick={() => setShowErrorModal(false)}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button variant='primary' destructive onClick={onDSLConfirm}>{t('app.newApp.Confirm')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Yaml as YamlIcon } from '@/app/components/base/icons/src/public/files'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
@@ -58,8 +59,13 @@ const Uploader: FC<Props> = ({
|
||||
updateFile(files[0])
|
||||
}
|
||||
const selectHandle = () => {
|
||||
if (fileUploader.current)
|
||||
const originalFile = file
|
||||
if (fileUploader.current) {
|
||||
fileUploader.current.value = ''
|
||||
fileUploader.current.click()
|
||||
// If no file is selected, restore the original file
|
||||
fileUploader.current.oncancel = () => updateFile(originalFile)
|
||||
}
|
||||
}
|
||||
const removeFile = () => {
|
||||
if (fileUploader.current)
|
||||
@@ -96,7 +102,7 @@ const Uploader: FC<Props> = ({
|
||||
/>
|
||||
<div ref={dropRef}>
|
||||
{!file && (
|
||||
<div className={cn('flex items-center h-20 rounded-xl bg-gray-50 border border-dashed border-gray-200 text-sm font-normal', dragging && 'bg-[#F5F8FF] border border-[#B2CCFF]')}>
|
||||
<div className={cn('flex items-center h-12 rounded-xl bg-gray-50 border border-dashed border-gray-200 text-sm font-normal', dragging && 'bg-[#F5F8FF] border border-[#B2CCFF]')}>
|
||||
<div className='w-full flex items-center justify-center space-x-2'>
|
||||
<UploadCloud01 className='w-6 h-6 mr-2' />
|
||||
<div className='text-gray-500'>
|
||||
@@ -108,17 +114,23 @@ const Uploader: FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
{file && (
|
||||
<div className={cn('flex items-center h-20 px-6 rounded-xl bg-gray-50 border border-gray-200 text-sm font-normal group', 'hover:bg-[#F5F8FF] hover:border-[#B2CCFF]')}>
|
||||
<YamlIcon className="shrink-0" />
|
||||
<div className='flex ml-2 w-0 grow'>
|
||||
<span className='max-w-[calc(100%_-_30px)] text-ellipsis whitespace-nowrap overflow-hidden text-gray-800'>{file.name.replace(/(.yaml|.yml)$/, '')}</span>
|
||||
<span className='shrink-0 text-gray-500'>.yml</span>
|
||||
<div className={cn('flex items-center rounded-lg bg-components-panel-on-panel-item-bg border-[0.5px] border-components-panel-border shadow-xs group', 'hover:bg-[#F5F8FF] hover:border-[#B2CCFF]')}>
|
||||
<div className='flex p-3 justify-center items-center'>
|
||||
<YamlIcon className="w-6 h-6 shrink-0" />
|
||||
</div>
|
||||
<div className='flex py-1 pr-2 grow flex-col items-start gap-0.5'>
|
||||
<span className='max-w-[calc(100%_-_30px)] text-ellipsis whitespace-nowrap overflow-hidden text-text-secondary font-inter text-[12px] font-medium leading-4'>{file.name}</span>
|
||||
<div className='flex h-3 items-center gap-1 self-stretch text-text-tertiary font-inter text-[10px] font-medium leading-3 uppercase'>
|
||||
<span>YAML</span>
|
||||
<span className='text-text-quaternary'>·</span>
|
||||
<span>{formatFileSize(file.size)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='hidden group-hover:flex items-center'>
|
||||
<Button onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
|
||||
<div className='mx-2 w-px h-4 bg-gray-200' />
|
||||
<div className='p-2 cursor-pointer' onClick={removeFile}>
|
||||
<RiDeleteBinLine className='w-4 h-4 text-gray-500' />
|
||||
<RiDeleteBinLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,16 +3,19 @@ import type { ReactNode } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InformationCircleIcon,
|
||||
XCircleIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
RiAlertFill,
|
||||
RiCheckboxCircleFill,
|
||||
RiCloseLine,
|
||||
RiErrorWarningFill,
|
||||
RiInformation2Fill,
|
||||
} from '@remixicon/react'
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
export type IToastProps = {
|
||||
type?: 'success' | 'error' | 'warning' | 'info'
|
||||
size?: 'md' | 'sm'
|
||||
duration?: number
|
||||
message: string
|
||||
children?: ReactNode
|
||||
@@ -21,60 +24,55 @@ export type IToastProps = {
|
||||
}
|
||||
type IToastContext = {
|
||||
notify: (props: IToastProps) => void
|
||||
close: () => void
|
||||
}
|
||||
|
||||
export const ToastContext = createContext<IToastContext>({} as IToastContext)
|
||||
export const useToastContext = () => useContext(ToastContext)
|
||||
const Toast = ({
|
||||
type = 'info',
|
||||
size = 'md',
|
||||
message,
|
||||
children,
|
||||
className,
|
||||
}: IToastProps) => {
|
||||
const { close } = useToastContext()
|
||||
// sometimes message is react node array. Not handle it.
|
||||
if (typeof message !== 'string')
|
||||
return null
|
||||
|
||||
return <div className={classNames(
|
||||
className,
|
||||
'fixed rounded-md p-4 my-4 mx-8 z-[9999]',
|
||||
'fixed w-[360px] rounded-xl my-4 mx-8 flex-grow z-[9999] overflow-hidden',
|
||||
size === 'md' ? 'p-3' : 'p-2',
|
||||
'border border-components-panel-border-subtle bg-components-panel-bg-blur shadow-sm',
|
||||
'top-0',
|
||||
'right-0',
|
||||
type === 'success' ? 'bg-green-50' : '',
|
||||
type === 'error' ? 'bg-red-50' : '',
|
||||
type === 'warning' ? 'bg-yellow-50' : '',
|
||||
type === 'info' ? 'bg-blue-50' : '',
|
||||
)}>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
{type === 'success' && <CheckCircleIcon className="w-5 h-5 text-green-400" aria-hidden="true" />}
|
||||
{type === 'error' && <XCircleIcon className="w-5 h-5 text-red-400" aria-hidden="true" />}
|
||||
{type === 'warning' && <ExclamationTriangleIcon className="w-5 h-5 text-yellow-400" aria-hidden="true" />}
|
||||
{type === 'info' && <InformationCircleIcon className="w-5 h-5 text-blue-400" aria-hidden="true" />}
|
||||
<div className={`absolute inset-0 opacity-40 ${
|
||||
(type === 'success' && 'bg-[linear-gradient(92deg,rgba(23,178,106,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||
|| (type === 'warning' && 'bg-[linear-gradient(92deg,rgba(247,144,9,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||
|| (type === 'error' && 'bg-[linear-gradient(92deg,rgba(240,68,56,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||
|| (type === 'info' && 'bg-[linear-gradient(92deg,rgba(11,165,236,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||
}`}
|
||||
/>
|
||||
<div className={`flex ${size === 'md' ? 'gap-1' : 'gap-0.5'}`}>
|
||||
<div className={`flex justify-center items-center ${size === 'md' ? 'p-0.5' : 'p-1'}`}>
|
||||
{type === 'success' && <RiCheckboxCircleFill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-success`} aria-hidden="true" />}
|
||||
{type === 'error' && <RiErrorWarningFill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-destructive`} aria-hidden="true" />}
|
||||
{type === 'warning' && <RiAlertFill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-warning-secondary`} aria-hidden="true" />}
|
||||
{type === 'info' && <RiInformation2Fill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-accent`} aria-hidden="true" />}
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className={
|
||||
classNames(
|
||||
'text-sm font-medium',
|
||||
type === 'success' ? 'text-green-800' : '',
|
||||
type === 'error' ? 'text-red-800' : '',
|
||||
type === 'warning' ? 'text-yellow-800' : '',
|
||||
type === 'info' ? 'text-blue-800' : '',
|
||||
)
|
||||
}>{message}</h3>
|
||||
{children && <div className={
|
||||
classNames(
|
||||
'mt-2 text-sm',
|
||||
type === 'success' ? 'text-green-700' : '',
|
||||
type === 'error' ? 'text-red-700' : '',
|
||||
type === 'warning' ? 'text-yellow-700' : '',
|
||||
type === 'info' ? 'text-blue-700' : '',
|
||||
)
|
||||
}>
|
||||
<div className={`flex py-1 ${size === 'md' ? 'px-1' : 'px-0.5'} flex-col items-start gap-1 flex-grow`}>
|
||||
<div className='text-text-primary system-sm-semibold'>{message}</div>
|
||||
{children && <div className='text-text-secondary system-xs-regular'>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<ActionButton className='z-[1000]' onClick={close}>
|
||||
<RiCloseLine className='w-4 h-4 flex-shrink-0 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -106,6 +104,7 @@ export const ToastProvider = ({
|
||||
setMounted(true)
|
||||
setParams(props)
|
||||
},
|
||||
close: () => setMounted(false),
|
||||
}}>
|
||||
{mounted && <Toast {...params} />}
|
||||
{children}
|
||||
@@ -114,16 +113,17 @@ export const ToastProvider = ({
|
||||
|
||||
Toast.notify = ({
|
||||
type,
|
||||
size = 'md',
|
||||
message,
|
||||
duration,
|
||||
className,
|
||||
}: Pick<IToastProps, 'type' | 'message' | 'duration' | 'className'>) => {
|
||||
}: Pick<IToastProps, 'type' | 'size' | 'message' | 'duration' | 'className'>) => {
|
||||
const defaultDuring = (type === 'success' || type === 'info') ? 3000 : 6000
|
||||
if (typeof window === 'object') {
|
||||
const holder = document.createElement('div')
|
||||
const root = createRoot(holder)
|
||||
|
||||
root.render(<Toast type={type} message={message} duration={duration} className={className} />)
|
||||
root.render(<Toast type={type} size={size} message={message} duration={duration} className={className} />)
|
||||
document.body.appendChild(holder)
|
||||
setTimeout(() => {
|
||||
if (holder)
|
||||
|
||||
@@ -10,8 +10,9 @@ import {
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAlertLine,
|
||||
RiAlertFill,
|
||||
RiCloseLine,
|
||||
RiFileDownloadLine,
|
||||
} from '@remixicon/react'
|
||||
import { WORKFLOW_DATA_UPDATE } from './constants'
|
||||
import {
|
||||
@@ -21,11 +22,19 @@ import {
|
||||
initialEdges,
|
||||
initialNodes,
|
||||
} from './utils'
|
||||
import {
|
||||
importDSL,
|
||||
importDSLConfirm,
|
||||
} from '@/service/apps'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import {
|
||||
DSLImportMode,
|
||||
DSLImportStatus,
|
||||
} from '@/models/app'
|
||||
import Uploader from '@/app/components/app/create-from-dsl-modal/uploader'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { updateWorkflowDraftFromDSL } from '@/service/workflow'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
@@ -48,6 +57,10 @@ const UpdateDSLModal = ({
|
||||
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 readFile = (file: File) => {
|
||||
const reader = new FileReader()
|
||||
@@ -66,6 +79,51 @@ const UpdateDSLModal = ({
|
||||
setFileContent('')
|
||||
}
|
||||
|
||||
const handleWorkflowUpdate = async (app_id: string) => {
|
||||
const {
|
||||
graph,
|
||||
features,
|
||||
hash,
|
||||
} = await fetchWorkflowDraft(`/apps/${app_id}/workflows/draft`)
|
||||
|
||||
const { nodes, edges, viewport } = graph
|
||||
const newFeatures = {
|
||||
file: {
|
||||
image: {
|
||||
enabled: !!features.file_upload?.image?.enabled,
|
||||
number_limits: features.file_upload?.image?.number_limits || 3,
|
||||
transfer_methods: features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
},
|
||||
enabled: !!(features.file_upload?.enabled || features.file_upload?.image?.enabled),
|
||||
allowed_file_types: features.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
|
||||
allowed_file_extensions: features.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
|
||||
allowed_file_upload_methods: features.file_upload?.allowed_file_upload_methods || features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
number_limits: features.file_upload?.number_limits || features.file_upload?.image?.number_limits || 3,
|
||||
},
|
||||
opening: {
|
||||
enabled: !!features.opening_statement,
|
||||
opening_statement: features.opening_statement,
|
||||
suggested_questions: features.suggested_questions,
|
||||
},
|
||||
suggested: features.suggested_questions_after_answer || { enabled: false },
|
||||
speech2text: features.speech_to_text || { enabled: false },
|
||||
text2speech: features.text_to_speech || { enabled: false },
|
||||
citation: features.retriever_resource || { enabled: false },
|
||||
moderation: features.sensitive_word_avoidance || { enabled: false },
|
||||
}
|
||||
|
||||
eventEmitter?.emit({
|
||||
type: WORKFLOW_DATA_UPDATE,
|
||||
payload: {
|
||||
nodes: initialNodes(nodes, edges),
|
||||
edges: initialEdges(edges, nodes),
|
||||
viewport,
|
||||
features: newFeatures,
|
||||
hash,
|
||||
},
|
||||
} as any)
|
||||
}
|
||||
|
||||
const isCreatingRef = useRef(false)
|
||||
const handleImport: MouseEventHandler = useCallback(async () => {
|
||||
if (isCreatingRef.current)
|
||||
@@ -76,51 +134,39 @@ const UpdateDSLModal = ({
|
||||
try {
|
||||
if (appDetail && fileContent) {
|
||||
setLoading(true)
|
||||
const {
|
||||
graph,
|
||||
features,
|
||||
hash,
|
||||
} = await updateWorkflowDraftFromDSL(appDetail.id, fileContent)
|
||||
const { nodes, edges, viewport } = graph
|
||||
const newFeatures = {
|
||||
file: {
|
||||
image: {
|
||||
enabled: !!features.file_upload?.image?.enabled,
|
||||
number_limits: features.file_upload?.image?.number_limits || 3,
|
||||
transfer_methods: features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
},
|
||||
enabled: !!(features.file_upload?.enabled || features.file_upload?.image?.enabled),
|
||||
allowed_file_types: features.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
|
||||
allowed_file_extensions: features.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
|
||||
allowed_file_upload_methods: features.file_upload?.allowed_file_upload_methods || features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
number_limits: features.file_upload?.number_limits || features.file_upload?.image?.number_limits || 3,
|
||||
},
|
||||
opening: {
|
||||
enabled: !!features.opening_statement,
|
||||
opening_statement: features.opening_statement,
|
||||
suggested_questions: features.suggested_questions,
|
||||
},
|
||||
suggested: features.suggested_questions_after_answer || { enabled: false },
|
||||
speech2text: features.speech_to_text || { enabled: false },
|
||||
text2speech: features.text_to_speech || { enabled: false },
|
||||
citation: features.retriever_resource || { enabled: false },
|
||||
moderation: features.sensitive_word_avoidance || { enabled: false },
|
||||
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 (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
|
||||
if (!app_id) {
|
||||
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||
return
|
||||
}
|
||||
handleWorkflowUpdate(app_id)
|
||||
if (onImport)
|
||||
onImport()
|
||||
notify({
|
||||
type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
|
||||
message: t(status === DSLImportStatus.COMPLETED ? 'workflow.common.importSuccess' : 'workflow.common.importWarning'),
|
||||
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('workflow.common.importWarningDetails'),
|
||||
})
|
||||
setLoading(false)
|
||||
onCancel()
|
||||
}
|
||||
else if (status === DSLImportStatus.PENDING) {
|
||||
setShow(false)
|
||||
setTimeout(() => {
|
||||
setShowErrorModal(true)
|
||||
}, 300)
|
||||
setVersions({
|
||||
importedVersion: imported_dsl_version ?? '',
|
||||
systemVersion: current_dsl_version ?? '',
|
||||
})
|
||||
setImportId(id)
|
||||
}
|
||||
else {
|
||||
setLoading(false)
|
||||
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||
}
|
||||
eventEmitter?.emit({
|
||||
type: WORKFLOW_DATA_UPDATE,
|
||||
payload: {
|
||||
nodes: initialNodes(nodes, edges),
|
||||
edges: initialEdges(edges, nodes),
|
||||
viewport,
|
||||
features: newFeatures,
|
||||
hash,
|
||||
},
|
||||
} as any)
|
||||
if (onImport)
|
||||
onImport()
|
||||
notify({ type: 'success', message: t('workflow.common.importSuccess') })
|
||||
setLoading(false)
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
@@ -130,52 +176,119 @@ const UpdateDSLModal = ({
|
||||
isCreatingRef.current = false
|
||||
}, [currentFile, fileContent, onCancel, notify, t, eventEmitter, appDetail, onImport])
|
||||
|
||||
const onUpdateDSLConfirm: MouseEventHandler = async () => {
|
||||
try {
|
||||
if (!importId)
|
||||
return
|
||||
const response = await importDSLConfirm({
|
||||
import_id: importId,
|
||||
})
|
||||
|
||||
const { status, app_id } = response
|
||||
|
||||
if (status === DSLImportStatus.COMPLETED) {
|
||||
if (!app_id) {
|
||||
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||
return
|
||||
}
|
||||
handleWorkflowUpdate(app_id)
|
||||
if (onImport)
|
||||
onImport()
|
||||
notify({ type: 'success', message: t('workflow.common.importSuccess') })
|
||||
setLoading(false)
|
||||
onCancel()
|
||||
}
|
||||
else if (status === DSLImportStatus.FAILED) {
|
||||
setLoading(false)
|
||||
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
setLoading(false)
|
||||
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className='p-6 w-[520px] rounded-2xl'
|
||||
isShow={true}
|
||||
onClose={() => {}}
|
||||
>
|
||||
<div className='flex items-center justify-between mb-6'>
|
||||
<div className='text-2xl font-semibold text-[#101828]'>{t('workflow.common.importDSL')}</div>
|
||||
<div className='flex items-center justify-center w-[22px] h-[22px] cursor-pointer' onClick={onCancel}>
|
||||
<RiCloseLine className='w-5 h-5 text-gray-500' />
|
||||
<>
|
||||
<Modal
|
||||
className='p-6 w-[520px] rounded-2xl'
|
||||
isShow={show}
|
||||
onClose={onCancel}
|
||||
>
|
||||
<div className='flex items-center justify-between mb-3'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t('workflow.common.importDSL')}</div>
|
||||
<div className='flex items-center justify-center w-[22px] h-[22px] cursor-pointer' onClick={onCancel}>
|
||||
<RiCloseLine className='w-[18px] h-[18px] text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex relative p-2 mb-2 gap-0.5 flex-grow rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xs overflow-hidden'>
|
||||
<div className='absolute top-0 left-0 w-full h-full opacity-40 bg-[linear-gradient(92deg,rgba(247,144,9,0.25)_0%,rgba(255,255,255,0.00)_100%)]' />
|
||||
<div className='flex p-1 justify-center items-start'>
|
||||
<RiAlertFill className='w-4 h-4 flex-shrink-0 text-text-warning-secondary' />
|
||||
</div>
|
||||
<div className='flex py-1 flex-col items-start gap-0.5 flex-grow'>
|
||||
<div className='text-text-primary system-xs-medium whitespace-pre-line'>{t('workflow.common.importDSLTip')}</div>
|
||||
<div className='flex pt-1 pb-0.5 items-start gap-1 self-stretch'>
|
||||
<Button
|
||||
size='small'
|
||||
variant='secondary'
|
||||
className='z-[1000]'
|
||||
onClick={onBackup}
|
||||
>
|
||||
<RiFileDownloadLine className='w-3.5 h-3.5 text-components-button-secondary-text' />
|
||||
<div className='flex px-[3px] justify-center items-center gap-1'>
|
||||
{t('workflow.common.backupCurrentDraft')}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex mb-4 px-4 py-3 bg-[#FFFAEB] rounded-xl border border-[#FEDF89]'>
|
||||
<RiAlertLine className='shrink-0 mt-0.5 mr-2 w-4 h-4 text-[#F79009]' />
|
||||
<div>
|
||||
<div className='mb-2 text-sm font-medium text-[#354052]'>{t('workflow.common.importDSLTip')}</div>
|
||||
<div className='pt-2 text-text-primary system-md-semibold'>
|
||||
{t('workflow.common.chooseDSL')}
|
||||
</div>
|
||||
<div className='flex w-full py-4 flex-col justify-center items-start gap-4 self-stretch'>
|
||||
<Uploader
|
||||
file={currentFile}
|
||||
updateFile={handleFile}
|
||||
className='!mt-0 w-full'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex pt-5 gap-2 items-center justify-end self-stretch'>
|
||||
<Button onClick={onCancel}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button
|
||||
variant='secondary-accent'
|
||||
onClick={onBackup}
|
||||
disabled={!currentFile || loading}
|
||||
variant='warning'
|
||||
onClick={handleImport}
|
||||
loading={loading}
|
||||
>
|
||||
{t('workflow.common.backupCurrentDraft')}
|
||||
{t('workflow.common.overwriteAndImport')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mb-8'>
|
||||
<div className='mb-1 text-[13px] font-semibold text-[#354052]'>
|
||||
{t('workflow.common.chooseDSL')}
|
||||
</Modal>
|
||||
<Modal
|
||||
isShow={showErrorModal}
|
||||
onClose={() => setShowErrorModal(false)}
|
||||
className='w-[480px]'
|
||||
>
|
||||
<div className='flex pb-4 flex-col items-start gap-2 self-stretch'>
|
||||
<div className='text-text-primary title-2xl-semi-bold'>{t('app.newApp.appCreateDSLErrorTitle')}</div>
|
||||
<div className='flex flex-grow flex-col text-text-secondary system-md-regular'>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart1')}</div>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart2')}</div>
|
||||
<br />
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart3')}<span className='system-md-medium'>{versions?.importedVersion}</span></div>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart4')}<span className='system-md-medium'>{versions?.systemVersion}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<Uploader
|
||||
file={currentFile}
|
||||
updateFile={handleFile}
|
||||
className='!mt-0'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex justify-end'>
|
||||
<Button className='mr-2' onClick={onCancel}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button
|
||||
disabled={!currentFile || loading}
|
||||
variant='warning'
|
||||
onClick={handleImport}
|
||||
loading={loading}
|
||||
>
|
||||
{t('workflow.common.overwriteAndImport')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<div className='flex pt-6 justify-end items-start gap-2 self-stretch'>
|
||||
<Button variant='secondary' onClick={() => setShowErrorModal(false)}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button variant='primary' destructive onClick={onUpdateDSLConfirm}>{t('app.newApp.Confirm')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user