mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 04:26:30 +08:00
feat: dataset metadata
This commit is contained in:
parent
5e2bd407a8
commit
0ed892a747
@ -22,6 +22,8 @@ import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/d
|
|||||||
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
|
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
|
||||||
import CreateModal from '@/app/components/datasets/metadata/create-metadata-modal'
|
import CreateModal from '@/app/components/datasets/metadata/create-metadata-modal'
|
||||||
import SelectMetadataModal from '@/app/components/datasets/metadata/select-metadata-modal'
|
import SelectMetadataModal from '@/app/components/datasets/metadata/select-metadata-modal'
|
||||||
|
import DatasetMetadataDrawer from '@/app/components/datasets/metadata/dataset-metadata-drawer'
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
import { fetchDatasetApiBaseUrl } from '@/service/datasets'
|
import { fetchDatasetApiBaseUrl } from '@/service/datasets'
|
||||||
|
|
||||||
@ -30,6 +32,7 @@ import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
|||||||
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useExternalApiPanel } from '@/context/external-api-panel-context'
|
import { useExternalApiPanel } from '@/context/external-api-panel-context'
|
||||||
|
import { DataType } from '@/app/components/datasets/metadata/types'
|
||||||
|
|
||||||
const Container = () => {
|
const Container = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -82,11 +85,32 @@ const Container = () => {
|
|||||||
return router.replace('/apps')
|
return router.replace('/apps')
|
||||||
}, [currentWorkspace, router])
|
}, [currentWorkspace, router])
|
||||||
|
|
||||||
|
const [userMetadata, setUserMetadata] = useState([
|
||||||
|
{ id: '1', name: 'name1', type: DataType.string, valueLength: 1 },
|
||||||
|
{ id: '2', name: 'name2', type: DataType.number, valueLength: 2 },
|
||||||
|
{ id: '3', name: 'name3', type: DataType.time, valueLength: 3 },
|
||||||
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className='grow relative flex flex-col bg-background-body overflow-y-auto scroll-container'>
|
<div ref={containerRef} className='grow relative flex flex-col bg-background-body overflow-y-auto scroll-container'>
|
||||||
<div className='ml-[600px] mt-[300px]'>
|
<div className='ml-[600px] mt-[300px]'>
|
||||||
<SelectMetadataModal trigger={<Button className='w-[200px]'>select</Button>} onSave={(data) => { console.log(data) }} />
|
<SelectMetadataModal trigger={<Button className='w-[200px]'>select</Button>} onSave={(data) => { console.log(data) }} />
|
||||||
<CreateModal trigger={<Button className='w-[200px]'>add</Button>} hasBack onSave={(data) => { console.log(data) }} />
|
<CreateModal trigger={<Button className='w-[200px]'>add</Button>} hasBack onSave={(data) => { console.log(data) }} />
|
||||||
|
<Button className='flex w-[200px]' size="medium" onClick={() => setShowExternalApiPanel(true)}>
|
||||||
|
Metadata
|
||||||
|
</Button>
|
||||||
|
<DatasetMetadataDrawer
|
||||||
|
userMetadata={userMetadata}
|
||||||
|
onChange={setUserMetadata}
|
||||||
|
builtInMetadata={[
|
||||||
|
{ id: '1', name: 'name1', type: DataType.string, valueLength: 1 },
|
||||||
|
{ id: '2', name: 'name2', type: DataType.number, valueLength: 2 },
|
||||||
|
{ id: '3', name: 'name3', type: DataType.time, valueLength: 3 },
|
||||||
|
]}
|
||||||
|
isBuiltInEnabled={false}
|
||||||
|
onIsBuiltInEnabledChange={() => { }}
|
||||||
|
onClose={() => { }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-background-body z-10 flex-wrap gap-y-2'>
|
<div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-background-body z-10 flex-wrap gap-y-2'>
|
||||||
<TabSliderNew
|
<TabSliderNew
|
||||||
|
|||||||
@ -53,15 +53,18 @@ export default function Drawer({
|
|||||||
/>
|
/>
|
||||||
<div className={cn('relative z-50 flex flex-col justify-between bg-components-panel-bg w-full max-w-sm p-6 overflow-hidden text-left align-middle shadow-xl', panelClassname)}>
|
<div className={cn('relative z-50 flex flex-col justify-between bg-components-panel-bg w-full max-w-sm p-6 overflow-hidden text-left align-middle shadow-xl', panelClassname)}>
|
||||||
<>
|
<>
|
||||||
{title && <Dialog.Title
|
<div className='flex justify-between'>
|
||||||
as="h3"
|
{title && <Dialog.Title
|
||||||
className="text-lg font-medium leading-6 text-text-primary"
|
as="h3"
|
||||||
>
|
className="text-lg font-medium leading-6 text-text-primary"
|
||||||
{title}
|
>
|
||||||
</Dialog.Title>}
|
{title}
|
||||||
{showClose && <Dialog.Title className="flex items-center mb-4" as="div">
|
</Dialog.Title>}
|
||||||
<XMarkIcon className='w-4 h-4 text-text-tertiary' onClick={onClose} />
|
{showClose && <Dialog.Title className="flex items-center mb-4" as="div">
|
||||||
</Dialog.Title>}
|
<XMarkIcon className='w-4 h-4 text-text-tertiary' onClick={onClose} />
|
||||||
|
</Dialog.Title>}
|
||||||
|
</div>
|
||||||
|
|
||||||
{description && <Dialog.Description className='text-text-tertiary text-xs font-normal mt-2'>{description}</Dialog.Description>}
|
{description && <Dialog.Description className='text-text-tertiary text-xs font-normal mt-2'>{description}</Dialog.Description>}
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
|
|||||||
187
web/app/components/datasets/metadata/dataset-metadata-drawer.tsx
Normal file
187
web/app/components/datasets/metadata/dataset-metadata-drawer.tsx
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useCallback, useState } from 'react'
|
||||||
|
import type { MetadataItemWithValueLength } from './types'
|
||||||
|
import Drawer from '../../base/drawer'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import { RiAddLine, RiDeleteBinLine, RiEditLine } from '@remixicon/react'
|
||||||
|
import { getIcon } from './utils/get-icon'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import Modal from '../../base/modal'
|
||||||
|
import Field from './field'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import produce from 'immer'
|
||||||
|
import Switch from '../../base/switch'
|
||||||
|
import Tooltip from '../../base/tooltip'
|
||||||
|
import CreateModal from '@/app/components/datasets/metadata/create-metadata-modal'
|
||||||
|
|
||||||
|
const i18nPrefix = 'dataset.metadata.datasetMetadata'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
userMetadata: MetadataItemWithValueLength[]
|
||||||
|
builtInMetadata: MetadataItemWithValueLength[]
|
||||||
|
isBuiltInEnabled: boolean
|
||||||
|
onIsBuiltInEnabledChange: (value: boolean) => void
|
||||||
|
onClose: () => void
|
||||||
|
onChange: (data: MetadataItemWithValueLength[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemProps = {
|
||||||
|
readonly?: boolean
|
||||||
|
payload: MetadataItemWithValueLength
|
||||||
|
onRename?: () => void
|
||||||
|
onDelete?: () => void
|
||||||
|
}
|
||||||
|
const Item: FC<ItemProps> = ({
|
||||||
|
readonly,
|
||||||
|
payload,
|
||||||
|
onRename,
|
||||||
|
onDelete,
|
||||||
|
}) => {
|
||||||
|
const Icon = getIcon(payload.type)
|
||||||
|
|
||||||
|
const handleRename = useCallback(() => {
|
||||||
|
onRename?.()
|
||||||
|
}, [onRename])
|
||||||
|
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
onDelete?.()
|
||||||
|
}, [onDelete])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={payload.id}
|
||||||
|
className={cn(!readonly && 'group/item', 'mx-1 flex items-center h-6 px-3 justify-between rounded-md hover:bg-state-base-hover cursor-pointer')}
|
||||||
|
>
|
||||||
|
<div className='w-0 grow flex items-center h-full text-text-secondary'>
|
||||||
|
<Icon className='shrink-0 mr-[5px] size-3.5' />
|
||||||
|
<div className='w-0 grow truncate system-sm-medium'>{payload.name}</div>
|
||||||
|
</div>
|
||||||
|
<div className='group-hover/item:hidden ml-1 shrink-0 system-xs-regular text-text-tertiary'>
|
||||||
|
{payload.valueLength || 0} values
|
||||||
|
</div>
|
||||||
|
<div className='group-hover/item:flex hidden items-center h-6 px-3 text-text-secondary rounded-md hover:bg-state-base-hover cursor-pointer space-x-1'>
|
||||||
|
<RiEditLine className='size-4 cursor-pointer' onClick={handleRename} />
|
||||||
|
<RiDeleteBinLine className='size-4 cursor-pointer' onClick={handleDelete} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DatasetMetadataDrawer: FC<Props> = ({
|
||||||
|
userMetadata,
|
||||||
|
builtInMetadata,
|
||||||
|
isBuiltInEnabled,
|
||||||
|
onIsBuiltInEnabledChange,
|
||||||
|
onClose,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [isShowRenameModal, setIsShowRenameModal] = useState(false)
|
||||||
|
const [currPayload, setCurrPayload] = useState<MetadataItemWithValueLength | null>(null)
|
||||||
|
const [templeName, setTempleName] = useState('')
|
||||||
|
const handleRename = useCallback((payload: MetadataItemWithValueLength) => {
|
||||||
|
return () => {
|
||||||
|
setCurrPayload(payload)
|
||||||
|
setTempleName(payload.name)
|
||||||
|
setIsShowRenameModal(true)
|
||||||
|
}
|
||||||
|
}, [setCurrPayload, setIsShowRenameModal])
|
||||||
|
|
||||||
|
const handleAdd = useCallback((data: MetadataItemWithValueLength) => {
|
||||||
|
const nextUserMetadata = produce(userMetadata, (draft) => {
|
||||||
|
draft.push(data)
|
||||||
|
})
|
||||||
|
onChange(nextUserMetadata)
|
||||||
|
}, [userMetadata, onChange])
|
||||||
|
|
||||||
|
const handleRenamed = useCallback(() => {
|
||||||
|
const nextUserMetadata = produce(userMetadata, (draft) => {
|
||||||
|
const index = draft.findIndex(p => p.id === currPayload?.id)
|
||||||
|
if (index !== -1)
|
||||||
|
draft[index].name = templeName!
|
||||||
|
})
|
||||||
|
|
||||||
|
onChange(nextUserMetadata)
|
||||||
|
setIsShowRenameModal(false)
|
||||||
|
}, [currPayload, templeName, userMetadata, onChange])
|
||||||
|
|
||||||
|
const handleDelete = useCallback((payload: MetadataItemWithValueLength) => {
|
||||||
|
return () => {
|
||||||
|
const nextUserMetadata = userMetadata.filter(p => p.id !== payload.id)
|
||||||
|
onChange(nextUserMetadata)
|
||||||
|
}
|
||||||
|
}, [userMetadata, onChange])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
isOpen={true}
|
||||||
|
onClose={onClose}
|
||||||
|
showClose
|
||||||
|
title='Metadata'
|
||||||
|
footer={null}
|
||||||
|
panelClassname='block !max-w-[420px] my-2 rounded-l-2xl'
|
||||||
|
>
|
||||||
|
<div className='system-sm-regular text-text-tertiary'>You can manage all metadata in this knowledge here. Modifications will be synchronized to every document.</div>
|
||||||
|
|
||||||
|
<CreateModal trigger={<Button variant='primary' className='mt-3'>
|
||||||
|
<RiAddLine className='mr-1' />
|
||||||
|
Add Metadata
|
||||||
|
</Button>} hasBack onSave={handleAdd} />
|
||||||
|
|
||||||
|
{userMetadata.map(payload => (
|
||||||
|
<Item
|
||||||
|
key={payload.id}
|
||||||
|
payload={payload}
|
||||||
|
onRename={handleRename(payload)}
|
||||||
|
onDelete={handleDelete(payload)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className='flex'>
|
||||||
|
<Switch
|
||||||
|
defaultValue={isBuiltInEnabled}
|
||||||
|
onChange={onIsBuiltInEnabledChange}
|
||||||
|
/>
|
||||||
|
<div>Built-in</div>
|
||||||
|
<Tooltip popupContent="xxx" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{builtInMetadata.map(payload => (
|
||||||
|
<Item
|
||||||
|
key={payload.id}
|
||||||
|
readonly
|
||||||
|
payload={payload}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isShowRenameModal && (
|
||||||
|
<Modal isShow title="rename">
|
||||||
|
<Field label={t(`${i18nPrefix}.name`)}>
|
||||||
|
<Input
|
||||||
|
value={templeName}
|
||||||
|
onChange={e => setTempleName(e.target.value)}
|
||||||
|
placeholder={t(`${i18nPrefix}.namePlaceholder`)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<div className='mt-4 flex justify-end'>
|
||||||
|
<Button
|
||||||
|
className='mr-2'
|
||||||
|
onClick={() => {
|
||||||
|
setIsShowRenameModal(false)
|
||||||
|
setTempleName(currPayload!.name)
|
||||||
|
}}>{t('common.operation.cancel')}</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleRenamed}
|
||||||
|
variant='primary'
|
||||||
|
disabled={!templeName}
|
||||||
|
>{t('common.operation.save')}</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(DatasetMetadataDrawer)
|
||||||
@ -1,11 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { DataType } from './types'
|
|
||||||
import type { MetadataItem } from './types'
|
import type { MetadataItem } from './types'
|
||||||
import SearchInput from '../../base/search-input'
|
import SearchInput from '../../base/search-input'
|
||||||
import { RiAddLine, RiArrowRightUpLine, RiHashtag, RiTextSnippet, RiTimeLine } from '@remixicon/react'
|
import { RiAddLine, RiArrowRightUpLine } from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { getIcon } from './utils/get-icon'
|
||||||
|
|
||||||
const i18nPrefix = 'dataset.metadata.selectMetadata'
|
const i18nPrefix = 'dataset.metadata.selectMetadata'
|
||||||
|
|
||||||
@ -16,14 +16,6 @@ type Props = {
|
|||||||
onManage: () => void
|
onManage: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const getIcon = (type: DataType) => {
|
|
||||||
return ({
|
|
||||||
[DataType.string]: RiTextSnippet,
|
|
||||||
[DataType.number]: RiHashtag,
|
|
||||||
[DataType.time]: RiTimeLine,
|
|
||||||
}[type] || RiTextSnippet)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SelectMetadata: FC<Props> = ({
|
const SelectMetadata: FC<Props> = ({
|
||||||
list,
|
list,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
|||||||
@ -9,3 +9,7 @@ export type MetadataItem = {
|
|||||||
type: DataType
|
type: DataType
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MetadataItemWithValueLength = MetadataItem & {
|
||||||
|
valueLength: number
|
||||||
|
}
|
||||||
|
|||||||
10
web/app/components/datasets/metadata/utils/get-icon.ts
Normal file
10
web/app/components/datasets/metadata/utils/get-icon.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { DataType } from '../types'
|
||||||
|
import { RiHashtag, RiTextSnippet, RiTimeLine } from '@remixicon/react'
|
||||||
|
|
||||||
|
export const getIcon = (type: DataType) => {
|
||||||
|
return ({
|
||||||
|
[DataType.string]: RiTextSnippet,
|
||||||
|
[DataType.number]: RiHashtag,
|
||||||
|
[DataType.time]: RiTimeLine,
|
||||||
|
}[type] || RiTextSnippet)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user