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/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/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/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/CloudBeaver/index.ce.test.tsx b/packages/base/src/page/CloudBeaver/index.ce.test.tsx index 3439aabde..7b6dce21c 100644 --- a/packages/base/src/page/CloudBeaver/index.ce.test.tsx +++ b/packages/base/src/page/CloudBeaver/index.ce.test.tsx @@ -8,12 +8,22 @@ import { createSpySuccessResponse } from '@actiontech/shared/lib/testUtil/mockAp import { enableSqlQueryUrlData } from '@actiontech/shared/lib/testUtil/mockApi/base/cloudBeaver/data'; import { baseSuperRender } from '../../testUtils/superRender'; import { OPEN_CLOUD_BEAVER_URL_PARAM_NAME } from '@actiontech/dms-kit'; +import { mockUsePermission } from '@actiontech/shared/lib/testUtil'; describe('test base/page/CloudBeaver', () => { let getSqlQueryUrlSpy: jest.SpyInstance; beforeEach(() => { jest.useFakeTimers(); getSqlQueryUrlSpy = cloudBeaver.getSqlQueryUrl(); + mockUsePermission( + { + checkActionDisabledByBWP: jest.fn().mockReturnValue(false), + checkActionPermission: jest.fn().mockReturnValue(true) + }, + { + useSpyOnMockHooks: true + } + ); }); afterEach(() => { diff --git a/packages/base/src/page/CloudBeaver/index.test.tsx b/packages/base/src/page/CloudBeaver/index.test.tsx index c56baccfa..07d26b1ab 100644 --- a/packages/base/src/page/CloudBeaver/index.test.tsx +++ b/packages/base/src/page/CloudBeaver/index.test.tsx @@ -12,6 +12,7 @@ import dbServices from '@actiontech/shared/lib/testUtil/mockApi/base/dbServices' import { useDispatch, useSelector } from 'react-redux'; import { driverMeta } from 'sqle/src/hooks/useDatabaseType/index.test.data'; import { OPEN_CLOUD_BEAVER_URL_PARAM_NAME } from '@actiontech/dms-kit'; +import { mockUsePermission } from '@actiontech/shared/lib/testUtil/mockHook/mockUsePermission'; jest.mock('react-redux', () => { return { @@ -138,4 +139,143 @@ describe('test base/page/CloudBeaver', () => { expect(window.location.href).toBe(enableSqlQueryUrlData.sql_query_root_uri); }); + + it('should disable jump to cloud beaver button when business write permission is off', async () => { + mockUseCurrentUser({ + businessWritePermission: false + }); + + getSqlQueryUrlSpy.mockImplementation(() => + createSpySuccessResponse({ + data: enableSqlQueryUrlData + }) + ); + + superRender(); + + await act(async () => jest.advanceTimersByTime(3000)); + + const button = screen.getByText('打开SQL工作台').closest('button'); + expect(button).toBeDisabled(); + }); + + it('should not auto redirect when business write permission is off', async () => { + const savedHref = window.location.href; + window.location.href = ''; + + mockUseCurrentUser({ + businessWritePermission: false + }); + + getSqlQueryUrlSpy.mockImplementation(() => + createSpySuccessResponse({ + data: enableSqlQueryUrlData + }) + ); + + superRender(, undefined, { + routerProps: { + initialEntries: [ + `/cloudBeaver?${OPEN_CLOUD_BEAVER_URL_PARAM_NAME}=true` + ] + } + }); + + await act(async () => jest.advanceTimersByTime(3000)); + + expect(window.location.href).not.toBe( + enableSqlQueryUrlData.sql_query_root_uri + ); + + window.location.href = savedHref; + }); + + describe('usePermission OPEN_CLOUD_BEAVER', () => { + let usePermissionSpy: jest.SpyInstance | undefined; + + afterEach(() => { + usePermissionSpy?.mockRestore(); + usePermissionSpy = undefined; + }); + + it('should hide jump button in header when checkActionPermission returns false', async () => { + usePermissionSpy = mockUsePermission( + { + checkActionPermission: jest.fn().mockReturnValue(false), + checkActionDisabledByBWP: jest.fn().mockReturnValue(false) + }, + { useSpyOnMockHooks: true } + ); + + getSqlQueryUrlSpy.mockImplementation(() => + createSpySuccessResponse({ + data: enableSqlQueryUrlData + }) + ); + + superRender(); + + await act(async () => jest.advanceTimersByTime(3000)); + + expect(screen.queryByText('打开SQL工作台')).not.toBeInTheDocument(); + }); + + it('should render disabled jump button when checkActionDisabledByBWP returns true', async () => { + usePermissionSpy = mockUsePermission( + { + checkActionPermission: jest.fn().mockReturnValue(true), + checkActionDisabledByBWP: jest.fn().mockReturnValue(true) + }, + { useSpyOnMockHooks: true } + ); + + getSqlQueryUrlSpy.mockImplementation(() => + createSpySuccessResponse({ + data: enableSqlQueryUrlData + }) + ); + + superRender(); + + await act(async () => jest.advanceTimersByTime(3000)); + + const button = screen.getByText('打开SQL工作台').closest('button'); + expect(button).toBeDisabled(); + }); + + it('should not auto redirect when checkActionDisabledByBWP returns true', async () => { + const savedHref = window.location.href; + window.location.href = ''; + + usePermissionSpy = mockUsePermission( + { + checkActionPermission: jest.fn().mockReturnValue(true), + checkActionDisabledByBWP: jest.fn().mockReturnValue(true) + }, + { useSpyOnMockHooks: true } + ); + + getSqlQueryUrlSpy.mockImplementation(() => + createSpySuccessResponse({ + data: enableSqlQueryUrlData + }) + ); + + superRender(, undefined, { + routerProps: { + initialEntries: [ + `/cloudBeaver?${OPEN_CLOUD_BEAVER_URL_PARAM_NAME}=true` + ] + } + }); + + await act(async () => jest.advanceTimersByTime(3000)); + + expect(window.location.href).not.toBe( + enableSqlQueryUrlData.sql_query_root_uri + ); + + window.location.href = savedHref; + }); + }); }); diff --git a/packages/base/src/page/CloudBeaver/index.tsx b/packages/base/src/page/CloudBeaver/index.tsx index c6219bec8..e33154cb3 100644 --- a/packages/base/src/page/CloudBeaver/index.tsx +++ b/packages/base/src/page/CloudBeaver/index.tsx @@ -17,10 +17,19 @@ import { import CBOperationLogsList from './List/index'; import { DownOutlined } from '@ant-design/icons'; import { EnterpriseFeatureDisplay, useTypedQuery } from '@actiontech/shared'; +import { usePermission, PERMISSIONS } from '@actiontech/shared/lib/features'; const CloudBeaver = () => { const { t } = useTranslation(); const extractQueries = useTypedQuery(); + const { checkActionDisabledByBWP, checkActionPermission } = usePermission(); + + const isBusinessWriteDisabled = checkActionDisabledByBWP( + PERMISSIONS.ACTIONS.BASE.CLOUD_BEAVER.OPEN_CLOUD_BEAVER + ); + const allowOpenCloudBeaver = checkActionPermission( + PERMISSIONS.ACTIONS.BASE.CLOUD_BEAVER.OPEN_CLOUD_BEAVER + ); const [getOperationLogsLoading, setGetOperationLogsLoading] = useState(false); const { @@ -47,7 +56,8 @@ const CloudBeaver = () => { extractQueries(ROUTE_PATHS.BASE.CLOUD_BEAVER.index)?.open_cloud_beaver === String(true) && !loading && - data + data && + !isBusinessWriteDisabled ) { let url = ''; @@ -61,9 +71,17 @@ const CloudBeaver = () => { window.location.href = url; } } - }, [extractQueries, loading, data]); + }, [extractQueries, loading, data, isBusinessWriteDisabled]); const renderActionButton = useMemo(() => { + if (isBusinessWriteDisabled) { + return ( + + {t('dmsCloudBeaver.jumpToCloudBeaver')} + + ); + } + if (loading) { return ( @@ -126,7 +144,7 @@ const CloudBeaver = () => { ); - }, [data, loading, t, handleMenuClick]); + }, [data, loading, t, handleMenuClick, isBusinessWriteDisabled]); // Determine if the main content should be displayed const isFeatureEnabled = useMemo(() => { @@ -139,7 +157,11 @@ const CloudBeaver = () => { <> {renderActionButton}} + extra={ + + {renderActionButton} + + } /> diff --git a/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/__tests__/index.test.tsx b/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/__tests__/index.test.tsx index 521c3bf14..8a4862b42 100644 --- a/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/__tests__/index.test.tsx +++ b/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/__tests__/index.test.tsx @@ -10,6 +10,7 @@ import { mockUseDataExportDetailReduxManage } from '../../../testUtils/mockUseDataExportDetailReduxManage'; import { fireEvent, screen } from '@testing-library/react'; +import { mockUsePermission } from '@actiontech/shared/lib/testUtil/mockHook/mockUsePermission'; describe('test base/DataExport/Detail/PageHeaderAction', () => { beforeEach(() => { @@ -76,4 +77,51 @@ describe('test base/DataExport/Detail/PageHeaderAction', () => { mockActionButtonStateData.executeExportButtonMeta.action ).toHaveBeenCalledTimes(1); }); + + describe('PermissionControl / usePermission', () => { + let usePermissionSpy: jest.SpyInstance | undefined; + + afterEach(() => { + usePermissionSpy?.mockRestore(); + usePermissionSpy = undefined; + }); + + it('should hide workflow action buttons when checkActionPermission returns false', () => { + usePermissionSpy = mockUsePermission( + { + checkActionPermission: jest.fn().mockReturnValue(false), + checkActionDisabledByBWP: jest.fn().mockReturnValue(false) + }, + { useSpyOnMockHooks: true } + ); + + baseSuperRender(); + + expect(screen.queryByText('关闭工单')).not.toBeInTheDocument(); + expect(screen.queryByText('审核通过')).not.toBeInTheDocument(); + expect(screen.queryByText('审核驳回')).not.toBeInTheDocument(); + expect(screen.queryByText('执行导出')).not.toBeInTheDocument(); + expect(screen.getByText('工单详情')).toBeInTheDocument(); + }); + + it('should render approve button as disabled when workflow meta has disabled true', () => { + usePermissionSpy = mockUsePermission( + { + checkActionPermission: jest.fn().mockReturnValue(true), + checkActionDisabledByBWP: jest.fn().mockReturnValue(false) + }, + { useSpyOnMockHooks: true } + ); + mockUseActionButtonState({ + approveWorkflowButtonMeta: { + ...mockActionButtonStateData.approveWorkflowButtonMeta, + disabled: true + } + }); + + baseSuperRender(); + + expect(screen.getByText('审核通过').closest('button')).toBeDisabled(); + }); + }); }); diff --git a/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/__tests__/useActionButtonState.test.tsx b/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/__tests__/useActionButtonState.test.tsx index 3745d2f62..59aae8c24 100644 --- a/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/__tests__/useActionButtonState.test.tsx +++ b/packages/base/src/page/DataExportManagement/Detail/components/PageHeaderAction/__tests__/useActionButtonState.test.tsx @@ -12,6 +12,8 @@ import { import { IGetDataExportWorkflow } from '@actiontech/shared/lib/api/base/service/common'; import { GetDataExportWorkflowResponseData } from '@actiontech/shared/lib/testUtil/mockApi/base/dataExport/data'; import { WorkflowRecordStatusEnum } from '@actiontech/shared/lib/api/base/service/common.enum'; +import { PERMISSIONS } from '@actiontech/shared/lib/features'; +import { mockUsePermission } from '@actiontech/shared/lib/testUtil/mockHook/mockUsePermission'; const notExistCurrentStepWorkflowInfo: IGetDataExportWorkflow = { ...GetDataExportWorkflowResponseData, @@ -64,7 +66,17 @@ describe('test useActionButtonState', () => { destroy: jest.fn() }; + let checkActionDisabledByBWPMock: jest.Mock; + let usePermissionSpy: jest.SpyInstance | undefined; + beforeEach(() => { + checkActionDisabledByBWPMock = jest.fn().mockReturnValue(false); + usePermissionSpy = mockUsePermission( + { + checkActionDisabledByBWP: checkActionDisabledByBWPMock + }, + { useSpyOnMockHooks: true } + ); mockUseCurrentUser({ userId: '700200' }); @@ -72,6 +84,8 @@ describe('test useActionButtonState', () => { mockUseDataExportDetailReduxManage(); }); afterEach(() => { + usePermissionSpy?.mockRestore(); + usePermissionSpy = undefined; jest.clearAllMocks(); jest.clearAllTimers(); }); @@ -247,4 +261,57 @@ describe('test useActionButtonState', () => { workflowID ); }); + + it('should set approveWorkflowButtonMeta.disabled when BWP blocks APPROVE permission', () => { + checkActionDisabledByBWPMock.mockImplementation( + (permission: string) => + permission === PERMISSIONS.ACTIONS.BASE.DATA_EXPORT.APPROVE + ); + mockUseDataExportDetailReduxManage({ + workflowInfo: waitForApproveStatusWorkflow + }); + const { result } = renderHook(() => useActionButtonState(messageApiSpy)); + expect(result.current.approveWorkflowButtonMeta.disabled).toBe(true); + expect(result.current.rejectWorkflowButtonMeta.disabled).toBe(false); + expect(result.current.executeExportButtonMeta.disabled).toBe(false); + }); + + it('should set rejectWorkflowButtonMeta.disabled when BWP blocks REJECT permission', () => { + checkActionDisabledByBWPMock.mockImplementation( + (permission: string) => + permission === PERMISSIONS.ACTIONS.BASE.DATA_EXPORT.REJECT + ); + mockUseDataExportDetailReduxManage({ + workflowInfo: waitForApproveStatusWorkflow + }); + const { result } = renderHook(() => useActionButtonState(messageApiSpy)); + expect(result.current.approveWorkflowButtonMeta.disabled).toBe(false); + expect(result.current.rejectWorkflowButtonMeta.disabled).toBe(true); + expect(result.current.executeExportButtonMeta.disabled).toBe(false); + }); + + it('should set executeExportButtonMeta.disabled when BWP blocks EXECUTE permission', () => { + checkActionDisabledByBWPMock.mockImplementation( + (permission: string) => + permission === PERMISSIONS.ACTIONS.BASE.DATA_EXPORT.EXECUTE + ); + mockUseDataExportDetailReduxManage({ + workflowInfo: waitForExportStatusWorkflow + }); + const { result } = renderHook(() => useActionButtonState(messageApiSpy)); + expect(result.current.approveWorkflowButtonMeta.disabled).toBe(false); + expect(result.current.rejectWorkflowButtonMeta.disabled).toBe(false); + expect(result.current.executeExportButtonMeta.disabled).toBe(true); + }); + + it('should set disabled on approve, reject and execute metas when checkActionDisabledByBWP always returns true', () => { + checkActionDisabledByBWPMock.mockReturnValue(true); + mockUseDataExportDetailReduxManage({ + workflowInfo: waitForApproveStatusWorkflow + }); + const { result } = renderHook(() => useActionButtonState(messageApiSpy)); + expect(result.current.approveWorkflowButtonMeta.disabled).toBe(true); + expect(result.current.rejectWorkflowButtonMeta.disabled).toBe(true); + expect(result.current.executeExportButtonMeta.disabled).toBe(true); + }); }); 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..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,10 +27,14 @@ export const CloseWorkflowAction = (closeWorkflowButtonMeta: ActionMeta) => { }; export const RejectWorkflowAction = (rejectWorkflowButtonMeta: ActionMeta) => { return ( - + @@ -42,10 +46,12 @@ export const ApproveWorkflowAction = ( return (