From f8ab11f9262791125dffb9a99800d79e7592450e Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Fri, 8 May 2026 10:35:42 +0000 Subject: [PATCH 01/24] feat(user): add businessWritePermission to Redux store, extend useCurrentUser hook, remove projectDirector enum - Redux store (user slice): add businessWritePermission boolean field with default true, add updateBusinessWritePermission action, read from API response in useUserInfo hook - useCurrentUser hook: return businessWritePermission from store, remove projectDirector from userRoles computation - enum: remove SystemRole.projectDirector and OpPermissionTypeUid.project_director - permissionManifest.ts: remove projectDirector from PROJECT_MANAGER IMPORT/EXPORT/CREATE roles - SystemRoleTagList: remove project_director color case Refs: dms-ee#813 --- .../src/components/SystemRoleTagList/index.tsx | 2 -- packages/base/src/store/user/index.ts | 13 +++++++++++-- packages/dms-kit/src/enum/index.ts | 4 +--- .../lib/features/useCurrentUser/index.ts | 12 ++++++------ .../usePermission/permissionManifest.ts | 18 +++--------------- .../shared/lib/features/useUserInfo/index.ts | 12 +++++++++++- 6 files changed, 32 insertions(+), 29 deletions(-) diff --git a/packages/base/src/components/SystemRoleTagList/index.tsx b/packages/base/src/components/SystemRoleTagList/index.tsx index 62c75f8a9..e38f6c243 100644 --- a/packages/base/src/components/SystemRoleTagList/index.tsx +++ b/packages/base/src/components/SystemRoleTagList/index.tsx @@ -17,8 +17,6 @@ const SystemRoleTagList: React.FC = ({ } const getPermissionTagColor = (uid: string): BasicTagProps['color'] => { switch (uid) { - case OpPermissionTypeUid.project_director: - return 'orange'; case OpPermissionTypeUid.audit_administrator: return 'blue'; case OpPermissionTypeUid.system_administrator: diff --git a/packages/base/src/store/user/index.ts b/packages/base/src/store/user/index.ts index 7042e9953..af92c9a06 100644 --- a/packages/base/src/store/user/index.ts +++ b/packages/base/src/store/user/index.ts @@ -27,6 +27,7 @@ type UserReduxState = { language: SupportLanguage; systemPreference?: GetUserSystemEnum; isLoggingIn: boolean; + businessWritePermission: boolean; }; const initialState: UserReduxState = { @@ -46,7 +47,8 @@ const initialState: UserReduxState = { DEFAULT_LANGUAGE ) as SupportLanguage, systemPreference: undefined, - isLoggingIn: false + isLoggingIn: false, + businessWritePermission: true }; const user = createSlice({ @@ -123,6 +125,12 @@ const user = createSlice({ }, updateIsLoggingIn: (state, { payload }: PayloadAction) => { state.isLoggingIn = payload; + }, + updateBusinessWritePermission: ( + state, + { payload }: PayloadAction + ) => { + state.businessWritePermission = payload; } } }); @@ -137,7 +145,8 @@ export const { updateUserUid, updateUserInfoFetchStatus, updateSystemPreference, - updateIsLoggingIn + updateIsLoggingIn, + updateBusinessWritePermission } = user.actions; export default user.reducer; diff --git a/packages/dms-kit/src/enum/index.ts b/packages/dms-kit/src/enum/index.ts index e830d1228..ad8faa839 100644 --- a/packages/dms-kit/src/enum/index.ts +++ b/packages/dms-kit/src/enum/index.ts @@ -30,8 +30,7 @@ export enum SystemRole { admin = 'admin', certainProjectManager = 'certainProjectManager', auditAdministrator = 'auditAdministrator', - systemAdministrator = 'systemAdministrator', - projectDirector = 'projectDirector' + systemAdministrator = 'systemAdministrator' } export type UserRolesType = { @@ -74,7 +73,6 @@ export enum StorageKey { * 后端暂时无法在swagger中暴露OpPermissionType的映射,但权限对应的uid一般不会变化,因此前端先保存一份。 */ export enum OpPermissionTypeUid { - 'project_director' = '700001', // 项目总监;创建项目的用户自动拥有该项目管理权限 700001 'project_admin' = '700002', // 项目管理;拥有该权限的用户可以管理项目下的所有资源 700002 'create_workflow' = '700003', // 创建/编辑工单;拥有该权限的用户可以创建/编辑工单 700003 'audit_workflow' = '700004', // 审核/驳回工单;拥有该权限的用户可以审核/驳回工单 700004 diff --git a/packages/shared/lib/features/useCurrentUser/index.ts b/packages/shared/lib/features/useCurrentUser/index.ts index 918ef21fc..3d86ac34b 100644 --- a/packages/shared/lib/features/useCurrentUser/index.ts +++ b/packages/shared/lib/features/useCurrentUser/index.ts @@ -24,7 +24,8 @@ const useCurrentUser = () => { isUserInfoFetched, userId, language, - systemPreference + systemPreference, + businessWritePermission } = useSelector((state: IReduxState) => { return { username: state.user.username, @@ -35,7 +36,8 @@ const useCurrentUser = () => { isUserInfoFetched: state.user.isUserInfoFetched, userId: state.user.uid, language: state.user.language, - systemPreference: state.user.systemPreference + systemPreference: state.user.systemPreference, + businessWritePermission: state.user.businessWritePermission }; }); @@ -76,9 +78,6 @@ const useCurrentUser = () => { ), [SystemRole.systemAdministrator]: managementPermissions.some( (v) => v.uid === OpPermissionTypeUid.system_administrator - ), - [SystemRole.projectDirector]: managementPermissions.some( - (v) => v.uid === OpPermissionTypeUid.project_director ) }; }, [isAdmin, isCertainProjectManager, managementPermissions]); @@ -98,7 +97,8 @@ const useCurrentUser = () => { userRoles, language, updateLanguage, - systemPreference + systemPreference, + businessWritePermission }; }; export default useCurrentUser; diff --git a/packages/shared/lib/features/usePermission/permissionManifest.ts b/packages/shared/lib/features/usePermission/permissionManifest.ts index dc8caf4d0..5403ca732 100644 --- a/packages/shared/lib/features/usePermission/permissionManifest.ts +++ b/packages/shared/lib/features/usePermission/permissionManifest.ts @@ -1108,29 +1108,17 @@ export const PERMISSION_MANIFEST: Record< [PERMISSIONS.ACTIONS.BASE.PROJECT_MANAGER.IMPORT]: { id: PERMISSIONS.ACTIONS.BASE.PROJECT_MANAGER.IMPORT, type: 'action', - role: [ - SystemRole.admin, - SystemRole.systemAdministrator, - SystemRole.projectDirector - ] + role: [SystemRole.admin, SystemRole.systemAdministrator] }, [PERMISSIONS.ACTIONS.BASE.PROJECT_MANAGER.EXPORT]: { id: PERMISSIONS.ACTIONS.BASE.PROJECT_MANAGER.EXPORT, type: 'action', - role: [ - SystemRole.admin, - SystemRole.systemAdministrator, - SystemRole.projectDirector - ] + role: [SystemRole.admin, SystemRole.systemAdministrator] }, [PERMISSIONS.ACTIONS.BASE.PROJECT_MANAGER.CREATE]: { id: PERMISSIONS.ACTIONS.BASE.PROJECT_MANAGER.CREATE, type: 'action', - role: [ - SystemRole.admin, - SystemRole.systemAdministrator, - SystemRole.projectDirector - ] + role: [SystemRole.admin, SystemRole.systemAdministrator] }, [PERMISSIONS.ACTIONS.BASE.PROJECT_MANAGER.EDIT]: { id: PERMISSIONS.ACTIONS.BASE.PROJECT_MANAGER.EDIT, diff --git a/packages/shared/lib/features/useUserInfo/index.ts b/packages/shared/lib/features/useUserInfo/index.ts index ea48d43be..f71d4a2c6 100644 --- a/packages/shared/lib/features/useUserInfo/index.ts +++ b/packages/shared/lib/features/useUserInfo/index.ts @@ -11,7 +11,8 @@ import { updateUserInfoFetchStatus, updateUserUid, updateLanguage, - updateSystemPreference + updateSystemPreference, + updateBusinessWritePermission } from '../../../../base/src/store/user'; import { ResponseCode, @@ -61,6 +62,8 @@ const useUserInfo = () => { }) ); + dispatch(updateBusinessWritePermission(true)); + dispatch(updateUserInfoFetchStatus(false)); }, [dispatch]); @@ -110,6 +113,13 @@ const useUserInfo = () => { }) ); + dispatch( + updateBusinessWritePermission( + (data as Record)?.business_write_permission !== + false + ) + ); + if (!systemPreference) { dispatch( updateSystemPreference({ From f6eb12e5e270b1c34c0dda1d8166ae0828e117a6 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Fri, 8 May 2026 10:35:51 +0000 Subject: [PATCH 02/24] test(user): update test files for businessWritePermission and projectDirector removal - Add businessWritePermission to user store test initial state and all assertions - Add test case for updateBusinessWritePermission action - Remove SystemRole.projectDirector from all test userRoles objects - Update mockCurrentUserReturn: replace project_director with system_administrator, remove projectDirector from userRoles, add businessWritePermission - Update SystemRoleTagList test: remove project_director test case Refs: dms-ee#813 --- packages/base/src/App.test.tsx | 3 +- .../__tests__/index.test.tsx | 5 - .../src/page/CloudBeaver/List/index.test.tsx | 3 +- .../DefaultScene/__tests__/index.ce.test.tsx | 3 +- .../DefaultScene/__tests__/index.test.tsx | 3 +- .../Modal/CompanyNoticeModal/index.test.tsx | 3 +- .../base/src/page/Project/List/index.test.tsx | 1 - packages/base/src/page/Project/index.test.tsx | 12 +- .../page/SyncDataSource/List/index.test.tsx | 1 - .../SMSSetting/__tests__/index.test.tsx | 3 +- .../System/PersonalizeSetting/index.test.tsx | 1 - .../CodingSetting/__tests__/index.test.tsx | 3 +- .../page/UserCenter/__tests__/index.test.tsx | 1 - .../UserList/__tests__/UserList.test.tsx | 2 - packages/base/src/store/user/index.test.ts | 55 +- .../page/DatabaseAccount/List/index.test.tsx | 599 ++++++++++++++++++ .../ExpirationAccount/index.test.tsx | 206 ++++++ .../lib/features/useCurrentUser/index.test.ts | 2 - .../usePermission/__tests__/index.test.ts | 8 - .../shared/lib/testUtil/mockHook/data.tsx | 10 +- .../page/OperationRecord/List/index.test.tsx | 3 +- .../Detail/__tests__/index.test.tsx | 1 - 22 files changed, 865 insertions(+), 63 deletions(-) create mode 100644 packages/provision/src/page/DatabaseAccount/List/index.test.tsx create mode 100644 packages/provision/src/page/DatabaseAccountPassword/ExpirationAccount/index.test.tsx diff --git a/packages/base/src/App.test.tsx b/packages/base/src/App.test.tsx index b01430f44..8b27be1d1 100644 --- a/packages/base/src/App.test.tsx +++ b/packages/base/src/App.test.tsx @@ -297,8 +297,7 @@ describe('App', () => { [SystemRole.admin]: false, [SystemRole.certainProjectManager]: false, [SystemRole.systemAdministrator]: false, - [SystemRole.auditAdministrator]: false, - [SystemRole.projectDirector]: false + [SystemRole.auditAdministrator]: false } }); baseSuperRender(); diff --git a/packages/base/src/components/SystemRoleTagList/__tests__/index.test.tsx b/packages/base/src/components/SystemRoleTagList/__tests__/index.test.tsx index dec67983d..8f4a89684 100644 --- a/packages/base/src/components/SystemRoleTagList/__tests__/index.test.tsx +++ b/packages/base/src/components/SystemRoleTagList/__tests__/index.test.tsx @@ -16,10 +16,6 @@ describe('PermissionTagList', () => { it('should render permission tags with correct colors', () => { const roles = [ - { - uid: OpPermissionTypeUid.project_director, - name: '项目总监' - }, { uid: OpPermissionTypeUid.audit_administrator, name: '审计管理员' @@ -36,7 +32,6 @@ describe('PermissionTagList', () => { superRender(); - expect(screen.getByText('项目总监')).toBeInTheDocument(); expect(screen.getByText('审计管理员')).toBeInTheDocument(); expect(screen.getByText('系统管理员')).toBeInTheDocument(); expect(screen.getByText('创建工单')).toBeInTheDocument(); diff --git a/packages/base/src/page/CloudBeaver/List/index.test.tsx b/packages/base/src/page/CloudBeaver/List/index.test.tsx index d4dedacf0..79f837506 100644 --- a/packages/base/src/page/CloudBeaver/List/index.test.tsx +++ b/packages/base/src/page/CloudBeaver/List/index.test.tsx @@ -250,8 +250,7 @@ describe('test base/CloudBeaver/List', () => { [SystemRole.admin]: false, [SystemRole.certainProjectManager]: false, [SystemRole.auditAdministrator]: false, - [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: false + [SystemRole.systemAdministrator]: false }, bindProjects: [ { diff --git a/packages/base/src/page/Home/DefaultScene/__tests__/index.ce.test.tsx b/packages/base/src/page/Home/DefaultScene/__tests__/index.ce.test.tsx index b79e0a5f0..0f050adc8 100644 --- a/packages/base/src/page/Home/DefaultScene/__tests__/index.ce.test.tsx +++ b/packages/base/src/page/Home/DefaultScene/__tests__/index.ce.test.tsx @@ -20,8 +20,7 @@ describe('test base/home/CEDefaultScene', () => { [SystemRole.admin]: false, [SystemRole.certainProjectManager]: false, [SystemRole.systemAdministrator]: false, - [SystemRole.auditAdministrator]: false, - [SystemRole.projectDirector]: false + [SystemRole.auditAdministrator]: false } }); diff --git a/packages/base/src/page/Home/DefaultScene/__tests__/index.test.tsx b/packages/base/src/page/Home/DefaultScene/__tests__/index.test.tsx index 62407a414..7580f2b4d 100644 --- a/packages/base/src/page/Home/DefaultScene/__tests__/index.test.tsx +++ b/packages/base/src/page/Home/DefaultScene/__tests__/index.test.tsx @@ -39,8 +39,7 @@ describe('test base/home/DefaultScene', () => { [SystemRole.admin]: false, [SystemRole.certainProjectManager]: false, [SystemRole.systemAdministrator]: false, - [SystemRole.auditAdministrator]: false, - [SystemRole.projectDirector]: false + [SystemRole.auditAdministrator]: false } }); diff --git a/packages/base/src/page/Nav/SideMenu/UserMenu/Modal/CompanyNoticeModal/index.test.tsx b/packages/base/src/page/Nav/SideMenu/UserMenu/Modal/CompanyNoticeModal/index.test.tsx index 7280f7364..9b370e972 100644 --- a/packages/base/src/page/Nav/SideMenu/UserMenu/Modal/CompanyNoticeModal/index.test.tsx +++ b/packages/base/src/page/Nav/SideMenu/UserMenu/Modal/CompanyNoticeModal/index.test.tsx @@ -181,8 +181,7 @@ describe('base/page/Nav/SideMenu/UserMenu/CompanyNoticeModal', () => { [SystemRole.admin]: false, [SystemRole.systemAdministrator]: false, [SystemRole.auditAdministrator]: false, - [SystemRole.certainProjectManager]: false, - [SystemRole.projectDirector]: false + [SystemRole.certainProjectManager]: false } }); }); diff --git a/packages/base/src/page/Project/List/index.test.tsx b/packages/base/src/page/Project/List/index.test.tsx index 185119447..ffb2fc8ae 100644 --- a/packages/base/src/page/Project/List/index.test.tsx +++ b/packages/base/src/page/Project/List/index.test.tsx @@ -201,7 +201,6 @@ describe('test base/project/list', () => { [SystemRole.admin]: false, [SystemRole.certainProjectManager]: false, [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: false, [SystemRole.auditAdministrator]: false } }); diff --git a/packages/base/src/page/Project/index.test.tsx b/packages/base/src/page/Project/index.test.tsx index f42bb4b74..9ef471ca1 100644 --- a/packages/base/src/page/Project/index.test.tsx +++ b/packages/base/src/page/Project/index.test.tsx @@ -78,8 +78,7 @@ describe('test base/page/project', () => { userRoles: { ...mockCurrentUserReturn.userRoles, [SystemRole.admin]: true, - [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: false + [SystemRole.systemAdministrator]: false } }); baseSuperRender(); @@ -102,8 +101,7 @@ describe('test base/page/project', () => { userRoles: { ...mockCurrentUserReturn.userRoles, [SystemRole.admin]: false, - [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: true + [SystemRole.systemAdministrator]: false } }); baseSuperRender(); @@ -123,8 +121,7 @@ describe('test base/page/project', () => { userRoles: { ...mockCurrentUserReturn.userRoles, [SystemRole.admin]: false, - [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: false + [SystemRole.systemAdministrator]: false } }); baseSuperRender(); @@ -167,8 +164,7 @@ describe('test base/page/project', () => { userRoles: { ...mockCurrentUserReturn.userRoles, [SystemRole.admin]: true, - [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: false + [SystemRole.systemAdministrator]: false } }); baseSuperRender(); diff --git a/packages/base/src/page/SyncDataSource/List/index.test.tsx b/packages/base/src/page/SyncDataSource/List/index.test.tsx index 93c778999..51ca0a27b 100644 --- a/packages/base/src/page/SyncDataSource/List/index.test.tsx +++ b/packages/base/src/page/SyncDataSource/List/index.test.tsx @@ -110,7 +110,6 @@ describe('page/SyncDataSource/SyncTaskList', () => { [SystemRole.admin]: false, [SystemRole.systemAdministrator]: false, [SystemRole.auditAdministrator]: false, - [SystemRole.projectDirector]: false, [SystemRole.certainProjectManager]: true } }); diff --git a/packages/base/src/page/System/LoginConnection/SMSSetting/__tests__/index.test.tsx b/packages/base/src/page/System/LoginConnection/SMSSetting/__tests__/index.test.tsx index 1642f6d93..c5f090236 100644 --- a/packages/base/src/page/System/LoginConnection/SMSSetting/__tests__/index.test.tsx +++ b/packages/base/src/page/System/LoginConnection/SMSSetting/__tests__/index.test.tsx @@ -47,8 +47,7 @@ describe('base/System/GlobalSetting/SMSSetting', () => { [SystemRole.admin]: false, [SystemRole.systemAdministrator]: false, [SystemRole.certainProjectManager]: true, - [SystemRole.auditAdministrator]: true, - [SystemRole.projectDirector]: true + [SystemRole.auditAdministrator]: true } }); customRender(); diff --git a/packages/base/src/page/System/PersonalizeSetting/index.test.tsx b/packages/base/src/page/System/PersonalizeSetting/index.test.tsx index c73f528a7..55dd2c022 100644 --- a/packages/base/src/page/System/PersonalizeSetting/index.test.tsx +++ b/packages/base/src/page/System/PersonalizeSetting/index.test.tsx @@ -49,7 +49,6 @@ describe('base/System/PersonalizeSetting', () => { [SystemRole.admin]: false, [SystemRole.systemAdministrator]: false, [SystemRole.auditAdministrator]: false, - [SystemRole.projectDirector]: false, [SystemRole.certainProjectManager]: false } }); diff --git a/packages/base/src/page/System/ProcessConnection/CodingSetting/__tests__/index.test.tsx b/packages/base/src/page/System/ProcessConnection/CodingSetting/__tests__/index.test.tsx index 974145d86..7d80c7236 100644 --- a/packages/base/src/page/System/ProcessConnection/CodingSetting/__tests__/index.test.tsx +++ b/packages/base/src/page/System/ProcessConnection/CodingSetting/__tests__/index.test.tsx @@ -48,8 +48,7 @@ describe('base/System/ProcessConnection/CodingSetting', () => { [SystemRole.admin]: false, [SystemRole.systemAdministrator]: false, [SystemRole.certainProjectManager]: true, - [SystemRole.auditAdministrator]: true, - [SystemRole.projectDirector]: true + [SystemRole.auditAdministrator]: true } }); const { baseElement } = customRender(); diff --git a/packages/base/src/page/UserCenter/__tests__/index.test.tsx b/packages/base/src/page/UserCenter/__tests__/index.test.tsx index eb9aa04fc..009de212e 100644 --- a/packages/base/src/page/UserCenter/__tests__/index.test.tsx +++ b/packages/base/src/page/UserCenter/__tests__/index.test.tsx @@ -138,7 +138,6 @@ describe('base/UserCenter', () => { [SystemRole.admin]: false, [SystemRole.systemAdministrator]: false, [SystemRole.auditAdministrator]: true, - [SystemRole.projectDirector]: true, [SystemRole.certainProjectManager]: true } }); diff --git a/packages/base/src/page/UserCenter/components/UserList/__tests__/UserList.test.tsx b/packages/base/src/page/UserCenter/components/UserList/__tests__/UserList.test.tsx index aad7deed5..59fbed5ae 100644 --- a/packages/base/src/page/UserCenter/components/UserList/__tests__/UserList.test.tsx +++ b/packages/base/src/page/UserCenter/components/UserList/__tests__/UserList.test.tsx @@ -179,7 +179,6 @@ describe('base/UserCenter/UserList', () => { [SystemRole.admin]: false, [SystemRole.auditAdministrator]: true, [SystemRole.systemAdministrator]: true, - [SystemRole.projectDirector]: true, [SystemRole.certainProjectManager]: true } }); @@ -199,7 +198,6 @@ describe('base/UserCenter/UserList', () => { [SystemRole.admin]: false, [SystemRole.auditAdministrator]: true, [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: false, [SystemRole.certainProjectManager]: true } }); diff --git a/packages/base/src/store/user/index.test.ts b/packages/base/src/store/user/index.test.ts index ef96c401d..0d18ebfa9 100644 --- a/packages/base/src/store/user/index.test.ts +++ b/packages/base/src/store/user/index.test.ts @@ -8,7 +8,8 @@ import reducers, { updateManagementPermissions, updateLanguage, updateSystemPreference, - updateIsLoggingIn + updateIsLoggingIn, + updateBusinessWritePermission } from '.'; import { IReduxState } from '..'; import { LocalStorageWrapper } from '@actiontech/dms-kit'; @@ -35,7 +36,8 @@ describe('store user', () => { isUserInfoFetched: false, language: SupportLanguage.zhCN, systemPreference: undefined, - isLoggingIn: false + isLoggingIn: false, + businessWritePermission: true }; it('should update token when dispatch updateToken action', () => { @@ -57,7 +59,8 @@ describe('store user', () => { role: '', isUserInfoFetched: false, systemPreference: undefined, - isLoggingIn: false + isLoggingIn: false, + businessWritePermission: true }); }); @@ -80,7 +83,8 @@ describe('store user', () => { role: '', isUserInfoFetched: false, systemPreference: undefined, - isLoggingIn: false + isLoggingIn: false, + businessWritePermission: true }); }); @@ -104,7 +108,8 @@ describe('store user', () => { role: '', isUserInfoFetched: false, systemPreference: undefined, - isLoggingIn: false + isLoggingIn: false, + businessWritePermission: true }); }); @@ -128,7 +133,8 @@ describe('store user', () => { role: SystemRole.admin, isUserInfoFetched: false, systemPreference: undefined, - isLoggingIn: false + isLoggingIn: false, + businessWritePermission: true }); }); @@ -151,7 +157,8 @@ describe('store user', () => { role: '', isUserInfoFetched: false, systemPreference: undefined, - isLoggingIn: false + isLoggingIn: false, + businessWritePermission: true }); }); @@ -169,7 +176,8 @@ describe('store user', () => { role: '', isUserInfoFetched: true, systemPreference: undefined, - isLoggingIn: false + isLoggingIn: false, + businessWritePermission: true }); }); @@ -200,7 +208,8 @@ describe('store user', () => { role: '', isUserInfoFetched: false, systemPreference: undefined, - isLoggingIn: false + isLoggingIn: false, + businessWritePermission: true }); }); @@ -224,7 +233,8 @@ describe('store user', () => { role: '', isUserInfoFetched: false, systemPreference: undefined, - isLoggingIn: false + isLoggingIn: false, + businessWritePermission: true }); }); @@ -247,7 +257,8 @@ describe('store user', () => { role: '', isUserInfoFetched: false, systemPreference: GetUserSystemEnum.MANAGEMENT, - isLoggingIn: false + isLoggingIn: false, + businessWritePermission: true }); }); @@ -265,7 +276,27 @@ describe('store user', () => { role: '', isUserInfoFetched: false, systemPreference: undefined, - isLoggingIn: true + isLoggingIn: true, + businessWritePermission: true + }); + }); + + it('should update businessWritePermission when dispatch updateBusinessWritePermission action', () => { + const newState = reducers(state, updateBusinessWritePermission(false)); + expect(newState).not.toBe(state); + expect(newState).toEqual({ + username: '', + uid: '', + token: '', + theme: SupportTheme.LIGHT, + language: SupportLanguage.zhCN, + bindProjects: [], + managementPermissions: [], + role: '', + isUserInfoFetched: false, + systemPreference: undefined, + isLoggingIn: false, + businessWritePermission: false }); }); }); diff --git a/packages/provision/src/page/DatabaseAccount/List/index.test.tsx b/packages/provision/src/page/DatabaseAccount/List/index.test.tsx new file mode 100644 index 000000000..d198f3a67 --- /dev/null +++ b/packages/provision/src/page/DatabaseAccount/List/index.test.tsx @@ -0,0 +1,599 @@ +import { superRender } from '@actiontech/shared/lib/testUtil/superRender'; +import { act, cleanup, fireEvent, screen } from '@testing-library/react'; +import { useDispatch } from 'react-redux'; +import { + getAllBySelector, + getBySelector +} from '@actiontech/shared/lib/testUtil/customQuery'; +import { useNavigate } from 'react-router-dom'; +import dbAccountService from '@actiontech/shared/lib/testUtil/mockApi/provision/dbAccountService'; +import { dbAccountMockData } from '@actiontech/shared/lib/testUtil/mockApi/provision/dbAccountService/data'; +import { mockProjectInfo } from '@actiontech/shared/lib/testUtil/mockHook/data'; +import { mockUseCurrentProject } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentProject'; +import { mockUseCurrentUser } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentUser'; +import { mockUseDbServiceDriver } from '@actiontech/shared/lib/testUtil/mockHook/mockUseDbServiceDriver'; +import auth from '@actiontech/shared/lib/testUtil/mockApi/provision/auth'; +import DatabaseAccountList from './index'; +import RecoilObservable from '../../../testUtil/RecoilObservable'; +import { DatabaseAccountModalStatus } from '../../../store/databaseAccount'; +import { EventEmitterKey, ModalName } from '../../../data/enum'; +import { createSpySuccessResponse } from '@actiontech/shared/lib/testUtil/mockApi'; +import { ListDBAccountStatusEnum } from '@actiontech/shared/lib/api/provision/service/common.enum'; +import EventEmitter from '../../../utils/EventEmitter'; +import user from '@actiontech/shared/lib/testUtil/mockApi/provision/user'; +import { mockUsePermission } from '@actiontech/shared/lib/testUtil/mockHook/mockUsePermission'; +import { MemberGroupService } from '@actiontech/shared/lib/api/base'; +import { IListMemberGroup } from '@actiontech/shared/lib/api/base/service/common'; +import { + ignoreConsoleErrors, + UtilsConsoleErrorStringsEnum +} from '@actiontech/shared/lib/testUtil/common'; +import { paramsSerializer, SystemRole } from '@actiontech/dms-kit'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), + useDispatch: jest.fn() +})); + +jest.mock('react-router-dom', () => { + return { + ...jest.requireActual('react-router-dom'), + useNavigate: jest.fn() + }; +}); + +let authListDBAccountSpy: jest.SpyInstance; +let authGetAccountStaticsSpy: jest.SpyInstance; +let authUpdateDBAccountSpy: jest.SpyInstance; +let authDelDBAccountSpy: jest.SpyInstance; +let authListServicesSpy: jest.SpyInstance; +let authListMemberGroupSpy: jest.SpyInstance; +const dispatchSpy = jest.fn(); +const navigateSpy = jest.fn(); + +const memberGroupList: IListMemberGroup[] = [ + { + name: 'member-group1', + uid: '10029384' + }, + { + name: 'member-group2', + uid: '10039482' + }, + { + name: 'member-group3', + uid: '10039483' + }, + { + name: 'member-group9', + uid: '10029385' + } +]; + +describe('provision/DatabaseAccount/List', () => { + ignoreConsoleErrors([UtilsConsoleErrorStringsEnum.UNKNOWN_EVENT_HANDLER]); + + beforeEach(() => { + authListDBAccountSpy = dbAccountService.authListDBAccount(); + authGetAccountStaticsSpy = dbAccountService.authGetAccountStatics(); + authUpdateDBAccountSpy = dbAccountService.authUpdateDBAccount(); + authDelDBAccountSpy = dbAccountService.authDelDBAccount(); + authListServicesSpy = auth.listServices(); + auth.mockAllApi(); + user.mockAllApi(); + authListMemberGroupSpy = jest + .spyOn(MemberGroupService, 'ListMemberGroupTips') + .mockImplementation(() => + createSpySuccessResponse({ data: memberGroupList }) + ); + mockUseCurrentUser(); + mockUseDbServiceDriver(); + mockUseCurrentProject(); + mockUsePermission(undefined, { mockSelector: true }); + jest.useFakeTimers(); + + (useDispatch as jest.Mock).mockReturnValue(dispatchSpy); + (useNavigate as jest.Mock).mockImplementation(() => navigateSpy); + }); + + afterEach(() => { + jest.useRealTimers(); + jest.clearAllMocks(); + cleanup(); + }); + + const customRender = () => { + dbAccountService.authGetDBAccount(); + authListDBAccountSpy.mockClear(); + authListDBAccountSpy.mockImplementation(() => + createSpySuccessResponse({ data: [dbAccountMockData[0]] }) + ); + const modalStatusChangeSpy = jest.fn(); + superRender( + <> + + + + ); + return { modalStatusChangeSpy }; + }; + + it('render init snap', async () => { + const { baseElement } = superRender(); + await act(async () => jest.advanceTimersByTime(3000)); + expect(baseElement).toMatchSnapshot(); + expect(authListDBAccountSpy).toHaveBeenCalled(); + expect(authGetAccountStaticsSpy).toHaveBeenCalled(); + expect(authListServicesSpy).toHaveBeenCalled(); + expect(screen.getByText('账号发现')).toBeInTheDocument(); + expect(screen.getByText('创建账号')).toBeInTheDocument(); + expect(screen.getByText('批量修改密码')).toBeInTheDocument(); + expect(screen.getByText('批量修改密码').closest('button')).toHaveAttribute( + 'disabled' + ); + expect( + getAllBySelector('.actiontech-table-actions-more-button') + ).toHaveLength(5); + }); + + it('render update table filter', async () => { + const { baseElement } = superRender(); + await act(async () => jest.advanceTimersByTime(3000)); + expect(authListDBAccountSpy).toHaveBeenCalledTimes(1); + expect(authListMemberGroupSpy).toHaveBeenCalledTimes(1); + fireEvent.click(screen.getByText('test1')); + await act(async () => jest.advanceTimersByTime(3000)); + expect(authListDBAccountSpy).toHaveBeenCalledTimes(2); + await act(async () => jest.advanceTimersByTime(3000)); + fireEvent.click(getAllBySelector('.ant-tag')[0]); + expect(authListDBAccountSpy).toHaveBeenCalledTimes(3); + expect(authListDBAccountSpy).toHaveBeenNthCalledWith( + 3, + { + page_index: 1, + page_size: 20, + filter_by_db_service: '1793883708181188608', + filter_by_users: '1767103833235787776', + fuzzy_keyword: '', + project_uid: mockProjectInfo.projectID + }, + { paramsSerializer } + ); + expect(baseElement).toMatchSnapshot(); + }); + + it('filter data with search', async () => { + superRender(); + expect(authListDBAccountSpy).toHaveBeenCalled(); + const searchText = 'search text'; + const inputEle = getBySelector('#actiontech-table-search-input'); + fireEvent.change(inputEle, { + target: { value: searchText } + }); + + await act(async () => { + fireEvent.keyDown(inputEle, { + key: 'Enter', + code: 'Enter', + keyCode: 13 + }); + await jest.advanceTimersByTime(300); + }); + await act(async () => jest.advanceTimersByTime(3000)); + expect(authListDBAccountSpy).toHaveBeenNthCalledWith( + 2, + { + page_index: 1, + page_size: 20, + fuzzy_keyword: searchText, + project_uid: mockProjectInfo.projectID + }, + { paramsSerializer } + ); + }); + + it('render emit "Refresh_Account_Management_List_Table" event', async () => { + superRender(); + await act(async () => jest.advanceTimersByTime(3000)); + expect(authListDBAccountSpy).toHaveBeenCalledTimes(1); + await act(async () => + EventEmitter.emit(EventEmitterKey.Refresh_Account_Management_List_Table) + ); + await act(async () => jest.advanceTimersByTime(3000)); + expect(authListDBAccountSpy).toHaveBeenCalledTimes(2); + await act(async () => + EventEmitter.emit( + EventEmitterKey.Refresh_Account_Management_List_Table, + 'filter_by_db_service', + '1793883708181188608' + ) + ); + await act(async () => jest.advanceTimersByTime(3000)); + expect(authListDBAccountSpy).toHaveBeenCalledTimes(3); + expect(authListDBAccountSpy).toHaveBeenNthCalledWith( + 3, + { + page_index: 1, + page_size: 20, + filter_by_db_service: '1793883708181188608', + fuzzy_keyword: '', + project_uid: mockProjectInfo.projectID + }, + { paramsSerializer } + ); + }); + + it('render account discovery', async () => { + const modalStatusChangeSpy = jest.fn(); + superRender( + <> + + + + ); + await act(async () => jest.advanceTimersByTime(3000)); + fireEvent.click(screen.getByText('账号发现')); + await act(async () => jest.advanceTimersByTime(100)); + expect(modalStatusChangeSpy).toHaveBeenCalledTimes(2); + expect(modalStatusChangeSpy).toHaveBeenNthCalledWith(2, { + [ModalName.DatabaseAccountDiscoveryModal]: true, + [ModalName.DatabaseAccountDetailModal]: false, + [ModalName.DatabaseAccountAuthorizeModal]: false, + [ModalName.DatabaseAccountModifyPasswordModal]: false, + [ModalName.DatabaseAccountRenewalPasswordModal]: false, + [ModalName.DatabaseAccountBatchModifyPasswordModal]: false, + [ModalName.DatabaseAccountManagePasswordModal]: false + }); + }); + + it('render batch modify password', async () => { + const modalStatusChangeSpy = jest.fn(); + superRender( + <> + + + + ); + await act(async () => jest.advanceTimersByTime(3000)); + fireEvent.click(getBySelector('.ant-table-thead .ant-checkbox-input')); + await act(async () => jest.advanceTimersByTime(100)); + expect( + screen.getByText('批量修改密码').closest('button') + ).not.toHaveAttribute('disabled'); + fireEvent.click(screen.getByText('批量修改密码')); + await act(async () => jest.advanceTimersByTime(100)); + expect(modalStatusChangeSpy).toHaveBeenCalledTimes(2); + expect(modalStatusChangeSpy).toHaveBeenNthCalledWith(2, { + [ModalName.DatabaseAccountDiscoveryModal]: false, + [ModalName.DatabaseAccountDetailModal]: false, + [ModalName.DatabaseAccountAuthorizeModal]: false, + [ModalName.DatabaseAccountModifyPasswordModal]: false, + [ModalName.DatabaseAccountRenewalPasswordModal]: false, + [ModalName.DatabaseAccountBatchModifyPasswordModal]: true, + [ModalName.DatabaseAccountManagePasswordModal]: false + }); + }); + + it('render check account detail', async () => { + const { modalStatusChangeSpy } = customRender(); + await act(async () => jest.advanceTimersByTime(3000)); + fireEvent.click(screen.getByText('查 看')); + expect(modalStatusChangeSpy).toHaveBeenCalledTimes(2); + expect(modalStatusChangeSpy).toHaveBeenNthCalledWith(2, { + [ModalName.DatabaseAccountDiscoveryModal]: false, + [ModalName.DatabaseAccountDetailModal]: true, + [ModalName.DatabaseAccountAuthorizeModal]: false, + [ModalName.DatabaseAccountModifyPasswordModal]: false, + [ModalName.DatabaseAccountRenewalPasswordModal]: false, + [ModalName.DatabaseAccountBatchModifyPasswordModal]: false, + [ModalName.DatabaseAccountManagePasswordModal]: false + }); + }); + + it('render authorize', async () => { + const { modalStatusChangeSpy } = customRender(); + await act(async () => jest.advanceTimersByTime(3000)); + expect(screen.getByText('授 权')).toBeInTheDocument(); + fireEvent.click(screen.getByText('授 权')); + await act(async () => jest.advanceTimersByTime(100)); + expect(modalStatusChangeSpy).toHaveBeenCalledTimes(2); + expect(modalStatusChangeSpy).toHaveBeenNthCalledWith(2, { + [ModalName.DatabaseAccountDiscoveryModal]: false, + [ModalName.DatabaseAccountDetailModal]: false, + [ModalName.DatabaseAccountAuthorizeModal]: true, + [ModalName.DatabaseAccountModifyPasswordModal]: false, + [ModalName.DatabaseAccountRenewalPasswordModal]: false, + [ModalName.DatabaseAccountBatchModifyPasswordModal]: false, + [ModalName.DatabaseAccountManagePasswordModal]: false + }); + }); + + it('render modify password', async () => { + const { modalStatusChangeSpy } = customRender(); + await act(async () => jest.advanceTimersByTime(3000)); + fireEvent.click(getBySelector('.actiontech-table-actions-more-button')); + await act(async () => jest.advanceTimersByTime(100)); + expect(screen.getByText('修改密码')).toBeInTheDocument(); + fireEvent.click(screen.getByText('修改密码')); + expect(modalStatusChangeSpy).toHaveBeenCalledTimes(2); + expect(modalStatusChangeSpy).toHaveBeenNthCalledWith(2, { + [ModalName.DatabaseAccountDiscoveryModal]: false, + [ModalName.DatabaseAccountDetailModal]: false, + [ModalName.DatabaseAccountAuthorizeModal]: false, + [ModalName.DatabaseAccountModifyPasswordModal]: true, + [ModalName.DatabaseAccountRenewalPasswordModal]: false, + [ModalName.DatabaseAccountBatchModifyPasswordModal]: false, + [ModalName.DatabaseAccountManagePasswordModal]: false + }); + }); + + it('render renewal password', async () => { + const { modalStatusChangeSpy } = customRender(); + await act(async () => jest.advanceTimersByTime(3000)); + fireEvent.click(getBySelector('.actiontech-table-actions-more-button')); + await act(async () => jest.advanceTimersByTime(100)); + expect(screen.getByText('续用当前密码')).toBeInTheDocument(); + fireEvent.click(screen.getByText('续用当前密码')); + expect(modalStatusChangeSpy).toHaveBeenCalledTimes(2); + expect(modalStatusChangeSpy).toHaveBeenNthCalledWith(2, { + [ModalName.DatabaseAccountDiscoveryModal]: false, + [ModalName.DatabaseAccountDetailModal]: false, + [ModalName.DatabaseAccountAuthorizeModal]: false, + [ModalName.DatabaseAccountModifyPasswordModal]: false, + [ModalName.DatabaseAccountRenewalPasswordModal]: true, + [ModalName.DatabaseAccountBatchModifyPasswordModal]: false, + [ModalName.DatabaseAccountManagePasswordModal]: false + }); + }); + + it('render update permission', async () => { + customRender(); + await act(async () => jest.advanceTimersByTime(3000)); + fireEvent.click(getBySelector('.actiontech-table-actions-more-button')); + await act(async () => jest.advanceTimersByTime(100)); + expect(screen.getByText('变更账号权限')).toBeInTheDocument(); + fireEvent.click(screen.getByText('变更账号权限')); + await act(async () => jest.advanceTimersByTime(100)); + expect(navigateSpy).toHaveBeenCalledTimes(1); + expect(navigateSpy).toHaveBeenNthCalledWith( + 1, + `/provision/project/${mockProjectInfo.projectID}/database-account/update/${dbAccountMockData[0].db_account_uid}` + ); + }); + + it('render unlock account', async () => { + customRender(); + await act(async () => jest.advanceTimersByTime(3000)); + fireEvent.click(getBySelector('.actiontech-table-actions-more-button')); + await act(async () => jest.advanceTimersByTime(100)); + expect(screen.getByText('启用')).toBeInTheDocument(); + expect(getBySelector('.ant-popover .ant-popover-inner')).toMatchSnapshot(); + fireEvent.click(screen.getByText('启用')); + await act(async () => jest.advanceTimersByTime(100)); + expect(authUpdateDBAccountSpy).toHaveBeenCalledTimes(1); + expect(authUpdateDBAccountSpy).toHaveBeenNthCalledWith(1, { + project_uid: mockProjectInfo.projectID, + db_account_uid: dbAccountMockData[0].db_account_uid, + db_account: { + lock: false + } + }); + await act(async () => jest.advanceTimersByTime(2900)); + expect(screen.getByText('账号已启用')).toBeInTheDocument(); + }); + + it('render lock account', async () => { + dbAccountService.authGetDBAccount(); + authListDBAccountSpy.mockClear(); + authListDBAccountSpy.mockImplementation(() => + createSpySuccessResponse({ + data: [ + { + ...dbAccountMockData[0], + status: ListDBAccountStatusEnum.unlock + } + ] + }) + ); + const modalStatusChangeSpy = jest.fn(); + superRender( + <> + + + + ); + await act(async () => jest.advanceTimersByTime(3000)); + fireEvent.click(getBySelector('.actiontech-table-actions-more-button')); + await act(async () => jest.advanceTimersByTime(100)); + expect(screen.getByText('禁用')).toBeInTheDocument(); + expect(getBySelector('.ant-popover .ant-popover-inner')).toMatchSnapshot(); + fireEvent.click(screen.getByText('禁用')); + await act(async () => jest.advanceTimersByTime(100)); + expect(authUpdateDBAccountSpy).toHaveBeenCalledTimes(1); + expect(authUpdateDBAccountSpy).toHaveBeenNthCalledWith(1, { + project_uid: mockProjectInfo.projectID, + db_account_uid: dbAccountMockData[0].db_account_uid, + db_account: { + lock: true + } + }); + await act(async () => jest.advanceTimersByTime(2900)); + expect(screen.getByText('账号已禁用')).toBeInTheDocument(); + }); + + it('render delete account', async () => { + customRender(); + await act(async () => jest.advanceTimersByTime(3000)); + fireEvent.click(getBySelector('.actiontech-table-actions-more-button')); + await act(async () => jest.advanceTimersByTime(100)); + expect(screen.getByText('删除')).toBeInTheDocument(); + fireEvent.click(screen.getByText('删除')); + await act(async () => jest.advanceTimersByTime(100)); + expect(screen.getByText(`确定要删除账号:test1@%?`)).toBeInTheDocument(); + fireEvent.click(screen.getByText('确 认')); + await act(async () => jest.advanceTimersByTime(100)); + expect(authDelDBAccountSpy).toHaveBeenCalled(); + expect(authDelDBAccountSpy).toHaveBeenCalledWith({ + project_uid: mockProjectInfo.projectID, + db_account_uid: dbAccountMockData[0].db_account_uid + }); + await act(async () => jest.advanceTimersByTime(2900)); + expect(screen.getByText('账号删除成功')).toBeInTheDocument(); + }); + + it('render unsync account', async () => { + customRender(); + await act(async () => jest.advanceTimersByTime(3000)); + fireEvent.click(getBySelector('.actiontech-table-actions-more-button')); + await act(async () => jest.advanceTimersByTime(100)); + expect(screen.getByText('解除同步')).toBeInTheDocument(); + fireEvent.click(screen.getByText('解除同步')); + await act(async () => jest.advanceTimersByTime(100)); + expect( + screen.getByText(`确定要解除同步账号:test1@%?`) + ).toBeInTheDocument(); + fireEvent.click(screen.getByText('确 认')); + await act(async () => jest.advanceTimersByTime(100)); + expect(authDelDBAccountSpy).toHaveBeenCalled(); + expect(authDelDBAccountSpy).toHaveBeenCalledWith({ + project_uid: mockProjectInfo.projectID, + db_account_uid: dbAccountMockData[0].db_account_uid, + detach_from_database: true + }); + await act(async () => jest.advanceTimersByTime(2900)); + expect(screen.getByText('账号解除同步成功')).toBeInTheDocument(); + }); + + it('render cancel managed', async () => { + customRender(); + await act(async () => jest.advanceTimersByTime(3000)); + fireEvent.click(getBySelector('.actiontech-table-actions-more-button')); + await act(async () => jest.advanceTimersByTime(100)); + expect(screen.getByText('取消托管')).toBeInTheDocument(); + expect(getBySelector('.ant-popover .ant-popover-inner')).toMatchSnapshot(); + fireEvent.click(screen.getByText('取消托管')); + await act(async () => jest.advanceTimersByTime(100)); + expect( + screen.getByText( + `取消托管后,平台将不再记录账号密码,成员无法通过CB工作台访问该账号,是否确认取消托管?` + ) + ).toBeInTheDocument(); + fireEvent.click(screen.getByText('确 认')); + await act(async () => jest.advanceTimersByTime(100)); + expect(authUpdateDBAccountSpy).toHaveBeenCalled(); + expect(authUpdateDBAccountSpy).toHaveBeenCalledWith({ + project_uid: mockProjectInfo.projectID, + db_account_uid: dbAccountMockData[0].db_account_uid, + db_account: { + platform_managed: { + platform_managed: false + } + } + }); + await act(async () => jest.advanceTimersByTime(2900)); + expect(screen.getByText('密码已取消托管')).toBeInTheDocument(); + }); + + it('render manage account', async () => { + dbAccountService.authGetDBAccount(); + authListDBAccountSpy.mockClear(); + authListDBAccountSpy.mockImplementation(() => + createSpySuccessResponse({ + data: [ + { + ...dbAccountMockData[0], + platform_managed: false + } + ] + }) + ); + const modalStatusChangeSpy = jest.fn(); + superRender( + <> + + + + ); + await act(async () => jest.advanceTimersByTime(3000)); + fireEvent.click(getBySelector('.actiontech-table-actions-more-button')); + await act(async () => jest.advanceTimersByTime(100)); + expect(screen.queryAllByText('托管密码')).toHaveLength(2); + expect(getBySelector('.ant-popover .ant-popover-inner')).toMatchSnapshot(); + const moreButtons = getAllBySelector('.ant-popover .more-button-item'); + fireEvent.click(moreButtons[moreButtons.length - 1]); + await act(async () => jest.advanceTimersByTime(100)); + expect(modalStatusChangeSpy).toHaveBeenCalledTimes(2); + expect(modalStatusChangeSpy).toHaveBeenNthCalledWith(2, { + [ModalName.DatabaseAccountDiscoveryModal]: false, + [ModalName.DatabaseAccountDetailModal]: false, + [ModalName.DatabaseAccountAuthorizeModal]: false, + [ModalName.DatabaseAccountModifyPasswordModal]: false, + [ModalName.DatabaseAccountRenewalPasswordModal]: false, + [ModalName.DatabaseAccountBatchModifyPasswordModal]: false, + [ModalName.DatabaseAccountManagePasswordModal]: true + }); + }); + + it('render actions when no permission', async () => { + mockUseCurrentUser({ + isAdmin: false, + userRoles: { + [SystemRole.admin]: false, + [SystemRole.certainProjectManager]: false, + [SystemRole.systemAdministrator]: false, + [SystemRole.auditAdministrator]: false + } + }); + const { baseElement } = superRender(); + await act(async () => jest.advanceTimersByTime(3000)); + expect(baseElement).toMatchSnapshot(); + expect(authListDBAccountSpy).toHaveBeenCalled(); + expect(authGetAccountStaticsSpy).toHaveBeenCalled(); + expect(authListServicesSpy).toHaveBeenCalled(); + expect(screen.queryByText('账号发现')).not.toBeInTheDocument(); + expect(screen.queryByText('创建账号')).not.toBeInTheDocument(); + expect(screen.queryByText('批量修改密码')).not.toBeInTheDocument(); + }); + + it('render url query', async () => { + const user_uid = '1767103833235787776'; + const group_uid = '1767103833235787773'; + + superRender(, undefined, { + routerProps: { + initialEntries: [ + `/provision/project/123/database-account?user_uid=${user_uid}&group_uid=${group_uid}` + ] + } + }); + await act(async () => jest.advanceTimersByTime(3000)); + expect(authListDBAccountSpy).toHaveBeenCalled(); + expect(authListDBAccountSpy).toHaveBeenCalledWith( + { + page_index: 1, + page_size: 20, + filter_by_users: user_uid, + filter_by_user_group: group_uid, + project_uid: mockProjectInfo.projectID, + fuzzy_keyword: '' + }, + { paramsSerializer } + ); + }); +}); diff --git a/packages/provision/src/page/DatabaseAccountPassword/ExpirationAccount/index.test.tsx b/packages/provision/src/page/DatabaseAccountPassword/ExpirationAccount/index.test.tsx new file mode 100644 index 000000000..5e963d78b --- /dev/null +++ b/packages/provision/src/page/DatabaseAccountPassword/ExpirationAccount/index.test.tsx @@ -0,0 +1,206 @@ +import { superRender } from '@actiontech/shared/lib/testUtil/superRender'; +import { act, cleanup, fireEvent, screen } from '@testing-library/react'; +import { getBySelector } from '@actiontech/shared/lib/testUtil/customQuery'; +import dbAccountService from '@actiontech/shared/lib/testUtil/mockApi/provision/dbAccountService'; +import { dbAccountMockData } from '@actiontech/shared/lib/testUtil/mockApi/provision/dbAccountService/data'; +import { mockProjectInfo } from '@actiontech/shared/lib/testUtil/mockHook/data'; +import { mockUseCurrentProject } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentProject'; +import { mockUseCurrentUser } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentUser'; +import { mockCurrentUserReturn } from '@actiontech/shared/lib/testUtil/mockHook/data'; +import { mockUseDbServiceDriver } from '@actiontech/shared/lib/testUtil/mockHook/mockUseDbServiceDriver'; +import auth from '@actiontech/shared/lib/testUtil/mockApi/provision/auth'; +import ExpirationAccountList from './index'; +import RecoilObservable from '../../../testUtil/RecoilObservable'; +import { DatabaseAccountModalStatus } from '../../../store/databaseAccount'; +import { EventEmitterKey, ModalName } from '../../../data/enum'; +import { createSpySuccessResponse } from '@actiontech/shared/lib/testUtil/mockApi'; +import EventEmitter from '../../../utils/EventEmitter'; +import MockDate from 'mockdate'; +import dayjs from 'dayjs'; +import user from '@actiontech/shared/lib/testUtil/mockApi/provision/user'; +import { mockUsePermission } from '@actiontech/shared/lib/testUtil/mockHook/mockUsePermission'; +import customDBPasswordRule from '@actiontech/shared/lib/testUtil/mockApi/provision/customDBPasswordRule'; +import { SystemRole } from '@actiontech/dms-kit'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn() +})); + +let authListDBAccountSpy: jest.SpyInstance; +let authListServicesSpy: jest.SpyInstance; +const checkActionPermissionSpy = jest.fn(); + +describe('provision/DatabaseAccountPassword/ExpirationAccount-1', () => { + beforeEach(() => { + authListDBAccountSpy = dbAccountService.authListDBAccount(); + authListServicesSpy = auth.listServices(); + customDBPasswordRule.mockAllApi(); + auth.mockAllApi(); + user.mockAllApi(); + mockUseCurrentUser({ + ...mockCurrentUserReturn, + userRoles: { + [SystemRole.admin]: false, + [SystemRole.certainProjectManager]: false, + [SystemRole.systemAdministrator]: false, + [SystemRole.auditAdministrator]: false + } + }); + mockUseDbServiceDriver(); + mockUseCurrentProject(); + mockUsePermission(undefined, { mockSelector: true }); + MockDate.set(dayjs('2024-06-01 12:00:00').valueOf()); + jest.useFakeTimers({ legacyFakeTimers: true }); + }); + + afterEach(() => { + jest.useRealTimers(); + jest.clearAllMocks(); + cleanup(); + MockDate.reset(); + }); + + test('render accout list when user do not have permission', async () => { + const { baseElement } = superRender(); + await act(async () => jest.advanceTimersByTime(3000)); + expect(baseElement).toMatchSnapshot(); + expect(authListDBAccountSpy).toHaveBeenCalled(); + expect(authListServicesSpy).toHaveBeenCalled(); + expect(screen.queryAllByText('修改密码')).toHaveLength(0); + expect(screen.queryAllByText('续用当前密码')).toHaveLength(0); + }); +}); + +describe('provision/DatabaseAccountPassword/ExpirationAccount-2', () => { + beforeEach(() => { + authListDBAccountSpy = dbAccountService.authListDBAccount(); + authListServicesSpy = auth.listServices(); + customDBPasswordRule.mockAllApi(); + auth.mockAllApi(); + user.mockAllApi(); + mockUseCurrentUser(); + mockUseDbServiceDriver(); + mockUseCurrentProject(); + mockUsePermission(undefined, { mockSelector: true }); + checkActionPermissionSpy.mockReturnValue(true); + MockDate.set(dayjs('2024-06-01 12:00:00').valueOf()); + jest.useFakeTimers({ legacyFakeTimers: true }); + }); + + afterEach(() => { + jest.useRealTimers(); + jest.clearAllMocks(); + cleanup(); + MockDate.reset(); + }); + + const customRender = () => { + dbAccountService.authGetDBAccount(); + authListDBAccountSpy.mockClear(); + authListDBAccountSpy.mockImplementation(() => + createSpySuccessResponse({ data: [dbAccountMockData[0]] }) + ); + const modalStatusChangeSpy = jest.fn(); + superRender( + <> + + + + ); + return { modalStatusChangeSpy }; + }; + + test('render init snap', async () => { + const { baseElement } = superRender(); + await act(async () => jest.advanceTimersByTime(3000)); + expect(authListDBAccountSpy).toHaveBeenCalledTimes(1); + expect(authListServicesSpy).toHaveBeenCalledTimes(1); + expect(baseElement).toMatchSnapshot(); + }); + + test('render update table filter', async () => { + const { baseElement } = superRender(); + await act(async () => jest.advanceTimersByTime(3000)); + expect(authListDBAccountSpy).toHaveBeenCalledTimes(1); + fireEvent.click(screen.getByText('test1')); + await act(async () => jest.advanceTimersByTime(3000)); + expect(authListDBAccountSpy).toHaveBeenCalledTimes(2); + expect(authListDBAccountSpy).toHaveBeenNthCalledWith(2, { + page_index: 1, + page_size: 20, + filter_by_db_service: '1793883708181188608', + fuzzy_keyword: '', + filter_by_expired_time_from: '', + filter_by_expired_time_to: '2024-06-06T12:00:00+08:00', + project_uid: mockProjectInfo.projectID + }); + expect(baseElement).toMatchSnapshot(); + }); + + test('filter data with search', async () => { + superRender(); + expect(authListDBAccountSpy).toHaveBeenCalled(); + const searchText = 'search text'; + const inputEle = getBySelector('#actiontech-table-search-input'); + fireEvent.change(inputEle, { + target: { value: searchText } + }); + + await act(async () => { + fireEvent.keyDown(inputEle, { + key: 'Enter', + code: 'Enter', + keyCode: 13 + }); + await jest.advanceTimersByTime(300); + }); + await act(async () => jest.advanceTimersByTime(3000)); + expect(authListDBAccountSpy).toHaveBeenCalledWith({ + page_index: 1, + page_size: 20, + fuzzy_keyword: searchText, + filter_by_expired_time_from: '', + filter_by_expired_time_to: '2024-06-06T12:00:00+08:00', + project_uid: mockProjectInfo.projectID + }); + }); + + test('render emit "Refresh_Account_Management_List_Table" event', async () => { + superRender(); + await act(async () => jest.advanceTimersByTime(3000)); + expect(authListDBAccountSpy).toHaveBeenCalledTimes(1); + await act(async () => + EventEmitter.emit(EventEmitterKey.Refresh_Account_Management_List_Table) + ); + await act(async () => jest.advanceTimersByTime(3000)); + expect(authListDBAccountSpy).toHaveBeenCalledTimes(2); + }); + + test('render modify password', async () => { + const { modalStatusChangeSpy } = customRender(); + await act(async () => jest.advanceTimersByTime(3000)); + expect(screen.getByText('修改密码')).toBeInTheDocument(); + fireEvent.click(screen.getByText('修改密码')); + expect(modalStatusChangeSpy).toHaveBeenCalledTimes(2); + expect(modalStatusChangeSpy).toHaveBeenNthCalledWith(2, { + [ModalName.DatabaseAccountModifyPasswordModal]: true, + [ModalName.DatabaseAccountRenewalPasswordModal]: false + }); + }); + + test('render renewal password', async () => { + const { modalStatusChangeSpy } = customRender(); + await act(async () => jest.advanceTimersByTime(3000)); + expect(screen.getByText('续用当前密码')).toBeInTheDocument(); + fireEvent.click(screen.getByText('续用当前密码')); + expect(modalStatusChangeSpy).toHaveBeenCalledTimes(2); + expect(modalStatusChangeSpy).toHaveBeenNthCalledWith(2, { + [ModalName.DatabaseAccountModifyPasswordModal]: false, + [ModalName.DatabaseAccountRenewalPasswordModal]: true + }); + }); +}); diff --git a/packages/shared/lib/features/useCurrentUser/index.test.ts b/packages/shared/lib/features/useCurrentUser/index.test.ts index db4addd34..6637a11b7 100644 --- a/packages/shared/lib/features/useCurrentUser/index.test.ts +++ b/packages/shared/lib/features/useCurrentUser/index.test.ts @@ -90,7 +90,6 @@ describe('hooks/useCurrentUser', () => { [SystemRole.admin]: true, [SystemRole.certainProjectManager]: true, [SystemRole.auditAdministrator]: false, - [SystemRole.projectDirector]: true, [SystemRole.systemAdministrator]: false }); expect(result.current.systemPreference).toBe(GetUserSystemEnum.MANAGEMENT); @@ -127,7 +126,6 @@ describe('hooks/useCurrentUser', () => { [SystemRole.admin]: false, [SystemRole.certainProjectManager]: false, [SystemRole.auditAdministrator]: true, - [SystemRole.projectDirector]: false, [SystemRole.systemAdministrator]: false }); }); diff --git a/packages/shared/lib/features/usePermission/__tests__/index.test.ts b/packages/shared/lib/features/usePermission/__tests__/index.test.ts index a1d3a7d55..5f8e3fcf3 100644 --- a/packages/shared/lib/features/usePermission/__tests__/index.test.ts +++ b/packages/shared/lib/features/usePermission/__tests__/index.test.ts @@ -155,7 +155,6 @@ describe('usePermission', () => { [SystemRole.admin]: false, [SystemRole.certainProjectManager]: true, [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: false, [SystemRole.auditAdministrator]: false } }); @@ -191,7 +190,6 @@ describe('usePermission', () => { [SystemRole.admin]: false, [SystemRole.certainProjectManager]: false, [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: false, [SystemRole.auditAdministrator]: false } }); @@ -236,7 +234,6 @@ describe('usePermission', () => { [SystemRole.admin]: true, [SystemRole.certainProjectManager]: false, [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: false, [SystemRole.auditAdministrator]: false } }); @@ -277,7 +274,6 @@ describe('usePermission', () => { [SystemRole.admin]: false, [SystemRole.certainProjectManager]: false, [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: false, [SystemRole.auditAdministrator]: false }, bindProjects: [ @@ -316,7 +312,6 @@ describe('usePermission', () => { [SystemRole.admin]: false, [SystemRole.certainProjectManager]: false, [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: false, [SystemRole.auditAdministrator]: false } }); @@ -352,7 +347,6 @@ describe('usePermission', () => { [SystemRole.admin]: false, [SystemRole.certainProjectManager]: false, [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: false, [SystemRole.auditAdministrator]: false } }); @@ -382,7 +376,6 @@ describe('usePermission', () => { [SystemRole.admin]: false, [SystemRole.certainProjectManager]: false, [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: false, [SystemRole.auditAdministrator]: false }, bindProjects: [ @@ -419,7 +412,6 @@ describe('usePermission', () => { [SystemRole.admin]: false, [SystemRole.certainProjectManager]: false, [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: false, [SystemRole.auditAdministrator]: false } }); diff --git a/packages/shared/lib/testUtil/mockHook/data.tsx b/packages/shared/lib/testUtil/mockHook/data.tsx index b50a68c4d..90a98ba26 100644 --- a/packages/shared/lib/testUtil/mockHook/data.tsx +++ b/packages/shared/lib/testUtil/mockHook/data.tsx @@ -33,8 +33,8 @@ export const mockCurrentUserReturn = { ], managementPermissions: [ { - uid: OpPermissionTypeUid.project_director, - name: '创建项目' + uid: OpPermissionTypeUid.system_administrator, + name: '系统管理员' } ], projectID: '1', @@ -51,11 +51,11 @@ export const mockCurrentUserReturn = { [SystemRole.admin]: true, [SystemRole.certainProjectManager]: true, [SystemRole.systemAdministrator]: true, - [SystemRole.auditAdministrator]: true, - [SystemRole.projectDirector]: true + [SystemRole.auditAdministrator]: true }, hasGlobalViewingPermission: true, - systemPreference: GetUserSystemEnum.MANAGEMENT + systemPreference: GetUserSystemEnum.MANAGEMENT, + businessWritePermission: true }; export const mockProjectInfo = { diff --git a/packages/sqle/src/page/OperationRecord/List/index.test.tsx b/packages/sqle/src/page/OperationRecord/List/index.test.tsx index 8e40a033e..564efafde 100644 --- a/packages/sqle/src/page/OperationRecord/List/index.test.tsx +++ b/packages/sqle/src/page/OperationRecord/List/index.test.tsx @@ -149,8 +149,7 @@ describe('sqle/OperationRecord/List', () => { admin: false, certainProjectManager: true, systemAdministrator: false, - auditAdministrator: false, - projectDirector: false + auditAdministrator: false } }); const operationRecordListSpy = operationRecord.getOperationRecordList(); diff --git a/packages/sqle/src/page/VersionManagement/Detail/__tests__/index.test.tsx b/packages/sqle/src/page/VersionManagement/Detail/__tests__/index.test.tsx index b7fad541d..1a75847b3 100644 --- a/packages/sqle/src/page/VersionManagement/Detail/__tests__/index.test.tsx +++ b/packages/sqle/src/page/VersionManagement/Detail/__tests__/index.test.tsx @@ -270,7 +270,6 @@ describe('sqle/VersionManagement/Detail', () => { [SystemRole.admin]: false, [SystemRole.auditAdministrator]: false, [SystemRole.systemAdministrator]: false, - [SystemRole.projectDirector]: false, [SystemRole.certainProjectManager]: false } }); From 53c8ce6a31936c646df6366a1e0139e60b6705e3 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Fri, 8 May 2026 10:49:13 +0000 Subject: [PATCH 03/24] feat(user): add business write permission Switch to user edit/create panels - Add BWP Switch control in UserForm, visible when role is system_administrator or when editing admin account - Switch defaults to true (on), resets to true when role switches back to system_administrator - UpdateUser submits business_write_permission field, reads initial value from API response using safe type assertion (API types not yet regenerated) - AddUser submits business_write_permission field for new users - Non-system-administrator roles force business_write_permission=true on submit - Add zh-CN/en-US i18n keys for BWP label and description text --- .../base/src/locale/en-US/dmsUserCenter.ts | 5 +- .../base/src/locale/zh-CN/dmsUserCenter.ts | 5 +- .../UserCenter/Drawer/User/AddUser/index.tsx | 35 ++++++++---- .../Drawer/User/UpdateUser/index.tsx | 25 +++++++-- .../UserCenter/Drawer/User/UserForm/index.tsx | 55 ++++++++++++++++++- .../Drawer/User/UserForm/index.type.ts | 2 + 6 files changed, 107 insertions(+), 20 deletions(-) diff --git a/packages/base/src/locale/en-US/dmsUserCenter.ts b/packages/base/src/locale/en-US/dmsUserCenter.ts index 87c969a9a..54173680b 100644 --- a/packages/base/src/locale/en-US/dmsUserCenter.ts +++ b/packages/base/src/locale/en-US/dmsUserCenter.ts @@ -28,7 +28,10 @@ export default { opPermissions: 'Platform management permissions', isDisabled: 'Disabled', disabledTips: - 'When the user is disabled, the user will not be able to log in' + 'When the user is disabled, the user will not be able to log in', + businessWritePermission: 'Business write permission', + businessWritePermissionDesc: + 'When disabled, this account retains only resource configuration and read-only access, and will no longer participate in business writes or notifications.' }, createUser: { createSuccessTips: 'Add user "{{name}}" successfully' diff --git a/packages/base/src/locale/zh-CN/dmsUserCenter.ts b/packages/base/src/locale/zh-CN/dmsUserCenter.ts index 26fddd512..7e1c655bf 100644 --- a/packages/base/src/locale/zh-CN/dmsUserCenter.ts +++ b/packages/base/src/locale/zh-CN/dmsUserCenter.ts @@ -32,7 +32,10 @@ export default { userGroups: '所属用户组', opPermissions: '平台角色', isDisabled: '是否禁用', - disabledTips: '当用户被禁用,该用户将无法登录' + disabledTips: '当用户被禁用,该用户将无法登录', + businessWritePermission: '业务写权', + businessWritePermissionDesc: + '关闭后该账号仅保留资源配置与业务只读能力,不再参与业务写入与通知。' }, createUser: { createSuccessTips: '添加用户 "{{name}}" 成功' diff --git a/packages/base/src/page/UserCenter/Drawer/User/AddUser/index.tsx b/packages/base/src/page/UserCenter/Drawer/User/AddUser/index.tsx index 33ee4a899..1f4ebadd1 100644 --- a/packages/base/src/page/UserCenter/Drawer/User/AddUser/index.tsx +++ b/packages/base/src/page/UserCenter/Drawer/User/AddUser/index.tsx @@ -11,8 +11,13 @@ import EmitterKey from '../../../../../data/EmitterKey'; import UserForm from '../UserForm'; import { IUserFormFields } from '../UserForm/index.type'; import EventEmitter from '../../../../../utils/EventEmitter'; -import { BasicDrawer, BasicButton } from '@actiontech/dms-kit'; +import { + BasicDrawer, + BasicButton, + OpPermissionTypeUid +} from '@actiontech/dms-kit'; import User from '@actiontech/shared/lib/api/base/service/User'; +import { IUser } from '@actiontech/shared/lib/api/base/service/common'; import dayjs from 'dayjs'; const AddUser = () => { const [form] = Form.useForm(); @@ -34,19 +39,25 @@ const AddUser = () => { }, [dispatch, form]); const addUser = useCallback(async () => { const values = await form.validateFields(); + const isRoleSysAdmin = + values.opPermissionUid === OpPermissionTypeUid.system_administrator; + const userPayload: IUser & { business_write_permission?: boolean } = { + name: values.username, + password: values.passwordConfirm, + email: values.email ?? '', + phone: values.phone ?? '', + wxid: values.wxid ?? '', + op_permission_uids: values.opPermissionUid + ? [values.opPermissionUid] + : [], + uid: dayjs().format('YYYYMMDDHHmmssSSS'), + business_write_permission: isRoleSysAdmin + ? !!values.businessWritePermission + : true + }; setTrue(); User.AddUser({ - user: { - name: values.username, - password: values.passwordConfirm, - email: values.email ?? '', - phone: values.phone ?? '', - wxid: values.wxid ?? '', - op_permission_uids: values.opPermissionUid - ? [values.opPermissionUid] - : [], - uid: dayjs().format('YYYYMMDDHHmmssSSS') - } + user: userPayload }) .then((res) => { if (res.data.code === ResponseCode.SUCCESS) { diff --git a/packages/base/src/page/UserCenter/Drawer/User/UpdateUser/index.tsx b/packages/base/src/page/UserCenter/Drawer/User/UpdateUser/index.tsx index 125c28322..c5f4420ed 100644 --- a/packages/base/src/page/UserCenter/Drawer/User/UpdateUser/index.tsx +++ b/packages/base/src/page/UserCenter/Drawer/User/UpdateUser/index.tsx @@ -18,7 +18,7 @@ import { import User from '@actiontech/shared/lib/api/base/service/User'; import { ListUserStatEnum } from '@actiontech/shared/lib/api/base/service/common.enum'; import { BasicDrawer, BasicButton } from '@actiontech/dms-kit'; -import { SystemRole } from '@actiontech/dms-kit'; +import { SystemRole, OpPermissionTypeUid } from '@actiontech/dms-kit'; const UpdateUser = () => { const [form] = Form.useForm(); const { t } = useTranslation(); @@ -40,9 +40,16 @@ const UpdateUser = () => { }) ); }, [dispatch, form]); + const isEditingAdmin = currentUser?.name === SystemRole.admin; + const updateUser = async () => { const values = await form.validateFields(); - const userParams: IUpdateUser = { + const isRoleSysAdmin = + values.opPermissionUid === OpPermissionTypeUid.system_administrator; + const shouldSendBWP = isRoleSysAdmin || isEditingAdmin; + const userParams: IUpdateUser & { + business_write_permission?: boolean; + } = { password: values.passwordConfirm, email: values.email ?? '', phone: values.phone ?? '', @@ -50,7 +57,10 @@ const UpdateUser = () => { op_permission_uids: values.opPermissionUid ? [values.opPermissionUid] : [], - is_disabled: values.username !== 'admin' ? !!values.isDisabled : false + is_disabled: values.username !== 'admin' ? !!values.isDisabled : false, + business_write_permission: shouldSendBWP + ? !!values.businessWritePermission + : true }; setTrue(); User.UpdateUser({ @@ -74,6 +84,9 @@ const UpdateUser = () => { }; useEffect(() => { if (visible) { + const bwpValue = + (currentUser as Record | null) + ?.business_write_permission ?? true; form.setFieldsValue({ username: currentUser?.name, email: currentUser?.email, @@ -84,7 +97,8 @@ const UpdateUser = () => { )?.[0], isDisabled: (currentUser?.stat ?? ListUserStatEnum.未知) === - ListUserStatEnum.被禁用 + ListUserStatEnum.被禁用, + businessWritePermission: !!bwpValue }); } }, [visible, currentUser, form]); @@ -114,7 +128,8 @@ const UpdateUser = () => { form={form} visible={visible} isUpdate={true} - isAdmin={currentUser?.name === SystemRole.admin} + isAdmin={isEditingAdmin} + isEditingAdmin={isEditingAdmin} /> ); diff --git a/packages/base/src/page/UserCenter/Drawer/User/UserForm/index.tsx b/packages/base/src/page/UserCenter/Drawer/User/UserForm/index.tsx index feda7bc98..108ea0ff2 100644 --- a/packages/base/src/page/UserCenter/Drawer/User/UserForm/index.tsx +++ b/packages/base/src/page/UserCenter/Drawer/User/UserForm/index.tsx @@ -2,10 +2,11 @@ import { IUserFormProps } from './index.type'; import { Form, Switch } from 'antd'; import type { Rule } from 'antd/es/form'; import { BasicInput, BasicSelect, EmptyBox } from '@actiontech/dms-kit'; -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { phoneRule } from '@actiontech/dms-kit'; import { BasicToolTip } from '@actiontech/dms-kit'; +import { OpPermissionTypeUid } from '@actiontech/dms-kit'; import useOpPermission from '../../../../../hooks/useOpPermission'; import { ListOpPermissionsFilterByTargetEnum } from '@actiontech/shared/lib/api/base/service/OpPermission/index.enum'; const UserForm: React.FC = (props) => { @@ -15,6 +16,7 @@ const UserForm: React.FC = (props) => { opPermissionOptions, updateOpPermissionList } = useOpPermission(); + const prevOpPermissionUidRef = useRef(undefined); const getUsernameRules = (): Rule[] => { const rules: Rule[] = [ @@ -41,6 +43,17 @@ const UserForm: React.FC = (props) => { updateOpPermissionList(ListOpPermissionsFilterByTargetEnum.user); } }, [updateOpPermissionList, props.visible]); + + useEffect(() => { + if (!props.visible) { + prevOpPermissionUidRef.current = undefined; + } + }, [props.visible]); + + const isSystemAdministrator = (uid: string | undefined): boolean => { + return uid === OpPermissionTypeUid.system_administrator; + }; + return (
= (props) => { optionFilterProp="label" /> + + prevValues.opPermissionUid !== curValues.opPermissionUid + } + > + {() => { + const currentOpPermissionUid = + props.form.getFieldValue('opPermissionUid'); + const prevUid = prevOpPermissionUidRef.current; + + if ( + prevUid !== undefined && + prevUid !== currentOpPermissionUid && + isSystemAdministrator(currentOpPermissionUid) + ) { + props.form.setFieldValue('businessWritePermission', true); + } + prevOpPermissionUidRef.current = currentOpPermissionUid; + + const shouldShowBWP = + isSystemAdministrator(currentOpPermissionUid) || + !!props.isEditingAdmin; + + return ( + + + + + + ); + }} + Date: Fri, 8 May 2026 11:23:19 +0000 Subject: [PATCH 04/24] feat(permission): add BWP disable logic to business buttons and SQL workbench entry - Create useBusinessWritePermission hook for shared BWP disable check - Workflow detail: disable approve/reject/exec buttons when BWP=false - Data export detail: disable approve/reject/export buttons when BWP=false - Structure comparison: disable execute comparison and generate SQL buttons when BWP=false - CloudBeaver: disable jump to workbench button and auto-redirect when BWP=false - Operation logs viewing remains accessible when BWP=false --- packages/base/src/page/CloudBeaver/index.tsx | 17 +++++++-- .../components/PageHeaderAction/actions.tsx | 6 +++- .../components/PageHeaderAction/index.type.ts | 1 + .../PageHeaderAction/useActionButtonState.ts | 15 +++++--- packages/shared/lib/features/index.ts | 1 + .../useBusinessWritePermission/index.ts | 35 +++++++++++++++++++ .../ComparisonEntry/index.tsx | 6 ++-- .../components/PageHeaderExtra/action.tsx | 18 ++++++++-- .../hooks/useWorkflowDetailAction.tsx | 18 +++++++--- .../components/PageHeaderExtra/index.type.ts | 1 + 10 files changed, 100 insertions(+), 18 deletions(-) create mode 100644 packages/shared/lib/features/useBusinessWritePermission/index.ts diff --git a/packages/base/src/page/CloudBeaver/index.tsx b/packages/base/src/page/CloudBeaver/index.tsx index c6219bec8..eaaea17d5 100644 --- a/packages/base/src/page/CloudBeaver/index.tsx +++ b/packages/base/src/page/CloudBeaver/index.tsx @@ -17,10 +17,12 @@ import { import CBOperationLogsList from './List/index'; import { DownOutlined } from '@ant-design/icons'; import { EnterpriseFeatureDisplay, useTypedQuery } from '@actiontech/shared'; +import { useBusinessWritePermission } from '@actiontech/shared/lib/features'; const CloudBeaver = () => { const { t } = useTranslation(); const extractQueries = useTypedQuery(); + const { isBusinessWriteDisabled } = useBusinessWritePermission(); const [getOperationLogsLoading, setGetOperationLogsLoading] = useState(false); const { @@ -47,7 +49,8 @@ const CloudBeaver = () => { extractQueries(ROUTE_PATHS.BASE.CLOUD_BEAVER.index)?.open_cloud_beaver === String(true) && !loading && - data + data && + !isBusinessWriteDisabled ) { let url = ''; @@ -61,7 +64,7 @@ const CloudBeaver = () => { window.location.href = url; } } - }, [extractQueries, loading, data]); + }, [extractQueries, loading, data, isBusinessWriteDisabled]); const renderActionButton = useMemo(() => { if (loading) { @@ -99,6 +102,14 @@ const CloudBeaver = () => { ); } + if (isBusinessWriteDisabled) { + return ( + + {t('dmsCloudBeaver.jumpToCloudBeaver')} + + ); + } + if (menuItems.length === 1) { return ( { ); - }, [data, loading, t, handleMenuClick]); + }, [data, loading, t, handleMenuClick, isBusinessWriteDisabled]); // Determine if the main content should be displayed const isFeatureEnabled = useMemo(() => { diff --git a/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/actions.tsx b/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/actions.tsx index 36cd5e51f..085e34805 100644 --- a/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/actions.tsx +++ b/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/actions.tsx @@ -31,6 +31,7 @@ export const RejectWorkflowAction = (rejectWorkflowButtonMeta: ActionMeta) => { - + + - - + })} + options={opPermissionOptions} + optionFilterProp="label" + /> + + diff --git a/packages/shared/lib/features/PermissionControl/index.tsx b/packages/shared/lib/features/PermissionControl/index.tsx index 0fbef6bab..d35653ed4 100644 --- a/packages/shared/lib/features/PermissionControl/index.tsx +++ b/packages/shared/lib/features/PermissionControl/index.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import usePermission from '../usePermission/usePermission'; import { PermissionControlProps } from './index.type'; @@ -7,7 +8,7 @@ const PermissionControl: React.FC = ({ projectID, authDataSourceId }) => { - const { checkActionPermission } = usePermission(); + const { checkActionPermission, checkActionDisabledByBWP } = usePermission(); if ( checkActionPermission(permission, { @@ -15,6 +16,22 @@ const PermissionControl: React.FC = ({ authDataSourceId }) ) { + const bwpDisabled = checkActionDisabledByBWP(permission); + if (bwpDisabled) { + return ( + <> + {React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement( + child as React.ReactElement>, + { disabled: true } + ); + } + return child; + })} + + ); + } return <>{children}; } diff --git a/packages/shared/lib/features/usePermission/usePermission.ts b/packages/shared/lib/features/usePermission/usePermission.ts index 976d4c45e..7224bfdf7 100644 --- a/packages/shared/lib/features/usePermission/usePermission.ts +++ b/packages/shared/lib/features/usePermission/usePermission.ts @@ -174,13 +174,6 @@ const usePermission = () => { return false; } - // BWP=off 时,所有标记为 businessWrite 的操作一律禁止 - // 白名单思路:只有项目配置模块(数据源、审核流程模板、成员与权限、推送规则、审核SQL例外、管控SQL例外) - // 下的操作不标记 businessWrite,其余项目内业务写操作均标记 businessWrite=true - if (permissionDetails.businessWrite === true && isBusinessWriteDisabled) { - return false; - } - // 是否有对应的项目管理权限 const hasProjectPermission = checkProjectPermission( permissionDetails.projectPermission @@ -212,11 +205,40 @@ const usePermission = () => { getProjectAttributesStatus, checkRoles, checkDbServicePermission, - checkProjectPermission, - isBusinessWriteDisabled + checkProjectPermission ] ); + const checkActionDisabledByBWP = useCallback( + (requiredPermission: PermissionsConstantType): boolean => { + const permissionDetails = PERMISSION_MANIFEST[requiredPermission]; + // BWP=off 时,所有标记为 businessWrite 的操作保留页面结构但禁用 + // 白名单思路:只有项目配置模块(数据源、审核流程模板、成员与权限、推送规则、审核SQL例外、管控SQL例外) + // 下的操作不标记 businessWrite,其余项目内业务写操作均标记 businessWrite=true + return ( + permissionDetails.businessWrite === true && isBusinessWriteDisabled + ); + }, + [isBusinessWriteDisabled] + ); + + const mergeActionButtonPropsWithBWPDisabled = useCallback( + ( + buttonProps: ((record?: T) => Record) | undefined, + bwpDisabled: boolean + ): ((record?: T) => Record) | undefined => { + if (!bwpDisabled) return buttonProps; + if (typeof buttonProps === 'function') { + return (record?: T) => ({ + ...buttonProps(record), + disabled: true + }); + } + return () => ({ disabled: true }); + }, + [] + ); + const parse2TableActionPermissions = useCallback( < T = Record, @@ -226,12 +248,21 @@ const usePermission = () => { actions: ActiontechTableActionsWithPermissions ): ActiontechTableProps['actions'] => { if (Array.isArray(actions)) { - return actions.map((item) => ({ - ...item, - permissions: item.permissions - ? (record) => checkActionPermission(item.permissions!, { record }) - : undefined - })); + return actions.map((item) => { + const bwpDisabled = item.permissions + ? checkActionDisabledByBWP(item.permissions) + : false; + return { + ...item, + permissions: item.permissions + ? (record) => checkActionPermission(item.permissions!, { record }) + : undefined, + buttonProps: mergeActionButtonPropsWithBWPDisabled( + item.buttonProps, + bwpDisabled + ) + }; + }); } const parseActionMoreButtons = ( @@ -239,49 +270,83 @@ const usePermission = () => { ): ActiontechTableActionsConfig['moreButtons'] => { if (typeof moreButtons === 'function') { return (record: T) => - moreButtons(record).map((item) => ({ - ...item, - permissions: item.permissions - ? (data) => - checkActionPermission(item.permissions!, { record: data }) - : undefined - })); + moreButtons(record).map((item) => { + const bwpDisabled = item.permissions + ? checkActionDisabledByBWP(item.permissions) + : false; + return { + ...item, + permissions: item.permissions + ? (data) => + checkActionPermission(item.permissions!, { record: data }) + : undefined, + disabled: bwpDisabled || !!item.disabled + }; + }); } - return moreButtons?.map((item) => ({ - ...item, - permissions: item.permissions - ? (record) => checkActionPermission(item.permissions!, { record }) - : undefined - })); + return moreButtons?.map((item) => { + const bwpDisabled = item.permissions + ? checkActionDisabledByBWP(item.permissions) + : false; + return { + ...item, + permissions: item.permissions + ? (record) => checkActionPermission(item.permissions!, { record }) + : undefined, + disabled: bwpDisabled || !!item.disabled + }; + }); }; return { ...actions, - buttons: actions.buttons.map((item) => ({ - ...item, - permissions: item.permissions - ? (record) => checkActionPermission(item.permissions!, { record }) - : undefined - })), + buttons: actions.buttons.map((item) => { + const bwpDisabled = item.permissions + ? checkActionDisabledByBWP(item.permissions) + : false; + return { + ...item, + permissions: item.permissions + ? (record) => checkActionPermission(item.permissions!, { record }) + : undefined, + buttonProps: mergeActionButtonPropsWithBWPDisabled( + item.buttonProps, + bwpDisabled + ) + }; + }), moreButtons: parseActionMoreButtons(actions.moreButtons) }; }, - [checkActionPermission] + [ + checkActionPermission, + checkActionDisabledByBWP, + mergeActionButtonPropsWithBWPDisabled + ] ); const parse2TableToolbarActionPermissions = useCallback( ( actions: ActiontechTableToolbarActionWithPermissions ): ActiontechTableToolbarActionMeta[] => { - return actions.map((item) => ({ - ...item, - permissions: item.permissions - ? checkActionPermission(item.permissions!) - : undefined - })); + return actions.map((item) => { + const bwpDisabled = item.permissions + ? checkActionDisabledByBWP(item.permissions) + : false; + return { + ...item, + permissions: item.permissions + ? checkActionPermission(item.permissions!) + : undefined, + buttonProps: { + ...item.buttonProps, + ...(bwpDisabled ? { disabled: true } : {}) + } + }; + }); }, - [checkActionPermission] + [checkActionPermission, checkActionDisabledByBWP] ); return { @@ -290,6 +355,7 @@ const usePermission = () => { checkDbServicePermission, checkPagePermission, checkActionPermission, + checkActionDisabledByBWP, parse2TableActionPermissions, parse2TableToolbarActionPermissions, checkProjectPermission diff --git a/packages/shared/lib/testUtil/mockHook/mockUsePermission.ts b/packages/shared/lib/testUtil/mockHook/mockUsePermission.ts index 654defe59..131403e78 100644 --- a/packages/shared/lib/testUtil/mockHook/mockUsePermission.ts +++ b/packages/shared/lib/testUtil/mockHook/mockUsePermission.ts @@ -19,6 +19,7 @@ const mockUsePermissionData = { checkDbServicePermission: jest.fn(), checkPagePermission: jest.fn(), checkActionPermission: jest.fn(), + checkActionDisabledByBWP: jest.fn().mockReturnValue(false), parse2TableActionPermissions: jest.fn(), parse2TableToolbarActionPermissions: jest.fn(), checkProjectPermission: jest.fn() From b4fcb7e8785ae1de0788fcaa71395ecdccdb41a6 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Sat, 9 May 2026 08:53:14 +0000 Subject: [PATCH 11/24] fix(permission): pass checkActionDisabledByBWP to VersionManagement list actions Wire up BWP disabled check for edit/delete/lock buttons in the version management table, so these buttons are properly disabled (not hidden) when BWP=off. --- .../page/VersionManagement/List/action.tsx | 22 ++++++++++++++++--- .../src/page/VersionManagement/List/index.tsx | 5 +++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/sqle/src/page/VersionManagement/List/action.tsx b/packages/sqle/src/page/VersionManagement/List/action.tsx index a2db373eb..731f2fcc2 100644 --- a/packages/sqle/src/page/VersionManagement/List/action.tsx +++ b/packages/sqle/src/page/VersionManagement/List/action.tsx @@ -39,20 +39,32 @@ export const VersionManagementTableActions = ({ onEdit, onDelete, onLock, - checkActionPermission + checkActionPermission, + checkActionDisabledByBWP }: { onEdit: (id?: number) => void; onDelete: (id?: number) => void; onLock: (id?: number) => void; checkActionPermission: (permission: PermissionsConstantType) => boolean; + checkActionDisabledByBWP: (permission: PermissionsConstantType) => boolean; }): ActiontechTableActionsConfig => { + const editBwpDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.SQLE.VERSION_MANAGEMENT.EDIT + ); + const lockBwpDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.SQLE.VERSION_MANAGEMENT.LOCK + ); + const deleteBwpDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.SQLE.VERSION_MANAGEMENT.DELETE + ); return { buttons: [ { key: 'edit-button', text: t('common.edit'), buttonProps: (record) => ({ - onClick: () => onEdit(record?.version_id) + onClick: () => onEdit(record?.version_id), + disabled: editBwpDisabled }), permissions: (record) => record?.status === SqlVersionResV1StatusEnum.is_being_released && @@ -67,6 +79,9 @@ export const VersionManagementTableActions = ({ title: t('versionManagement.list.action.lockConfirm'), onConfirm: () => onLock(record?.version_id) }), + buttonProps: () => ({ + disabled: lockBwpDisabled + }), permissions: (record) => !!record?.lockable && record?.status !== SqlVersionResV1StatusEnum.locked && @@ -78,7 +93,8 @@ export const VersionManagementTableActions = ({ key: 'delete-button', text: t('common.delete'), buttonProps: () => ({ - danger: true + danger: true, + disabled: deleteBwpDisabled }), confirm: (record) => ({ title: t('versionManagement.list.action.deleteConfirm'), diff --git a/packages/sqle/src/page/VersionManagement/List/index.tsx b/packages/sqle/src/page/VersionManagement/List/index.tsx index 3c7c85124..971cccfab 100644 --- a/packages/sqle/src/page/VersionManagement/List/index.tsx +++ b/packages/sqle/src/page/VersionManagement/List/index.tsx @@ -32,7 +32,7 @@ const VersionManagementList = () => { const { t } = useTranslation(); const navigate = useTypedNavigate(); const { projectID, projectName } = useCurrentProject(); - const { checkActionPermission } = usePermission(); + const { checkActionPermission, checkActionDisabledByBWP } = usePermission(); const [messageApi, messageContextHolder] = message.useMessage(); const { tableFilterInfo, @@ -164,7 +164,8 @@ const VersionManagementList = () => { onEdit, onDelete, onLock, - checkActionPermission + checkActionPermission, + checkActionDisabledByBWP })} scroll={{}} /> From 3ed293a09539ce48fe12fd4663f0666495a3c5c2 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Sat, 9 May 2026 11:10:49 +0000 Subject: [PATCH 12/24] fix(bwp): project admin permission overrides global BWP=off; update self immediately - useBusinessWritePermission: when BWP=off, check if user is project manager in the current project (bindProjects.is_manager). If yes, isBusinessWriteDisabled returns false so project-scoped business buttons remain usable (AC-1.4.3/AC-1.8.5). Hook is safe outside project routes: projectID='' means no override. - UpdateUser: after successful self-update, call updateUserInfo() to refresh Redux store immediately so BWP changes take effect without page reload (fix-4.3). - Add test cases for project-level permission override scenarios. Closes #813 --- .../Drawer/User/UpdateUser/index.tsx | 12 ++ .../__tests__/index.test.ts | 113 +++++++++++++++++- .../useBusinessWritePermission/index.ts | 39 +++++- 3 files changed, 156 insertions(+), 8 deletions(-) diff --git a/packages/base/src/page/UserCenter/Drawer/User/UpdateUser/index.tsx b/packages/base/src/page/UserCenter/Drawer/User/UpdateUser/index.tsx index c5f4420ed..46cc0c747 100644 --- a/packages/base/src/page/UserCenter/Drawer/User/UpdateUser/index.tsx +++ b/packages/base/src/page/UserCenter/Drawer/User/UpdateUser/index.tsx @@ -19,6 +19,7 @@ import User from '@actiontech/shared/lib/api/base/service/User'; import { ListUserStatEnum } from '@actiontech/shared/lib/api/base/service/common.enum'; import { BasicDrawer, BasicButton } from '@actiontech/dms-kit'; import { SystemRole, OpPermissionTypeUid } from '@actiontech/dms-kit'; +import useUserInfo from '@actiontech/shared/lib/features/useUserInfo'; const UpdateUser = () => { const [form] = Form.useForm(); const { t } = useTranslation(); @@ -30,7 +31,12 @@ const UpdateUser = () => { const currentUser = useSelector( (state) => state.userCenter.selectUser ); + // Current logged-in user's UID (from Redux store) + const currentLoginUserId = useSelector( + (state) => state.user.uid + ); const [messageApi, contextHolder] = message.useMessage(); + const { updateUserInfo } = useUserInfo(); const onClose = useCallback(() => { form.resetFields(); dispatch( @@ -76,6 +82,12 @@ const UpdateUser = () => { }) ); EventEmitter.emit(EmitterKey.DMS_Refresh_User_Center_List); + // If the updated user is the currently logged-in user, refresh user + // info in the Redux store so changes (e.g. BWP toggle) take effect + // immediately without requiring a full page reload. + if (currentUser?.uid && currentUser.uid === currentLoginUserId) { + updateUserInfo(); + } } }) .finally(() => { diff --git a/packages/shared/lib/features/useBusinessWritePermission/__tests__/index.test.ts b/packages/shared/lib/features/useBusinessWritePermission/__tests__/index.test.ts index 9d8abbaba..e3ad037a1 100644 --- a/packages/shared/lib/features/useBusinessWritePermission/__tests__/index.test.ts +++ b/packages/shared/lib/features/useBusinessWritePermission/__tests__/index.test.ts @@ -1,8 +1,11 @@ import { superRenderHook } from '../../../testUtil/superRender'; import useBusinessWritePermission from '../index'; import { mockUseCurrentUser } from '../../../testUtil/mockHook/mockUseCurrentUser'; +import { mockUseCurrentProject } from '../../../testUtil/mockHook/mockUseCurrentProject'; import { SystemRole } from '@actiontech/dms-kit'; +const PROJECT_ID = 'proj-001'; + describe('useBusinessWritePermission', () => { afterEach(() => { jest.clearAllMocks(); @@ -27,7 +30,7 @@ describe('useBusinessWritePermission', () => { } }, { - name: 'should return isBusinessWriteDisabled=true when admin has BWP=false', + name: 'should return isBusinessWriteDisabled=true when admin has BWP=false (no project context)', mockData: { isAdmin: true, userRoles: { @@ -44,7 +47,7 @@ describe('useBusinessWritePermission', () => { } }, { - name: 'should return isBusinessWriteDisabled=true when systemAdministrator has BWP=false', + name: 'should return isBusinessWriteDisabled=true when systemAdministrator has BWP=false (no project context)', mockData: { isAdmin: false, userRoles: { @@ -108,4 +111,110 @@ describe('useBusinessWritePermission', () => { ); }); }); + + describe('project-level permission overrides BWP=off', () => { + it('should return isBusinessWriteDisabled=false when admin has BWP=false but is project manager in current project', () => { + mockUseCurrentUser({ + isAdmin: true, + userRoles: { + [SystemRole.admin]: true, + [SystemRole.certainProjectManager]: true, + [SystemRole.systemAdministrator]: false, + [SystemRole.auditAdministrator]: false + }, + businessWritePermission: false, + bindProjects: [ + { + project_id: PROJECT_ID, + project_name: 'proj', + is_manager: true, + archived: false + } + ] + }); + mockUseCurrentProject({ projectID: PROJECT_ID, projectName: 'proj' }); + + const { result } = superRenderHook(() => useBusinessWritePermission()); + expect(result.current.isBusinessWriteDisabled).toBe(false); + expect(result.current.businessWritePermission).toBe(false); + }); + + it('should return isBusinessWriteDisabled=true when admin has BWP=false and is NOT project manager in current project', () => { + mockUseCurrentUser({ + isAdmin: true, + userRoles: { + [SystemRole.admin]: true, + [SystemRole.certainProjectManager]: false, + [SystemRole.systemAdministrator]: false, + [SystemRole.auditAdministrator]: false + }, + businessWritePermission: false, + bindProjects: [ + { + project_id: PROJECT_ID, + project_name: 'proj', + is_manager: false, + archived: false + } + ] + }); + mockUseCurrentProject({ projectID: PROJECT_ID, projectName: 'proj' }); + + const { result } = superRenderHook(() => useBusinessWritePermission()); + expect(result.current.isBusinessWriteDisabled).toBe(true); + expect(result.current.businessWritePermission).toBe(false); + }); + + it('should return isBusinessWriteDisabled=false when sysAdmin has BWP=false but is project manager in current project', () => { + mockUseCurrentUser({ + isAdmin: false, + userRoles: { + [SystemRole.admin]: false, + [SystemRole.certainProjectManager]: true, + [SystemRole.systemAdministrator]: true, + [SystemRole.auditAdministrator]: false + }, + businessWritePermission: false, + bindProjects: [ + { + project_id: PROJECT_ID, + project_name: 'proj', + is_manager: true, + archived: false + } + ] + }); + mockUseCurrentProject({ projectID: PROJECT_ID, projectName: 'proj' }); + + const { result } = superRenderHook(() => useBusinessWritePermission()); + expect(result.current.isBusinessWriteDisabled).toBe(false); + expect(result.current.businessWritePermission).toBe(false); + }); + + it('should return isBusinessWriteDisabled=true when in project context with is_manager=true but BWP=true (normal flow)', () => { + mockUseCurrentUser({ + isAdmin: true, + userRoles: { + [SystemRole.admin]: true, + [SystemRole.certainProjectManager]: true, + [SystemRole.systemAdministrator]: false, + [SystemRole.auditAdministrator]: false + }, + businessWritePermission: true, + bindProjects: [ + { + project_id: PROJECT_ID, + project_name: 'proj', + is_manager: true, + archived: false + } + ] + }); + mockUseCurrentProject({ projectID: PROJECT_ID, projectName: 'proj' }); + + const { result } = superRenderHook(() => useBusinessWritePermission()); + expect(result.current.isBusinessWriteDisabled).toBe(false); + expect(result.current.businessWritePermission).toBe(true); + }); + }); }); diff --git a/packages/shared/lib/features/useBusinessWritePermission/index.ts b/packages/shared/lib/features/useBusinessWritePermission/index.ts index d0fb4c9b0..1b30dd3f8 100644 --- a/packages/shared/lib/features/useBusinessWritePermission/index.ts +++ b/packages/shared/lib/features/useBusinessWritePermission/index.ts @@ -1,30 +1,57 @@ import { useMemo } from 'react'; import useCurrentUser from '../useCurrentUser'; +import useCurrentProject from '../useCurrentProject'; /** * Hook to check if business write actions should be disabled for the current user. * * When a system administrator (or admin) has businessWritePermission=false, - * business write operations should be disabled in the UI. - * This is a UI experience optimization - the backend also enforces this via - * assignee mechanism and permission checks. + * business write operations should be disabled in the UI, UNLESS the user + * has project-level manager permission in the current project (project admin + * granted via project member config). In that case the project-level permission + * takes precedence over the global BWP flag. + * + * This hook is safe to use both inside and outside a project route context: + * - Inside a project route: projectID is non-empty, project-admin check is performed. + * - Outside a project route (global pages): projectID is empty, BWP=off always disables. * * @returns {object} - * - isBusinessWriteDisabled: true when the user is admin/sysAdmin AND BWP=false + * - isBusinessWriteDisabled: true when BWP=false and user has no project-level override * - businessWritePermission: raw BWP value from user info */ const useBusinessWritePermission = () => { - const { isAdmin, userRoles, businessWritePermission } = useCurrentUser(); + const { isAdmin, userRoles, businessWritePermission, bindProjects } = + useCurrentUser(); + const { projectID } = useCurrentProject(); + + // Determine if the user is a project manager (is_manager=true) in the current project. + // If we are not in a project context (projectID empty), this is always false. + const isProjectManagerInCurrentProject = useMemo(() => { + if (!projectID) return false; + const project = bindProjects.find((v) => v.project_id === projectID); + return !!project?.is_manager; + }, [projectID, bindProjects]); const isBusinessWriteDisabled = useMemo(() => { if ( (isAdmin || userRoles.systemAdministrator) && !businessWritePermission ) { + // Project-level manager permission overrides the global BWP=off flag. + // Only applies when we are inside a project context. + if (projectID && isProjectManagerInCurrentProject) { + return false; + } return true; } return false; - }, [isAdmin, userRoles.systemAdministrator, businessWritePermission]); + }, [ + isAdmin, + userRoles.systemAdministrator, + businessWritePermission, + projectID, + isProjectManagerInCurrentProject + ]); return { isBusinessWriteDisabled, From dec8d5ed7a43b20bcda6ff7f7fc4be8ed7b4b05f Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Sat, 9 May 2026 12:08:14 +0000 Subject: [PATCH 13/24] fix(useBusinessWritePermission): match project by both project_id and project_name The hook previously only matched bindProjects entries by project_id, which failed when the URL route param contained the project name (e.g. "default") instead of the numeric UID (e.g. "700300"). Now find() checks both project_id and project_name to handle both navigation patterns. Fixes project admin BWP override not taking effect (AC-1.4.3, AC-1.8.5). --- .../__tests__/index.test.ts | 27 +++++++++++++++++++ .../useBusinessWritePermission/index.ts | 7 ++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/shared/lib/features/useBusinessWritePermission/__tests__/index.test.ts b/packages/shared/lib/features/useBusinessWritePermission/__tests__/index.test.ts index e3ad037a1..449f05664 100644 --- a/packages/shared/lib/features/useBusinessWritePermission/__tests__/index.test.ts +++ b/packages/shared/lib/features/useBusinessWritePermission/__tests__/index.test.ts @@ -191,6 +191,33 @@ describe('useBusinessWritePermission', () => { expect(result.current.businessWritePermission).toBe(false); }); + it('should return isBusinessWriteDisabled=false when BWP=false and projectID from URL matches project_name (name-based routing)', () => { + mockUseCurrentUser({ + isAdmin: true, + userRoles: { + [SystemRole.admin]: true, + [SystemRole.certainProjectManager]: true, + [SystemRole.systemAdministrator]: false, + [SystemRole.auditAdministrator]: false + }, + businessWritePermission: false, + bindProjects: [ + { + project_id: '700300', + project_name: 'default', + is_manager: true, + archived: false + } + ] + }); + // Simulate URL param being project name instead of UID + mockUseCurrentProject({ projectID: 'default', projectName: 'default' }); + + const { result } = superRenderHook(() => useBusinessWritePermission()); + expect(result.current.isBusinessWriteDisabled).toBe(false); + expect(result.current.businessWritePermission).toBe(false); + }); + it('should return isBusinessWriteDisabled=true when in project context with is_manager=true but BWP=true (normal flow)', () => { mockUseCurrentUser({ isAdmin: true, diff --git a/packages/shared/lib/features/useBusinessWritePermission/index.ts b/packages/shared/lib/features/useBusinessWritePermission/index.ts index 1b30dd3f8..4b8b8e026 100644 --- a/packages/shared/lib/features/useBusinessWritePermission/index.ts +++ b/packages/shared/lib/features/useBusinessWritePermission/index.ts @@ -26,9 +26,14 @@ const useBusinessWritePermission = () => { // Determine if the user is a project manager (is_manager=true) in the current project. // If we are not in a project context (projectID empty), this is always false. + // Note: projectID from URL params may be either the numeric UID (e.g. "700300") + // or the project name (e.g. "default"), so we match against both project_id and + // project_name to handle both navigation patterns robustly. const isProjectManagerInCurrentProject = useMemo(() => { if (!projectID) return false; - const project = bindProjects.find((v) => v.project_id === projectID); + const project = bindProjects.find( + (v) => v.project_id === projectID || v.project_name === projectID + ); return !!project?.is_manager; }, [projectID, bindProjects]); From 505782e0da1caa3fa7578a869b6bee2272d35928 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Mon, 11 May 2026 04:53:26 +0000 Subject: [PATCH 14/24] fix: enforce BWP disable check on bypassed business write operations (#813) - SqlAuditTags: disable add-tag button and tag closable when BWP is off - AuditResultList EditText: render plain text instead of editable when BWP is off - StageNode: disable addExistingWorkflow/createWorkflow/updateInfo/offlineExecuted buttons when BWP is off - DatabaseAccount List: add isBusinessWriteDisabled to row-level action buttons --- .../src/page/DatabaseAccount/List/column.tsx | 299 +++++++++++ .../src/page/DatabaseAccount/List/index.tsx | 473 ++++++++++++++++++ .../List/component/SqlAuditTags/index.tsx | 5 +- .../Common/AuditResultList/Table/column.tsx | 6 +- .../Common/AuditResultList/Table/index.tsx | 12 +- .../Detail/components/StageNode/index.tsx | 18 +- 6 files changed, 805 insertions(+), 8 deletions(-) create mode 100644 packages/provision/src/page/DatabaseAccount/List/column.tsx create mode 100644 packages/provision/src/page/DatabaseAccount/List/index.tsx diff --git a/packages/provision/src/page/DatabaseAccount/List/column.tsx b/packages/provision/src/page/DatabaseAccount/List/column.tsx new file mode 100644 index 000000000..feffcaa9c --- /dev/null +++ b/packages/provision/src/page/DatabaseAccount/List/column.tsx @@ -0,0 +1,299 @@ +import { IListDBAccount } from '@actiontech/shared/lib/api/provision/service/common'; +import { t } from '../../../locale'; +import { Space, Typography } from 'antd'; +import { DBAccountStatusDictionary } from '../index.data'; +import { ModalName } from '../../../data/enum'; +import { + ListDBAccountPasswordExpirationPolicyEnum, + ListDBAccountStatusEnum +} from '@actiontech/shared/lib/api/provision/service/common.enum'; +import { accountNameRender } from '../index.utils'; +import { DatabaseAccountListFilterParamType } from './index.type'; +import AuthDisplay, { AuthType } from '../components/AuthDisplay'; +import { + ActiontechTableColumn, + formatTime, + BasicToolTip, + InlineActiontechTableMoreActionsButtonMeta, + ActiontechTableActionMeta +} from '@actiontech/dms-kit'; + +export const databaseAccountListColumns = ( + onUpdateFilter: ( + key: keyof DatabaseAccountListFilterParamType, + value?: string + ) => void +): ActiontechTableColumn< + IListDBAccount, + DatabaseAccountListFilterParamType +> => { + return [ + { + dataIndex: 'account_info', + title: t('databaseAccount.list.column.account'), + render: (value) => { + return accountNameRender(value); + } + }, + { + dataIndex: 'db_service', + title: t('databaseAccount.list.column.dbService'), + render: (value) => { + return ( + onUpdateFilter('filter_by_db_service', value?.uid!)} + > + {value?.name || '-'} + + ); + }, + filterCustomType: 'select', + filterKey: 'filter_by_db_service' + }, + { + dataIndex: 'expired_time', + title: t('databaseAccount.list.column.expiredTime'), + render: (val, record) => { + if (!val) { + return '-'; + } + if (record.password_expired) { + return `${formatTime(val)}(${t( + 'databaseAccount.list.column.expired' + )})`; + } + return formatTime(val); + }, + filterKey: ['filter_by_expired_time_from', 'filter_by_expired_time_to'], + filterCustomType: 'date-range' + }, + { + dataIndex: 'password_expiration_policy', + title: t('databaseAccount.list.column.passwordExpirationPolicy'), + render: (val) => + val === ListDBAccountPasswordExpirationPolicyEnum.expiration_lock + ? t('databaseAccount.list.column.lock') + : t('databaseAccount.list.column.available') + }, + { + dataIndex: 'status', + title: () => ( + + {t('databaseAccount.list.column.statusTips')} + + } + > + {t('databaseAccount.list.column.status')} + + ), + filterKey: 'filter_by_status', + filterCustomType: 'select', + render: (value) => { + return value ? DBAccountStatusDictionary[value] : '-'; + } + }, + { + dataIndex: 'platform_managed', + title: () => ( + + {t('databaseAccount.list.column.deposit')} + + ), + render: (value) => { + return value + ? t('databaseAccount.list.managed') + : t('databaseAccount.list.unmanaged'); + }, + filterKey: 'filter_by_password_managed', + filterCustomType: 'select' + }, + { + dataIndex: 'auth_users', + className: 'ellipsis-column-width', + title: t('databaseAccount.list.column.auth'), + filterKey: 'filter_by_users', + filterCustomType: 'select', + render: (value, record) => { + const { auth_users, auth_user_groups } = record; + if (!value || (!auth_users?.length && !auth_user_groups?.length)) { + return '-'; + } + return ( + + onUpdateFilter('filter_by_users', uid)} + /> + + onUpdateFilter('filter_by_user_group', uid) + } + /> + + ); + } + }, + { + dataIndex: 'auth_user_groups', + show: false, + title: t('databaseAccount.list.column.authUserGroup'), + filterKey: 'filter_by_user_group', + filterCustomType: 'select' + }, + { + dataIndex: 'explanation', + title: t('databaseAccount.list.column.desc'), + render: (value) => { + return value || '-'; + } + } + ]; +}; + +export const databaseAccountListActions = ( + onOpenModal: (name: ModalName, record?: IListDBAccount) => void, + onSetLockedStatus: (lock: boolean, id?: string) => void, + onSetManagedStatus: (managed: boolean, id?: string) => void, + onDeleteAccount: (id?: string) => void, + onUnsyncAccount: (id?: string) => void, + onNavigateToUpdatePage: (id?: string) => void, + checkActionPermission: () => boolean, + isBusinessWriteDisabled?: boolean +): { + moreButtons: ( + record: IListDBAccount + ) => InlineActiontechTableMoreActionsButtonMeta[]; + buttons: ActiontechTableActionMeta[]; +} => ({ + buttons: [ + { + key: 'account_view', + text: t('databaseAccount.list.action.view'), + buttonProps: (record) => ({ + onClick: () => onOpenModal(ModalName.DatabaseAccountDetailModal, record) + }) + }, + { + key: 'account_authorize', + text: t('databaseAccount.list.action.authorize'), + buttonProps: (record) => { + return { + onClick: () => + onOpenModal(ModalName.DatabaseAccountAuthorizeModal, record), + disabled: isBusinessWriteDisabled + }; + }, + permissions: (record) => + !!record?.platform_managed && checkActionPermission() + } + ], + moreButtons: () => { + return checkActionPermission() + ? [ + { + key: 'modifyPassword', + text: t('databaseAccount.list.action.modifyPassword'), + onClick: (record) => + onOpenModal(ModalName.DatabaseAccountModifyPasswordModal, record), + disabled: isBusinessWriteDisabled + }, + { + key: 'account_renewal', + text: t('databaseAccount.list.action.renewal'), + onClick: (record) => + onOpenModal( + ModalName.DatabaseAccountRenewalPasswordModal, + record + ), + permissions: (record) => !!record?.expired_time, + disabled: isBusinessWriteDisabled + }, + { + key: 'modify_permission', + text: t('databaseAccount.list.action.modifyPrivilege'), + onClick: (record) => onNavigateToUpdatePage(record?.db_account_uid), + disabled: isBusinessWriteDisabled + }, + { + key: 'account_disable', + text: t('databaseAccount.list.action.disable'), + onClick: (record) => + onSetLockedStatus(true, record?.db_account_uid), + permissions: (record) => + record?.status === ListDBAccountStatusEnum.unlock, + disabled: isBusinessWriteDisabled + }, + { + key: 'account_enable', + text: t('databaseAccount.list.action.enable'), + onClick: (record) => + onSetLockedStatus(false, record?.db_account_uid), + permissions: (record) => + record?.status === ListDBAccountStatusEnum.lock, + disabled: isBusinessWriteDisabled + }, + { + key: 'account_delete', + text: t('databaseAccount.list.action.delete'), + confirm: (record) => ({ + title: t('databaseAccount.list.deleteConfirm', { + name: accountNameRender(record?.account_info) + }), + okText: t('common.ok'), + cancelText: t('common.cancel'), + onConfirm: () => { + onDeleteAccount(record?.db_account_uid); + } + }), + disabled: isBusinessWriteDisabled + }, + { + key: 'account_unsync', + text: t('databaseAccount.list.action.unsync'), + confirm: (record) => ({ + title: t('databaseAccount.list.unsyncConfirm', { + name: accountNameRender(record?.account_info) + }), + okText: t('common.ok'), + cancelText: t('common.cancel'), + onConfirm: () => { + onUnsyncAccount(record?.db_account_uid); + } + }), + disabled: isBusinessWriteDisabled + }, + { + key: 'account_manage', + text: t('databaseAccount.list.action.manage'), + onClick: (record) => + onOpenModal(ModalName.DatabaseAccountManagePasswordModal, record), + permissions: (record) => !record?.platform_managed, + disabled: isBusinessWriteDisabled + }, + { + key: 'account_cancelManage', + text: t('databaseAccount.list.action.cancelManage'), + confirm: (record) => ({ + title: t('databaseAccount.list.cancelManage'), + okText: t('common.ok'), + cancelText: t('common.cancel'), + onConfirm: () => { + onSetManagedStatus(false, record?.db_account_uid); + } + }), + permissions: (record) => !!record?.platform_managed, + disabled: isBusinessWriteDisabled + } + ] + : []; + } +}); diff --git a/packages/provision/src/page/DatabaseAccount/List/index.tsx b/packages/provision/src/page/DatabaseAccount/List/index.tsx new file mode 100644 index 000000000..2be6ed0c3 --- /dev/null +++ b/packages/provision/src/page/DatabaseAccount/List/index.tsx @@ -0,0 +1,473 @@ +import { useTranslation } from 'react-i18next'; +import { useTypedNavigate, useTypedQuery } from '@actiontech/shared'; +import { Space, message, Spin } from 'antd'; +import { useRequest } from 'ahooks'; +import { + useCurrentProject, + useCurrentUser, + usePermission, + useBusinessWritePermission, + PERMISSIONS +} from '@actiontech/shared/lib/features'; +import dbAccountService from '@actiontech/shared/lib/api/provision/service/db_account/'; +import { IAuthListDBAccountParams } from '@actiontech/shared/lib/api/provision/service/db_account/index.d'; +import { IListDBAccount } from '@actiontech/shared/lib/api/provision/service/common'; +import { useMemo, useEffect, useState, useCallback } from 'react'; +import { + databaseAccountListColumns, + databaseAccountListActions +} from './column'; +import { + DBAccountStatusOptions, + DBAccountPasswordManagedOptions +} from '../index.data'; +import useProvisionUser from '../../../hooks/useProvisionUser'; +import useServiceOptions from '../../../hooks/useServiceOptions'; +import useModalStatus from '../../../hooks/useModalStatus'; +import { + DatabaseAccountModalStatus, + DatabaseAccountSelectData, + DatabaseAccountBatchActionSelectedData +} from '../../../store/databaseAccount'; +import { EventEmitterKey, ModalName } from '../../../data/enum'; +import EventEmitter from '../../../utils/EventEmitter'; +import { useSetRecoilState } from 'recoil'; +import AccountStatistics from '../components/AccountStatistics'; +import { DatabaseAccountListFilterParamType } from './index.type'; +import useMemberGroup from '../hooks/useMemberGroup'; +import { + databaseAccountListTableToolbarActions, + databaseAccountListPageHeaderActions +} from './actions'; +import { + useTableRequestError, + useTableRequestParams, + paramsSerializer, + ColumnsSettingProps, + FilterCustomProps, + ResponseCode, + ROUTE_PATHS, + useTableFilterContainer, + PageHeader, + TableToolbar, + TableFilterContainer, + ActiontechTable +} from '@actiontech/dms-kit'; + +const DatabaseAccountList = () => { + const { t } = useTranslation(); + + const navigate = useTypedNavigate(); + + const extractQueries = useTypedQuery(); + + const [messageApi, contextHolder] = message.useMessage(); + + const { projectID } = useCurrentProject(); + + const { username } = useCurrentUser(); + + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + + const { updateUserList, userIDOptions } = useProvisionUser(); + + const { updateMemberGroupList, memberIDOptions } = useMemberGroup(); + + const { updateServiceList, serviceOptions } = useServiceOptions(); + + const { checkActionPermission, parse2TableToolbarActionPermissions } = + usePermission(); + const { isBusinessWriteDisabled } = useBusinessWritePermission(); + + const { toggleModal, initModalStatus } = useModalStatus( + DatabaseAccountModalStatus + ); + + const updateSelectData = useSetRecoilState(DatabaseAccountSelectData); + + const updateBatchActionSelectedData = useSetRecoilState( + DatabaseAccountBatchActionSelectedData + ); + + const { requestErrorMessage, handleTableRequestError } = + useTableRequestError(); + + const { + tableFilterInfo, + updateTableFilterInfo, + tableChange, + pagination, + setSearchKeyword, + refreshBySearchKeyword, + searchKeyword + } = useTableRequestParams< + IListDBAccount, + DatabaseAccountListFilterParamType + >(); + + const { data, loading, refresh } = useRequest( + () => { + const params: IAuthListDBAccountParams = { + ...tableFilterInfo, + ...pagination, + project_uid: projectID, + fuzzy_keyword: searchKeyword + }; + return handleTableRequestError( + dbAccountService.AuthListDBAccount(params, { paramsSerializer }) + ); + }, + { + refreshDeps: [projectID, tableFilterInfo, pagination, searchKeyword] + } + ); + + const { + data: accountStatic, + loading: accountStaticLoading, + refresh: refreshAccountStatic + } = useRequest(() => + dbAccountService + .AuthGetAccountStatics({ project_uid: projectID }) + .then((res) => res.data.data) + ); + + const onRefresh = useCallback(() => { + refreshAccountStatic(); + refresh(); + }, [refreshAccountStatic, refresh]); + + const tableSetting = useMemo( + () => ({ + tableName: 'provision_database_account_list', + username: username + }), + [username] + ); + + const filterCustomProps = useMemo(() => { + return new Map([ + [ + 'expired_time', + { + showTime: true, + disabledDate: undefined + } + ], + [ + 'status', + { + options: DBAccountStatusOptions + } + ], + [ + 'platform_managed', + { + options: DBAccountPasswordManagedOptions + } + ], + [ + 'auth_users', + { + options: userIDOptions, + value: tableFilterInfo.filter_by_users + } + ], + [ + 'auth_user_groups', + { + options: memberIDOptions, + value: tableFilterInfo.filter_by_user_group + } + ], + [ + 'db_service', + { + options: serviceOptions, + value: tableFilterInfo.filter_by_db_service + } + ] + ]); + }, [userIDOptions, serviceOptions, memberIDOptions, tableFilterInfo]); + + const onUpdateFilter = useCallback( + (key: keyof DatabaseAccountListFilterParamType, value?: string) => { + updateAllSelectedFilterItem(true); + updateTableFilterInfo((values) => { + return { + ...values, + [key]: value + }; + }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const onSetLockedStatus = useCallback( + (lock: boolean, id?: string) => { + dbAccountService + .AuthUpdateDBAccount({ + project_uid: projectID, + db_account_uid: id ?? '', + db_account: { + lock + } + }) + .then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + messageApi.success( + lock + ? t('databaseAccount.list.lockSuccessTips') + : t('databaseAccount.list.unlockSuccessTips') + ); + onRefresh(); + } + }); + }, + [messageApi, projectID, onRefresh, t] + ); + + const onSetManagedStatus = useCallback( + (managed: boolean, id?: string) => { + dbAccountService + .AuthUpdateDBAccount({ + project_uid: projectID, + db_account_uid: id ?? '', + db_account: { + platform_managed: { + platform_managed: managed + } + } + }) + .then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + messageApi.success(t('databaseAccount.list.unmanagedSuccessTips')); + onRefresh(); + } + }); + }, + [messageApi, projectID, onRefresh, t] + ); + + const onDeleteAccount = useCallback( + (id?: string) => { + dbAccountService + .AuthDelDBAccount({ + project_uid: projectID, + db_account_uid: id ?? '' + }) + .then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + messageApi.success(t('databaseAccount.list.deleteSuccessTips')); + onRefresh(); + } + }); + }, + [messageApi, projectID, onRefresh, t] + ); + + const onUnsyncAccount = useCallback( + (id?: string) => { + dbAccountService + .AuthDelDBAccount({ + detach_from_database: true, + project_uid: projectID, + db_account_uid: id ?? '' + }) + .then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + messageApi.success(t('databaseAccount.list.unsyncSuccessTips')); + onRefresh(); + } + }); + }, + [messageApi, projectID, onRefresh, t] + ); + + const columns = useMemo(() => { + return databaseAccountListColumns(onUpdateFilter); + }, [onUpdateFilter]); + + const onOpenModal = useCallback( + (name: ModalName, record?: IListDBAccount) => { + toggleModal(name, true); + updateSelectData(record ?? null); + }, + [toggleModal, updateSelectData] + ); + + const onNavigateToUpdatePage = useCallback( + (id?: string) => { + navigate(ROUTE_PATHS.PROVISION.DATABASE_ACCOUNT.update, { + params: { + projectID, + id: id ?? '' + } + }); + }, + [navigate, projectID] + ); + + const actions = useMemo(() => { + return databaseAccountListActions( + onOpenModal, + onSetLockedStatus, + onSetManagedStatus, + onDeleteAccount, + onUnsyncAccount, + onNavigateToUpdatePage, + () => + checkActionPermission( + PERMISSIONS.ACTIONS.PROVISION.DATABASE_ACCOUNT.TABLE_ACTIONS + ), + isBusinessWriteDisabled + ); + }, [ + onOpenModal, + onSetLockedStatus, + onSetManagedStatus, + onDeleteAccount, + onUnsyncAccount, + onNavigateToUpdatePage, + checkActionPermission, + isBusinessWriteDisabled + ]); + + const onBatchAction = (name: ModalName) => { + toggleModal(name, true); + updateBatchActionSelectedData( + data?.list?.filter((i) => + selectedRowKeys.includes(i.db_account_uid ?? '') + ) ?? [] + ); + }; + + const { filterButtonMeta, filterContainerMeta, updateAllSelectedFilterItem } = + useTableFilterContainer(columns, updateTableFilterInfo); + + const onSelectChange = (newSelectedRowKeys: React.Key[]) => { + setSelectedRowKeys(newSelectedRowKeys); + }; + + useEffect(() => { + updateUserList(); + updateServiceList(); + updateMemberGroupList(); + }, [updateUserList, updateServiceList, updateMemberGroupList]); + + useEffect(() => { + const queries = extractQueries( + ROUTE_PATHS.PROVISION.DATABASE_ACCOUNT.index + ); + + if (queries?.user_uid) { + onUpdateFilter('filter_by_users', queries.user_uid); + } + + if (queries?.group_uid) { + onUpdateFilter('filter_by_user_group', queries.group_uid); + } + + if (queries?.filter_keyword) { + setSearchKeyword(queries.filter_keyword); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + initModalStatus({ + [ModalName.DatabaseAccountDiscoveryModal]: false, + [ModalName.DatabaseAccountDetailModal]: false, + [ModalName.DatabaseAccountAuthorizeModal]: false, + [ModalName.DatabaseAccountModifyPasswordModal]: false, + [ModalName.DatabaseAccountRenewalPasswordModal]: false, + [ModalName.DatabaseAccountBatchModifyPasswordModal]: false, + [ModalName.DatabaseAccountManagePasswordModal]: false + }); + }, [initModalStatus]); + + useEffect(() => { + const { unsubscribe } = EventEmitter.subscribe( + EventEmitterKey.Refresh_Account_Management_List_Table, + (key: keyof DatabaseAccountListFilterParamType, value: string) => { + if (key && value) { + onUpdateFilter(key, value); + refreshAccountStatic(); + } else { + onRefresh(); + } + } + ); + return unsubscribe; + }, [onRefresh, onUpdateFilter, refreshAccountStatic]); + + const pageHeaderActions = useMemo(() => { + return databaseAccountListPageHeaderActions(projectID, () => + toggleModal(ModalName.DatabaseAccountDiscoveryModal, true) + ); + }, [projectID, toggleModal]); + + return ( + <> + {contextHolder} + + {pageHeaderActions['discover-account']} + {pageHeaderActions['create-account']} + + } + /> + + + { + refreshBySearchKeyword(); + } + }} + /> + + + + + ); +}; + +export default DatabaseAccountList; diff --git a/packages/sqle/src/page/SqlAudit/List/component/SqlAuditTags/index.tsx b/packages/sqle/src/page/SqlAudit/List/component/SqlAuditTags/index.tsx index c8f96975a..80e74162f 100644 --- a/packages/sqle/src/page/SqlAudit/List/component/SqlAuditTags/index.tsx +++ b/packages/sqle/src/page/SqlAudit/List/component/SqlAuditTags/index.tsx @@ -13,6 +13,7 @@ import { Divider, Form, InputRef, Popover, Space, Spin, message } from 'antd'; import { useForm } from 'antd/es/form/Form'; import useSQLAuditRecordTag from '../../../../../hooks/useSQLAuditRecordTag'; import { tagNameRule } from '@actiontech/dms-kit'; +import { useBusinessWritePermission } from '@actiontech/shared/lib/features'; export interface ISqlAuditTags { projectName: string; defaultTags: string[]; @@ -24,6 +25,7 @@ const SqlAuditTags = ({ updateTags }: ISqlAuditTags) => { const { t } = useTranslation(); + const { isBusinessWriteDisabled } = useBusinessWritePermission(); const [messageApi, messageContextHolder] = message.useMessage(); const [open, setOpen] = useState(false); const [extraTagForm] = useForm<{ @@ -141,7 +143,7 @@ const SqlAuditTags = ({ color="geekblue" size="small" key={v} - closable + closable={!isBusinessWriteDisabled} onClose={(e) => { e.preventDefault(); removeTag(v); @@ -172,6 +174,7 @@ const SqlAuditTags = ({ shape="circle" size="small" onClick={handelClickAddTagsIcon} + disabled={isBusinessWriteDisabled} > diff --git a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/column.tsx b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/column.tsx index d9820a953..a02c8f0f0 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/column.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/column.tsx @@ -12,7 +12,8 @@ import { BackupStrategyDictionary } from './index.data'; export const AuditResultForCreateWorkflowColumn = ( updateSqlDescribe: (sqlNum: number, sqlDescribe: string) => void, onClickAuditResult: (record: IAuditTaskSQLResV2) => void, - onSwitchSqlBackupPolicy: (sqlID?: number) => void + onSwitchSqlBackupPolicy: (sqlID?: number) => void, + isBusinessWriteDisabled?: boolean ): ActiontechTableColumn => { return [ { @@ -90,6 +91,9 @@ export const AuditResultForCreateWorkflowColumn = ( title: () => t('execWorkflow.audit.table.describe'), className: 'audit-result-describe-column', render: (description, record) => { + if (isBusinessWriteDisabled) { + return description || '-'; + } return ( = ({ const { requestErrorMessage, handleTableRequestError } = useTableRequestError(); const { parse2TableActionPermissions } = usePermission(); + const { isBusinessWriteDisabled } = useBusinessWritePermission(); const { openCreateWhitelistModal, updateSelectWhitelistRecord } = useWhitelistRedux(); const [ @@ -187,7 +191,8 @@ const AuditResultTable: React.FC = ({ const columnList = AuditResultForCreateWorkflowColumn( updateSqlDescribe, onClickAuditResult, - onSwitchSqlBackupPolicy + onSwitchSqlBackupPolicy, + isBusinessWriteDisabled ); return allowSwitchBackupPolicy ? columnList @@ -196,7 +201,8 @@ const AuditResultTable: React.FC = ({ onSwitchSqlBackupPolicy, updateSqlDescribe, onClickAuditResult, - allowSwitchBackupPolicy + allowSwitchBackupPolicy, + isBusinessWriteDisabled ]); // @feature: useTableRequestParams 整合自定义filter info diff --git a/packages/sqle/src/page/VersionManagement/Detail/components/StageNode/index.tsx b/packages/sqle/src/page/VersionManagement/Detail/components/StageNode/index.tsx index 96e42437a..37e533ae7 100644 --- a/packages/sqle/src/page/VersionManagement/Detail/components/StageNode/index.tsx +++ b/packages/sqle/src/page/VersionManagement/Detail/components/StageNode/index.tsx @@ -4,7 +4,10 @@ import { BasicTypographyEllipsis, TypedLink } from '@actiontech/shared'; import { Card, Space, Typography } from 'antd'; import type { Node, NodeProps } from '@xyflow/react'; import { StageNodeStyleWrapper } from '../../style'; -import { useCurrentProject } from '@actiontech/shared/lib/features'; +import { + useCurrentProject, + useBusinessWritePermission +} from '@actiontech/shared/lib/features'; import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; import WorkflowStatus from '../../../../SqlExecWorkflow/List/components/WorkflowStatus'; @@ -32,6 +35,7 @@ const StageNode: React.FC>> = ({ } = data; const { t } = useTranslation(); const { projectID } = useCurrentProject(); + const { isBusinessWriteDisabled } = useBusinessWritePermission(); const displayWorkflow = useMemo(() => { // 版本初始化不存在工单时 统一展示一个空占位 if (!workflowList?.length) { @@ -99,6 +103,7 @@ const StageNode: React.FC>> = ({ onRetry?.(workflow?.workflow_id ?? '')} + disabled={isBusinessWriteDisabled} > {t('versionManagement.stageNode.updateInfo')} @@ -107,6 +112,7 @@ const StageNode: React.FC>> = ({ onClick={() => onOfflineExecute?.(workflow.workflow_id ?? '') } + disabled={isBusinessWriteDisabled} > {t('versionManagement.stageNode.offlineExecuted')} @@ -120,14 +126,20 @@ const StageNode: React.FC>> = ({ onAssociateWorkflow?.(data.stageId ?? 0)} - disabled={versionStatus === SqlVersionDetailResV1StatusEnum.locked} + disabled={ + isBusinessWriteDisabled || + versionStatus === SqlVersionDetailResV1StatusEnum.locked + } > {t('versionManagement.stageNode.addExistingWorkflow')} {t('versionManagement.stageNode.createWorkflow')} From cab01efbe8482e35084bdf6e0bbbeab01dc3183e Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Mon, 11 May 2026 06:40:51 +0000 Subject: [PATCH 15/24] fix(useBusinessWritePermission): check project-level op permissions, not just is_manager (#813) When BWP=off, the hook now checks userOperationPermissions.op_permission_list for any data-source-level or project-level permissions in the current project, in addition to the existing is_manager check. This ensures that users with project roles (e.g., "development engineer" with create_workflow permission) can still use business write buttons within their authorized scope (AC-1.4.3, AC-1.8.5). --- .../__tests__/index.test.ts | 105 +++++++++++++++++- .../useBusinessWritePermission/index.ts | 53 +++++++-- 2 files changed, 146 insertions(+), 12 deletions(-) diff --git a/packages/shared/lib/features/useBusinessWritePermission/__tests__/index.test.ts b/packages/shared/lib/features/useBusinessWritePermission/__tests__/index.test.ts index 449f05664..01f23aa25 100644 --- a/packages/shared/lib/features/useBusinessWritePermission/__tests__/index.test.ts +++ b/packages/shared/lib/features/useBusinessWritePermission/__tests__/index.test.ts @@ -139,7 +139,7 @@ describe('useBusinessWritePermission', () => { expect(result.current.businessWritePermission).toBe(false); }); - it('should return isBusinessWriteDisabled=true when admin has BWP=false and is NOT project manager in current project', () => { + it('should return isBusinessWriteDisabled=true when admin has BWP=false and is NOT project manager and has NO project permissions in current project', () => { mockUseCurrentUser({ isAdmin: true, userRoles: { @@ -165,6 +165,109 @@ describe('useBusinessWritePermission', () => { expect(result.current.businessWritePermission).toBe(false); }); + it('should return isBusinessWriteDisabled=false when admin has BWP=false but has data-source-level permissions in current project', () => { + mockUseCurrentUser({ + isAdmin: true, + userRoles: { + [SystemRole.admin]: true, + [SystemRole.certainProjectManager]: false, + [SystemRole.systemAdministrator]: false, + [SystemRole.auditAdministrator]: false + }, + businessWritePermission: false, + bindProjects: [ + { + project_id: PROJECT_ID, + project_name: 'proj', + is_manager: false, + archived: false + } + ] + }); + mockUseCurrentProject({ projectID: PROJECT_ID, projectName: 'proj' }); + + const { result } = superRenderHook( + () => useBusinessWritePermission(), + undefined, + { + initStore: { + permission: { + moduleFeatureSupport: { + sqlOptimization: false, + knowledge: false + }, + userOperationPermissions: { + is_admin: false, + op_permission_list: [ + { + op_permission_type: 'create_workflow', + range_type: 'db_service', + range_uids: ['ds-001'] + } + ] + } + } + } + } + ); + expect(result.current.isBusinessWriteDisabled).toBe(false); + expect(result.current.businessWritePermission).toBe(false); + }); + + it('should return isBusinessWriteDisabled=false when sysAdmin has BWP=false but has db_service permissions in current project', () => { + mockUseCurrentUser({ + isAdmin: false, + userRoles: { + [SystemRole.admin]: false, + [SystemRole.certainProjectManager]: false, + [SystemRole.systemAdministrator]: true, + [SystemRole.auditAdministrator]: false + }, + businessWritePermission: false, + bindProjects: [ + { + project_id: PROJECT_ID, + project_name: 'proj', + is_manager: false, + archived: false + } + ] + }); + mockUseCurrentProject({ projectID: PROJECT_ID, projectName: 'proj' }); + + const { result } = superRenderHook( + () => useBusinessWritePermission(), + undefined, + { + initStore: { + permission: { + moduleFeatureSupport: { + sqlOptimization: false, + knowledge: false + }, + userOperationPermissions: { + is_admin: false, + op_permission_list: [ + { + op_permission_type: 'create_workflow', + range_type: 'db_service', + range_uids: ['ds-001'] + }, + { + op_permission_type: 'sql_query', + range_type: 'db_service', + range_uids: ['ds-001'] + } + ] + } + } + } + } + ); + expect(result.current.isBusinessWriteDisabled).toBe(false); + expect(result.current.businessWritePermission).toBe(false); + }); + it('should return isBusinessWriteDisabled=false when sysAdmin has BWP=false but is project manager in current project', () => { mockUseCurrentUser({ isAdmin: false, diff --git a/packages/shared/lib/features/useBusinessWritePermission/index.ts b/packages/shared/lib/features/useBusinessWritePermission/index.ts index 4b8b8e026..686e21914 100644 --- a/packages/shared/lib/features/useBusinessWritePermission/index.ts +++ b/packages/shared/lib/features/useBusinessWritePermission/index.ts @@ -1,4 +1,6 @@ import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { IReduxState } from '../../../../base/src/store'; import useCurrentUser from '../useCurrentUser'; import useCurrentProject from '../useCurrentProject'; @@ -7,12 +9,19 @@ import useCurrentProject from '../useCurrentProject'; * * When a system administrator (or admin) has businessWritePermission=false, * business write operations should be disabled in the UI, UNLESS the user - * has project-level manager permission in the current project (project admin - * granted via project member config). In that case the project-level permission - * takes precedence over the global BWP flag. + * has project-level authorization in the current project. Project-level + * authorization includes: + * - Being a project manager (is_manager=true in bindProjects) + * - Having any explicit operation permission in the current project + * (e.g., data-source-level roles like "development engineer" that grant + * create_workflow, sql_query, pipeline permissions, etc.) + * + * This implements AC-1.4.3 / AC-1.8.5: BWP=off users with project-level + * authorization can still perform business write operations within the + * authorized project scope. * * This hook is safe to use both inside and outside a project route context: - * - Inside a project route: projectID is non-empty, project-admin check is performed. + * - Inside a project route: projectID is non-empty, project-level check is performed. * - Outside a project route (global pages): projectID is empty, BWP=off always disables. * * @returns {object} @@ -23,28 +32,50 @@ const useBusinessWritePermission = () => { const { isAdmin, userRoles, businessWritePermission, bindProjects } = useCurrentUser(); const { projectID } = useCurrentProject(); + const { userOperationPermissions } = useSelector((state: IReduxState) => ({ + userOperationPermissions: state.permission.userOperationPermissions + })); - // Determine if the user is a project manager (is_manager=true) in the current project. + // Determine if the user has any project-level authorization in the current project. + // This includes being a project manager (is_manager=true) OR having any explicit + // operation permission (e.g., data-source-level create_workflow, sql_query, etc.). // If we are not in a project context (projectID empty), this is always false. // Note: projectID from URL params may be either the numeric UID (e.g. "700300") // or the project name (e.g. "default"), so we match against both project_id and // project_name to handle both navigation patterns robustly. - const isProjectManagerInCurrentProject = useMemo(() => { + const hasProjectLevelAuthorizationInCurrentProject = useMemo(() => { if (!projectID) return false; + + // Check 1: is_manager in bindProjects (project admin) const project = bindProjects.find( (v) => v.project_id === projectID || v.project_name === projectID ); - return !!project?.is_manager; - }, [projectID, bindProjects]); + if (project?.is_manager) { + return true; + } + + // Check 2: any explicit operation permission in the current project + // (e.g., data-source-level roles granted via project member config) + if (userOperationPermissions?.op_permission_list?.length) { + return userOperationPermissions.op_permission_list.some( + (permission) => + // db_service-level permission means user has specific data source access + // in this project (the op_permission_list is already scoped to current project) + permission.range_uids?.length + ); + } + + return false; + }, [projectID, bindProjects, userOperationPermissions]); const isBusinessWriteDisabled = useMemo(() => { if ( (isAdmin || userRoles.systemAdministrator) && !businessWritePermission ) { - // Project-level manager permission overrides the global BWP=off flag. + // Project-level authorization overrides the global BWP=off flag. // Only applies when we are inside a project context. - if (projectID && isProjectManagerInCurrentProject) { + if (projectID && hasProjectLevelAuthorizationInCurrentProject) { return false; } return true; @@ -55,7 +86,7 @@ const useBusinessWritePermission = () => { userRoles.systemAdministrator, businessWritePermission, projectID, - isProjectManagerInCurrentProject + hasProjectLevelAuthorizationInCurrentProject ]); return { From ae21e3d4ce56c3b37f5188373561b6f7860874f9 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Mon, 11 May 2026 07:19:37 +0000 Subject: [PATCH 16/24] fix(dms-kit): add tsc declaration generation to build script father build with babel transformer does not emit .d.ts files despite tsconfig having declaration:true. Add explicit tsc --emitDeclarationOnly steps for both es/ and lib/ outputs after father build to ensure downstream packages can resolve TypeScript declarations. Refs: #813 --- packages/dms-kit/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dms-kit/package.json b/packages/dms-kit/package.json index 311f8f1eb..4e224d62a 100644 --- a/packages/dms-kit/package.json +++ b/packages/dms-kit/package.json @@ -14,7 +14,7 @@ ], "sideEffects": false, "scripts": { - "build": "father build", + "build": "father build && tsc --project tsconfig.json --declaration --emitDeclarationOnly --outDir es && tsc --project tsconfig.json --declaration --emitDeclarationOnly --outDir lib", "dev": "father dev --incremental", "docs:dev": "dumi dev", "docs:build": "dumi build", From fa8a727a85caa030eddd7a33662c32cc90ad3cf1 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Mon, 11 May 2026 08:27:34 +0000 Subject: [PATCH 17/24] fix(bwp): per-action permission check when BWP=off with partial project roles (#813) When BWP=off and user has project-level authorization (e.g. data-source-level role "development engineer"), previously ALL businessWrite buttons were enabled regardless of the user's actual permissions. Now checkActionDisabledByBWP verifies that the user has the specific permission for each action (dbServicePermission or projectPermission). Buttons for features not covered by the user's role (e.g. data export, data masking) remain disabled. Changes: - useBusinessWritePermission: export isBWPOff flag for per-action checks - checkActionDisabledByBWP: check specific action permissions when BWP=off with project-level auth, instead of blanket enable/disable - parse2TableActionPermissions: pass record context for per-row BWP checks - Update 8 components to use per-action checkActionDisabledByBWP instead of blanket isBusinessWriteDisabled boolean - Add dbServicePermission (sql_query) to CLOUD_BEAVER.EXPORT manifest entry --- packages/base/src/page/CloudBeaver/index.tsx | 7 +- .../PageHeaderAction/useActionButtonState.ts | 20 +- .../src/page/DatabaseAccount/List/index.tsx | 12 +- .../useBusinessWritePermission/index.ts | 13 + .../__snapshots__/index.test.ts.snap | 240 ++++++++++++++++++ .../usePermission/permissionManifest.ts | 3 + .../features/usePermission/usePermission.ts | 137 ++++++++-- .../mockUseBusinessWritePermission.ts | 1 + .../ComparisonEntry/index.tsx | 7 +- .../List/component/SqlAuditTags/index.tsx | 7 +- .../Common/AuditResultList/Table/index.tsx | 12 +- .../hooks/useWorkflowDetailAction.tsx | 19 +- .../Detail/components/StageNode/index.tsx | 22 +- 13 files changed, 450 insertions(+), 50 deletions(-) diff --git a/packages/base/src/page/CloudBeaver/index.tsx b/packages/base/src/page/CloudBeaver/index.tsx index eaaea17d5..2923f4594 100644 --- a/packages/base/src/page/CloudBeaver/index.tsx +++ b/packages/base/src/page/CloudBeaver/index.tsx @@ -17,12 +17,15 @@ import { import CBOperationLogsList from './List/index'; import { DownOutlined } from '@ant-design/icons'; import { EnterpriseFeatureDisplay, useTypedQuery } from '@actiontech/shared'; -import { useBusinessWritePermission } from '@actiontech/shared/lib/features'; +import { usePermission, PERMISSIONS } from '@actiontech/shared/lib/features'; const CloudBeaver = () => { const { t } = useTranslation(); const extractQueries = useTypedQuery(); - const { isBusinessWriteDisabled } = useBusinessWritePermission(); + const { checkActionDisabledByBWP } = usePermission(); + const isBusinessWriteDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.BASE.CLOUD_BEAVER.EXPORT + ); const [getOperationLogsLoading, setGetOperationLogsLoading] = useState(false); const { diff --git a/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/useActionButtonState.ts b/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/useActionButtonState.ts index 9ea8227ea..59032523e 100644 --- a/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/useActionButtonState.ts +++ b/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/useActionButtonState.ts @@ -4,7 +4,8 @@ import { useMemo } from 'react'; import { WorkflowRecordStatusEnum } from '@actiontech/shared/lib/api/base/service/common.enum'; import { useCurrentUser, - useBusinessWritePermission + usePermission, + PERMISSIONS } from '@actiontech/shared/lib/features'; import { MessageInstance } from 'antd/es/message/interface'; import { ActionMeta } from './index.type'; @@ -16,7 +17,16 @@ const useActionButtonState: (messageApi: MessageInstance) => { executeExportButtonMeta: ActionMeta; } = (messageApi) => { const { userId } = useCurrentUser(); - const { isBusinessWriteDisabled } = useBusinessWritePermission(); + const { checkActionDisabledByBWP } = usePermission(); + const isExportApproveBWPDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.BASE.DATA_EXPORT.APPROVE + ); + const isExportRejectBWPDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.BASE.DATA_EXPORT.REJECT + ); + const isExportExecuteBWPDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.BASE.DATA_EXPORT.EXECUTE + ); const { workflowInfo, updateWorkflowRejectOpen } = useDataExportDetailReduxManage(); @@ -100,19 +110,19 @@ const useActionButtonState: (messageApi: MessageInstance) => { action: () => approveWorkflow(workflowID), hidden: !approveWorkflowButtonVisibility, loading: approveWorkflowLoading, - disabled: isBusinessWriteDisabled + disabled: isExportApproveBWPDisabled }, rejectWorkflowButtonMeta: { action: () => updateWorkflowRejectOpen(true), hidden: !rejectWorkflowButtonVisibility, loading: false, - disabled: isBusinessWriteDisabled + disabled: isExportRejectBWPDisabled }, executeExportButtonMeta: { action: () => executeExport(workflowID), hidden: !executingButtonVisibility, loading: executeExportLoading, - disabled: isBusinessWriteDisabled + disabled: isExportExecuteBWPDisabled } }; }; diff --git a/packages/provision/src/page/DatabaseAccount/List/index.tsx b/packages/provision/src/page/DatabaseAccount/List/index.tsx index 2be6ed0c3..976007dbd 100644 --- a/packages/provision/src/page/DatabaseAccount/List/index.tsx +++ b/packages/provision/src/page/DatabaseAccount/List/index.tsx @@ -6,7 +6,6 @@ import { useCurrentProject, useCurrentUser, usePermission, - useBusinessWritePermission, PERMISSIONS } from '@actiontech/shared/lib/features'; import dbAccountService from '@actiontech/shared/lib/api/provision/service/db_account/'; @@ -75,9 +74,14 @@ const DatabaseAccountList = () => { const { updateServiceList, serviceOptions } = useServiceOptions(); - const { checkActionPermission, parse2TableToolbarActionPermissions } = - usePermission(); - const { isBusinessWriteDisabled } = useBusinessWritePermission(); + const { + checkActionPermission, + parse2TableToolbarActionPermissions, + checkActionDisabledByBWP + } = usePermission(); + const isBusinessWriteDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.PROVISION.DATABASE_ACCOUNT.TABLE_ACTIONS + ); const { toggleModal, initModalStatus } = useModalStatus( DatabaseAccountModalStatus diff --git a/packages/shared/lib/features/useBusinessWritePermission/index.ts b/packages/shared/lib/features/useBusinessWritePermission/index.ts index 686e21914..d794f7f4d 100644 --- a/packages/shared/lib/features/useBusinessWritePermission/index.ts +++ b/packages/shared/lib/features/useBusinessWritePermission/index.ts @@ -89,8 +89,21 @@ const useBusinessWritePermission = () => { hasProjectLevelAuthorizationInCurrentProject ]); + /** + * isBWPOff: true when the user is an admin/systemAdministrator AND has + * businessWritePermission=false. This is the raw BWP-off flag regardless + * of project-level overrides. Used by checkActionDisabledByBWP to apply + * per-action permission checks when the user has partial project roles. + */ + const isBWPOff = useMemo(() => { + return ( + (isAdmin || userRoles.systemAdministrator) && !businessWritePermission + ); + }, [isAdmin, userRoles.systemAdministrator, businessWritePermission]); + return { isBusinessWriteDisabled, + isBWPOff, businessWritePermission }; }; diff --git a/packages/shared/lib/features/usePermission/__tests__/__snapshots__/index.test.ts.snap b/packages/shared/lib/features/usePermission/__tests__/__snapshots__/index.test.ts.snap index ab4518741..70b399e56 100644 --- a/packages/shared/lib/features/usePermission/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/shared/lib/features/usePermission/__tests__/__snapshots__/index.test.ts.snap @@ -26,6 +26,11 @@ exports[`usePermission should match snapshot 1`] = ` "OVERVIEW": { "CONFIGURE_RULE": "action:masking_overview_configure_rule", }, + "RULE": { + "CREATE": "action:masking_rule_create", + "DELETE": "action:masking_rule_delete", + "EDIT": "action:masking_rule_edit", + }, "TASK": { "CREATE": "action:masking_task_create", "EDIT": "action:masking_task_edit", @@ -138,6 +143,19 @@ exports[`usePermission should match snapshot 1`] = ` }, }, }, + "PROVISION": { + "DATABASE_ACCOUNT": { + "BATCH_MODIFY_PASSWORD": "action:db_auth_account_batch_modify_password", + "CREATE": "action:create_db_auth_account", + "DISCOVER_ACCOUNT": "action:db_auth_account_discover", + "TABLE_ACTIONS": "action:db_auth_account_table_actions", + }, + "DATABASE_ROLE": { + "CREATE": "action:create_db_auth_role", + "DELETE": "action:delete_db_auth_role", + "EDIT": "action:edit_db_auth_role", + }, + }, "SQLE": { "CUSTOM_RULE": { "CREATE": "action:create_custom_rule", @@ -255,6 +273,7 @@ exports[`usePermission should match snapshot 1`] = ` }, "PAGES": { "BASE": { + "DATA_MASKING": "page:data_masking", "DATA_SOURCE_MANAGEMENT": "page:data_source_management", "DB_SERVICE": "page:db_service", "GLOBAL_DATA_SOURCE": "page:global_data_source", @@ -264,6 +283,10 @@ exports[`usePermission should match snapshot 1`] = ` "SYSTEM_SETTING": "page:system_setting", "USER_CENTER": "page:user_center", }, + "PROVISION": { + "DATABASE_ACCOUNT": "page:database_account", + "DATABASE_ROLE": "page:database_role", + }, "SQLE": { "GLOBAL_OPERATION_RECORD": "page:global_operation_record", "KNOWLEDGE": "page:knowledge", @@ -358,11 +381,13 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:approve_data_export": { + "businessWrite": true, "id": "action:approve_data_export", "projectArchived": false, "type": "action", }, "action:approve_workflow": { + "businessWrite": true, "id": "action:approve_workflow", "projectArchived": false, "type": "action", @@ -392,6 +417,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:batch_close_workflow": { + "businessWrite": true, "id": "action:batch_close_workflow", "role": [ "admin", @@ -400,11 +426,13 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:batch_exec_workflow": { + "businessWrite": true, "id": "action:batch_exec_workflow", "projectArchived": false, "type": "action", }, "action:batch_ignore": { + "businessWrite": true, "id": "action:batch_ignore", "projectArchived": false, "projectManager": true, @@ -435,11 +463,13 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:batch_reject_workflow": { + "businessWrite": true, "id": "action:batch_reject_workflow", "projectArchived": false, "type": "action", }, "action:batch_resolve": { + "businessWrite": true, "id": "action:batch_resolve", "projectArchived": false, "projectManager": true, @@ -450,6 +480,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:batch_sql_assignment": { + "businessWrite": true, "id": "action:batch_sql_assignment", "projectArchived": false, "projectManager": true, @@ -479,11 +510,13 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:cancel_schedule_time_exec_task": { + "businessWrite": true, "id": "action:cancel_schedule_time_exec_task", "projectArchived": false, "type": "action", }, "action:cb_create_white_list": { + "businessWrite": true, "id": "action:cb_create_white_list", "projectArchived": false, "projectManager": true, @@ -529,6 +562,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:clone_workflow": { + "businessWrite": true, "dbServicePermission": { "opType": "create_workflow", }, @@ -541,11 +575,13 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:close_data_export": { + "businessWrite": true, "id": "action:close_data_export", "projectArchived": false, "type": "action", }, "action:close_workflow": { + "businessWrite": true, "id": "action:close_workflow", "projectArchived": false, "type": "action", @@ -559,6 +595,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:create_SQL_exception": { + "businessWrite": true, "id": "action:create_SQL_exception", "projectArchived": false, "projectManager": true, @@ -578,6 +615,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:create_data_export": { + "businessWrite": true, "dbServicePermission": { "opType": "create_export_task", }, @@ -590,7 +628,32 @@ exports[`usePermission should match snapshot 2`] = ` ], "type": "action", }, + "action:create_db_auth_account": { + "businessWrite": true, + "id": "action:create_db_auth_account", + "projectArchived": false, + "projectManager": true, + "projectPermission": "auth_db_service_data", + "role": [ + "admin", + "systemAdministrator", + ], + "type": "action", + }, + "action:create_db_auth_role": { + "businessWrite": true, + "id": "action:create_db_auth_role", + "projectArchived": false, + "projectManager": true, + "projectPermission": "manage_role_mange", + "role": [ + "admin", + "systemAdministrator", + ], + "type": "action", + }, "action:create_modified_sql_workflow": { + "businessWrite": true, "dbServicePermission": { "opType": "create_workflow", }, @@ -603,6 +666,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:create_pipeline_configuration": { + "businessWrite": true, "dbServicePermission": { "opType": "create_pipeline", }, @@ -627,6 +691,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:create_sql_audit": { + "businessWrite": true, "dbServicePermission": { "opType": "create_workflow", }, @@ -651,6 +716,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:create_sql_optimization": { + "businessWrite": true, "dbServicePermission": { "opType": "create_optimization", }, @@ -664,6 +730,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:create_workflow": { + "businessWrite": true, "dbServicePermission": { "opType": "create_workflow", }, @@ -677,6 +744,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:data_export_batch_close": { + "businessWrite": true, "id": "action:data_export_batch_close", "projectManager": true, "role": [ @@ -686,6 +754,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:data_export_create_whitelist": { + "businessWrite": true, "id": "action:data_export_create_whitelist", "projectArchived": false, "projectManager": true, @@ -696,7 +765,44 @@ exports[`usePermission should match snapshot 2`] = ` ], "type": "action", }, + "action:db_auth_account_batch_modify_password": { + "businessWrite": true, + "id": "action:db_auth_account_batch_modify_password", + "projectArchived": false, + "projectManager": true, + "projectPermission": "auth_db_service_data", + "role": [ + "admin", + "systemAdministrator", + ], + "type": "action", + }, + "action:db_auth_account_discover": { + "businessWrite": true, + "id": "action:db_auth_account_discover", + "projectArchived": false, + "projectManager": true, + "projectPermission": "auth_db_service_data", + "role": [ + "admin", + "systemAdministrator", + ], + "type": "action", + }, + "action:db_auth_account_table_actions": { + "businessWrite": true, + "id": "action:db_auth_account_table_actions", + "projectArchived": false, + "projectManager": true, + "projectPermission": "auth_db_service_data", + "role": [ + "admin", + "systemAdministrator", + ], + "type": "action", + }, "action:db_service_create_audit_plan": { + "businessWrite": true, "dbServicePermission": { "fieldName": "uid", "opType": "save_audit_plan", @@ -718,6 +824,18 @@ exports[`usePermission should match snapshot 2`] = ` ], "type": "action", }, + "action:delete_db_auth_role": { + "businessWrite": true, + "id": "action:delete_db_auth_role", + "projectArchived": false, + "projectManager": true, + "projectPermission": "manage_role_mange", + "role": [ + "admin", + "systemAdministrator", + ], + "type": "action", + }, "action:delete_db_service": { "id": "action:delete_db_service", "projectArchived": false, @@ -761,6 +879,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:delete_pipeline_configuration": { + "businessWrite": true, "dbServicePermission": { "opType": "create_pipeline", }, @@ -846,6 +965,18 @@ exports[`usePermission should match snapshot 2`] = ` ], "type": "action", }, + "action:edit_db_auth_role": { + "businessWrite": true, + "id": "action:edit_db_auth_role", + "projectArchived": false, + "projectManager": true, + "projectPermission": "manage_role_mange", + "role": [ + "admin", + "systemAdministrator", + ], + "type": "action", + }, "action:edit_db_service": { "id": "action:edit_db_service", "projectArchived": false, @@ -889,6 +1020,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:edit_pipeline_configuration": { + "businessWrite": true, "dbServicePermission": { "opType": "create_pipeline", }, @@ -948,6 +1080,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:edit_sql_remark": { + "businessWrite": true, "id": "action:sql_management_action_layout", "projectArchived": false, "projectManager": true, @@ -1065,16 +1198,22 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:exec_task": { + "businessWrite": true, "id": "action:exec_task", "projectArchived": false, "type": "action", }, "action:execute_data_export": { + "businessWrite": true, "id": "action:execute_data_export", "projectArchived": false, "type": "action", }, "action:export_cb_operation_log": { + "businessWrite": true, + "dbServicePermission": { + "opType": "sql_query", + }, "id": "action:export_cb_operation_log", "type": "action", }, @@ -1151,11 +1290,13 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:manually_exec_workflow": { + "businessWrite": true, "id": "action:manually_exec_workflow", "projectArchived": false, "type": "action", }, "action:masking_overview_configure_rule": { + "businessWrite": true, "id": "action:masking_overview_configure_rule", "projectPermission": "desensitization", "role": [ @@ -1165,7 +1306,41 @@ exports[`usePermission should match snapshot 2`] = ` ], "type": "action", }, + "action:masking_rule_create": { + "businessWrite": true, + "id": "action:masking_rule_create", + "projectPermission": "desensitization", + "role": [ + "admin", + "systemAdministrator", + "auditAdministrator", + ], + "type": "action", + }, + "action:masking_rule_delete": { + "businessWrite": true, + "id": "action:masking_rule_delete", + "projectPermission": "desensitization", + "role": [ + "admin", + "systemAdministrator", + "auditAdministrator", + ], + "type": "action", + }, + "action:masking_rule_edit": { + "businessWrite": true, + "id": "action:masking_rule_edit", + "projectPermission": "desensitization", + "role": [ + "admin", + "systemAdministrator", + "auditAdministrator", + ], + "type": "action", + }, "action:masking_task_create": { + "businessWrite": true, "id": "action:masking_task_create", "projectPermission": "desensitization", "role": [ @@ -1176,6 +1351,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:masking_task_edit": { + "businessWrite": true, "id": "action:masking_task_edit", "projectPermission": "desensitization", "role": [ @@ -1186,6 +1362,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:masking_task_view_history": { + "businessWrite": true, "id": "action:masking_task_view_history", "projectPermission": "desensitization", "role": [ @@ -1196,6 +1373,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:masking_template_create": { + "businessWrite": true, "id": "action:masking_template_create", "projectPermission": "desensitization", "role": [ @@ -1206,6 +1384,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:masking_template_delete": { + "businessWrite": true, "id": "action:masking_template_delete", "projectPermission": "desensitization", "role": [ @@ -1216,6 +1395,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:masking_template_edit": { + "businessWrite": true, "id": "action:masking_template_edit", "projectPermission": "desensitization", "role": [ @@ -1250,6 +1430,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:plugin_audit_create_whitelist": { + "businessWrite": true, "id": "action:plugin_audit_create_whitelist", "projectArchived": false, "projectManager": true, @@ -1348,6 +1529,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:push_to_coding": { + "businessWrite": true, "id": "action:push_to_coding", "projectArchived": false, "projectManager": true, @@ -1358,6 +1540,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:reject_data_export": { + "businessWrite": true, "id": "action:reject_data_export", "projectArchived": false, "type": "action", @@ -1372,16 +1555,19 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:retry_workflow": { + "businessWrite": true, "id": "action:retry_workflow", "projectArchived": false, "type": "action", }, "action:rollback_workflow": { + "businessWrite": true, "id": "action:rollback_workflow", "projectArchived": false, "type": "action", }, "action:rule_create_rule_template": { + "businessWrite": true, "id": "action:rule_create_rule_template", "projectArchived": false, "projectManager": true, @@ -1392,6 +1578,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:schedule_time_exec_task": { + "businessWrite": true, "id": "action:schedule_time_exec_task", "projectArchived": false, "type": "action", @@ -1405,6 +1592,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:sql_assignment": { + "businessWrite": true, "id": "action:sql_assignment", "projectArchived": false, "projectManager": true, @@ -1415,6 +1603,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:sql_management_action_layout": { + "businessWrite": true, "id": "action:sql_management_action_layout", "projectArchived": false, "projectManager": true, @@ -1426,6 +1615,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:sql_management_conf_create_operator": { + "businessWrite": true, "dbServicePermission": { "opType": "save_audit_plan", }, @@ -1439,6 +1629,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:sql_management_conf_delete_operator": { + "businessWrite": true, "dbServicePermission": { "fieldName": "instance_id", "opType": "save_audit_plan", @@ -1453,6 +1644,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:sql_management_conf_detail_audit_operator": { + "businessWrite": true, "dbServicePermission": { "opType": "save_audit_plan", }, @@ -1466,6 +1658,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:sql_management_conf_detail_delete_operator": { + "businessWrite": true, "dbServicePermission": { "opType": "save_audit_plan", }, @@ -1479,6 +1672,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:sql_management_conf_detail_enable_operator": { + "businessWrite": true, "dbServicePermission": { "opType": "save_audit_plan", }, @@ -1492,6 +1686,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:sql_management_conf_detail_stop_operator": { + "businessWrite": true, "dbServicePermission": { "opType": "save_audit_plan", }, @@ -1505,6 +1700,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:sql_management_conf_edit_operator": { + "businessWrite": true, "dbServicePermission": { "fieldName": "instance_id", "opType": "save_audit_plan", @@ -1519,6 +1715,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:sql_management_conf_enable_operator": { + "businessWrite": true, "dbServicePermission": { "fieldName": "instance_id", "opType": "save_audit_plan", @@ -1533,6 +1730,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:sql_management_conf_reset_token": { + "businessWrite": true, "dbServicePermission": { "opType": "save_audit_plan", }, @@ -1546,6 +1744,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:sql_management_conf_stop_operator": { + "businessWrite": true, "dbServicePermission": { "fieldName": "instance_id", "opType": "save_audit_plan", @@ -1560,6 +1759,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:sql_management_create_white_list": { + "businessWrite": true, "id": "action:sql_management_create_white_list", "projectArchived": false, "projectManager": true, @@ -1598,11 +1798,13 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:terminate_exec_task": { + "businessWrite": true, "id": "action:terminate_exec_task", "projectArchived": false, "type": "action", }, "action:terminate_exec_workflow": { + "businessWrite": true, "id": "action:terminate_exec_workflow", "projectArchived": false, "type": "action", @@ -1622,6 +1824,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:update_sql_priority": { + "businessWrite": true, "id": "action:update_sql_priority", "projectArchived": false, "projectManager": true, @@ -1632,6 +1835,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:update_sql_status": { + "businessWrite": true, "id": "action:update_sql_status", "projectArchived": false, "projectManager": true, @@ -1661,6 +1865,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:version_management_add_operator": { + "businessWrite": true, "dbServicePermission": { "opType": "version_manage", }, @@ -1674,6 +1879,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:version_management_delete_operator": { + "businessWrite": true, "dbServicePermission": { "opType": "version_manage", }, @@ -1687,6 +1893,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:version_management_deploy_operator": { + "businessWrite": true, "dbServicePermission": { "opType": "create_workflow", }, @@ -1700,6 +1907,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:version_management_edit_operator": { + "businessWrite": true, "dbServicePermission": { "opType": "version_manage", }, @@ -1713,6 +1921,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:version_management_lock_operator": { + "businessWrite": true, "dbServicePermission": { "opType": "version_manage", }, @@ -1747,6 +1956,7 @@ exports[`usePermission should match snapshot 2`] = ` "type": "action", }, "action:workflow_sql_audit_result_create_white_list": { + "businessWrite": true, "id": "action:workflow_sql_audit_result_create_white_list", "projectArchived": false, "projectManager": true, @@ -1767,6 +1977,16 @@ exports[`usePermission should match snapshot 2`] = ` ], "type": "page", }, + "page:data_masking": { + "id": "page:data_masking", + "projectPermission": "desensitization", + "role": [ + "admin", + "systemAdministrator", + "auditAdministrator", + ], + "type": "page", + }, "page:data_source_management": { "id": "page:data_source_management", "role": [ @@ -1777,6 +1997,26 @@ exports[`usePermission should match snapshot 2`] = ` ], "type": "page", }, + "page:database_account": { + "id": "page:database_account", + "projectPermission": "auth_db_service_data", + "role": [ + "admin", + "systemAdministrator", + "auditAdministrator", + ], + "type": "page", + }, + "page:database_role": { + "id": "page:database_role", + "projectPermission": "manage_role_mange", + "role": [ + "admin", + "systemAdministrator", + "auditAdministrator", + ], + "type": "page", + }, "page:db_service": { "id": "page:db_service", "projectPermission": "manage_project_data_source", diff --git a/packages/shared/lib/features/usePermission/permissionManifest.ts b/packages/shared/lib/features/usePermission/permissionManifest.ts index 3c05f7c8e..fa6e9ddea 100644 --- a/packages/shared/lib/features/usePermission/permissionManifest.ts +++ b/packages/shared/lib/features/usePermission/permissionManifest.ts @@ -348,6 +348,9 @@ export const PERMISSION_MANIFEST: Record< [PERMISSIONS.ACTIONS.BASE.CLOUD_BEAVER.EXPORT]: { id: PERMISSIONS.ACTIONS.BASE.CLOUD_BEAVER.EXPORT, type: 'action', + dbServicePermission: { + opType: OpPermissionItemOpPermissionTypeEnum.sql_query + }, businessWrite: true }, [PERMISSIONS.ACTIONS.BASE.CLOUD_BEAVER.CREATE_WHITE_LIST]: { diff --git a/packages/shared/lib/features/usePermission/usePermission.ts b/packages/shared/lib/features/usePermission/usePermission.ts index 7224bfdf7..4f108ee31 100644 --- a/packages/shared/lib/features/usePermission/usePermission.ts +++ b/packages/shared/lib/features/usePermission/usePermission.ts @@ -23,7 +23,7 @@ import useBusinessWritePermission from '../useBusinessWritePermission'; const usePermission = () => { const { userRoles, bindProjects } = useCurrentUser(); - const { isBusinessWriteDisabled } = useBusinessWritePermission(); + const { isBusinessWriteDisabled, isBWPOff } = useBusinessWritePermission(); const { moduleFeatureSupport, userOperationPermissions } = useSelector( (state: IReduxState) => ({ moduleFeatureSupport: state.permission.moduleFeatureSupport, @@ -210,23 +210,115 @@ const usePermission = () => { ); const checkActionDisabledByBWP = useCallback( - (requiredPermission: PermissionsConstantType): boolean => { + ( + requiredPermission: PermissionsConstantType, + otherValues?: { + record?: Record; + authDataSourceId?: string; + } + ): boolean => { const permissionDetails = PERMISSION_MANIFEST[requiredPermission]; - // BWP=off 时,所有标记为 businessWrite 的操作保留页面结构但禁用 - // 白名单思路:只有项目配置模块(数据源、审核流程模板、成员与权限、推送规则、审核SQL例外、管控SQL例外) - // 下的操作不标记 businessWrite,其余项目内业务写操作均标记 businessWrite=true - return ( - permissionDetails.businessWrite === true && isBusinessWriteDisabled - ); + + // Non-businessWrite actions are never disabled by BWP + if (permissionDetails.businessWrite !== true) { + return false; + } + + // BWP is on, or user is not admin/systemAdministrator => no BWP restriction + if (!isBWPOff) { + return false; + } + + // BWP=off and user has NO project-level authorization at all => disabled + if (isBusinessWriteDisabled) { + return true; + } + + // BWP=off but user has project-level authorization (isBusinessWriteDisabled=false). + // In this case, we must check whether the user actually has the SPECIFIC + // permission for this particular action. If the action requires a + // dbServicePermission or projectPermission that the user doesn't have, + // the button should remain disabled. + // + // This fixes the bug where having ANY project role (e.g., "development + // engineer" with create_workflow) would enable ALL businessWrite buttons + // (including data export, data masking, etc.). + + // Check project-level permission (e.g., desensitization, manage_role_mange) + if (permissionDetails.projectPermission) { + if (checkProjectPermission(permissionDetails.projectPermission)) { + return false; // User has this specific project permission => not disabled + } + } + + // Check db-service-level permission (e.g., create_workflow, version_manage) + if (permissionDetails.dbServicePermission) { + const { fieldName, opType } = permissionDetails.dbServicePermission; + const recordTyped = otherValues?.record as + | Record + | undefined; + if ( + checkDbServicePermission( + opType, + fieldName ? recordTyped?.[fieldName] : otherValues?.authDataSourceId + ) + ) { + return false; // User has this specific db-service permission => not disabled + } + } + + // Check if user is project manager (project managers can do everything) + if (permissionDetails.projectManager === true) { + const { isManager } = getProjectAttributesStatus(); + if (isManager) { + return false; + } + } + + // If the action has no specific dbServicePermission and no projectPermission + // requirement, it is a generic businessWrite action (like workflow approve, + // batch close, etc.). Since the user already has project-level authorization + // (isBusinessWriteDisabled=false), these generic actions should be enabled. + // Their access control is handled by other mechanisms (e.g., assignee checks). + if ( + !permissionDetails.dbServicePermission && + !permissionDetails.projectPermission + ) { + return false; + } + + // User has a specific permission requirement but doesn't meet it => disabled + return true; }, - [isBusinessWriteDisabled] + [ + isBWPOff, + isBusinessWriteDisabled, + checkProjectPermission, + checkDbServicePermission, + getProjectAttributesStatus + ] ); const mergeActionButtonPropsWithBWPDisabled = useCallback( ( buttonProps: ((record?: T) => Record) | undefined, - bwpDisabled: boolean + bwpDisabled: boolean | ((record?: T) => boolean) ): ((record?: T) => Record) | undefined => { + if (typeof bwpDisabled === 'function') { + // Per-record BWP check: evaluate lazily for each row + if (typeof buttonProps === 'function') { + return (record?: T) => { + const disabled = bwpDisabled(record); + return disabled + ? { ...buttonProps(record), disabled: true } + : buttonProps(record); + }; + } + return (record?: T) => { + const disabled = bwpDisabled(record); + return disabled ? { disabled: true } : {}; + }; + } if (!bwpDisabled) return buttonProps; if (typeof buttonProps === 'function') { return (record?: T) => ({ @@ -249,8 +341,13 @@ const usePermission = () => { ): ActiontechTableProps['actions'] => { if (Array.isArray(actions)) { return actions.map((item) => { - const bwpDisabled = item.permissions - ? checkActionDisabledByBWP(item.permissions) + // Create per-record BWP check that passes record context for + // db-service-level permission evaluation + const bwpDisabledFn = item.permissions + ? (record?: T) => + checkActionDisabledByBWP(item.permissions!, { + record: record as unknown as Record + }) : false; return { ...item, @@ -259,7 +356,7 @@ const usePermission = () => { : undefined, buttonProps: mergeActionButtonPropsWithBWPDisabled( item.buttonProps, - bwpDisabled + bwpDisabledFn ) }; }); @@ -272,7 +369,9 @@ const usePermission = () => { return (record: T) => moreButtons(record).map((item) => { const bwpDisabled = item.permissions - ? checkActionDisabledByBWP(item.permissions) + ? checkActionDisabledByBWP(item.permissions!, { + record: record as unknown as Record + }) : false; return { ...item, @@ -302,8 +401,12 @@ const usePermission = () => { return { ...actions, buttons: actions.buttons.map((item) => { - const bwpDisabled = item.permissions - ? checkActionDisabledByBWP(item.permissions) + // Create per-record BWP check for row-level buttons + const bwpDisabledFn = item.permissions + ? (record?: T) => + checkActionDisabledByBWP(item.permissions!, { + record: record as unknown as Record + }) : false; return { ...item, @@ -312,7 +415,7 @@ const usePermission = () => { : undefined, buttonProps: mergeActionButtonPropsWithBWPDisabled( item.buttonProps, - bwpDisabled + bwpDisabledFn ) }; }), diff --git a/packages/shared/lib/testUtil/mockHook/mockUseBusinessWritePermission.ts b/packages/shared/lib/testUtil/mockHook/mockUseBusinessWritePermission.ts index eac432725..ee06e9e4b 100644 --- a/packages/shared/lib/testUtil/mockHook/mockUseBusinessWritePermission.ts +++ b/packages/shared/lib/testUtil/mockHook/mockUseBusinessWritePermission.ts @@ -2,6 +2,7 @@ import * as useBusinessWritePermission from '../../features/useBusinessWritePerm export const mockBusinessWritePermissionReturn = { isBusinessWriteDisabled: false, + isBWPOff: false, businessWritePermission: true }; diff --git a/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/index.tsx b/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/index.tsx index cdcec687e..b987a6c49 100644 --- a/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/index.tsx +++ b/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/index.tsx @@ -39,12 +39,15 @@ import { } from '@actiontech/shared/lib/api/sqle/service/common.enum'; import ModifiedSqlDrawer from './component/ModifiedSqlDrawer'; import { Key, useMemo, useState, useRef } from 'react'; -import { useBusinessWritePermission } from '@actiontech/shared/lib/features'; +import { usePermission, PERMISSIONS } from '@actiontech/shared/lib/features'; import { IGenDatabaseDiffModifySQLsV1Params } from '@actiontech/shared/lib/api/sqle/service/database_comparison/index.d'; const ComparisonEntry: React.FC = () => { const { t } = useTranslation(); const { projectName } = useCurrentProject(); - const { isBusinessWriteDisabled } = useBusinessWritePermission(); + const { checkActionDisabledByBWP } = usePermission(); + const isBusinessWriteDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.SQLE.DATA_SOURCE_COMPARISON.CREATE_MODIFIED_SQL_WORKFLOW + ); const [messageApi, messageContextHolder] = message.useMessage(); const treeRef = useRef(null); const [ diff --git a/packages/sqle/src/page/SqlAudit/List/component/SqlAuditTags/index.tsx b/packages/sqle/src/page/SqlAudit/List/component/SqlAuditTags/index.tsx index 80e74162f..1832cd6a7 100644 --- a/packages/sqle/src/page/SqlAudit/List/component/SqlAuditTags/index.tsx +++ b/packages/sqle/src/page/SqlAudit/List/component/SqlAuditTags/index.tsx @@ -13,7 +13,7 @@ import { Divider, Form, InputRef, Popover, Space, Spin, message } from 'antd'; import { useForm } from 'antd/es/form/Form'; import useSQLAuditRecordTag from '../../../../../hooks/useSQLAuditRecordTag'; import { tagNameRule } from '@actiontech/dms-kit'; -import { useBusinessWritePermission } from '@actiontech/shared/lib/features'; +import { usePermission, PERMISSIONS } from '@actiontech/shared/lib/features'; export interface ISqlAuditTags { projectName: string; defaultTags: string[]; @@ -25,7 +25,10 @@ const SqlAuditTags = ({ updateTags }: ISqlAuditTags) => { const { t } = useTranslation(); - const { isBusinessWriteDisabled } = useBusinessWritePermission(); + const { checkActionDisabledByBWP } = usePermission(); + const isBusinessWriteDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.SQLE.SQL_AUDIT.CREATE + ); const [messageApi, messageContextHolder] = message.useMessage(); const [open, setOpen] = useState(false); const [extraTagForm] = useForm<{ diff --git a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.tsx b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.tsx index c555afcb9..4961e796f 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.tsx @@ -14,10 +14,7 @@ import AuditResultDrawer from './AuditResultDrawer'; import useWhitelistRedux from '../../../../Whitelist/hooks/useWhitelistRedux'; import AddWhitelistModal from '../../../../Whitelist/Drawer/AddWhitelist'; import { AuditResultForCreateWorkflowActions } from './actions'; -import { - usePermission, - useBusinessWritePermission -} from '@actiontech/shared/lib/features'; +import { usePermission, PERMISSIONS } from '@actiontech/shared/lib/features'; import { parse2ReactRouterPath } from '@actiontech/shared/lib/components/TypedRouter/utils'; import { ROUTE_PATHS } from '@actiontech/dms-kit'; import SwitchSqlBackupStrategyModal from './SwitchSqlBackupStrategyModal'; @@ -56,8 +53,11 @@ const AuditResultTable: React.FC = ({ const { pagination, tableChange, setPagination } = useTableRequestParams(); const { requestErrorMessage, handleTableRequestError } = useTableRequestError(); - const { parse2TableActionPermissions } = usePermission(); - const { isBusinessWriteDisabled } = useBusinessWritePermission(); + const { parse2TableActionPermissions, checkActionDisabledByBWP } = + usePermission(); + const isBusinessWriteDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.SQLE.SQL_EXEC_WORKFLOW.CREATE + ); const { openCreateWhitelistModal, updateSelectWhitelistRecord } = useWhitelistRedux(); const [ diff --git a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/PageHeaderExtra/hooks/useWorkflowDetailAction.tsx b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/PageHeaderExtra/hooks/useWorkflowDetailAction.tsx index b8f72333d..5043995ae 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/PageHeaderExtra/hooks/useWorkflowDetailAction.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/PageHeaderExtra/hooks/useWorkflowDetailAction.tsx @@ -10,7 +10,8 @@ import { } from '@actiontech/shared/lib/api/sqle/service/common.enum'; import { useCurrentUser, - useBusinessWritePermission + usePermission, + PERMISSIONS } from '@actiontech/shared/lib/features'; import dayjs, { Dayjs } from 'dayjs'; import { @@ -52,7 +53,13 @@ const useWorkflowDetailAction = ({ const { t } = useTranslation(); const [messageApi, messageContextHolder] = message.useMessage(); const { username } = useCurrentUser(); - const { isBusinessWriteDisabled } = useBusinessWritePermission(); + const { checkActionDisabledByBWP } = usePermission(); + const isWorkflowApproveBWPDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.SQLE.SQL_EXEC_WORKFLOW.APPROVE + ); + const isWorkflowExecBWPDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.SQLE.SQL_EXEC_WORKFLOW.BATCH_EXEC + ); const currentStep = useMemo(() => { return workflowInfo?.record?.workflow_step_list?.find( @@ -340,25 +347,25 @@ const useWorkflowDetailAction = ({ action: auditPassWorkflow, loading: passLoading, hidden: !auditWorkflowButtonVisibility, - disabled: isBusinessWriteDisabled + disabled: isWorkflowApproveBWPDisabled }, rejectWorkflowButtonMeta: { action: rejectWorkflow, loading: rejectLoading, hidden: !rejectWorkflowButtonVisibility, - disabled: isBusinessWriteDisabled + disabled: isWorkflowApproveBWPDisabled }, batchExecutingWorkflowButtonMeta: { action: executingWorkflow, loading: executingLoading, hidden: !executingButtonVisibility, - disabled: isBusinessWriteDisabled + disabled: isWorkflowExecBWPDisabled }, manualExecuteWorkflowButtonMeta: { action: completeWorkflow, loading: completeLoading, hidden: !manualExecuteButtonVisibility, - disabled: isBusinessWriteDisabled + disabled: isWorkflowExecBWPDisabled }, terminateWorkflowButtonMeta: { action: terminateWorkflow, diff --git a/packages/sqle/src/page/VersionManagement/Detail/components/StageNode/index.tsx b/packages/sqle/src/page/VersionManagement/Detail/components/StageNode/index.tsx index 37e533ae7..6e36f7998 100644 --- a/packages/sqle/src/page/VersionManagement/Detail/components/StageNode/index.tsx +++ b/packages/sqle/src/page/VersionManagement/Detail/components/StageNode/index.tsx @@ -6,7 +6,8 @@ import type { Node, NodeProps } from '@xyflow/react'; import { StageNodeStyleWrapper } from '../../style'; import { useCurrentProject, - useBusinessWritePermission + usePermission, + PERMISSIONS } from '@actiontech/shared/lib/features'; import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; @@ -35,7 +36,16 @@ const StageNode: React.FC>> = ({ } = data; const { t } = useTranslation(); const { projectID } = useCurrentProject(); - const { isBusinessWriteDisabled } = useBusinessWritePermission(); + const { checkActionDisabledByBWP } = usePermission(); + const isVersionEditBWPDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.SQLE.VERSION_MANAGEMENT.EDIT + ); + const isVersionAddBWPDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.SQLE.VERSION_MANAGEMENT.ADD + ); + const isWorkflowCreateBWPDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.SQLE.SQL_EXEC_WORKFLOW.CREATE + ); const displayWorkflow = useMemo(() => { // 版本初始化不存在工单时 统一展示一个空占位 if (!workflowList?.length) { @@ -103,7 +113,7 @@ const StageNode: React.FC>> = ({ onRetry?.(workflow?.workflow_id ?? '')} - disabled={isBusinessWriteDisabled} + disabled={isVersionEditBWPDisabled} > {t('versionManagement.stageNode.updateInfo')} @@ -112,7 +122,7 @@ const StageNode: React.FC>> = ({ onClick={() => onOfflineExecute?.(workflow.workflow_id ?? '') } - disabled={isBusinessWriteDisabled} + disabled={isVersionEditBWPDisabled} > {t('versionManagement.stageNode.offlineExecuted')} @@ -127,7 +137,7 @@ const StageNode: React.FC>> = ({ type="primary" onClick={() => onAssociateWorkflow?.(data.stageId ?? 0)} disabled={ - isBusinessWriteDisabled || + isVersionAddBWPDisabled || versionStatus === SqlVersionDetailResV1StatusEnum.locked } > @@ -137,7 +147,7 @@ const StageNode: React.FC>> = ({ type="primary" onClick={onCreateNewWorkflow} disabled={ - isBusinessWriteDisabled || + isWorkflowCreateBWPDisabled || versionStatus === SqlVersionDetailResV1StatusEnum.locked } > From abf17b58a14582b75a62a279ff52a094eb5cf350 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Mon, 11 May 2026 09:56:16 +0000 Subject: [PATCH 18/24] fix(bwp): bypass BWP disable for workflow assignees in candidate set (#813) When a user is directly specified as an approver/executor/exporter in a workflow template node and the workflow transitions to that user, the corresponding action buttons (approve/reject/execute/export) should be enabled even if the user has BWP=off. The backend already handles this correctly by checking assignee membership, but the frontend was unconditionally disabling buttons based on BWP status without considering whether the current user is in the workflow step's assignee list. Fix: combine the BWP disable check with the assignee visibility check so that buttons are only disabled when BOTH BWP says disabled AND the user is NOT in the current step's assignee/candidate set. --- .../components/PageHeaderAction/useActionButtonState.ts | 6 +++--- .../PageHeaderExtra/hooks/useWorkflowDetailAction.tsx | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/useActionButtonState.ts b/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/useActionButtonState.ts index 59032523e..3207451e0 100644 --- a/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/useActionButtonState.ts +++ b/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/useActionButtonState.ts @@ -110,19 +110,19 @@ const useActionButtonState: (messageApi: MessageInstance) => { action: () => approveWorkflow(workflowID), hidden: !approveWorkflowButtonVisibility, loading: approveWorkflowLoading, - disabled: isExportApproveBWPDisabled + disabled: isExportApproveBWPDisabled && !allowOperateStep }, rejectWorkflowButtonMeta: { action: () => updateWorkflowRejectOpen(true), hidden: !rejectWorkflowButtonVisibility, loading: false, - disabled: isExportRejectBWPDisabled + disabled: isExportRejectBWPDisabled && !allowOperateStep }, executeExportButtonMeta: { action: () => executeExport(workflowID), hidden: !executingButtonVisibility, loading: executeExportLoading, - disabled: isExportExecuteBWPDisabled + disabled: isExportExecuteBWPDisabled && !executingButtonVisibility } }; }; diff --git a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/PageHeaderExtra/hooks/useWorkflowDetailAction.tsx b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/PageHeaderExtra/hooks/useWorkflowDetailAction.tsx index 5043995ae..7e16ef7cd 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Detail/components/PageHeaderExtra/hooks/useWorkflowDetailAction.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Detail/components/PageHeaderExtra/hooks/useWorkflowDetailAction.tsx @@ -347,25 +347,25 @@ const useWorkflowDetailAction = ({ action: auditPassWorkflow, loading: passLoading, hidden: !auditWorkflowButtonVisibility, - disabled: isWorkflowApproveBWPDisabled + disabled: isWorkflowApproveBWPDisabled && !auditWorkflowButtonVisibility }, rejectWorkflowButtonMeta: { action: rejectWorkflow, loading: rejectLoading, hidden: !rejectWorkflowButtonVisibility, - disabled: isWorkflowApproveBWPDisabled + disabled: isWorkflowApproveBWPDisabled && !rejectWorkflowButtonVisibility }, batchExecutingWorkflowButtonMeta: { action: executingWorkflow, loading: executingLoading, hidden: !executingButtonVisibility, - disabled: isWorkflowExecBWPDisabled + disabled: isWorkflowExecBWPDisabled && !executingButtonVisibility }, manualExecuteWorkflowButtonMeta: { action: completeWorkflow, loading: completeLoading, hidden: !manualExecuteButtonVisibility, - disabled: isWorkflowExecBWPDisabled + disabled: isWorkflowExecBWPDisabled && !manualExecuteButtonVisibility }, terminateWorkflowButtonMeta: { action: terminateWorkflow, From 1ef8aa8c8446e07162a1d4623c9f5bc0d3bd15b6 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Mon, 11 May 2026 10:32:52 +0000 Subject: [PATCH 19/24] fix(bwp): add skipBWPCheck to PermissionControl for workflow assignee buttons (#813) PermissionControl independently calls checkActionDisabledByBWP and force-sets disabled=true on child elements, overriding the disabled state computed by useWorkflowDetailAction/useActionButtonState hooks which already account for workflow assignee membership. Add skipBWPCheck prop to PermissionControl to let workflow detail action buttons delegate BWP logic entirely to their hooks. Affected components: - SQL workflow detail: approve, reject, batch exec, manual exec buttons - Data export workflow detail: approve, reject, execute buttons --- .../Detail/components/PageHeaderAction/actions.tsx | 7 ++++++- packages/shared/lib/features/PermissionControl/index.tsx | 5 +++-- .../shared/lib/features/PermissionControl/index.type.ts | 7 +++++++ .../Detail/components/PageHeaderExtra/action.tsx | 4 ++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/actions.tsx b/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/actions.tsx index 085e34805..c1ebb3e2a 100644 --- a/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/actions.tsx +++ b/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/actions.tsx @@ -27,7 +27,10 @@ export const CloseWorkflowAction = (closeWorkflowButtonMeta: ActionMeta) => { }; export const RejectWorkflowAction = (rejectWorkflowButtonMeta: ActionMeta) => { return ( - +