feat: Enhance dataset dropdown functionality with export and delete options

feat/rag-2
twwu 10 months ago
parent 8fc15c83d0
commit 59c3305dcc

@ -4,6 +4,17 @@ import ActionButton from '../../base/action-button'
import { RiMoreFill } from '@remixicon/react' import { RiMoreFill } from '@remixicon/react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import Menu from './menu' import Menu from './menu'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import type { DataSet } from '@/models/datasets'
import { datasetDetailQueryKeyPrefix, useResetDatasetList } from '@/service/knowledge/use-dataset'
import { useInvalid } from '@/service/use-base'
import { useExportPipelineDSL } from '@/service/use-pipeline'
import Toast from '../../base/toast'
import { useTranslation } from 'react-i18next'
import RenameDatasetModal from '../../datasets/rename-modal'
import { checkIsUsedInApp, deleteDataset } from '@/service/datasets'
import Confirm from '../../base/confirm'
type DropDownProps = { type DropDownProps = {
expand: boolean expand: boolean
@ -12,12 +23,81 @@ type DropDownProps = {
const DropDown = ({ const DropDown = ({
expand, expand,
}: DropDownProps) => { }: DropDownProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [showRenameModal, setShowRenameModal] = useState(false)
const [confirmMessage, setConfirmMessage] = useState<string>('')
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const isCurrentWorkspaceDatasetOperator = useAppContextWithSelector(state => state.isCurrentWorkspaceDatasetOperator)
const dataset = useDatasetDetailContextWithSelector(state => state.dataset) as DataSet
const handleTrigger = useCallback(() => { const handleTrigger = useCallback(() => {
setOpen(prev => !prev) setOpen(prev => !prev)
}, []) }, [])
const resetDatasetList = useResetDatasetList()
const invalidDatasetDetail = useInvalid([...datasetDetailQueryKeyPrefix, dataset.id])
const refreshDataset = useCallback(() => {
resetDatasetList()
invalidDatasetDetail()
}, [invalidDatasetDetail, resetDatasetList])
const openRenameModal = useCallback(() => {
setShowRenameModal(true)
handleTrigger()
}, [handleTrigger])
const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL()
const handleExportPipeline = useCallback(async (include = false) => {
const { pipeline_id, name } = dataset
if (!pipeline_id)
return
handleTrigger()
try {
const { data } = await exportPipelineConfig({
pipelineId: pipeline_id,
include,
})
const a = document.createElement('a')
const file = new Blob([data], { type: 'application/yaml' })
a.href = URL.createObjectURL(file)
a.download = `${name}.yml`
a.click()
}
catch {
Toast.notify({ type: 'error', message: t('app.exportFailed') })
}
}, [dataset, exportPipelineConfig, handleTrigger, t])
const detectIsUsedByApp = useCallback(async () => {
try {
const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
setConfirmMessage(isUsedByApp ? t('dataset.datasetUsedByApp')! : t('dataset.deleteDatasetConfirmContent')!)
setShowConfirmDelete(true)
}
catch (e: any) {
const res = await e.json()
Toast.notify({ type: 'error', message: res?.message || 'Unknown error' })
}
finally {
handleTrigger()
}
}, [dataset.id, handleTrigger, t])
const onConfirmDelete = useCallback(async () => {
try {
await deleteDataset(dataset.id)
Toast.notify({ type: 'success', message: t('dataset.datasetDeleted') })
refreshDataset()
}
finally {
setShowConfirmDelete(false)
}
}, [dataset.id, refreshDataset, t])
return ( return (
<PortalToFollowElem <PortalToFollowElem
open={open} open={open}
@ -36,8 +116,30 @@ const DropDown = ({
</ActionButton> </ActionButton>
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent> <PortalToFollowElemContent>
<Menu /> <Menu
showDelete={!isCurrentWorkspaceDatasetOperator}
openRenameModal={openRenameModal}
handleExportPipeline={handleExportPipeline}
detectIsUsedByApp={detectIsUsedByApp}
/>
</PortalToFollowElemContent> </PortalToFollowElemContent>
{showRenameModal && (
<RenameDatasetModal
show={showRenameModal}
dataset={dataset!}
onClose={() => setShowRenameModal(false)}
onSuccess={refreshDataset}
/>
)}
{showConfirmDelete && (
<Confirm
title={t('dataset.deleteDatasetConfirmTitle')}
content={confirmMessage}
isShow={showConfirmDelete}
onConfirm={onConfirmDelete}
onCancel={() => setShowConfirmDelete(false)}
/>
)}
</PortalToFollowElem> </PortalToFollowElem>
) )
} }

@ -4,7 +4,7 @@ import type { RemixiconComponentType } from '@remixicon/react'
type MenuItemProps = { type MenuItemProps = {
name: string name: string
Icon: RemixiconComponentType Icon: RemixiconComponentType
handleClick?: () => void handleClick?: (e: React.MouseEvent<HTMLDivElement>) => void
} }
const MenuItem = ({ const MenuItem = ({
@ -15,9 +15,7 @@ const MenuItem = ({
return ( return (
<div <div
className='flex items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover' className='flex items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
onClick={() => { onClick={handleClick}
handleClick?.()
}}
> >
<Icon className='size-4 text-text-tertiary' /> <Icon className='size-4 text-text-tertiary' />
<span className='system-md-regular px-1 text-text-secondary'>{name}</span> <span className='system-md-regular px-1 text-text-secondary'>{name}</span>

@ -1,19 +1,68 @@
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import MenuItem from './menu-item' import MenuItem from './menu-item'
import { RiEditLine } from '@remixicon/react' import { RiDeleteBinLine, RiEditLine, RiFileDownloadLine } from '@remixicon/react'
import { noop } from 'lodash-es' import Divider from '../../base/divider'
const Menu = () => { type MenuProps = {
showDelete: boolean
openRenameModal: () => void
handleExportPipeline: () => void
detectIsUsedByApp: () => void
}
const Menu = ({
showDelete,
openRenameModal,
handleExportPipeline,
detectIsUsedByApp,
}: MenuProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const dataset = useDatasetDetailContextWithSelector(state => state.dataset)
const onClickRename = async (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.preventDefault()
openRenameModal()
}
const onClickExport = async (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.preventDefault()
handleExportPipeline()
}
const onClickDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.preventDefault()
detectIsUsedByApp()
}
return ( return (
<div className='flex w-[200px] flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]'> <div className='flex w-[200px] flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]'>
<div className='flex flex-col p-1'> <div className='flex flex-col p-1'>
<MenuItem Icon={RiEditLine} name={t('common.operation.edit')} handleClick={noop} /> <MenuItem
Icon={RiEditLine}
name={t('common.operation.edit')}
handleClick={onClickRename}
/>
<MenuItem
Icon={RiFileDownloadLine}
name={t('datasetPipeline.operations.exportPipeline')}
handleClick={onClickExport}
/>
</div> </div>
{showDelete && (
<>
<Divider type='horizontal' className='my-0 bg-divider-subtle' />
<div className='flex flex-col p-1'>
<MenuItem
Icon={RiDeleteBinLine}
name={t('common.operation.delete')}
handleClick={onClickDelete}
/>
</div>
</>
)}
</div> </div>
) )
} }

@ -83,6 +83,10 @@ const DatasetCard = ({
return dayjs(time * 1_000).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow() return dayjs(time * 1_000).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow()
}, [language]) }, [language])
const openRenameModal = useCallback(() => {
setShowRenameModal(true)
}, [])
const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL() const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL()
const handleExportPipeline = useCallback(async (include = false) => { const handleExportPipeline = useCallback(async (include = false) => {
@ -117,13 +121,12 @@ const DatasetCard = ({
try { try {
const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id) const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
setConfirmMessage(isUsedByApp ? t('dataset.datasetUsedByApp')! : t('dataset.deleteDatasetConfirmContent')!) setConfirmMessage(isUsedByApp ? t('dataset.datasetUsedByApp')! : t('dataset.deleteDatasetConfirmContent')!)
setShowConfirmDelete(true)
} }
catch (e: any) { catch (e: any) {
const res = await e.json() const res = await e.json()
Toast.notify({ type: 'error', message: res?.message || 'Unknown error' }) Toast.notify({ type: 'error', message: res?.message || 'Unknown error' })
} }
setShowConfirmDelete(true)
}, [dataset.id, t]) }, [dataset.id, t])
const onConfirmDelete = useCallback(async () => { const onConfirmDelete = useCallback(async () => {
@ -133,9 +136,9 @@ const DatasetCard = ({
if (onSuccess) if (onSuccess)
onSuccess() onSuccess()
} }
catch { finally {
setShowConfirmDelete(false)
} }
setShowConfirmDelete(false)
}, [dataset.id, onSuccess, t]) }, [dataset.id, onSuccess, t])
useEffect(() => { useEffect(() => {
@ -262,11 +265,9 @@ const DatasetCard = ({
htmlContent={ htmlContent={
<Operations <Operations
showDelete={!isCurrentWorkspaceDatasetOperator} showDelete={!isCurrentWorkspaceDatasetOperator}
openRenameModal={() => { openRenameModal={openRenameModal}
setShowRenameModal(true)
}}
detectIsUsedByApp={detectIsUsedByApp}
handleExportPipeline={handleExportPipeline} handleExportPipeline={handleExportPipeline}
detectIsUsedByApp={detectIsUsedByApp}
/> />
} }
className={'z-20 min-w-[186px]'} className={'z-20 min-w-[186px]'}

@ -45,14 +45,18 @@ const DatasetNav = () => {
id: currentDataset.id, id: currentDataset.id,
name: currentDataset.name, name: currentDataset.name,
icon: currentDataset.icon_info.icon, icon: currentDataset.icon_info.icon,
icon_type: currentDataset.icon_info.icon_type,
icon_background: currentDataset.icon_info.icon_background, icon_background: currentDataset.icon_info.icon_background,
icon_url: currentDataset.icon_info.icon_url,
} as Omit<NavItem, 'link'>} } as Omit<NavItem, 'link'>}
navigationItems={datasetItems.map(dataset => ({ navigationItems={datasetItems.map(dataset => ({
id: dataset.id, id: dataset.id,
name: dataset.name, name: dataset.name,
link: dataset.provider === 'external' ? `/datasets/${dataset.id}/hitTesting` : `/datasets/${dataset.id}/documents`, link: dataset.provider === 'external' ? `/datasets/${dataset.id}/hitTesting` : `/datasets/${dataset.id}/documents`,
icon: dataset.icon_info.icon, icon: dataset.icon_info.icon,
icon_type: dataset.icon_info.icon_type,
icon_background: dataset.icon_info.icon_background, icon_background: dataset.icon_info.icon_background,
icon_url: dataset.icon_info.icon_url,
})) as NavItem[]} })) as NavItem[]}
createText={t('common.menus.newDataset')} createText={t('common.menus.newDataset')}
onCreate={() => router.push(`${basePath}/datasets/create`)} onCreate={() => router.push(`${basePath}/datasets/create`)}

@ -42,6 +42,7 @@ const Nav = ({
useEffect(() => { useEffect(() => {
if (pathname === link) if (pathname === link)
setLinkLastSearchParams(searchParams.toString()) setLinkLastSearchParams(searchParams.toString())
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname, searchParams]) }, [pathname, searchParams])
return ( return (
@ -53,11 +54,11 @@ const Nav = ({
<Link href={link + (linkLastSearchParams && `?${linkLastSearchParams}`)}> <Link href={link + (linkLastSearchParams && `?${linkLastSearchParams}`)}>
<div <div
onClick={() => setAppDetail()} onClick={() => setAppDetail()}
className={classNames(` className={classNames(
flex items-center h-7 px-2.5 cursor-pointer rounded-[10px] 'flex h-7 cursor-pointer items-center rounded-[10px] px-2.5',
${isActivated ? 'text-components-main-nav-nav-button-text-active' : 'text-components-main-nav-nav-button-text'} isActivated ? 'text-components-main-nav-nav-button-text-active' : 'text-components-main-nav-nav-button-text',
${curNav && isActivated && 'hover:bg-components-main-nav-nav-button-bg-active-hover'} curNav && isActivated && 'hover:bg-components-main-nav-nav-button-bg-active-hover',
`)} )}
onMouseEnter={() => setHovered(true)} onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)} onMouseLeave={() => setHovered(false)}
> >

@ -84,7 +84,13 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL
router.push(nav.link) router.push(nav.link)
}} title={nav.name}> }} title={nav.name}>
<div className='relative mr-2 h-6 w-6 rounded-md'> <div className='relative mr-2 h-6 w-6 rounded-md'>
<AppIcon size='tiny' iconType={nav.icon_type} icon={nav.icon} background={nav.icon_background} imageUrl={nav.icon_url} /> <AppIcon
size='tiny'
iconType={nav.icon_type}
icon={nav.icon}
background={nav.icon_background}
imageUrl={nav.icon_url}
/>
{!!nav.mode && ( {!!nav.mode && (
<span className={cn( <span className={cn(
'absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded border-[0.5px] border-[rgba(0,0,0,0.02)] bg-white p-0.5 shadow-sm', 'absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded border-[0.5px] border-[rgba(0,0,0,0.02)] bg-white p-0.5 shadow-sm',

@ -40,9 +40,11 @@ export const useResetDatasetList = () => {
return useReset([...DatasetListKey]) return useReset([...DatasetListKey])
} }
export const datasetDetailQueryKeyPrefix = [NAME_SPACE, 'detail']
export const useDatasetDetail = (datasetId: string) => { export const useDatasetDetail = (datasetId: string) => {
return useQuery({ return useQuery({
queryKey: [NAME_SPACE, 'detail', datasetId], queryKey: [...datasetDetailQueryKeyPrefix, datasetId],
queryFn: () => get<DataSet>(`/datasets/${datasetId}`), queryFn: () => get<DataSet>(`/datasets/${datasetId}`),
enabled: !!datasetId, enabled: !!datasetId,
}) })

Loading…
Cancel
Save