mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 01:18:05 +08:00
feat(web): add app bundle import/export UI support
This commit is contained in:
@ -27,7 +27,7 @@ import { useProviderContext } from '@/context/provider-context'
|
||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||
import { copyApp, deleteApp, exportAppBundle, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||
import { fetchInstalledAppList } from '@/service/explore'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
@ -193,6 +193,39 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
const onExportBundle = async (include = false) => {
|
||||
try {
|
||||
await exportAppBundle({
|
||||
appID: app.id,
|
||||
include,
|
||||
})
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('exportBundleFailed', { ns: 'app' }) })
|
||||
}
|
||||
}
|
||||
|
||||
const [secretEnvListForBundle, setSecretEnvListForBundle] = useState<EnvironmentVariable[]>([])
|
||||
|
||||
const exportBundleCheck = async () => {
|
||||
if (app.mode !== AppModeEnum.WORKFLOW && app.mode !== AppModeEnum.ADVANCED_CHAT) {
|
||||
onExportBundle()
|
||||
return
|
||||
}
|
||||
try {
|
||||
const workflowDraft = await fetchWorkflowDraft(`/apps/${app.id}/workflows/draft`)
|
||||
const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret')
|
||||
if (list.length === 0) {
|
||||
onExportBundle()
|
||||
return
|
||||
}
|
||||
setSecretEnvListForBundle(list)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('exportBundleFailed', { ns: 'app' }) })
|
||||
}
|
||||
}
|
||||
|
||||
const onSwitch = () => {
|
||||
if (onRefresh)
|
||||
onRefresh()
|
||||
@ -228,6 +261,12 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
e.preventDefault()
|
||||
exportCheck()
|
||||
}
|
||||
const onClickExportBundle = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
exportBundleCheck()
|
||||
}
|
||||
const onClickSwitch = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
@ -278,6 +317,9 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickExport}>
|
||||
<span className="system-sm-regular text-text-secondary">{t('export', { ns: 'app' })}</span>
|
||||
</button>
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickExportBundle}>
|
||||
<span className="system-sm-regular text-text-secondary">{t('exportBundle', { ns: 'app' })}</span>
|
||||
</button>
|
||||
{(app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT) && (
|
||||
<>
|
||||
<Divider className="my-1" />
|
||||
@ -514,6 +556,13 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
onClose={() => setSecretEnvList([])}
|
||||
/>
|
||||
)}
|
||||
{secretEnvListForBundle.length > 0 && (
|
||||
<DSLExportConfirmModal
|
||||
envList={secretEnvListForBundle}
|
||||
onConfirm={onExportBundle}
|
||||
onClose={() => setSecretEnvListForBundle([])}
|
||||
/>
|
||||
)}
|
||||
{showAccessControl && (
|
||||
<AccessControl app={app} onConfirm={onUpdateAccessControl} onClose={() => setShowAccessControl(false)} />
|
||||
)}
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export type DroppedFileType = 'dsl' | 'bundle'
|
||||
|
||||
type DSLDragDropHookProps = {
|
||||
onDSLFileDropped: (file: File) => void
|
||||
onBundleFileDropped?: (file: File) => void
|
||||
containerRef: React.RefObject<HTMLDivElement | null>
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export const useDSLDragDrop = ({ onDSLFileDropped, containerRef, enabled = true }: DSLDragDropHookProps) => {
|
||||
export const useDSLDragDrop = ({ onDSLFileDropped, onBundleFileDropped, containerRef, enabled = true }: DSLDragDropHookProps) => {
|
||||
const [dragging, setDragging] = useState(false)
|
||||
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
@ -41,8 +44,14 @@ export const useDSLDragDrop = ({ onDSLFileDropped, containerRef, enabled = true
|
||||
return
|
||||
|
||||
const file = files[0]
|
||||
if (file.name.toLowerCase().endsWith('.yaml') || file.name.toLowerCase().endsWith('.yml'))
|
||||
const fileName = file.name.toLowerCase()
|
||||
|
||||
if (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) {
|
||||
onDSLFileDropped(file)
|
||||
}
|
||||
else if (fileName.endsWith('.zip') && onBundleFileDropped) {
|
||||
onBundleFileDropped(file)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -17,17 +17,22 @@ import {
|
||||
import { parseAsString, useQueryState } from 'nuqs'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Input from '@/app/components/base/input'
|
||||
import TabSliderNew from '@/app/components/base/tab-slider-new'
|
||||
import TagFilter from '@/app/components/base/tag-management/filter'
|
||||
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { CheckModal } from '@/hooks/use-pay'
|
||||
import { DSLImportStatus } from '@/models/app'
|
||||
import { importAppBundle } from '@/service/apps'
|
||||
import { useInfiniteAppList } from '@/service/use-apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import AppCard from './app-card'
|
||||
import { AppCardSkeleton } from './app-card-skeleton'
|
||||
@ -61,8 +66,10 @@ const List: FC<Props> = ({
|
||||
controlRefreshList = 0,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const router = useRouter()
|
||||
const { push } = useRouter()
|
||||
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace } = useAppContext()
|
||||
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
|
||||
const [activeTab, setActiveTab] = useQueryState(
|
||||
@ -90,8 +97,41 @@ const List: FC<Props> = ({
|
||||
setShowCreateFromDSLModal(true)
|
||||
}, [])
|
||||
|
||||
const [importingBundle, setImportingBundle] = useState(false)
|
||||
|
||||
const handleBundleFileDropped = useCallback(async (file: File) => {
|
||||
if (importingBundle)
|
||||
return
|
||||
setImportingBundle(true)
|
||||
try {
|
||||
const response = await importAppBundle({ file })
|
||||
const { status, app_id, app_mode } = response
|
||||
|
||||
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
|
||||
notify({
|
||||
type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
|
||||
message: t(status === DSLImportStatus.COMPLETED ? 'newApp.appCreated' : 'newApp.caution', { ns: 'app' }),
|
||||
})
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
if (app_id)
|
||||
getRedirection(isCurrentWorkspaceEditor, { id: app_id, mode: app_mode }, push)
|
||||
}
|
||||
else {
|
||||
notify({ type: 'error', message: t('importBundleFailed', { ns: 'app' }) })
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
const error = e as Error
|
||||
notify({ type: 'error', message: error.message || t('importBundleFailed', { ns: 'app' }) })
|
||||
}
|
||||
finally {
|
||||
setImportingBundle(false)
|
||||
}
|
||||
}, [importingBundle, isCurrentWorkspaceEditor, notify, push, t])
|
||||
|
||||
const { dragging } = useDSLDragDrop({
|
||||
onDSLFileDropped: handleDSLFileDropped,
|
||||
onBundleFileDropped: handleBundleFileDropped,
|
||||
containerRef,
|
||||
enabled: isCurrentWorkspaceEditor,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user