Ver código fonte

Revert "feat: knowledge admin role" (#6018)

takatost 11 meses atrás
pai
commit
79df8825c8
46 arquivos alterados com 350 adições e 1028 exclusões
  1. 0 5
      api/configs/feature/__init__.py
  2. 8 59
      api/controllers/console/datasets/datasets.py
  3. 8 18
      api/controllers/console/datasets/datasets_document.py
  4. 6 6
      api/controllers/console/tag/tags.py
  5. 0 13
      api/controllers/console/workspace/members.py
  6. 0 42
      api/migrations/versions/7e6a8693e07a_add_table_dataset_permissions.py
  7. 2 22
      api/models/account.py
  8. 0 15
      api/models/dataset.py
  9. 0 22
      api/services/account_service.py
  10. 75 236
      api/services/dataset_service.py
  11. 0 2
      api/services/feature_service.py
  12. 1 12
      web/app/(commonLayout)/app/(appDetailLayout)/layout.tsx
  13. 1 8
      web/app/(commonLayout)/apps/Apps.tsx
  14. 1 3
      web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx
  15. 1 10
      web/app/(commonLayout)/datasets/Container.tsx
  16. 11 17
      web/app/(commonLayout)/datasets/DatasetCard.tsx
  17. 0 11
      web/app/(commonLayout)/tools/page.tsx
  18. 7 38
      web/app/components/app/configuration/dataset-config/settings-modal/index.tsx
  19. 0 10
      web/app/components/base/icons/assets/vender/solid/users/users-plus.svg
  20. 0 77
      web/app/components/base/icons/src/vender/solid/users/UsersPlus.json
  21. 0 16
      web/app/components/base/icons/src/vender/solid/users/UsersPlus.tsx
  22. 0 1
      web/app/components/base/icons/src/vender/solid/users/index.ts
  23. 1 1
      web/app/components/base/search-input/index.tsx
  24. 0 1
      web/app/components/billing/type.ts
  25. 22 40
      web/app/components/datasets/settings/form/index.tsx
  26. 0 174
      web/app/components/datasets/settings/permission-selector/index.tsx
  27. 7 0
      web/app/components/datasets/settings/permissions-radio/assets/user.svg
  28. 46 0
      web/app/components/datasets/settings/permissions-radio/index.module.css
  29. 66 0
      web/app/components/datasets/settings/permissions-radio/index.tsx
  30. 1 8
      web/app/components/explore/index.tsx
  31. 1 7
      web/app/components/header/account-setting/index.tsx
  32. 0 1
      web/app/components/header/account-setting/members-page/index.tsx
  33. 67 7
      web/app/components/header/account-setting/members-page/invite-modal/index.tsx
  34. 0 95
      web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx
  35. 4 14
      web/app/components/header/account-setting/members-page/operation/index.tsx
  36. 9 9
      web/app/components/header/index.tsx
  37. 1 1
      web/app/components/header/nav/nav-selector/index.tsx
  38. 0 4
      web/context/app-context.tsx
  39. 0 8
      web/context/provider-context.tsx
  40. 0 2
      web/i18n/en-US/common.ts
  41. 0 2
      web/i18n/en-US/dataset-settings.ts
  42. 0 2
      web/i18n/zh-Hans/common.ts
  43. 0 2
      web/i18n/zh-Hans/dataset-settings.ts
  44. 2 2
      web/models/common.ts
  45. 1 4
      web/models/datasets.ts
  46. 1 1
      web/service/datasets.ts

+ 0 - 5
api/configs/feature/__init__.py

@@ -395,11 +395,6 @@ class DataSetConfig(BaseModel):
         default=30,
     )
 
