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