feat: Check and compare the DSL version before import an app (#10969)

Co-authored-by: Yi <yxiaoisme@gmail.com>
This commit is contained in:
-LAN- 2024-11-22 15:05:04 +08:00 • committed by GitHub
commit 5172f0bf39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1101 additions and 874 deletions

View file

@ -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>
</>
)
}

View file

@ -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>