diff --git a/packages/sqle/src/page/Whitelist/List/columns.tsx b/packages/sqle/src/page/Whitelist/List/columns.tsx
index 6149a095dd..3ac76970aa 100644
--- a/packages/sqle/src/page/Whitelist/List/columns.tsx
+++ b/packages/sqle/src/page/Whitelist/List/columns.tsx
@@ -4,10 +4,16 @@ import {
PageInfoWithoutIndexAndSize
} from '@actiontech/shared/lib/components/ActiontechTable';
import { WhitelistMatchTypeLabel } from '../index.data';
-import { IAuditWhitelistResV1 } from '@actiontech/shared/lib/api/sqle/service/common';
+import {
+ IAuditWhitelistResV1,
+ ISQLRuleExceptionResV1
+} from '@actiontech/shared/lib/api/sqle/service/common';
import { CreateAuditWhitelistReqV1MatchTypeEnum } from '@actiontech/shared/lib/api/sqle/service/common.enum';
import { SQLRenderer, BasicTypographyEllipsis } from '@actiontech/shared';
-import { IGetAuditWhitelistV1Params } from '@actiontech/shared/lib/api/sqle/service/audit_whitelist/index.d';
+import {
+ IGetAuditWhitelistV1Params,
+ IGetSQLRuleExceptionV1Params
+} from '@actiontech/shared/lib/api/sqle/service/audit_whitelist/index.d';
import { formatTime } from '@actiontech/shared/lib/utils/Common';
export type WhitelistTableFilterParamType = PageInfoWithoutIndexAndSize<
@@ -18,6 +24,14 @@ export type WhitelistTableFilterParamType = PageInfoWithoutIndexAndSize<
'project_name'
>;
+export type SQLRuleExceptionTableFilterParamType = PageInfoWithoutIndexAndSize<
+ IGetSQLRuleExceptionV1Params & {
+ page_index: number;
+ page_size: number;
+ },
+ 'project_name'
+>;
+
export const WhitelistColumn = (): ActiontechTableColumn<
IAuditWhitelistResV1,
WhitelistTableFilterParamType
@@ -76,3 +90,123 @@ export const WhitelistColumn = (): ActiontechTableColumn<
}
];
};
+
+const renderEmptyValue = (value?: string | number) => {
+ if (value === 0) {
+ return value;
+ }
+ return value || '-';
+};
+
+const getRuleExceptionMatchInfo = (record: ISQLRuleExceptionResV1) => {
+ return (
+ record.match_info ??
+ record.hit_info ??
+ record.matched_count ??
+ record.hit_count ??
+ record.last_match_time
+ );
+};
+
+export const SQLRuleExceptionColumn = (): ActiontechTableColumn<
+ ISQLRuleExceptionResV1,
+ SQLRuleExceptionTableFilterParamType
+> => {
+ return [
+ {
+ dataIndex: 'project_name',
+ title: () => t('whitelist.ruleException.project'),
+ render: (projectName, record) => {
+ return renderEmptyValue(projectName ?? record.project_id);
+ }
+ },
+ {
+ dataIndex: 'instance_name',
+ title: () => t('whitelist.ruleException.instance'),
+ filterCustomType: 'select',
+ filterKey: 'filter_instance_id',
+ render: (instanceName, record) => {
+ return renderEmptyValue(instanceName ?? record.instance_id);
+ }
+ },
+ {
+ dataIndex: 'sql_fingerprint',
+ title: () => t('whitelist.ruleException.sqlFingerprint'),
+ className: 'ellipsis-column-width',
+ filterCustomType: 'search-input',
+ filterKey: 'filter_sql_fingerprint',
+ render: (sqlFingerprint) => {
+ if (!!sqlFingerprint) {
+ return (
+
+ );
+ }
+ return '-';
+ }
+ },
+ {
+ dataIndex: 'rule_name',
+ title: () => t('whitelist.ruleException.ruleName'),
+ className: 'ellipsis-column-width',
+ filterCustomType: 'input',
+ filterKey: 'filter_rule_name',
+ render: (ruleName) => {
+ return ruleName ?
: '-';
+ }
+ },
+ {
+ dataIndex: 'rule_desc',
+ title: () => t('whitelist.ruleException.ruleDesc'),
+ className: 'ellipsis-column-width',
+ render: (ruleDesc) => {
+ return ruleDesc ?
: '-';
+ }
+ },
+ {
+ dataIndex: 'rule_level',
+ title: () => t('whitelist.ruleException.ruleLevel'),
+ render: renderEmptyValue
+ },
+ {
+ dataIndex: 'created_by',
+ title: () => t('whitelist.ruleException.createdBy'),
+ filterCustomType: 'select',
+ filterKey: 'filter_created_by',
+ render: renderEmptyValue
+ },
+ {
+ dataIndex: 'created_at',
+ title: () => t('whitelist.ruleException.createdAt'),
+ filterCustomType: 'date-range',
+ filterKey: ['filter_created_time_from', 'filter_created_time_to'],
+ render: (createdAt) => formatTime(createdAt, '-')
+ },
+ {
+ dataIndex: 'reason',
+ title: () => t('whitelist.ruleException.reason'),
+ className: 'ellipsis-column-width',
+ render: (reason) => {
+ return reason ?
: '-';
+ }
+ },
+ {
+ dataIndex: 'match_info',
+ title: () => t('whitelist.ruleException.matchInfo'),
+ render: (_, record) => {
+ const matchInfo = getRuleExceptionMatchInfo(record);
+ if (record.last_match_time) {
+ return t('whitelist.ruleException.matchInfoWithTime', {
+ count: record.matched_count ?? record.hit_count ?? '-',
+ time: formatTime(record.last_match_time, '-')
+ });
+ }
+ return renderEmptyValue(matchInfo);
+ }
+ }
+ ];
+};
diff --git a/packages/sqle/src/page/Whitelist/List/index.test.tsx b/packages/sqle/src/page/Whitelist/List/index.test.tsx
index ebf091d957..6a1d73bf51 100644
--- a/packages/sqle/src/page/Whitelist/List/index.test.tsx
+++ b/packages/sqle/src/page/Whitelist/List/index.test.tsx
@@ -9,10 +9,13 @@ import { ModalName } from '../../../data/ModalName';
import { mockUseCurrentProject } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentProject';
import { mockUseCurrentUser } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentUser';
import { createSpySuccessResponse } from '@actiontech/shared/lib/testUtil/mockApi';
+import instance from '../../../testUtils/mockApi/instance';
+import user from '../../../testUtils/mockApi/user';
import {
mockProjectInfo,
mockCurrentUserReturn
} from '@actiontech/shared/lib/testUtil/mockHook/data';
+import { driverMeta } from '../../../hooks/useDatabaseType/index.test.data';
jest.mock('react-redux', () => {
return {
@@ -22,6 +25,13 @@ jest.mock('react-redux', () => {
};
});
+jest.mock('react-router-dom', () => {
+ return {
+ ...jest.requireActual('react-router-dom'),
+ useNavigate: jest.fn()
+ };
+});
+
describe('slqe/Whitelist/WhitelistList', () => {
let whitelistSpy: jest.SpyInstance;
const dispatchSpy = jest.fn();
@@ -30,8 +40,11 @@ describe('slqe/Whitelist/WhitelistList', () => {
beforeEach(() => {
jest.useFakeTimers();
whitelistSpy = auditWhiteList.getAuditWhitelist();
+ instance.getInstanceTipList();
+ user.getUserTipList();
(useSelector as jest.Mock).mockImplementation((e) =>
e({
+ database: { driverMeta },
whitelist: { modalStatus: { [ModalName.Add_Whitelist]: false } }
})
);
@@ -71,6 +84,62 @@ describe('slqe/Whitelist/WhitelistList', () => {
expect(whitelistSpy).toHaveBeenCalledTimes(2);
});
+ test('should render rule exception management view', async () => {
+ const ruleExceptionSpy = auditWhiteList.getSQLRuleException();
+ renderWithReduxAndTheme(
);
+ await act(async () => jest.advanceTimersByTime(3000));
+
+ fireEvent.click(screen.getByText('单规则例外'));
+ await act(async () => jest.advanceTimersByTime(3000));
+
+ expect(ruleExceptionSpy).toHaveBeenCalledTimes(1);
+ expect(screen.getByText('项目')).toBeInTheDocument();
+ expect(screen.getByText('数据源')).toBeInTheDocument();
+ expect(screen.getByText('SQL指纹')).toBeInTheDocument();
+ expect(screen.getByText('规则名')).toBeInTheDocument();
+ expect(screen.getByText('规则描述')).toBeInTheDocument();
+ expect(screen.getByText('规则原级别')).toBeInTheDocument();
+ expect(screen.getByText('添加例外的人')).toBeInTheDocument();
+ expect(screen.getByText('添加时间')).toBeInTheDocument();
+ expect(screen.getByText('添加原因')).toBeInTheDocument();
+ expect(screen.getByText('命中信息')).toBeInTheDocument();
+ expect(screen.getByText('mysql_local_sqle')).toBeInTheDocument();
+ expect(screen.getByText('ddl_check_pk_not_exist')).toBeInTheDocument();
+ expect(screen.getByText('建表语句必须包含主键')).toBeInTheDocument();
+ expect(screen.getByText('标准管理页回归验证')).toBeInTheDocument();
+ expect(screen.getByText('查看审计')).toBeInTheDocument();
+ expect(screen.getByText('取消例外')).toBeInTheDocument();
+ });
+
+ test('should submit sql fingerprint filter in rule exception view', async () => {
+ const ruleExceptionSpy = auditWhiteList.getSQLRuleException();
+ const { baseElement } = renderWithReduxAndTheme(
);
+ await act(async () => jest.advanceTimersByTime(3000));
+
+ fireEvent.click(screen.getByText('单规则例外'));
+ await act(async () => jest.advanceTimersByTime(3000));
+ fireEvent.click(screen.getByText('筛选'));
+ await act(async () => jest.advanceTimersByTime(300));
+
+ const sqlFingerprintInput = getBySelector(
+ '.filter-search-input input.ant-input',
+ baseElement
+ );
+ fireEvent.change(sqlFingerprintInput, {
+ target: { value: 'ac013_standard_filter_20260618191804' }
+ });
+ fireEvent.click(
+ getBySelector('.filter-search-input .custom-icon-search', baseElement)
+ );
+ await act(async () => jest.advanceTimersByTime(3000));
+
+ expect(ruleExceptionSpy).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ filter_sql_fingerprint: 'ac013_standard_filter_20260618191804'
+ })
+ );
+ });
+
it('should hide table actions', async () => {
useCurrentUserSpy.mockImplementation(() => ({
...mockCurrentUserReturn,
diff --git a/packages/sqle/src/page/Whitelist/List/index.tsx b/packages/sqle/src/page/Whitelist/List/index.tsx
index 797b1cf6bf..636d0e370c 100644
--- a/packages/sqle/src/page/Whitelist/List/index.tsx
+++ b/packages/sqle/src/page/Whitelist/List/index.tsx
@@ -1,18 +1,35 @@
-import { useCallback, useEffect, useMemo } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRequest } from 'ahooks';
import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
import { useCurrentProject } from '@actiontech/shared/lib/global';
-import { WhitelistColumn, WhitelistTableFilterParamType } from './columns';
+import {
+ SQLRuleExceptionColumn,
+ SQLRuleExceptionTableFilterParamType,
+ WhitelistColumn,
+ WhitelistTableFilterParamType
+} from './columns';
import { ModalName } from '../../../data/ModalName';
import { message } from 'antd';
import { ResponseCode } from '@actiontech/shared/lib/enum';
import { updateWhitelistModalStatus } from '../../../store/whitelist';
import EventEmitter from '../../../utils/EventEmitter';
import EmitterKey from '../../../data/EmitterKey';
-import { BasicButton, EmptyBox, PageHeader } from '@actiontech/shared';
+import {
+ BasicButton,
+ BasicSegmented,
+ EmptyBox,
+ PageHeader
+} from '@actiontech/shared';
import WhitelistDrawer from '../Drawer';
-import { IAuditWhitelistResV1 } from '@actiontech/shared/lib/api/sqle/service/common';
-import { IGetAuditWhitelistV1Params } from '@actiontech/shared/lib/api/sqle/service/audit_whitelist/index.d';
+import {
+ IAuditWhitelistResV1,
+ ISQLRuleExceptionResV1
+} from '@actiontech/shared/lib/api/sqle/service/common';
+import {
+ IGetAuditWhitelistV1Params,
+ IGetSQLRuleExceptionV1Params
+} from '@actiontech/shared/lib/api/sqle/service/audit_whitelist/index.d';
import audit_whitelist from '@actiontech/shared/lib/api/sqle/service/audit_whitelist';
import {
ActiontechTable,
@@ -27,11 +44,24 @@ import {
import { PlusOutlined } from '@actiontech/icons';
import { whitelistMatchTypeOptions } from '../index.data';
import useWhitelistRedux from '../hooks/useWhitelistRedux';
+import useInstance from '../../../hooks/useInstance';
+import useUsername from '../../../hooks/useUsername';
+
+enum WhitelistManageView {
+ sql = 'sql',
+ rule = 'rule'
+}
const WhitelistList = () => {
const { t } = useTranslation();
+ const navigate = useNavigate();
const [messageApi, messageContextHolder] = message.useMessage();
- const { projectName } = useCurrentProject();
+ const { projectName, projectID } = useCurrentProject();
+ const { instanceIDOptions, updateInstanceList } = useInstance();
+ const { usernameOptions, updateUsernameList } = useUsername();
+ const [activeView, setActiveView] = useState
(
+ WhitelistManageView.sql
+ );
const {
dispatch,
@@ -53,7 +83,21 @@ const WhitelistList = () => {
WhitelistTableFilterParamType
>();
+ const {
+ tableFilterInfo: ruleExceptionTableFilterInfo,
+ updateTableFilterInfo: updateRuleExceptionTableFilterInfo,
+ tableChange: ruleExceptionTableChange,
+ pagination: ruleExceptionPagination,
+ searchKeyword: ruleExceptionSearchKeyword,
+ setSearchKeyword: setRuleExceptionSearchKeyword,
+ refreshBySearchKeyword: refreshRuleExceptionBySearchKeyword
+ } = useTableRequestParams<
+ ISQLRuleExceptionResV1,
+ SQLRuleExceptionTableFilterParamType
+ >();
+
const columns = useMemo(() => WhitelistColumn(), []);
+ const ruleExceptionColumns = useMemo(() => SQLRuleExceptionColumn(), []);
const { requestErrorMessage, handleTableRequestError } =
useTableRequestError();
@@ -81,6 +125,30 @@ const WhitelistList = () => {
}
);
+ const {
+ data: ruleExceptionList,
+ loading: ruleExceptionLoading,
+ refresh: refreshRuleException
+ } = useRequest(
+ () => {
+ const params: IGetSQLRuleExceptionV1Params = {
+ ...ruleExceptionTableFilterInfo,
+ page_index: String(ruleExceptionPagination.page_index),
+ page_size: String(ruleExceptionPagination.page_size),
+ project_name: projectName,
+ fuzzy_search_value: ruleExceptionSearchKeyword
+ };
+
+ return handleTableRequestError(
+ audit_whitelist.getSQLRuleExceptionV1(params)
+ );
+ },
+ {
+ manual: true,
+ refreshDeps: [ruleExceptionPagination, ruleExceptionTableFilterInfo]
+ }
+ );
+
const openUpdateWhitelistModal = useCallback(
(selectRow: IAuditWhitelistResV1) => {
updateSelectWhitelistRecord(selectRow);
@@ -115,6 +183,46 @@ const WhitelistList = () => {
[messageApi, projectName, refresh, t]
);
+ const removeRuleException = useCallback(
+ (sqlRuleExceptionId: number) => {
+ const hide = messageApi.loading(t('whitelist.ruleException.deleting'));
+ audit_whitelist
+ .deleteSQLRuleExceptionV1({
+ sql_rule_exception_id: `${sqlRuleExceptionId}`,
+ project_name: projectName
+ })
+ .then((res) => {
+ if (res.data.code === ResponseCode.SUCCESS) {
+ messageApi.success(t('whitelist.ruleException.deleteSuccess'));
+ refreshRuleException();
+ }
+ })
+ .finally(() => {
+ hide();
+ });
+ },
+ [messageApi, projectName, refreshRuleException, t]
+ );
+
+ const viewRuleExceptionAudit = useCallback(
+ (record?: ISQLRuleExceptionResV1) => {
+ const contentKeywords = [
+ record?.sql_fingerprint,
+ record?.rule_name
+ ].filter(Boolean);
+
+ const searchParams = new URLSearchParams({
+ filter_operate_type_name: 'sql_rule_exception',
+ audit_content_keywords: contentKeywords.join('|')
+ });
+
+ navigate(
+ `/sqle/project/${projectID}/operation-record?${searchParams.toString()}`
+ );
+ },
+ [navigate, projectID]
+ );
+
const whitelistActionsInTable: {
buttons: ActiontechTableActionMeta[];
} = {
@@ -138,15 +246,71 @@ const WhitelistList = () => {
]
};
+ const ruleExceptionActionsInTable: {
+ buttons: ActiontechTableActionMeta[];
+ } = {
+ buttons: [
+ {
+ key: 'view-rule-exception-audit',
+ text: t('whitelist.ruleException.viewAudit'),
+ buttonProps: (record) => ({
+ onClick: viewRuleExceptionAudit.bind(null, record)
+ })
+ },
+ {
+ key: 'remove-rule-exception',
+ text: t('whitelist.ruleException.cancelAction'),
+ buttonProps: () => ({ danger: true }),
+ confirm: (record) => ({
+ title: t('whitelist.ruleException.confirmCancel'),
+ onConfirm: removeRuleException.bind(
+ null,
+ record?.sql_rule_exception_id ?? 0
+ )
+ })
+ }
+ ]
+ };
+
const filterCustomProps = useMemo(() => {
return new Map([
['match_type', { options: whitelistMatchTypeOptions }]
]);
}, []);
+ const ruleExceptionFilterCustomProps = useMemo(() => {
+ return new Map([
+ ['instance_name', { options: instanceIDOptions }],
+ [
+ 'created_by',
+ {
+ options: usernameOptions.map((item) => ({
+ ...item,
+ value: item.text
+ }))
+ }
+ ],
+ ['created_at', { showTime: true }]
+ ]);
+ }, [instanceIDOptions, usernameOptions]);
+
const { filterButtonMeta, filterContainerMeta, updateAllSelectedFilterItem } =
useTableFilterContainer(columns, updateTableFilterInfo);
+ const {
+ filterButtonMeta: ruleExceptionFilterButtonMeta,
+ filterContainerMeta: ruleExceptionFilterContainerMeta,
+ updateAllSelectedFilterItem: updateAllSelectedRuleExceptionFilterItem
+ } = useTableFilterContainer(
+ ruleExceptionColumns,
+ updateRuleExceptionTableFilterInfo
+ );
+
+ useEffect(() => {
+ updateInstanceList({ project_name: projectName });
+ updateUsernameList({ filter_project: projectName });
+ }, [projectName, updateInstanceList, updateUsernameList]);
+
useEffect(() => {
const { unsubscribe } = EventEmitter.subscribe(
EmitterKey.Refresh_Whitelist_List,
@@ -155,6 +319,33 @@ const WhitelistList = () => {
return unsubscribe;
}, [refresh]);
+ const segmentedOptions = useMemo(
+ () => [
+ {
+ label: t('whitelist.view.sql'),
+ value: WhitelistManageView.sql
+ },
+ {
+ label: t('whitelist.view.rule'),
+ value: WhitelistManageView.rule
+ }
+ ],
+ [t]
+ );
+
+ const isSqlWhitelistView = activeView === WhitelistManageView.sql;
+
+ useEffect(() => {
+ if (!isSqlWhitelistView) {
+ refreshRuleException();
+ }
+ }, [
+ isSqlWhitelistView,
+ refreshRuleException,
+ ruleExceptionPagination,
+ ruleExceptionTableFilterInfo
+ ]);
+
return (
<>
{messageContextHolder}
@@ -174,41 +365,96 @@ const WhitelistList = () => {
]}
/>
- {
- refreshBySearchKeyword();
- }
- }}
- loading={loading}
- />
-
- {
- return `${record?.audit_whitelist_id}`;
- }}
- pagination={{
- total: whitelistList?.total ?? 0
+ {
+ setActiveView(value as WhitelistManageView);
}}
- loading={loading}
- columns={columns}
- actions={actionPermission ? whitelistActionsInTable : undefined}
- errorMessage={requestErrorMessage}
- onChange={tableChange}
- scroll={{}}
/>
+ {isSqlWhitelistView ? (
+ <>
+
+ refreshButton={{
+ refresh,
+ disabled: loading
+ }}
+ filterButton={{
+ filterButtonMeta,
+ updateAllSelectedFilterItem
+ }}
+ searchInput={{
+ onChange: setSearchKeyword,
+ onSearch: refreshBySearchKeyword
+ }}
+ loading={loading}
+ />
+
+ {
+ return `${record?.audit_whitelist_id}`;
+ }}
+ pagination={{
+ total: whitelistList?.total ?? 0
+ }}
+ loading={loading}
+ columns={columns}
+ actions={actionPermission ? whitelistActionsInTable : undefined}
+ errorMessage={requestErrorMessage}
+ onChange={tableChange}
+ scroll={{}}
+ />
+ >
+ ) : (
+ <>
+
+ refreshButton={{
+ refresh: refreshRuleException,
+ disabled: ruleExceptionLoading
+ }}
+ filterButton={{
+ filterButtonMeta: ruleExceptionFilterButtonMeta,
+ updateAllSelectedFilterItem:
+ updateAllSelectedRuleExceptionFilterItem
+ }}
+ searchInput={{
+ onChange: setRuleExceptionSearchKeyword,
+ onSearch: refreshRuleExceptionBySearchKeyword
+ }}
+ loading={ruleExceptionLoading}
+ />
+
+ {
+ return `${
+ record?.sql_rule_exception_id ??
+ `${record.project_id}-${record.instance_id}-${record.sql_fingerprint}-${record.rule_name}`
+ }`;
+ }}
+ pagination={{
+ total: ruleExceptionList?.total ?? 0
+ }}
+ loading={ruleExceptionLoading}
+ columns={ruleExceptionColumns}
+ actions={actionPermission ? ruleExceptionActionsInTable : undefined}
+ errorMessage={requestErrorMessage}
+ onChange={ruleExceptionTableChange}
+ scroll={{ x: 1600 }}
+ />
+ >
+ )}
>
);
diff --git a/packages/sqle/src/page/Whitelist/__snapshots__/index.test.tsx.snap b/packages/sqle/src/page/Whitelist/__snapshots__/index.test.tsx.snap
index 7799c085b6..ef2d802549 100644
--- a/packages/sqle/src/page/Whitelist/__snapshots__/index.test.tsx.snap
+++ b/packages/sqle/src/page/Whitelist/__snapshots__/index.test.tsx.snap
@@ -43,6 +43,43 @@ exports[`slqe/Whitelist should render white list 1`] = `
+
diff --git a/packages/sqle/src/page/Whitelist/index.test.tsx b/packages/sqle/src/page/Whitelist/index.test.tsx
index f5914b155a..ff205cbdda 100644
--- a/packages/sqle/src/page/Whitelist/index.test.tsx
+++ b/packages/sqle/src/page/Whitelist/index.test.tsx
@@ -2,11 +2,14 @@ import { screen, cleanup, act } from '@testing-library/react';
import WhiteList from '.';
import { renderWithReduxAndTheme } from '@actiontech/shared/lib/testUtil/customRender';
import auditWhiteList from '../../testUtils/mockApi/auditWhiteList';
+import instance from '../../testUtils/mockApi/instance';
+import user from '../../testUtils/mockApi/user';
import { getBySelector } from '@actiontech/shared/lib/testUtil/customQuery';
import { useSelector } from 'react-redux';
import { ModalName } from '../../data/ModalName';
import { mockUseCurrentProject } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentProject';
import { mockUseCurrentUser } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentUser';
+import { driverMeta } from '../../hooks/useDatabaseType/index.test.data';
jest.mock('react-redux', () => {
return {
@@ -15,13 +18,23 @@ jest.mock('react-redux', () => {
};
});
+jest.mock('react-router-dom', () => {
+ return {
+ ...jest.requireActual('react-router-dom'),
+ useNavigate: jest.fn()
+ };
+});
+
describe('slqe/Whitelist', () => {
let whiteListSpy: jest.SpyInstance;
beforeEach(() => {
jest.useFakeTimers();
whiteListSpy = auditWhiteList.getAuditWhitelist();
+ instance.getInstanceTipList();
+ user.getUserTipList();
(useSelector as jest.Mock).mockImplementation((e) =>
e({
+ database: { driverMeta },
whitelist: { modalStatus: { [ModalName.Add_Whitelist]: false } }
})
);
diff --git a/packages/sqle/src/page/Whitelist/index.tsx b/packages/sqle/src/page/Whitelist/index.tsx
index 5a7de585bb..03a6b1724a 100644
--- a/packages/sqle/src/page/Whitelist/index.tsx
+++ b/packages/sqle/src/page/Whitelist/index.tsx
@@ -1,29 +1,7 @@
-import { useTranslation } from 'react-i18next';
-import { EnterpriseFeatureDisplay, PageHeader } from '@actiontech/shared';
-import { Typography } from 'antd';
import WhitelistList from './List';
const Whitelist = () => {
- const { t } = useTranslation();
-
- return (
- <>
- {/* #if [ce] */}
-
- {/* #endif */}
-
-
- {t('whitelist.ceTips')}
-
- }
- >
-
-
- >
- );
+ return
;
};
export default Whitelist;
diff --git a/packages/sqle/src/router/config.tsx b/packages/sqle/src/router/config.tsx
index 487b99360a..72c5ece062 100644
--- a/packages/sqle/src/router/config.tsx
+++ b/packages/sqle/src/router/config.tsx
@@ -181,6 +181,10 @@ const UpdateCustomRule = React.lazy(
);
const ReportStatistics = React.lazy(() => import('../page/ReportStatistics'));
+const SqlManagementRemediationReport = React.lazy(
+ () => import('../page/SqlManagementRemediationReport')
+);
+
const PushRuleConfiguration = React.lazy(
() => import('../page/PushRuleConfiguration')
);
@@ -456,6 +460,12 @@ export const projectDetailRouterConfig: RouterConfigItem[] = [
];
export const globalRouterConfig: RouterConfigItem[] = [
+ {
+ path: 'sqle/sql-management-remediation-report',
+ element:
,
+ key: 'sqlManagementRemediationReport',
+ role: [SystemRole.admin]
+ },
{
path: 'sqle/report-statistics',
label: 'menu.reportStatistics',
diff --git a/packages/sqle/src/testUtils/mockApi/auditWhiteList/data.ts b/packages/sqle/src/testUtils/mockApi/auditWhiteList/data.ts
index b6c39a9980..778367c593 100644
--- a/packages/sqle/src/testUtils/mockApi/auditWhiteList/data.ts
+++ b/packages/sqle/src/testUtils/mockApi/auditWhiteList/data.ts
@@ -1,4 +1,7 @@
-import { IAuditWhitelistResV1 } from '@actiontech/shared/lib/api/sqle/service/common';
+import {
+ IAuditWhitelistResV1,
+ ISQLRuleExceptionResV1
+} from '@actiontech/shared/lib/api/sqle/service/common';
import { CreateAuditWhitelistReqV1MatchTypeEnum } from '@actiontech/shared/lib/api/sqle/service/common.enum';
export const auditWhiteListMockData: IAuditWhitelistResV1[] = [
@@ -30,3 +33,21 @@ export const auditWhiteListMockData: IAuditWhitelistResV1[] = [
desc: 'test4'
}
];
+
+export const sqlRuleExceptionMockData: ISQLRuleExceptionResV1[] = [
+ {
+ sql_rule_exception_id: 11,
+ project_name: 'default',
+ instance_id: '1739531854064652288',
+ instance_name: 'mysql_local_sqle',
+ sql_fingerprint: 'create table rule_exc_management (id int)',
+ rule_name: 'ddl_check_pk_not_exist',
+ rule_desc: '建表语句必须包含主键',
+ rule_level: 'error',
+ reason: '标准管理页回归验证',
+ created_by: 'admin',
+ created_at: '2026-06-19T02:40:00+00:00',
+ matched_count: 2,
+ last_match_time: '2026-06-19T02:50:00+00:00'
+ }
+];
diff --git a/packages/sqle/src/testUtils/mockApi/auditWhiteList/index.ts b/packages/sqle/src/testUtils/mockApi/auditWhiteList/index.ts
index 81905050f4..f53936c01a 100644
--- a/packages/sqle/src/testUtils/mockApi/auditWhiteList/index.ts
+++ b/packages/sqle/src/testUtils/mockApi/auditWhiteList/index.ts
@@ -3,13 +3,15 @@ import {
MockSpyApy,
createSpySuccessResponse
} from '@actiontech/shared/lib/testUtil/mockApi';
-import { auditWhiteListMockData } from './data';
+import { auditWhiteListMockData, sqlRuleExceptionMockData } from './data';
class AuditWhiteList implements MockSpyApy {
public mockAllApi(): void {
this.getAuditWhitelist();
this.deleteAuthWhitelist();
this.addAuthWhitelist();
+ this.getSQLRuleException();
+ this.deleteSQLRuleException();
}
public getAuditWhitelist() {
@@ -39,6 +41,22 @@ class AuditWhiteList implements MockSpyApy {
spy.mockImplementation(() => createSpySuccessResponse({}));
return spy;
}
+
+ public getSQLRuleException() {
+ const spy = jest.spyOn(audit_whitelist, 'getSQLRuleExceptionV1');
+ spy.mockImplementation(() =>
+ createSpySuccessResponse({
+ data: sqlRuleExceptionMockData
+ })
+ );
+ return spy;
+ }
+
+ public deleteSQLRuleException() {
+ const spy = jest.spyOn(audit_whitelist, 'deleteSQLRuleExceptionV1');
+ spy.mockImplementation(() => createSpySuccessResponse({}));
+ return spy;
+ }
}
export default new AuditWhiteList();
diff --git a/packages/sqle/src/testUtils/mockApi/instance/index.ts b/packages/sqle/src/testUtils/mockApi/instance/index.ts
index 8a7fa4c6b4..937214cead 100644
--- a/packages/sqle/src/testUtils/mockApi/instance/index.ts
+++ b/packages/sqle/src/testUtils/mockApi/instance/index.ts
@@ -13,6 +13,7 @@ import {
class MockInstanceApi implements MockSpyApy {
public mockAllApi(): void {
this.getInstanceTipList();
+ this.getInstanceTipListV2();
this.getInstanceSchemas();
this.batchCheckInstanceIsConnectableByName();
this.getInstance();
@@ -29,6 +30,17 @@ class MockInstanceApi implements MockSpyApy {
return spy;
}
+ public getInstanceTipListV2() {
+ const spy = jest.spyOn(instance, 'getInstanceTipListV2');
+ spy.mockImplementation(() =>
+ createSpySuccessResponse({
+ data: instanceTipsMockData,
+ total_nums: instanceTipsMockData.length
+ })
+ );
+ return spy;
+ }
+
public getInstance() {
const spy = jest.spyOn(instance, 'getInstanceV2');
spy.mockImplementation(() =>