feat: add DSL modal header and tab components; enhance pipeline import functionality

This commit is contained in:
twwu
2025-05-14 14:49:01 +08:00
parent de0cb06f8c
commit b713218cab
13 changed files with 229 additions and 114 deletions

View File

@ -0,0 +1,27 @@
import { RiCloseLine } from '@remixicon/react'
import React from 'react'
import { useTranslation } from 'react-i18next'
type HeaderProps = {
onClose: () => void
}
const Header = ({
onClose,
}: HeaderProps) => {
const { t } = useTranslation()
return (
<div className='title-2xl-semi-bold relative flex items-center justify-between pb-3 pl-6 pr-14 pt-6 text-text-primary'>
{t('app.importFromDSL')}
<div
className='absolute right-5 top-5 flex size-8 cursor-pointer items-center'
onClick={onClose}
>
<RiCloseLine className='size-[18px] text-text-tertiary' />
</div>
</div>
)
}
export default React.memo(Header)

View File

@ -3,35 +3,29 @@ import { useMemo, useRef, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react'
import { useDebounceFn, useKeyPress } from 'ahooks'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import { ToastContext } from '@/app/components/base/toast'
import {
importDSL,
importDSLConfirm,
} from '@/service/apps'
import {
DSLImportMode,
DSLImportStatus,
} from '@/models/app'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { useProviderContextSelector } from '@/context/provider-context'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
import cn from '@/utils/classnames'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { noop } from 'lodash-es'
import Uploader from './uploader'
import Header from './header'
import Tab from './tab'
import { useImportPipelineDSL, useImportPipelineDSLConfirm } from '@/service/use-pipeline'
type CreateFromDSLModalProps = {
show: boolean
onSuccess?: () => void
onClose: () => void
activeTab?: string
activeTab?: CreateFromDSLModalTab
dslUrl?: string
}
@ -76,14 +70,14 @@ const CreateFromDSLModal = ({
setFileContent('')
}
const isCurrentWorkspaceEditor = useAppContextWithSelector(state => state.isCurrentWorkspaceEditor)
const plan = useProviderContextSelector(state => state.plan)
const enableBilling = useProviderContextSelector(state => state.enableBilling)
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
const isCreatingRef = useRef(false)
// todo: replace with pipeline import DSL and check plugin dependencies
const { mutateAsync: importDSL } = useImportPipelineDSL()
const onCreate = async () => {
if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile)
return
@ -94,7 +88,6 @@ const CreateFromDSLModal = ({
isCreatingRef.current = true
try {
let response
if (currentTab === CreateFromDSLModalTab.FROM_FILE) {
response = await importDSL({
mode: DSLImportMode.YAML_CONTENT,
@ -110,7 +103,7 @@ const CreateFromDSLModal = ({
if (!response)
return
const { id, status, app_id, app_mode, imported_dsl_version, current_dsl_version } = response
const { id, status, pipeline_id, imported_dsl_version, current_dsl_version } = response
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
if (onSuccess)
onSuccess()
@ -122,9 +115,9 @@ const CreateFromDSLModal = ({
message: t(status === DSLImportStatus.COMPLETED ? 'app.newApp.appCreated' : 'app.newApp.caution'),
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('app.newApp.appCreateDSLWarning'),
})
if (app_id)
await handleCheckPluginDependencies(app_id)
getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
if (pipeline_id)
await handleCheckPluginDependencies(pipeline_id, true)
push(`datasets/${pipeline_id}/pipeline`)
}
else if (status === DSLImportStatus.PENDING) {
setVersions({
@ -142,8 +135,7 @@ const CreateFromDSLModal = ({
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
}
// eslint-disable-next-line unused-imports/no-unused-vars
catch (e) {
catch {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
finally {
@ -153,16 +145,13 @@ const CreateFromDSLModal = ({
const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 })
useKeyPress(['meta.enter', 'ctrl.enter'], () => {
if (show && !isAppsFull && ((currentTab === CreateFromDSLModalTab.FROM_FILE && currentFile) || (currentTab === CreateFromDSLModalTab.FROM_URL && dslUrlValue)))
handleCreateApp()
})
useKeyPress('esc', () => {
if (show && !showErrorModal)
onClose()
})
const { mutateAsync: importDSLConfirm } = useImportPipelineDSLConfirm()
const onDSLConfirm = async () => {
try {
if (!importId)
@ -171,7 +160,7 @@ const CreateFromDSLModal = ({
import_id: importId,
})
const { status, app_id, app_mode } = response
const { status, pipeline_id } = response
if (status === DSLImportStatus.COMPLETED) {
if (onSuccess)
@ -183,32 +172,19 @@ const CreateFromDSLModal = ({
type: 'success',
message: t('app.newApp.appCreated'),
})
if (app_id)
await handleCheckPluginDependencies(app_id)
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
if (pipeline_id)
await handleCheckPluginDependencies(pipeline_id, true)
push(`datasets/${pipeline_id}/pipeline`)
}
else if (status === DSLImportStatus.FAILED) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
}
// eslint-disable-next-line unused-imports/no-unused-vars
catch (e) {
catch {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
}
const tabs = [
{
key: CreateFromDSLModalTab.FROM_FILE,
label: t('app.importFromDSLFile'),
},
{
key: CreateFromDSLModalTab.FROM_URL,
label: t('app.importFromDSLUrl'),
},
]
const buttonDisabled = useMemo(() => {
if (isAppsFull)
return true
@ -226,36 +202,11 @@ const CreateFromDSLModal = ({
isShow={show}
onClose={noop}
>
<div className='title-2xl-semi-bold flex items-center justify-between pb-3 pl-6 pr-5 pt-6 text-text-primary'>
{t('app.importFromDSL')}
<div
className='flex h-8 w-8 cursor-pointer items-center'
onClick={() => onClose()}
>
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
</div>
</div>
<div className='system-md-semibold flex h-9 items-center space-x-6 border-b border-divider-subtle px-6 text-text-tertiary'>
{
tabs.map(tab => (
<div
key={tab.key}
className={cn(
'relative flex h-full cursor-pointer items-center',
currentTab === tab.key && 'text-text-primary',
)}
onClick={() => setCurrentTab(tab.key)}
>
{tab.label}
{
currentTab === tab.key && (
<div className='absolute bottom-0 h-[2px] w-full bg-util-colors-blue-brand-blue-brand-600'></div>
)
}
</div>
))
}
</div>
<Header onClose={onClose} />
<Tab
currentTab={currentTab}
setCurrentTab={setCurrentTab}
/>
<div className='px-6 py-4'>
{
currentTab === CreateFromDSLModalTab.FROM_FILE && (
@ -284,19 +235,17 @@ const CreateFromDSLModal = ({
<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>
<div className='flex justify-end gap-x-2 p-6 pt-5'>
<Button onClick={onClose}>
{t('app.newApp.Cancel')}
</Button>
<Button
disabled={buttonDisabled}
variant='primary'
onClick={handleCreateApp}
className='gap-1'
>
<span>{t('app.newApp.Create')}</span>
<div className='flex gap-0.5'>
<RiCommandLine size={14} className='system-kbd rounded-sm bg-components-kbd-bg-white p-0.5' />
<RiCornerDownLeftLine size={14} className='system-kbd rounded-sm bg-components-kbd-bg-white p-0.5' />
</div>
<span>{t('app.newApp.import')}</span>
</Button>
</div>
</Modal>

View File

@ -0,0 +1,44 @@
import React from 'react'
import { CreateFromDSLModalTab } from '@/app/components/app/create-from-dsl-modal'
import { useTranslation } from 'react-i18next'
import Item from './item'
type TabProps = {
currentTab: CreateFromDSLModalTab
setCurrentTab: (tab: CreateFromDSLModalTab) => void
}
const Tab = ({
currentTab,
setCurrentTab,
}: TabProps) => {
const { t } = useTranslation()
const tabs = [
{
key: CreateFromDSLModalTab.FROM_FILE,
label: t('app.importFromDSLFile'),
},
{
key: CreateFromDSLModalTab.FROM_URL,
label: t('app.importFromDSLUrl'),
},
]
return (
<div className='system-md-semibold flex h-9 items-center gap-x-6 border-b border-divider-subtle px-6 text-text-tertiary'>
{
tabs.map(tab => (
<Item
key={tab.key}
isActive={currentTab === tab.key}
label={tab.label}
onClick={setCurrentTab.bind(null, tab.key)}
/>
))
}
</div>
)
}
export default Tab

View File

@ -0,0 +1,33 @@
import React from 'react'
import cn from '@/utils/classnames'
type ItemProps = {
isActive: boolean
label: string
onClick: () => void
}
const Item = ({
isActive,
label,
onClick,
}: ItemProps) => {
return (
<div
className={cn(
'system-md-semibold relative flex h-full cursor-pointer items-center text-text-tertiary',
isActive && 'text-text-primary',
)}
onClick={onClick}
>
{label}
{
isActive && (
<div className='absolute bottom-0 h-0.5 w-full bg-util-colors-blue-brand-blue-brand-600' />
)
}
</div>
)
}
export default React.memo(Item)

View File

@ -76,4 +76,4 @@ const CreateOptions = () => {
)
}
export default React.memo(CreateOptions)
export default CreateOptions

View File

@ -10,7 +10,7 @@ type ActionsProps = {
handleShowTemplateDetails: () => void
showMoreOperations: boolean
openEditModal: () => void
handleExportDSL: () => void
handleExportDSL: (includeSecret?: boolean) => void
handleDelete: () => void
}

View File

@ -4,7 +4,12 @@ import Modal from '@/app/components/base/modal'
import EditPipelineInfo from './edit-pipeline-info'
import type { PipelineTemplate } from '@/models/pipeline'
import Confirm from '@/app/components/base/confirm'
import { useDeletePipeline, useExportPipelineDSL, useImportPipelineDSL, usePipelineTemplateById } from '@/service/use-pipeline'
import {
useDeletePipeline,
useExportPipelineDSL,
useImportPipelineDSL,
usePipelineTemplateById,
} from '@/service/use-pipeline'
import { downloadFile } from '@/utils/format'
import Toast from '@/app/components/base/toast'
import { DSLImportMode } from '@/models/app'
@ -30,7 +35,7 @@ const TemplateCard = ({
const [showDetailModal, setShowDetailModal] = useState(false)
const { refetch: getPipelineTemplateInfo } = usePipelineTemplateById(pipeline.id, false)
const { mutateAsync: importPipelineDSL } = useImportPipelineDSL()
const { mutateAsync: importDSL } = useImportPipelineDSL()
const { handleCheckPluginDependencies } = usePluginDependencies()
const handleUseTemplate = useCallback(async () => {
@ -45,19 +50,16 @@ const TemplateCard = ({
}
const request = {
mode: DSLImportMode.YAML_CONTENT,
name: pipeline.name,
yaml_content: pipelineTemplateInfo.export_data,
icon_info: pipeline.icon_info,
description: pipeline.description,
}
const newPipeline = await importPipelineDSL(request)
const newPipeline = await importDSL(request)
Toast.notify({
type: 'success',
message: t('app.newApp.appCreated'),
})
if (newPipeline.dataset_id)
await handleCheckPluginDependencies(newPipeline.dataset_id) // todo: replace with pipeline dependency check
push(`dataset/${newPipeline.dataset_id}/pipeline`)
if (newPipeline.pipeline_id)
await handleCheckPluginDependencies(newPipeline.pipeline_id, true)
push(`dataset/${newPipeline.pipeline_id}/pipeline`)
}
catch {
Toast.notify({
@ -65,7 +67,7 @@ const TemplateCard = ({
message: t('datasetPipeline.creation.errorTip'),
})
}
}, [getPipelineTemplateInfo, importPipelineDSL, pipeline, t, push, handleCheckPluginDependencies])
}, [getPipelineTemplateInfo, importDSL, t, handleCheckPluginDependencies, push])
const handleShowTemplateDetails = useCallback(() => {
setShowDetailModal(true)
@ -85,9 +87,12 @@ const TemplateCard = ({
const { mutateAsync: exportPipelineDSL, isPending: isExporting } = useExportPipelineDSL()
const handleExportDSL = useCallback(async () => {
const handleExportDSL = useCallback(async (includeSecret = false) => {
if (isExporting) return
await exportPipelineDSL(pipeline.id, {
await exportPipelineDSL({
pipeline_id: pipeline.id,
include_secret: includeSecret,
}, {
onSuccess: (res) => {
const blob = new Blob([res.data], { type: 'application/yaml' })
downloadFile({

View File

@ -115,7 +115,7 @@ const Container = () => {
onChange={e => handleKeywordsChange(e.target.value)}
onClear={() => handleKeywordsChange('')}
/>
<div className="h-4 w-[1px] bg-divider-regular" />
<div className='h-4 w-[1px] bg-divider-regular' />
<Button
className='shadows-shadow-xs gap-0.5'
onClick={() => setShowExternalApiPanel(true)}