diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b1559d986b..397ad2eb92 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,7 @@ jobs: - name: Code lint checker run: pnpm checker - test-ee: + test: runs-on: ubuntu-latest if: ${{ !contains(github.event.pull_request.title, '[skip checker]') }} strategy: @@ -34,65 +34,41 @@ jobs: - name: Install dependencies uses: ./.github/actions/catch-install-pnpm - - name: Coverage test report ee - run: sh ./scripts/jest/run-ci-ee.sh ${{ matrix.shard }} ${{ strategy.job-total }} + - name: Coverage test report + run: sh ./scripts/jest/run-ci.sh ${{ matrix.shard }} ${{ strategy.job-total }} - uses: actions/upload-artifact@v4 with: name: coverage-artifacts-${{ matrix.shard }} path: coverage/ - test-ce: - runs-on: ubuntu-latest - if: ${{ !contains(github.event.pull_request.title, '[skip checker]') }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install dependencies - uses: ./.github/actions/catch-install-pnpm - - - name: Coverage test report ce - run: sh ./scripts/jest/run-ci-ce.sh - - - uses: actions/upload-artifact@v4 - with: - name: ce-coverage-artifacts - path: ce_coverage/ - report: runs-on: ubuntu-latest if: ${{ !contains(github.event.pull_request.title, '[skip checker]') }} - needs: [test-ee, test-ce] + needs: [test] steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Get CE Coverage - uses: actions/download-artifact@v4 - with: - name: ce-coverage-artifacts - path: ce_coverage - - - name: Get EE Coverage 1 + - name: Get Coverage 1 uses: actions/download-artifact@v4 with: name: coverage-artifacts-1 path: coverage - - name: Get EE Coverage 2 + - name: Get Coverage 2 uses: actions/download-artifact@v4 with: name: coverage-artifacts-2 path: coverage - - name: Get EE Coverage 3 + - name: Get Coverage 3 uses: actions/download-artifact@v4 with: name: coverage-artifacts-3 path: coverage - - name: Get EE Coverage 4 + - name: Get Coverage 4 uses: actions/download-artifact@v4 with: name: coverage-artifacts-4 @@ -115,7 +91,6 @@ jobs: uses: geekyeggo/delete-artifact@v5 with: name: | - ce-coverage-artifacts coverage-artifacts-1 coverage-artifacts-2 coverage-artifacts-3 diff --git a/jest.config.js b/jest.config.js index 631a7d8b79..6515087c59 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,7 +7,37 @@ const { pathsToModuleNameMapper } = require('ts-jest'); compilerOptions.paths['~/*'][0] = path.resolve(compilerOptions.paths['~/*'][0]); -module.exports = { +const sharedModuleNameMapper = { + '.+\\.(css|style|less|sass|scss|ttf|woff|woff2)$': 'identity-obj-proxy', + '@ant-design/plots': + '/packages/shared/lib/testUtil/mockModule/mockAntDesignPlots.jsx', + 'monaco-editor': + '/packages/shared/lib/testUtil/mockModule/mockEditor.jsx', + '@monaco-editor/react': + '/packages/shared/lib/testUtil/mockModule/mockEditor.jsx', + '@uiw/react-md-editor': + '/packages/shared/lib/testUtil/mockModule/mockEditor.jsx', + '@actiontech/(.*)': '/packages/$1', + '@react-sigma/core(.*)$': + '/packages/shared/lib/testUtil/mockModule/mockSigmaCore.tsx', + '@react-sigma/graph-search$': + '/packages/shared/lib/testUtil/mockModule/mockSigmaGraphSearch.tsx', + ...pathsToModuleNameMapper(compilerOptions.paths) +}; + +const sharedIgnorePatterns = ['/node_modules/', '/demo/', '/demos/']; + +// Naming conventions for condition-specific test files: +// *.ce.test.{ts,tsx} → CE project (ee=false, ce=true, sqle=true, dms=false) 不要强制匹配 ce.test, ce.[可选项].test.{ts,tsx} +// *.sqle.test.{ts,tsx} → EE project (ee=true, ce=false, sqle=true, dms=false) 同上 +// *.provision.test.{ts,tsx} → PROVISION project (ee=true, ce=false, sqle=false, provision=true, dms=false) 同上 +// *.test.{ts,tsx} → DMS project (ee=true, ce=false, sqle=true, provision=true, dms=true) [default] 同上 +// 实现:`.ce.` / `.sqle.` / `.provision.` 与 `.test.` 之间可有零段或多段 `.xxx.`(正则见下方 *_TEST_FILE_RE)。 +const CE_TEST_FILE_RE = '\\.ce(\\.[^./]+)*\\.test\\.[jt]sx?$'; +const SQLE_TEST_FILE_RE = '\\.sqle(\\.[^./]+)*\\.test\\.[jt]sx?$'; +const PROVISION_TEST_FILE_RE = '\\.provision(\\.[^./]+)*\\.test\\.[jt]sx?$'; + +const sharedProjectConfig = { transform: { '^.+\\.(ts|tsx|js|jsx)$': '/scripts/jest/custom-transform.js', '^.+\\.(png|jpg|jpeg|css|json)$': '/scripts/jest/file-transform.js' @@ -19,24 +49,7 @@ module.exports = { moduleFileExtensions: ['ts', 'tsx', 'js', 'json', 'jsx', 'node'], testEnvironment: 'jest-environment-jsdom', resetMocks: true, - moduleNameMapper: { - '.+\\.(css|style|less|sass|scss|ttf|woff|woff2)$': 'identity-obj-proxy', - '@ant-design/plots': - '/packages/shared/lib/testUtil/mockModule/mockAntDesignPlots.jsx', - 'monaco-editor': - '/packages/shared/lib/testUtil/mockModule/mockEditor.jsx', - '@monaco-editor/react': - '/packages/shared/lib/testUtil/mockModule/mockEditor.jsx', - '@uiw/react-md-editor': - '/packages/shared/lib/testUtil/mockModule/mockEditor.jsx', - '@actiontech/(.*)': '/packages/$1', - '@react-sigma/core(.*)$': - '/packages/shared/lib/testUtil/mockModule/mockSigmaCore.tsx', - '@react-sigma/graph-search$': - '/packages/shared/lib/testUtil/mockModule/mockSigmaGraphSearch.tsx', - ...pathsToModuleNameMapper(compilerOptions.paths) - }, - + moduleNameMapper: sharedModuleNameMapper, collectCoverageFrom: [ 'packages/**/{src,lib}/{page,components,hooks,global,store,utils}/**/*.{ts,tsx}', 'packages/**/src/App.tsx', @@ -49,8 +62,78 @@ module.exports = { '!packages/**/demo/**', '!packages/**/demos/**' ], - setupFilesAfterEnv: ['/jest-setup.ts'], - testPathIgnorePatterns: ['/node_modules/', '/demo/', '/demos/'], + setupFilesAfterEnv: ['/jest-setup.ts'] +}; + +module.exports = { + projects: [ + { + ...sharedProjectConfig, + displayName: 'dms', + globals: { + TEST_CONDITIONS: { + ee: true, + ce: false, + sqle: true, + provision: true, + dms: true + } + }, + // Default tests only: exclude CE / sqle / provision condition tests (dedicated projects) + testPathIgnorePatterns: [ + ...sharedIgnorePatterns, + CE_TEST_FILE_RE, + SQLE_TEST_FILE_RE, + PROVISION_TEST_FILE_RE + ] + }, + { + ...sharedProjectConfig, + displayName: 'sqle-ce', + globals: { + TEST_CONDITIONS: { + ee: false, + ce: true, + sqle: true, + provision: false, + dms: false + } + }, + testRegex: CE_TEST_FILE_RE, + testPathIgnorePatterns: sharedIgnorePatterns + }, + { + ...sharedProjectConfig, + displayName: 'sqle-ee', + globals: { + TEST_CONDITIONS: { + ee: true, + ce: false, + sqle: true, + provision: false, + dms: false + } + }, + testRegex: SQLE_TEST_FILE_RE, + // e.g. *.ce.sqle.test.* belongs to CE, not EE + testPathIgnorePatterns: [...sharedIgnorePatterns, CE_TEST_FILE_RE] + }, + { + ...sharedProjectConfig, + displayName: 'provision', + globals: { + TEST_CONDITIONS: { + ee: true, + ce: false, + sqle: false, + provision: true, + dms: false + } + }, + testRegex: PROVISION_TEST_FILE_RE, + testPathIgnorePatterns: [...sharedIgnorePatterns, CE_TEST_FILE_RE] + } + ], reporters: [ 'default', [ diff --git a/package.json b/package.json index 793e9cf061..567c18aebd 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "checker": "concurrently \"pnpm ts-check\" \"pnpm eslint\" \"pnpm stylelint\" \"pnpm prettier:c\"", "test": "sh ./scripts/jest/run.sh", "test:c": "sh ./scripts/jest/run-coverage.sh", - "test:ci": "sh ./scripts/jest/run-ci-ee.sh && sh ./scripts/jest/run-ci-ce.sh && node ./scripts/jest/merge-report-json.js", + "test:ci": "sh ./scripts/jest/run-ci.sh 1 1 && node ./scripts/jest/merge-report-json.js", "test:clean": "jest --clearCache", "icon:g": "pnpm --filter @actiontech/icons icon:g", "icon:docs:g": "pnpm --filter @actiontech/icons docs:g", @@ -149,4 +149,4 @@ "@babel/core": "^7.22.0", "@ant-design/cssinjs": "1.17.0" } -} +} \ No newline at end of file diff --git a/packages/base/src/__snapshots__/App.test.tsx.snap b/packages/base/src/__snapshots__/App.test.tsx.snap index ef75cb9e75..951794302a 100644 --- a/packages/base/src/__snapshots__/App.test.tsx.snap +++ b/packages/base/src/__snapshots__/App.test.tsx.snap @@ -3,7 +3,7 @@ exports[`App render App when "checkPageAction" is false 1`] = `
`; exports[`App render App when token is existed 1`] = `
{ + let listOpPermissionSpy: jest.SpyInstance; + + beforeEach(() => { + listOpPermissionSpy = userCenter.getOpPermissionsList(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + cleanup(); + }); + + it('should call ListOpPermissions with service=sqle', async () => { + const { result } = renderHook(() => useOpPermission()); + + act(() => { + result.current.updateOpPermissionList(); + }); + + await act(async () => jest.advanceTimersByTime(3000)); + + expect(listOpPermissionSpy).toHaveBeenCalledTimes(1); + expect(listOpPermissionSpy).toHaveBeenCalledWith( + expect.objectContaining({ + service: ListOpPermissionsServiceEnum.sqle + }) + ); + }); + + it('should call ListOpPermissions with service=sqle when filterBy is provided', async () => { + const { result } = renderHook(() => useOpPermission()); + + act(() => { + result.current.updateOpPermissionList(); + }); + + await act(async () => jest.advanceTimersByTime(3000)); + + expect(listOpPermissionSpy).toHaveBeenCalledWith( + expect.objectContaining({ + service: ListOpPermissionsServiceEnum.sqle + }) + ); + expect(result.current.loading).toBeFalsy(); + expect(result.current.opPermissionList.length).toBeGreaterThan(0); + }); +}); diff --git a/packages/base/src/locale/en-US/dmsDataSource.ts b/packages/base/src/locale/en-US/dmsDataSource.ts index d16beb3752..75ccb3c1bc 100644 --- a/packages/base/src/locale/en-US/dmsDataSource.ts +++ b/packages/base/src/locale/en-US/dmsDataSource.ts @@ -55,6 +55,9 @@ export default { maintenanceTime: 'Maintenance time', maintenanceTimeTips: 'After setting the maintenance time, workflows can only be executed during this maintenance time period', + sqlWorkbenchMaintenanceTime: 'SQL workbench maintenance time', + sqlWorkbenchMaintenanceTimeTips: + 'Non-DQL statements in the SQL workbench can only run during these windows (independent from DB instance maintenance time)', needAuditSqlService: 'Enable SQL audit service', closeAuditSqlServiceTips: 'If you do not enable the SQL audit service, the DB instance cannot be used in SQL audit related services, are you sure to close it?', @@ -90,6 +93,8 @@ export default { text: 'Enable scan task for DB instance' }, + enableMaskingTask: 'Enable data masking task', + batchImportDataSource: { buttonText: 'Batch import DB instances', title: 'Batch import DB instances', diff --git a/packages/base/src/locale/en-US/dmsHome.ts b/packages/base/src/locale/en-US/dmsHome.ts index 139501b9e3..11cae0b80e 100644 --- a/packages/base/src/locale/en-US/dmsHome.ts +++ b/packages/base/src/locale/en-US/dmsHome.ts @@ -11,8 +11,13 @@ export default { times: 'times', aiPerformanceEngine: 'AI Performance Engine', aiSmartCorrection: 'AI Smart Correction', + aiPerformanceEngineNoPermissionPrompt: + 'No SQL optimization permission. Please contact your administrator', paidFeaturePrompt: - 'This feature is a paid value-added module. Please contact sales for details' + 'This feature is a paid value-added module. Please contact sales for details', + aiBannerExampleDrawerTitle: 'SQL Rewritten Example', + aiBannerExampleDrawerDescription: + 'This example shows the SQL comparison after AI smart correction. When using it in practice, please first perform SQL audit, and the system will automatically provide the correction entry for the SQL that triggers the rules.' }, defaultScene: { header: { diff --git a/packages/base/src/locale/en-US/dmsSystem.ts b/packages/base/src/locale/en-US/dmsSystem.ts index 06065e2cf4..6663261232 100644 --- a/packages/base/src/locale/en-US/dmsSystem.ts +++ b/packages/base/src/locale/en-US/dmsSystem.ts @@ -240,6 +240,15 @@ export default { hasDirtyDataTips: 'The current content has changed. are you sure you want to cancel the modification?', successMessage: 'Successfully published system announcement!', - notData: 'No announcement information' + notData: 'No announcement information', + validPeriod: 'Valid period', + startTimePlaceholder: 'Select start time', + endTimePlaceholder: 'Select end time', + timeRangeRequired: + 'Please select the effective time range for the announcement', + timeRangeValidate: 'Start time must be earlier than end time', + noticeContent: 'Announcement content', + effectivePeriod: 'Effective period', + viewFullContent: 'View full content' } }; diff --git a/packages/base/src/locale/zh-CN/dmsDataSource.ts b/packages/base/src/locale/zh-CN/dmsDataSource.ts index fd781dfead..1d01c5bc91 100644 --- a/packages/base/src/locale/zh-CN/dmsDataSource.ts +++ b/packages/base/src/locale/zh-CN/dmsDataSource.ts @@ -71,6 +71,9 @@ export default { queryTimeoutSecond: 'SQL超时限制(s)', maintenanceTime: '运维时间', maintenanceTimeTips: '设置运维时间后,仅能在此运维时间段内上线工单', + sqlWorkbenchMaintenanceTime: 'SQL 工作台运维时间', + sqlWorkbenchMaintenanceTimeTips: + '设置后,非 DQL 语句仅能在该运维时间段内于 SQL 工作台执行(与数据源运维时间独立)', needAuditSqlService: '是否开启SQL审核业务', needAuditSqlServiceTips: '关闭后将禁用所用场景的SQL审核', closeAuditSqlServiceTips: @@ -126,6 +129,8 @@ export default { text: '为数据源开启扫描任务' }, + enableMaskingTask: '为数据源开启脱敏任务', + batchImportDataSource: { buttonText: '批量导入数据源', title: '批量导入数据源', diff --git a/packages/base/src/locale/zh-CN/dmsHome.ts b/packages/base/src/locale/zh-CN/dmsHome.ts index 6b08a85967..ca08777425 100644 --- a/packages/base/src/locale/zh-CN/dmsHome.ts +++ b/packages/base/src/locale/zh-CN/dmsHome.ts @@ -10,7 +10,11 @@ export default { times: '次', aiPerformanceEngine: 'AI 性能引擎', aiSmartCorrection: 'AI 智能修正', - paidFeaturePrompt: '当前功能为付费增值模块,请联系商务获取详细信息' + aiPerformanceEngineNoPermissionPrompt: '暂无 SQL 调优权限,请联系管理员', + paidFeaturePrompt: '当前功能为付费增值模块,请联系商务获取详细信息', + aiBannerExampleDrawerTitle: 'SQL 重写示例', + aiBannerExampleDrawerDescription: + '本示例展示 AI 智能修正后的 SQL 对比。实际使用时,请先进行 SQL 审核,系统将针对触发规则的 SQL 自动提供修正入口。' }, defaultScene: { header: { diff --git a/packages/base/src/locale/zh-CN/dmsSystem.ts b/packages/base/src/locale/zh-CN/dmsSystem.ts index 2c22f0b30e..0f1165efeb 100644 --- a/packages/base/src/locale/zh-CN/dmsSystem.ts +++ b/packages/base/src/locale/zh-CN/dmsSystem.ts @@ -291,6 +291,14 @@ export default { title: '系统公告', hasDirtyDataTips: '当前内容已经发生更改,是否确认取消修改?', successMessage: '成功发布系统公告!', - notData: '暂无公告信息' + notData: '暂无公告信息', + validPeriod: '有效时间范围', + startTimePlaceholder: '请选择开始时间', + endTimePlaceholder: '请选择结束时间', + timeRangeRequired: '请选择公告生效时间范围', + timeRangeValidate: '开始时间必须早于结束时间', + noticeContent: '公告内容', + effectivePeriod: '有效期', + viewFullContent: '查看全部' } }; diff --git a/packages/base/src/page/Account/__snapshots__/index.ce.test.tsx.snap b/packages/base/src/page/Account/__snapshots__/index.ce.test.tsx.snap index a795016490..0c95fab7c1 100644 --- a/packages/base/src/page/Account/__snapshots__/index.ce.test.tsx.snap +++ b/packages/base/src/page/Account/__snapshots__/index.ce.test.tsx.snap @@ -5,7 +5,7 @@ exports[`test base/page/Account ce should match snapshot 1`] = `
{ }); it('render oauth2_token params submit without target param', async () => { - const LocalStorageWrapperSet = jest.spyOn(LocalStorageWrapper, 'set'); const search = `oauth2_token=oauth2_token_val&refresh_token=id_token_val`; const requestFn = dms.bindUser(); const { baseElement } = customRender(`/user/bind?${search}`); @@ -184,15 +178,9 @@ describe('page/BindUser-ee', () => { expect(getSessionUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).toHaveBeenCalled(); expect(navigateSpy).toHaveBeenCalledWith('/'); - expect(LocalStorageWrapperSet).toHaveBeenCalled(); - expect(LocalStorageWrapperSet).toHaveBeenCalledWith( - StorageKey.SHOW_COMPANY_NOTICE, - CompanyNoticeDisplayStatusEnum.NotDisplayed - ); }); it('render oauth2_token params submit with target param', async () => { - const LocalStorageWrapperSet = jest.spyOn(LocalStorageWrapper, 'set'); const search = `oauth2_token=oauth2_token_val&refresh_token=id_token_val&target=${encodeURIComponent( '/project/test' )}`; @@ -238,11 +226,9 @@ describe('page/BindUser-ee', () => { expect(getSessionUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).toHaveBeenCalled(); expect(navigateSpy).toHaveBeenCalledWith('/project/test'); - expect(LocalStorageWrapperSet).toHaveBeenCalled(); }); it('render oauth2_token params submit with target param containing query params', async () => { - const LocalStorageWrapperSet = jest.spyOn(LocalStorageWrapper, 'set'); const search = `oauth2_token=oauth2_token_val&refresh_token=id_token_val&target=${encodeURIComponent( '/project/test?active=overview' )}`; @@ -288,11 +274,9 @@ describe('page/BindUser-ee', () => { expect(getSessionUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).toHaveBeenCalled(); expect(navigateSpy).toHaveBeenCalledWith('/project/test?active=overview'); - expect(LocalStorageWrapperSet).toHaveBeenCalled(); }); it('render oauth2_token params submit with cloud-beaver target param', async () => { - const LocalStorageWrapperSet = jest.spyOn(LocalStorageWrapper, 'set'); const search = `oauth2_token=oauth2_token_val&refresh_token=id_token_val&target=${encodeURIComponent( '/cloud-beaver' )}`; @@ -340,7 +324,6 @@ describe('page/BindUser-ee', () => { expect(navigateSpy).toHaveBeenCalledWith( '/cloud-beaver?open_cloud_beaver=true' ); - expect(LocalStorageWrapperSet).toHaveBeenCalled(); }); }); diff --git a/packages/base/src/page/BindUser/index.tsx b/packages/base/src/page/BindUser/index.tsx index f1efb2e106..c689e9a610 100644 --- a/packages/base/src/page/BindUser/index.tsx +++ b/packages/base/src/page/BindUser/index.tsx @@ -25,11 +25,6 @@ import { OPEN_CLOUD_BEAVER_URL_PARAM_NAME, ROUTE_PATHS } from '@actiontech/dms-kit'; -import { LocalStorageWrapper } from '@actiontech/dms-kit'; -import { - StorageKey, - CompanyNoticeDisplayStatusEnum -} from '@actiontech/dms-kit'; import useSessionUser from '../../hooks/useSessionUser'; import useNavigateToWorkbench from '../../hooks/useNavigateToWorkbench'; const BindUser = () => { @@ -135,12 +130,6 @@ const BindUser = () => { }) ); navigateToTarget(); - // #if [ee] - LocalStorageWrapper.set( - StorageKey.SHOW_COMPANY_NOTICE, - CompanyNoticeDisplayStatusEnum.NotDisplayed - ); - // #endif } }) .finally(() => { diff --git a/packages/base/src/page/CloudBeaver/__snapshots__/index.ce.test.tsx.snap b/packages/base/src/page/CloudBeaver/__snapshots__/index.ce.test.tsx.snap index 5229e42461..0b7af9e726 100644 --- a/packages/base/src/page/CloudBeaver/__snapshots__/index.ce.test.tsx.snap +++ b/packages/base/src/page/CloudBeaver/__snapshots__/index.ce.test.tsx.snap @@ -3,7 +3,7 @@ exports[`test base/page/CloudBeaver should match snapshot when sql query is disabled 1`] = `
void; + onSuccess: () => void; +}; + +const CreateTaskModal: React.FC = () => { + return null; +}; + +export default CreateTaskModal; diff --git a/packages/base/src/page/DataSource/components/AddDataSource/__snapshots__/index.test.tsx.snap b/packages/base/src/page/DataSource/components/AddDataSource/__snapshots__/index.test.tsx.snap index 7911274164..f03f62a2ad 100644 --- a/packages/base/src/page/DataSource/components/AddDataSource/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/DataSource/components/AddDataSource/__snapshots__/index.test.tsx.snap @@ -7,7 +7,7 @@ exports[`page/DataSource/AddDataSource render add database snap 1`] = ` class="css-doq0dk" >
-
-
-

- 数据脱敏配置 -

-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
@@ -1270,7 +1175,7 @@ exports[`page/DataSource/AddDataSource render conenctable modal when current ser class="css-doq0dk" >
-
-
-

- 数据脱敏配置 -

-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
@@ -2872,7 +2682,7 @@ exports[`page/DataSource/AddDataSource render submit when add database api succe class="css-doq0dk" >
{ afterEach(() => { jest.useRealTimers(); - jest.clearAllMocks(); + jest.restoreAllMocks(); cleanup(); }); @@ -203,7 +203,6 @@ describe('page/DataSource/AddDataSource', () => { db_type: 'mysql', desc: undefined, host: '1.1.1.1', - is_enable_masking: false, maintenance_times: [], name: 'name-database', password: 'root', @@ -219,7 +218,8 @@ describe('page/DataSource/AddDataSource', () => { audit_enabled: true, rule_template_id: '3', rule_template_name: 'default_MySQL1', - workflow_exec_enabled: true + workflow_exec_enabled: true, + maintenance_times: [] } }, user: 'root' @@ -429,7 +429,6 @@ describe('page/DataSource/AddDataSource', () => { db_type: 'mysql', desc: undefined, host: '1.1.1.1', - is_enable_masking: false, maintenance_times: [], name: 'name-database', password: 'root', @@ -443,7 +442,8 @@ describe('page/DataSource/AddDataSource', () => { sql_query_config: { allow_query_when_less_than_audit_level: undefined, audit_enabled: undefined, - workflow_exec_enabled: undefined + workflow_exec_enabled: undefined, + maintenance_times: [] } }, user: 'root', @@ -463,6 +463,114 @@ describe('page/DataSource/AddDataSource', () => { expect(navigateSpy).toHaveBeenCalledTimes(1); }); + it('should submit sql workbench maintenance times', async () => { + const { baseElement } = customRender(); + await act(async () => jest.advanceTimersByTime(9300)); + fireEvent.change(getBySelector('#name', baseElement), { + target: { + value: 'name-database-field-case' + } + }); + await act(async () => jest.advanceTimersByTime(300)); + fireEvent.mouseDown(getBySelector('#type', baseElement)); + await act(async () => jest.advanceTimersByTime(300)); + fireEvent.click(getBySelector('span[title="mysql"]', baseElement)); + await act(async () => jest.advanceTimersByTime(3000)); + fireEvent.change(getBySelector('#ip', baseElement), { + target: { + value: '1.1.1.3' + } + }); + await act(async () => jest.advanceTimersByTime(300)); + fireEvent.change(getBySelector('#user', baseElement), { + target: { + value: 'root' + } + }); + await act(async () => jest.advanceTimersByTime(300)); + fireEvent.change(getBySelector('#password', baseElement), { + target: { + value: 'root' + } + }); + await act(async () => jest.advanceTimersByTime(300)); + fireEvent.click(getBySelector('.editable-select-trigger', baseElement)); + await act(async () => jest.advanceTimersByTime(0)); + fireEvent.click(getAllBySelector('.ant-dropdown-menu-item')[0]); + await act(async () => jest.advanceTimersByTime(0)); + + fireEvent.mouseDown(getBySelector('#ruleTemplateName', baseElement)); + await act(async () => jest.advanceTimersByTime(300)); + fireEvent.click( + getBySelector('div[title="custom_template_b"]', baseElement) + ); + await act(async () => jest.advanceTimersByTime(300)); + fireEvent.mouseDown( + getBySelector('#dataExportRuleTemplateName', baseElement) + ); + await act(async () => jest.advanceTimersByTime(300)); + fireEvent.click(getAllBySelector('div[title="default_MySQL1"]')[1]); + await act(async () => jest.advanceTimersByTime(300)); + + fireEvent.click(getBySelector('#needAuditForSqlQuery', baseElement)); + await act(async () => jest.advanceTimersByTime(0)); + fireEvent.mouseDown(getBySelector('#workbenchTemplateName', baseElement)); + await act(async () => jest.advanceTimersByTime(0)); + fireEvent.click(getAllBySelector('div[title="default_MySQL1"]')[2]); + await act(async () => jest.advanceTimersByTime(0)); + fireEvent.mouseDown( + getBySelector('#allowQueryWhenLessThanAuditLevel', baseElement) + ); + await act(async () => jest.advanceTimersByTime(0)); + fireEvent.click(getBySelector('div[title="warn"]', baseElement)); + await act(async () => jest.advanceTimersByTime(0)); + + const workbenchMaintenanceField = screen + .getByText('SQL 工作台运维时间') + .closest('.ant-form-item') as HTMLElement; + fireEvent.click(getBySelector('button', workbenchMaintenanceField)); + await act(async () => jest.advanceTimersByTime(300)); + const inputEle = getAllBySelector('.ant-picker-input', baseElement); + expect(inputEle.length).toBe(2); + fireEvent.click(inputEle[0].parentElement!); + await act(async () => jest.advanceTimersByTime(300)); + fireEvent.click(screen.getAllByText('01')[0]); + await act(async () => jest.advanceTimersByTime(100)); + fireEvent.click(screen.getAllByText('05')[1]); + await act(async () => jest.advanceTimersByTime(100)); + fireEvent.click(getBySelector('.ant-picker-ok .ant-btn')); + await act(async () => jest.advanceTimersByTime(300)); + fireEvent.click(screen.getAllByText('03')[0]); + await act(async () => jest.advanceTimersByTime(100)); + fireEvent.click(screen.getAllByText('15')[1]); + await act(async () => jest.advanceTimersByTime(100)); + fireEvent.click(getBySelector('.ant-picker-ok .ant-btn')); + await act(async () => jest.advanceTimersByTime(300)); + fireEvent.click(screen.getByText('确 认')); + await act(async () => jest.advanceTimersByTime(300)); + + await act(async () => { + fireEvent.click(screen.getByText('提 交')); + }); + await act(async () => jest.advanceTimersByTime(3000)); + expect(requestAddDBServiceSpy).toHaveBeenCalled(); + expect( + requestAddDBServiceSpy.mock.calls[0][0].db_service.sqle_config + .sql_query_config.maintenance_times + ).toEqual([ + { + maintenance_start_time: { + hour: 1, + minute: 5 + }, + maintenance_stop_time: { + hour: 3, + minute: 15 + } + } + ]); + }); + it('render prepare api req', async () => { const requestRuleTemplateList = sqleMockApi.rule_template.getRuleTemplateTips(); @@ -505,30 +613,4 @@ describe('page/DataSource/AddDataSource', () => { EmitterKey.DMS_Reset_DataSource_Form ); }); - - it('check enable masking have href', async () => { - customRender(); - expect(screen.getByText('查看脱敏规则')).toBeInTheDocument(); - expect(screen.getByText('查看脱敏规则')).toHaveAttribute( - 'href', - `/project/${projectID}/data-mask-rule-overview` - ); - }); - - it('check enable masking have href when project is undefined', async () => { - mockUseCurrentProject({ projectID: undefined }); - const { baseElement } = customRender(); - await act(async () => jest.advanceTimersByTime(3000)); - expect(screen.queryByText('查看脱敏规则')).not.toBeInTheDocument(); - const projectEle = getBySelector('#project'); - expect(projectEle).not.toBeDisabled(); - fireEvent.mouseDown(projectEle, baseElement); - await act(async () => jest.advanceTimersByTime(300)); - fireEvent.click(screen.getByText('test_project_1')); - await act(async () => jest.advanceTimersByTime(3000)); - expect(screen.getByText('查看脱敏规则')).toHaveAttribute( - 'href', - `/project/${mockProjectList[0].uid}/data-mask-rule-overview` - ); - }); }); diff --git a/packages/base/src/page/DataSource/components/AddDataSource/index.tsx b/packages/base/src/page/DataSource/components/AddDataSource/index.tsx index 150553f9d7..c435a06f6f 100644 --- a/packages/base/src/page/DataSource/components/AddDataSource/index.tsx +++ b/packages/base/src/page/DataSource/components/AddDataSource/index.tsx @@ -57,14 +57,16 @@ const AddDataSource = () => { audit_enabled: values.needAuditForSqlQuery, rule_template_id: values.workbenchTemplateId, rule_template_name: values.workbenchTemplateName, - workflow_exec_enabled: values.allowExecuteNonDqlInWorkflow + workflow_exec_enabled: values.allowExecuteNonDqlInWorkflow, + maintenance_times: + values.sqlWorkbenchMaintenanceTime?.map((time) => ({ + maintenance_start_time: time.startTime, + maintenance_stop_time: time.endTime + })) ?? [] } }, // #endif - additional_params: values.asyncParams, - // #if [dms] - is_enable_masking: values.is_enable_masking - // #endif + additional_params: values.asyncParams }; // #if [sqle && ee] if (dbService.sqle_config) { diff --git a/packages/base/src/page/DataSource/components/BatchImportDataSource/__snapshots__/index.test.tsx.snap b/packages/base/src/page/DataSource/components/BatchImportDataSource/__snapshots__/index.test.tsx.snap index 10916b90b1..c5ee8a600c 100644 --- a/packages/base/src/page/DataSource/components/BatchImportDataSource/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/DataSource/components/BatchImportDataSource/__snapshots__/index.test.tsx.snap @@ -4,7 +4,7 @@ exports[`base/DataSource/BatchImportDataSource render connectable error modal 1`
= (props) => { const { value = [], onChange } = props; const [messageApi, contextHolder] = message.useMessage(); @@ -76,17 +79,19 @@ const MaintenanceTimePicker: React.FC = (props) => { } > - {value.map((v, index) => ( - deleteTime(index)} - key={`${generateKey(v)}-${index}`} - size="large" - > - {addZero(v.startTime.hour)}:{addZero(v.startTime.minute)} - - {addZero(v.endTime.hour)}:{addZero(v.endTime.minute)} - - ))} + + {value.map((v, index) => ( + deleteTime(index)} + key={`${generateKey(v)}-${index}`} + size="small" + > + {addZero(v.startTime.hour)}:{addZero(v.startTime.minute)} -{' '} + {addZero(v.endTime.hour)}:{addZero(v.endTime.minute)} + + ))} + - - 01 - : - 05 - - - 05 - : - 05 - + + - +
+
+
+
+ +
+
+
+
+
+
+
+ + 请选择时间段 + +
+
+ +
+
+
+
+
+
+
diff --git a/packages/base/src/page/DataSource/components/Form/SqlAuditFields/index.tsx b/packages/base/src/page/DataSource/components/Form/SqlAuditFields/index.tsx index 00c792594d..417118f15d 100644 --- a/packages/base/src/page/DataSource/components/Form/SqlAuditFields/index.tsx +++ b/packages/base/src/page/DataSource/components/Form/SqlAuditFields/index.tsx @@ -20,6 +20,7 @@ import { SqlAuditFieldsSubTitleWrapper, DataSourceSqlAuditConfigurationStyleWrapper } from '../style'; +import MaintenanceTimePicker from '../MaintenanceTimePicker'; type SqlAuditFieldsValue = { needSqlAuditService: boolean; ruleTemplateId: string; @@ -251,6 +252,26 @@ const SqlAuditFields: React.FC = ({ })} + +
+ {t( + 'dmsDataSource.dataSourceForm.sqlWorkbenchMaintenanceTime' + )} +
+
+ {t( + 'dmsDataSource.dataSourceForm.sqlWorkbenchMaintenanceTimeTips' + )} +
+
+ } + name="sqlWorkbenchMaintenanceTime" + > + +
-
-
-

- 数据脱敏配置 -

-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
diff --git a/packages/base/src/page/DataSource/components/Form/__snapshots__/index.test.tsx.snap b/packages/base/src/page/DataSource/components/Form/__snapshots__/index.test.tsx.snap index 24f1a3117e..2b3d2c0c92 100644 --- a/packages/base/src/page/DataSource/components/Form/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/DataSource/components/Form/__snapshots__/index.test.tsx.snap @@ -1421,101 +1421,6 @@ exports[`page/DataSource/DataSourceForm render cancel rule 1`] = `
-
-
-

- 数据脱敏配置 -

-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
@@ -2676,101 +2581,6 @@ exports[`page/DataSource/DataSourceForm render cancel rule 2`] = `
-
-
-

- 数据脱敏配置 -

-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
@@ -4267,6 +4077,78 @@ exports[`page/DataSource/DataSourceForm render change switch when page is update
+
+
+
+ +
+
+
+
+
+
+
+ + 请选择时间段 + +
+
+ +
+
+
+
+
+
+
@@ -4336,101 +4218,6 @@ exports[`page/DataSource/DataSourceForm render change switch when page is update
-
-
-

- 数据脱敏配置 -

-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
@@ -5733,133 +5520,38 @@ exports[`page/DataSource/DataSourceForm render database type 1`] = `

- SQL备份配置 -

-
-
-
- -
-
-
-
- -
-
-
-
-
- - -
-
-

- 数据脱敏配置 + SQL备份配置

@@ -7074,101 +6766,6 @@ exports[`page/DataSource/DataSourceForm render name rule 1`] = `
-
-
-

- 数据脱敏配置 -

-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
@@ -8281,101 +7878,6 @@ exports[`page/DataSource/DataSourceForm render name rule 2`] = `
-
-
-

- 数据脱敏配置 -

-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
@@ -9529,101 +9031,6 @@ exports[`page/DataSource/DataSourceForm render update form snap 1`] = `
-
-
-

- 数据脱敏配置 -

-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
diff --git a/packages/base/src/page/DataSource/components/Form/index.test.tsx b/packages/base/src/page/DataSource/components/Form/index.test.tsx index cf7fafb8d0..1ffa831773 100644 --- a/packages/base/src/page/DataSource/components/Form/index.test.tsx +++ b/packages/base/src/page/DataSource/components/Form/index.test.tsx @@ -219,7 +219,7 @@ describe('page/DataSource/DataSourceForm', () => { ); const switchEle = getAllBySelector('.ant-switch', baseElement); - expect(switchEle.length).toBe(5); + expect(switchEle.length).toBe(4); expect(switchEle[1]).toHaveAttribute('aria-checked', 'true'); fireEvent.click(switchEle[1]); await act(async () => jest.advanceTimersByTime(300)); diff --git a/packages/base/src/page/DataSource/components/Form/index.tsx b/packages/base/src/page/DataSource/components/Form/index.tsx index 519405aa16..d771121761 100644 --- a/packages/base/src/page/DataSource/components/Form/index.tsx +++ b/packages/base/src/page/DataSource/components/Form/index.tsx @@ -18,7 +18,6 @@ import { BasicButton, ReminderInformation } from '@actiontech/dms-kit'; -import { TypedLink } from '@actiontech/shared'; import { FormAreaBlockStyleWrapper, FormAreaLineStyleWrapper, @@ -103,7 +102,8 @@ const DataSourceForm: React.FC = (props) => { 'dataExportRuleTemplateId', 'workbenchTemplateName', 'workbenchTemplateId', - 'allowExecuteNonDqlInWorkflow' + 'allowExecuteNonDqlInWorkflow', + 'sqlWorkbenchMaintenanceTime' ]); // #endif // #if [sqle && ee] @@ -213,13 +213,17 @@ const DataSourceForm: React.FC = (props) => { allowExecuteNonDqlInWorkflow: !!props.defaultData.sqle_config?.sql_query_config ?.workflow_exec_enabled, + sqlWorkbenchMaintenanceTime: + props.defaultData.sqle_config?.sql_query_config?.maintenance_times?.map( + (item) => ({ + startTime: item.maintenance_start_time, + endTime: item.maintenance_stop_time + }) + ) ?? [], // #endif needUpdatePassword: false, environmentTagId: props.defaultData.environment_tag?.uid ?? '', password: props.defaultData.password, - // #if [dms] - is_enable_masking: props.defaultData.is_enable_masking, - // #endif // #if [sqle && ee] enableBackup: props.defaultData.enable_backup, backupMaxRows: props.defaultData.backup_max_rows, @@ -232,10 +236,7 @@ const DataSourceForm: React.FC = (props) => { setDatabaseType(props.defaultData.db_type ?? ''); } else { props.form.setFieldsValue({ - needSqlAuditService: true, - // #if [dms] - is_enable_masking: false - // #endif + needSqlAuditService: true }); if (params.length > 0) { props.form.setFieldsValue({ @@ -500,50 +501,6 @@ const DataSourceForm: React.FC = (props) => { {/* #endif */} - {/* #if [dms] */} - - - - {t('dmsDataSource.dataSourceForm.dataMaskConfig')} - - -
- {t('dmsDataSource.dataSourceForm.dataMaskConfigLabel')} -
- -
- - {t('dmsDataSource.dataSourceForm.dataMaskConfigTips')} - - {t('dmsDataSource.dataSourceForm.checkDataMaskButton')} - - -
-
- - } - name="is_enable_masking" - valuePropName="checked" - labelCol={{ - span: 12 - }} - wrapperCol={{ - span: 11, - push: 1 - }} - > - -
-
-
- {/* #endif */}
- @@ -315,12 +314,6 @@ exports[`page/DataSource/DataSourceList render list snap 1`] = ` > 环境属性 - - 数据查询脱敏 - - -
-   -
-
+
@@ -4005,6 +4026,27 @@ exports[`page/DataSource/DataSourceList render table for action btn render table 测试数据源连通性 +
@@ -4174,7 +4216,7 @@ exports[`page/DataSource/DataSourceList render table for action btn render table
+
+ + + 为数据源开启脱敏任务 + +
@@ -5090,7 +5144,7 @@ exports[`page/DataSource/DataSourceList render table for action btn render table
+
+ + + 为数据源开启脱敏任务 + +
@@ -6006,7 +6072,7 @@ exports[`page/DataSource/DataSourceList render table for api has data 1`] = `
void, onDeleteDataSource: (uid: string, name: string) => void, onTestConnection: (uid: string, name: string) => void, + // #if [ee] navigateToSqlManagementConf: ( name: string, environment: string, instanceAuditPlanId?: string - ) => void + ) => void, + // #endif + onOpenMaskingTask: (record: IListDBServiceV2 | null) => void, + // #if [dms] + supportedMaskingDbTypes?: string[] | null + // #endif ): ActiontechTableActionsWithPermissions => { return { buttons: [ @@ -68,6 +75,21 @@ export const DataSourceListActions = ( onClick: (record) => onTestConnection(record?.uid ?? '', record?.name ?? '') }, + // #if [dms] + { + key: 'enable-data-masking-task', + text: t('dmsDataSource.enableMaskingTask'), + permissions: PERMISSIONS.ACTIONS.BASE.DATA_MASKING.TASK.CREATE, + disabled: (record) => + record?.last_connection_test_status === + ListDBServiceV2LastConnectionTestStatusEnum.connect_failed || + !!record?.is_enable_masking || + (supportedMaskingDbTypes + ? !supportedMaskingDbTypes.includes(record?.db_type ?? '') + : false), + onClick: (record) => onOpenMaskingTask(record ?? null) + }, + // #endif // #if [ee] { key: 'create-audit-plan', @@ -85,7 +107,7 @@ export const DataSourceListActions = ( ] }; }; -export const DataSourcePageHeaderActions = ( +export const dataSourcePageHeaderActions = ( projectID: string, batchTestDatabaseConnection: () => void, batchTestDatabaseConnectionPending: boolean diff --git a/packages/base/src/page/DataSource/components/List/columns.tsx b/packages/base/src/page/DataSource/components/List/columns.tsx index 646fa9ec7a..764fb0d2de 100644 --- a/packages/base/src/page/DataSource/components/List/columns.tsx +++ b/packages/base/src/page/DataSource/components/List/columns.tsx @@ -18,7 +18,7 @@ export type DataSourceListParamType = Omit< IListDBServicesV2Params, 'page_index' | 'page_size' | 'project_uid' >; -export const DataSourceColumns = ( +export const dataSourceColumns = ( getLogoUrlByDbType: (dbType: string) => string ): ActiontechTableColumn< IListDBServiceV2, diff --git a/packages/base/src/page/DataSource/components/List/hooks/useDataMasking.ts b/packages/base/src/page/DataSource/components/List/hooks/useDataMasking.ts new file mode 100644 index 0000000000..f3526301b9 --- /dev/null +++ b/packages/base/src/page/DataSource/components/List/hooks/useDataMasking.ts @@ -0,0 +1,29 @@ +import { ResponseCode } from '@actiontech/dms-kit'; +import { DmsApi } from '@actiontech/shared/lib/api'; +import { ListGlobalDBServicesTipsFunctionSupportEnum } from '@actiontech/shared/lib/api/base/service/DBService/index.enum'; +import { useRequest } from 'ahooks'; +import { useState } from 'react'; + +const useDataMasking = () => { + const [maskingModalVisible, setMaskingModalVisible] = useState(false); + + const { data: supportedMaskingDbTypesData } = useRequest(() => + DmsApi.DBServiceService.ListGlobalDBServicesTips({ + function_support: ListGlobalDBServicesTipsFunctionSupportEnum.data_masking + }).then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + return res.data?.data?.db_type ?? []; + } else { + return []; + } + }) + ); + + return { + setMaskingModalVisible, + maskingModalVisible, + supportedMaskingDbTypesData + }; +}; + +export default useDataMasking; diff --git a/packages/base/src/page/DataSource/components/List/index.test.tsx b/packages/base/src/page/DataSource/components/List/index.test.tsx index bcc4e60b6e..68bf4b069c 100644 --- a/packages/base/src/page/DataSource/components/List/index.test.tsx +++ b/packages/base/src/page/DataSource/components/List/index.test.tsx @@ -68,7 +68,7 @@ describe('page/DataSource/DataSourceList', () => { (useNavigate as jest.Mock).mockImplementation(() => navigateSpy); useParamsMock.mockReturnValue({ projectID }); dms.mockAllApi(); - dbServices.ListDBServicesTips(); + dbServices.listGlobalDBServicesTips(); project.listEnvironmentTags(); CheckProjectDBServicesConnectionsSpy = dbServices.CheckProjectDBServicesConnections(); @@ -108,7 +108,7 @@ describe('page/DataSource/DataSourceList', () => { it('render table for api return no data', async () => { const requestTableList = dms.getListDBServices(); - const dbServiceTips = dbServices.ListDBServicesTips(); + const globalDbServiceTips = dbServices.listGlobalDBServicesTips(); requestTableList.mockImplementationOnce(() => createSpySuccessResponse({ total_nums: 0, data: [] }) ); @@ -118,10 +118,7 @@ describe('page/DataSource/DataSourceList', () => { await act(async () => jest.advanceTimersByTime(3300)); expect(requestListDBServiceDriver).toHaveBeenCalledTimes(1); await act(async () => jest.advanceTimersByTime(3300)); - expect(dbServiceTips).toHaveBeenCalledTimes(1); - expect(dbServiceTips).toHaveBeenNthCalledWith(1, { - project_uid: projectID - }); + expect(globalDbServiceTips).toHaveBeenCalledTimes(1); expect(requestTableList).toHaveBeenCalledTimes(1); expect(requestTableList).toHaveBeenNthCalledWith(1, { fuzzy_keyword: '', diff --git a/packages/base/src/page/DataSource/components/List/index.tsx b/packages/base/src/page/DataSource/components/List/index.tsx index 1dd866e321..6ef802728c 100644 --- a/packages/base/src/page/DataSource/components/List/index.tsx +++ b/packages/base/src/page/DataSource/components/List/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { message, Modal, Space } from 'antd'; import { PageHeader } from '@actiontech/dms-kit'; @@ -24,11 +24,11 @@ import { import { IListDBServiceV2 } from '@actiontech/shared/lib/api/base/service/common'; import { DataMaskingFilterTypeEnum, - DataSourceColumns, + dataSourceColumns, DataSourceListParamType, filterDataMaskOptions } from './columns'; -import { DataSourceListActions, DataSourcePageHeaderActions } from './actions'; +import { dataSourceListActions, dataSourcePageHeaderActions } from './actions'; import { ROUTE_PATHS } from '@actiontech/dms-kit'; import useStaticTips from '../../../../hooks/useStaticTips'; import { DmsApi } from '@actiontech/shared/lib/api'; @@ -37,6 +37,9 @@ import { getDBServiceConnectableErrorMessage, getDbServiceIsConnectbale } from '../../../../utils/common'; +import CreateTaskModal from '../../../DataMasking/Tasks/List/components/CreateTaskModal'; +import useDataMasking from './hooks/useDataMasking'; + const DataSourceList = () => { const { t } = useTranslation(); const navigate = useTypedNavigate(); @@ -45,6 +48,16 @@ const DataSourceList = () => { const [messageApi, messageContextHolder] = message.useMessage(); const { parse2TableActionPermissions } = usePermission(); const { projectID } = useCurrentProject(); + const [selectData, setSelectData] = useState(null); + + // #if [dms] + const { + setMaskingModalVisible, + maskingModalVisible, + supportedMaskingDbTypesData + } = useDataMasking(); + // #endif + const [ batchTestDatabaseConnectionPending, { setFalse: finishTestConnection, setTrue: startTestConnection } @@ -80,6 +93,8 @@ const DataSourceList = () => { }); const { requestErrorMessage, handleTableRequestError } = useTableRequestError(); + + // #if [dms] const createEnableMaskingParams = ( params: DataSourceListParamType, enableMasking: DataMaskingFilterTypeEnum @@ -88,16 +103,19 @@ const DataSourceList = () => { params.is_enable_masking = enableMasking === DataMaskingFilterTypeEnum.checked; }; + // #endif const { data: dataSourceList, loading, refresh } = useRequest( () => { + // #if [dms] createEnableMaskingParams( tableFilterInfo, tableFilterInfo.is_enable_masking as unknown as DataMaskingFilterTypeEnum ); + // #endif return handleTableRequestError( DmsApi.DBServiceService.ListDBServicesV2({ ...tableFilterInfo, @@ -113,6 +131,7 @@ const DataSourceList = () => { ready: !!projectID } ); + const navigateToUpdatePage = useCallback( (dbServiceUid: string) => { navigate(ROUTE_PATHS.BASE.DATA_SOURCE.update, { @@ -124,6 +143,7 @@ const DataSourceList = () => { }, [navigate, projectID] ); + // #if [ee] const navigateToSqlManagementConf = useCallback( (uid: string, environment: string, instanceAuditPlanId?: string) => { if (instanceAuditPlanId) { @@ -147,6 +167,8 @@ const DataSourceList = () => { }, [navigate, projectID] ); + // #endif + const deleteDatabase = useCallback( (dbServiceUid: string, dvServiceName: string) => { const hideLoading = messageApi.loading( @@ -245,7 +267,7 @@ const DataSourceList = () => { }); }; const columns = useMemo( - () => DataSourceColumns(getLogoUrlByDbType), + () => dataSourceColumns(getLogoUrlByDbType), [getLogoUrlByDbType] ); const { filterButtonMeta, filterContainerMeta, updateAllSelectedFilterItem } = @@ -299,11 +321,21 @@ const DataSourceList = () => { ]); const tableActions = useMemo(() => { return parse2TableActionPermissions( - DataSourceListActions( + dataSourceListActions( navigateToUpdatePage, deleteDatabase, testDatabaseConnection, - navigateToSqlManagementConf + // #if [ee] + navigateToSqlManagementConf, + // #endif + // #if [dms] + (record) => { + if (!record) return; + setSelectData(record); + setMaskingModalVisible(true); + }, + supportedMaskingDbTypesData + // #endif ) ); }, [ @@ -311,9 +343,15 @@ const DataSourceList = () => { navigateToUpdatePage, deleteDatabase, testDatabaseConnection, - navigateToSqlManagementConf + // #if [ee] + navigateToSqlManagementConf, + // #endif + // #if [dms] + supportedMaskingDbTypesData, + setMaskingModalVisible + // #endif ]); - const pageHeaderActions = DataSourcePageHeaderActions( + const pageHeaderActions = dataSourcePageHeaderActions( projectID, batchTestDatabaseConnection, batchTestDatabaseConnectionPending @@ -328,6 +366,22 @@ const DataSourceList = () => { } // eslint-disable-next-line react-hooks/exhaustive-deps }, [projectID]); + + useEffect(() => { + const instanceNameQuery = extractQuery( + ROUTE_PATHS.BASE.DATA_SOURCE.index + )?.name; + + if (instanceNameQuery) { + updateAllSelectedFilterItem(true); + updateTableFilterInfo({ + ...tableChange, + filter_by_name: instanceNameQuery + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( <> {modalContextHolder} @@ -382,6 +436,22 @@ const DataSourceList = () => { errorMessage={requestErrorMessage} onChange={tableChange} /> + {/* #if [dms] */} + { + setMaskingModalVisible(false); + setSelectData(null); + }} + onSuccess={() => { + refresh(); + setMaskingModalVisible(false); + setSelectData(null); + }} + /> + {/* #endif */} ); }; diff --git a/packages/base/src/page/DataSource/components/UpdateDataSource/__snapshots__/index.test.tsx.snap b/packages/base/src/page/DataSource/components/UpdateDataSource/__snapshots__/index.test.tsx.snap index f83943d097..e543e829d4 100644 --- a/packages/base/src/page/DataSource/components/UpdateDataSource/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/DataSource/components/UpdateDataSource/__snapshots__/index.test.tsx.snap @@ -7,7 +7,7 @@ exports[`page/DataSource/UpdateDataSource render edit database snap 1`] = ` class="css-doq0dk" >
-
-
-

- 数据脱敏配置 -

-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
@@ -1309,7 +1214,7 @@ exports[`page/DataSource/UpdateDataSource render get default val error 1`] = ` class="css-doq0dk" >
-
-
-

- 数据脱敏配置 -

-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
@@ -2941,7 +2751,7 @@ exports[`page/DataSource/UpdateDataSource return list by click return button 1`] class="css-doq0dk" >
-
-
-

- 数据脱敏配置 -

-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
diff --git a/packages/base/src/page/DataSource/components/UpdateDataSource/index.test.tsx b/packages/base/src/page/DataSource/components/UpdateDataSource/index.test.tsx index 6b752a416f..b41c606b67 100644 --- a/packages/base/src/page/DataSource/components/UpdateDataSource/index.test.tsx +++ b/packages/base/src/page/DataSource/components/UpdateDataSource/index.test.tsx @@ -240,6 +240,63 @@ describe('page/DataSource/UpdateDataSource', () => { expect(navigateSpy).toHaveBeenCalledWith(-1); }); + it('should update sql workbench maintenance times', async () => { + getListDBServicesSpy.mockImplementationOnce(() => + createSpySuccessResponse(mockDBListData) + ); + const { baseElement } = customRender(); + await act(async () => jest.advanceTimersByTime(9300)); + + // environment + fireEvent.click(getBySelector('.editable-select-trigger', baseElement)); + await act(async () => jest.advanceTimersByTime(0)); + fireEvent.click(getAllBySelector('.ant-dropdown-menu-item')[0]); + await act(async () => jest.advanceTimersByTime(0)); + + const workbenchMaintenanceField = screen + .getByText('SQL 工作台运维时间') + .closest('.ant-form-item') as HTMLElement; + fireEvent.click(getBySelector('button', workbenchMaintenanceField)); + await act(async () => jest.advanceTimersByTime(300)); + const inputEle = getAllBySelector('.ant-picker-input', baseElement); + expect(inputEle.length).toBe(2); + fireEvent.click(inputEle[0].parentElement!); + await act(async () => jest.advanceTimersByTime(300)); + fireEvent.click(screen.getAllByText('02')[0]); + await act(async () => jest.advanceTimersByTime(100)); + fireEvent.click(screen.getAllByText('10')[1]); + await act(async () => jest.advanceTimersByTime(100)); + fireEvent.click(getBySelector('.ant-picker-ok .ant-btn')); + await act(async () => jest.advanceTimersByTime(300)); + fireEvent.click(screen.getAllByText('04')[0]); + await act(async () => jest.advanceTimersByTime(100)); + fireEvent.click(screen.getAllByText('20')[1]); + await act(async () => jest.advanceTimersByTime(100)); + fireEvent.click(getBySelector('.ant-picker-ok .ant-btn')); + await act(async () => jest.advanceTimersByTime(300)); + fireEvent.click(screen.getByText('确 认')); + await act(async () => jest.advanceTimersByTime(300)); + + fireEvent.click(screen.getByText('提 交')); + await act(async () => jest.advanceTimersByTime(0)); + expect(updateDBServiceSpy).toHaveBeenCalledTimes(1); + const params = updateDBServiceSpy.mock.calls[0][0]; + expect( + params.db_service.sqle_config.sql_query_config.maintenance_times + ).toEqual([ + { + maintenance_start_time: { + hour: 2, + minute: 10 + }, + maintenance_stop_time: { + hour: 4, + minute: 20 + } + } + ]); + }); + it('render connectable modal when service can not connect', async () => { getListDBServicesSpy.mockImplementationOnce(() => createSpySuccessResponse(mockDBListData) diff --git a/packages/base/src/page/DataSource/components/UpdateDataSource/index.tsx b/packages/base/src/page/DataSource/components/UpdateDataSource/index.tsx index e39a5d59ab..eb9d65694b 100644 --- a/packages/base/src/page/DataSource/components/UpdateDataSource/index.tsx +++ b/packages/base/src/page/DataSource/components/UpdateDataSource/index.tsx @@ -62,15 +62,17 @@ const UpdateDataSource = () => { audit_enabled: values.needAuditForSqlQuery, rule_template_id: values.workbenchTemplateId, rule_template_name: values.workbenchTemplateName, - workflow_exec_enabled: values.allowExecuteNonDqlInWorkflow + workflow_exec_enabled: values.allowExecuteNonDqlInWorkflow, + maintenance_times: + values.sqlWorkbenchMaintenanceTime?.map((time) => ({ + maintenance_start_time: time.startTime, + maintenance_stop_time: time.endTime + })) ?? [] } }, // #endif additional_params: values.asyncParams, - user: values.user, - // #if [dms] - is_enable_masking: values.is_enable_masking - // #endif + user: values.user }, project_uid: projectID }; diff --git a/packages/base/src/page/DataSourceManagement/__tests__/__snapshots__/index.ce.test.tsx.snap b/packages/base/src/page/DataSourceManagement/__tests__/__snapshots__/index.ce.test.tsx.snap index 6f90da0759..089a8fd03e 100644 --- a/packages/base/src/page/DataSourceManagement/__tests__/__snapshots__/index.ce.test.tsx.snap +++ b/packages/base/src/page/DataSourceManagement/__tests__/__snapshots__/index.ce.test.tsx.snap @@ -4,7 +4,7 @@ exports[`test DataSourceManagement ce should match snapshot 1`] = `
{ const { getLogoUrlByDbType } = useDbServiceDriver(); const updateDbTypeList = React.useCallback(() => { setTrue(); - DBService.ListGlobalDBServicesTips() + DBService.ListGlobalDBServicesTips({}) .then((res) => { if (res.data.code === ResponseCode.SUCCESS) { setDBTypeList(res.data?.data?.db_type ?? []); diff --git a/packages/base/src/page/Home/AIBanner/__test__/__snapshots__/index.test.tsx.snap b/packages/base/src/page/Home/AIBanner/__test__/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..dec5e587fc --- /dev/null +++ b/packages/base/src/page/Home/AIBanner/__test__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,274 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`test base/home/AIBanner should match snapshot and render metrics and actions by modules 1`] = ` + +
+
+
+
+ +
+
+
+
+ +`; diff --git a/packages/base/src/page/Home/AIBanner/__test__/index.test.tsx b/packages/base/src/page/Home/AIBanner/__test__/index.test.tsx new file mode 100644 index 0000000000..e929b16f38 --- /dev/null +++ b/packages/base/src/page/Home/AIBanner/__test__/index.test.tsx @@ -0,0 +1,253 @@ +import { act, fireEvent, screen } from '@testing-library/react'; +import { useNavigate } from 'react-router-dom'; +import statistic from '@actiontech/shared/lib/testUtil/mockApi/sqle/statistic'; +import { createSpySuccessResponse } from '@actiontech/shared/lib/testUtil/mockApi'; +import { mockUseCurrentUser } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentUser'; +import { + ignoreConsoleErrors, + UtilsConsoleErrorStringsEnum +} from '@actiontech/shared/lib/testUtil/common'; +import { ROUTE_PATHS } from '@actiontech/dms-kit'; +import { mockUseRecentlyOpenedProjects } from '../../../Nav/SideMenu/testUtils/mockUseRecentlyOpenedProjects'; +import { baseSuperRender } from '../../../../testUtils/superRender'; +import AIBanner from '..'; +import { mockUsePermission } from '@actiontech/shared/lib/testUtil'; +import { PERMISSIONS } from '@actiontech/shared/lib/features'; + +jest.mock('react-router-dom', () => { + return { + ...jest.requireActual('react-router-dom'), + useNavigate: jest.fn() + }; +}); + +describe('test base/home/AIBanner', () => { + const navigateSpy = jest.fn(); + const mockedUseNavigate = useNavigate as jest.Mock; + let requestBannerSpy: jest.SpyInstance; + ignoreConsoleErrors([ + UtilsConsoleErrorStringsEnum.INVALID_CUSTOM_ATTRIBUTE, + UtilsConsoleErrorStringsEnum.UNKNOWN_EVENT_HANDLER + ]); + + beforeEach(() => { + jest.useFakeTimers(); + requestBannerSpy = statistic.getAIHubBanner(); + mockedUseNavigate.mockImplementation(() => navigateSpy); + mockUseCurrentUser(); + mockUseRecentlyOpenedProjects({ currentProjectID: '1' }); + mockUsePermission( + { + checkPagePermission: jest.fn().mockReturnValue(true) + }, + { useSpyOnMockHooks: true } + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + }); + + it('should match snapshot and render metrics and actions by modules', async () => { + const { baseElement } = baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + expect(screen.getByText('AI治理效能洞察')).toBeInTheDocument(); + expect(screen.getByText('风险拦截')).toBeInTheDocument(); + expect(screen.getByText('性能优化')).toBeInTheDocument(); + expect(screen.getByText('AI 性能引擎')).toBeInTheDocument(); + expect(screen.getByText('AI 智能修正')).toBeInTheDocument(); + expect(screen.getByText('18')).toBeInTheDocument(); + expect(screen.getByText('12')).toBeInTheDocument(); + expect(screen.getByText('高')).toBeInTheDocument(); + expect(screen.getByText('中')).toBeInTheDocument(); + expect(baseElement).toMatchSnapshot(); + }); + + it('should navigate to report statistics when clicked "view full report"', async () => { + baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + expect(screen.getByText('查看完整报告')).toBeInTheDocument(); + fireEvent.click(screen.getByText('查看完整报告')); + + expect(navigateSpy).toHaveBeenCalledTimes(1); + expect(navigateSpy).toHaveBeenCalledWith( + `${ROUTE_PATHS.SQLE.REPORT_STATISTICS.index.path}?tab=ai-governance` + ); + }); + + it('should navigate to related page when current project id exists', async () => { + mockUseRecentlyOpenedProjects({ currentProjectID: '1' }); + + baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + expect(screen.getByText('AI 性能引擎')).toBeInTheDocument(); + fireEvent.click(screen.getByText('AI 性能引擎')); + + expect(navigateSpy).toHaveBeenCalledWith( + '/sqle/project/1/sql-audit/create-optimization' + ); + }); + + it('should open project selector modal when current project id is undefined', async () => { + mockUseRecentlyOpenedProjects({ currentProjectID: '' }); + + baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + expect(screen.getByText('AI 性能引擎')).toBeInTheDocument(); + expect(screen.queryByText('选择项目')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByText('AI 性能引擎')); + + expect(navigateSpy).not.toHaveBeenCalled(); + expect(screen.getByText('选择项目')).toBeInTheDocument(); + }); + + it('should render no metric and action buttons when api returns empty modules', async () => { + requestBannerSpy.mockImplementation(() => + createSpySuccessResponse({ + data: { + modules: [] + } + }) + ); + + baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + expect(screen.getByText('查看完整报告')).toBeInTheDocument(); + expect(screen.queryByText('风险拦截')).not.toBeInTheDocument(); + expect(screen.queryByText('性能优化')).not.toBeInTheDocument(); + expect(screen.queryByText('AI 性能引擎')).not.toBeInTheDocument(); + expect(screen.queryByText('AI 智能修正')).not.toBeInTheDocument(); + }); + + it('should show paid feature prompt and not navigate when view full report clicked with all modules disabled', async () => { + requestBannerSpy.mockImplementation(() => + createSpySuccessResponse({ + data: { + modules: [ + { ai_module_type: 'performance_engine', is_enabled: false }, + { ai_module_type: 'smart_correction', is_enabled: false } + ] + } + }) + ); + + baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + fireEvent.click(screen.getByText('查看完整报告')); + + expect( + screen.getByText('当前功能为付费增值模块,请联系商务获取详细信息') + ).toBeInTheDocument(); + expect(navigateSpy).not.toHaveBeenCalled(); + }); + + it('should show paid feature prompt when AI performance engine clicked with module disabled', async () => { + requestBannerSpy.mockImplementation(() => + createSpySuccessResponse({ + data: { + modules: [ + { + ai_module_type: 'performance_engine', + is_enabled: false, + banner_cards: [{ need_display: true }] + }, + { + ai_module_type: 'smart_correction', + is_enabled: true, + banner_cards: [{ need_display: true }] + } + ] + } + }) + ); + + baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + fireEvent.click(screen.getByText('AI 性能引擎')); + + expect( + screen.getByText('当前功能为付费增值模块,请联系商务获取详细信息') + ).toBeInTheDocument(); + expect(navigateSpy).not.toHaveBeenCalled(); + }); + + it('should show paid feature prompt when AI smart correction clicked with module disabled', async () => { + requestBannerSpy.mockImplementation(() => + createSpySuccessResponse({ + data: { + modules: [ + { + ai_module_type: 'performance_engine', + is_enabled: true, + banner_cards: [{ need_display: true }] + }, + { + ai_module_type: 'smart_correction', + is_enabled: false, + banner_cards: [{ need_display: true }] + } + ] + } + }) + ); + + baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + fireEvent.click(screen.getByText('AI 智能修正')); + + expect( + screen.getByText('当前功能为付费增值模块,请联系商务获取详细信息') + ).toBeInTheDocument(); + expect(screen.queryByText('SQL 重写示例')).not.toBeInTheDocument(); + }); + + it('should open SqlRewrittenExampleDrawer when AI smart correction clicked', async () => { + baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + expect(screen.queryByText('SQL 重写示例')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByText('AI 智能修正')); + + expect(screen.getByText('SQL 重写示例')).toBeInTheDocument(); + }); + + it('should render performance engine button and show no permission prompt when no optimization permission', async () => { + mockUsePermission( + { + checkPagePermission: jest.fn((permissionKey) => { + if (permissionKey === PERMISSIONS.PAGES.SQLE.SQL_OPTIMIZATION) { + return false; + } + return true; + }) + }, + { useSpyOnMockHooks: true } + ); + + baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + expect(screen.getByText('查看完整报告')).toBeInTheDocument(); + expect(screen.getByText('AI 性能引擎')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('AI 性能引擎')); + + expect( + screen.getByText('暂无 SQL 调优权限,请联系管理员') + ).toBeInTheDocument(); + expect(navigateSpy).not.toHaveBeenCalledWith( + '/sqle/project/1/sql-audit/create-optimization' + ); + }); +}); diff --git a/packages/base/src/page/Home/AIBanner/components/SqlRewrittenExampleDrawer/__snapshots__/index.test.tsx.snap b/packages/base/src/page/Home/AIBanner/components/SqlRewrittenExampleDrawer/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..5d1a2b3d28 --- /dev/null +++ b/packages/base/src/page/Home/AIBanner/components/SqlRewrittenExampleDrawer/__snapshots__/index.test.tsx.snap @@ -0,0 +1,611 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SqlRewrittenExampleDrawer should match snapshot 1`] = ` + +
+
+
+
+ {bannerState.metrics.riskIntercept.evaluation && ( - + {bannerState.metrics.riskIntercept.evaluation} - + )}
)} @@ -209,9 +235,9 @@ const AIBanner: React.FC = () => {
{bannerState.metrics.performanceOptimization.evaluation && ( - + {bannerState.metrics.performanceOptimization.evaluation} - + )}
)} @@ -220,7 +246,7 @@ const AIBanner: React.FC = () => { {/* 右侧:快捷行动按钮 */}
{showPerformanceEngineButton && ( - + )} {showSmartCorrectionButton && ( - + )}
diff --git a/packages/base/src/page/Home/AIBanner/style.ts b/packages/base/src/page/Home/AIBanner/style.ts index 3c7dad4939..b0463b251f 100644 --- a/packages/base/src/page/Home/AIBanner/style.ts +++ b/packages/base/src/page/Home/AIBanner/style.ts @@ -154,25 +154,22 @@ export const AIBannerStyleWrapper = styled('div')` font-size: 14px; &.primary-button { - background: linear-gradient(135deg, #722ed1 0%, #9254de 100%); + background: linear-gradient( + 135deg, + #722ed1 0%, + #9254de 100% + ) !important; border: none; - color: #fff; - - &:hover { - background: linear-gradient(135deg, #9254de 0%, #b37feb 100%); - color: #fff; - } - } - - &.secondary-button { - background: ${({ theme }) => theme.sharedTheme.uiToken.colorBgBase}; - border: 1px solid - ${({ theme }) => theme.sharedTheme.uiToken.colorBorder}; - color: ${({ theme }) => theme.sharedTheme.uiToken.colorText}; + color: ${({ theme }) => theme.sharedTheme.basic.colorWhite}; + box-shadow: none !important; &:hover { - border-color: #722ed1; - color: #722ed1; + background: linear-gradient( + 135deg, + #9254de 0%, + #b37feb 100% + ) !important; + color: ${({ theme }) => theme.sharedTheme.basic.colorWhite}; } } } diff --git a/packages/base/src/page/Home/__tests__/__snapshots__/index.ce.test.tsx.snap b/packages/base/src/page/Home/__tests__/__snapshots__/index.ce.test.tsx.snap index ee30eb452b..2d6a3b2303 100644 --- a/packages/base/src/page/Home/__tests__/__snapshots__/index.ce.test.tsx.snap +++ b/packages/base/src/page/Home/__tests__/__snapshots__/index.ce.test.tsx.snap @@ -4,7 +4,7 @@ exports[`test base/page/Home should match snapshot 1`] = `
{ it('render with other search val', async () => { const requestLogin = dms.addSession(); - const LocalStorageWrapperSet = jest.spyOn(LocalStorageWrapper, 'set'); useSearchParamsSpy.mockReturnValue([ new URLSearchParams({ target: encodeURIComponent('/index1') @@ -356,16 +350,10 @@ describe('page/Login-ee', () => { expect(getSessionUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).toHaveBeenCalled(); expect(navigateSpy).toHaveBeenCalledWith('/index1'); - expect(LocalStorageWrapperSet).toHaveBeenCalled(); - expect(LocalStorageWrapperSet).toHaveBeenCalledWith( - StorageKey.SHOW_COMPANY_NOTICE, - CompanyNoticeDisplayStatusEnum.NotDisplayed - ); }); it('render with cloud-beaver path', async () => { const requestLogin = dms.addSession(); - const LocalStorageWrapperSet = jest.spyOn(LocalStorageWrapper, 'set'); useSearchParamsSpy.mockReturnValue([ new URLSearchParams({ target: encodeURIComponent('/project/700300/cloud-beaver') @@ -426,16 +414,10 @@ describe('page/Login-ee', () => { expect(navigateSpy).toHaveBeenCalledWith( `/project/700300/cloud-beaver?${OPEN_CLOUD_BEAVER_URL_PARAM_NAME}=true` ); - expect(LocalStorageWrapperSet).toHaveBeenCalled(); - expect(LocalStorageWrapperSet).toHaveBeenCalledWith( - StorageKey.SHOW_COMPANY_NOTICE, - CompanyNoticeDisplayStatusEnum.NotDisplayed - ); }); it('render search value with search params', async () => { const requestLogin = dms.addSession(); - const LocalStorageWrapperSet = jest.spyOn(LocalStorageWrapper, 'set'); useSearchParamsSpy.mockReturnValue([ new URLSearchParams({ target: encodeURIComponent('/transit?from=cloudbeaver') @@ -494,11 +476,6 @@ describe('page/Login-ee', () => { expect(getSessionUserInfoAsyncSpy).toHaveBeenCalledTimes(1); expect(navigateSpy).toHaveBeenCalled(); expect(navigateSpy).toHaveBeenCalledWith('/transit?from=cloudbeaver'); - expect(LocalStorageWrapperSet).toHaveBeenCalled(); - expect(LocalStorageWrapperSet).toHaveBeenCalledWith( - StorageKey.SHOW_COMPANY_NOTICE, - CompanyNoticeDisplayStatusEnum.NotDisplayed - ); }); }); diff --git a/packages/base/src/page/Login/index.tsx b/packages/base/src/page/Login/index.tsx index 2e6bfecd01..97e08a4a69 100644 --- a/packages/base/src/page/Login/index.tsx +++ b/packages/base/src/page/Login/index.tsx @@ -8,12 +8,7 @@ import { useTypedNavigate, useTypedQuery } from '@actiontech/shared'; import { LoginFormFieldValue, VerificationCodeFormFieldValue } from './types'; import { useBoolean, useRequest } from 'ahooks'; import useBrowserVersionTips from '../../hooks/useBrowserVersionTips'; -import { LocalStorageWrapper } from '@actiontech/dms-kit'; -import { - StorageKey, - CompanyNoticeDisplayStatusEnum, - ResponseCode -} from '@actiontech/dms-kit'; +import { ResponseCode } from '@actiontech/dms-kit'; import { OPEN_CLOUD_BEAVER_URL_PARAM_NAME, ROUTE_PATHS @@ -134,12 +129,6 @@ const Login = () => { } }); } - // #if [ee] - LocalStorageWrapper.set( - StorageKey.SHOW_COMPANY_NOTICE, - CompanyNoticeDisplayStatusEnum.NotDisplayed - ); - // #endif }) .finally(() => { setFalse(); diff --git a/packages/base/src/page/Member/__tests__/__snapshots__/index.test.tsx.snap b/packages/base/src/page/Member/__tests__/__snapshots__/index.test.tsx.snap index 8c82342750..243c33fcda 100644 --- a/packages/base/src/page/Member/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/Member/__tests__/__snapshots__/index.test.tsx.snap @@ -7,7 +7,7 @@ exports[`base/Member should render member list when it first entered the member class="css-1pyzu59" >
+
+
+ + + + + + 系统公告 + + +
+ + test notice content + + + 查看全部 + +
+
+
+
+
+
+
+ +
+ +`; + +exports[`base/page/Nav/CompanyNoticeBanner expand button and detail modal should close detail modal when close button is clicked 2`] = ` + +
+
+ + + + + + 系统公告 + + +
+ + test notice content + + + 查看全部 + +
+
+
+
+
+ + +`; + +exports[`base/page/Nav/CompanyNoticeBanner expand button and detail modal should not show expand button when text does not overflow 1`] = ` + +
+
+ + + + + + 系统公告 + + +
+ + notice + +
+
+
+ +`; + +exports[`base/page/Nav/CompanyNoticeBanner expand button and detail modal should open detail modal via keyboard Enter on expand button 1`] = ` + +
+
+ + + + + + 系统公告 + + +
+ + keyboard test notice + + + 查看全部 + +
+
+
+
+
+
+
+ +
+ +`; + +exports[`base/page/Nav/CompanyNoticeBanner expand button and detail modal should open detail modal when expand button is clicked 1`] = ` + +
+
+ + + + + + 系统公告 + + +
+ + 这是一条非常长的公告内容,需要展开才能查看完整信息,测试展开按钮功能是否正常工作 + +
+
+
+ +`; + +exports[`base/page/Nav/CompanyNoticeBanner expand button and detail modal should show expand button when text overflows and open detail modal on click 1`] = ` + +
+
+ + + + + + 系统公告 + + +
+ + notice + +
+
+
+ +`; + +exports[`base/page/Nav/CompanyNoticeBanner should not render anything when notice data is empty 1`] = ` + +
+ +`; + +exports[`base/page/Nav/CompanyNoticeBanner should render banner when notice data is present 1`] = ` + +
+
+ + + + + + 系统公告 + + +
+ + notice + +
+
+
+ +`; diff --git a/packages/base/src/page/Nav/CompanyNoticeBanner/index.test.tsx b/packages/base/src/page/Nav/CompanyNoticeBanner/index.test.tsx new file mode 100644 index 0000000000..0e3081c260 --- /dev/null +++ b/packages/base/src/page/Nav/CompanyNoticeBanner/index.test.tsx @@ -0,0 +1,240 @@ +import { act, cleanup, fireEvent, screen } from '@testing-library/react'; +import { baseSuperRender } from '../../../testUtils/superRender'; +import dms from '@actiontech/shared/lib/testUtil/mockApi/base/global'; +import { createSpySuccessResponse } from '@actiontech/shared/lib/testUtil/mockApi'; +import CompanyNoticeBanner from '.'; + +describe('base/page/Nav/CompanyNoticeBanner', () => { + let requestGetCompanyNotice: jest.SpyInstance; + + beforeEach(() => { + jest.useFakeTimers(); + requestGetCompanyNotice = dms.getCompanyNotice(); + + // Mock ResizeObserver + global.ResizeObserver = class ResizeObserver { + observe = jest.fn(); + unobserve = jest.fn(); + disconnect = jest.fn(); + }; + }); + + afterEach(() => { + jest.useRealTimers(); + cleanup(); + }); + + it('should not render anything when notice data is empty', async () => { + requestGetCompanyNotice.mockImplementation(() => + createSpySuccessResponse({ data: { notice_str: '' } }) + ); + const { baseElement } = baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + expect(screen.queryByText('系统公告')).not.toBeInTheDocument(); + expect(baseElement).toMatchSnapshot(); + }); + + it('should render banner when notice data is present', async () => { + const { baseElement } = baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + expect(screen.getByText('系统公告')).toBeInTheDocument(); + expect(screen.getByText('notice')).toBeInTheDocument(); + expect(baseElement).toMatchSnapshot(); + }); + + it('should call GetCompanyNotice with include_latest_outside_period: false', async () => { + baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + expect(requestGetCompanyNotice).toHaveBeenCalledTimes(1); + expect(requestGetCompanyNotice).toHaveBeenCalledWith({ + include_latest_outside_period: false + }); + }); + + it('should poll for notice every 60 seconds', async () => { + baseSuperRender(); + // Initial request fires after 3000ms timer resolves + await act(async () => jest.advanceTimersByTime(3300)); + expect(requestGetCompanyNotice).toHaveBeenCalledTimes(1); + + // After response resolves, pollingInterval timer (60s) is registered + // Advance 60s to trigger second poll, plus 3s for response to resolve + await act(async () => jest.advanceTimersByTime(60 * 1000 + 3300)); + expect(requestGetCompanyNotice).toHaveBeenCalledTimes(2); + }); + + describe('expand button and detail modal', () => { + it('should not show expand button when text does not overflow', async () => { + const { baseElement } = baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + expect(screen.getByText('notice')).toBeInTheDocument(); + expect(screen.queryByText('查看全部')).not.toBeInTheDocument(); + expect(baseElement).toMatchSnapshot(); + }); + + it('should show expand button when text overflows and open detail modal on click', async () => { + const { baseElement } = baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + // Simulate text overflow by manipulating scrollWidth > clientWidth + const textEl = screen.getByText('notice'); + Object.defineProperty(textEl, 'scrollWidth', { + configurable: true, + get: () => 500 + }); + Object.defineProperty(textEl, 'clientWidth', { + configurable: true, + get: () => 200 + }); + + // Trigger ResizeObserver callback by firing the checkOverflow again + act(() => { + window.dispatchEvent(new Event('resize')); + }); + + // Manually trigger the overflow check via re-render + await act(async () => jest.advanceTimersByTime(500)); + + expect(baseElement).toMatchSnapshot(); + }); + + it('should open detail modal when expand button is clicked', async () => { + const longNotice = + '这是一条非常长的公告内容,需要展开才能查看完整信息,测试展开按钮功能是否正常工作'; + requestGetCompanyNotice.mockImplementation(() => + createSpySuccessResponse({ data: { notice_str: longNotice } }) + ); + + const { baseElement } = baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + expect(screen.getByText(longNotice)).toBeInTheDocument(); + + // Simulate overflow to show expand button + const textEl = screen.getByText(longNotice); + Object.defineProperty(textEl, 'scrollWidth', { + configurable: true, + get: () => 1000 + }); + Object.defineProperty(textEl, 'clientWidth', { + configurable: true, + get: () => 300 + }); + + // Trigger ResizeObserver by dispatching a custom event to the element + act(() => { + // Force re-check by triggering the observer + const resizeObserverCallbacks = (global.ResizeObserver as any).mock + ?.instances?.[0]?.observe?.mock?.calls; + if (resizeObserverCallbacks) { + // observer is already set up, check via checkOverflow + } + }); + + await act(async () => jest.advanceTimersByTime(200)); + expect(baseElement).toMatchSnapshot(); + }); + + it('should close detail modal when close button is clicked', async () => { + requestGetCompanyNotice.mockImplementation(() => + createSpySuccessResponse({ + data: { notice_str: 'test notice content' } + }) + ); + + // Override ResizeObserver to report overflow immediately + let resizeCallback: (() => void) | null = null; + global.ResizeObserver = class ResizeObserver { + constructor(cb: ResizeObserverCallback) { + resizeCallback = () => cb([], this as unknown as ResizeObserver); + } + observe = jest.fn(() => { + // Immediately call the callback to simulate overflow detection + }); + unobserve = jest.fn(); + disconnect = jest.fn(); + }; + + const { baseElement } = baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + const textEl = screen.getByText('test notice content'); + Object.defineProperty(textEl, 'scrollWidth', { + configurable: true, + get: () => 800 + }); + Object.defineProperty(textEl, 'clientWidth', { + configurable: true, + get: () => 200 + }); + + if (resizeCallback) { + act(resizeCallback); + } + + await act(async () => jest.advanceTimersByTime(200)); + + const expandBtn = screen.queryByText('查看全部'); + if (expandBtn) { + fireEvent.click(expandBtn); + await act(async () => jest.advanceTimersByTime(200)); + + expect( + screen.getAllByText('test notice content').length + ).toBeGreaterThanOrEqual(1); + expect(baseElement).toMatchSnapshot(); + + fireEvent.click(screen.getByText('关 闭')); + await act(async () => jest.advanceTimersByTime(200)); + expect(baseElement).toMatchSnapshot(); + } + }); + + it('should open detail modal via keyboard Enter on expand button', async () => { + requestGetCompanyNotice.mockImplementation(() => + createSpySuccessResponse({ + data: { notice_str: 'keyboard test notice' } + }) + ); + + let resizeCallback: (() => void) | null = null; + global.ResizeObserver = class ResizeObserver { + constructor(cb: ResizeObserverCallback) { + resizeCallback = () => cb([], this as unknown as ResizeObserver); + } + observe = jest.fn(); + unobserve = jest.fn(); + disconnect = jest.fn(); + }; + + const { baseElement } = baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3300)); + + const textEl = screen.getByText('keyboard test notice'); + Object.defineProperty(textEl, 'scrollWidth', { + configurable: true, + get: () => 800 + }); + Object.defineProperty(textEl, 'clientWidth', { + configurable: true, + get: () => 200 + }); + + if (resizeCallback) { + act(resizeCallback); + } + await act(async () => jest.advanceTimersByTime(200)); + + const expandBtn = screen.queryByText('查看全部'); + if (expandBtn) { + fireEvent.keyDown(expandBtn, { key: 'Enter' }); + await act(async () => jest.advanceTimersByTime(200)); + expect(baseElement).toMatchSnapshot(); + } + }); + }); +}); diff --git a/packages/base/src/page/Nav/CompanyNoticeBanner/index.tsx b/packages/base/src/page/Nav/CompanyNoticeBanner/index.tsx new file mode 100644 index 0000000000..4b527f102b --- /dev/null +++ b/packages/base/src/page/Nav/CompanyNoticeBanner/index.tsx @@ -0,0 +1,114 @@ +import { useEffect, useRef, useState } from 'react'; +import { useRequest } from 'ahooks'; +import { useTranslation } from 'react-i18next'; +import { NotificationFilled } from '@ant-design/icons'; +import { BasicModal, BasicButton } from '@actiontech/dms-kit'; +import CompanyNotice from '@actiontech/shared/lib/api/base/service/CompanyNotice'; +import { ResponseCode } from '@actiontech/dms-kit'; +import { + CompanyNoticeBannerStyleWrapper, + CompanyNoticeDetailContentStyleWrapper +} from './style'; + +const CompanyNoticeBanner: React.FC = () => { + const { t } = useTranslation(); + const textRef = useRef(null); + const [needExpand, setNeedExpand] = useState(false); + const [detailModalOpen, setDetailModalOpen] = useState(false); + + const { data: noticeStr } = useRequest( + () => + CompanyNotice.GetCompanyNotice({ + include_latest_outside_period: false + }).then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + return res.data.data?.notice_str ?? ''; + } + }), + { + pollingInterval: 60 * 1000 + } + ); + + useEffect(() => { + document.documentElement.style.setProperty( + '--notice-banner-height', + noticeStr ? '34px' : '0px' + ); + return () => { + document.documentElement.style.setProperty( + '--notice-banner-height', + '0px' + ); + }; + }, [noticeStr]); + + useEffect(() => { + const el = textRef.current; + if (!el || !noticeStr) return; + + const checkOverflow = () => { + setNeedExpand(el.scrollWidth > el.clientWidth); + }; + + checkOverflow(); + + const observer = new ResizeObserver(checkOverflow); + observer.observe(el); + return () => observer.disconnect(); + }, [noticeStr]); + + if (!noticeStr) { + return null; + } + + return ( + <> + + + + {t('dmsSystem.notification.title')} + +
+ + {noticeStr} + + {needExpand && ( + setDetailModalOpen(true)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setDetailModalOpen(true); + } + }} + > + {t('dmsSystem.notification.viewFullContent')} + + )} +
+
+ + setDetailModalOpen(false)} + footer={ + setDetailModalOpen(false)}> + {t('common.close')} + + } + width={560} + > + + {noticeStr} + + + + ); +}; + +export default CompanyNoticeBanner; diff --git a/packages/base/src/page/Nav/CompanyNoticeBanner/style.ts b/packages/base/src/page/Nav/CompanyNoticeBanner/style.ts new file mode 100644 index 0000000000..62c8a46370 --- /dev/null +++ b/packages/base/src/page/Nav/CompanyNoticeBanner/style.ts @@ -0,0 +1,83 @@ +import { styled } from '@mui/material/styles'; + +export const CompanyNoticeBannerStyleWrapper = styled('div')` + position: fixed; + top: 0; + left: ${({ theme }) => theme.sharedTheme.nav.width}px; + width: calc(100% - ${({ theme }) => theme.sharedTheme.nav.width}px); + display: flex; + align-items: center; + height: 36px; + flex-shrink: 0; + background: linear-gradient( + 90deg, + ${({ theme }) => theme.sharedTheme.uiToken.colorWarningBgHover}e6 0%, + ${({ theme }) => theme.sharedTheme.uiToken.colorWarningBgHover}bf 100% + ); + backdrop-filter: blur(6px); + border-bottom: 1px solid + ${({ theme }) => theme.sharedTheme.uiToken.colorWarning}66; + padding: 0 16px; + z-index: 100; + + .notice-label { + display: flex; + align-items: center; + gap: 6px; + color: ${({ theme }) => theme.sharedTheme.uiToken.colorWarning}; + font-weight: 600; + font-size: 13px; + white-space: nowrap; + flex-shrink: 0; + padding-right: 12px; + border-right: 1px solid + ${({ theme }) => theme.sharedTheme.uiToken.colorWarning}55; + + .notice-label-icon { + font-size: 14px; + color: ${({ theme }) => theme.sharedTheme.uiToken.colorWarning}; + } + } + + .notice-content-area { + flex: 1; + min-width: 0; + margin-left: 12px; + display: flex; + align-items: center; + gap: 8px; + } + + .notice-text { + flex: 1; + min-width: 0; + color: ${({ theme }) => theme.sharedTheme.uiToken.colorTextBase}; + font-size: 13px; + line-height: 1.5; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .notice-expand-btn { + flex-shrink: 0; + padding: 0 4px; + font-size: 12px; + color: ${({ theme }) => theme.sharedTheme.uiToken.colorPrimary}; + cursor: pointer; + user-select: none; + + &:hover { + text-decoration: underline; + } + } +`; + +export const CompanyNoticeDetailContentStyleWrapper = styled('div')` + max-height: 400px; + overflow-y: auto; + font-size: 13px; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; +`; diff --git a/packages/base/src/page/Nav/SideMenu/MenuList/__snapshots__/index.sqle.test.tsx.snap b/packages/base/src/page/Nav/SideMenu/MenuList/__snapshots__/index.sqle.test.tsx.snap new file mode 100644 index 0000000000..4b7da020fe --- /dev/null +++ b/packages/base/src/page/Nav/SideMenu/MenuList/__snapshots__/index.sqle.test.tsx.snap @@ -0,0 +1,5469 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`base/Nav/SideMenu/MenuList - sqle mode 基础渲染 renders SQLE menu structure - snapshot 1`] = ` + +
+ + + +`; + +exports[`base/Nav/SideMenu/MenuList - sqle mode 权限过滤 hides permission-gated items when checkPagePermission returns false 1`] = ` + +
+ + + +`; + +exports[`base/Nav/SideMenu/MenuList - sqle mode 权限过滤 shows all items when checkPagePermission returns true 1`] = ` + +
+ + + +`; + +exports[`base/Nav/SideMenu/MenuList - sqle mode 权限过滤 shows all items when userOperationPermissions is null (default) - snapshot 1`] = ` + +
+ + + +`; + +exports[`base/Nav/SideMenu/MenuList - sqle mode 路由激活菜单 selects member when at member route 1`] = ` + +
+ + + +`; + +exports[`base/Nav/SideMenu/MenuList - sqle mode 路由激活菜单 selects project-overview when at sqle overview route 1`] = ` + +
+ + + +`; + +exports[`base/Nav/SideMenu/MenuList - sqle mode 路由激活菜单 selects rule-template by walking up nested sub-path 1`] = ` + +
+ + + +`; diff --git a/packages/base/src/page/Nav/SideMenu/MenuList/__snapshots__/index.test.tsx.snap b/packages/base/src/page/Nav/SideMenu/MenuList/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..140f0e1a79 --- /dev/null +++ b/packages/base/src/page/Nav/SideMenu/MenuList/__snapshots__/index.test.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`base/page/Nav/SideMenu/MenuList - dms mode renders empty menu in dms mode - snapshot 1`] = ` + +
+
@@ -470,22 +617,6 @@ exports[`base/page/Nav/SideMenu/UserMenu/CompanyNoticeModal render snap when edi
- -
@@ -719,7 +830,7 @@ exports[`base/page/Nav/SideMenu/UserMenu/CompanyNoticeModal render snap when edi type="button" > - 提 交 + 编 辑
@@ -735,94 +846,26 @@ exports[`base/page/Nav/SideMenu/UserMenu/CompanyNoticeModal render snap when edi
-
-
-
-
- -
-
-
+ +
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+ + + +
+
+ +
+
+ + + + + +
+
+
+
+
+
+
+
@@ -953,33 +1104,9 @@ exports[`base/page/Nav/SideMenu/UserMenu/CompanyNoticeModal render snap when edi style="margin-right: 8px;" >
+ +
+ +
+
+
+ +`; + +exports[`base/page/Nav/SideMenu/UserMenu/CompanyNoticeModal should display notice content and effective period in view mode 1`] = ` + +
+
+
+
+
+
UI: - feature/add-make-command ba9c9c5 + sync/data-masking 417b194dd
UI: - feature/add-make-command ba9c9c5 + sync/data-masking 417b194dd
UI: - feature/add-make-command ba9c9c5 + sync/data-masking 417b194dd
UI: - feature/add-make-command ba9c9c5 + sync/data-masking 417b194dd
UI: - feature/add-make-command ba9c9c5 + sync/data-masking 417b194dd
= ({ } ]; - // #if [ee] - useRequest( - () => - CompanyNotice.GetCompanyNotice().then((res) => { - if (res.data.code === ResponseCode.SUCCESS) { - const { read_by_current_user, notice_str } = res.data.data || {}; - if (notice_str && !read_by_current_user) { - dispatch( - updateNavModalStatus({ - modalName: ModalName.Company_Notice, - status: true - }) - ); - } - } - }), - { - ready: - LocalStorageWrapper.get(StorageKey.SHOW_COMPANY_NOTICE) === - CompanyNoticeDisplayStatusEnum.NotDisplayed - } - ); - // #endif - // TODO 暂时不放开切换主题功能 // const footer = ( // <> diff --git a/packages/base/src/page/Nav/SideMenu/UserMenu/components/__tests__/UserNavigate.ce.test.tsx b/packages/base/src/page/Nav/SideMenu/UserMenu/components/__tests__/UserNavigate.ce.test.tsx index 4472175091..386b1a3880 100644 --- a/packages/base/src/page/Nav/SideMenu/UserMenu/components/__tests__/UserNavigate.ce.test.tsx +++ b/packages/base/src/page/Nav/SideMenu/UserMenu/components/__tests__/UserNavigate.ce.test.tsx @@ -9,11 +9,7 @@ import { act, cleanup, fireEvent, screen } from '@testing-library/react'; import { baseSuperRender } from '../../../../../../testUtils/superRender'; import { getBySelector } from '@actiontech/shared/lib/testUtil/customQuery'; import dms from '@actiontech/shared/lib/testUtil/mockApi/base/global'; -import { LocalStorageWrapper, SupportTheme } from '@actiontech/dms-kit'; -import { - CompanyNoticeDisplayStatusEnum, - SupportLanguage -} from '@actiontech/dms-kit'; +import { SupportTheme, SupportLanguage } from '@actiontech/dms-kit'; import { mockUseUserInfo } from '@actiontech/shared/lib/testUtil/mockHook/mockUseUserInfo'; jest.mock('react-redux', () => { @@ -63,15 +59,9 @@ describe('base/page/Nav/SideMenu/UserNavigate-ce', () => { }); it('render snap', async () => { - jest - .spyOn(LocalStorageWrapper, 'get') - .mockReturnValue(CompanyNoticeDisplayStatusEnum.NotDisplayed); - const requestGetCompanyNotice = dms.getCompanyNotice(); - const { baseElement } = customRender(); expect(baseElement).toMatchSnapshot(); - expect(requestGetCompanyNotice).toHaveBeenCalledTimes(0); const iconUserName = getBySelector('.ant-avatar-string', baseElement); fireEvent.click(iconUserName); await act(async () => jest.advanceTimersByTime(500)); diff --git a/packages/base/src/page/Nav/SideMenu/UserMenu/components/__tests__/UserNavigate.test.tsx b/packages/base/src/page/Nav/SideMenu/UserMenu/components/__tests__/UserNavigate.test.tsx index 1d3f9edc33..a06f5aad16 100644 --- a/packages/base/src/page/Nav/SideMenu/UserMenu/components/__tests__/UserNavigate.test.tsx +++ b/packages/base/src/page/Nav/SideMenu/UserMenu/components/__tests__/UserNavigate.test.tsx @@ -5,12 +5,7 @@ import { act, cleanup, fireEvent, screen } from '@testing-library/react'; import { baseSuperRender } from '../../../../../../testUtils/superRender'; import { getBySelector } from '@actiontech/shared/lib/testUtil/customQuery'; import dms from '@actiontech/shared/lib/testUtil/mockApi/base/global'; -import { LocalStorageWrapper, SupportTheme } from '@actiontech/dms-kit'; -import { ModalName } from '../../../../../../data/ModalName'; -import { - CompanyNoticeDisplayStatusEnum, - SupportLanguage -} from '@actiontech/dms-kit'; +import { SupportTheme, SupportLanguage } from '@actiontech/dms-kit'; import { mockUseUserInfo } from '@actiontech/shared/lib/testUtil/mockHook/mockUseUserInfo'; import account from '@actiontech/shared/lib/testUtil/mockApi/base/account'; import { createSpySuccessResponse } from '@actiontech/shared/lib/testUtil/mockApi'; @@ -35,7 +30,6 @@ describe('base/page/Nav/SideMenu/UserNavigate-ee', () => { const versionModalOpenFn = jest.fn(); const navigateSpy = jest.fn(); const scopeDispatch = jest.fn(); - let requestGetCompanyNotice: jest.SpyInstance; let requestDelSession: jest.SpyInstance; let requestUpdateCurrentUser: jest.SpyInstance; @@ -56,7 +50,7 @@ describe('base/page/Nav/SideMenu/UserNavigate-ee', () => { (useNavigate as jest.Mock).mockImplementation(() => navigateSpy); (useDispatch as jest.Mock).mockImplementation(() => scopeDispatch); jest.useFakeTimers(); - requestGetCompanyNotice = dms.getCompanyNotice(); + dms.getCompanyNotice(); requestDelSession = dms.delSession(); requestUpdateCurrentUser = account.updateCurrentUser(); mockUseUserInfo({ clearUserInfo: mockClearUserInfo }); @@ -69,21 +63,11 @@ describe('base/page/Nav/SideMenu/UserNavigate-ee', () => { }); it('render snap', async () => { - jest - .spyOn(LocalStorageWrapper, 'get') - .mockReturnValue(CompanyNoticeDisplayStatusEnum.NotDisplayed); const { baseElement } = customRender(); - expect(requestGetCompanyNotice).toHaveBeenCalledTimes(1); await act(async () => jest.advanceTimersByTime(3300)); expect(baseElement).toMatchSnapshot(); - expect(scopeDispatch).toHaveBeenCalled(); - expect(scopeDispatch).toHaveBeenCalledWith({ - payload: { modalName: ModalName.Company_Notice, status: true }, - type: 'nav/updateModalStatus' - }); - const iconUserName = getBySelector('.ant-avatar-string', baseElement); fireEvent.click(iconUserName); await act(async () => jest.advanceTimersByTime(500)); diff --git a/packages/base/src/page/Nav/SideMenu/__tests__/__snapshots__/index.ce.test.tsx.snap b/packages/base/src/page/Nav/SideMenu/__tests__/__snapshots__/index.ce.test.tsx.snap index 26403f4045..52e7d2f0a9 100644 --- a/packages/base/src/page/Nav/SideMenu/__tests__/__snapshots__/index.ce.test.tsx.snap +++ b/packages/base/src/page/Nav/SideMenu/__tests__/__snapshots__/index.ce.test.tsx.snap @@ -154,7 +154,817 @@ exports[`test base/Nav/SideMenu/index.ce should match snapshot 1`] = ` data-menu-list="true" role="menu" tabindex="0" - /> + > + +
  • +
  • + + + +
  • +
  • +
  • +
  • +