mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-06-03 18:54:11 +08:00
fix: add dataset edit permissions (#13223)
This commit is contained in:
parent
186e2d972e
commit
49b4144ffd
@ -23,12 +23,14 @@ type ItemProps = {
|
||||
onRemove: (id: string) => void
|
||||
readonly?: boolean
|
||||
onSave: (newDataset: DataSet) => void
|
||||
editable?: boolean
|
||||
}
|
||||
|
||||
const Item: FC<ItemProps> = ({
|
||||
config,
|
||||
onSave,
|
||||
onRemove,
|
||||
editable = true,
|
||||
}) => {
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
@ -68,19 +70,21 @@ const Item: FC<ItemProps> = ({
|
||||
<div className='flex items-center h-[18px]'>
|
||||
<div className='grow text-[13px] font-medium text-gray-800 truncate' title={config.name}>{config.name}</div>
|
||||
{config.provider === 'external'
|
||||
? <Badge text={t('dataset.externalTag')}></Badge>
|
||||
? <Badge text={t('dataset.externalTag') as string} />
|
||||
: <Badge
|
||||
text={formatIndexingTechniqueAndMethod(config.indexing_technique, config.retrieval_model_dict?.search_method)}
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
<div className='hidden rounded-lg group-hover:flex items-center justify-end absolute right-0 top-0 bottom-0 pr-2 w-[124px] bg-gradient-to-r from-white/50 to-white to-50%'>
|
||||
<div
|
||||
className='flex items-center justify-center mr-1 w-6 h-6 hover:bg-black/5 rounded-md cursor-pointer'
|
||||
onClick={() => setShowSettingsModal(true)}
|
||||
>
|
||||
<RiEditLine className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
{
|
||||
editable && <div
|
||||
className='flex items-center justify-center mr-1 w-6 h-6 hover:bg-black/5 rounded-md cursor-pointer'
|
||||
onClick={() => setShowSettingsModal(true)}
|
||||
>
|
||||
<RiEditLine className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
className='group/action flex items-center justify-center w-6 h-6 hover:bg-[#FEE4E2] rounded-md cursor-pointer'
|
||||
onClick={() => onRemove(config.id)}
|
||||
|
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
@ -19,6 +19,8 @@ import {
|
||||
} from '@/app/components/workflow/nodes/knowledge-retrieval/utils'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useSelector as useAppContextSelector } from '@/context/app-context'
|
||||
import { hasEditPermissionForDataset } from '@/utils/permission'
|
||||
|
||||
const Icon = (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@ -29,6 +31,7 @@ const Icon = (
|
||||
|
||||
const DatasetConfig: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const userProfile = useAppContextSelector(s => s.userProfile)
|
||||
const {
|
||||
mode,
|
||||
dataSets: dataSet,
|
||||
@ -105,6 +108,20 @@ const DatasetConfig: FC = () => {
|
||||
setModelConfig(newModelConfig)
|
||||
}
|
||||
|
||||
const formattedDataset = useMemo(() => {
|
||||
return dataSet.map((item) => {
|
||||
const datasetConfig = {
|
||||
createdBy: item.created_by,
|
||||
partialMemberList: item.partial_member_list || [],
|
||||
permission: item.permission,
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
editable: hasEditPermissionForDataset(userProfile?.id || '', datasetConfig),
|
||||
}
|
||||
})
|
||||
}, [dataSet, userProfile?.id])
|
||||
|
||||
return (
|
||||
<FeaturePanel
|
||||
className='mt-2'
|
||||
@ -122,12 +139,13 @@ const DatasetConfig: FC = () => {
|
||||
{hasData
|
||||
? (
|
||||
<div className='flex flex-wrap mt-1 px-3 pb-3 justify-between'>
|
||||
{dataSet.map(item => (
|
||||
{formattedDataset.map(item => (
|
||||
<CardItem
|
||||
key={item.id}
|
||||
config={item}
|
||||
onRemove={onRemove}
|
||||
onSave={handleSave}
|
||||
editable={item.editable}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -12,7 +12,7 @@ import Divider from '@/app/components/base/divider'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { type DataSet } from '@/models/datasets'
|
||||
import { type DataSet, DatasetPermission } from '@/models/datasets'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { updateDatasetSetting } from '@/service/datasets'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
@ -134,7 +134,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
}),
|
||||
},
|
||||
} as any
|
||||
if (permission === 'partial_members') {
|
||||
if (permission === DatasetPermission.partialMembers) {
|
||||
requestParams.body.partial_member_list = selectedMemberIDs.map((id) => {
|
||||
return {
|
||||
user_id: id,
|
||||
|
@ -17,7 +17,7 @@ import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { updateDatasetSetting } from '@/service/datasets'
|
||||
import { type DataSetListResponse } from '@/models/datasets'
|
||||
import { type DataSetListResponse, DatasetPermission } from '@/models/datasets'
|
||||
import DatasetDetailContext from '@/context/dataset-detail'
|
||||
import { type RetrievalConfig } from '@/types/app'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
@ -145,7 +145,7 @@ const Form = () => {
|
||||
}),
|
||||
},
|
||||
} as any
|
||||
if (permission === 'partial_members') {
|
||||
if (permission === DatasetPermission.partialMembers) {
|
||||
requestParams.body.partial_member_list = selectedMemberIDs.map((id) => {
|
||||
return {
|
||||
user_id: id,
|
||||
|
@ -12,7 +12,7 @@ import Avatar from '@/app/components/base/avatar'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { Check } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Users01, UsersPlus } from '@/app/components/base/icons/src/vender/solid/users'
|
||||
import type { DatasetPermission } from '@/models/datasets'
|
||||
import { DatasetPermission } from '@/models/datasets'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import type { Member } from '@/models/common'
|
||||
export type RoleSelectorProps = {
|
||||
@ -60,6 +60,10 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
|
||||
return memberList.filter(member => (member.name.includes(searchKeywords) || member.email.includes(searchKeywords)) && member.id !== userProfile.id && ['owner', 'admin', 'editor', 'dataset_operator'].includes(member.role))
|
||||
}, [memberList, searchKeywords, userProfile])
|
||||
|
||||
const isOnlyMe = permission === DatasetPermission.onlyMe
|
||||
const isAllTeamMembers = permission === DatasetPermission.allTeamMembers
|
||||
const isPartialMembers = permission === DatasetPermission.partialMembers
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
@ -72,14 +76,14 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
|
||||
onClick={() => !disabled && setOpen(v => !v)}
|
||||
className='block'
|
||||
>
|
||||
{permission === 'only_me' && (
|
||||
{isOnlyMe && (
|
||||
<div className={cn('flex items-center px-3 py-[6px] rounded-lg bg-gray-100 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200', disabled && 'hover:!bg-gray-100 !cursor-default')}>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='shrink-0 mr-2' size={24} />
|
||||
<div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('datasetSettings.form.permissionsOnlyMe')}</div>
|
||||
{!disabled && <RiArrowDownSLine className='shrink-0 w-4 h-4 text-gray-700' />}
|
||||
</div>
|
||||
)}
|
||||
{permission === 'all_team_members' && (
|
||||
{isAllTeamMembers && (
|
||||
<div className={cn('flex items-center px-3 py-[6px] rounded-lg bg-gray-100 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
|
||||
<div className='mr-2 flex items-center justify-center w-6 h-6 rounded-lg bg-[#EEF4FF]'>
|
||||
<Users01 className='w-3.5 h-3.5 text-[#444CE7]' />
|
||||
@ -88,7 +92,7 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
|
||||
{!disabled && <RiArrowDownSLine className='shrink-0 w-4 h-4 text-gray-700' />}
|
||||
</div>
|
||||
)}
|
||||
{permission === 'partial_members' && (
|
||||
{isPartialMembers && (
|
||||
<div className={cn('flex items-center px-3 py-[6px] rounded-lg bg-gray-100 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
|
||||
<div className='mr-2 flex items-center justify-center w-6 h-6 rounded-lg bg-[#EEF4FF]'>
|
||||
<Users01 className='w-3.5 h-3.5 text-[#444CE7]' />
|
||||
@ -102,17 +106,17 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
|
||||
<div className='relative w-[480px] rounded-lg border-[0.5px] bg-white shadow-lg'>
|
||||
<div className='p-1'>
|
||||
<div className='pl-3 pr-2 py-1 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
|
||||
onChange('only_me')
|
||||
onChange(DatasetPermission.onlyMe)
|
||||
setOpen(false)
|
||||
}}>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='shrink-0 mr-2' size={24} />
|
||||
<div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('datasetSettings.form.permissionsOnlyMe')}</div>
|
||||
{permission === 'only_me' && <Check className='w-4 h-4 text-primary-600' />}
|
||||
{isOnlyMe && <Check className='w-4 h-4 text-primary-600' />}
|
||||
</div>
|
||||
</div>
|
||||
<div className='pl-3 pr-2 py-1 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
|
||||
onChange('all_team_members')
|
||||
onChange(DatasetPermission.allTeamMembers)
|
||||
setOpen(false)
|
||||
}}>
|
||||
<div className='flex items-center gap-2'>
|
||||
@ -120,23 +124,23 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
|
||||
<Users01 className='w-3.5 h-3.5 text-[#444CE7]' />
|
||||
</div>
|
||||
<div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('datasetSettings.form.permissionsAllMember')}</div>
|
||||
{permission === 'all_team_members' && <Check className='w-4 h-4 text-primary-600' />}
|
||||
{isAllTeamMembers && <Check className='w-4 h-4 text-primary-600' />}
|
||||
</div>
|
||||
</div>
|
||||
<div className='pl-3 pr-2 py-1 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
|
||||
onChange('partial_members')
|
||||
onChange(DatasetPermission.partialMembers)
|
||||
onMemberSelect([userProfile.id])
|
||||
}}>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className={cn('mr-2 flex items-center justify-center w-6 h-6 rounded-lg bg-[#FFF6ED]', permission === 'partial_members' && '!bg-[#EEF4FF]')}>
|
||||
<UsersPlus className={cn('w-3.5 h-3.5 text-[#FB6514]', permission === 'partial_members' && '!text-[#444CE7]')} />
|
||||
<div className={cn('mr-2 flex items-center justify-center w-6 h-6 rounded-lg bg-[#FFF6ED]', isPartialMembers && '!bg-[#EEF4FF]')}>
|
||||
<UsersPlus className={cn('w-3.5 h-3.5 text-[#FB6514]', isPartialMembers && '!text-[#444CE7]')} />
|
||||
</div>
|
||||
<div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('datasetSettings.form.permissionsInvitedMembers')}</div>
|
||||
{permission === 'partial_members' && <Check className='w-4 h-4 text-primary-600' />}
|
||||
{isPartialMembers && <Check className='w-4 h-4 text-primary-600' />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{permission === 'partial_members' && (
|
||||
{isPartialMembers && (
|
||||
<div className='max-h-[360px] border-t-[1px] border-gray-100 p-1 overflow-y-auto'>
|
||||
<div className='sticky left-0 top-0 p-2 pb-1 bg-white'>
|
||||
<Input
|
||||
|
@ -1,66 +0,0 @@
|
||||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import s from './index.module.css'
|
||||
import classNames from '@/utils/classnames'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
|
||||
const itemClass = `
|
||||
flex items-center w-full sm:w-[234px] h-12 px-3 rounded-xl bg-gray-25 border border-gray-100 cursor-pointer
|
||||
`
|
||||
const radioClass = `
|
||||
w-4 h-4 border-[2px] border-gray-200 rounded-full
|
||||
`
|
||||
type IPermissionsRadioProps = {
|
||||
value?: DataSet['permission']
|
||||
onChange: (v?: DataSet['permission']) => void
|
||||
itemClassName?: string
|
||||
disable?: boolean
|
||||
}
|
||||
|
||||
const PermissionsRadio = ({
|
||||
value,
|
||||
onChange,
|
||||
itemClassName,
|
||||
disable,
|
||||
}: IPermissionsRadioProps) => {
|
||||
const { t } = useTranslation()
|
||||
const options = [
|
||||
{
|
||||
key: 'only_me',
|
||||
text: t('datasetSettings.form.permissionsOnlyMe'),
|
||||
},
|
||||
{
|
||||
key: 'all_team_members',
|
||||
text: t('datasetSettings.form.permissionsAllMember'),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className={classNames(s.wrapper, 'flex justify-between w-full flex-wrap gap-y-2')}>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option.key}
|
||||
className={classNames(
|
||||
itemClass,
|
||||
itemClassName,
|
||||
s.item,
|
||||
option.key === value && s['item-active'],
|
||||
disable && s.disable,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!disable)
|
||||
onChange(option.key as DataSet['permission'])
|
||||
}}
|
||||
>
|
||||
<div className={classNames(s['user-icon'], 'mr-3')} />
|
||||
<div className='grow text-sm text-gray-900'>{option.text}</div>
|
||||
<div className={classNames(radioClass, s.radio)} />
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PermissionsRadio
|
@ -23,6 +23,7 @@ type Props = {
|
||||
onRemove: () => void
|
||||
onChange: (dataSet: DataSet) => void
|
||||
readonly?: boolean
|
||||
editable?: boolean
|
||||
}
|
||||
|
||||
const DatasetItem: FC<Props> = ({
|
||||
@ -30,6 +31,7 @@ const DatasetItem: FC<Props> = ({
|
||||
onRemove,
|
||||
onChange,
|
||||
readonly,
|
||||
editable = true,
|
||||
}) => {
|
||||
const media = useBreakpoints()
|
||||
const { t } = useTranslation()
|
||||
@ -75,14 +77,16 @@ const DatasetItem: FC<Props> = ({
|
||||
</div>
|
||||
{!readonly && (
|
||||
<div className='hidden group-hover/dataset-item:flex shrink-0 ml-2 items-center space-x-1'>
|
||||
<ActionButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
showSettingsModal()
|
||||
}}
|
||||
>
|
||||
<RiEditLine className='w-4 h-4 flex-shrink-0 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
{
|
||||
editable && <ActionButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
showSettingsModal()
|
||||
}}
|
||||
>
|
||||
<RiEditLine className='w-4 h-4 flex-shrink-0 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
}
|
||||
<ActionButton
|
||||
onClick={handleRemove}
|
||||
state={ActionButtonState.Destructive}
|
||||
@ -102,7 +106,7 @@ const DatasetItem: FC<Props> = ({
|
||||
{
|
||||
payload.provider === 'external' && <Badge
|
||||
className='group-hover/dataset-item:hidden shrink-0'
|
||||
text={t('dataset.externalTag')}
|
||||
text={t('dataset.externalTag') as string}
|
||||
/>
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,13 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Item from './dataset-item'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { useSelector as useAppContextSelector } from '@/context/app-context'
|
||||
import { hasEditPermissionForDataset } from '@/utils/permission'
|
||||
|
||||
type Props = {
|
||||
list: DataSet[]
|
||||
onChange: (list: DataSet[]) => void
|
||||
@ -17,6 +20,7 @@ const DatasetList: FC<Props> = ({
|
||||
readonly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const userProfile = useAppContextSelector(s => s.userProfile)
|
||||
|
||||
const handleRemove = useCallback((index: number) => {
|
||||
return () => {
|
||||
@ -35,10 +39,25 @@ const DatasetList: FC<Props> = ({
|
||||
onChange(newList)
|
||||
}
|
||||
}, [list, onChange])
|
||||
|
||||
const formattedList = useMemo(() => {
|
||||
return list.map((item) => {
|
||||
const datasetConfig = {
|
||||
createdBy: item.created_by,
|
||||
partialMemberList: item.partial_member_list || [],
|
||||
permission: item.permission,
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
editable: hasEditPermissionForDataset(userProfile?.id || '', datasetConfig),
|
||||
}
|
||||
})
|
||||
}, [list, userProfile?.id])
|
||||
|
||||
return (
|
||||
<div className='space-y-1'>
|
||||
{list.length
|
||||
? list.map((item, index) => {
|
||||
{formattedList.length
|
||||
? formattedList.map((item, index) => {
|
||||
return (
|
||||
<Item
|
||||
key={index}
|
||||
@ -46,6 +65,7 @@ const DatasetList: FC<Props> = ({
|
||||
onRemove={handleRemove(index)}
|
||||
onChange={handleChange(index)}
|
||||
readonly={readonly}
|
||||
editable={item.editable}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
@ -9,7 +9,11 @@ export enum DataSourceType {
|
||||
WEB = 'website_crawl',
|
||||
}
|
||||
|
||||
export type DatasetPermission = 'only_me' | 'all_team_members' | 'partial_members'
|
||||
export enum DatasetPermission {
|
||||
'onlyMe' = 'only_me',
|
||||
'allTeamMembers' = 'all_team_members',
|
||||
'partialMembers' = 'partial_members',
|
||||
}
|
||||
|
||||
export enum ChunkingMode {
|
||||
'text' = 'text_model', // General text
|
||||
@ -40,7 +44,7 @@ export type DataSet = {
|
||||
retrieval_model_dict: RetrievalConfig
|
||||
retrieval_model: RetrievalConfig
|
||||
tags: Tag[]
|
||||
partial_member_list?: any[]
|
||||
partial_member_list?: string[]
|
||||
external_knowledge_info: {
|
||||
external_knowledge_id: string
|
||||
external_knowledge_api_id: string
|
||||
|
18
web/utils/permission.ts
Normal file
18
web/utils/permission.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { DatasetPermission } from '@/models/datasets'
|
||||
|
||||
type DatasetConfig = {
|
||||
createdBy: string
|
||||
partialMemberList: string[]
|
||||
permission: DatasetPermission
|
||||
}
|
||||
|
||||
export const hasEditPermissionForDataset = (userId: string, datasetConfig: DatasetConfig) => {
|
||||
const { createdBy, partialMemberList, permission } = datasetConfig
|
||||
if (permission === DatasetPermission.onlyMe)
|
||||
return userId === createdBy
|
||||
if (permission === DatasetPermission.allTeamMembers)
|
||||
return true
|
||||
if (permission === DatasetPermission.partialMembers)
|
||||
return partialMemberList.includes(userId)
|
||||
return false
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user