From eec07faf90cc15671e0e29f56885c542c57c8671 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Thu, 30 Jul 2026 11:19:27 +0800 Subject: [PATCH 1/2] feat: split connectivity and privilege check UI entry points Add dual-entry privilege check flow, batch import/check result columns, and align locale/API types with the new privilege reply shape. --- packages/base/src/data/EmitterKey.ts | 1 + .../src/hooks/useStaticTips/index.data.ts | 5 +- .../base/src/locale/en-US/dmsDataSource.ts | 14 +- packages/base/src/locale/en-US/dmsProject.ts | 12 +- .../base/src/locale/zh-CN/dmsDataSource.ts | 12 +- packages/base/src/locale/zh-CN/dmsProject.ts | 8 +- .../__snapshots__/index.test.tsx.snap | 104 +++- .../components/AddDataSource/index.tsx | 16 +- .../__snapshots__/index.test.tsx.snap | 450 +++++++++++++++++- .../BatchImportDataSource/index.test.tsx | 47 +- .../BatchImportDataSource/index.tsx | 43 +- .../__snapshots__/index.test.tsx.snap | 364 +++++++++++--- .../components/Form/FormItem/index.tsx | 48 +- .../Form/PrivilegeCheckResult/index.tsx | 137 ++++++ .../Form/PrivilegeCheckResult/style.ts | 30 ++ .../Form/__snapshots__/index.test.tsx.snap | 364 +++++++++++--- .../page/DataSource/components/Form/index.tsx | 7 + .../List/ConnectionResultColumn.tsx | 2 + .../DataSource/components/List/columns.tsx | 1 + .../__snapshots__/index.test.tsx.snap | 156 +++++- .../UpdateDataSource/index.test.tsx | 11 +- .../components/UpdateDataSource/index.tsx | 13 + .../base/src/page/DataSource/context/index.ts | 8 +- .../DataSource/hooks/useCheckConnectable.ts | 4 +- .../DataSource/hooks/useCheckPrivileges.ts | 80 ++++ .../__snapshots__/index.test.tsx.snap | 450 +++++++++++++++++- .../__tests__/index.test.tsx | 46 +- .../BatchImportDataSource/index.tsx | 43 +- .../page/GlobalDataSource/List/columns.tsx | 1 + .../src/page/GlobalDataSource/List/index.tsx | 2 + .../BatchImportCheckResult/index.tsx | 84 ++++ .../__snapshots__/index.test.tsx.snap | 450 +++++++++++++++++- .../useBatchCheckConnectable.test.ts | 108 ++++- .../hooks/useBatchCheckConnectable.ts | 130 +++-- .../BatchImportDataSource/index.test.tsx | 46 +- .../Project/BatchImportDataSource/index.tsx | 41 +- .../Project/BatchImportDataSource/style.tsx | 27 ++ .../shared/lib/api/base/service/common.d.ts | 37 ++ .../lib/testUtil/mockApi/base/project/data.ts | 2 + 39 files changed, 3092 insertions(+), 312 deletions(-) create mode 100644 packages/base/src/page/DataSource/components/Form/PrivilegeCheckResult/index.tsx create mode 100644 packages/base/src/page/DataSource/components/Form/PrivilegeCheckResult/style.ts create mode 100644 packages/base/src/page/DataSource/hooks/useCheckPrivileges.ts create mode 100644 packages/base/src/page/Project/BatchImportDataSource/BatchImportCheckResult/index.tsx diff --git a/packages/base/src/data/EmitterKey.ts b/packages/base/src/data/EmitterKey.ts index d4baac55ba..c142edae44 100644 --- a/packages/base/src/data/EmitterKey.ts +++ b/packages/base/src/data/EmitterKey.ts @@ -3,6 +3,7 @@ enum EmitterKey { Reset_Test_Data_Source_Connect = 'DMS_Reset_Test_Data_Source_Connect', DMS_Reset_DataSource_Form = 'DMS_Reset_DataSource_Form', DMS_Submit_DataSource_Form = 'DMS_Submit_DataSource_Form', + DMS_Open_DataSource_Connect_Error_Modal = 'DMS_Open_DataSource_Connect_Error_Modal', DMS_Refresh_Member_List = 'DMS_REFRESH_MEMBER_LIST', DMS_Refresh_Project_List = 'DMS_REFRESH_PROJECT_LIST', DMS_SYNC_TASK_RESET_FORM = 'DMS_SYNC_TASK_RESET_FORM', diff --git a/packages/base/src/hooks/useStaticTips/index.data.ts b/packages/base/src/hooks/useStaticTips/index.data.ts index c61673f82a..b6b6494d72 100644 --- a/packages/base/src/hooks/useStaticTips/index.data.ts +++ b/packages/base/src/hooks/useStaticTips/index.data.ts @@ -5,12 +5,13 @@ type IStaticEnumDictionary = { [key in T]: string; }; +/** AC-012 / S1 §5.6:过滤选项仅连通三态文案,与列表列「连通性测试成功/失败」对齐;不含权限语义 */ export const databaseTestConnectionStatusDictionary: IStaticEnumDictionary = { [ListDBServicesFilterLastConnectionTestStatusEnum.connect_failed]: t( - 'dmsDataSource.batchTestConnection.connectFailed' + 'dmsDataSource.databaseList.connectFailed' ), [ListDBServicesFilterLastConnectionTestStatusEnum.connect_success]: t( - 'dmsDataSource.batchTestConnection.connectSucceed' + 'dmsDataSource.databaseList.connectSucceed' ) }; diff --git a/packages/base/src/locale/en-US/dmsDataSource.ts b/packages/base/src/locale/en-US/dmsDataSource.ts index 75ccb3c1bc..36c650aa36 100644 --- a/packages/base/src/locale/en-US/dmsDataSource.ts +++ b/packages/base/src/locale/en-US/dmsDataSource.ts @@ -76,7 +76,19 @@ export default { 'Whether to enable data mask configuration for SQL workbench', dataMaskConfigTips: 'After enabling, the query results of the SQL workbench will be masked', - checkDataMaskButton: 'View data mask rule' + checkDataMaskButton: 'View data mask rule', + checkPrivileges: 'Check feature privileges', + privilegeResultTitle: 'Feature privilege check result', + privilegeStatusAvailable: 'Available', + privilegeStatusPartial: 'Partially available', + privilegeStatusUnavailable: 'Unavailable', + privilegeStatusUnsupported: 'Auto check not supported', + privilegeUnsupportedAutoCheck: 'Auto check not supported', + privilegeNotBlockSave: + 'Insufficient feature privileges do not block create/save', + privilegePrecheckFailed: 'Privilege check pre-connection failed', + privilegePrecheckHint: + 'To verify connectivity, use "Test data source connectivity"' }, testConnectModal: { diff --git a/packages/base/src/locale/en-US/dmsProject.ts b/packages/base/src/locale/en-US/dmsProject.ts index c57fe2f7bf..09842b36aa 100644 --- a/packages/base/src/locale/en-US/dmsProject.ts +++ b/packages/base/src/locale/en-US/dmsProject.ts @@ -79,7 +79,17 @@ export default { testConnectFail: 'Test connectivity failed {{count}}, data source is {{name}}', requestAuditErrorMessage: - 'The current import information has validation failures, please modify it according to the hints in the downloaded file, and import again' + 'The current import information has validation failures, please modify it according to the hints in the downloaded file, and import again', + dataSourceConnectError: 'Data source connectivity test failed', + returnModify: 'Return to modify', + continueSubmit: 'Continue to submit', + checkResultTitle: 'Pre-import check result', + connectivityColumn: 'Connectivity result', + privilegeColumn: 'Feature privilege result', + connectivitySuccess: 'Connectivity test succeeded', + connectivityFailed: 'Connectivity test failed', + privilegeNotBlockImport: + 'Insufficient feature privileges do not block import' }, backToList: 'Back to project list', businessDescription: { diff --git a/packages/base/src/locale/zh-CN/dmsDataSource.ts b/packages/base/src/locale/zh-CN/dmsDataSource.ts index 1d01c5bc91..0caad04e3b 100644 --- a/packages/base/src/locale/zh-CN/dmsDataSource.ts +++ b/packages/base/src/locale/zh-CN/dmsDataSource.ts @@ -105,7 +105,17 @@ export default { lineNumberLimitTips: '当预计影响行数超过指定值则不回滚', dataSourceConnectError: '数据源连通性测试失败', returnModify: '返回修改', - continueSubmit: '继续提交' + continueSubmit: '继续提交', + checkPrivileges: '检查功能权限', + privilegeResultTitle: '功能权限检查结果', + privilegeStatusAvailable: '可用', + privilegeStatusPartial: '部分可用', + privilegeStatusUnavailable: '不可用', + privilegeStatusUnsupported: '暂不支持自动检查', + privilegeUnsupportedAutoCheck: '暂不支持自动检查', + privilegeNotBlockSave: '功能权限不足不影响创建/保存', + privilegePrecheckFailed: '权限检查前置连接失败', + privilegePrecheckHint: '如需验证连通性,请使用「测试数据源连通性」' }, testConnectModal: { diff --git a/packages/base/src/locale/zh-CN/dmsProject.ts b/packages/base/src/locale/zh-CN/dmsProject.ts index 8fd1f3f4d4..a0ea1817a6 100644 --- a/packages/base/src/locale/zh-CN/dmsProject.ts +++ b/packages/base/src/locale/zh-CN/dmsProject.ts @@ -80,7 +80,13 @@ export default { '当前导入信息存在校验失败,请结合下载文件中的提示进行修改,并重新导入', dataSourceConnectError: '数据源连通性测试失败', returnModify: '返回修改', - continueSubmit: '继续提交' + continueSubmit: '继续提交', + checkResultTitle: '导入前校验结果', + connectivityColumn: '连通结果', + privilegeColumn: '功能权限结果', + connectivitySuccess: '连通性测试成功', + connectivityFailed: '连通性测试失败', + privilegeNotBlockImport: '功能权限不足不影响导入' }, backToList: '返回项目列表', businessDescription: { 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 7c86bca849..4fd22761c2 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 @@ -582,19 +582,55 @@ exports[`page/DataSource/AddDataSource render add database snap 1`] = ` >
- +
+
+
+ +
+
+
+
+
+ +
+
- +
+
+
+ +
+
+
+
+
+ +
+
{ const { t } = useTranslation(); const navigate = useTypedNavigate(); const [form] = useForm(); const { onCheckConnectable, loading, connectAble, connectErrorMessage } = useCheckConnectable(form); + const { + privilegeLoading, + privilegeResult, + privilegeChecked, + onCheckPrivileges, + resetPrivilegeResult + } = useCheckPrivileges(form); const [resultVisible, { setTrue: showResult, setFalse: hideResult }] = useBoolean(); const [submitLoading, { setTrue: startSubmit, setFalse: submitFinish }] = @@ -94,8 +102,9 @@ const AddDataSource = () => { const resetAndHideResult = useCallback(() => { hideResult(); form.resetFields(); + resetPrivilegeResult(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form]); + }, [form, resetPrivilegeResult]); const onReset = () => { EventEmitter.emit(EmitterKey.DMS_Reset_DataSource_Form); }; @@ -110,6 +119,11 @@ const AddDataSource = () => { connectAble, connectErrorMessage, onCheckConnectable, + privilegeLoading, + privilegeResult, + privilegeChecked, + onCheckPrivileges, + resetPrivilegeResult, submitLoading }} > 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 c5ee8a600c..751f743dc5 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 @@ -289,6 +289,209 @@ exports[`base/DataSource/BatchImportDataSource render connectable error modal 1`
+
+ + + 导入前校验结果 + + +
+ + 连通结果 + +
+
+
+ + mysql_1 + +
+
+
+
+ + + + + +
+
+ +
+                      sqle: connection refused 
+                    
+
+
+
+
+
+
+
+
+
+ + oracle_1 + +
+
+
+
+ + + + + +
+
+ +
+                      sqle: connection refused 
+                    
+
+
+
+
+
+
+
+
+ + 功能权限结果 + +
+ + mysql_1 + +
+ + + 功能权限检查结果 + + +
+ + 功能权限不足不影响创建/保存 + +
+
+
+ + oracle_1 + +
+ + + 功能权限检查结果 + + +
+ + 功能权限不足不影响创建/保存 + +
+
+
+
-                          slqe: 权限不足 
-
+                          sqle: connection refused 
+                        
+ +
+
+
+
+
+ oracle_1 +
+
+
+ + + + + +
+
+ +
+                          sqle: connection refused 
                         
@@ -1068,6 +1311,209 @@ exports[`base/DataSource/BatchImportDataSource render upload file 1`] = `
+
+ + + 导入前校验结果 + + +
+ + 连通结果 + +
+
+
+ + mysql_1 + +
+
+
+
+ + + + + +
+
+ +
+                      连通性测试成功
+                    
+
+
+
+
+
+
+
+
+
+ + oracle_1 + +
+
+
+
+ + + + + +
+
+ +
+                      连通性测试成功
+                    
+
+
+
+
+
+
+
+
+ + 功能权限结果 + +
+ + mysql_1 + +
+ + + 功能权限检查结果 + + +
+ + 功能权限不足不影响创建/保存 + +
+
+
+ + oracle_1 + +
+ + + 功能权限检查结果 + + +
+ + 功能权限不足不影响创建/保存 + +
+
+
+
`; diff --git a/packages/base/src/page/DataSource/components/BatchImportDataSource/index.test.tsx b/packages/base/src/page/DataSource/components/BatchImportDataSource/index.test.tsx index 6e88c73fb4..5db65cb0ef 100644 --- a/packages/base/src/page/DataSource/components/BatchImportDataSource/index.test.tsx +++ b/packages/base/src/page/DataSource/components/BatchImportDataSource/index.test.tsx @@ -1,8 +1,6 @@ import project from '@actiontech/shared/lib/testUtil/mockApi/base/project'; -import { - mockBatchImportDBCheckData, - mockCheckDBServicesPrivilegesIncludeErrorData -} from '@actiontech/shared/lib/testUtil/mockApi/base/project/data'; +import dbServices from '@actiontech/shared/lib/testUtil/mockApi/base/dbServices'; +import { mockBatchImportDBCheckData } from '@actiontech/shared/lib/testUtil/mockApi/base/project/data'; import { baseSuperRender } from '../../../../testUtils/superRender'; import ProjectImport from '.'; import { act, cleanup, fireEvent, screen } from '@testing-library/react'; @@ -17,11 +15,16 @@ import { AxiosResponse } from 'axios'; import { mockUseCurrentProject } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentProject'; import { mockProjectInfo } from '@actiontech/shared/lib/testUtil/mockHook/data'; +const connectableSuccessReply = [ + { component: 'sqle', is_connectable: true }, + { component: 'provision', is_connectable: true } +]; + describe('base/DataSource/BatchImportDataSource', () => { let importDBServicesOfOneProjectSpy: jest.SpyInstance; let getImportDBServicesTemplateSpy: jest.SpyInstance; let importDBServicesOfOneProjectCheckSpy: jest.SpyInstance; - let dbServicesConnectionSpy: jest.SpyInstance; + let checkDbServiceIsConnectableSpy: jest.SpyInstance; let checkDBServicesPrivilegesSpy: jest.SpyInstance; beforeEach(() => { @@ -30,7 +33,12 @@ describe('base/DataSource/BatchImportDataSource', () => { getImportDBServicesTemplateSpy = project.getImportDBServicesTemplate(); importDBServicesOfOneProjectCheckSpy = project.importDBServicesOfOneProjectCheck(); - dbServicesConnectionSpy = project.dbServicesConnection(); + checkDbServiceIsConnectableSpy = dbServices.checkDbServiceIsConnectable(); + checkDbServiceIsConnectableSpy.mockImplementation(() => + createSpySuccessResponse({ + data: connectableSuccessReply + }) + ); checkDBServicesPrivilegesSpy = project.checkDBServicesPrivileges(); mockUseCurrentProject(); }); @@ -136,14 +144,17 @@ describe('base/DataSource/BatchImportDataSource', () => { expect(screen.getByText('导 入').closest('button')).toHaveClass( 'ant-btn-loading' ); + // connect API then privilege API (each createSpySuccessResponse waits 3000ms) + await act(async () => jest.advanceTimersByTimeAsync(3000)); + expect(checkDbServiceIsConnectableSpy).toHaveBeenCalled(); expect(checkDBServicesPrivilegesSpy).toHaveBeenCalledTimes(1); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(importDBServicesOfOneProjectSpy).toHaveBeenCalledTimes(1); expect(importDBServicesOfOneProjectSpy).toHaveBeenNthCalledWith(1, { db_services: mockBatchImportDBCheckData, project_uid: mockProjectInfo.projectID }); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(screen.getByText('批量导入数据源成功')).toBeInTheDocument(); expect(baseElement).toMatchSnapshot(); fireEvent.click(screen.getByText('关闭并重置表单')); @@ -172,10 +183,11 @@ describe('base/DataSource/BatchImportDataSource', () => { expect(screen.getByText('导 入').closest('button')).toHaveClass( 'ant-btn-loading' ); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(checkDBServicesPrivilegesSpy).toHaveBeenCalledTimes(1); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(importDBServicesOfOneProjectSpy).toHaveBeenCalledTimes(1); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(screen.getByText('导 入').closest('button')).not.toHaveClass( 'ant-btn-loading' ); @@ -183,9 +195,15 @@ describe('base/DataSource/BatchImportDataSource', () => { }); it('render connectable error modal', async () => { - checkDBServicesPrivilegesSpy.mockImplementation(() => + checkDbServiceIsConnectableSpy.mockImplementation(() => createSpySuccessResponse({ - data: mockCheckDBServicesPrivilegesIncludeErrorData + data: [ + { + component: 'sqle', + is_connectable: false, + connect_error_message: 'connection refused' + } + ] }) ); const { baseElement } = baseSuperRender(); @@ -204,8 +222,9 @@ describe('base/DataSource/BatchImportDataSource', () => { expect(screen.getByText('导 入').closest('button')).toHaveClass( 'ant-btn-loading' ); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(checkDBServicesPrivilegesSpy).toHaveBeenCalledTimes(1); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(screen.getByText('数据源连通性测试失败')).toBeInTheDocument(); expect(baseElement).toMatchSnapshot(); fireEvent.click(screen.getByText('继续提交')); @@ -214,7 +233,7 @@ describe('base/DataSource/BatchImportDataSource', () => { db_services: mockBatchImportDBCheckData, project_uid: mockProjectInfo.projectID }); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(screen.getByText('批量导入数据源成功')).toBeInTheDocument(); }); }); diff --git a/packages/base/src/page/DataSource/components/BatchImportDataSource/index.tsx b/packages/base/src/page/DataSource/components/BatchImportDataSource/index.tsx index edc794428a..44f8ea7a22 100644 --- a/packages/base/src/page/DataSource/components/BatchImportDataSource/index.tsx +++ b/packages/base/src/page/DataSource/components/BatchImportDataSource/index.tsx @@ -17,6 +17,8 @@ import { ROUTE_PATHS } from '@actiontech/dms-kit'; import { DmsApi } from '@actiontech/shared/lib/api'; import ConnectableErrorModal from '../../../Project/BatchImportDataSource/ConnectableErrorModal'; import useBatchCheckConnectable from '../../../Project/BatchImportDataSource/hooks/useBatchCheckConnectable'; +import BatchImportCheckResult from '../../../Project/BatchImportDataSource/BatchImportCheckResult'; + const BatchImportDataSource = () => { const { t } = useTranslation(); const { projectID } = useCurrentProject(); @@ -41,7 +43,7 @@ const BatchImportDataSource = () => { showConnectErrorModal, hideConnectErrorModal, connectableInfo - } = useBatchCheckConnectable(); + } = useBatchCheckConnectable(projectID); const onSubmit = async () => { setImportPending(); DmsApi.DBServiceService.ImportDBServicesOfOneProjectV2({ @@ -93,6 +95,15 @@ const BatchImportDataSource = () => { }, [importServicesCheck, projectID, setDBservices, clearUploadCheckStatus] ); + + const checkResultNode = ( + + ); + return ( <> { - {t('common.resetAndClose')} - - ]} - /> + <> + + {t('common.resetAndClose')} + + ]} + /> + {checkResultNode} + } > { uploadCheckStatus={uploadCheckStatus} clearUploadCheckStatus={clearUploadCheckStatus} /> + {checkResultNode}
- +
+
+
+ +
+
+
+
+
+ +
+
- +
+
+
+ +
+
+
+
+
+ +
+
- +
+
+
+ +
+
+
+
+
+ +
+
- +
+
+
+ +
+
+
+
+
+ +
+
- +
+
+
+ +
+
+
+
+
+ +
+
- +
+
+
+ +
+
+
+
+
+ +
+
- +
+
+
+ +
+
+
+
+
+ +
+
; isUpdate?: boolean; @@ -42,13 +44,25 @@ const DatabaseFormItem: React.FC<{ }); }; const testDatabaseConnect = async () => { - formContext?.onCheckConnectable(props.currentAsyncParams).finally(() => { + try { + const isConnectable = await formContext?.onCheckConnectable( + props.currentAsyncParams + ); + // S1 §5.2.3:连通 API 失败须打开「数据源连通性测试失败」Modal(返回修改 / 继续提交) + if (isConnectable === false) { + EventEmitter.emit(EmitterKey.DMS_Open_DataSource_Connect_Error_Modal); + } + } finally { setConnectionInfoShow(); - }); + } + }; + const checkPrivileges = async () => { + await formContext?.onCheckPrivileges(props.currentAsyncParams); }; useEffect(() => { const resetConnectAbleStatus = () => { setConnectionInfoHide(); + formContext?.resetPrivilegeResult(); }; EventEmitter.subscribe( EmitterKey.Reset_Test_Data_Source_Connect, @@ -193,13 +207,27 @@ const DatabaseFormItem: React.FC<{ - + + + + + {t('dmsDataSource.dataSourceForm.checkPrivileges')} + + + + ); diff --git a/packages/base/src/page/DataSource/components/Form/PrivilegeCheckResult/index.tsx b/packages/base/src/page/DataSource/components/Form/PrivilegeCheckResult/index.tsx new file mode 100644 index 0000000000..2e5d093c48 --- /dev/null +++ b/packages/base/src/page/DataSource/components/Form/PrivilegeCheckResult/index.tsx @@ -0,0 +1,137 @@ +import { useMemo } from 'react'; +import { Space, Typography } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { BasicTag, EmptyBox, ReminderInformation } from '@actiontech/dms-kit'; +import { BasicTagColor } from '@actiontech/dms-kit/es/theme/theme.type'; +import { + ICheckDBServicePrivilegeModule, + ICheckDBServicesPrivilegesItem +} from '@actiontech/shared/lib/api/base/service/common'; +import { PrivilegeCheckResultStyleWrapper } from './style'; + +type PrivilegeCheckResultProps = { + result: ICheckDBServicesPrivilegesItem | null; + visible: boolean; +}; + +const STATUS_TAG: Record = { + available: { + color: 'green', + i18nKey: 'dmsDataSource.dataSourceForm.privilegeStatusAvailable' + }, + partially_available: { + color: 'orange', + i18nKey: 'dmsDataSource.dataSourceForm.privilegeStatusPartial' + }, + unavailable: { + color: 'red', + i18nKey: 'dmsDataSource.dataSourceForm.privilegeStatusUnavailable' + }, + unsupported_auto_check: { + color: 'default', + i18nKey: 'dmsDataSource.dataSourceForm.privilegeStatusUnsupported' + } +}; + +const ModuleRow: React.FC<{ module: ICheckDBServicePrivilegeModule }> = ({ + module +}) => { + const { t } = useTranslation(); + const statusMeta = + STATUS_TAG[module.status ?? ''] ?? STATUS_TAG.unsupported_auto_check; + const missing = module.missing_privileges ?? []; + + return ( +
+ + {module.module_name || module.module} + + {t(statusMeta.i18nKey)} + + + 0 + } + > + + {missing + .map((item) => + item.object_scope + ? `${item.privilege} (${item.object_scope})` + : item.privilege + ) + .filter(Boolean) + .join('、')} + + + + + {module.message} + + +
+ ); +}; + +const PrivilegeCheckResult: React.FC = ({ + result, + visible +}) => { + const { t } = useTranslation(); + + const isUnsupported = result?.check_support === 'unsupported_auto_check'; + const precheckFailed = result?.connectivity_precheck?.ok === false; + const modules = useMemo(() => result?.modules ?? [], [result?.modules]); + + if (!visible || !result) { + return null; + } + + return ( + + + {t('dmsDataSource.dataSourceForm.privilegeResultTitle')} + + + + + + {t('dmsDataSource.dataSourceForm.privilegePrecheckHint')} + + + + + + {t('dmsDataSource.dataSourceForm.privilegeUnsupportedAutoCheck')} + + + + +
+ {modules.map((module) => ( + + ))} +
+
+ + + {result.summary_message || + t('dmsDataSource.dataSourceForm.privilegeNotBlockSave')} + +
+ ); +}; + +export default PrivilegeCheckResult; diff --git a/packages/base/src/page/DataSource/components/Form/PrivilegeCheckResult/style.ts b/packages/base/src/page/DataSource/components/Form/PrivilegeCheckResult/style.ts new file mode 100644 index 0000000000..cd3051737a --- /dev/null +++ b/packages/base/src/page/DataSource/components/Form/PrivilegeCheckResult/style.ts @@ -0,0 +1,30 @@ +import { styled } from '@mui/material/styles'; + +export const PrivilegeCheckResultStyleWrapper = styled('div')` + margin-top: 12px; + padding: 12px; + border-radius: 4px; + border: 1px solid ${({ theme }) => theme.sharedTheme.basic.colorGrayLine}; + background-color: ${({ theme }) => theme.sharedTheme.uiToken.colorBgBase}; + display: flex; + flex-direction: column; + gap: 8px; + + .privilege-module-list { + display: flex; + flex-direction: column; + gap: 8px; + } + + .privilege-module-row { + display: flex; + flex-direction: column; + gap: 2px; + } + + .privilege-missing, + .privilege-module-msg, + .privilege-summary { + font-size: 12px; + } +`; 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 acb7cd7109..ddf91599c1 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 @@ -628,19 +628,55 @@ exports[`page/DataSource/DataSourceForm render cancel rule 1`] = ` >
- +
+
+
+ +
+
+
+
+
+ +
+
- +
+
+
+ +
+
+
+
+
+ +
+
- +
+
+
+ +
+
+
+
+
+ +
+
- +
+
+
+ +
+
+
+
+
+ +
+
- +
+
+
+ +
+
+
+
+
+ +
+
- +
+
+
+ +
+
+
+
+
+ +
+
- +
+
+
+ +
+
+
+
+
+ +
+
= (props) => { ); return unsubscribeSubmit; }, [onCheckConnectableBeforeSubmit]); + useEffect(() => { + const { unsubscribe } = EventEmitter.subscribe( + EmitterKey.DMS_Open_DataSource_Connect_Error_Modal, + openModal + ); + return unsubscribe; + }, [openModal]); useEffect(() => { updateDriverList(); updateProjects(); diff --git a/packages/base/src/page/DataSource/components/List/ConnectionResultColumn.tsx b/packages/base/src/page/DataSource/components/List/ConnectionResultColumn.tsx index be551c036e..ab1f51ef09 100644 --- a/packages/base/src/page/DataSource/components/List/ConnectionResultColumn.tsx +++ b/packages/base/src/page/DataSource/components/List/ConnectionResultColumn.tsx @@ -4,10 +4,12 @@ import { formatTime } from '@actiontech/dms-kit'; import { Space, Typography } from 'antd'; import { ListDBServiceV2LastConnectionTestStatusEnum } from '@actiontech/shared/lib/api/base/service/common.enum'; type Props = { + /** 仅接受 list API 的 last_connection_test_status(连通三态);勿传入权限结果 */ connectionStatus?: ListDBServiceV2LastConnectionTestStatusEnum; connectionTestTime?: string; connectionErrorMessage?: string; }; +/** 列表「上一次连接状态」:只渲染连通写回三态(AC-012) */ const ConnectionStatusColumn: React.FC = ({ connectionStatus, connectionTestTime, diff --git a/packages/base/src/page/DataSource/components/List/columns.tsx b/packages/base/src/page/DataSource/components/List/columns.tsx index a3be10105c..5ca6cab513 100644 --- a/packages/base/src/page/DataSource/components/List/columns.tsx +++ b/packages/base/src/page/DataSource/components/List/columns.tsx @@ -42,6 +42,7 @@ export const dataSourceColumns = ( return `${record.host}:${record.port}`; } }, + // AC-012 / S1 §5.6:只读连通写回字段;权限检查结果不得驱动本列 { dataIndex: 'last_connection_test_status', title: () => t('dmsDataSource.databaseList.lastTestConnectionStatus'), 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 6dfa9768e8..13cc896183 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 @@ -619,19 +619,55 @@ exports[`page/DataSource/UpdateDataSource render edit database snap 1`] = ` >
- +
+
+
+ +
+
+
+
+
+ +
+
- +
+
+
+ +
+
+
+
+
+ +
+
- +
+
+
+ +
+
+
+
+
+ +
+
{ }); await act(async () => jest.advanceTimersByTime(3000)); - expect(screen.queryByText('provision: 链接失败')).toBeInTheDocument(); + expect(screen.queryAllByText('provision: 链接失败').length).toBeGreaterThan( + 0 + ); + expect(screen.getByText('数据源连通性测试失败')).toBeInTheDocument(); expect(checkDbServiceIsConnectableSpy).toHaveBeenCalledTimes(1); + // 关闭测试连通性失败 Modal,继续改表单 + fireEvent.click(screen.getByText('返回修改')); + await act(async () => jest.advanceTimersByTime(300)); await act(async () => { EventEmitter.emit(EmitterKey.Reset_Test_Data_Source_Connect); await act(async () => jest.advanceTimersByTime(300)); }); - expect(screen.queryByText('provision: 链接失败')).not.toBeInTheDocument(); // environment fireEvent.click(getBySelector('.editable-select-trigger', baseElement)); await act(async () => jest.advanceTimersByTime(0)); @@ -347,7 +352,7 @@ describe('page/DataSource/UpdateDataSource', () => { EmitterKey.DMS_Submit_DataSource_Form ); await act(async () => jest.advanceTimersByTime(3000)); - expect(screen.queryByText('provision: 链接失败')).toBeInTheDocument(); + expect(screen.getByText('继续提交')).toBeInTheDocument(); fireEvent.click(screen.getByText('继续提交')); await act(async () => jest.advanceTimersByTime(3000)); expect(updateDBServiceSpy).toHaveBeenCalledTimes(1); diff --git a/packages/base/src/page/DataSource/components/UpdateDataSource/index.tsx b/packages/base/src/page/DataSource/components/UpdateDataSource/index.tsx index eb9d65694b..67b96d1ec7 100644 --- a/packages/base/src/page/DataSource/components/UpdateDataSource/index.tsx +++ b/packages/base/src/page/DataSource/components/UpdateDataSource/index.tsx @@ -19,6 +19,7 @@ import { ROUTE_PATHS } from '@actiontech/dms-kit'; import { DmsApi } from '@actiontech/shared/lib/api'; import { DataSourceFormContextProvide } from '../../context'; import useCheckConnectable from '../../hooks/useCheckConnectable'; +import useCheckPrivileges from '../../hooks/useCheckPrivileges'; const UpdateDataSource = () => { const { t } = useTranslation(); const navigate = useTypedNavigate(); @@ -30,6 +31,13 @@ const UpdateDataSource = () => { useTypedParams(); const { onCheckConnectable, loading, connectAble, connectErrorMessage } = useCheckConnectable(form); + const { + privilegeLoading, + privilegeResult, + privilegeChecked, + onCheckPrivileges, + resetPrivilegeResult + } = useCheckPrivileges(form); const [retryLoading, setRetryLoading] = useState(false); const [submitLoading, { setTrue: startSubmit, setFalse: submitFinish }] = useBoolean(); @@ -152,6 +160,11 @@ const UpdateDataSource = () => { loading, connectAble, connectErrorMessage, + privilegeLoading, + privilegeResult, + privilegeChecked, + onCheckPrivileges, + resetPrivilegeResult, submitLoading }} > diff --git a/packages/base/src/page/DataSource/context/index.ts b/packages/base/src/page/DataSource/context/index.ts index 766cf6bf46..e74716819b 100644 --- a/packages/base/src/page/DataSource/context/index.ts +++ b/packages/base/src/page/DataSource/context/index.ts @@ -1,9 +1,11 @@ import { createContext } from 'react'; import useCheckConnectable from '../hooks/useCheckConnectable'; +import useCheckPrivileges from '../hooks/useCheckPrivileges'; -type DataSourceFormContextType = ReturnType & { - submitLoading: boolean; -}; +type DataSourceFormContextType = ReturnType & + ReturnType & { + submitLoading: boolean; + }; const DataSourceFormContext = createContext( null diff --git a/packages/base/src/page/DataSource/hooks/useCheckConnectable.ts b/packages/base/src/page/DataSource/hooks/useCheckConnectable.ts index 7916055d64..76d404a5fa 100644 --- a/packages/base/src/page/DataSource/hooks/useCheckConnectable.ts +++ b/packages/base/src/page/DataSource/hooks/useCheckConnectable.ts @@ -22,7 +22,9 @@ const useCheckConnectable = (form: FormInstance) => { const onCheckConnectable = useCallback( async (currentAsyncParams?: BackendFormItemParams[]) => { + // 含 project:全局添加页无 currentProject,须先选「所属项目」再挂 project_uid const values = await form.validateFields([ + 'project', 'ip', 'password', 'port', @@ -49,7 +51,7 @@ const useCheckConnectable = (form: FormInstance) => { password: values.password, additional_params: values.asyncParams ?? [] }, - project_uid: projectID + project_uid: values.project || projectID }) .then((res) => { if (res.data.code === ResponseCode.SUCCESS) { diff --git a/packages/base/src/page/DataSource/hooks/useCheckPrivileges.ts b/packages/base/src/page/DataSource/hooks/useCheckPrivileges.ts new file mode 100644 index 0000000000..315a7fb420 --- /dev/null +++ b/packages/base/src/page/DataSource/hooks/useCheckPrivileges.ts @@ -0,0 +1,80 @@ +import { useCallback, useState } from 'react'; +import { FormInstance } from 'antd'; +import { DataSourceFormField } from '../components/Form/index.type'; +import { useBoolean } from 'ahooks'; +import { DmsApi } from '@actiontech/shared/lib/api/'; +import { ResponseCode } from '@actiontech/dms-kit'; +import { useAsyncParams, BackendFormItemParams } from '@actiontech/shared'; +import { ICheckDBServicesPrivilegesItem } from '@actiontech/shared/lib/api/base/service/common'; + +const useCheckPrivileges = (form: FormInstance) => { + const [loading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = + useBoolean(); + const [privilegeResult, setPrivilegeResult] = + useState(null); + const [privilegeChecked, setPrivilegeChecked] = useState(false); + + const { mergeFromValueIntoParams } = useAsyncParams(); + + const onCheckPrivileges = useCallback( + async (currentAsyncParams?: BackendFormItemParams[]) => { + const values = await form.validateFields([ + 'ip', + 'password', + 'port', + 'user', + 'type', + 'params' + ]); + + if (values.params && currentAsyncParams) { + values.asyncParams = mergeFromValueIntoParams( + values.params, + currentAsyncParams + ).map((v) => ({ name: v.key, value: v.value })); + delete values.params; + } + + setLoadingTrue(); + return DmsApi.ProjectService.CheckDBServicesPrivileges({ + db_services: [ + { + host: values.ip, + port: `${values.port}`, + user: values.user, + db_type: values.type, + password: values.password, + additional_params: values.asyncParams ?? [] + } + ] + }) + .then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + const result = res.data.data?.[0] ?? null; + setPrivilegeResult(result); + setPrivilegeChecked(true); + return result; + } + }) + .finally(() => { + setLoadingFalse(); + }); + }, + [form, mergeFromValueIntoParams, setLoadingTrue, setLoadingFalse] + ); + + const resetPrivilegeResult = useCallback(() => { + setPrivilegeResult(null); + setPrivilegeChecked(false); + }, []); + + return { + privilegeLoading: loading, + privilegeResult, + privilegeChecked, + onCheckPrivileges, + resetPrivilegeResult + }; +}; + +export default useCheckPrivileges; diff --git a/packages/base/src/page/GlobalDataSource/BatchImportDataSource/__tests__/__snapshots__/index.test.tsx.snap b/packages/base/src/page/GlobalDataSource/BatchImportDataSource/__tests__/__snapshots__/index.test.tsx.snap index f1b080ac13..1345a306e8 100644 --- a/packages/base/src/page/GlobalDataSource/BatchImportDataSource/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/GlobalDataSource/BatchImportDataSource/__tests__/__snapshots__/index.test.tsx.snap @@ -575,6 +575,209 @@ exports[`base/GlobalDataSource/BatchImportDataSource render connectable error mo
+
+ + + 导入前校验结果 + + +
+ + 连通结果 + +
+
+
+ + mysql_1 + +
+
+
+
+ + + + + +
+
+ +
+                      sqle: connection refused 
+                    
+
+
+
+
+
+
+
+
+
+ + oracle_1 + +
+
+
+
+ + + + + +
+
+ +
+                      sqle: connection refused 
+                    
+
+
+
+
+
+
+
+
+ + 功能权限结果 + +
+ + mysql_1 + +
+ + + 功能权限检查结果 + + +
+ + 功能权限不足不影响创建/保存 + +
+
+
+ + oracle_1 + +
+ + + 功能权限检查结果 + + +
+ + 功能权限不足不影响创建/保存 + +
+
+
+
-                          slqe: 权限不足 
-
+                          sqle: connection refused 
+                        
+ +
+
+
+
+
+ oracle_1 +
+
+
+ + + + + +
+
+ +
+                          sqle: connection refused 
                         
@@ -1599,6 +1842,209 @@ exports[`base/GlobalDataSource/BatchImportDataSource render upload file 1`] = `
+
+ + + 导入前校验结果 + + +
+ + 连通结果 + +
+
+
+ + mysql_1 + +
+
+
+
+ + + + + +
+
+ +
+                      连通性测试成功
+                    
+
+
+
+
+
+
+
+
+
+ + oracle_1 + +
+
+
+
+ + + + + +
+
+ +
+                      连通性测试成功
+                    
+
+
+
+
+
+
+
+
+ + 功能权限结果 + +
+ + mysql_1 + +
+ + + 功能权限检查结果 + + +
+ + 功能权限不足不影响创建/保存 + +
+
+
+ + oracle_1 + +
+ + + 功能权限检查结果 + + +
+ + 功能权限不足不影响创建/保存 + +
+
+
+
`; diff --git a/packages/base/src/page/GlobalDataSource/BatchImportDataSource/__tests__/index.test.tsx b/packages/base/src/page/GlobalDataSource/BatchImportDataSource/__tests__/index.test.tsx index e47d6a3dfc..d273421ee5 100644 --- a/packages/base/src/page/GlobalDataSource/BatchImportDataSource/__tests__/index.test.tsx +++ b/packages/base/src/page/GlobalDataSource/BatchImportDataSource/__tests__/index.test.tsx @@ -1,8 +1,6 @@ import project from '@actiontech/shared/lib/testUtil/mockApi/base/project'; -import { - mockBatchImportDBCheckData, - mockCheckDBServicesPrivilegesIncludeErrorData -} from '@actiontech/shared/lib/testUtil/mockApi/base/project/data'; +import dbServices from '@actiontech/shared/lib/testUtil/mockApi/base/dbServices'; +import { mockBatchImportDBCheckData } from '@actiontech/shared/lib/testUtil/mockApi/base/project/data'; import { baseSuperRender } from '../../../../testUtils/superRender'; import GlobalBatchImportDataSource from '../'; import { act, cleanup, fireEvent, screen } from '@testing-library/react'; @@ -15,11 +13,16 @@ import Project from '@actiontech/shared/lib/api/base/service/Project'; import { AxiosResponse } from 'axios'; import 'blob-polyfill'; +const connectableSuccessReply = [ + { component: 'sqle', is_connectable: true }, + { component: 'provision', is_connectable: true } +]; + describe('base/GlobalDataSource/BatchImportDataSource', () => { let importDBServicesOfProjectsSpy: jest.SpyInstance; let getImportDBServicesTemplateSpy: jest.SpyInstance; let importDBServicesOfProjectsCheckSpy: jest.SpyInstance; - let dbServicesConnectionSpy: jest.SpyInstance; + let checkDbServiceIsConnectableSpy: jest.SpyInstance; let checkDBServicesPrivilegesSpy: jest.SpyInstance; beforeEach(() => { @@ -28,7 +31,12 @@ describe('base/GlobalDataSource/BatchImportDataSource', () => { getImportDBServicesTemplateSpy = project.getImportDBServicesTemplate(); importDBServicesOfProjectsCheckSpy = project.importDBServicesOfProjectsCheck(); - dbServicesConnectionSpy = project.dbServicesConnection(); + checkDbServiceIsConnectableSpy = dbServices.checkDbServiceIsConnectable(); + checkDbServiceIsConnectableSpy.mockImplementation(() => + createSpySuccessResponse({ + data: connectableSuccessReply + }) + ); checkDBServicesPrivilegesSpy = project.checkDBServicesPrivileges(); }); @@ -135,13 +143,15 @@ describe('base/GlobalDataSource/BatchImportDataSource', () => { expect(screen.getByText('导 入').closest('button')).toHaveClass( 'ant-btn-loading' ); + await act(async () => jest.advanceTimersByTimeAsync(3000)); + expect(checkDbServiceIsConnectableSpy).toHaveBeenCalled(); expect(checkDBServicesPrivilegesSpy).toHaveBeenCalledTimes(1); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(importDBServicesOfProjectsSpy).toHaveBeenCalledTimes(1); expect(importDBServicesOfProjectsSpy).toHaveBeenNthCalledWith(1, { db_services: mockBatchImportDBCheckData }); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(screen.getByText('批量导入数据源成功')).toBeInTheDocument(); expect(baseElement).toMatchSnapshot(); fireEvent.click(screen.getByText('关闭并重置表单')); @@ -170,10 +180,11 @@ describe('base/GlobalDataSource/BatchImportDataSource', () => { expect(screen.getByText('导 入').closest('button')).toHaveClass( 'ant-btn-loading' ); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(checkDBServicesPrivilegesSpy).toHaveBeenCalledTimes(1); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(importDBServicesOfProjectsSpy).toHaveBeenCalledTimes(1); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(screen.getByText('导 入').closest('button')).not.toHaveClass( 'ant-btn-loading' ); @@ -181,9 +192,15 @@ describe('base/GlobalDataSource/BatchImportDataSource', () => { }); it('render connectable error modal', async () => { - checkDBServicesPrivilegesSpy.mockImplementation(() => + checkDbServiceIsConnectableSpy.mockImplementation(() => createSpySuccessResponse({ - data: mockCheckDBServicesPrivilegesIncludeErrorData + data: [ + { + component: 'sqle', + is_connectable: false, + connect_error_message: 'connection refused' + } + ] }) ); const { baseElement } = baseSuperRender(); @@ -202,8 +219,9 @@ describe('base/GlobalDataSource/BatchImportDataSource', () => { expect(screen.getByText('导 入').closest('button')).toHaveClass( 'ant-btn-loading' ); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(checkDBServicesPrivilegesSpy).toHaveBeenCalledTimes(1); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(screen.getByText('数据源连通性测试失败')).toBeInTheDocument(); expect(baseElement).toMatchSnapshot(); fireEvent.click(screen.getByText('继续提交')); @@ -211,7 +229,7 @@ describe('base/GlobalDataSource/BatchImportDataSource', () => { expect(importDBServicesOfProjectsSpy).toHaveBeenNthCalledWith(1, { db_services: mockBatchImportDBCheckData }); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(screen.getByText('批量导入数据源成功')).toBeInTheDocument(); }); }); diff --git a/packages/base/src/page/GlobalDataSource/BatchImportDataSource/index.tsx b/packages/base/src/page/GlobalDataSource/BatchImportDataSource/index.tsx index 93055fcfce..7fa0900118 100644 --- a/packages/base/src/page/GlobalDataSource/BatchImportDataSource/index.tsx +++ b/packages/base/src/page/GlobalDataSource/BatchImportDataSource/index.tsx @@ -15,6 +15,8 @@ import { LeftArrowOutlined } from '@actiontech/icons'; import { DmsApi } from '@actiontech/shared/lib/api'; import ConnectableErrorModal from '../../Project/BatchImportDataSource/ConnectableErrorModal'; import useBatchCheckConnectable from '../../Project/BatchImportDataSource/hooks/useBatchCheckConnectable'; +import BatchImportCheckResult from '../../Project/BatchImportDataSource/BatchImportCheckResult'; + const GlobalBatchImportDataSource = () => { const { t } = useTranslation(); const { @@ -88,6 +90,15 @@ const GlobalBatchImportDataSource = () => { }, [importServicesCheck, setDBservices, clearUploadCheckStatus] ); + + const checkResultNode = ( + + ); + return ( <> { - {t('common.resetAndClose')} - - ]} - /> + <> + + {t('common.resetAndClose')} + + ]} + /> + {checkResultNode} + } > { uploadCheckStatus={uploadCheckStatus} clearUploadCheckStatus={clearUploadCheckStatus} /> + {checkResultNode} t('dmsGlobalDataSource.list.lastTestConnectionStatus'), diff --git a/packages/base/src/page/GlobalDataSource/List/index.tsx b/packages/base/src/page/GlobalDataSource/List/index.tsx index 3b265b2cd7..f522fcbe7a 100644 --- a/packages/base/src/page/GlobalDataSource/List/index.tsx +++ b/packages/base/src/page/GlobalDataSource/List/index.tsx @@ -202,6 +202,8 @@ const GlobalDataSourceList = () => { .then((res) => { hide(); if (res.data.code === ResponseCode.SUCCESS) { + // AC-012:连通写回后刷新列表,使「上一次连接状态」只反映 last_connection_test_* + refresh(); const connections = res.data.data ?? []; const isConnectable = getDbServiceIsConnectbale(connections); const connectErrorMessage = diff --git a/packages/base/src/page/Project/BatchImportDataSource/BatchImportCheckResult/index.tsx b/packages/base/src/page/Project/BatchImportDataSource/BatchImportCheckResult/index.tsx new file mode 100644 index 0000000000..25128c0126 --- /dev/null +++ b/packages/base/src/page/Project/BatchImportDataSource/BatchImportCheckResult/index.tsx @@ -0,0 +1,84 @@ +import { EmptyBox, ReminderInformation } from '@actiontech/dms-kit'; +import { Space, Typography } from 'antd'; +import { useTranslation } from 'react-i18next'; +import PrivilegeCheckResult from '../../../DataSource/components/Form/PrivilegeCheckResult'; +import { + BatchImportConnectResultItem, + BatchImportPrivilegeResultItem +} from '../hooks/useBatchCheckConnectable'; +import { BatchImportCheckResultStyleWrapper } from '../style'; + +type BatchImportCheckResultProps = { + visible: boolean; + connectResultList?: BatchImportConnectResultItem[]; + privilegeResultList?: BatchImportPrivilegeResultItem[]; +}; + +const BatchImportCheckResult: React.FC = ({ + visible, + connectResultList, + privilegeResultList +}) => { + const { t } = useTranslation(); + + if (!visible) { + return null; + } + + const hasConnect = (connectResultList?.length ?? 0) > 0; + const hasPrivilege = (privilegeResultList?.length ?? 0) > 0; + + if (!hasConnect && !hasPrivilege) { + return null; + } + + return ( + + + {t('dmsProject.batchImportDataSource.checkResultTitle')} + + + +
+ + {t('dmsProject.batchImportDataSource.connectivityColumn')} + + {connectResultList?.map((item) => ( +
+ + {item.name} + + +
+ ))} +
+
+ + +
+ + {t('dmsProject.batchImportDataSource.privilegeColumn')} + + {privilegeResultList?.map((item) => ( +
+ {item.name} + +
+ ))} +
+
+
+ ); +}; + +export default BatchImportCheckResult; diff --git a/packages/base/src/page/Project/BatchImportDataSource/__snapshots__/index.test.tsx.snap b/packages/base/src/page/Project/BatchImportDataSource/__snapshots__/index.test.tsx.snap index c60224eb71..f62d36c97e 100644 --- a/packages/base/src/page/Project/BatchImportDataSource/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/Project/BatchImportDataSource/__snapshots__/index.test.tsx.snap @@ -289,6 +289,209 @@ exports[`base/Project/BatchImportDataSource render connectable error modal 1`] =
+
+ + + 导入前校验结果 + + +
+ + 连通结果 + +
+
+
+ + mysql_1 + +
+
+
+
+ + + + + +
+
+ +
+                      sqle: connection refused 
+                    
+
+
+
+
+
+
+
+
+
+ + oracle_1 + +
+
+
+
+ + + + + +
+
+ +
+                      sqle: connection refused 
+                    
+
+
+
+
+
+
+
+
+ + 功能权限结果 + +
+ + mysql_1 + +
+ + + 功能权限检查结果 + + +
+ + 功能权限不足不影响创建/保存 + +
+
+
+ + oracle_1 + +
+ + + 功能权限检查结果 + + +
+ + 功能权限不足不影响创建/保存 + +
+
+
+
-                          slqe: 权限不足 
-
+                          sqle: connection refused 
+                        
+ +
+
+
+
+
+ oracle_1 +
+
+
+ + + + + +
+
+ +
+                          sqle: connection refused 
                         
@@ -1068,6 +1311,209 @@ exports[`base/Project/BatchImportDataSource render upload file 1`] = `
+
+ + + 导入前校验结果 + + +
+ + 连通结果 + +
+
+
+ + mysql_1 + +
+
+
+
+ + + + + +
+
+ +
+                      连通性测试成功
+                    
+
+
+
+
+
+
+
+
+
+ + oracle_1 + +
+
+
+
+ + + + + +
+
+ +
+                      连通性测试成功
+                    
+
+
+
+
+
+
+
+
+ + 功能权限结果 + +
+ + mysql_1 + +
+ + + 功能权限检查结果 + + +
+ + 功能权限不足不影响创建/保存 + +
+
+
+ + oracle_1 + +
+ + + 功能权限检查结果 + + +
+ + 功能权限不足不影响创建/保存 + +
+
+
+
`; diff --git a/packages/base/src/page/Project/BatchImportDataSource/hooks/__tests__/useBatchCheckConnectable.test.ts b/packages/base/src/page/Project/BatchImportDataSource/hooks/__tests__/useBatchCheckConnectable.test.ts index b2f265d1a6..abce2240db 100644 --- a/packages/base/src/page/Project/BatchImportDataSource/hooks/__tests__/useBatchCheckConnectable.test.ts +++ b/packages/base/src/page/Project/BatchImportDataSource/hooks/__tests__/useBatchCheckConnectable.test.ts @@ -1,6 +1,7 @@ import { act } from '@testing-library/react'; import useBatchCheckConnectable from '../useBatchCheckConnectable'; import project from '@actiontech/shared/lib/testUtil/mockApi/base/project'; +import dbServices from '@actiontech/shared/lib/testUtil/mockApi/base/dbServices'; import { mockBatchImportDBCheckData, mockCheckDBServicesPrivilegesIncludeErrorData @@ -8,11 +9,29 @@ import { import { createSpySuccessResponse } from '@actiontech/shared/lib/testUtil/mockApi'; import { baseSuperRenderHook } from '../../../../../testUtils/superRender'; +const projectUid = '700300'; +const dbServicesWithProject = mockBatchImportDBCheckData.map((item) => ({ + ...item, + project_uid: projectUid +})); + +const connectableSuccessReply = [ + { component: 'sqle', is_connectable: true }, + { component: 'provision', is_connectable: true } +]; + describe('useBatchCheckConnectable', () => { let checkDBServicesPrivilegesSpy: jest.SpyInstance; + let checkDBServiceIsConnectableSpy: jest.SpyInstance; beforeEach(() => { checkDBServicesPrivilegesSpy = project.checkDBServicesPrivileges(); + checkDBServiceIsConnectableSpy = dbServices.checkDbServiceIsConnectable(); + checkDBServiceIsConnectableSpy.mockImplementation(() => + createSpySuccessResponse({ + data: connectableSuccessReply + }) + ); jest.useFakeTimers(); }); @@ -22,7 +41,9 @@ describe('useBatchCheckConnectable', () => { }); it('should initialize with correct default values', () => { - const { result } = baseSuperRenderHook(() => useBatchCheckConnectable()); + const { result } = baseSuperRenderHook(() => + useBatchCheckConnectable(projectUid) + ); expect(result.current.connectableInfo).toBeUndefined(); expect(result.current.batchCheckConnectableLoading).toBe(false); @@ -32,45 +53,108 @@ describe('useBatchCheckConnectable', () => { expect(typeof result.current.hideConnectErrorModal).toBe('function'); }); - it('should handle successful API response with all connectable services', async () => { - const { result } = baseSuperRenderHook(() => useBatchCheckConnectable()); + it('should call connection and privilege APIs separately on success', async () => { + const { result } = baseSuperRenderHook(() => + useBatchCheckConnectable(projectUid) + ); await act(async () => { - result.current.batchCheckConnectable(mockBatchImportDBCheckData); - await jest.advanceTimersByTime(3000); + const pending = result.current.batchCheckConnectable( + dbServicesWithProject + ); + // createSpySuccessResponse uses setTimeout(3000); connect then privilege are sequential + await jest.advanceTimersByTimeAsync(3000); + await jest.advanceTimersByTimeAsync(3000); + await pending; }); + expect(checkDBServiceIsConnectableSpy).toHaveBeenCalled(); expect(checkDBServicesPrivilegesSpy).toHaveBeenCalledTimes(1); expect(result.current.connectableInfo?.isConnectable).toBe(true); expect(result.current.connectableInfo?.connectErrorList).toEqual([]); + expect( + result.current.connectableInfo?.privilegeResultList?.length + ).toBeGreaterThan(0); }); - it('should handle API response with non-connectable services', async () => { + it('should treat only connection API failures as connect errors', async () => { + checkDBServiceIsConnectableSpy.mockImplementation(() => + createSpySuccessResponse({ + data: [ + { + component: 'sqle', + is_connectable: false, + connect_error_message: 'connection refused' + } + ] + }) + ); checkDBServicesPrivilegesSpy.mockImplementation(() => createSpySuccessResponse({ data: mockCheckDBServicesPrivilegesIncludeErrorData }) ); - const { result } = baseSuperRenderHook(() => useBatchCheckConnectable()); + const { result } = baseSuperRenderHook(() => + useBatchCheckConnectable(projectUid) + ); expect(result.current.batchCheckConnectableLoading).toBe(false); + let pending: Promise | undefined; act(() => { - result.current.batchCheckConnectable(mockBatchImportDBCheckData); + pending = result.current.batchCheckConnectable(dbServicesWithProject); }); expect(result.current.batchCheckConnectableLoading).toBe(true); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => { + await jest.advanceTimersByTimeAsync(3000); + await jest.advanceTimersByTimeAsync(3000); + await pending; + }); + expect(checkDBServiceIsConnectableSpy).toHaveBeenCalled(); expect(checkDBServicesPrivilegesSpy).toHaveBeenCalledTimes(1); expect(result.current.connectableInfo?.isConnectable).toBe(false); - expect(result.current.connectableInfo?.connectErrorList).toHaveLength(1); + expect(result.current.connectableInfo?.connectErrorList).toHaveLength(2); expect(result.current.connectableInfo?.connectErrorList?.[0]).toEqual({ name: 'mysql_1', - connect_error_message: expect.stringContaining('slqe: 权限不足') + is_connectable: false, + connect_error_message: expect.stringContaining('connection refused') }); }); + it('should not mark privilege-only issues as connect failures', async () => { + checkDBServicesPrivilegesSpy.mockImplementation(() => + createSpySuccessResponse({ + data: mockCheckDBServicesPrivilegesIncludeErrorData + }) + ); + + const { result } = baseSuperRenderHook(() => + useBatchCheckConnectable(projectUid) + ); + + await act(async () => { + const pending = result.current.batchCheckConnectable( + dbServicesWithProject + ); + await jest.advanceTimersByTimeAsync(3000); + await jest.advanceTimersByTimeAsync(3000); + await pending; + }); + + expect(result.current.connectableInfo?.isConnectable).toBe(true); + expect(result.current.connectableInfo?.connectErrorList).toEqual([]); + expect(result.current.connectableInfo?.privilegeResultList?.[0]).toEqual( + expect.objectContaining({ + name: 'mysql_1', + CheckDBServicesPrivileges: expect.any(Array) + }) + ); + }); + it('should handle modal visibility functions', () => { - const { result } = baseSuperRenderHook(() => useBatchCheckConnectable()); + const { result } = baseSuperRenderHook(() => + useBatchCheckConnectable(projectUid) + ); expect(result.current.connectErrorModalVisible).toBe(false); diff --git a/packages/base/src/page/Project/BatchImportDataSource/hooks/useBatchCheckConnectable.ts b/packages/base/src/page/Project/BatchImportDataSource/hooks/useBatchCheckConnectable.ts index f99cd4394e..60966df65c 100644 --- a/packages/base/src/page/Project/BatchImportDataSource/hooks/useBatchCheckConnectable.ts +++ b/packages/base/src/page/Project/BatchImportDataSource/hooks/useBatchCheckConnectable.ts @@ -1,14 +1,47 @@ import { useRequest } from 'ahooks'; import { DmsApi } from '@actiontech/shared/lib/api'; import { ResponseCode } from '@actiontech/dms-kit'; -import { IImportDBServiceV2 } from '@actiontech/shared/lib/api/base/service/common'; +import { + ICheckDBServicesPrivilegesItem, + IImportDBServiceV2 +} from '@actiontech/shared/lib/api/base/service/common'; import { useBoolean } from 'ahooks'; import { getDBServiceConnectableErrorMessage, getDbServiceIsConnectbale } from '../../../../utils/common'; -const useBatchCheckConnectable = () => { +export type BatchImportConnectResultItem = { + name: string | undefined; + is_connectable: boolean; + connect_error_message: string; +}; + +export type BatchImportPrivilegeResultItem = ICheckDBServicesPrivilegesItem & { + name?: string; +}; + +export type BatchImportCheckInfo = { + isConnectable: boolean; + connectErrorList: BatchImportConnectResultItem[]; + connectResultList: BatchImportConnectResultItem[]; + privilegeResultList: BatchImportPrivilegeResultItem[]; +}; + +const toConnectablePayload = (item: IImportDBServiceV2) => ({ + db_type: item.db_type ?? '', + host: item.host ?? '', + port: item.port ?? '', + user: item.user ?? '', + password: item.password ?? '', + additional_params: item.additional_params +}); + +/** + * 批量导入导入前校验:连通 API 与权限 API 分流。 + * Modal 开关仅由连通结果驱动;权限结果只进 privilegeResultList。 + */ +const useBatchCheckConnectable = (fallbackProjectUid?: string) => { const [ connectErrorModalVisible, { setTrue: showConnectErrorModal, setFalse: hideConnectErrorModal } @@ -19,43 +52,68 @@ const useBatchCheckConnectable = () => { runAsync: batchCheckConnectable, loading: batchCheckConnectableLoading } = useRequest( - (dbServices: IImportDBServiceV2[]) => - DmsApi.ProjectService.CheckDBServicesPrivileges({ - db_services: dbServices.map((item) => ({ - db_type: item.db_type ?? '', - host: item.host ?? '', - port: item.port ?? '', - user: item.user ?? '', - password: item.password ?? '', - additional_params: item.additional_params - })) - }).then((res) => { - if (res.data.code === ResponseCode.SUCCESS) { - const formattedData = res.data.data?.map((i, index) => { - const CheckDBServicesPrivileges = i.CheckDBServicesPrivileges ?? []; + async (dbServices: IImportDBServiceV2[]): Promise => { + const connectSettled = await Promise.all( + dbServices.map(async (item) => { + const projectUid = item.project_uid || fallbackProjectUid || ''; + if (!projectUid) { return { - ...i, - name: dbServices[index].name, - is_connectable: getDbServiceIsConnectbale( - CheckDBServicesPrivileges - ), - connect_error_message: getDBServiceConnectableErrorMessage( - CheckDBServicesPrivileges - ) - }; - }); + name: item.name, + is_connectable: false, + connect_error_message: 'project_uid is required' + } satisfies BatchImportConnectResultItem; + } + const res = await DmsApi.DBServiceService.CheckDBServiceIsConnectable( + { + project_uid: projectUid, + db_service: toConnectablePayload(item) + } + ); + + if (res.data.code !== ResponseCode.SUCCESS) { + return { + name: item.name, + is_connectable: false, + connect_error_message: res.data.message || '' + } satisfies BatchImportConnectResultItem; + } + + const connections = res.data.data ?? []; return { - isConnectable: formattedData?.every((item) => item.is_connectable), - connectErrorList: formattedData - ?.filter((item) => !item.is_connectable) - .map((item) => ({ - name: item.name, - connect_error_message: item.connect_error_message - })) - }; - } - }), + name: item.name, + is_connectable: getDbServiceIsConnectbale(connections), + connect_error_message: + getDBServiceConnectableErrorMessage(connections) + } satisfies BatchImportConnectResultItem; + }) + ); + + let privilegeResultList: BatchImportPrivilegeResultItem[] = []; + const privilegeRes = + await DmsApi.ProjectService.CheckDBServicesPrivileges({ + db_services: dbServices.map(toConnectablePayload) + }); + + if (privilegeRes.data.code === ResponseCode.SUCCESS) { + privilegeResultList = + privilegeRes.data.data?.map((item, index) => ({ + ...item, + name: dbServices[index]?.name + })) ?? []; + } + + const connectErrorList = connectSettled.filter( + (item) => !item.is_connectable + ); + + return { + isConnectable: connectSettled.every((item) => item.is_connectable), + connectErrorList, + connectResultList: connectSettled, + privilegeResultList + }; + }, { manual: true } diff --git a/packages/base/src/page/Project/BatchImportDataSource/index.test.tsx b/packages/base/src/page/Project/BatchImportDataSource/index.test.tsx index 5248fd0fc4..5858341930 100644 --- a/packages/base/src/page/Project/BatchImportDataSource/index.test.tsx +++ b/packages/base/src/page/Project/BatchImportDataSource/index.test.tsx @@ -1,8 +1,6 @@ import project from '@actiontech/shared/lib/testUtil/mockApi/base/project'; -import { - mockBatchImportDBCheckData, - mockCheckDBServicesPrivilegesIncludeErrorData -} from '@actiontech/shared/lib/testUtil/mockApi/base/project/data'; +import dbServices from '@actiontech/shared/lib/testUtil/mockApi/base/dbServices'; +import { mockBatchImportDBCheckData } from '@actiontech/shared/lib/testUtil/mockApi/base/project/data'; import { baseSuperRender } from '../../../testUtils/superRender'; import ProjectImport from '.'; import { act, cleanup, fireEvent, screen } from '@testing-library/react'; @@ -15,11 +13,16 @@ import Project from '@actiontech/shared/lib/api/base/service/Project'; import { AxiosResponse } from 'axios'; import 'blob-polyfill'; +const connectableSuccessReply = [ + { component: 'sqle', is_connectable: true }, + { component: 'provision', is_connectable: true } +]; + describe('base/Project/BatchImportDataSource', () => { let importDBServicesOfProjectsSpy: jest.SpyInstance; let getImportDBServicesTemplateSpy: jest.SpyInstance; let importDBServicesOfProjectsCheckSpy: jest.SpyInstance; - let dbServicesConnectionSpy: jest.SpyInstance; + let checkDbServiceIsConnectableSpy: jest.SpyInstance; let checkDBServicesPrivilegesSpy: jest.SpyInstance; beforeEach(() => { @@ -28,7 +31,12 @@ describe('base/Project/BatchImportDataSource', () => { getImportDBServicesTemplateSpy = project.getImportDBServicesTemplate(); importDBServicesOfProjectsCheckSpy = project.importDBServicesOfProjectsCheck(); - dbServicesConnectionSpy = project.dbServicesConnection(); + checkDbServiceIsConnectableSpy = dbServices.checkDbServiceIsConnectable(); + checkDbServiceIsConnectableSpy.mockImplementation(() => + createSpySuccessResponse({ + data: connectableSuccessReply + }) + ); checkDBServicesPrivilegesSpy = project.checkDBServicesPrivileges(); }); @@ -133,13 +141,15 @@ describe('base/Project/BatchImportDataSource', () => { expect(screen.getByText('导 入').closest('button')).toHaveClass( 'ant-btn-loading' ); + await act(async () => jest.advanceTimersByTimeAsync(3000)); + expect(checkDbServiceIsConnectableSpy).toHaveBeenCalled(); expect(checkDBServicesPrivilegesSpy).toHaveBeenCalledTimes(1); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(importDBServicesOfProjectsSpy).toHaveBeenCalledTimes(1); expect(importDBServicesOfProjectsSpy).toHaveBeenNthCalledWith(1, { db_services: mockBatchImportDBCheckData }); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(screen.getByText('批量导入数据源成功')).toBeInTheDocument(); expect(baseElement).toMatchSnapshot(); fireEvent.click(screen.getByText('关闭并重置表单')); @@ -168,10 +178,11 @@ describe('base/Project/BatchImportDataSource', () => { expect(screen.getByText('导 入').closest('button')).toHaveClass( 'ant-btn-loading' ); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(checkDBServicesPrivilegesSpy).toHaveBeenCalledTimes(1); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(importDBServicesOfProjectsSpy).toHaveBeenCalledTimes(1); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(screen.getByText('导 入').closest('button')).not.toHaveClass( 'ant-btn-loading' ); @@ -179,9 +190,15 @@ describe('base/Project/BatchImportDataSource', () => { }); it('render connectable error modal', async () => { - checkDBServicesPrivilegesSpy.mockImplementation(() => + checkDbServiceIsConnectableSpy.mockImplementation(() => createSpySuccessResponse({ - data: mockCheckDBServicesPrivilegesIncludeErrorData + data: [ + { + component: 'sqle', + is_connectable: false, + connect_error_message: 'connection refused' + } + ] }) ); const { baseElement } = baseSuperRender(); @@ -200,8 +217,9 @@ describe('base/Project/BatchImportDataSource', () => { expect(screen.getByText('导 入').closest('button')).toHaveClass( 'ant-btn-loading' ); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(checkDBServicesPrivilegesSpy).toHaveBeenCalledTimes(1); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(screen.getByText('数据源连通性测试失败')).toBeInTheDocument(); expect(baseElement).toMatchSnapshot(); fireEvent.click(screen.getByText('继续提交')); @@ -209,7 +227,7 @@ describe('base/Project/BatchImportDataSource', () => { expect(importDBServicesOfProjectsSpy).toHaveBeenNthCalledWith(1, { db_services: mockBatchImportDBCheckData }); - await act(async () => jest.advanceTimersByTime(3000)); + await act(async () => jest.advanceTimersByTimeAsync(3000)); expect(screen.getByText('批量导入数据源成功')).toBeInTheDocument(); }); }); diff --git a/packages/base/src/page/Project/BatchImportDataSource/index.tsx b/packages/base/src/page/Project/BatchImportDataSource/index.tsx index 8669e26deb..839a72f328 100644 --- a/packages/base/src/page/Project/BatchImportDataSource/index.tsx +++ b/packages/base/src/page/Project/BatchImportDataSource/index.tsx @@ -16,6 +16,8 @@ import { ROUTE_PATHS } from '@actiontech/dms-kit'; import { DmsApi } from '@actiontech/shared/lib/api'; import useBatchCheckConnectable from './hooks/useBatchCheckConnectable'; import ConnectableErrorModal from './ConnectableErrorModal'; +import BatchImportCheckResult from './BatchImportCheckResult'; + const BatchImportDataSource = () => { const { t } = useTranslation(); const { @@ -89,6 +91,15 @@ const BatchImportDataSource = () => { }, [importServicesCheck, setDBservices, clearUploadCheckStatus] ); + + const checkResultNode = ( + + ); + return ( <> { - {t('common.resetAndClose')} - - ]} - /> + <> + + {t('common.resetAndClose')} + + ]} + /> + {checkResultNode} + } > { uploadCheckStatus={uploadCheckStatus} clearUploadCheckStatus={clearUploadCheckStatus} /> + {checkResultNode} theme.sharedTheme.basic.colorGrayLine}; + background-color: ${({ theme }) => theme.sharedTheme.uiToken.colorBgBase}; + display: flex; + flex-direction: column; + gap: 12px; + } + + .column-title { + font-weight: 600; + } + + .column-row { + display: flex; + flex-direction: column; + gap: 8px; + } +`; diff --git a/packages/shared/lib/api/base/service/common.d.ts b/packages/shared/lib/api/base/service/common.d.ts index 56e5c50b2b..3989f2ce5b 100644 --- a/packages/shared/lib/api/base/service/common.d.ts +++ b/packages/shared/lib/api/base/service/common.d.ts @@ -489,7 +489,44 @@ export interface ICheckDBServicesIsConnectableReq { db_services?: IDbServiceConnections[]; } +export interface ICheckDBServicePrivilegeMissingPrivilege { + privilege?: string; + + object_scope?: string; + + note?: string; +} + +export interface ICheckDBServicePrivilegeModule { + module?: string; + + module_name?: string; + + status?: string; + + missing_privileges?: ICheckDBServicePrivilegeMissingPrivilege[]; + + message?: string; +} + +export interface ICheckDBServicePrivilegeConnectivityPrecheck { + ok?: boolean; + + error_message?: string; +} + export interface ICheckDBServicesPrivilegesItem { + db_type?: string; + + check_support?: string; + + connectivity_precheck?: ICheckDBServicePrivilegeConnectivityPrecheck; + + modules?: ICheckDBServicePrivilegeModule[]; + + summary_message?: string; + + /** @deprecated P2 起权限 API 已改为 modules 结构;批量导入 P3 前兼容旧 mock */ CheckDBServicesPrivileges?: ICheckDBServiceIsConnectableReplyItem[]; } diff --git a/packages/shared/lib/testUtil/mockApi/base/project/data.ts b/packages/shared/lib/testUtil/mockApi/base/project/data.ts index 390e777490..9cbccf4283 100644 --- a/packages/shared/lib/testUtil/mockApi/base/project/data.ts +++ b/packages/shared/lib/testUtil/mockApi/base/project/data.ts @@ -92,6 +92,7 @@ export const mockBatchImportDBCheckData: IImportDBServiceV2[] = [ port: '3306', user: 'root', password: '123456', + project_uid: '700300', environment_tag_name: 'environment-1', maintenance_times: [ { @@ -123,6 +124,7 @@ export const mockBatchImportDBCheckData: IImportDBServiceV2[] = [ port: '1521', user: 'system', password: '123456', + project_uid: '700300', environment_tag_name: 'environment-2', maintenance_times: [], desc: 'oracle_1', From b65dbcdccefe886df34ad981a95db3b991513084 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Thu, 30 Jul 2026 11:19:53 +0800 Subject: [PATCH 2/2] chore: add code_review_manifest for feat-950 --- docs/dev/code_review_manifest.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/dev/code_review_manifest.json diff --git a/docs/dev/code_review_manifest.json b/docs/dev/code_review_manifest.json new file mode 100644 index 0000000000..e9be5844db --- /dev/null +++ b/docs/dev/code_review_manifest.json @@ -0,0 +1,6 @@ +{ + "repo": "dms-ui", + "pr_branch": "dms-ui/feat-950", + "target_branch": "main", + "local_head": "eec07faf90cc15671e0e29f56885c542c57c8671" +}