-    DATASET_OPERATOR_ENABLED: bool = Field(
-        description='whether to enable dataset operator',
-        default=False,
-    )
-
 
 class WorkspaceConfig(BaseModel):
     """

+ 8 - 59
api/controllers/console/datasets/datasets.py

@@ -25,7 +25,7 @@ from fields.document_fields import document_status_fields
 from libs.login import login_required
 from models.dataset import Dataset, Document, DocumentSegment
 from models.model import ApiToken, UploadFile
-from services.dataset_service import DatasetPermissionService, DatasetService, DocumentService
+from services.dataset_service import DatasetService, DocumentService
 
 
 def _validate_name(name):
@@ -85,12 +85,6 @@ class DatasetListApi(Resource):
             else:
                 item['embedding_available'] = True
 
-            if item.get('permission') == 'partial_members':
-                part_users_list = DatasetPermissionService.get_dataset_partial_member_list(item['id'])
-                item.update({'partial_member_list': part_users_list})
-            else:
-                item.update({'partial_member_list': []})
-
         response = {
             'data': data,
             'has_more': len(datasets) == limit,
@@ -114,7 +108,7 @@ class DatasetListApi(Resource):
                             help='Invalid indexing technique.')
         args = parser.parse_args()
 
-        # The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
+        # The role of the current user in the ta table must be admin, owner, or editor
         if not current_user.is_editor:
             raise Forbidden()
 
@@ -146,10 +140,6 @@ class DatasetApi(Resource):
         except services.errors.account.NoPermissionError as e:
             raise Forbidden(str(e))
         data = marshal(dataset, dataset_detail_fields)
-        if data.get('permission') == 'partial_members':
-            part_users_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)
-            data.update({'partial_member_list': part_users_list})
-
         # check embedding setting
         provider_manager = ProviderManager()
         configurations = provider_manager.get_configurations(
@@ -173,11 +163,6 @@ class DatasetApi(Resource):
                 data['embedding_available'] = False
         else:
             data['embedding_available'] = True
-
-        if data.get('permission') == 'partial_members':
-            part_users_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)
-            data.update({'partial_member_list': part_users_list})
-
         return data, 200
 
     @setup_required
@@ -203,21 +188,17 @@ class DatasetApi(Resource):
                             nullable=True,
                             help='Invalid indexing technique.')
         parser.add_argument('permission', type=str, location='json', choices=(
-            'only_me', 'all_team_members', 'partial_members'), help='Invalid permission.'
-                            )
+            'only_me', 'all_team_members'), help='Invalid permission.')
         parser.add_argument('embedding_model', type=str,
                             location='json', help='Invalid embedding model.')
         parser.add_argument('embedding_model_provider', type=str,
                             location='json', help='Invalid embedding model provider.')
         parser.add_argument('retrieval_model', type=dict, location='json', help='Invalid retrieval model.')
-        parser.add_argument('partial_member_list', type=list, location='json', help='Invalid parent user list.')
         args = parser.parse_args()
-        data = request.get_json()
 
-        # The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
-        DatasetPermissionService.check_permission(
-            current_user, dataset, data.get('permission'), data.get('partial_member_list')
-        )
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
 
         dataset = DatasetService.update_dataset(
             dataset_id_str, args, current_user)
@@ -225,17 +206,7 @@ class DatasetApi(Resource):
         if dataset is None:
             raise NotFound("Dataset not found.")
 
-        result_data = marshal(dataset, dataset_detail_fields)
-
-        if data.get('partial_member_list') and data.get('permission') == 'partial_members':
-            DatasetPermissionService.update_partial_member_list(dataset_id_str, data.get('partial_member_list'))
-        else:
-            DatasetPermissionService.clear_partial_member_list(dataset_id_str)
-
-        partial_member_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)
-        result_data.update({'partial_member_list': partial_member_list})
-
-        return result_data, 200
+        return marshal(dataset, dataset_detail_fields), 200
 
     @setup_required
     @login_required
@@ -244,7 +215,7 @@ class DatasetApi(Resource):
         dataset_id_str = str(dataset_id)
 
         # The role of the current user in the ta table must be admin, owner, or editor
-        if not current_user.is_editor or current_user.is_dataset_operator:
+        if not current_user.is_editor:
             raise Forbidden()
 
         try:
@@ -598,27 +569,6 @@ class DatasetErrorDocs(Resource):
         }, 200
 
 
-class DatasetPermissionUserListApi(Resource):
-    @setup_required
-    @login_required
-    @account_initialization_required
-    def get(self, dataset_id):
-        dataset_id_str = str(dataset_id)
-        dataset = DatasetService.get_dataset(dataset_id_str)
-        if dataset is None:
-            raise NotFound("Dataset not found.")
-        try:
-            DatasetService.check_dataset_permission(dataset, current_user)
-        except services.errors.account.NoPermissionError as e:
-            raise Forbidden(str(e))
-
-        partial_members_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)
-
-        return {
-            'data': partial_members_list,
-        }, 200
-
-
 api.add_resource(DatasetListApi, '/datasets')
 api.add_resource(DatasetApi, '/datasets/<uuid:dataset_id>')
 api.add_resource(DatasetUseCheckApi, '/datasets/<uuid:dataset_id>/use-check')
@@ -632,4 +582,3 @@ api.add_resource(DatasetApiDeleteApi, '/datasets/api-keys/<uuid:api_key_id>')
 api.add_resource(DatasetApiBaseUrlApi, '/datasets/api-base-info')
 api.add_resource(DatasetRetrievalSettingApi, '/datasets/retrieval-setting')
 api.add_resource(DatasetRetrievalSettingMockApi, '/datasets/retrieval-setting/<string:vector_type>')
-api.add_resource(DatasetPermissionUserListApi, '/datasets/<uuid:dataset_id>/permission-part-users')

+ 8 - 18
api/controllers/console/datasets/datasets_document.py

@@ -228,7 +228,7 @@ class DatasetDocumentListApi(Resource):
             raise NotFound('Dataset not found.')
 
         # The role of the current user in the ta table must be admin, owner, or editor
-        if not current_user.is_dataset_editor:
+        if not current_user.is_editor:
             raise Forbidden()
 
         try:
@@ -294,11 +294,6 @@ class DatasetInitApi(Resource):
         parser.add_argument('retrieval_model', type=dict, required=False, nullable=False,
                             location='json')
         args = parser.parse_args()
-
-        # The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
-        if not current_user.is_dataset_editor:
-            raise Forbidden()
-
         if args['indexing_technique'] == 'high_quality':
             try:
                 model_manager = ModelManager()
@@ -762,19 +757,15 @@ class DocumentStatusApi(DocumentResource):
         dataset = DatasetService.get_dataset(dataset_id)
         if dataset is None:
             raise NotFound("Dataset not found.")
-
-        # The role of the current user in the ta table must be admin, owner, or editor
-        if not current_user.is_dataset_editor:
-            raise Forbidden()
-
         # check user's model setting
         DatasetService.check_dataset_model_setting(dataset)
 
-        # check user's permission
-        DatasetService.check_dataset_permission(dataset, current_user)
-
         document = self.get_document(dataset_id, document_id)
 
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+
         indexing_cache_key = 'document_{}_indexing'.format(document.id)
         cache_result = redis_client.get(indexing_cache_key)
         if cache_result is not None:
@@ -964,11 +955,10 @@ class DocumentRenameApi(DocumentResource):
     @account_initialization_required
     @marshal_with(document_fields)
     def post(self, dataset_id, document_id):
-        # The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
-        if not current_user.is_dataset_editor:
+        # The role of the current user in the ta table must be admin or owner
+        if not current_user.is_admin_or_owner:
             raise Forbidden()
-        dataset = DatasetService.get_dataset(dataset_id)
-        DatasetService.check_dataset_operator_permission(current_user, dataset)
+
         parser = reqparse.RequestParser()
         parser.add_argument('name', type=str, required=True, nullable=False, location='json')
         args = parser.parse_args()

+ 6 - 6
api/controllers/console/tag/tags.py

@@ -36,7 +36,7 @@ class TagListApi(Resource):
     @account_initialization_required
     def post(self):
         # The role of the current user in the ta table must be admin, owner, or editor
-        if not (current_user.is_editor or current_user.is_dataset_editor):
+        if not current_user.is_editor:
             raise Forbidden()
 
         parser = reqparse.RequestParser()
@@ -68,7 +68,7 @@ class TagUpdateDeleteApi(Resource):
     def patch(self, tag_id):
         tag_id = str(tag_id)
         # The role of the current user in the ta table must be admin, owner, or editor
-        if not (current_user.is_editor or current_user.is_dataset_editor):
+        if not current_user.is_editor:
             raise Forbidden()
 
         parser = reqparse.RequestParser()
@@ -109,8 +109,8 @@ class TagBindingCreateApi(Resource):
     @login_required
     @account_initialization_required
     def post(self):
-        # The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
-        if not (current_user.is_editor or current_user.is_dataset_editor):
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
             raise Forbidden()
 
         parser = reqparse.RequestParser()
@@ -134,8 +134,8 @@ class TagBindingDeleteApi(Resource):
     @login_required
     @account_initialization_required
     def post(self):
-        # The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
-        if not (current_user.is_editor or current_user.is_dataset_editor):
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
             raise Forbidden()
 
         parser = reqparse.RequestParser()

+ 0 - 13
api/controllers/console/workspace/members.py

@@ -131,20 +131,7 @@ class MemberUpdateRoleApi(Resource):
         return {'result': 'success'}
 
 
-class DatasetOperatorMemberListApi(Resource):
-    """List all members of current tenant."""
-
-    @setup_required
-    @login_required
-    @account_initialization_required
-    @marshal_with(account_with_role_list_fields)
-    def get(self):
-        members = TenantService.get_dataset_operator_members(current_user.current_tenant)
-        return {'result': 'success', 'accounts': members}, 200
-
-
 api.add_resource(MemberListApi, '/workspaces/current/members')
 api.add_resource(MemberInviteEmailApi, '/workspaces/current/members/invite-email')
 api.add_resource(MemberCancelInviteApi, '/workspaces/current/members/<uuid:member_id>')
 api.add_resource(MemberUpdateRoleApi, '/workspaces/current/members/<uuid:member_id>/update-role')
-api.add_resource(DatasetOperatorMemberListApi, '/workspaces/current/dataset-operators')

+ 0 - 42
api/migrations/versions/7e6a8693e07a_add_table_dataset_permissions.py

@@ -1,42 +0,0 @@
-"""add table dataset_permissions
-
-Revision ID: 7e6a8693e07a
-Revises: 4ff534e1eb11
-Create Date: 2024-06-25 03:20:46.012193
-
-"""
-import sqlalchemy as sa
-from alembic import op
-
-import models as models
-
-# revision identifiers, used by Alembic.
-revision = '7e6a8693e07a'
-down_revision = 'b2602e131636'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.create_table('dataset_permissions',
-    sa.Column('id', models.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
-    sa.Column('dataset_id', models.StringUUID(), nullable=False),
-    sa.Column('account_id', models.StringUUID(), nullable=False),
-    sa.Column('has_permission', sa.Boolean(), server_default=sa.text('true'), nullable=False),
-    sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
-    sa.PrimaryKeyConstraint('id', name='dataset_permission_pkey')
-    )
-    with op.batch_alter_table('dataset_permissions', schema=None) as batch_op:
-        batch_op.create_index('idx_dataset_permissions_account_id', ['account_id'], unique=False)
-        batch_op.create_index('idx_dataset_permissions_dataset_id', ['dataset_id'], unique=False)
-    # ### end Alembic commands ###
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    with op.batch_alter_table('dataset_permissions', schema=None) as batch_op:
-        batch_op.drop_index('idx_dataset_permissions_dataset_id')
-        batch_op.drop_index('idx_dataset_permissions_account_id')
-    op.drop_table('dataset_permissions')
-    # ### end Alembic commands ###

+ 2 - 22
api/models/account.py

@@ -80,10 +80,6 @@ class Account(UserMixin, db.Model):
 
         self._current_tenant = tenant
 
-    @property
-    def current_role(self):
-        return self._current_tenant.current_role
-
     def get_status(self) -> AccountStatus:
         status_str = self.status
         return AccountStatus(status_str)
@@ -114,14 +110,6 @@ class Account(UserMixin, db.Model):
     def is_editor(self):
         return TenantAccountRole.is_editing_role(self._current_tenant.current_role)
 
-    @property
-    def is_dataset_editor(self):
-        return TenantAccountRole.is_dataset_edit_role(self._current_tenant.current_role)
-
-    @property
-    def is_dataset_operator(self):
-        return self._current_tenant.current_role == TenantAccountRole.DATASET_OPERATOR
-
 class TenantStatus(str, enum.Enum):
     NORMAL = 'normal'
     ARCHIVE = 'archive'
@@ -132,12 +120,10 @@ class TenantAccountRole(str, enum.Enum):
     ADMIN = 'admin'
     EDITOR = 'editor'
     NORMAL = 'normal'
-    DATASET_OPERATOR = 'dataset_operator'
 
     @staticmethod
     def is_valid_role(role: str) -> bool:
-        return role and role in {TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR,
-                                 TenantAccountRole.NORMAL, TenantAccountRole.DATASET_OPERATOR}
+        return role and role in {TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR, TenantAccountRole.NORMAL}
 
     @staticmethod
     def is_privileged_role(role: str) -> bool:
@@ -145,17 +131,12 @@ class TenantAccountRole(str, enum.Enum):
     
     @staticmethod
     def is_non_owner_role(role: str) -> bool:
-        return role and role in {TenantAccountRole.ADMIN, TenantAccountRole.EDITOR, TenantAccountRole.NORMAL,
-                                 TenantAccountRole.DATASET_OPERATOR}
+        return role and role in {TenantAccountRole.ADMIN, TenantAccountRole.EDITOR, TenantAccountRole.NORMAL}
     
     @staticmethod
     def is_editing_role(role: str) -> bool:
         return role and role in {TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR}
 
-    @staticmethod
-    def is_dataset_edit_role(role: str) -> bool:
-        return role and role in {TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR,
-                                 TenantAccountRole.DATASET_OPERATOR}
 
 class Tenant(db.Model):
     __tablename__ = 'tenants'
@@ -191,7 +172,6 @@ class TenantAccountJoinRole(enum.Enum):
     OWNER = 'owner'
     ADMIN = 'admin'
     NORMAL = 'normal'
-    DATASET_OPERATOR = 'dataset_operator'
 
 
 class TenantAccountJoin(db.Model):

+ 0 - 15
api/models/dataset.py

@@ -663,18 +663,3 @@ class DatasetCollectionBinding(db.Model):
     type = db.Column(db.String(40), server_default=db.text("'dataset'::character varying"), nullable=False)
     collection_name = db.Column(db.String(64), nullable=False)
     created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
-
-
-class DatasetPermission(db.Model):
-    __tablename__ = 'dataset_permissions'
-    __table_args__ = (
-        db.PrimaryKeyConstraint('id', name='dataset_permission_pkey'),
-        db.Index('idx_dataset_permissions_dataset_id', 'dataset_id'),
-        db.Index('idx_dataset_permissions_account_id', 'account_id')
-    )
-
-    id = db.Column(StringUUID, server_default=db.text('uuid_generate_v4()'), primary_key=True)
-    dataset_id = db.Column(StringUUID, nullable=False)
-    account_id = db.Column(StringUUID, nullable=False)
-    has_permission = db.Column(db.Boolean, nullable=False, server_default=db.text('true'))
-    created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))

+ 0 - 22
api/services/account_service.py

@@ -370,28 +370,6 @@ class TenantService:
         return updated_accounts
 
     @staticmethod
-    def get_dataset_operator_members(tenant: Tenant) -> list[Account]:
-        """Get dataset admin members"""
-        query = (
-            db.session.query(Account, TenantAccountJoin.role)
-            .select_from(Account)
-            .join(
-                TenantAccountJoin, Account.id == TenantAccountJoin.account_id
-            )
-            .filter(TenantAccountJoin.tenant_id == tenant.id)
-            .filter(TenantAccountJoin.role == 'dataset_operator')
-        )
-
-        # Initialize an empty list to store the updated accounts
-        updated_accounts = []
-
-        for account, role in query:
-            account.role = role
-            updated_accounts.append(account)
-
-        return updated_accounts
-
-    @staticmethod
     def has_roles(tenant: Tenant, roles: list[TenantAccountJoinRole]) -> bool:
         """Check if user has any of the given roles for a tenant"""
         if not all(isinstance(role, TenantAccountJoinRole) for role in roles):

+ 75 - 236
api/services/dataset_service.py

@@ -21,12 +21,11 @@ from events.document_event import document_was_deleted
 from extensions.ext_database import db
 from extensions.ext_redis import redis_client
 from libs import helper
-from models.account import Account, TenantAccountRole
+from models.account import Account
 from models.dataset import (
     AppDatasetJoin,
     Dataset,
     DatasetCollectionBinding,
-    DatasetPermission,
     DatasetProcessRule,
     DatasetQuery,
     Document,
@@ -57,38 +56,22 @@ class DatasetService:
 
     @staticmethod
     def get_datasets(page, per_page, provider="vendor", tenant_id=None, user=None, search=None, tag_ids=None):
-        query = Dataset.query.filter(Dataset.provider == provider, Dataset.tenant_id == tenant_id)
-
         if user:
-            if user.current_role == TenantAccountRole.DATASET_OPERATOR:
-                dataset_permission = DatasetPermission.query.filter_by(account_id=user.id).all()
-                if dataset_permission:
-                    dataset_ids = [dp.dataset_id for dp in dataset_permission]
-                    query = query.filter(Dataset.id.in_(dataset_ids))
-                else:
-                    query = query.filter(db.false())
-            else:
-                permission_filter = db.or_(
-                    Dataset.created_by == user.id,
-                    Dataset.permission == 'all_team_members',
-                    Dataset.permission == 'partial_members',
-                    Dataset.permission == 'only_me'
-                )
-                query = query.filter(permission_filter)
+            permission_filter = db.or_(Dataset.created_by == user.id,
+                                       Dataset.permission == 'all_team_members')
         else:
             permission_filter = Dataset.permission == 'all_team_members'
-            query = query.filter(permission_filter)
-
+        query = Dataset.query.filter(
+            db.and_(Dataset.provider == provider, Dataset.tenant_id == tenant_id, permission_filter)) \
+            .order_by(Dataset.created_at.desc())
         if search:
-            query = query.filter(Dataset.name.ilike(f'%{search}%'))
-
+            query = query.filter(db.and_(Dataset.name.ilike(f'%{search}%')))
         if tag_ids:
             target_ids = TagService.get_target_ids_by_tag_ids('knowledge', tenant_id, tag_ids)
             if target_ids:
-                query = query.filter(Dataset.id.in_(target_ids))
+                query = query.filter(db.and_(Dataset.id.in_(target_ids)))
             else:
                 return [], 0
-
         datasets = query.paginate(
             page=page,
             per_page=per_page,
@@ -96,12 +79,6 @@ class DatasetService:
             error_out=False
         )
 
-        # check datasets permission,
-        if user and user.current_role != TenantAccountRole.DATASET_OPERATOR:
-            datasets.items, datasets.total = DatasetService.filter_datasets_by_permission(
-                user, datasets
-            )
-
         return datasets.items, datasets.total
 
     @staticmethod
@@ -125,12 +102,9 @@ class DatasetService:
 
     @staticmethod
     def get_datasets_by_ids(ids, tenant_id):
-        datasets = Dataset.query.filter(
-            Dataset.id.in_(ids),
-            Dataset.tenant_id == tenant_id
-        ).paginate(
-            page=1, per_page=len(ids), max_per_page=len(ids), error_out=False
-        )
+        datasets = Dataset.query.filter(Dataset.id.in_(ids),
+                                        Dataset.tenant_id == tenant_id).paginate(
+            page=1, per_page=len(ids), max_per_page=len(ids), error_out=False)
         return datasets.items, datasets.total
 
     @staticmethod
@@ -138,8 +112,7 @@ class DatasetService:
         # check if dataset name already exists
         if Dataset.query.filter_by(name=name, tenant_id=tenant_id).first():
             raise DatasetNameDuplicateError(
-                f'Dataset with name {name} already exists.'
-            )
+                f'Dataset with name {name} already exists.')
         embedding_model = None
         if indexing_technique == 'high_quality':
             model_manager = ModelManager()
@@ -178,17 +151,13 @@ class DatasetService:
             except LLMBadRequestError:
                 raise ValueError(
                     "No Embedding Model available. Please configure a valid provider "
-                    "in the Settings -> Model Provider."
-                )
+                    "in the Settings -> Model Provider.")
             except ProviderTokenNotInitError as ex:
-                raise ValueError(
-                    f"The dataset in unavailable, due to: "
-                    f"{ex.description}"
-                )
+                raise ValueError(f"The dataset in unavailable, due to: "
+                                 f"{ex.description}")
 
     @staticmethod
     def update_dataset(dataset_id, data, user):
-        data.pop('partial_member_list', None)
         filtered_data = {k: v for k, v in data.items() if v is not None or k == 'description'}
         dataset = DatasetService.get_dataset(dataset_id)
         DatasetService.check_dataset_permission(dataset, user)
@@ -221,13 +190,12 @@ class DatasetService:
                 except LLMBadRequestError:
                     raise ValueError(
                         "No Embedding Model available. Please configure a valid provider "
-                        "in the Settings -> Model Provider."
-                    )
+                        "in the Settings -> Model Provider.")
                 except ProviderTokenNotInitError as ex:
                     raise ValueError(ex.description)
         else:
             if data['embedding_model_provider'] != dataset.embedding_model_provider or \
-                data['embedding_model'] != dataset.embedding_model:
+                    data['embedding_model'] != dataset.embedding_model:
                 action = 'update'
                 try:
                     model_manager = ModelManager()
@@ -247,8 +215,7 @@ class DatasetService:
                 except LLMBadRequestError:
                     raise ValueError(
                         "No Embedding Model available. Please configure a valid provider "
-                        "in the Settings -> Model Provider."
-                    )
+                        "in the Settings -> Model Provider.")
                 except ProviderTokenNotInitError as ex:
                     raise ValueError(ex.description)
 
@@ -292,41 +259,14 @@ class DatasetService:
     def check_dataset_permission(dataset, user):
         if dataset.tenant_id != user.current_tenant_id:
             logging.debug(
-                f'User {user.id} does not have permission to access dataset {dataset.id}'
-            )
+                f'User {user.id} does not have permission to access dataset {dataset.id}')
             raise NoPermissionError(
-                'You do not have permission to access this dataset.'
-            )
+                'You do not have permission to access this dataset.')
         if dataset.permission == 'only_me' and dataset.created_by != user.id:
             logging.debug(
-                f'User {user.id} does not have permission to access dataset {dataset.id}'
-            )
+                f'User {user.id} does not have permission to access dataset {dataset.id}')
             raise NoPermissionError(
-                'You do not have permission to access this dataset.'
-            )
-        if dataset.permission == 'partial_members':
-            user_permission = DatasetPermission.query.filter_by(
-                dataset_id=dataset.id, account_id=user.id
-            ).first()
-            if not user_permission and dataset.tenant_id != user.current_tenant_id and dataset.created_by != user.id:
-                logging.debug(
-                    f'User {user.id} does not have permission to access dataset {dataset.id}'
-                )
-                raise NoPermissionError(
-                    'You do not have permission to access this dataset.'
-                )
-
-    @staticmethod
-    def check_dataset_operator_permission(user: Account = None, dataset: Dataset = None):
-        if dataset.permission == 'only_me':
-            if dataset.created_by != user.id:
-                raise NoPermissionError('You do not have permission to access this dataset.')
-
-        elif dataset.permission == 'partial_members':
-            if not any(
-                dp.dataset_id == dataset.id for dp in DatasetPermission.query.filter_by(account_id=user.id).all()
-            ):
-                raise NoPermissionError('You do not have permission to access this dataset.')
+                'You do not have permission to access this dataset.')
 
     @staticmethod
     def get_dataset_queries(dataset_id: str, page: int, per_page: int):
@@ -342,22 +282,6 @@ class DatasetService:
         return AppDatasetJoin.query.filter(AppDatasetJoin.dataset_id == dataset_id) \
             .order_by(db.desc(AppDatasetJoin.created_at)).all()
 
-    @staticmethod
-    def filter_datasets_by_permission(user, datasets):
-        dataset_permission = DatasetPermission.query.filter_by(account_id=user.id).all()
-        permitted_dataset_ids = {dp.dataset_id for dp in dataset_permission} if dataset_permission else set()
-
-        filtered_datasets = [
-            dataset for dataset in datasets if
-            (dataset.permission == 'all_team_members') or
-            (dataset.permission == 'only_me' and dataset.created_by == user.id) or
-            (dataset.id in permitted_dataset_ids)
-        ]
-
-        filtered_count = len(filtered_datasets)
-
-        return filtered_datasets, filtered_count
-
 
 class DocumentService:
     DEFAULT_RULES = {
@@ -623,7 +547,6 @@ class DocumentService:
         redis_client.setex(sync_indexing_cache_key, 600, 1)
 
         sync_website_document_indexing_task.delay(dataset_id, document.id)
-
     @staticmethod
     def get_documents_position(dataset_id):
         document = Document.query.filter_by(dataset_id=dataset_id).order_by(Document.position.desc()).first()
@@ -633,11 +556,9 @@ class DocumentService:
             return 1
 
     @staticmethod
-    def save_document_with_dataset_id(
-        dataset: Dataset, document_data: dict,
-        account: Account, dataset_process_rule: Optional[DatasetProcessRule] = None,
-        created_from: str = 'web'
-    ):
+    def save_document_with_dataset_id(dataset: Dataset, document_data: dict,
+                                      account: Account, dataset_process_rule: Optional[DatasetProcessRule] = None,
+                                      created_from: str = 'web'):
 
         # check document limit
         features = FeatureService.get_features(current_user.current_tenant_id)
@@ -667,7 +588,7 @@ class DocumentService:
 
         if not dataset.indexing_technique:
             if 'indexing_technique' not in document_data \
-                or document_data['indexing_technique'] not in Dataset.INDEXING_TECHNIQUE_LIST:
+                    or document_data['indexing_technique'] not in Dataset.INDEXING_TECHNIQUE_LIST:
                 raise ValueError("Indexing technique is required")
 
             dataset.indexing_technique = document_data["indexing_technique"]
@@ -697,8 +618,7 @@ class DocumentService:
                     }
 
                     dataset.retrieval_model = document_data.get('retrieval_model') if document_data.get(
-                        'retrieval_model'
-                    ) else default_retrieval_model
+                        'retrieval_model') else default_retrieval_model
 
         documents = []
         batch = time.strftime('%Y%m%d%H%M%S') + str(random.randint(100000, 999999))
@@ -766,14 +686,12 @@ class DocumentService:
                             documents.append(document)
                             duplicate_document_ids.append(document.id)
                             continue
-                    document = DocumentService.build_document(
-                        dataset, dataset_process_rule.id,
-                        document_data["data_source"]["type"],
-                        document_data["doc_form"],
-                        document_data["doc_language"],
-                        data_source_info, created_from, position,
-                        account, file_name, batch
-                    )
+                    document = DocumentService.build_document(dataset, dataset_process_rule.id,
+                                                              document_data["data_source"]["type"],
+                                                              document_data["doc_form"],
+                                                              document_data["doc_language"],
+                                                              data_source_info, created_from, position,
+                                                              account, file_name, batch)
                     db.session.add(document)
                     db.session.flush()
                     document_ids.append(document.id)
@@ -814,14 +732,12 @@ class DocumentService:
                                 "notion_page_icon": page['page_icon'],
                                 "type": page['type']
                             }
-                            document = DocumentService.build_document(
-                                dataset, dataset_process_rule.id,
-                                document_data["data_source"]["type"],
-                                document_data["doc_form"],
-                                document_data["doc_language"],
-                                data_source_info, created_from, position,
-                                account, page['page_name'], batch
-                            )
+                            document = DocumentService.build_document(dataset, dataset_process_rule.id,
+                                                                      document_data["data_source"]["type"],
+                                                                      document_data["doc_form"],
+                                                                      document_data["doc_language"],
+                                                                      data_source_info, created_from, position,
+                                                                      account, page['page_name'], batch)
                             db.session.add(document)
                             db.session.flush()
                             document_ids.append(document.id)
@@ -843,14 +759,12 @@ class DocumentService:
                         'only_main_content': website_info.get('only_main_content', False),
                         'mode': 'crawl',
                     }
-                    document = DocumentService.build_document(
-                        dataset, dataset_process_rule.id,
-                        document_data["data_source"]["type"],
-                        document_data["doc_form"],
-                        document_data["doc_language"],
-                        data_source_info, created_from, position,
-                        account, url, batch
-                    )
+                    document = DocumentService.build_document(dataset, dataset_process_rule.id,
+                                                              document_data["data_source"]["type"],
+                                                              document_data["doc_form"],
+                                                              document_data["doc_language"],
+                                                              data_source_info, created_from, position,
+                                                              account, url, batch)
                     db.session.add(document)
                     db.session.flush()
                     document_ids.append(document.id)
@@ -871,16 +785,13 @@ class DocumentService:
         can_upload_size = features.documents_upload_quota.limit - features.documents_upload_quota.size
         if count > can_upload_size:
             raise ValueError(
-                f'You have reached the limit of your subscription. Only {can_upload_size} documents can be uploaded.'
-            )
+                f'You have reached the limit of your subscription. Only {can_upload_size} documents can be uploaded.')
 
     @staticmethod
-    def build_document(
-        dataset: Dataset, process_rule_id: str, data_source_type: str, document_form: str,
-        document_language: str, data_source_info: dict, created_from: str, position: int,
-        account: Account,
-        name: str, batch: str
-    ):
+    def build_document(dataset: Dataset, process_rule_id: str, data_source_type: str, document_form: str,
+                       document_language: str, data_source_info: dict, created_from: str, position: int,
+                       account: Account,
+                       name: str, batch: str):
         document = Document(
             tenant_id=dataset.tenant_id,
             dataset_id=dataset.id,
@@ -899,20 +810,16 @@ class DocumentService:
 
     @staticmethod
     def get_tenant_documents_count():
-        documents_count = Document.query.filter(
-            Document.completed_at.isnot(None),
-            Document.enabled == True,
-            Document.archived == False,
-            Document.tenant_id == current_user.current_tenant_id
-        ).count()
+        documents_count = Document.query.filter(Document.completed_at.isnot(None),
+                                                Document.enabled == True,
+                                                Document.archived == False,
+                                                Document.tenant_id == current_user.current_tenant_id).count()
         return documents_count
 
     @staticmethod
-    def update_document_with_dataset_id(
-        dataset: Dataset, document_data: dict,
-        account: Account, dataset_process_rule: Optional[DatasetProcessRule] = None,
-        created_from: str = 'web'
-    ):
+    def update_document_with_dataset_id(dataset: Dataset, document_data: dict,
+                                        account: Account, dataset_process_rule: Optional[DatasetProcessRule] = None,
+                                        created_from: str = 'web'):
         DatasetService.check_dataset_model_setting(dataset)
         document = DocumentService.get_document(dataset.id, document_data["original_document_id"])
         if document.display_status != 'available':
@@ -1100,7 +1007,7 @@ class DocumentService:
             DocumentService.process_rule_args_validate(args)
         else:
             if ('data_source' not in args and not args['data_source']) \
-                and ('process_rule' not in args and not args['process_rule']):
+                    and ('process_rule' not in args and not args['process_rule']):
                 raise ValueError("Data source or Process rule is required")
             else:
                 if args.get('data_source'):
@@ -1162,7 +1069,7 @@ class DocumentService:
                 raise ValueError("Process rule rules is invalid")
 
             if 'pre_processing_rules' not in args['process_rule']['rules'] \
-                or args['process_rule']['rules']['pre_processing_rules'] is None:
+                    or args['process_rule']['rules']['pre_processing_rules'] is None:
                 raise ValueError("Process rule pre_processing_rules is required")
 
             if not isinstance(args['process_rule']['rules']['pre_processing_rules'], list):
@@ -1187,21 +1094,21 @@ class DocumentService:
             args['process_rule']['rules']['pre_processing_rules'] = list(unique_pre_processing_rule_dicts.values())
 
             if 'segmentation' not in args['process_rule']['rules'] \
-                or args['process_rule']['rules']['segmentation'] is None:
+                    or args['process_rule']['rules']['segmentation'] is None:
                 raise ValueError("Process rule segmentation is required")
 
             if not isinstance(args['process_rule']['rules']['segmentation'], dict):
                 raise ValueError("Process rule segmentation is invalid")
 
             if 'separator' not in args['process_rule']['rules']['segmentation'] \
-                or not args['process_rule']['rules']['segmentation']['separator']:
+                    or not args['process_rule']['rules']['segmentation']['separator']:
                 raise ValueError("Process rule segmentation separator is required")
 
             if not isinstance(args['process_rule']['rules']['segmentation']['separator'], str):
                 raise ValueError("Process rule segmentation separator is invalid")
 
             if 'max_tokens' not in args['process_rule']['rules']['segmentation'] \
-                or not args['process_rule']['rules']['segmentation']['max_tokens']:
+                    or not args['process_rule']['rules']['segmentation']['max_tokens']:
                 raise ValueError("Process rule segmentation max_tokens is required")
 
             if not isinstance(args['process_rule']['rules']['segmentation']['max_tokens'], int):
@@ -1237,7 +1144,7 @@ class DocumentService:
                 raise ValueError("Process rule rules is invalid")
 
             if 'pre_processing_rules' not in args['process_rule']['rules'] \
-                or args['process_rule']['rules']['pre_processing_rules'] is None:
+                    or args['process_rule']['rules']['pre_processing_rules'] is None:
                 raise ValueError("Process rule pre_processing_rules is required")
 
             if not isinstance(args['process_rule']['rules']['pre_processing_rules'], list):
@@ -1262,21 +1169,21 @@ class DocumentService:
             args['process_rule']['rules']['pre_processing_rules'] = list(unique_pre_processing_rule_dicts.values())
 
             if 'segmentation' not in args['process_rule']['rules'] \
-                or args['process_rule']['rules']['segmentation'] is None:
+                    or args['process_rule']['rules']['segmentation'] is None:
                 raise ValueError("Process rule segmentation is required")
 
             if not isinstance(args['process_rule']['rules']['segmentation'], dict):
                 raise ValueError("Process rule segmentation is invalid")
 
             if 'separator' not in args['process_rule']['rules']['segmentation'] \
-                or not args['process_rule']['rules']['segmentation']['separator']:
+                    or not args['process_rule']['rules']['segmentation']['separator']:
                 raise ValueError("Process rule segmentation separator is required")
 
             if not isinstance(args['process_rule']['rules']['segmentation']['separator'], str):
                 raise ValueError("Process rule segmentation separator is invalid")
 
             if 'max_tokens' not in args['process_rule']['rules']['segmentation'] \
-                or not args['process_rule']['rules']['segmentation']['max_tokens']:
+                    or not args['process_rule']['rules']['segmentation']['max_tokens']:
                 raise ValueError("Process rule segmentation max_tokens is required")
 
             if not isinstance(args['process_rule']['rules']['segmentation']['max_tokens'], int):
@@ -1530,16 +1437,12 @@ class SegmentService:
 
 class DatasetCollectionBindingService:
     @classmethod
-    def get_dataset_collection_binding(
-        cls, provider_name: str, model_name: str,
-        collection_type: str = 'dataset'
-    ) -> DatasetCollectionBinding:
+    def get_dataset_collection_binding(cls, provider_name: str, model_name: str,
+                                       collection_type: str = 'dataset') -> DatasetCollectionBinding:
         dataset_collection_binding = db.session.query(DatasetCollectionBinding). \
-            filter(
-            DatasetCollectionBinding.provider_name == provider_name,
-            DatasetCollectionBinding.model_name == model_name,
-            DatasetCollectionBinding.type == collection_type
-        ). \
+            filter(DatasetCollectionBinding.provider_name == provider_name,
+                   DatasetCollectionBinding.model_name == model_name,
+                   DatasetCollectionBinding.type == collection_type). \
             order_by(DatasetCollectionBinding.created_at). \
             first()
 
@@ -1555,76 +1458,12 @@ class DatasetCollectionBindingService:
         return dataset_collection_binding
 
     @classmethod
-    def get_dataset_collection_binding_by_id_and_type(
-        cls, collection_binding_id: str,
-        collection_type: str = 'dataset'
-    ) -> DatasetCollectionBinding:
+    def get_dataset_collection_binding_by_id_and_type(cls, collection_binding_id: str,
+                                                      collection_type: str = 'dataset') -> DatasetCollectionBinding:
         dataset_collection_binding = db.session.query(DatasetCollectionBinding). \
-            filter(
-            DatasetCollectionBinding.id == collection_binding_id,
-            DatasetCollectionBinding.type == collection_type
-        ). \
+            filter(DatasetCollectionBinding.id == collection_binding_id,
+                   DatasetCollectionBinding.type == collection_type). \
             order_by(DatasetCollectionBinding.created_at). \
             first()
 
         return dataset_collection_binding
-
-
-class DatasetPermissionService:
-    @classmethod
-    def get_dataset_partial_member_list(cls, dataset_id):
-        user_list_query = db.session.query(
-            DatasetPermission.account_id,
-        ).filter(
-            DatasetPermission.dataset_id == dataset_id
-        ).all()
-
-        user_list = []
-        for user in user_list_query:
-            user_list.append(user.account_id)
-
-        return user_list
-
-    @classmethod
-    def update_partial_member_list(cls, dataset_id, user_list):
-        try:
-            db.session.query(DatasetPermission).filter(DatasetPermission.dataset_id == dataset_id).delete()
-            permissions = []
-            for user in user_list:
-                permission = DatasetPermission(
-                    dataset_id=dataset_id,
-                    account_id=user['user_id'],
-                )
-                permissions.append(permission)
-
-            db.session.add_all(permissions)
-            db.session.commit()
-        except Exception as e:
-            db.session.rollback()
-            raise e
-
-    @classmethod
-    def check_permission(cls, user, dataset, requested_permission, requested_partial_member_list):
-        if not user.is_dataset_editor:
-            raise NoPermissionError('User does not have permission to edit this dataset.')
-
-        if user.is_dataset_operator and dataset.permission != requested_permission:
-            raise NoPermissionError('Dataset operators cannot change the dataset permissions.')
-
-        if user.is_dataset_operator and requested_permission == 'partial_members':
-            if not requested_partial_member_list:
-                raise ValueError('Partial member list is required when setting to partial members.')
-
-            local_member_list = cls.get_dataset_partial_member_list(dataset.id)
-            request_member_list = [user['user_id'] for user in requested_partial_member_list]
-            if set(local_member_list) != set(request_member_list):
-                raise ValueError('Dataset operators cannot change the dataset permissions.')
-
-    @classmethod
-    def clear_partial_member_list(cls, dataset_id):
-        try:
-            db.session.query(DatasetPermission).filter(DatasetPermission.dataset_id == dataset_id).delete()
-            db.session.commit()
-        except Exception as e:
-            db.session.rollback()
-            raise e

+ 0 - 2
api/services/feature_service.py

@@ -30,7 +30,6 @@ class FeatureModel(BaseModel):
     docs_processing: str = 'standard'
     can_replace_logo: bool = False
     model_load_balancing_enabled: bool = False
-    dataset_operator_enabled: bool = False
 
     # pydantic configs
     model_config = ConfigDict(protected_namespaces=())
@@ -69,7 +68,6 @@ class FeatureService:
     def _fulfill_params_from_env(cls, features: FeatureModel):
         features.can_replace_logo = current_app.config['CAN_REPLACE_LOGO']
         features.model_load_balancing_enabled = current_app.config['MODEL_LB_ENABLED']
-        features.dataset_operator_enabled = current_app.config['DATASET_OPERATOR_ENABLED']
 
     @classmethod
     def _fulfill_params_from_billing_api(cls, features: FeatureModel, tenant_id: str):

+ 1 - 12
web/app/(commonLayout)/app/(appDetailLayout)/layout.tsx

@@ -1,22 +1,11 @@
-'use client'
 import type { FC } from 'react'
-import React, { useEffect } from 'react'
-import { useRouter } from 'next/navigation'
-import { useAppContext } from '@/context/app-context'
+import React from 'react'
 
 export type IAppDetail = {
   children: React.ReactNode
 }
 
 const AppDetail: FC<IAppDetail> = ({ children }) => {
-  const router = useRouter()
-  const { isCurrentWorkspaceDatasetOperator } = useAppContext()
-
-  useEffect(() => {
-    if (isCurrentWorkspaceDatasetOperator)
-      return router.replace('/datasets')
-  }, [isCurrentWorkspaceDatasetOperator])
-
   return (
     <>
       {children}

+ 1 - 8
web/app/(commonLayout)/apps/Apps.tsx

@@ -1,7 +1,6 @@
 'use client'
 
 import { useCallback, useEffect, useRef, useState } from 'react'
-import { useRouter } from 'next/navigation'
 import useSWRInfinite from 'swr/infinite'
 import { useTranslation } from 'react-i18next'
 import { useDebounceFn } from 'ahooks'
@@ -51,8 +50,7 @@ const getKey = (
 
 const Apps = () => {
   const { t } = useTranslation()
-  const router = useRouter()
-  const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext()
+  const { isCurrentWorkspaceEditor } = useAppContext()
   const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
   const [activeTab, setActiveTab] = useTabSearchParams({
     defaultTab: 'all',
@@ -89,11 +87,6 @@ const Apps = () => {
     }
   }, [])
 
-  useEffect(() => {
-    if (isCurrentWorkspaceDatasetOperator)
-      return router.replace('/datasets')
-  }, [isCurrentWorkspaceDatasetOperator])
-
   const hasMore = data?.at(-1)?.has_more ?? true
   useEffect(() => {
     let observer: IntersectionObserver | undefined

+ 1 - 3
web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx

@@ -38,7 +38,6 @@ import { useStore } from '@/app/components/app/store'
 import { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication'
 import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
 import { getLocaleOnClient } from '@/i18n'
-import { useAppContext } from '@/context/app-context'
 
 export type IAppDetailLayoutProps = {
   children: React.ReactNode
@@ -188,7 +187,6 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
   const pathname = usePathname()
   const hideSideBar = /documents\/create$/.test(pathname)
   const { t } = useTranslation()
-  const { isCurrentWorkspaceDatasetOperator } = useAppContext()
 
   const media = useBreakpoints()
   const isMobile = media === MediaType.mobile
@@ -234,7 +232,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
         icon_background={datasetRes?.icon_background || '#F5F5F5'}
         desc={datasetRes?.description || '--'}
         navigation={navigation}
-        extraInfo={!isCurrentWorkspaceDatasetOperator ? mode => <ExtraInfo isMobile={mode === 'collapse'} relatedApps={relatedApps} /> : undefined}
+        extraInfo={mode => <ExtraInfo isMobile={mode === 'collapse'} relatedApps={relatedApps} />}
         iconType={datasetRes?.data_source_type === DataSourceType.NOTION ? 'notion' : 'dataset'}
       />}
       <DatasetDetailContext.Provider value={{

+ 1 - 10
web/app/(commonLayout)/datasets/Container.tsx

@@ -1,8 +1,7 @@
 'use client'
 
 // Libraries
-import { useEffect, useRef, useState } from 'react'
-import { useRouter } from 'next/navigation'
+import { useRef, useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import { useDebounceFn } from 'ahooks'
 import useSWR from 'swr'
@@ -23,12 +22,9 @@ import { fetchDatasetApiBaseUrl } from '@/service/datasets'
 // Hooks
 import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
 import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
-import { useAppContext } from '@/context/app-context'
 
 const Container = () => {
   const { t } = useTranslation()
-  const router = useRouter()
-  const { currentWorkspace } = useAppContext()
   const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
 
   const options = [
@@ -61,11 +57,6 @@ const Container = () => {
     handleTagsUpdate()
   }
 
-  useEffect(() => {
-    if (currentWorkspace.role === 'normal')
-      return router.replace('/apps')
-  }, [currentWorkspace])
-
   return (
     <div ref={containerRef} className='grow relative flex flex-col bg-gray-100 overflow-y-auto'>
       <div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'>

+ 11 - 17
web/app/(commonLayout)/datasets/DatasetCard.tsx

@@ -20,7 +20,6 @@ import Divider from '@/app/components/base/divider'
 import RenameDatasetModal from '@/app/components/datasets/rename-modal'
 import type { Tag } from '@/app/components/base/tag-management/constant'
 import TagSelector from '@/app/components/base/tag-management/selector'
-import { useAppContext } from '@/context/app-context'
 
 export type DatasetCardProps = {
   dataset: DataSet
@@ -33,7 +32,6 @@ const DatasetCard = ({
 }: DatasetCardProps) => {
   const { t } = useTranslation()
   const { notify } = useContext(ToastContext)
-  const { isCurrentWorkspaceDatasetOperator } = useAppContext()
   const [tags, setTags] = useState<Tag[]>(dataset.tags)
 
   const [showRenameModal, setShowRenameModal] = useState(false)
@@ -63,7 +61,7 @@ const DatasetCard = ({
     setShowConfirmDelete(false)
   }, [dataset.id, notify, onSuccess, t])
 
-  const Operations = (props: HtmlContentProps & { showDelete: boolean }) => {
+  const Operations = (props: HtmlContentProps) => {
     const onMouseLeave = async () => {
       props.onClose?.()
     }
@@ -84,19 +82,15 @@ const DatasetCard = ({
         <div className='h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer' onClick={onClickRename}>
           <span className='text-gray-700 text-sm'>{t('common.operation.settings')}</span>
         </div>
-        {props.showDelete && (
-          <>
-            <Divider className="!my-1" />
-            <div
-              className='group h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-red-50 rounded-lg cursor-pointer'
-              onClick={onClickDelete}
-            >
-              <span className={cn('text-gray-700 text-sm', 'group-hover:text-red-500')}>
-                {t('common.operation.delete')}
-              </span>
-            </div>
-          </>
-        )}
+        <Divider className="!my-1" />
+        <div
+          className='group h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-red-50 rounded-lg cursor-pointer'
+          onClick={onClickDelete}
+        >
+          <span className={cn('text-gray-700 text-sm', 'group-hover:text-red-500')}>
+            {t('common.operation.delete')}
+          </span>
+        </div>
       </div>
     )
   }
@@ -180,7 +174,7 @@ const DatasetCard = ({
           <div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-gray-200' />
           <div className='!hidden group-hover:!flex shrink-0'>
             <CustomPopover
-              htmlContent={<Operations showDelete={!isCurrentWorkspaceDatasetOperator} />}
+              htmlContent={<Operations />}
               position="br"
               trigger="click"
               btnElement={

+ 0 - 11
web/app/(commonLayout)/tools/page.tsx

@@ -1,27 +1,16 @@
 'use client'
 import type { FC } from 'react'
-import { useRouter } from 'next/navigation'
 import { useTranslation } from 'react-i18next'
 import React, { useEffect } from 'react'
 import ToolProviderList from '@/app/components/tools/provider-list'
-import { useAppContext } from '@/context/app-context'
 
 const Layout: FC = () => {
   const { t } = useTranslation()
-  const router = useRouter()
-  const { isCurrentWorkspaceDatasetOperator } = useAppContext()
 
   useEffect(() => {
     document.title = `${t('tools.title')} - Dify`
-    if (isCurrentWorkspaceDatasetOperator)
-      return router.replace('/datasets')
   }, [])
 
-  useEffect(() => {
-    if (isCurrentWorkspaceDatasetOperator)
-      return router.replace('/datasets')
-  }, [isCurrentWorkspaceDatasetOperator])
-
   return <ToolProviderList />
 }
 export default React.memo(Layout)

+ 7 - 38
web/app/components/app/configuration/dataset-config/settings-modal/index.tsx

@@ -1,6 +1,5 @@
 import type { FC } from 'react'
 import { useRef, useState } from 'react'
-import { useMount } from 'ahooks'
 import { useTranslation } from 'react-i18next'
 import { isEqual } from 'lodash-es'
 import cn from 'classnames'
@@ -11,22 +10,19 @@ import Button from '@/app/components/base/button'
 import type { DataSet } from '@/models/datasets'
 import { useToastContext } from '@/app/components/base/toast'
 import { updateDatasetSetting } from '@/service/datasets'
-import { useAppContext } from '@/context/app-context'
 import { useModalContext } from '@/context/modal-context'
 import type { RetrievalConfig } from '@/types/app'
 import RetrievalMethodConfig from '@/app/components/datasets/common/retrieval-method-config'
 import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/economical-retrieval-method-config'
 import { ensureRerankModelSelected, isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
 import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
-import PermissionSelector from '@/app/components/datasets/settings/permission-selector'
+import PermissionsRadio from '@/app/components/datasets/settings/permissions-radio'
 import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
 import {
   useModelList,
   useModelListAndDefaultModelAndCurrentProviderAndModel,
 } from '@/app/components/header/account-setting/model-provider-page/hooks'
 import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
-import { fetchMembers } from '@/service/common'
-import type { Member } from '@/models/common'
 
 type SettingsModalProps = {
   currentDataset: DataSet
@@ -59,11 +55,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
 
   const { setShowAccountSettingModal } = useModalContext()
   const [loading, setLoading] = useState(false)
-  const { isCurrentWorkspaceDatasetOperator } = useAppContext()
   const [localeCurrentDataset, setLocaleCurrentDataset] = useState({ ...currentDataset })
-  const [selectedMemberIDs, setSelectedMemberIDs] = useState<string[]>(currentDataset.partial_member_list || [])
-  const [memberList, setMemberList] = useState<Member[]>([])
-
   const [indexMethod, setIndexMethod] = useState(currentDataset.indexing_technique)
   const [retrievalConfig, setRetrievalConfig] = useState(localeCurrentDataset?.retrieval_model_dict as RetrievalConfig)
 
@@ -100,7 +92,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
     try {
       setLoading(true)
       const { id, name, description, permission } = localeCurrentDataset
-      const requestParams = {
+      await updateDatasetSetting({
         datasetId: id,
         body: {
           name,
@@ -114,16 +106,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
           embedding_model: localeCurrentDataset.embedding_model,
           embedding_model_provider: localeCurrentDataset.embedding_model_provider,
         },
-      } as any
-      if (permission === 'partial_members') {
-        requestParams.body.partial_member_list = selectedMemberIDs.map((id) => {
-          return {
-            user_id: id,
-            role: memberList.find(member => member.id === id)?.role,
-          }
-        })
-      }
-      await updateDatasetSetting(requestParams)
+      })
       notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
       onSave({
         ...localeCurrentDataset,
@@ -139,18 +122,6 @@ const SettingsModal: FC<SettingsModalProps> = ({
     }
   }
 
-  const getMembers = async () => {
-    const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} })
-    if (!accounts)
-      setMemberList([])
-    else
-      setMemberList(accounts)
-  }
-
-  useMount(() => {
-    getMembers()
-  })
-
   return (
     <div
       className='overflow-hidden w-full flex flex-col bg-white border-[0.5px] border-gray-200 rounded-xl shadow-xl'
@@ -209,13 +180,11 @@ const SettingsModal: FC<SettingsModalProps> = ({
             <div>{t('datasetSettings.form.permissions')}</div>
           </div>
           <div className='w-full'>
-            <PermissionSelector
-              disabled={!localeCurrentDataset?.embedding_available || isCurrentWorkspaceDatasetOperator}
-              permission={localeCurrentDataset.permission}
-              value={selectedMemberIDs}
+            <PermissionsRadio
+              disable={!localeCurrentDataset?.embedding_available}
+              value={localeCurrentDataset.permission}
               onChange={v => handleValueChange('permission', v!)}
-              onMemberSelect={setSelectedMemberIDs}
-              memberList={memberList}
+              itemClassName='sm:!w-[280px]'
             />
           </div>
         </div>

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 10
web/app/components/base/icons/assets/vender/solid/users/users-plus.svg


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 77
web/app/components/base/icons/src/vender/solid/users/UsersPlus.json


+ 0 - 16
web/app/components/base/icons/src/vender/solid/users/UsersPlus.tsx

@@ -1,16 +0,0 @@
-// GENERATE BY script
-// DON NOT EDIT IT MANUALLY
-
-import * as React from 'react'
-import data from './UsersPlus.json'
-import IconBase from '@/app/components/base/icons/IconBase'
-import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
-
-const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
-  props,
-  ref,
-) => <IconBase {...props} ref={ref} data={data as IconData} />)
-
-Icon.displayName = 'UsersPlus'
-
-export default Icon

+ 0 - 1
web/app/components/base/icons/src/vender/solid/users/index.ts

@@ -1,4 +1,3 @@
 export { default as User01 } from './User01'
 export { default as UserEdit02 } from './UserEdit02'
 export { default as Users01 } from './Users01'
-export { default as UsersPlus } from './UsersPlus'

+ 1 - 1
web/app/components/base/search-input/index.tsx

@@ -37,7 +37,7 @@ const SearchInput: FC<SearchInputProps> = ({
         type="text"
         name="query"
         className={cn(
-          'grow block h-[18px] bg-gray-200 border-0 text-gray-700 text-[13px] placeholder:text-gray-500 appearance-none outline-none group-hover:bg-gray-300 caret-blue-600',
+          'grow block h-[18px] bg-gray-200 rounded-md border-0 text-gray-700 text-[13px] placeholder:text-gray-500 appearance-none outline-none group-hover:bg-gray-300 caret-blue-600',
           focus && '!bg-white hover:bg-white group-hover:bg-white placeholder:!text-gray-400',
           !focus && value && 'hover:!bg-gray-200 group-hover:!bg-gray-200',
           white && '!bg-white hover:!bg-white group-hover:!bg-white placeholder:!text-gray-400',

+ 0 - 1
web/app/components/billing/type.ts

@@ -66,7 +66,6 @@ export type CurrentPlanInfoBackend = {
   docs_processing: DocumentProcessingPriority
   can_replace_logo: boolean
   model_load_balancing_enabled: boolean
-  dataset_operator_enabled: boolean
 }
 
 export type SubscriptionItem = {

+ 22 - 40
web/app/components/datasets/settings/form/index.tsx

@@ -1,33 +1,31 @@
 'use client'
-import { useState } from 'react'
-import { useMount } from 'ahooks'
+import { useEffect, useState } from 'react'
+import type { Dispatch } from 'react'
 import { useContext } from 'use-context-selector'
 import { BookOpenIcon } from '@heroicons/react/24/outline'
 import { useTranslation } from 'react-i18next'
 import cn from 'classnames'
 import { useSWRConfig } from 'swr'
 import { unstable_serialize } from 'swr/infinite'
-import PermissionSelector from '../permission-selector'
+import PermissionsRadio from '../permissions-radio'
 import IndexMethodRadio from '../index-method-radio'
 import RetrievalMethodConfig from '@/app/components/datasets/common/retrieval-method-config'
 import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/economical-retrieval-method-config'
 import { ToastContext } from '@/app/components/base/toast'
 import Button from '@/app/components/base/button'
 import { updateDatasetSetting } from '@/service/datasets'
-import type { DataSetListResponse } from '@/models/datasets'
+import type { DataSet, DataSetListResponse } from '@/models/datasets'
 import DatasetDetailContext from '@/context/dataset-detail'
 import { type RetrievalConfig } from '@/types/app'
-import { useAppContext } from '@/context/app-context'
+import { useModalContext } from '@/context/modal-context'
 import { ensureRerankModelSelected, isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
 import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
 import {
   useModelList,
   useModelListAndDefaultModelAndCurrentProviderAndModel,
 } from '@/app/components/header/account-setting/model-provider-page/hooks'
-import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
 import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
-import { fetchMembers } from '@/service/common'
-import type { Member } from '@/models/common'
+import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
 
 const rowClass = `
   flex justify-between py-4 flex-wrap gap-y-2
@@ -38,6 +36,11 @@ const labelClass = `
 const inputClass = `
   w-full max-w-[480px] px-3 bg-gray-100 text-sm text-gray-800 rounded-lg outline-none appearance-none
 `
+const useInitialValue: <T>(depend: T, dispatch: Dispatch<T>) => void = (depend, dispatch) => {
+  useEffect(() => {
+    dispatch(depend)
+  }, [depend])
+}
 
 const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => {
   if (!pageIndex || previousPageData.has_more)
@@ -49,14 +52,12 @@ const Form = () => {
   const { t } = useTranslation()
   const { notify } = useContext(ToastContext)
   const { mutate } = useSWRConfig()
-  const { isCurrentWorkspaceDatasetOperator } = useAppContext()
   const { dataset: currentDataset, mutateDatasetRes: mutateDatasets } = useContext(DatasetDetailContext)
+  const { setShowAccountSettingModal } = useModalContext()
   const [loading, setLoading] = useState(false)
   const [name, setName] = useState(currentDataset?.name ?? '')
   const [description, setDescription] = useState(currentDataset?.description ?? '')
   const [permission, setPermission] = useState(currentDataset?.permission)
-  const [selectedMemberIDs, setSelectedMemberIDs] = useState<string[]>(currentDataset?.partial_member_list || [])
-  const [memberList, setMemberList] = useState<Member[]>([])
   const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique)
   const [retrievalConfig, setRetrievalConfig] = useState(currentDataset?.retrieval_model_dict as RetrievalConfig)
   const [embeddingModel, setEmbeddingModel] = useState<DefaultModel>(
@@ -77,18 +78,6 @@ const Form = () => {
   } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
   const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
 
-  const getMembers = async () => {
-    const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} })
-    if (!accounts)
-      setMemberList([])
-    else
-      setMemberList(accounts)
-  }
-
-  useMount(() => {
-    getMembers()
-  })
-
   const handleSave = async () => {
     if (loading)
       return
@@ -115,7 +104,7 @@ const Form = () => {
     })
     try {
       setLoading(true)
-      const requestParams = {
+      await updateDatasetSetting({
         datasetId: currentDataset!.id,
         body: {
           name,
@@ -129,16 +118,7 @@ const Form = () => {
           embedding_model: embeddingModel.model,
           embedding_model_provider: embeddingModel.provider,
         },
-      } as any
-      if (permission === 'partial_members') {
-        requestParams.body.partial_member_list = selectedMemberIDs.map((id) => {
-          return {
-            user_id: id,
-            role: memberList.find(member => member.id === id)?.role,
-          }
-        })
-      }
-      await updateDatasetSetting(requestParams)
+      })
       notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
       if (mutateDatasets) {
         await mutateDatasets()
@@ -153,6 +133,11 @@ const Form = () => {
     }
   }
 
+  useInitialValue<string>(currentDataset?.name ?? '', setName)
+  useInitialValue<string>(currentDataset?.description ?? '', setDescription)
+  useInitialValue<DataSet['permission'] | undefined>(currentDataset?.permission, setPermission)
+  useInitialValue<DataSet['indexing_technique'] | undefined>(currentDataset?.indexing_technique, setIndexMethod)
+
   return (
     <div className='w-full sm:w-[800px] p-4 sm:px-16 sm:py-6'>
       <div className={rowClass}>
@@ -189,13 +174,10 @@ const Form = () => {
           <div>{t('datasetSettings.form.permissions')}</div>
         </div>
         <div className='w-full sm:w-[480px]'>
-          <PermissionSelector
-            disabled={!currentDataset?.embedding_available || isCurrentWorkspaceDatasetOperator}
-            permission={permission}
-            value={selectedMemberIDs}
+          <PermissionsRadio
+            disable={!currentDataset?.embedding_available}
+            value={permission}
             onChange={v => setPermission(v)}
-            onMemberSelect={setSelectedMemberIDs}
-            memberList={memberList}
           />
         </div>
       </div>

+ 0 - 174
web/app/components/datasets/settings/permission-selector/index.tsx

@@ -1,174 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import cn from 'classnames'
-import React, { useMemo, useState } from 'react'
-import { useDebounceFn } from 'ahooks'
-import { RiArrowDownSLine } from '@remixicon/react'
-import {
-  PortalToFollowElem,
-  PortalToFollowElemContent,
-  PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
-import Avatar from '@/app/components/base/avatar'
-import SearchInput from '@/app/components/base/search-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 { useAppContext } from '@/context/app-context'
-import type { Member } from '@/models/common'
-export type RoleSelectorProps = {
-  disabled?: boolean
-  permission?: DatasetPermission
-  value: string[]
-  memberList: Member[]
-  onChange: (permission?: DatasetPermission) => void
-  onMemberSelect: (v: string[]) => void
-}
-
-const PermissionSelector = ({ disabled, permission, value, memberList, onChange, onMemberSelect }: RoleSelectorProps) => {
-  const { t } = useTranslation()
-  const { userProfile } = useAppContext()
-  const [open, setOpen] = useState(false)
-
-  const [keywords, setKeywords] = useState('')
-  const [searchKeywords, setSearchKeywords] = useState('')
-  const { run: handleSearch } = useDebounceFn(() => {
-    setSearchKeywords(keywords)
-  }, { wait: 500 })
-  const handleKeywordsChange = (value: string) => {
-    setKeywords(value)
-    handleSearch()
-  }
-  const selectMember = (member: Member) => {
-    if (value.includes(member.id))
-      onMemberSelect(value.filter(v => v !== member.id))
-    else
-      onMemberSelect([...value, member.id])
-  }
-
-  const selectedMembers = useMemo(() => {
-    return [
-      userProfile,
-      ...memberList.filter(member => member.id !== userProfile.id).filter(member => value.includes(member.id)),
-    ].map(member => member.name).join(', ')
-  }, [userProfile, value, memberList])
-  const showMe = useMemo(() => {
-    return userProfile.name.includes(searchKeywords) || userProfile.email.includes(searchKeywords)
-  }, [searchKeywords, userProfile])
-  const filteredMemberList = useMemo(() => {
-    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])
-
-  return (
-    <PortalToFollowElem
-      open={open}
-      onOpenChange={setOpen}
-      placement='bottom-start'
-      offset={4}
-    >
-      <div className='relative'>
-        <PortalToFollowElemTrigger
-          onClick={() => !disabled && setOpen(v => !v)}
-          className='block'
-        >
-          {permission === 'only_me' && (
-            <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 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' && (
-            <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]' />
-              </div>
-              <div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('datasetSettings.form.permissionsAllMember')}</div>
-              {!disabled && <RiArrowDownSLine className='shrink-0 w-4 h-4 text-gray-700' />}
-            </div>
-          )}
-          {permission === 'partial_members' && (
-            <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]' />
-              </div>
-              <div title={selectedMembers} className='grow mr-2 text-gray-900 text-sm leading-5 truncate'>{selectedMembers}</div>
-              {!disabled && <RiArrowDownSLine className='shrink-0 w-4 h-4 text-gray-700' />}
-            </div>
-          )}
-        </PortalToFollowElemTrigger>
-        <PortalToFollowElemContent className='z-[1002]'>
-          <div className='relative w-[480px] bg-white rounded-lg border-[0.5px] bg-gray-200 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')
-                setOpen(false)
-              }}>
-                <div className='flex items-center gap-2'>
-                  <Avatar 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' />}
-                </div>
-              </div>
-              <div className='pl-3 pr-2 py-1 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
-                onChange('all_team_members')
-                setOpen(false)
-              }}>
-                <div className='flex items-center gap-2'>
-                  <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]' />
-                  </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' />}
-                </div>
-              </div>
-              <div className='pl-3 pr-2 py-1 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
-                onChange('partial_members')
-                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>
-                  <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' />}
-                </div>
-              </div>
-            </div>
-            {permission === 'partial_members' && (
-              <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'>
-                  <SearchInput white value={keywords} onChange={handleKeywordsChange} />
-                </div>
-                {showMe && (
-                  <div className='pl-3 pr-[10px] py-1 flex gap-2 items-center rounded-lg'>
-                    <Avatar name={userProfile.name} className='shrink-0' size={24} />
-                    <div className='grow'>
-                      <div className='text-[13px] text-gray-700 font-medium leading-[18px] truncate'>
-                        {userProfile.name}
-                        <span className='text-xs text-gray-500 font-normal'>{t('datasetSettings.form.me')}</span>
-                      </div>
-                      <div className='text-xs text-gray-500 leading-[18px] truncate'>{userProfile.email}</div>
-                    </div>
-                    <Check className='shrink-0 w-4 h-4 text-primary-600 opacity-30' />
-                  </div>
-                )}
-                {filteredMemberList.map(member => (
-                  <div key={member.id} className='pl-3 pr-[10px] py-1 flex gap-2 items-center rounded-lg hover:bg-gray-100 cursor-pointer' onClick={() => selectMember(member)}>
-                    <Avatar name={member.name} className='shrink-0' size={24} />
-                    <div className='grow'>
-                      <div className='text-[13px] text-gray-700 font-medium leading-[18px] truncate'>{member.name}</div>
-                      <div className='text-xs text-gray-500 leading-[18px] truncate'>{member.email}</div>
-                    </div>
-                    {value.includes(member.id) && <Check className='shrink-0 w-4 h-4 text-primary-600' />}
-                  </div>
-                ))}
-              </div>
-            )}
-          </div>
-        </PortalToFollowElemContent>
-      </div>
-    </PortalToFollowElem>
-  )
-}
-
-export default PermissionSelector

Diferenças do arquivo suprimidas por serem muito extensas
+ 7 - 0
web/app/components/datasets/settings/permissions-radio/assets/user.svg


+ 46 - 0
web/app/components/datasets/settings/permissions-radio/index.module.css

@@ -0,0 +1,46 @@
+.user-icon {
+  width: 24px;
+  height: 24px;
+  background: url(./assets/user.svg) center center;
+  background-size: contain;
+}
+
+.wrapper .item:hover {
+  background-color: #ffffff;
+  border-color: #B2CCFF;
+  box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
+}
+
+.wrapper .item-active {
+  background-color: #ffffff;
+  border-width: 1.5px;
+  border-color: #528BFF;
+  box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
+}
+
+.wrapper .item-active .radio {
+  border-width: 5px;
+  border-color: #155EEF;
+}
+
+.wrapper .item-active:hover {
+  border-width: 1.5px;
+  border-color: #528BFF;
+  box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
+}
+
+.wrapper .item.disable {
+  @apply opacity-60;
+}
+.wrapper .item-active.disable {
+  @apply opacity-60;
+}
+.wrapper .item.disable:hover {
+  @apply bg-gray-25 border border-gray-100 shadow-none cursor-default opacity-60;
+}
+.wrapper .item-active.disable:hover {
+  @apply cursor-default opacity-60;
+  border-width: 1.5px;
+  border-color: #528BFF;
+  box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
+}

+ 66 - 0
web/app/components/datasets/settings/permissions-radio/index.tsx

@@ -0,0 +1,66 @@
+'use client'
+import { useTranslation } from 'react-i18next'
+import classNames from 'classnames'
+import s from './index.module.css'
+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

+ 1 - 8
web/app/components/explore/index.tsx

@@ -1,7 +1,6 @@
 'use client'
 import type { FC } from 'react'
 import React, { useEffect, useState } from 'react'
-import { useRouter } from 'next/navigation'
 import { useTranslation } from 'react-i18next'
 import ExploreContext from '@/context/explore-context'
 import Sidebar from '@/app/components/explore/sidebar'
@@ -17,9 +16,8 @@ const Explore: FC<IExploreProps> = ({
   children,
 }) => {
   const { t } = useTranslation()
-  const router = useRouter()
   const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0)
-  const { userProfile, isCurrentWorkspaceDatasetOperator } = useAppContext()
+  const { userProfile } = useAppContext()
   const [hasEditPermission, setHasEditPermission] = useState(false)
   const [installedApps, setInstalledApps] = useState<InstalledApp[]>([])
 
@@ -34,11 +32,6 @@ const Explore: FC<IExploreProps> = ({
     })()
   }, [])
 
-  useEffect(() => {
-    if (isCurrentWorkspaceDatasetOperator)
-      return router.replace('/datasets')
-  }, [isCurrentWorkspaceDatasetOperator])
-
   return (
     <div className='flex h-full bg-gray-100 border-t border-gray-200 overflow-hidden'>
       <ExploreContext.Provider

+ 1 - 7
web/app/components/header/account-setting/index.tsx

@@ -35,7 +35,6 @@ import CustomPage from '@/app/components/custom/custom-page'
 import Modal from '@/app/components/base/modal'
 import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
 import { useProviderContext } from '@/context/provider-context'
-import { useAppContext } from '@/context/app-context'
 
 const iconClassName = `
   w-4 h-4 ml-3 mr-2
@@ -65,11 +64,8 @@ export default function AccountSetting({
   const [activeMenu, setActiveMenu] = useState(activeTab)
   const { t } = useTranslation()
   const { enableBilling, enableReplaceWebAppLogo } = useProviderContext()
-  const { isCurrentWorkspaceDatasetOperator } = useAppContext()
 
   const workplaceGroupItems = (() => {
-    if (isCurrentWorkspaceDatasetOperator)
-      return []
     return [
       {
         key: 'provider',
@@ -176,9 +172,7 @@ export default function AccountSetting({
             {
               menuItems.map(menuItem => (
                 <div key={menuItem.key} className='mb-4'>
-                  {!isCurrentWorkspaceDatasetOperator && (
-                    <div className='px-2 mb-[6px] text-[10px] sm:text-xs font-medium text-gray-500'>{menuItem.name}</div>
-                  )}
+                  <div className='px-2 mb-[6px] text-[10px] sm:text-xs font-medium text-gray-500'>{menuItem.name}</div>
                   <div>
                     {
                       menuItem.items.map(item => (

+ 0 - 1
web/app/components/header/account-setting/members-page/index.tsx

@@ -29,7 +29,6 @@ const MembersPage = () => {
     owner: t('common.members.owner'),
     admin: t('common.members.admin'),
     editor: t('common.members.editor'),
-    dataset_operator: t('common.members.datasetOperator'),
     normal: t('common.members.normal'),
   }
   const { locale } = useContext(I18n)

+ 67 - 7
web/app/components/header/account-setting/members-page/invite-modal/index.tsx

@@ -1,12 +1,13 @@
 'use client'
-import { useCallback, useState } from 'react'
+import { Fragment, useCallback, useMemo, useState } from 'react'
 import { useContext } from 'use-context-selector'
 import { XMarkIcon } from '@heroicons/react/24/outline'
 import { useTranslation } from 'react-i18next'
 import { ReactMultiEmail } from 'react-multi-email'
+import { Listbox, Transition } from '@headlessui/react'
+import { CheckIcon } from '@heroicons/react/20/solid'
 import cn from 'classnames'
 import s from './index.module.css'
-import RoleSelector from './role-selector'
 import Modal from '@/app/components/base/modal'
 import Button from '@/app/components/base/button'
 import { inviteMember } from '@/service/common'
@@ -30,14 +31,29 @@ const InviteModal = ({
   const { notify } = useContext(ToastContext)
 
   const { locale } = useContext(I18n)
-  const [role, setRole] = useState<string>('normal')
+
+  const InvitingRoles = useMemo(() => [
+    {
+      name: 'normal',
+      description: t('common.members.normalTip'),
+    },
+    {
+      name: 'editor',
+      description: t('common.members.editorTip'),
+    },
+    {
+      name: 'admin',
+      description: t('common.members.adminTip'),
+    },
+  ], [t])
+  const [role, setRole] = useState(InvitingRoles[0])
 
   const handleSend = useCallback(async () => {
     if (emails.map((email: string) => emailRegex.test(email)).every(Boolean)) {
       try {
         const { result, invitation_results } = await inviteMember({
           url: '/workspaces/current/members/invite-email',
-          body: { emails, role, language: locale },
+          body: { emails, role: role.name, language: locale },
         })
 
         if (result === 'success') {
@@ -83,9 +99,53 @@ const InviteModal = ({
               placeholder={t('common.members.emailPlaceholder') || ''}
             />
           </div>
-          <div className='mb-6'>
-            <RoleSelector value={role} onChange={setRole} />
-          </div>
+          <Listbox value={role} onChange={setRole}>
+            <div className="relative pb-6">
+              <Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-gray-100 outline-none border-none appearance-none text-sm text-gray-900 rounded-lg">
+                <span className="block truncate capitalize">{t('common.members.invitedAsRole', { role: t(`common.members.${role.name}`) })}</span>
+              </Listbox.Button>
+              <Transition
+                as={Fragment}
+                leave="transition ease-in duration-200"
+                leaveFrom="opacity-200"
+                leaveTo="opacity-0"
+              >
+                <Listbox.Options className="absolute w-full py-1 my-2 overflow-auto text-base bg-white rounded-md shadow-lg max-h-60 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
+                  {InvitingRoles.map(role =>
+                    <Listbox.Option
+                      key={role.name}
+                      className={({ active }) =>
+                        `${active ? ' bg-gray-50 rounded-xl' : ' bg-transparent'}
+                          cursor-default select-none relative py-2 px-4 mx-2 flex flex-col`
+                      }
+                      value={role}
+                    >
+                      {({ selected }) => (
+                        <div className='flex flex-row'>
+                          <span
+                            className={cn(
+                              'text-indigo-600 mr-2',
+                              'flex items-center',
+                            )}
+                          >
+                            {selected && (<CheckIcon className="h-5 w-5" aria-hidden="true" />)}
+                          </span>
+                          <div className=' flex flex-col flex-grow'>
+                            <span className={`${selected ? 'font-medium' : 'font-normal'} capitalize block truncate`}>
+                              {t(`common.members.${role.name}`)}
+                            </span>
+                            <span className={`${selected ? 'font-medium' : 'font-normal'} capitalize block text-gray-500`}>
+                              {role.description}
+                            </span>
+                          </div>
+                        </div>
+                      )}
+                    </Listbox.Option>,
+                  )}
+                </Listbox.Options>
+              </Transition>
+            </div>
+          </Listbox>
           <Button
             tabIndex={0}
             className='w-full'

+ 0 - 95
web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx

@@ -1,95 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import cn from 'classnames'
-import React, { useState } from 'react'
-import { RiArrowDownSLine } from '@remixicon/react'
-import { useProviderContext } from '@/context/provider-context'
-import {
-  PortalToFollowElem,
-  PortalToFollowElemContent,
-  PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
-import { Check } from '@/app/components/base/icons/src/vender/line/general'
-
-export type RoleSelectorProps = {
-  value: string
-  onChange: (role: string) => void
-}
-
-const RoleSelector = ({ value, onChange }: RoleSelectorProps) => {
-  const { t } = useTranslation()
-  const [open, setOpen] = useState(false)
-  const { datasetOperatorEnabled } = useProviderContext()
-
-  const toHump = (name: string) => name.replace(/_(\w)/g, (all, letter) => letter.toUpperCase())
-
-  return (
-    <PortalToFollowElem
-      open={open}
-      onOpenChange={setOpen}
-      placement='bottom-start'
-      offset={4}
-    >
-      <div className='relative'>
-        <PortalToFollowElemTrigger
-          onClick={() => setOpen(v => !v)}
-          className='block'
-        >
-          <div className={cn('flex items-center px-3 py-2 rounded-lg bg-gray-100 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
-            <div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('common.members.invitedAsRole', { role: t(`common.members.${toHump(value)}`) })}</div>
-            <RiArrowDownSLine className='shrink-0 w-4 h-4 text-gray-700' />
-          </div>
-        </PortalToFollowElemTrigger>
-        <PortalToFollowElemContent className='z-[1002]'>
-          <div className='relative w-[336px] bg-white rounded-lg border-[0.5px] bg-gray-200 shadow-lg'>
-            <div className='p-1'>
-              <div className='p-2 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
-                onChange('normal')
-                setOpen(false)
-              }}>
-                <div className='relative pl-5'>
-                  <div className='text-gray-700 text-sm leading-5'>{t('common.members.normal')}</div>
-                  <div className='text-gray-500 text-xs leading-[18px]'>{t('common.members.normalTip')}</div>
-                  {value === 'normal' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-primary-600'/>}
-                </div>
-              </div>
-              <div className='p-2 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
-                onChange('editor')
-                setOpen(false)
-              }}>
-                <div className='relative pl-5'>
-                  <div className='text-gray-700 text-sm leading-5'>{t('common.members.editor')}</div>
-                  <div className='text-gray-500 text-xs leading-[18px]'>{t('common.members.editorTip')}</div>
-                  {value === 'editor' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-primary-600'/>}
-                </div>
-              </div>
-              <div className='p-2 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
-                onChange('admin')
-                setOpen(false)
-              }}>
-                <div className='relative pl-5'>
-                  <div className='text-gray-700 text-sm leading-5'>{t('common.members.admin')}</div>
-                  <div className='text-gray-500 text-xs leading-[18px]'>{t('common.members.adminTip')}</div>
-                  {value === 'admin' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-primary-600'/>}
-                </div>
-              </div>
-              {datasetOperatorEnabled && (
-                <div className='p-2 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => {
-                  onChange('dataset_operator')
-                  setOpen(false)
-                }}>
-                  <div className='relative pl-5'>
-                    <div className='text-gray-700 text-sm leading-5'>{t('common.members.datasetOperator')}</div>
-                    <div className='text-gray-500 text-xs leading-[18px]'>{t('common.members.datasetOperatorTip')}</div>
-                    {value === 'dataset_operator' && <Check className='absolute top-0.5 left-0 w-4 h-4 text-primary-600'/>}
-                  </div>
-                </div>
-              )}
-            </div>
-          </div>
-        </PortalToFollowElemContent>
-      </div>
-    </PortalToFollowElem>
-  )
-}
-
-export default RoleSelector

+ 4 - 14
web/app/components/header/account-setting/members-page/operation/index.tsx

@@ -1,12 +1,11 @@
 'use client'
 import { useTranslation } from 'react-i18next'
-import { Fragment, useMemo } from 'react'
+import { Fragment } from 'react'
 import { useContext } from 'use-context-selector'
 import { Menu, Transition } from '@headlessui/react'
 import cn from 'classnames'
 import { CheckIcon, ChevronDownIcon } from '@heroicons/react/24/outline'
 import s from './index.module.css'
-import { useProviderContext } from '@/context/provider-context'
 import type { Member } from '@/models/common'
 import { deleteMemberOrCancelInvitation, updateMemberRole } from '@/service/common'
 import { ToastContext } from '@/app/components/base/toast'
@@ -34,22 +33,13 @@ const Operation = ({
   onOperate,
 }: IOperationProps) => {
   const { t } = useTranslation()
-  const { datasetOperatorEnabled } = useProviderContext()
   const RoleMap = {
     owner: t('common.members.owner'),
     admin: t('common.members.admin'),
     editor: t('common.members.editor'),
     normal: t('common.members.normal'),
-    dataset_operator: t('common.members.datasetOperator'),
   }
-  const roleList = useMemo(() => {
-    return [
-      ...['admin', 'editor', 'normal'],
-      ...(datasetOperatorEnabled ? ['dataset_operator'] : []),
-    ]
-  }, [datasetOperatorEnabled])
   const { notify } = useContext(ToastContext)
-  const toHump = (name: string) => name.replace(/_(\w)/g, (all, letter) => letter.toUpperCase())
   const handleDeleteMemberOrCancelInvitation = async () => {
     try {
       await deleteMemberOrCancelInvitation({ url: `/workspaces/current/members/${member.id}` })
@@ -109,7 +99,7 @@ const Operation = ({
               >
                 <div className="px-1 py-1">
                   {
-                    roleList.map(role => (
+                    ['admin', 'editor', 'normal'].map(role => (
                       <Menu.Item key={role}>
                         <div className={itemClassName} onClick={() => handleUpdateMemberRole(role)}>
                           {
@@ -118,8 +108,8 @@ const Operation = ({
                               : <div className={itemIconClassName} />
                           }
                           <div>
-                            <div className={itemTitleClassName}>{t(`common.members.${toHump(role)}`)}</div>
-                            <div className={itemDescClassName}>{t(`common.members.${toHump(role)}Tip`)}</div>
+                            <div className={itemTitleClassName}>{t(`common.members.${role}`)}</div>
+                            <div className={itemDescClassName}>{t(`common.members.${role}Tip`)}</div>
                           </div>
                         </div>
                       </Menu.Item>

+ 9 - 9
web/app/components/header/index.tsx

@@ -26,7 +26,7 @@ const navClassName = `
 `
 
 const Header = () => {
-  const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext()
+  const { isCurrentWorkspaceEditor } = useAppContext()
 
   const selectedSegment = useSelectedLayoutSegment()
   const media = useBreakpoints()
@@ -72,10 +72,10 @@ const Header = () => {
       )}
       {!isMobile && (
         <div className='flex items-center'>
-          {!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />}
-          {!isCurrentWorkspaceDatasetOperator && <AppNav />}
-          {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
-          {!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
+          <ExploreNav className={navClassName} />
+          <AppNav />
+          {isCurrentWorkspaceEditor && <DatasetNav />}
+          <ToolsNav className={navClassName} />
         </div>
       )}
       <div className='flex items-center flex-shrink-0'>
@@ -91,10 +91,10 @@ const Header = () => {
       </div>
       {(isMobile && isShowNavMenu) && (
         <div className='w-full flex flex-col p-2 gap-y-1'>
-          {!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />}
-          {!isCurrentWorkspaceDatasetOperator && <AppNav />}
-          {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
-          {!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
+          <ExploreNav className={navClassName} />
+          <AppNav />
+          {isCurrentWorkspaceEditor && <DatasetNav />}
+          <ToolsNav className={navClassName} />
         </div>
       )}
     </div>

+ 1 - 1
web/app/components/header/nav/nav-selector/index.tsx

@@ -113,7 +113,7 @@ const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }:
                   ))
                 }
               </div>
-              {!isApp && isCurrentWorkspaceEditor && (
+              {!isApp && (
                 <Menu.Button className='p-1 w-full'>
                   <div onClick={() => onCreate('')} className={cn(
                     'flex items-center gap-2 px-3 py-[6px] rounded-lg cursor-pointer hover:bg-gray-100',

+ 0 - 4
web/context/app-context.tsx

@@ -20,7 +20,6 @@ export type AppContextValue = {
   isCurrentWorkspaceManager: boolean
   isCurrentWorkspaceOwner: boolean
   isCurrentWorkspaceEditor: boolean
-  isCurrentWorkspaceDatasetOperator: boolean
   mutateCurrentWorkspace: VoidFunction
   pageContainerRef: React.RefObject<HTMLDivElement>
   langeniusVersionInfo: LangGeniusVersionResponse
@@ -62,7 +61,6 @@ const AppContext = createContext<AppContextValue>({
   isCurrentWorkspaceManager: false,
   isCurrentWorkspaceOwner: false,
   isCurrentWorkspaceEditor: false,
-  isCurrentWorkspaceDatasetOperator: false,
   mutateUserProfile: () => { },
   mutateCurrentWorkspace: () => { },
   pageContainerRef: createRef(),
@@ -91,7 +89,6 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
   const isCurrentWorkspaceManager = useMemo(() => ['owner', 'admin'].includes(currentWorkspace.role), [currentWorkspace.role])
   const isCurrentWorkspaceOwner = useMemo(() => currentWorkspace.role === 'owner', [currentWorkspace.role])
   const isCurrentWorkspaceEditor = useMemo(() => ['owner', 'admin', 'editor'].includes(currentWorkspace.role), [currentWorkspace.role])
-  const isCurrentWorkspaceDatasetOperator = useMemo(() => currentWorkspace.role === 'dataset_operator', [currentWorkspace.role])
   const updateUserProfileAndVersion = useCallback(async () => {
     if (userProfileResponse && !userProfileResponse.bodyUsed) {
       const result = await userProfileResponse.json()
@@ -128,7 +125,6 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
       isCurrentWorkspaceManager,
       isCurrentWorkspaceOwner,
       isCurrentWorkspaceEditor,
-      isCurrentWorkspaceDatasetOperator,
       mutateCurrentWorkspace,
     }}>
       <div className='flex flex-col h-full overflow-y-auto'>

+ 0 - 8
web/context/provider-context.tsx

@@ -34,7 +34,6 @@ type ProviderContextState = {
   onPlanInfoChanged: () => void
   enableReplaceWebAppLogo: boolean
   modelLoadBalancingEnabled: boolean
-  datasetOperatorEnabled: boolean
 }
 const ProviderContext = createContext<ProviderContextState>({
   modelProviders: [],
@@ -48,14 +47,12 @@ const ProviderContext = createContext<ProviderContextState>({
       buildApps: 12,
       teamMembers: 1,
       annotatedResponse: 1,
-      documentsUploadQuota: 50,
     },
     total: {
       vectorSpace: 200,
       buildApps: 50,
       teamMembers: 1,
       annotatedResponse: 10,
-      documentsUploadQuota: 500,
     },
   },
   isFetchedPlan: false,
@@ -63,7 +60,6 @@ const ProviderContext = createContext<ProviderContextState>({
   onPlanInfoChanged: () => { },
   enableReplaceWebAppLogo: false,
   modelLoadBalancingEnabled: false,
-  datasetOperatorEnabled: false,
 })
 
 export const useProviderContext = () => useContext(ProviderContext)
@@ -90,7 +86,6 @@ export const ProviderContextProvider = ({
   const [enableBilling, setEnableBilling] = useState(true)
   const [enableReplaceWebAppLogo, setEnableReplaceWebAppLogo] = useState(false)
   const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false)
-  const [datasetOperatorEnabled, setDatasetOperatorEnabled] = useState(false)
 
   const fetchPlan = async () => {
     const data = await fetchCurrentPlanInfo()
@@ -103,8 +98,6 @@ export const ProviderContextProvider = ({
     }
     if (data.model_load_balancing_enabled)
       setModelLoadBalancingEnabled(true)
-    if (data.dataset_operator_enabled)
-      setDatasetOperatorEnabled(true)
   }
   useEffect(() => {
     fetchPlan()
@@ -122,7 +115,6 @@ export const ProviderContextProvider = ({
       onPlanInfoChanged: fetchPlan,
       enableReplaceWebAppLogo,
       modelLoadBalancingEnabled,
-      datasetOperatorEnabled,
     }}>
       {children}
     </ProviderContext.Provider>

+ 0 - 2
web/i18n/en-US/common.ts

@@ -181,8 +181,6 @@ const translation = {
     builderTip: 'Can build & edit own apps',
     editor: 'Editor',
     editorTip: 'Can build & edit apps',
-    datasetOperator: 'Knowledge Admin',
-    datasetOperatorTip: 'Only can manage the knowledge base',
     inviteTeamMember: 'Add team member',
     inviteTeamMemberTip: 'They can access your team data directly after signing in.',
     email: 'Email',

+ 0 - 2
web/i18n/en-US/dataset-settings.ts

@@ -12,8 +12,6 @@ const translation = {
     permissions: 'Permissions',
     permissionsOnlyMe: 'Only me',
     permissionsAllMember: 'All team members',
-    permissionsInvitedMembers: 'Partial team members',
-    me: '(You)',
     indexMethod: 'Index Method',
     indexMethodHighQuality: 'High Quality',
     indexMethodHighQualityTip: 'Call Embedding model for processing to provide higher accuracy when users query.',

+ 0 - 2
web/i18n/zh-Hans/common.ts

@@ -179,8 +179,6 @@ const translation = {
     normalTip: '只能使用应用程序,不能建立应用程序',
     editor: '编辑',
     editorTip: '能够建立并编辑应用程序,不能管理团队设置',
-    datasetOperator: '知识库管理员',
-    datasetOperatorTip: '只能管理知识库',
     inviteTeamMember: '添加团队成员',
     inviteTeamMemberTip: '对方在登录后可以访问你的团队数据。',
     email: '邮箱',

+ 0 - 2
web/i18n/zh-Hans/dataset-settings.ts

@@ -12,8 +12,6 @@ const translation = {
     permissions: '可见权限',
     permissionsOnlyMe: '只有我',
     permissionsAllMember: '所有团队成员',
-    permissionsInvitedMembers: '部分团队成员',
-    me: '(你)',
     indexMethod: '索引模式',
     indexMethodHighQuality: '高质量',
     indexMethodHighQualityTip: '调用 Embedding 模型进行处理,以在用户查询时提供更高的准确度。',

+ 2 - 2
web/models/common.ts

@@ -65,7 +65,7 @@ export type TenantInfoResponse = {
 export type Member = Pick<UserProfileResponse, 'id' | 'name' | 'email' | 'last_login_at' | 'last_active_at' | 'created_at'> & {
   avatar: string
   status: 'pending' | 'active' | 'banned' | 'closed'
-  role: 'owner' | 'admin' | 'editor' | 'normal' | 'dataset_operator'
+  role: 'owner' | 'admin' | 'editor' | 'normal'
 }
 
 export enum ProviderName {
@@ -126,7 +126,7 @@ export type IWorkspace = {
 }
 
 export type ICurrentWorkspace = Omit<IWorkspace, 'current'> & {
-  role: 'owner' | 'admin' | 'editor' | 'dataset_operator' | 'normal'
+  role: 'owner' | 'admin' | 'editor' | 'normal'
   providers: Provider[]
   in_trail: boolean
   trial_end_reason?: string

+ 1 - 4
web/models/datasets.ts

@@ -8,15 +8,13 @@ export enum DataSourceType {
   WEB = 'website_crawl',
 }
 
-export type DatasetPermission = 'only_me' | 'all_team_members' | 'partial_members'
-
 export type DataSet = {
   id: string
   name: string
   icon: string
   icon_background: string
   description: string
-  permission: DatasetPermission
+  permission: 'only_me' | 'all_team_members'
   data_source_type: DataSourceType
   indexing_technique: 'high_quality' | 'economy'
   created_by: string
@@ -31,7 +29,6 @@ export type DataSet = {
   retrieval_model_dict: RetrievalConfig
   retrieval_model: RetrievalConfig
   tags: Tag[]
-  partial_member_list?: any[]
 }
 
 export type CustomFile = File & {

+ 1 - 1
web/service/datasets.ts

@@ -53,7 +53,7 @@ export const fetchDatasetDetail: Fetcher<DataSet, string> = (datasetId: string)
 export const updateDatasetSetting: Fetcher<DataSet, {
   datasetId: string
   body: Partial<Pick<DataSet,
-    'name' | 'description' | 'permission' | 'partial_member_list' | 'indexing_technique' | 'retrieval_model' | 'embedding_model' | 'embedding_model_provider'
+    'name' | 'description' | 'permission' | 'indexing_technique' | 'retrieval_model' | 'embedding_model' | 'embedding_model_provider'
   >>
 }> = ({ datasetId, body }) => {
   return patch<DataSet>(`/datasets/${datasetId}`, { body })