fix: add dataset edit permissions (#13223)

This commit is contained in:
Wu Tianwei 2025-02-06 14:26:16 +08:00 committed by GitHub
parent 186e2d972e
commit 49b4144ffd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 112 additions and 106 deletions

View File

@ -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)}

View File

@ -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>

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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}
/>
}

View File

@ -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}
/>
)
})

View File

@ -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
View 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
}