dify/web/app/components/app-sidebar/dataset-info/dropdown.tsx
Stephen Zhou a84c2d36a3
style: format with vp fmt (#38803)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-12 15:57:46 +00:00

235 lines
7.9 KiB
TypeScript

import type { DataSet } from '@/models/datasets'
import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { cn } from '@langgenius/dify-ui/cn'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { toast } from '@langgenius/dify-ui/toast'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { userProfileIdAtom } from '@/context/account-state'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { useRouter } from '@/next/navigation'
import { checkIsUsedInApp, deleteDataset } from '@/service/datasets'
import { datasetDetailQueryKeyPrefix, useInvalidDatasetList } from '@/service/knowledge/use-dataset'
import { useInvalid } from '@/service/use-base'
import { useExportPipelineDSL } from '@/service/use-pipeline'
import { downloadBlob } from '@/utils/download'
import { getDatasetACLCapabilities } from '@/utils/permission'
import ActionButton from '../../base/action-button'
import RenameDatasetModal from '../../datasets/rename-modal'
import Menu from './menu'
type DropDownProps = {
expand: boolean
triggerClassName?: string
}
type JsonErrorResponse = {
json: () => Promise<{ message?: string }>
}
const isJsonErrorResponse = (error: unknown): error is JsonErrorResponse => {
return (
typeof error === 'object' &&
error !== null &&
'json' in error &&
typeof error.json === 'function'
)
}
const getErrorMessage = async (error: unknown) => {
if (!isJsonErrorResponse(error)) return 'Unknown error'
const res = await error.json()
return res?.message || 'Unknown error'
}
const DropDown = ({ expand, triggerClassName }: DropDownProps) => {
const { t } = useTranslation()
const { push, replace } = useRouter()
const [open, setOpen] = useState(false)
const [showRenameModal, setShowRenameModal] = useState(false)
const [confirmMessage, setConfirmMessage] = useState<string>('')
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const dataset = useDatasetDetailContextWithSelector((state) => state.dataset) as DataSet
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const isRbacEnabled = systemFeatures.rbac_enabled
const datasetACLCapabilities = React.useMemo(
() =>
getDatasetACLCapabilities(dataset?.permission_keys, {
currentUserId,
resourceMaintainer: dataset?.maintainer,
workspacePermissionKeys,
isRbacEnabled,
}),
[
dataset?.maintainer,
dataset?.permission_keys,
currentUserId,
isRbacEnabled,
workspacePermissionKeys,
],
)
const canShowOperations =
datasetACLCapabilities.canEdit ||
datasetACLCapabilities.canImportExportDSL ||
datasetACLCapabilities.canAccessConfig ||
datasetACLCapabilities.canDelete
const invalidDatasetList = useInvalidDatasetList()
const invalidDatasetDetail = useInvalid([...datasetDetailQueryKeyPrefix, dataset.id])
const refreshDataset = useCallback(() => {
invalidDatasetList()
invalidDatasetDetail()
}, [invalidDatasetDetail, invalidDatasetList])
const openRenameModal = useCallback(() => {
setOpen(false)
queueMicrotask(() => {
setShowRenameModal(true)
})
}, [])
const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL()
const handleExportPipeline = useCallback(
async (include = false) => {
const { pipeline_id, name } = dataset
if (!pipeline_id) return
setOpen(false)
try {
const { data } = await exportPipelineConfig({
pipelineId: pipeline_id,
include,
})
const file = new Blob([data], { type: 'application/yaml' })
downloadBlob({ data: file, fileName: `${name}.pipeline` })
} catch {
toast.error(t(($) => $.exportFailed, { ns: 'app' }))
}
},
[dataset, exportPipelineConfig, t],
)
const detectIsUsedByApp = useCallback(async () => {
setOpen(false)
try {
const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
setConfirmMessage(
isUsedByApp
? t(($) => $.datasetUsedByApp, { ns: 'dataset' })!
: t(($) => $.deleteDatasetConfirmContent, { ns: 'dataset' })!,
)
setShowConfirmDelete(true)
} catch (e: unknown) {
toast.error(await getErrorMessage(e))
}
}, [dataset.id, t])
const openAccessConfig = useCallback(() => {
setOpen(false)
push(`/datasets/${dataset.id}/access-config`)
}, [dataset.id, push])
const onConfirmDelete = useCallback(async () => {
try {
await deleteDataset(dataset.id)
toast(
t(($) => $.datasetDeleted, { ns: 'dataset' }),
{ type: 'success' },
)
invalidDatasetList()
replace('/datasets')
} finally {
setShowConfirmDelete(false)
}
}, [dataset.id, replace, invalidDatasetList, t])
if (!canShowOperations) return null
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
render={
<ActionButton
aria-label={t(($) => $['operation.more'], { ns: 'common' })}
size={expand ? 'l' : 'm'}
className={cn('data-popup-open:bg-state-base-hover', triggerClassName)}
/>
}
>
<span aria-hidden className="i-ri-more-fill size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent
placement={expand ? 'bottom-end' : 'right-start'}
sideOffset={4}
popupClassName="border-0 bg-transparent p-0 shadow-none backdrop-blur-none"
>
<Menu
showEdit={datasetACLCapabilities.canEdit}
showDelete={datasetACLCapabilities.canDelete}
showExportPipeline={datasetACLCapabilities.canImportExportDSL}
showAccessConfig={datasetACLCapabilities.canAccessConfig}
openRenameModal={openRenameModal}
handleExportPipeline={handleExportPipeline}
detectIsUsedByApp={detectIsUsedByApp}
openAccessConfig={openAccessConfig}
/>
</DropdownMenuContent>
{showRenameModal && (
<RenameDatasetModal
show={showRenameModal}
dataset={dataset!}
onClose={() => setShowRenameModal(false)}
onSuccess={refreshDataset}
/>
)}
<AlertDialog
open={showConfirmDelete}
onOpenChange={(open) => !open && setShowConfirmDelete(false)}
>
<AlertDialogContent>
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
{t(($) => $.deleteDatasetConfirmTitle, { ns: 'dataset' })}
</AlertDialogTitle>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{confirmMessage}
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton>
{t(($) => $['operation.cancel'], { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton onClick={onConfirmDelete}>
{t(($) => $['operation.confirm'], { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
</DropdownMenu>
)
}
export default React.memo(DropDown)