Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f8ab11f
feat(user): add businessWritePermission to Redux store, extend useCur…
LordofAvernus May 8, 2026
f6eb12e
test(user): update test files for businessWritePermission and project…
LordofAvernus May 8, 2026
53c8ce6
feat(user): add business write permission Switch to user edit/create …
LordofAvernus May 8, 2026
880a791
feat(permission): add BWP disable logic to business buttons and SQL w…
LordofAvernus May 8, 2026
13bddf3
test(permission): add frontend unit tests for BWP disable and enum re…
LordofAvernus May 8, 2026
e35e30b
fix(test): update tests and snapshots for BWP field and projectDirect…
LordofAvernus May 8, 2026
2571f4c
fix(user): BWP switch defaults to ON when selecting system administra…
LordofAvernus May 8, 2026
4b61543
fix(permission): fix BWP switch echo bug and business write button vi…
LordofAvernus May 9, 2026
ebcb89e
fix(permission): enforce BWP whitelist approach - deny all business o…
LordofAvernus May 9, 2026
1d70d2c
fix(permission): disable BWP-blocked buttons instead of hiding them; …
LordofAvernus May 9, 2026
b4fcb7e
fix(permission): pass checkActionDisabledByBWP to VersionManagement l…
LordofAvernus May 9, 2026
3ed293a
fix(bwp): project admin permission overrides global BWP=off; update s…
LordofAvernus May 9, 2026
dec8d5e
fix(useBusinessWritePermission): match project by both project_id and…
LordofAvernus May 9, 2026
505782e
fix: enforce BWP disable check on bypassed business write operations …
LordofAvernus May 11, 2026
cab01ef
fix(useBusinessWritePermission): check project-level op permissions, …
LordofAvernus May 11, 2026
ae21e3d
fix(dms-kit): add tsc declaration generation to build script
LordofAvernus May 11, 2026
fa8a727
fix(bwp): per-action permission check when BWP=off with partial proje…
LordofAvernus May 11, 2026
abf17b5
fix(bwp): bypass BWP disable for workflow assignees in candidate set …
LordofAvernus May 11, 2026
1ef8aa8
fix(bwp): add skipBWPCheck to PermissionControl for workflow assignee…
LordofAvernus May 11, 2026
6e7c633
fix: code review
LZS911 May 12, 2026
9ae3a18
fix: code review
LZS911 May 12, 2026
6d8ce9e
fix: sql management permission
LZS911 May 13, 2026
2b96345
fix(permission): ai code review
LZS911 May 13, 2026
386a318
test: fix unit tests broken by permission manifest changes
LZS911 May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/base/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<App />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: '审计管理员'
Expand All @@ -36,7 +32,6 @@ describe('PermissionTagList', () => {

superRender(<SystemRoleTagList roles={roles} />);

expect(screen.getByText('项目总监')).toBeInTheDocument();
expect(screen.getByText('审计管理员')).toBeInTheDocument();
expect(screen.getByText('系统管理员')).toBeInTheDocument();
expect(screen.getByText('创建工单')).toBeInTheDocument();
Expand Down
2 changes: 0 additions & 2 deletions packages/base/src/components/SystemRoleTagList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ const SystemRoleTagList: React.FC<SystemRoleTagListProps> = ({
}
const getPermissionTagColor = (uid: string): BasicTagProps['color'] => {
switch (uid) {
case OpPermissionTypeUid.project_director:
return 'orange';
case OpPermissionTypeUid.audit_administrator:
return 'blue';
case OpPermissionTypeUid.system_administrator:
Expand Down
5 changes: 4 additions & 1 deletion packages/base/src/locale/en-US/dmsUserCenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
5 changes: 4 additions & 1 deletion packages/base/src/locale/zh-CN/dmsUserCenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ export default {
userGroups: '所属用户组',
opPermissions: '平台角色',
isDisabled: '是否禁用',
disabledTips: '当用户被禁用,该用户将无法登录'
disabledTips: '当用户被禁用,该用户将无法登录',
businessWritePermission: '业务写权',
businessWritePermissionDesc:
'关闭后该账号仅保留资源配置与业务只读能力,不再参与业务写入与通知。'
},
createUser: {
createSuccessTips: '添加用户 "{{name}}" 成功'
Expand Down
3 changes: 1 addition & 2 deletions packages/base/src/page/CloudBeaver/List/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down
10 changes: 10 additions & 0 deletions packages/base/src/page/CloudBeaver/index.ce.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
140 changes: 140 additions & 0 deletions packages/base/src/page/CloudBeaver/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(<CloudBeaver />);

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(<CloudBeaver />, 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(<CloudBeaver />);

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(<CloudBeaver />);

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(<CloudBeaver />, 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;
});
});
});
30 changes: 26 additions & 4 deletions packages/base/src/page/CloudBeaver/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -47,7 +56,8 @@ const CloudBeaver = () => {
extractQueries(ROUTE_PATHS.BASE.CLOUD_BEAVER.index)?.open_cloud_beaver ===
String(true) &&
!loading &&
data
data &&
!isBusinessWriteDisabled
) {
let url = '';

Expand All @@ -61,9 +71,17 @@ const CloudBeaver = () => {
window.location.href = url;
}
}
}, [extractQueries, loading, data]);
}, [extractQueries, loading, data, isBusinessWriteDisabled]);

const renderActionButton = useMemo(() => {
if (isBusinessWriteDisabled) {
return (
<BasicButton disabled>
{t('dmsCloudBeaver.jumpToCloudBeaver')}
</BasicButton>
);
}

if (loading) {
return (
<BasicButton loading type="primary">
Expand Down Expand Up @@ -126,7 +144,7 @@ const CloudBeaver = () => {
</BasicButton>
</Dropdown>
);
}, [data, loading, t, handleMenuClick]);
}, [data, loading, t, handleMenuClick, isBusinessWriteDisabled]);

// Determine if the main content should be displayed
const isFeatureEnabled = useMemo(() => {
Expand All @@ -139,7 +157,11 @@ const CloudBeaver = () => {
<>
<PageHeader
title={t('dmsCloudBeaver.pageTitle')}
extra={<EmptyBox if={!loading}>{renderActionButton}</EmptyBox>}
extra={
<EmptyBox if={!loading && allowOpenCloudBeaver}>
{renderActionButton}
</EmptyBox>
}
/>
<Spin spinning={getOperationLogsLoading || loading}>
<EmptyBox if={!isFeatureEnabled && !loading}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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(<ExportDetailPageHeaderAction />);

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(<ExportDetailPageHeaderAction />);

expect(screen.getByText('审核通过').closest('button')).toBeDisabled();
});
});
});
Loading
Loading