Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: crazywoola <427733928@qq.com>tags/0.6.11
| @@ -68,8 +68,8 @@ class AppListApi(Resource): | |||
| parser.add_argument('icon_background', type=str, location='json') | |||
| args = parser.parse_args() | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # The role of the current user in the ta table must be admin, owner, or editor | |||
| if not current_user.is_editor: | |||
| raise Forbidden() | |||
| if 'mode' not in args or args['mode'] is None: | |||
| @@ -89,8 +89,8 @@ class AppImportApi(Resource): | |||
| @cloud_edition_billing_resource_check('apps') | |||
| def post(self): | |||
| """Import app""" | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # 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() | |||
| @@ -147,7 +147,7 @@ class AppApi(Resource): | |||
| @get_app_model | |||
| def delete(self, app_model): | |||
| """Delete app""" | |||
| if not current_user.is_admin_or_owner: | |||
| if not current_user.is_editor: | |||
| raise Forbidden() | |||
| app_service = AppService() | |||
| @@ -164,8 +164,8 @@ class AppCopyApi(Resource): | |||
| @marshal_with(app_detail_fields_with_site) | |||
| def post(self, app_model): | |||
| """Copy app""" | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # 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() | |||
| @@ -238,6 +238,9 @@ class AppSiteStatus(Resource): | |||
| @get_app_model | |||
| @marshal_with(app_detail_fields) | |||
| def post(self, app_model): | |||
| # 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() | |||
| parser.add_argument('enable_site', type=bool, required=True, location='json') | |||
| args = parser.parse_args() | |||
| @@ -255,6 +258,9 @@ class AppApiStatus(Resource): | |||
| @get_app_model | |||
| @marshal_with(app_detail_fields) | |||
| def post(self, app_model): | |||
| # 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() | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument('enable_api', type=bool, required=True, location='json') | |||
| args = parser.parse_args() | |||
| @@ -6,7 +6,7 @@ from flask_restful import Resource, marshal_with, reqparse | |||
| from flask_restful.inputs import int_range | |||
| from sqlalchemy import func, or_ | |||
| from sqlalchemy.orm import joinedload | |||
| from werkzeug.exceptions import NotFound | |||
| from werkzeug.exceptions import Forbidden, NotFound | |||
| from controllers.console import api | |||
| from controllers.console.app.wraps import get_app_model | |||
| @@ -33,6 +33,8 @@ class CompletionConversationApi(Resource): | |||
| @get_app_model(mode=AppMode.COMPLETION) | |||
| @marshal_with(conversation_pagination_fields) | |||
| def get(self, app_model): | |||
| if not current_user.is_admin_or_owner: | |||
| raise Forbidden() | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument('keyword', type=str, location='args') | |||
| parser.add_argument('start', type=datetime_string('%Y-%m-%d %H:%M'), location='args') | |||
| @@ -106,6 +108,8 @@ class CompletionConversationDetailApi(Resource): | |||
| @get_app_model(mode=AppMode.COMPLETION) | |||
| @marshal_with(conversation_message_detail_fields) | |||
| def get(self, app_model, conversation_id): | |||
| if not current_user.is_admin_or_owner: | |||
| raise Forbidden() | |||
| conversation_id = str(conversation_id) | |||
| return _get_conversation(app_model, conversation_id) | |||
| @@ -115,6 +119,8 @@ class CompletionConversationDetailApi(Resource): | |||
| @account_initialization_required | |||
| @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT]) | |||
| def delete(self, app_model, conversation_id): | |||
| if not current_user.is_admin_or_owner: | |||
| raise Forbidden() | |||
| conversation_id = str(conversation_id) | |||
| conversation = db.session.query(Conversation) \ | |||
| @@ -137,6 +143,8 @@ class ChatConversationApi(Resource): | |||
| @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT]) | |||
| @marshal_with(conversation_with_summary_pagination_fields) | |||
| def get(self, app_model): | |||
| if not current_user.is_admin_or_owner: | |||
| raise Forbidden() | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument('keyword', type=str, location='args') | |||
| parser.add_argument('start', type=datetime_string('%Y-%m-%d %H:%M'), location='args') | |||
| @@ -225,6 +233,8 @@ class ChatConversationDetailApi(Resource): | |||
| @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT]) | |||
| @marshal_with(conversation_detail_fields) | |||
| def get(self, app_model, conversation_id): | |||
| if not current_user.is_admin_or_owner: | |||
| raise Forbidden() | |||
| conversation_id = str(conversation_id) | |||
| return _get_conversation(app_model, conversation_id) | |||
| @@ -234,6 +244,8 @@ class ChatConversationDetailApi(Resource): | |||
| @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT]) | |||
| @account_initialization_required | |||
| def delete(self, app_model, conversation_id): | |||
| if not current_user.is_admin_or_owner: | |||
| raise Forbidden() | |||
| conversation_id = str(conversation_id) | |||
| conversation = db.session.query(Conversation) \ | |||
| @@ -40,8 +40,8 @@ class AppSite(Resource): | |||
| def post(self, app_model): | |||
| args = parse_app_site_args() | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # The role of the current user in the ta table must be editor, admin, or owner | |||
| if not current_user.is_editor: | |||
| raise Forbidden() | |||
| site = db.session.query(Site). \ | |||
| @@ -107,8 +107,8 @@ class DatasetListApi(Resource): | |||
| help='Invalid indexing technique.') | |||
| args = parser.parse_args() | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # The role of the current user in the ta table must be admin, owner, or editor | |||
| if not current_user.is_editor: | |||
| raise Forbidden() | |||
| try: | |||
| @@ -195,8 +195,8 @@ class DatasetApi(Resource): | |||
| parser.add_argument('retrieval_model', type=dict, location='json', help='Invalid retrieval model.') | |||
| args = parser.parse_args() | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # 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( | |||
| @@ -213,8 +213,8 @@ class DatasetApi(Resource): | |||
| def delete(self, dataset_id): | |||
| dataset_id_str = str(dataset_id) | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # The role of the current user in the ta table must be admin, owner, or editor | |||
| if not current_user.is_editor: | |||
| raise Forbidden() | |||
| try: | |||
| @@ -226,8 +226,8 @@ class DatasetDocumentListApi(Resource): | |||
| if not dataset: | |||
| raise NotFound('Dataset not found.') | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # The role of the current user in the ta table must be admin, owner, or editor | |||
| if not current_user.is_editor: | |||
| raise Forbidden() | |||
| try: | |||
| @@ -278,8 +278,8 @@ class DatasetInitApi(Resource): | |||
| @marshal_with(dataset_and_document_fields) | |||
| @cloud_edition_billing_resource_check('vector_space') | |||
| def post(self): | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # 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() | |||
| @@ -632,8 +632,8 @@ class DocumentProcessingApi(DocumentResource): | |||
| document_id = str(document_id) | |||
| document = self.get_document(dataset_id, document_id) | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # The role of the current user in the ta table must be admin, owner, or editor | |||
| if not current_user.is_editor: | |||
| raise Forbidden() | |||
| if action == "pause": | |||
| @@ -696,8 +696,8 @@ class DocumentMetadataApi(DocumentResource): | |||
| doc_type = req_data.get('doc_type') | |||
| doc_metadata = req_data.get('doc_metadata') | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # The role of the current user in the ta table must be admin, owner, or editor | |||
| if not current_user.is_editor: | |||
| raise Forbidden() | |||
| if doc_type is None or doc_metadata is None: | |||
| @@ -743,8 +743,8 @@ class DocumentStatusApi(DocumentResource): | |||
| document = self.get_document(dataset_id, document_id) | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # 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) | |||
| @@ -126,8 +126,8 @@ class DatasetDocumentSegmentApi(Resource): | |||
| raise NotFound('Dataset not found.') | |||
| # check user's model setting | |||
| DatasetService.check_dataset_model_setting(dataset) | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # The role of the current user in the ta table must be admin, owner, or editor | |||
| if not current_user.is_editor: | |||
| raise Forbidden() | |||
| try: | |||
| @@ -302,8 +302,8 @@ class DatasetDocumentSegmentUpdateApi(Resource): | |||
| ).first() | |||
| if not segment: | |||
| raise NotFound('Segment not found.') | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # The role of the current user in the ta table must be admin, owner, or editor | |||
| if not current_user.is_editor: | |||
| raise Forbidden() | |||
| try: | |||
| DatasetService.check_dataset_permission(dataset, current_user) | |||
| @@ -35,8 +35,8 @@ class TagListApi(Resource): | |||
| @login_required | |||
| @account_initialization_required | |||
| def post(self): | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # 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() | |||
| @@ -67,8 +67,8 @@ class TagUpdateDeleteApi(Resource): | |||
| @account_initialization_required | |||
| def patch(self, tag_id): | |||
| tag_id = str(tag_id) | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # 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() | |||
| @@ -94,8 +94,8 @@ class TagUpdateDeleteApi(Resource): | |||
| @account_initialization_required | |||
| def delete(self, tag_id): | |||
| tag_id = str(tag_id) | |||
| # The role of the current user in the ta table must be admin or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # The role of the current user in the ta table must be admin, owner, or editor | |||
| if not current_user.is_editor: | |||
| raise Forbidden() | |||
| TagService.delete_tag(tag_id) | |||
| @@ -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 or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # 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 or owner | |||
| if not current_user.is_admin_or_owner: | |||
| # 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() | |||
| @@ -43,7 +43,7 @@ class MemberInviteEmailApi(Resource): | |||
| invitee_emails = args['emails'] | |||
| invitee_role = args['role'] | |||
| interface_language = args['language'] | |||
| if invitee_role not in [TenantAccountRole.ADMIN, TenantAccountRole.NORMAL]: | |||
| if not TenantAccountRole.is_non_owner_role(invitee_role): | |||
| return {'code': 'invalid-role', 'message': 'Invalid role'}, 400 | |||
| inviter = current_user | |||
| @@ -114,7 +114,7 @@ class MemberUpdateRoleApi(Resource): | |||
| args = parser.parse_args() | |||
| new_role = args['role'] | |||
| if new_role not in ['admin', 'normal', 'owner']: | |||
| if not TenantAccountRole.is_valid_role(new_role): | |||
| return {'code': 'invalid-role', 'message': 'Invalid role'}, 400 | |||
| member = Account.query.get(str(member_id)) | |||
| @@ -11,7 +11,6 @@ from core.model_runtime.entities.model_entities import ModelType | |||
| from core.model_runtime.errors.validate import CredentialsValidateFailedError | |||
| from core.model_runtime.utils.encoders import jsonable_encoder | |||
| from libs.login import login_required | |||
| from models.account import TenantAccountRole | |||
| from services.model_load_balancing_service import ModelLoadBalancingService | |||
| from services.model_provider_service import ModelProviderService | |||
| @@ -43,6 +42,9 @@ class DefaultModelApi(Resource): | |||
| @login_required | |||
| @account_initialization_required | |||
| def post(self): | |||
| if not current_user.is_admin_or_owner: | |||
| raise Forbidden() | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument('model_settings', type=list, required=True, nullable=False, location='json') | |||
| args = parser.parse_args() | |||
| @@ -96,7 +98,7 @@ class ModelProviderModelApi(Resource): | |||
| @login_required | |||
| @account_initialization_required | |||
| def post(self, provider: str): | |||
| if not TenantAccountRole.is_privileged_role(current_user.current_tenant.current_role): | |||
| if not current_user.is_admin_or_owner: | |||
| raise Forbidden() | |||
| tenant_id = current_user.current_tenant_id | |||
| @@ -162,7 +164,7 @@ class ModelProviderModelApi(Resource): | |||
| @login_required | |||
| @account_initialization_required | |||
| def delete(self, provider: str): | |||
| if not TenantAccountRole.is_privileged_role(current_user.current_tenant.current_role): | |||
| if not current_user.is_admin_or_owner: | |||
| raise Forbidden() | |||
| tenant_id = current_user.current_tenant_id | |||
| @@ -106,6 +106,9 @@ class Account(UserMixin, db.Model): | |||
| def is_admin_or_owner(self): | |||
| return TenantAccountRole.is_privileged_role(self._current_tenant.current_role) | |||
| @property | |||
| def is_editor(self): | |||
| return TenantAccountRole.is_editing_role(self._current_tenant.current_role) | |||
| class TenantStatus(str, enum.Enum): | |||
| NORMAL = 'normal' | |||
| @@ -115,11 +118,24 @@ class TenantStatus(str, enum.Enum): | |||
| class TenantAccountRole(str, enum.Enum): | |||
| OWNER = 'owner' | |||
| ADMIN = 'admin' | |||
| EDITOR = 'editor' | |||
| NORMAL = 'normal' | |||
| @staticmethod | |||
| def is_valid_role(role: str) -> bool: | |||
| return role and role in {TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR, TenantAccountRole.NORMAL} | |||
| @staticmethod | |||
| def is_privileged_role(role: str) -> bool: | |||
| return role and role in {TenantAccountRole.ADMIN, TenantAccountRole.OWNER} | |||
| return role and role in {TenantAccountRole.OWNER, TenantAccountRole.ADMIN} | |||
| @staticmethod | |||
| def is_non_owner_role(role: str) -> bool: | |||
| 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} | |||
| class Tenant(db.Model): | |||
| @@ -4,9 +4,11 @@ from models.account import TenantAccountRole | |||
| def test_account_is_privileged_role() -> None: | |||
| assert TenantAccountRole.ADMIN == 'admin' | |||
| assert TenantAccountRole.OWNER == 'owner' | |||
| assert TenantAccountRole.EDITOR == 'editor' | |||
| assert TenantAccountRole.NORMAL == 'normal' | |||
| assert TenantAccountRole.is_privileged_role(TenantAccountRole.ADMIN) | |||
| assert TenantAccountRole.is_privileged_role(TenantAccountRole.OWNER) | |||
| assert not TenantAccountRole.is_privileged_role(TenantAccountRole.NORMAL) | |||
| assert not TenantAccountRole.is_privileged_role(TenantAccountRole.EDITOR) | |||
| assert not TenantAccountRole.is_privileged_role('') | |||
| @@ -32,7 +32,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => { | |||
| const pathname = usePathname() | |||
| const media = useBreakpoints() | |||
| const isMobile = media === MediaType.mobile | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { isCurrentWorkspaceManager, isCurrentWorkspaceEditor } = useAppContext() | |||
| const { appDetail, setAppDetail, setAppSiderbarExpand } = useStore(useShallow(state => ({ | |||
| appDetail: state.appDetail, | |||
| setAppDetail: state.setAppDetail, | |||
| @@ -45,9 +45,9 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => { | |||
| selectedIcon: NavIcon | |||
| }>>([]) | |||
| const getNavigations = useCallback((appId: string, isCurrentWorkspaceManager: boolean, mode: string) => { | |||
| const getNavigations = useCallback((appId: string, isCurrentWorkspaceManager: boolean, isCurrentWorkspaceEditor: boolean, mode: string) => { | |||
| const navs = [ | |||
| ...(isCurrentWorkspaceManager | |||
| ...(isCurrentWorkspaceEditor | |||
| ? [{ | |||
| name: t('common.appMenus.promptEng'), | |||
| href: `/app/${appId}/${(mode === 'workflow' || mode === 'advanced-chat') ? 'workflow' : 'configuration'}`, | |||
| @@ -62,14 +62,17 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => { | |||
| icon: TerminalSquare, | |||
| selectedIcon: TerminalSquareSolid, | |||
| }, | |||
| { | |||
| name: mode !== 'workflow' | |||
| ? t('common.appMenus.logAndAnn') | |||
| : t('common.appMenus.logs'), | |||
| href: `/app/${appId}/logs`, | |||
| icon: FileHeart02, | |||
| selectedIcon: FileHeart02Solid, | |||
| }, | |||
| ...(isCurrentWorkspaceManager | |||
| ? [{ | |||
| name: mode !== 'workflow' | |||
| ? t('common.appMenus.logAndAnn') | |||
| : t('common.appMenus.logs'), | |||
| href: `/app/${appId}/logs`, | |||
| icon: FileHeart02, | |||
| selectedIcon: FileHeart02Solid, | |||
| }] | |||
| : [] | |||
| ), | |||
| { | |||
| name: t('common.appMenus.overview'), | |||
| href: `/app/${appId}/overview`, | |||
| @@ -104,10 +107,10 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => { | |||
| } | |||
| else { | |||
| setAppDetail(res) | |||
| setNavigation(getNavigations(appId, isCurrentWorkspaceManager, res.mode)) | |||
| setNavigation(getNavigations(appId, isCurrentWorkspaceManager, isCurrentWorkspaceEditor, res.mode)) | |||
| } | |||
| }) | |||
| }, [appId, isCurrentWorkspaceManager]) | |||
| }, [appId, isCurrentWorkspaceManager, isCurrentWorkspaceEditor]) | |||
| useUnmount(() => { | |||
| setAppDetail() | |||
| @@ -37,7 +37,7 @@ export type AppCardProps = { | |||
| const AppCard = ({ app, onRefresh }: AppCardProps) => { | |||
| const { t } = useTranslation() | |||
| const { notify } = useContext(ToastContext) | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { isCurrentWorkspaceEditor } = useAppContext() | |||
| const { onPlanInfoChanged } = useProviderContext() | |||
| const { push } = useRouter() | |||
| @@ -116,7 +116,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { | |||
| onRefresh() | |||
| mutateApps() | |||
| onPlanInfoChanged() | |||
| getRedirection(isCurrentWorkspaceManager, newApp, push) | |||
| getRedirection(isCurrentWorkspaceEditor, newApp, push) | |||
| } | |||
| catch (e) { | |||
| notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) | |||
| @@ -224,7 +224,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { | |||
| <div | |||
| onClick={(e) => { | |||
| e.preventDefault() | |||
| getRedirection(isCurrentWorkspaceManager, app, push) | |||
| getRedirection(isCurrentWorkspaceEditor, app, push) | |||
| }} | |||
| className='group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg' | |||
| > | |||
| @@ -298,7 +298,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { | |||
| /> | |||
| </div> | |||
| </div> | |||
| {isCurrentWorkspaceManager && ( | |||
| {isCurrentWorkspaceEditor && ( | |||
| <> | |||
| <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'> | |||
| @@ -50,7 +50,7 @@ const getKey = ( | |||
| const Apps = () => { | |||
| const { t } = useTranslation() | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { isCurrentWorkspaceEditor } = useAppContext() | |||
| const showTagManagementModal = useTagStore(s => s.showTagManagementModal) | |||
| const [activeTab, setActiveTab] = useTabSearchParams({ | |||
| defaultTab: 'all', | |||
| @@ -130,7 +130,7 @@ const Apps = () => { | |||
| </div> | |||
| </div> | |||
| <nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'> | |||
| {isCurrentWorkspaceManager | |||
| {isCurrentWorkspaceEditor | |||
| && <NewAppCard onSuccess={mutate} />} | |||
| {data?.map(({ data: apps }: any) => apps.map((app: any) => ( | |||
| <AppCard key={app.id} app={app} onRefresh={mutate} /> | |||
| @@ -44,7 +44,7 @@ const Datasets = ({ | |||
| tags, | |||
| keywords, | |||
| }: Props) => { | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { isCurrentWorkspaceEditor } = useAppContext() | |||
| const { data, isLoading, setSize, mutate } = useSWRInfinite( | |||
| (pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords), | |||
| fetchDatasets, | |||
| @@ -76,7 +76,7 @@ const Datasets = ({ | |||
| return ( | |||
| <nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'> | |||
| { isCurrentWorkspaceManager && <NewDatasetCard ref={anchorRef} /> } | |||
| { isCurrentWorkspaceEditor && <NewDatasetCard ref={anchorRef} /> } | |||
| {data?.map(({ data: datasets }) => datasets.map(dataset => ( | |||
| <DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />), | |||
| ))} | |||
| @@ -44,7 +44,7 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => { | |||
| const { plan, enableBilling } = useProviderContext() | |||
| const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { isCurrentWorkspaceEditor } = useAppContext() | |||
| const isCreatingRef = useRef(false) | |||
| const onCreate: MouseEventHandler = useCallback(async () => { | |||
| @@ -72,13 +72,13 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => { | |||
| onClose() | |||
| mutateApps() | |||
| localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') | |||
| getRedirection(isCurrentWorkspaceManager, app, push) | |||
| getRedirection(isCurrentWorkspaceEditor, app, push) | |||
| } | |||
| catch (e) { | |||
| notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) | |||
| } | |||
| isCreatingRef.current = false | |||
| }, [name, notify, t, appMode, emoji.icon, emoji.icon_background, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceManager]) | |||
| }, [name, notify, t, appMode, emoji.icon, emoji.icon_background, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceEditor]) | |||
| return ( | |||
| <Modal | |||
| @@ -47,7 +47,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose }: CreateFromDSLModalProp | |||
| setFileContent('') | |||
| } | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { isCurrentWorkspaceEditor } = useAppContext() | |||
| const { plan, enableBilling } = useProviderContext() | |||
| const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) | |||
| @@ -68,7 +68,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose }: CreateFromDSLModalProp | |||
| onClose() | |||
| notify({ type: 'success', message: t('app.newApp.appCreated') }) | |||
| localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') | |||
| getRedirection(isCurrentWorkspaceManager, app, push) | |||
| getRedirection(isCurrentWorkspaceEditor, app, push) | |||
| } | |||
| catch (e) { | |||
| notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) | |||
| @@ -53,7 +53,7 @@ function AppCard({ | |||
| }: IAppCardProps) { | |||
| const router = useRouter() | |||
| const pathname = usePathname() | |||
| const { currentWorkspace, isCurrentWorkspaceManager } = useAppContext() | |||
| const { currentWorkspace, isCurrentWorkspaceManager, isCurrentWorkspaceEditor } = useAppContext() | |||
| const [showSettingsModal, setShowSettingsModal] = useState(false) | |||
| const [showEmbedded, setShowEmbedded] = useState(false) | |||
| const [showCustomizeModal, setShowCustomizeModal] = useState(false) | |||
| @@ -74,16 +74,17 @@ function AppCard({ | |||
| if (appInfo.mode !== 'completion' && appInfo.mode !== 'workflow') | |||
| operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.embedded.entry'), opIcon: EmbedIcon }) | |||
| if (isCurrentWorkspaceManager) | |||
| if (isCurrentWorkspaceEditor) | |||
| operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: Cog8ToothIcon }) | |||
| return operationsMap | |||
| }, [isCurrentWorkspaceManager, appInfo, t]) | |||
| }, [isCurrentWorkspaceEditor, appInfo, t]) | |||
| const isApp = cardType === 'webapp' | |||
| const basicName = isApp | |||
| ? appInfo?.site?.title | |||
| : t('appOverview.overview.apiInfo.title') | |||
| const toggleDisabled = isApp ? !isCurrentWorkspaceEditor : !isCurrentWorkspaceManager | |||
| const runningStatus = isApp ? appInfo.enable_site : appInfo.enable_api | |||
| const { app_base_url, access_token } = appInfo.site ?? {} | |||
| const appMode = (appInfo.mode !== 'completion' && appInfo.mode !== 'workflow') ? 'chat' : appInfo.mode | |||
| @@ -154,7 +155,7 @@ function AppCard({ | |||
| ? t('appOverview.overview.status.running') | |||
| : t('appOverview.overview.status.disable')} | |||
| </Tag> | |||
| <Switch defaultValue={runningStatus} onChange={onChangeStatus} disabled={currentWorkspace?.role === 'normal'} /> | |||
| <Switch defaultValue={runningStatus} onChange={onChangeStatus} disabled={toggleDisabled} /> | |||
| </div> | |||
| </div> | |||
| <div className="flex flex-col justify-center py-2"> | |||
| @@ -37,7 +37,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo | |||
| const { notify } = useContext(ToastContext) | |||
| const setAppDetail = useAppStore(s => s.setAppDetail) | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { isCurrentWorkspaceEditor } = useAppContext() | |||
| const { plan, enableBilling } = useProviderContext() | |||
| const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) | |||
| @@ -66,7 +66,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo | |||
| await deleteApp(appDetail.id) | |||
| localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') | |||
| getRedirection( | |||
| isCurrentWorkspaceManager, | |||
| isCurrentWorkspaceEditor, | |||
| { | |||
| id: newAppID, | |||
| mode: appDetail.mode === 'completion' ? 'workflow' : 'advanced-chat', | |||
| @@ -143,7 +143,7 @@ const SecretKeyModal = ({ | |||
| ) | |||
| } | |||
| <div className='flex'> | |||
| <Button type='default' className={`flex flex-shrink-0 mt-4 ${s.autoWidth}`} onClick={onCreate} disabled={ !currentWorkspace || currentWorkspace.role === 'normal'}> | |||
| <Button type='default' className={`flex flex-shrink-0 mt-4 ${s.autoWidth}`} onClick={onCreate} disabled={ !currentWorkspace || !isCurrentWorkspaceManager}> | |||
| <PlusIcon className='flex flex-shrink-0 w-4 h-4' /> | |||
| <div className='text-xs font-medium text-gray-800'>{t('appApi.apiKeyModal.createNewSecretKey')}</div> | |||
| </Button> | |||
| @@ -38,7 +38,7 @@ const Apps = ({ | |||
| onSuccess, | |||
| }: AppsProps) => { | |||
| const { t } = useTranslation() | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { isCurrentWorkspaceEditor } = useAppContext() | |||
| const { push } = useRouter() | |||
| const { hasEditPermission } = useContext(ExploreContext) | |||
| const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' }) | |||
| @@ -116,7 +116,7 @@ const Apps = ({ | |||
| if (onSuccess) | |||
| onSuccess() | |||
| localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') | |||
| getRedirection(isCurrentWorkspaceManager, app, push) | |||
| getRedirection(isCurrentWorkspaceEditor, app, push) | |||
| } | |||
| catch (e) { | |||
| Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) | |||
| @@ -28,6 +28,7 @@ const MembersPage = () => { | |||
| const RoleMap = { | |||
| owner: t('common.members.owner'), | |||
| admin: t('common.members.admin'), | |||
| editor: t('common.members.editor'), | |||
| normal: t('common.members.normal'), | |||
| } | |||
| const { locale } = useContext(I18n) | |||
| @@ -37,6 +37,10 @@ const InviteModal = ({ | |||
| name: 'normal', | |||
| description: t('common.members.normalTip'), | |||
| }, | |||
| { | |||
| name: 'editor', | |||
| description: t('common.members.editorTip'), | |||
| }, | |||
| { | |||
| name: 'admin', | |||
| description: t('common.members.adminTip'), | |||
| @@ -120,7 +124,7 @@ const InviteModal = ({ | |||
| <div className='flex flex-row'> | |||
| <span | |||
| className={cn( | |||
| 'text-indigo-600 w-8', | |||
| 'text-indigo-600 mr-2', | |||
| 'flex items-center', | |||
| )} | |||
| > | |||
| @@ -130,7 +134,7 @@ const InviteModal = ({ | |||
| <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 truncate`}> | |||
| <span className={`${selected ? 'font-medium' : 'font-normal'} capitalize block text-gray-500`}> | |||
| {role.description} | |||
| </span> | |||
| </div> | |||
| @@ -36,6 +36,7 @@ const Operation = ({ | |||
| const RoleMap = { | |||
| owner: t('common.members.owner'), | |||
| admin: t('common.members.admin'), | |||
| editor: t('common.members.editor'), | |||
| normal: t('common.members.normal'), | |||
| } | |||
| const { notify } = useContext(ToastContext) | |||
| @@ -98,7 +99,7 @@ const Operation = ({ | |||
| > | |||
| <div className="px-1 py-1"> | |||
| { | |||
| ['admin', 'normal'].map(role => ( | |||
| ['admin', 'editor', 'normal'].map(role => ( | |||
| <Menu.Item key={role}> | |||
| <div className={itemClassName} onClick={() => handleUpdateMemberRole(role)}> | |||
| { | |||
| @@ -47,6 +47,7 @@ import { | |||
| } from '@/app/components/base/portal-to-follow-elem' | |||
| import { useToastContext } from '@/app/components/base/toast' | |||
| import ConfirmCommon from '@/app/components/base/confirm/common' | |||
| import { useAppContext } from '@/context/app-context' | |||
| type ModelModalProps = { | |||
| provider: ModelProvider | |||
| @@ -74,7 +75,8 @@ const ModelModal: FC<ModelModalProps> = ({ | |||
| providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active, | |||
| currentCustomConfigurationModelFixedFields, | |||
| ) | |||
| const isEditMode = !!formSchemasValue | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const isEditMode = !!formSchemasValue && isCurrentWorkspaceManager | |||
| const { t } = useTranslation() | |||
| const { notify } = useToastContext() | |||
| const language = useLanguage() | |||
| @@ -344,6 +346,7 @@ const ModelModal: FC<ModelModalProps> = ({ | |||
| || filteredRequiredFormSchemas.some(item => value[item.variable] === undefined) | |||
| || (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2) | |||
| } | |||
| > | |||
| {t('common.operation.save')} | |||
| </Button> | |||
| @@ -23,6 +23,7 @@ import Button from '@/app/components/base/button' | |||
| import { useProviderContext } from '@/context/provider-context' | |||
| import { updateDefaultModel } from '@/service/common' | |||
| import { useToastContext } from '@/app/components/base/toast' | |||
| import { useAppContext } from '@/context/app-context' | |||
| type SystemModelSelectorProps = { | |||
| textGenerationDefaultModel: DefaultModelResponse | undefined | |||
| @@ -40,6 +41,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({ | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { notify } = useToastContext() | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { textGenerationModelList } = useProviderContext() | |||
| const updateModelList = useUpdateModelList() | |||
| const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding) | |||
| @@ -248,6 +250,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({ | |||
| type='primary' | |||
| className='!h-8 !text-[13px]' | |||
| onClick={handleSave} | |||
| disabled={!isCurrentWorkspaceManager} | |||
| > | |||
| {t('common.operation.save')} | |||
| </Button> | |||
| @@ -39,7 +39,7 @@ const getKey = ( | |||
| const AppNav = () => { | |||
| const { t } = useTranslation() | |||
| const { appId } = useParams() | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { isCurrentWorkspaceEditor } = useAppContext() | |||
| const appDetail = useAppStore(state => state.appDetail) | |||
| const [showNewAppDialog, setShowNewAppDialog] = useState(false) | |||
| const [showNewAppTemplateDialog, setShowNewAppTemplateDialog] = useState(false) | |||
| @@ -71,8 +71,8 @@ const AppNav = () => { | |||
| if (appsData) { | |||
| const appItems = flatten(appsData?.map(appData => appData.data)) | |||
| const navItems = appItems.map((app) => { | |||
| const link = ((isCurrentWorkspaceManager, app) => { | |||
| if (!isCurrentWorkspaceManager) { | |||
| const link = ((isCurrentWorkspaceEditor, app) => { | |||
| if (!isCurrentWorkspaceEditor) { | |||
| return `/app/${app.id}/overview` | |||
| } | |||
| else { | |||
| @@ -81,7 +81,7 @@ const AppNav = () => { | |||
| else | |||
| return `/app/${app.id}/configuration` | |||
| } | |||
| })(isCurrentWorkspaceManager, app) | |||
| })(isCurrentWorkspaceEditor, app) | |||
| return { | |||
| id: app.id, | |||
| icon: app.icon, | |||
| @@ -93,7 +93,7 @@ const AppNav = () => { | |||
| }) | |||
| setNavItems(navItems) | |||
| } | |||
| }, [appsData, isCurrentWorkspaceManager, setNavItems]) | |||
| }, [appsData, isCurrentWorkspaceEditor, setNavItems]) | |||
| // update current app name | |||
| useEffect(() => { | |||
| @@ -17,7 +17,7 @@ type IAppSelectorProps = { | |||
| export default function AppSelector({ appItems, curApp }: IAppSelectorProps) { | |||
| const router = useRouter() | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { isCurrentWorkspaceEditor } = useAppContext() | |||
| const [showNewAppDialog, setShowNewAppDialog] = useState(false) | |||
| const { t } = useTranslation() | |||
| @@ -65,7 +65,7 @@ export default function AppSelector({ appItems, curApp }: IAppSelectorProps) { | |||
| appItems.map((app: AppDetailResponse) => ( | |||
| <Menu.Item key={app.id}> | |||
| <div className={itemClassName} onClick={() => | |||
| router.push(`/app/${app.id}/${isCurrentWorkspaceManager ? 'configuration' : 'overview'}`) | |||
| router.push(`/app/${app.id}/${isCurrentWorkspaceEditor ? 'configuration' : 'overview'}`) | |||
| }> | |||
| <div className='relative w-6 h-6 mr-2 bg-[#D5F5F6] rounded-[6px]'> | |||
| <AppIcon size='tiny' /> | |||
| @@ -79,7 +79,7 @@ export default function AppSelector({ appItems, curApp }: IAppSelectorProps) { | |||
| )) | |||
| } | |||
| </div>)} | |||
| {isCurrentWorkspaceManager && <Menu.Item> | |||
| {isCurrentWorkspaceEditor && <Menu.Item> | |||
| <div className='p-1' onClick={() => setShowNewAppDialog(true)}> | |||
| <div | |||
| className='flex items-center h-12 rounded-lg cursor-pointer hover:bg-gray-100' | |||
| @@ -26,7 +26,7 @@ const navClassName = ` | |||
| ` | |||
| const Header = () => { | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { isCurrentWorkspaceEditor } = useAppContext() | |||
| const selectedSegment = useSelectedLayoutSegment() | |||
| const media = useBreakpoints() | |||
| @@ -74,7 +74,7 @@ const Header = () => { | |||
| <div className='flex items-center'> | |||
| <ExploreNav className={navClassName} /> | |||
| <AppNav /> | |||
| {isCurrentWorkspaceManager && <DatasetNav />} | |||
| {isCurrentWorkspaceEditor && <DatasetNav />} | |||
| <ToolsNav className={navClassName} /> | |||
| </div> | |||
| )} | |||
| @@ -93,7 +93,7 @@ const Header = () => { | |||
| <div className='w-full flex flex-col p-2 gap-y-1'> | |||
| <ExploreNav className={navClassName} /> | |||
| <AppNav /> | |||
| {isCurrentWorkspaceManager && <DatasetNav />} | |||
| {isCurrentWorkspaceEditor && <DatasetNav />} | |||
| <ToolsNav className={navClassName} /> | |||
| </div> | |||
| )} | |||
| @@ -34,7 +34,7 @@ export type INavSelectorProps = { | |||
| const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }: INavSelectorProps) => { | |||
| const { t } = useTranslation() | |||
| const router = useRouter() | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { isCurrentWorkspaceEditor } = useAppContext() | |||
| const setAppDetail = useAppStore(state => state.setAppDetail) | |||
| const handleScroll = useCallback(debounce((e) => { | |||
| @@ -122,7 +122,7 @@ const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }: | |||
| </div> | |||
| </Menu.Button> | |||
| )} | |||
| {isApp && isCurrentWorkspaceManager && ( | |||
| {isApp && isCurrentWorkspaceEditor && ( | |||
| <Menu as="div" className="relative w-full h-full"> | |||
| {({ open }) => ( | |||
| <> | |||
| @@ -11,6 +11,7 @@ import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows | |||
| import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal' | |||
| import { createCustomCollection } from '@/service/tools' | |||
| import Toast from '@/app/components/base/toast' | |||
| import { useAppContext } from '@/context/app-context' | |||
| type Props = { | |||
| onRefreshData: () => void | |||
| @@ -20,6 +21,7 @@ const Contribute = ({ onRefreshData }: Props) => { | |||
| const { t } = useTranslation() | |||
| const { locale } = useContext(I18n) | |||
| const language = getLanguage(locale) | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const linkUrl = useMemo(() => { | |||
| if (language.startsWith('zh_')) | |||
| @@ -40,23 +42,25 @@ const Contribute = ({ onRefreshData }: Props) => { | |||
| return ( | |||
| <> | |||
| <div className='flex flex-col col-span-1 bg-gray-200 border-[0.5px] border-black/5 rounded-xl min-h-[160px] transition-all duration-200 ease-in-out cursor-pointer hover:bg-gray-50 hover:shadow-lg'> | |||
| <div className='group grow rounded-t-xl hover:bg-white' onClick={() => setIsShowEditCustomCollectionModal(true)}> | |||
| <div className='shrink-0 flex items-center p-4 pb-3'> | |||
| <div className='w-10 h-10 flex items-center justify-center border border-gray-200 bg-gray-100 rounded-lg group-hover:border-primary-100 group-hover:bg-primary-50'> | |||
| <Plus className='w-4 h-4 text-gray-500 group-hover:text-primary-600'/> | |||
| {isCurrentWorkspaceManager && ( | |||
| <div className='flex flex-col col-span-1 bg-gray-200 border-[0.5px] border-black/5 rounded-xl min-h-[160px] transition-all duration-200 ease-in-out cursor-pointer hover:bg-gray-50 hover:shadow-lg'> | |||
| <div className='group grow rounded-t-xl hover:bg-white' onClick={() => setIsShowEditCustomCollectionModal(true)}> | |||
| <div className='shrink-0 flex items-center p-4 pb-3'> | |||
| <div className='w-10 h-10 flex items-center justify-center border border-gray-200 bg-gray-100 rounded-lg group-hover:border-primary-100 group-hover:bg-primary-50'> | |||
| <Plus className='w-4 h-4 text-gray-500 group-hover:text-primary-600'/> | |||
| </div> | |||
| <div className='ml-3 text-sm font-semibold leading-5 text-gray-800 group-hover:text-primary-600'>{t('tools.createCustomTool')}</div> | |||
| </div> | |||
| <div className='ml-3 text-sm font-semibold leading-5 text-gray-800 group-hover:text-primary-600'>{t('tools.createCustomTool')}</div> | |||
| </div> | |||
| <div className='px-4 py-3 rounded-b-xl border-t-[0.5px] border-black/5 text-gray-500 hover:text-[#155EEF] hover:bg-white'> | |||
| <a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'> | |||
| <BookOpen01 className='shrink-0 w-3 h-3' /> | |||
| <div className='grow leading-[18px] text-xs font-normal truncate' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div> | |||
| <ArrowUpRight className='shrink-0 w-3 h-3' /> | |||
| </a> | |||
| </div> | |||
| </div> | |||
| <div className='px-4 py-3 rounded-b-xl border-t-[0.5px] border-black/5 text-gray-500 hover:text-[#155EEF] hover:bg-white'> | |||
| <a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'> | |||
| <BookOpen01 className='shrink-0 w-3 h-3' /> | |||
| <div className='grow leading-[18px] text-xs font-normal truncate' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div> | |||
| <ArrowUpRight className='shrink-0 w-3 h-3' /> | |||
| </a> | |||
| </div> | |||
| </div> | |||
| )} | |||
| {isShowEditCollectionToolModal && ( | |||
| <EditCustomToolModal | |||
| payload={null} | |||
| @@ -33,6 +33,7 @@ import { useModalContext } from '@/context/modal-context' | |||
| import { useProviderContext } from '@/context/provider-context' | |||
| import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { useAppContext } from '@/context/app-context' | |||
| type Props = { | |||
| collection: Collection | |||
| @@ -51,6 +52,7 @@ const ProviderDetail = ({ | |||
| const isAuthed = collection.is_team_authorization | |||
| const isBuiltIn = collection.type === CollectionType.builtIn | |||
| const isModel = collection.type === CollectionType.model | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const [isDetailLoading, setIsDetailLoading] = useState(false) | |||
| @@ -221,6 +223,7 @@ const ProviderDetail = ({ | |||
| if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model) | |||
| showSettingAuthModal() | |||
| }} | |||
| disabled={!isCurrentWorkspaceManager} | |||
| > | |||
| {isAuthed && <Indicator className='mr-2' color={'green'} />} | |||
| <div className={cn('text-white leading-[18px] text-[13px] font-medium', isAuthed && '!text-gray-700')}> | |||
| @@ -251,6 +254,7 @@ const ProviderDetail = ({ | |||
| <Button | |||
| className={cn('shrink-0 my-3 w-[183px] flex items-center bg-white')} | |||
| onClick={() => setIsShowEditWorkflowToolModal(true)} | |||
| disabled={!isCurrentWorkspaceManager} | |||
| > | |||
| <div className='leading-5 text-sm font-medium text-gray-700'>{t('tools.createTool.editAction')}</div> | |||
| </Button> | |||
| @@ -11,6 +11,7 @@ import { fetchBuiltInToolCredential, fetchBuiltInToolCredentialSchema } from '@/ | |||
| import Loading from '@/app/components/base/loading' | |||
| import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' | |||
| import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general' | |||
| import { useAppContext } from '@/context/app-context' | |||
| type Props = { | |||
| collection: Collection | |||
| @@ -29,6 +30,7 @@ const ConfigCredential: FC<Props> = ({ | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const [credentialSchema, setCredentialSchema] = useState<any>(null) | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { name: collectionName } = collection | |||
| const [tempCredential, setTempCredential] = React.useState<any>({}) | |||
| useEffect(() => { | |||
| @@ -13,6 +13,7 @@ import Toast from '@/app/components/base/toast' | |||
| import { createWorkflowToolProvider, fetchWorkflowToolDetailByAppID, saveWorkflowToolProvider } from '@/service/tools' | |||
| import type { Emoji, WorkflowToolProviderParameter, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '@/app/components/tools/types' | |||
| import type { InputVar } from '@/app/components/workflow/types' | |||
| import { useAppContext } from '@/context/app-context' | |||
| type Props = { | |||
| disabled: boolean | |||
| @@ -44,6 +45,7 @@ const WorkflowToolConfigureButton = ({ | |||
| const [showModal, setShowModal] = useState(false) | |||
| const [isLoading, setIsLoading] = useState(false) | |||
| const [detail, setDetail] = useState<WorkflowToolProviderResponse>() | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const outdated = useMemo(() => { | |||
| if (!detail) | |||
| @@ -175,22 +177,33 @@ const WorkflowToolConfigureButton = ({ | |||
| disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'cursor-pointer', | |||
| !published && 'hover:bg-primary-50', | |||
| )}> | |||
| <div | |||
| className='flex justify-start items-center gap-2 px-2.5 py-2' | |||
| onClick={() => !published && setShowModal(true)} | |||
| > | |||
| <Tools className={cn('relative w-4 h-4', !published && 'group-hover:text-primary-600')}/> | |||
| <div title={t('workflow.common.workflowAsTool') || ''} className={cn('grow shrink basis-0 text-[13px] font-medium leading-[18px] truncate', !published && 'group-hover:text-primary-600')}>{t('workflow.common.workflowAsTool')}</div> | |||
| {!published && ( | |||
| <span className='shrink-0 px-1 border border-black/8 rounded-[5px] bg-white text-[10px] font-medium leading-[18px] text-gray-500'>{t('workflow.common.configureRequired').toLocaleUpperCase()}</span> | |||
| {isCurrentWorkspaceManager | |||
| ? ( | |||
| <div | |||
| className='flex justify-start items-center gap-2 px-2.5 py-2' | |||
| onClick={() => !published && setShowModal(true)} | |||
| > | |||
| <Tools className={cn('relative w-4 h-4', !published && 'group-hover:text-primary-600')} /> | |||
| <div title={t('workflow.common.workflowAsTool') || ''} className={cn('grow shrink basis-0 text-[13px] font-medium leading-[18px] truncate', !published && 'group-hover:text-primary-600')}>{t('workflow.common.workflowAsTool')}</div> | |||
| {!published && ( | |||
| <span className='shrink-0 px-1 border border-black/8 rounded-[5px] bg-white text-[10px] font-medium leading-[18px] text-gray-500'>{t('workflow.common.configureRequired').toLocaleUpperCase()}</span> | |||
| )} | |||
| </div>) | |||
| : ( | |||
| <div | |||
| className='flex justify-start items-center gap-2 px-2.5 py-2' | |||
| > | |||
| <Tools className='w-4 h-4 text-gray-500' /> | |||
| <div title={t('workflow.common.workflowAsTool') || ''} className='grow shrink basis-0 text-[13px] font-medium leading-[18px] truncate text-gray-500'>{t('workflow.common.workflowAsTool')}</div> | |||
| </div> | |||
| )} | |||
| </div> | |||
| {published && ( | |||
| <div className='px-2.5 py-2 border-t-[0.5px] border-black/5'> | |||
| <div className='flex justify-between'> | |||
| <Button | |||
| className='px-2 w-[140px] py-0 h-6 shadow-xs rounded-md text-xs font-medium text-gray-700 border-[0.5px] bg-white border-gray-200' | |||
| onClick={() => setShowModal(true)} | |||
| disabled={!isCurrentWorkspaceManager} | |||
| > | |||
| {t('workflow.common.configure')} | |||
| {outdated && <Indicator className='ml-1' color={'yellow'} />} | |||
| @@ -208,7 +221,7 @@ const WorkflowToolConfigureButton = ({ | |||
| )} | |||
| </div> | |||
| )} | |||
| {published && isLoading && <div className='pt-2'><Loading type='app'/></div>} | |||
| {published && isLoading && <div className='pt-2'><Loading type='app' /></div>} | |||
| </div> | |||
| {showModal && ( | |||
| <WorkflowToolModal | |||
| @@ -19,6 +19,7 @@ export type AppContextValue = { | |||
| currentWorkspace: ICurrentWorkspace | |||
| isCurrentWorkspaceManager: boolean | |||
| isCurrentWorkspaceOwner: boolean | |||
| isCurrentWorkspaceEditor: boolean | |||
| mutateCurrentWorkspace: VoidFunction | |||
| pageContainerRef: React.RefObject<HTMLDivElement> | |||
| langeniusVersionInfo: LangGeniusVersionResponse | |||
| @@ -59,6 +60,7 @@ const AppContext = createContext<AppContextValue>({ | |||
| currentWorkspace: initialWorkspaceInfo, | |||
| isCurrentWorkspaceManager: false, | |||
| isCurrentWorkspaceOwner: false, | |||
| isCurrentWorkspaceEditor: false, | |||
| mutateUserProfile: () => { }, | |||
| mutateCurrentWorkspace: () => { }, | |||
| pageContainerRef: createRef(), | |||
| @@ -86,6 +88,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => | |||
| const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo) | |||
| 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 updateUserProfileAndVersion = useCallback(async () => { | |||
| if (userProfileResponse && !userProfileResponse.bodyUsed) { | |||
| const result = await userProfileResponse.json() | |||
| @@ -121,6 +124,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => | |||
| currentWorkspace, | |||
| isCurrentWorkspaceManager, | |||
| isCurrentWorkspaceOwner, | |||
| isCurrentWorkspaceEditor, | |||
| mutateCurrentWorkspace, | |||
| }}> | |||
| <div className='flex flex-col h-full overflow-y-auto'> | |||
| @@ -169,6 +169,8 @@ const translation = { | |||
| adminTip: 'Kann Apps erstellen & Team-Einstellungen verwalten', | |||
| normal: 'Normal', | |||
| normalTip: 'Kann nur Apps verwenden, kann keine Apps erstellen', | |||
| editor: 'Editor', | |||
| editorTip: 'Kann Apps erstellen & bearbeiten', | |||
| inviteTeamMember: 'Teammitglied hinzufügen', | |||
| inviteTeamMemberTip: 'Sie können direkt nach der Anmeldung auf Ihre Teamdaten zugreifen.', | |||
| email: 'E-Mail', | |||
| @@ -185,6 +187,7 @@ const translation = { | |||
| removeFromTeamTip: 'Wird den Teamzugang entfernen', | |||
| setAdmin: 'Als Administrator einstellen', | |||
| setMember: 'Als normales Mitglied einstellen', | |||
| setEditor: 'Als Editor einstellen', | |||
| disinvite: 'Einladung widerrufen', | |||
| deleteMember: 'Mitglied löschen', | |||
| you: '(Du)', | |||
| @@ -169,6 +169,10 @@ const translation = { | |||
| adminTip: 'Can build apps & manage team settings', | |||
| normal: 'Normal', | |||
| normalTip: 'Only can use apps, can not build apps', | |||
| builder: 'Builder', | |||
| builderTip: 'Can build & edit own apps', | |||
| editor: 'Editor', | |||
| editorTip: 'Can build & edit apps', | |||
| inviteTeamMember: 'Add team member', | |||
| inviteTeamMemberTip: 'They can access your team data directly after signing in.', | |||
| email: 'Email', | |||
| @@ -185,6 +189,8 @@ const translation = { | |||
| removeFromTeamTip: 'Will remove team access', | |||
| setAdmin: 'Set as administrator', | |||
| setMember: 'Set to ordinary member', | |||
| setBuilder: 'Set as builder', | |||
| setEditor: 'Set as editor', | |||
| disinvite: 'Cancel the invitation', | |||
| deleteMember: 'Delete Member', | |||
| you: '(You)', | |||
| @@ -169,6 +169,8 @@ const translation = { | |||
| adminTip: 'Peut construire des applications & gérer les paramètres de l\'équipe', | |||
| normal: 'Normal', | |||
| normalTip: 'Peut seulement utiliser des applications, ne peut pas construire des applications', | |||
| editor: 'Éditeur', | |||
| editorTip: 'Peut construire des applications, mais ne peut pas gérer les paramètres de l\'équipe', | |||
| inviteTeamMember: 'Ajouter un membre de l\'équipe', | |||
| inviteTeamMemberTip: 'Ils peuvent accéder directement à vos données d\'équipe après s\'être connectés.', | |||
| email: 'Courrier électronique', | |||
| @@ -185,6 +187,7 @@ const translation = { | |||
| removeFromTeamTip: 'Supprimera l\'accès de l\'équipe', | |||
| setAdmin: 'Définir comme administrateur', | |||
| setMember: 'Définir en tant que membre ordinaire', | |||
| setEditor: 'Définir en tant qu\'éditeur', | |||
| disinvite: 'Annuler l\'invitation', | |||
| deleteMember: 'Supprimer Membre', | |||
| you: '(Vous)', | |||
| @@ -169,6 +169,8 @@ const translation = { | |||
| adminTip: 'アプリの構築およびチーム設定の管理ができます', | |||
| normal: '通常', | |||
| normalTip: 'アプリの使用のみが可能で、アプリの構築はできません', | |||
| editor: 'エディター', | |||
| editorTip: 'アプリの構築ができますが、チーム設定の管理はできません', | |||
| inviteTeamMember: 'チームメンバーを招待する', | |||
| inviteTeamMemberTip: '彼らはサインイン後、直接あなたのチームデータにアクセスできます。', | |||
| email: 'メール', | |||
| @@ -185,6 +187,7 @@ const translation = { | |||
| removeFromTeamTip: 'チームへのアクセスが削除されます', | |||
| setAdmin: '管理者に設定', | |||
| setMember: '通常のメンバーに設定', | |||
| setEditor: 'エディターに設定', | |||
| disinvite: '招待をキャンセル', | |||
| deleteMember: 'メンバーを削除', | |||
| you: '(あなた)', | |||
| @@ -165,6 +165,8 @@ const translation = { | |||
| adminTip: '앱 빌드 및 팀 설정 관리 가능', | |||
| normal: '일반', | |||
| normalTip: '앱 사용만 가능하고 앱 빌드는 불가능', | |||
| editor: '편집자', | |||
| editorTip: '앱 빌드만 가능하고 팀 설정 관리 불가능', | |||
| inviteTeamMember: '팀 멤버 초대', | |||
| inviteTeamMemberTip: '로그인 후에 바로 팀 데이터에 액세스할 수 있습니다.', | |||
| email: '이메일', | |||
| @@ -181,6 +183,7 @@ const translation = { | |||
| removeFromTeamTip: '팀 액세스가 제거됩니다', | |||
| setAdmin: '관리자 설정', | |||
| setMember: '일반 멤버 설정', | |||
| setEditor: '편집자 설정', | |||
| disinvite: '초대 취소', | |||
| deleteMember: '멤버 삭제', | |||
| you: '(나)', | |||
| @@ -175,6 +175,8 @@ const translation = { | |||
| adminTip: 'Może tworzyć aplikacje i zarządzać ustawieniami zespołu', | |||
| normal: 'Normalny', | |||
| normalTip: 'Może tylko korzystać z aplikacji, nie może tworzyć aplikacji', | |||
| editor: 'Edytor', | |||
| editorTip: 'Może tworzyć i edytować aplikacje, ale nie zarządzać ustawieniami zespołu', | |||
| inviteTeamMember: 'Dodaj członka zespołu', | |||
| inviteTeamMemberTip: | |||
| 'Mogą uzyskać bezpośredni dostęp do danych Twojego zespołu po zalogowaniu.', | |||
| @@ -193,6 +195,7 @@ const translation = { | |||
| removeFromTeamTip: 'Usunie dostęp do zespołu', | |||
| setAdmin: 'Ustaw jako administratora', | |||
| setMember: 'Ustaw jako zwykłego członka', | |||
| setEditor: 'Ustaw jako edytora', | |||
| disinvite: 'Anuluj zaproszenie', | |||
| deleteMember: 'Usuń członka', | |||
| you: '(Ty)', | |||
| @@ -169,6 +169,8 @@ const translation = { | |||
| adminTip: 'Pode criar aplicativos e gerenciar configurações da equipe', | |||
| normal: 'Normal', | |||
| normalTip: 'Só pode usar aplicativos, não pode criar aplicativos', | |||
| editor: 'Editor', | |||
| editorTip: 'Pode editar aplicativos, mas não pode gerenciar configurações da equipe', | |||
| inviteTeamMember: 'Adicionar membro da equipe', | |||
| inviteTeamMemberTip: 'Eles podem acessar os dados da sua equipe diretamente após fazer login.', | |||
| email: 'E-mail', | |||
| @@ -185,6 +187,7 @@ const translation = { | |||
| removeFromTeamTip: 'Removerá o acesso da equipe', | |||
| setAdmin: 'Definir como administrador', | |||
| setMember: 'Definir como membro comum', | |||
| setEditor: 'Definir como editor', | |||
| disinvite: 'Cancelar o convite', | |||
| deleteMember: 'Excluir Membro', | |||
| you: '(Você)', | |||
| @@ -168,6 +168,8 @@ const translation = { | |||
| adminTip: 'Poate construi aplicații și gestiona setările echipei', | |||
| normal: 'Normal', | |||
| normalTip: 'Poate doar utiliza aplicații, nu poate construi aplicații', | |||
| editor: 'Editor', | |||
| editorTip: 'Poate construi aplicații, dar nu poate gestiona setările echipei', | |||
| inviteTeamMember: 'Adaugă membru în echipă', | |||
| inviteTeamMemberTip: 'Pot accesa direct datele echipei dvs. după autentificare.', | |||
| email: 'Email', | |||
| @@ -184,6 +186,7 @@ const translation = { | |||
| removeFromTeamTip: 'Va elimina accesul la echipă', | |||
| setAdmin: 'Setează ca administrator', | |||
| setMember: 'Setează ca membru obișnuit', | |||
| setEditor: 'Setează ca editor', | |||
| disinvite: 'Anulează invitația', | |||
| deleteMember: 'Șterge membru', | |||
| you: '(Dvs.)', | |||
| @@ -169,6 +169,8 @@ const translation = { | |||
| adminTip: 'Може створювати програми та керувати налаштуваннями команди', | |||
| normal: 'Звичайний', | |||
| normalTip: 'Може лише використовувати програми, не може створювати програми', | |||
| editor: 'Редактор', | |||
| editorTip: 'Може створювати програми, але не може керувати налаштуваннями команди', | |||
| inviteTeamMember: 'Додати учасника команди', | |||
| inviteTeamMemberTip: 'Вони зможуть отримати доступ до даних вашої команди безпосередньо після входу.', | |||
| email: 'Електронна пошта', | |||
| @@ -185,6 +187,7 @@ const translation = { | |||
| removeFromTeamTip: 'Буде видалено доступ до команди', | |||
| setAdmin: 'Призначити адміністратором', | |||
| setMember: 'Встановити як звичайного члена', | |||
| setEditor: 'Встановити як Редактор', | |||
| disinvite: 'Скасувати запрошення', | |||
| deleteMember: 'Видалити учасника', | |||
| you: '(Ви)', | |||
| @@ -168,6 +168,8 @@ const translation = { | |||
| adminTip: 'Có thể xây dựng ứng dụng và quản lý cài đặt nhóm', | |||
| normal: 'Bình thường', | |||
| normalTip: 'Chỉ có thể sử dụng ứng dụng, không thể xây dựng ứng dụng', | |||
| editor: 'Biên tập viên', | |||
| editorTip: 'Chỉ có thể xây dựng ứng dụng, không thể quản lý cài đặt nhóm', | |||
| inviteTeamMember: 'Mời thành viên nhóm', | |||
| inviteTeamMemberTip: 'Sau khi đăng nhập, họ có thể truy cập trực tiếp vào dữ liệu nhóm của bạn.', | |||
| email: 'Email', | |||
| @@ -184,6 +186,7 @@ const translation = { | |||
| removeFromTeamTip: 'Sẽ xóa quyền truy cập nhóm', | |||
| setAdmin: 'Đặt làm quản trị viên', | |||
| setMember: 'Đặt thành viên bình thường', | |||
| setEditor: 'Đặt làm biên tập viên', | |||
| disinvite: 'Hủy lời mời', | |||
| deleteMember: 'Xóa thành viên', | |||
| you: '(Bạn)', | |||
| @@ -169,6 +169,8 @@ const translation = { | |||
| adminTip: '能够建立应用程序和管理团队设置', | |||
| normal: '成员', | |||
| normalTip: '只能使用应用程序,不能建立应用程序', | |||
| editor: '编辑', | |||
| editorTip: '能够建立并编辑应用程序,不能管理团队设置', | |||
| inviteTeamMember: '添加团队成员', | |||
| inviteTeamMemberTip: '对方在登录后可以访问你的团队数据。', | |||
| email: '邮箱', | |||
| @@ -185,6 +187,7 @@ const translation = { | |||
| removeFromTeamTip: '将取消团队访问', | |||
| setAdmin: '设为管理员', | |||
| setMember: '设为普通成员', | |||
| setEditor: '设为编辑', | |||
| disinvite: '取消邀请', | |||
| deleteMember: '删除成员', | |||
| you: '(你)', | |||
| @@ -169,6 +169,8 @@ const translation = { | |||
| adminTip: '能夠建立應用程式和管理團隊設定', | |||
| normal: '成員', | |||
| normalTip: '只能使用應用程式,不能建立應用程式', | |||
| editor: '編輯', | |||
| editorTip: '能夠建立並編輯應用程式,不能管理團隊設定', | |||
| inviteTeamMember: '新增團隊成員', | |||
| inviteTeamMemberTip: '對方在登入後可以訪問你的團隊資料。', | |||
| email: '郵箱', | |||
| @@ -185,6 +187,7 @@ const translation = { | |||
| removeFromTeamTip: '將取消團隊訪問', | |||
| setAdmin: '設為管理員', | |||
| setMember: '設為普通成員', | |||
| setEditor: '設為編輯', | |||
| disinvite: '取消邀請', | |||
| deleteMember: '刪除成員', | |||
| you: '(你)', | |||
| @@ -64,7 +64,7 @@ export type TenantInfoResponse = { | |||
| export type Member = Pick<UserProfileResponse, 'id' | 'name' | 'email' | 'last_login_at' | 'created_at'> & { | |||
| avatar: string | |||
| status: 'pending' | 'active' | 'banned' | 'closed' | |||
| role: 'owner' | 'admin' | 'normal' | |||
| role: 'owner' | 'admin' | 'editor' | 'normal' | |||
| } | |||
| export enum ProviderName { | |||
| @@ -125,7 +125,7 @@ export type IWorkspace = { | |||
| } | |||
| export type ICurrentWorkspace = Omit<IWorkspace, 'current'> & { | |||
| role: 'normal' | 'admin' | 'owner' | |||
| role: 'owner' | 'admin' | 'editor' | 'normal' | |||
| providers: Provider[] | |||
| in_trail: boolean | |||
| trial_end_reason?: string | |||
| @@ -1,9 +1,9 @@ | |||
| export const getRedirection = ( | |||
| isCurrentWorkspaceManager: boolean, | |||
| isCurrentWorkspaceEditor: boolean, | |||
| app: any, | |||
| redirectionFunc: (href: string) => void, | |||
| ) => { | |||
| if (!isCurrentWorkspaceManager) { | |||
| if (!isCurrentWorkspaceEditor) { | |||
| redirectionFunc(`/app/${app.id}/overview`) | |||
| } | |||
| else { | |||