From 2f1abdf3f74e8eb588816d0328fff51a1901d5da Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Fri, 19 Jun 2026 02:18:32 +0800 Subject: [PATCH 1/2] feat: add SQL remediation tracking delivery Fixes #2988 --- packages/base/src/locale/en-US/dmsMenu.ts | 1 + packages/base/src/locale/zh-CN/dmsMenu.ts | 1 + .../UserMenu/components/GlobalSetting.tsx | 14 +- packages/base/src/scripts/version.ts | 2 +- .../lib/api/sqle/service/SqlManage/index.d.ts | 42 ++++- .../api/sqle/service/SqlManage/index.enum.ts | 20 +++ .../lib/api/sqle/service/SqlManage/index.ts | 69 ++++++- .../shared/lib/api/sqle/service/common.d.ts | 84 +++++++++ packages/sqle/src/data/EmitterKey.ts | 1 + .../sqle/src/locale/en-US/managementConf.ts | 13 ++ .../sqle/src/locale/en-US/sqlManagement.ts | 27 ++- .../sqle/src/locale/zh-CN/managementConf.ts | 12 ++ .../sqle/src/locale/zh-CN/sqlManagement.ts | 38 +++- .../page/SqlAnalyze/SqlAnalyze/SqlAnalyze.tsx | 21 ++- .../src/page/SqlAnalyze/SqlAnalyze/index.tsx | 2 + .../src/page/SqlAnalyze/SqlAnalyze/style.ts | 36 ++++ .../SqlManage/RemediationCompare.tsx | 91 ++++++++++ .../src/page/SqlAnalyze/SqlManage/index.tsx | 47 +++-- .../SQLEEIndex/RemediationStatusTag.tsx | 36 ++++ .../component/SQLEEIndex/column.tsx | 16 ++ .../SQLEEIndex/hooks/useGetTableFilterInfo.ts | 15 +- .../component/SQLEEIndex/index.tsx | 65 +++++-- .../ScanTypeSqlCollection/index.type.ts | 2 + .../Detail/ScanTypeSqlCollection/indx.tsx | 169 +++++++++++++++++- .../Detail/ScanTypeSqlCollection/style.ts | 57 +++++- .../page/SqlManagementConf/Detail/index.tsx | 79 +++++++- .../SqlManagementRemediationReport/index.tsx | 70 ++++++++ packages/sqle/src/router/config.tsx | 10 ++ 28 files changed, 1002 insertions(+), 38 deletions(-) create mode 100644 packages/sqle/src/page/SqlAnalyze/SqlManage/RemediationCompare.tsx create mode 100644 packages/sqle/src/page/SqlManagement/component/SQLEEIndex/RemediationStatusTag.tsx create mode 100644 packages/sqle/src/page/SqlManagementRemediationReport/index.tsx diff --git a/packages/base/src/locale/en-US/dmsMenu.ts b/packages/base/src/locale/en-US/dmsMenu.ts index 3d91f1a22a..6c37b233ec 100644 --- a/packages/base/src/locale/en-US/dmsMenu.ts +++ b/packages/base/src/locale/en-US/dmsMenu.ts @@ -54,6 +54,7 @@ export default { title: 'Global settings', userCenter: 'User center', reportStatistics: 'Report statistics', + sqlManagementRemediationReport: 'SQL management remediation report', viewRule: 'View rule', ruleManage: 'Rule management', system: 'System settings', diff --git a/packages/base/src/locale/zh-CN/dmsMenu.ts b/packages/base/src/locale/zh-CN/dmsMenu.ts index 14cf0a1dc7..cf1975d789 100644 --- a/packages/base/src/locale/zh-CN/dmsMenu.ts +++ b/packages/base/src/locale/zh-CN/dmsMenu.ts @@ -56,6 +56,7 @@ export default { title: '全局设置', userCenter: '用户中心', reportStatistics: '报表统计', + sqlManagementRemediationReport: 'SQL 管控整改报表', viewRule: '查看规则', ruleManage: '规则管理', system: '系统设置', diff --git a/packages/base/src/page/Nav/SideMenu/UserMenu/components/GlobalSetting.tsx b/packages/base/src/page/Nav/SideMenu/UserMenu/components/GlobalSetting.tsx index a90d2ee7a6..a3ea90420d 100644 --- a/packages/base/src/page/Nav/SideMenu/UserMenu/components/GlobalSetting.tsx +++ b/packages/base/src/page/Nav/SideMenu/UserMenu/components/GlobalSetting.tsx @@ -14,7 +14,8 @@ import { // #if [sqle] ProfileSquareFilled, SignalFilled, - ProfileEditFilled + ProfileEditFilled, + ManagementFilled // #endif } from '@actiontech/icons'; @@ -91,6 +92,17 @@ const GlobalSetting: React.FC<{ {t('dmsMenu.globalSettings.reportStatistics')} +
+ handleClickItem('/sqle/sql-management-remediation-report') + } + > + + + {t('dmsMenu.globalSettings.sqlManagementRemediationReport')} + +
handleClickItem(`/sqle/rule`)} diff --git a/packages/base/src/scripts/version.ts b/packages/base/src/scripts/version.ts index 2bf915458b..c94a2e835f 100644 --- a/packages/base/src/scripts/version.ts +++ b/packages/base/src/scripts/version.ts @@ -1 +1 @@ -export const UI_VERSION = 'feature/add-make-command ba9c9c5'; +export const UI_VERSION = 'zjrc_3.2408 a32b5993'; diff --git a/packages/shared/lib/api/sqle/service/SqlManage/index.d.ts b/packages/shared/lib/api/sqle/service/SqlManage/index.d.ts index 01e56d6156..af275c9db7 100644 --- a/packages/shared/lib/api/sqle/service/SqlManage/index.d.ts +++ b/packages/shared/lib/api/sqle/service/SqlManage/index.d.ts @@ -10,10 +10,12 @@ import { exportSqlManageV1FilterStatusEnum, exportSqlManageV1SortFieldEnum, exportSqlManageV1SortOrderEnum, + exportSqlManageRemediationV1ExportScopeEnum, GetSqlManageListV2FilterSourceEnum, GetSqlManageListV2FilterAuditLevelEnum, GetSqlManageListV2FilterStatusEnum, GetSqlManageListV2FilterPriorityEnum, + GetSqlManageListV2FilterRemediationStatusEnum, GetSqlManageListV2SortFieldEnum, GetSqlManageListV2SortOrderEnum } from './index.enum'; @@ -23,7 +25,9 @@ import { IBatchUpdateSqlManageReq, IBaseRes, IGetSqlManageRuleTipsResp, - IGetSqlManageSqlAnalysisResp + IGetSqlManageSqlAnalysisResp, + IGetSqlManageRemediationResp, + IGetSqlManageRemediationOverviewResp } from '../common.d'; export interface IGetSqlManageListParams { @@ -106,6 +110,20 @@ export interface IExportSqlManageV1Params { sort_order?: exportSqlManageV1SortOrderEnum; } +export interface IExportGlobalSqlManageRemediationV1Params {} + +export interface IExportSqlManageRemediationV1Params { + project_name: string; + + export_scope: exportSqlManageRemediationV1ExportScopeEnum; + + filter_instance_id?: string; + + instance_audit_plan_id?: number; + + audit_plan_type?: string; +} + export interface IGetSqlManageRuleTipsParams { project_name: string; } @@ -141,6 +159,8 @@ export interface IGetSqlManageListV2Params { filter_status?: GetSqlManageListV2FilterStatusEnum; + filter_remediation_status?: GetSqlManageListV2FilterRemediationStatusEnum; + filter_rule_name?: string; filter_db_type?: string; @@ -163,3 +183,23 @@ export interface IGetSqlManageListV2Params { } export interface IGetSqlManageListV2Return extends IGetSqlManageListResp {} + +export interface IGetSqlManageRemediationV1Params { + project_name: string; + + sql_manage_id: string; +} + +export interface IGetSqlManageRemediationV1Return + extends IGetSqlManageRemediationResp {} + +export interface IGetSqlManageRemediationOverviewV1Params { + project_name: string; + + instance_audit_plan_id?: number; + + audit_plan_type?: string; +} + +export interface IGetSqlManageRemediationOverviewV1Return + extends IGetSqlManageRemediationOverviewResp {} diff --git a/packages/shared/lib/api/sqle/service/SqlManage/index.enum.ts b/packages/shared/lib/api/sqle/service/SqlManage/index.enum.ts index 98bef5cd12..21026caca9 100644 --- a/packages/shared/lib/api/sqle/service/SqlManage/index.enum.ts +++ b/packages/shared/lib/api/sqle/service/SqlManage/index.enum.ts @@ -86,6 +86,14 @@ export enum exportSqlManageV1SortOrderEnum { 'desc' = 'desc' } +export enum exportSqlManageRemediationV1ExportScopeEnum { + 'project' = 'project', + + 'data_source' = 'data_source', + + 'scan_task' = 'scan_task' +} + export enum GetSqlManageListV2FilterSourceEnum { 'audit_plan' = 'audit_plan', @@ -118,6 +126,18 @@ export enum GetSqlManageListV2FilterPriorityEnum { 'low' = 'low' } +export enum GetSqlManageListV2FilterRemediationStatusEnum { + 'resolved' = 'resolved', + + 'partially_fixed' = 'partially_fixed', + + 'unchanged' = 'unchanged', + + 'deteriorated' = 'deteriorated', + + 'newly_discovered' = 'newly_discovered' +} + export enum GetSqlManageListV2SortFieldEnum { 'first_appear_timestamp' = 'first_appear_timestamp', diff --git a/packages/shared/lib/api/sqle/service/SqlManage/index.ts b/packages/shared/lib/api/sqle/service/SqlManage/index.ts index 7ba00d3593..cff70a956d 100644 --- a/packages/shared/lib/api/sqle/service/SqlManage/index.ts +++ b/packages/shared/lib/api/sqle/service/SqlManage/index.ts @@ -12,12 +12,18 @@ import { IBatchUpdateSqlManageParams, IBatchUpdateSqlManageReturn, IExportSqlManageV1Params, + IExportGlobalSqlManageRemediationV1Params, + IExportSqlManageRemediationV1Params, IGetSqlManageRuleTipsParams, IGetSqlManageRuleTipsReturn, IGetSqlManageSqlAnalysisV1Params, IGetSqlManageSqlAnalysisV1Return, IGetSqlManageListV2Params, - IGetSqlManageListV2Return + IGetSqlManageListV2Return, + IGetSqlManageRemediationV1Params, + IGetSqlManageRemediationV1Return, + IGetSqlManageRemediationOverviewV1Params, + IGetSqlManageRemediationOverviewV1Return } from './index.d'; class SqlManageService extends ServiceBase { @@ -66,6 +72,34 @@ class SqlManageService extends ServiceBase { ); } + public exportGlobalSqlManageRemediationV1( + params: IExportGlobalSqlManageRemediationV1Params = {}, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + + return this.get( + `/v1/sql_manages/remediation_exports`, + paramsData, + options + ); + } + + public exportSqlManageRemediationV1( + params: IExportSqlManageRemediationV1Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + const project_name = paramsData.project_name; + delete paramsData.project_name; + + return this.get( + `/v1/projects/${project_name}/sql_manages/remediation_exports`, + paramsData, + options + ); + } + public GetSqlManageRuleTips( params: IGetSqlManageRuleTipsParams, options?: AxiosRequestConfig @@ -113,6 +147,39 @@ class SqlManageService extends ServiceBase { options ); } + + public GetSqlManageRemediationV1( + params: IGetSqlManageRemediationV1Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + const project_name = paramsData.project_name; + delete paramsData.project_name; + + const sql_manage_id = paramsData.sql_manage_id; + delete paramsData.sql_manage_id; + + return this.get( + `/v1/projects/${project_name}/sql_manages/${sql_manage_id}/remediation`, + paramsData, + options + ); + } + + public getSqlManageRemediationOverviewV1( + params: IGetSqlManageRemediationOverviewV1Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + const project_name = paramsData.project_name; + delete paramsData.project_name; + + return this.get( + `/v1/projects/${project_name}/sql_manages/remediation_overview`, + paramsData, + options + ); + } } export default new SqlManageService(); diff --git a/packages/shared/lib/api/sqle/service/common.d.ts b/packages/shared/lib/api/sqle/service/common.d.ts index 71e67a8c9d..f432835830 100644 --- a/packages/shared/lib/api/sqle/service/common.d.ts +++ b/packages/shared/lib/api/sqle/service/common.d.ts @@ -2360,6 +2360,16 @@ export interface ISqlManage { remark?: string; + remediation_status?: string; + + first_audit_missing?: boolean; + + first_audit_result?: IAuditResult[]; + + first_audit_time?: string; + + rule_diff?: IRuleDiff; + schema_name?: string; source?: ISource; @@ -3458,3 +3468,77 @@ export interface IWorkflowStepResV2 { workflow_step_id?: number; } + +export interface IRuleDiff { + new?: IAuditResult[]; + + resolved?: IAuditResult[]; + + unchanged?: IAuditResult[]; +} + +export interface ISqlManageRemediation { + first_audit_missing?: boolean; + + first_audit_result?: IAuditResult[]; + + first_audit_time?: string; + + id?: number; + + latest_audit_result?: IAuditResult[]; + + latest_audit_time?: string; + + remediation_status?: string; + + rule_diff?: IRuleDiff; + + sql?: string; + + sql_fingerprint?: string; +} + +export interface IGetSqlManageRemediationResp { + code?: number; + + data?: ISqlManageRemediation; + + message?: string; +} + +export interface ISqlManageRemediationOverviewStatusCount { + deteriorated?: number; + + newly_discovered?: number; + + partially_fixed?: number; + + resolved?: number; + + unchanged?: number; +} + +export interface ISqlManageRemediationOverview { + first_audit_missing_num?: number; + + first_score?: number; + + latest_score?: number; + + remediation_rate?: number; + + score_change?: number; + + sql_total_num?: number; + + remediation_status_count?: ISqlManageRemediationOverviewStatusCount; +} + +export interface IGetSqlManageRemediationOverviewResp { + code?: number; + + data?: ISqlManageRemediationOverview; + + message?: string; +} diff --git a/packages/sqle/src/data/EmitterKey.ts b/packages/sqle/src/data/EmitterKey.ts index 3bb4197dad..09802cf7fe 100644 --- a/packages/sqle/src/data/EmitterKey.ts +++ b/packages/sqle/src/data/EmitterKey.ts @@ -30,6 +30,7 @@ enum EmitterKey { Refresh_Sql_Management_Conf_Overview_List = 'Refresh_Sql_Management_Conf_Overview_List', Refresh_Sql_Management_Conf_Detail_Sql_List = 'Refresh_Sql_Management_Conf_Detail_Sql_List', Export_Sql_Management_Conf_Detail_Sql_List = 'Export_Sql_Management_Conf_Detail_Sql_List', + Export_Sql_Management_Conf_Detail_Remediation = 'Export_Sql_Management_Conf_Detail_Remediation', Refresh_Sql_management_Exception_List = 'Refresh_Sql_management_Exception_List', diff --git a/packages/sqle/src/locale/en-US/managementConf.ts b/packages/sqle/src/locale/en-US/managementConf.ts index ba29a61570..a515518e1a 100644 --- a/packages/sqle/src/locale/en-US/managementConf.ts +++ b/packages/sqle/src/locale/en-US/managementConf.ts @@ -111,6 +111,19 @@ export default { auditImmediately: 'Audit immediately', auditImmediatelySuccessTips: 'Audit successfully', exportTips: 'Exporting scan task details', + remediationExport: 'SQL remediation', + remediationExportTips: 'Exporting SQL management remediation report', + remediationExportSuccessTips: + 'Export SQL management remediation report successfully', + remediationOverview: { + title: 'Remediation overview', + sqlTotal: 'SQL total', + firstScore: 'First score', + latestScore: 'Latest score', + scoreChange: 'Score change', + remediationRate: 'Remediation rate', + loadFailed: 'Failed to load remediation overview: {{message}}' + }, overview: { title: 'Overview', column: { diff --git a/packages/sqle/src/locale/en-US/sqlManagement.ts b/packages/sqle/src/locale/en-US/sqlManagement.ts index 038fd5144d..e46db8cc91 100644 --- a/packages/sqle/src/locale/en-US/sqlManagement.ts +++ b/packages/sqle/src/locale/en-US/sqlManagement.ts @@ -5,9 +5,26 @@ export default { action: { export: 'Export', exporting: 'Exporting file', - exportSuccessTips: 'Export file successfully' + exportSuccessTips: 'Export file successfully', + remediationExport: 'SQL remediation', + remediationExporting: 'Exporting SQL management remediation report', + remediationExportSuccessTips: + 'Export SQL management remediation report successfully' } }, + remediationReport: { + pageTitle: 'SQL management remediation report', + description: + 'Export SQL management remediation tracking data in global scope. The Excel file contains Overview, Rule summary and Details.', + exportButton: 'Export SQL management remediation report', + exporting: 'Exporting SQL management remediation report', + exportSuccessTips: 'Export SQL management remediation report successfully', + scopeTitle: 'Export scope', + scopeContent: + 'Global scope: includes SQL management remediation data across all projects available to platform administrators.', + permissionTips: + 'Only platform administrators / global operators can see and export this report.' + }, statistics: { SQLTotalNum: 'SQL total', problemSQlNum: 'Problem SQL', @@ -57,9 +74,17 @@ export default { occurrenceCount: 'Occurrence count', personInCharge: 'Person in charge', status: 'Status', + remediationStatus: 'Remediation status', comment: 'Comment', endpoints: 'Endpoint info' }, + remediationStatus: { + resolved: 'Resolved', + partially_fixed: 'Partially fixed', + unchanged: 'Unchanged', + deteriorated: 'Deteriorated', + newly_discovered: 'Newly discovered' + }, filter: { time: 'Time range', status: { diff --git a/packages/sqle/src/locale/zh-CN/managementConf.ts b/packages/sqle/src/locale/zh-CN/managementConf.ts index 71b2c965b4..0b9100561e 100644 --- a/packages/sqle/src/locale/zh-CN/managementConf.ts +++ b/packages/sqle/src/locale/zh-CN/managementConf.ts @@ -107,6 +107,18 @@ export default { auditImmediately: '立即审核', auditImmediatelySuccessTips: '审核成功', exportTips: '正在导出扫描任务详情', + remediationExport: 'SQL 管控整改', + remediationExportTips: '正在导出 SQL 管控整改报表', + remediationExportSuccessTips: 'SQL 管控整改报表导出成功', + remediationOverview: { + title: '整改概览', + sqlTotal: 'SQL 总数', + firstScore: '首次评分', + latestScore: '最末次评分', + scoreChange: '评分变化', + remediationRate: '整改率', + loadFailed: '整改概览加载失败:{{message}}' + }, overview: { title: '概览', column: { diff --git a/packages/sqle/src/locale/zh-CN/sqlManagement.ts b/packages/sqle/src/locale/zh-CN/sqlManagement.ts index c21fda69b1..b70e1fe431 100644 --- a/packages/sqle/src/locale/zh-CN/sqlManagement.ts +++ b/packages/sqle/src/locale/zh-CN/sqlManagement.ts @@ -5,9 +5,24 @@ export default { action: { export: '导出', exporting: '正在导出文件', - exportSuccessTips: '导出文件成功' + exportSuccessTips: '导出文件成功', + remediationExport: 'SQL 管控整改', + remediationExporting: '正在导出 SQL 管控整改报表', + remediationExportSuccessTips: 'SQL 管控整改报表导出成功' } }, + remediationReport: { + pageTitle: 'SQL 管控整改报表', + description: + '导出全局范围内 SQL 管控整改追踪数据,Excel 包含概览、规则维度汇总和明细。', + exportButton: '导出 SQL 管控整改报表', + exporting: '正在导出 SQL 管控整改报表', + exportSuccessTips: 'SQL 管控整改报表导出成功', + scopeTitle: '导出范围', + scopeContent: + '全局范围:覆盖当前用户有平台管理权限的全部项目 SQL 管控整改数据', + permissionTips: '仅平台超管 / 全局运维可见并可导出。' + }, statistics: { SQLTotalNum: 'SQL总数', problemSQlNum: '问题SQL数', @@ -63,9 +78,17 @@ export default { occurrenceCount: '出现数量', personInCharge: '负责人', status: '状态', + remediationStatus: '整改状态', comment: '备注', endpoints: '端点信息' }, + remediationStatus: { + resolved: '已整改', + partially_fixed: '部分整改', + unchanged: '未变化', + deteriorated: '恶化', + newly_discovered: '新发现' + }, filter: { time: '时间范围', status: { @@ -96,5 +119,18 @@ export default { statusReport: { title: 'SQL审核结果' } + }, + remediationCompare: { + tab: '整改对比', + title: '整改对比', + description: '对比首次审核与最末次审核结果,展示规则整改变化', + firstAuditMissing: '无首次审核快照,当前按最末次结果展示新发现问题。', + firstAuditResult: '首次审核结果', + latestAuditResult: '最末次审核结果', + ruleDiffTitle: '规则差异', + resolved: '已解决规则', + new: '新增规则', + unchanged: '维持规则', + emptyRules: '无命中规则' } }; diff --git a/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/SqlAnalyze.tsx b/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/SqlAnalyze.tsx index 988bc0ba9c..c22bb1a71f 100644 --- a/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/SqlAnalyze.tsx +++ b/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/SqlAnalyze.tsx @@ -13,6 +13,7 @@ import { SqlAnalyzeContStyleWrapper, SqlContStyleWrapper } from './style'; import useTableSchema from './useTableSchema'; import useSQLExecPlan from './useSQLExecPlan'; import { SqlAnalyzeProps } from '.'; +import RemediationCompare from '../SqlManage/RemediationCompare'; const SqlAnalyze: React.FC = (props) => { const { t } = useTranslation(); @@ -22,6 +23,7 @@ const SqlAnalyze: React.FC = (props) => { errorMessage, loading = false, performanceStatistics, + remediationCompare, errorType = 'error' } = props; @@ -50,9 +52,21 @@ const SqlAnalyze: React.FC = (props) => { tableMetas?.table_meta_items.length ) ) { - return [{ label: t('sqlAnalyze.sqlExplain'), value: 'sql' }]; + return [ + { label: t('sqlAnalyze.sqlExplain'), value: 'sql' }, + { + label: t('sqlManagement.remediationCompare.tab'), + value: 'remediation' + } + ]; } - return [{ label: t('sqlAnalyze.sqlExplain'), value: 'sql' }].concat( + return [ + { label: t('sqlAnalyze.sqlExplain'), value: 'sql' }, + { + label: t('sqlManagement.remediationCompare.tab'), + value: 'remediation' + } + ].concat( (tableMetas?.table_meta_items ?? []).map((table) => { return { label: t('sqlAnalyze.tableTitle', { @@ -86,6 +100,9 @@ const SqlAnalyze: React.FC = (props) => { ...sqlExplain, ...performanceStatistics })} + {tabStatus === 'remediation' && ( + + )} {tableMetas?.table_meta_items?.map((table) => { return ( diff --git a/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/index.tsx b/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/index.tsx index 2d7e6f255c..2a0da0c250 100644 --- a/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/index.tsx +++ b/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/index.tsx @@ -2,6 +2,7 @@ import { ResultStatusType } from 'antd/es/result'; import SqlAnalyze from './SqlAnalyze'; import { IPerformanceStatistics, + ISqlManageRemediation, ISQLExplain, ITableMeta, ITableMetas @@ -13,6 +14,7 @@ export type SqlAnalyzeProps = { tableMetas?: ITableMetas; sqlExplain?: ISQLExplain; performanceStatistics?: IPerformanceStatistics; + remediationCompare?: ISqlManageRemediation; loading?: boolean; }; diff --git a/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/style.ts b/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/style.ts index f2db374ed5..d790d7c004 100644 --- a/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/style.ts +++ b/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/style.ts @@ -84,4 +84,40 @@ export const SqlContStyleWrapper = styled('section')` line-height: 28px; } } + + .remediation-compare-wrapper { + padding: 24px 40px; + + .remediation-section-space { + width: 100%; + } + + .remediation-header { + display: flex; + align-items: center; + justify-content: space-between; + } + + .remediation-title { + margin-bottom: 0; + } + + .remediation-columns, + .remediation-diff-columns { + display: grid; + grid-column-gap: 16px; + } + + .remediation-columns { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .remediation-diff-columns { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .ant-list-item { + align-items: flex-start; + } + } `; diff --git a/packages/sqle/src/page/SqlAnalyze/SqlManage/RemediationCompare.tsx b/packages/sqle/src/page/SqlAnalyze/SqlManage/RemediationCompare.tsx new file mode 100644 index 0000000000..3241044a87 --- /dev/null +++ b/packages/sqle/src/page/SqlAnalyze/SqlManage/RemediationCompare.tsx @@ -0,0 +1,91 @@ +import { Empty, Alert, Card, Space, Typography } from 'antd'; +import { useTranslation } from 'react-i18next'; +import AuditResultMessage from '../../../components/AuditResultMessage'; +import { ISqlManageRemediation } from '@actiontech/shared/lib/api/sqle/service/common'; +import RemediationStatusTag from '../../SqlManagement/component/SQLEEIndex/RemediationStatusTag'; + +type RemediationCompareProps = { + data?: ISqlManageRemediation; +}; + +const ruleNames = (rules?: ISqlManageRemediation['rule_diff']) => ({ + resolved: rules?.resolved?.map((rule) => rule.rule_name).filter(Boolean), + newRules: rules?.new?.map((rule) => rule.rule_name).filter(Boolean), + unchanged: rules?.unchanged?.map((rule) => rule.rule_name).filter(Boolean) +}); + +const RuleList: React.FC<{ title: string; rules?: string[] }> = ({ + title, + rules +}) => { + const { t } = useTranslation(); + + return ( + + {title} + {rules?.length ? ( + rules.map((rule) => ( + {rule} + )) + ) : ( + + {t('sqlManagement.remediationCompare.emptyRules')} + + )} + + ); +}; + +const RemediationCompare: React.FC = ({ data }) => { + const { t } = useTranslation(); + + if (!data) { + return ; + } + + const rules = ruleNames(data.rule_diff); + + return ( + + + + + {t('sqlManagement.remediationCompare.description')} + + {data.first_audit_missing && ( + + )} + + + + + + + + + + + + + + + + + + ); +}; + +export default RemediationCompare; diff --git a/packages/sqle/src/page/SqlAnalyze/SqlManage/index.tsx b/packages/sqle/src/page/SqlAnalyze/SqlManage/index.tsx index 008261e069..6506351502 100644 --- a/packages/sqle/src/page/SqlAnalyze/SqlManage/index.tsx +++ b/packages/sqle/src/page/SqlAnalyze/SqlManage/index.tsx @@ -8,6 +8,7 @@ import SqlManage from '@actiontech/shared/lib/api/sqle/service/SqlManage'; import { SQLManageAnalyzeUrlParams } from './index.type'; import { IPerformanceStatistics, + ISqlManageRemediation, ISQLExplain, ITableMetas } from '@actiontech/shared/lib/api/sqle/service/common'; @@ -24,6 +25,8 @@ const SQLManageAnalyze = () => { const [tableMetas, setTableMetas] = useState(); const [performanceStatistics, setPerformancesStatistics] = useState(); + const [remediationCompare, setRemediationCompare] = + useState(); const [ loading, { setTrue: startGetSqlAnalyze, setFalse: getSqlAnalyzeFinish } @@ -33,22 +36,35 @@ const SQLManageAnalyze = () => { const getSqlAnalyze = useCallback(async () => { startGetSqlAnalyze(); try { - const res = await SqlManage.GetSqlManageSqlAnalysisV1({ - sql_manage_id: urlParams.sqlManageId ?? '', - project_name: projectName - }); - if (res.data.code === ResponseCode.SUCCESS) { - setErrorMessage(''); - setSqlExplain(res.data.data?.sql_explain); - setTableMetas(res.data.data?.table_metas); - setPerformancesStatistics(res.data.data?.performance_statistics); - } else { - if (res.data.code === ResponseCode.NotSupportDML) { - setErrorType('info'); - } else { - setErrorType('error'); + const [res, remediationRes] = await Promise.all([ + SqlManage.GetSqlManageSqlAnalysisV1({ + sql_manage_id: urlParams.sqlManageId ?? '', + project_name: projectName + }), + SqlManage.GetSqlManageRemediationV1({ + sql_manage_id: urlParams.sqlManageId ?? '', + project_name: projectName + }) + ]); + + if (res.data.code !== ResponseCode.SUCCESS) { + if (remediationRes.data.code !== ResponseCode.SUCCESS) { + if (res.data.code === ResponseCode.NotSupportDML) { + setErrorType('info'); + } else { + setErrorType('error'); + } + setErrorMessage(res.data.message ?? ''); + return; } - setErrorMessage(res.data.message ?? ''); + } + + setErrorMessage(''); + setSqlExplain(res.data.data?.sql_explain); + setTableMetas(res.data.data?.table_metas); + setPerformancesStatistics(res.data.data?.performance_statistics); + if (remediationRes.data.code === ResponseCode.SUCCESS) { + setRemediationCompare(remediationRes.data.data); } } finally { getSqlAnalyzeFinish(); @@ -71,6 +87,7 @@ const SQLManageAnalyze = () => { sqlExplain={sqlExplain} errorMessage={errorMessage} performanceStatistics={performanceStatistics} + remediationCompare={remediationCompare} loading={loading} /> ); diff --git a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/RemediationStatusTag.tsx b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/RemediationStatusTag.tsx new file mode 100644 index 0000000000..daa9ac7b35 --- /dev/null +++ b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/RemediationStatusTag.tsx @@ -0,0 +1,36 @@ +import { BasicTag } from '@actiontech/shared'; +import { t } from '../../../../locale'; + +export const remediationStatusOptions = [ + 'resolved', + 'partially_fixed', + 'unchanged', + 'deteriorated', + 'newly_discovered' +]; + +type RemediationStatusTagProps = { + status?: string; +}; + +const remediationStatusColor: Record = { + resolved: 'green', + partially_fixed: 'blue', + unchanged: 'gray', + deteriorated: 'red', + newly_discovered: 'orange' +}; + +const RemediationStatusTag: React.FC = ({ + status +}) => { + const currentStatus = status || 'unchanged'; + + return ( + + {t(`sqlManagement.table.remediationStatus.${currentStatus}`)} + + ); +}; + +export default RemediationStatusTag; diff --git a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/column.tsx b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/column.tsx index 212c8651b4..d952816bc3 100644 --- a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/column.tsx +++ b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/column.tsx @@ -19,6 +19,7 @@ import StatusTag from './StatusTag'; import { BasicTag, BasicTypographyEllipsis } from '@actiontech/shared'; import { ACTIONTECH_TABLE_ACTION_BUTTON_WIDTH } from '@actiontech/shared/lib/components/ActiontechTable/hooks/useTableAction'; import { SQLAuditRecordListUrlParamsKey } from './index.data'; +import RemediationStatusTag from './RemediationStatusTag'; export type SqlManagementTableFilterParamType = PageInfoWithoutIndexAndSize< IGetSqlManageListV2Params, @@ -31,6 +32,7 @@ export type ExtraFilterMetaType = ISqlManage & { filter_instance_id?: string; filter_audit_level?: string; filter_rule_name?: string; + filter_remediation_status?: string; time?: string; }; @@ -98,6 +100,15 @@ export const ExtraFilterMeta: () => ActiontechTableFilterMeta< filterLabel: t('sqlManagement.table.filter.rule'), checked: false } + ], + [ + 'filter_remediation_status', + { + filterCustomType: 'select', + filterKey: 'filter_remediation_status', + filterLabel: t('sqlManagement.table.column.remediationStatus'), + checked: false + } ] ]); }; @@ -318,6 +329,11 @@ const SqlManagementColumn: ( return '-'; } }, + { + dataIndex: 'remediation_status', + title: () => t('sqlManagement.table.column.remediationStatus'), + render: (status) => + }, // { // dataIndex: 'first_appear_timestamp', // title: () => t('sqlManagement.table.column.firstOccurrence'), diff --git a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/hooks/useGetTableFilterInfo.ts b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/hooks/useGetTableFilterInfo.ts index 1a82251e16..7081126cdb 100644 --- a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/hooks/useGetTableFilterInfo.ts +++ b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/hooks/useGetTableFilterInfo.ts @@ -9,8 +9,11 @@ import useInstance from '../../../../../hooks/useInstance'; import useRuleTips from './useRuleTips'; import { ExtraFilterMetaType } from '../column'; import useSourceTips from './useSourceTips'; +import { remediationStatusOptions } from '../RemediationStatusTag'; +import { useTranslation } from 'react-i18next'; const useGetTableFilterInfo = () => { + const { t } = useTranslation(); const { projectName } = useCurrentProject(); const { generateAuditLevelSelectOptions } = useStaticStatus(); @@ -70,6 +73,15 @@ const useGetTableFilterInfo = () => { loading: getRuleTipsLoading, popupMatchSelectWidth: 400 } + ], + [ + 'filter_remediation_status', + { + options: remediationStatusOptions.map((status) => ({ + label: t(`sqlManagement.table.remediationStatus.${status}`), + value: status + })) + } ] ]); }, [ @@ -81,7 +93,8 @@ const useGetTableFilterInfo = () => { getSourceTipsLoading, generateAuditLevelSelectOptions, generateRuleTipsSelectOptions, - getRuleTipsLoading + getRuleTipsLoading, + t ]); return { diff --git a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/index.tsx b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/index.tsx index 5d53e8bd76..1d3faddda0 100644 --- a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/index.tsx +++ b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/index.tsx @@ -1,7 +1,7 @@ import { useTranslation } from 'react-i18next'; import { useBoolean, useRequest } from 'ahooks'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { BasicButton, PageHeader } from '@actiontech/shared'; +import { BasicButton, EmptyBox, PageHeader } from '@actiontech/shared'; import SQLStatistics, { ISQLStatisticsProps } from '../SQLStatistics'; import { ActiontechTable, @@ -29,7 +29,8 @@ import { GetSqlManageListV2SortFieldEnum, GetSqlManageListV2SortOrderEnum, exportSqlManageV1FilterPriorityEnum, - exportSqlManageV1FilterStatusEnum + exportSqlManageV1FilterStatusEnum, + exportSqlManageRemediationV1ExportScopeEnum } from '@actiontech/shared/lib/api/sqle/service/SqlManage/index.enum'; import SqlManagementColumn, { ExtraFilterMeta, @@ -39,7 +40,7 @@ import SqlManagementColumn, { import { ModalName } from '../../../../data/ModalName'; import { SorterResult, TableRowSelection } from 'antd/es/table/interface'; import { ISqlManage } from '@actiontech/shared/lib/api/sqle/service/common'; -import { Spin, message } from 'antd'; +import { Space, Spin, message } from 'antd'; import SqlManagementModal from './Modal'; import EmitterKey from '../../../../data/EmitterKey'; import EventEmitter from '../../../../utils/EventEmitter'; @@ -305,6 +306,10 @@ const SQLEEIndex = () => { exportButtonDisabled, { setFalse: finishExport, setTrue: startExport } ] = useBoolean(false); + const [ + remediationExportButtonDisabled, + { setFalse: finishRemediationExport, setTrue: startRemediationExport } + ] = useBoolean(false); const handleExport = () => { startExport(); const hideLoading = messageApi.loading( @@ -343,6 +348,35 @@ const SQLEEIndex = () => { }); }; + const handleRemediationExport = () => { + if (!actionPermission || projectArchive) { + return; + } + startRemediationExport(); + const hideLoading = messageApi.loading( + t('sqlManagement.pageHeader.action.remediationExporting') + ); + + SqlManage.exportSqlManageRemediationV1( + { + project_name: projectName, + export_scope: exportSqlManageRemediationV1ExportScopeEnum.project + }, + { responseType: 'blob' } + ) + .then((res) => { + if (res.status === 200) { + messageApi.success( + t('sqlManagement.pageHeader.action.remediationExportSuccessTips') + ); + } + }) + .finally(() => { + hideLoading(); + finishRemediationExport(); + }); + }; + useEffect(() => { EventEmitter.subscribe(EmitterKey.Refresh_SQL_Management, refresh); return () => { @@ -383,13 +417,24 @@ const SQLEEIndex = () => { } - disabled={exportButtonDisabled} - > - {t('sqlManagement.pageHeader.action.export')} - + + + } + disabled={remediationExportButtonDisabled} + > + {t('sqlManagement.pageHeader.action.remediationExport')} + + + } + disabled={exportButtonDisabled} + > + {t('sqlManagement.pageHeader.action.export')} + + } /> {/* page */} diff --git a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/index.type.ts b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/index.type.ts index 86227229b9..3cf8f6f36f 100644 --- a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/index.type.ts +++ b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/index.type.ts @@ -6,6 +6,8 @@ export type ScanTypeSqlCollectionProps = { instanceType: string; exportPending: () => void; exportDone: () => void; + remediationExportPending: () => void; + remediationExportDone: () => void; }; export type ScanTypeSqlTableDataSourceItem = { [key in string]: string }; diff --git a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/indx.tsx b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/indx.tsx index cf12b713d4..c8961bef4a 100644 --- a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/indx.tsx +++ b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/indx.tsx @@ -12,6 +12,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useBoolean, useRequest } from 'ahooks'; import { ScanTypeSqlCollectionStyleWrapper } from './style'; import instance_audit_plan from '@actiontech/shared/lib/api/sqle/service/instance_audit_plan'; +import SqlManage from '@actiontech/shared/lib/api/sqle/service/SqlManage'; import { useCurrentProject, useCurrentUser @@ -41,8 +42,27 @@ import { } from '@actiontech/shared/lib/api/sqle/service/instance_audit_plan/index.d'; import { mergeFilterButtonMeta } from '@actiontech/shared/lib/components/ActiontechTable/hooks/useTableFilterContainer'; import { ResponseCode } from '@actiontech/shared/lib/enum'; -import { message } from 'antd'; +import { Card, Spin, message } from 'antd'; import { Link } from 'react-router-dom'; +import { exportSqlManageRemediationV1ExportScopeEnum } from '@actiontech/shared/lib/api/sqle/service/SqlManage/index.enum'; +import { formatParamsBySeparator } from '@actiontech/shared/lib/utils/Tool'; +import RemediationStatusTag, { + remediationStatusOptions +} from '../../../SqlManagement/component/SQLEEIndex/RemediationStatusTag'; + +const formatOverviewNumber = (value?: number) => { + if (typeof value !== 'number') { + return '-'; + } + return formatParamsBySeparator(value); +}; + +const formatRemediationRate = (value?: number) => { + if (typeof value !== 'number') { + return '-'; + } + return `${(value * 100).toFixed(2)}%`; +}; const ScanTypeSqlCollection: React.FC = ({ instanceAuditPlanId, @@ -51,7 +71,9 @@ const ScanTypeSqlCollection: React.FC = ({ activeTabKey, instanceType, exportDone, - exportPending + exportPending, + remediationExportPending, + remediationExportDone }) => { const { t } = useTranslation(); const { sortableTableColumnFactory, tableFilterMetaFactory } = @@ -190,6 +212,27 @@ const ScanTypeSqlCollection: React.FC = ({ } ); + const { + data: remediationOverview, + loading: remediationOverviewLoading, + error: remediationOverviewError + } = useRequest( + () => + SqlManage.getSqlManageRemediationOverviewV1({ + project_name: projectName, + instance_audit_plan_id: Number(instanceAuditPlanId), + audit_plan_type: auditPlanType + }).then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + return res.data.data; + } + }), + { + ready: + activeTabKey === auditPlanId && !!instanceAuditPlanId && !!auditPlanType + } + ); + const recordAuditResult = useMemo(() => { try { return JSON.parse( @@ -260,6 +303,53 @@ const ScanTypeSqlCollection: React.FC = ({ t ]); + useEffect(() => { + const exportScanTypeRemediation = () => { + remediationExportPending(); + const hideLoading = messageApi.loading( + t('managementConf.detail.remediationExportTips'), + 0 + ); + + SqlManage.exportSqlManageRemediationV1( + { + project_name: projectName, + export_scope: exportSqlManageRemediationV1ExportScopeEnum.scan_task, + instance_audit_plan_id: Number(instanceAuditPlanId), + audit_plan_type: auditPlanType + }, + { responseType: 'blob' } + ) + .then((res) => { + if (res.status === 200) { + messageApi.success( + t('managementConf.detail.remediationExportSuccessTips') + ); + } + }) + .finally(() => { + remediationExportDone(); + hideLoading(); + }); + }; + const { unsubscribe } = eventEmitter.subscribe( + EmitterKey.Export_Sql_Management_Conf_Detail_Remediation, + exportScanTypeRemediation + ); + + return () => { + unsubscribe(); + }; + }, [ + auditPlanType, + instanceAuditPlanId, + messageApi, + projectName, + remediationExportDone, + remediationExportPending, + t + ]); + const tableSetting = useMemo(() => { return { tableName: `sql_management_conf_${auditPlanType}`, @@ -269,6 +359,81 @@ const ScanTypeSqlCollection: React.FC = ({ return ( + + + {remediationOverviewError ? ( +
+ {t('managementConf.detail.remediationOverview.loadFailed', { + message: getErrorMessage(remediationOverviewError) + })} +
+ ) : ( + <> +
+
+ + {formatOverviewNumber(remediationOverview?.sql_total_num)} + + + {t('managementConf.detail.remediationOverview.sqlTotal')} + +
+
+ + {formatOverviewNumber(remediationOverview?.first_score)} + + + {t('managementConf.detail.remediationOverview.firstScore')} + +
+
+ + {formatOverviewNumber(remediationOverview?.latest_score)} + + + {t('managementConf.detail.remediationOverview.latestScore')} + +
+
+ + {formatOverviewNumber(remediationOverview?.score_change)} + + + {t('managementConf.detail.remediationOverview.scoreChange')} + +
+
+ + {formatRemediationRate( + remediationOverview?.remediation_rate + )} + + + {t( + 'managementConf.detail.remediationOverview.remediationRate' + )} + +
+
+
+ {remediationStatusOptions.map((status) => ( +
+ + + {formatOverviewNumber( + remediationOverview?.remediation_status_count?.[status] + )} + +
+ ))} +
+ + )} +
+
{tableMetas?.filter_meta_list?.length && ( theme.sharedTheme.uiToken.colorTextBase}; + font-size: 24px; + font-weight: 600; + line-height: 32px; + } + + span { + color: ${({ theme }) => theme.sharedTheme.uiToken.colorTextTertiary}; + font-size: 13px; + } + } + + .remediation-overview-status-list { + display: flex; + flex-wrap: wrap; + margin: 8px -8px -8px; + padding-top: 12px; + border-top: 1px solid + ${({ theme }) => theme.sharedTheme.uiToken.colorBorderSecondary}; + } + + .remediation-overview-status { + display: flex; + align-items: center; + margin: 8px; + + strong { + margin-left: 8px; + color: ${({ theme }) => theme.sharedTheme.uiToken.colorTextBase}; + font-size: 16px; + font-weight: 600; + } + } + + .remediation-overview-error { + color: ${({ theme }) => theme.sharedTheme.uiToken.colorError}; + } + .table-describe-column { max-width: 600px; } diff --git a/packages/sqle/src/page/SqlManagementConf/Detail/index.tsx b/packages/sqle/src/page/SqlManagementConf/Detail/index.tsx index a632c0213b..274eff9a39 100644 --- a/packages/sqle/src/page/SqlManagementConf/Detail/index.tsx +++ b/packages/sqle/src/page/SqlManagementConf/Detail/index.tsx @@ -19,8 +19,10 @@ import { useSearchParams } from 'react-router-dom'; import instance_audit_plan from '@actiontech/shared/lib/api/sqle/service/instance_audit_plan'; +import SqlManage from '@actiontech/shared/lib/api/sqle/service/SqlManage'; import { useCurrentProject, + useCurrentUser, useUserOperationPermission } from '@actiontech/shared/lib/global'; import { Result, Space } from 'antd'; @@ -31,6 +33,8 @@ import EmitterKey from '../../../data/EmitterKey'; import { message } from 'antd'; import { ResponseCode } from '@actiontech/shared/lib/enum'; import { OpPermissionItemOpPermissionTypeEnum } from '@actiontech/shared/lib/api/base/service/common.enum'; +import { DownArrowLineOutlined } from '@actiontech/icons'; +import { exportSqlManageRemediationV1ExportScopeEnum } from '@actiontech/shared/lib/api/sqle/service/SqlManage/index.enum'; const ConfDetail: React.FC = () => { const { t } = useTranslation(); @@ -38,13 +42,18 @@ const ConfDetail: React.FC = () => { const [searchParams] = useSearchParams(); const location = useLocation(); const navigate = useNavigate(); - const { projectName } = useCurrentProject(); + const { projectName, projectArchive } = useCurrentProject(); + const { isAdmin, isProjectManager } = useCurrentUser(); const [activeKey, setActiveKey] = useState( SQL_MANAGEMENT_CONF_OVERVIEW_TAB_KEY ); const [exporting, { setTrue: exportPending, setFalse: exportDone }] = useBoolean(); + const [ + remediationExporting, + { setTrue: remediationExportPending, setFalse: remediationExportDone } + ] = useBoolean(); const [auditing, { setTrue: auditPending, setFalse: auditDone }] = useBoolean(); @@ -100,6 +109,14 @@ const ConfDetail: React.FC = () => { ); }, [data?.instance_id, isHaveServicePermission]); + const actionPermission = useMemo(() => { + return isAdmin || isProjectManager(projectName); + }, [isAdmin, isProjectManager, projectName]); + + const remediationExportPermission = useMemo(() => { + return (actionPermission || hasOpPermission) && !projectArchive; + }, [actionPermission, hasOpPermission, projectArchive]); + const items: SegmentedTabsProps['items'] = [ { label: t('managementConf.detail.overview.title'), @@ -127,6 +144,8 @@ const ConfDetail: React.FC = () => { instanceType={data.instance_type ?? ''} exportPending={exportPending} exportDone={exportDone} + remediationExportPending={remediationExportPending} + remediationExportDone={remediationExportDone} /> ) })) ?? []) @@ -144,6 +163,41 @@ const ConfDetail: React.FC = () => { eventEmitter.emit(EmitterKey.Export_Sql_Management_Conf_Detail_Sql_List); }; + const exportScanTypeRemediation = () => { + eventEmitter.emit(EmitterKey.Export_Sql_Management_Conf_Detail_Remediation); + }; + + const exportDataSourceRemediation = () => { + if (!remediationExportPermission || !data?.instance_id) { + return; + } + remediationExportPending(); + const hideLoading = messageApi.loading( + t('managementConf.detail.remediationExportTips'), + 0 + ); + + SqlManage.exportSqlManageRemediationV1( + { + project_name: projectName, + export_scope: exportSqlManageRemediationV1ExportScopeEnum.data_source, + filter_instance_id: data.instance_id + }, + { responseType: 'blob' } + ) + .then((res) => { + if (res.status === 200) { + messageApi.success( + t('managementConf.detail.remediationExportSuccessTips') + ); + } + }) + .finally(() => { + hideLoading(); + remediationExportDone(); + }); + }; + const onAuditImmediately = () => { auditPending(); instance_audit_plan @@ -203,6 +257,15 @@ const ConfDetail: React.FC = () => { > {t('managementConf.detail.export')} + + } + > + {t('managementConf.detail.remediationExport')} + + {hasOpPermission && ( { )} + + } + > + {t('managementConf.detail.remediationExport')} + + } diff --git a/packages/sqle/src/page/SqlManagementRemediationReport/index.tsx b/packages/sqle/src/page/SqlManagementRemediationReport/index.tsx new file mode 100644 index 0000000000..37e5d904e4 --- /dev/null +++ b/packages/sqle/src/page/SqlManagementRemediationReport/index.tsx @@ -0,0 +1,70 @@ +import { BasicButton, PageHeader } from '@actiontech/shared'; +import SqlManage from '@actiontech/shared/lib/api/sqle/service/SqlManage'; +import { DownArrowLineOutlined } from '@actiontech/icons'; +import { Card, Space, Typography, message } from 'antd'; +import { useBoolean } from 'ahooks'; +import { useTranslation } from 'react-i18next'; + +const SqlManagementRemediationReport = () => { + const { t } = useTranslation(); + const [messageApi, messageContextHolder] = message.useMessage(); + const [exporting, { setTrue: startExport, setFalse: finishExport }] = + useBoolean(false); + + const handleExport = () => { + startExport(); + const hideLoading = messageApi.loading( + t('sqlManagement.remediationReport.exporting') + ); + + SqlManage.exportGlobalSqlManageRemediationV1({}, { responseType: 'blob' }) + .then((res) => { + if (res.status === 200) { + messageApi.success( + t('sqlManagement.remediationReport.exportSuccessTips') + ); + } + }) + .finally(() => { + hideLoading(); + finishExport(); + }); + }; + + return ( +
+ {messageContextHolder} + } + onClick={handleExport} + disabled={exporting} + > + {t('sqlManagement.remediationReport.exportButton')} + + } + /> + + + + {t('sqlManagement.remediationReport.scopeTitle')} + + + {t('sqlManagement.remediationReport.description')} + + + {t('sqlManagement.remediationReport.scopeContent')} + + + {t('sqlManagement.remediationReport.permissionTips')} + + + +
+ ); +}; + +export default SqlManagementRemediationReport; 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', From e2529049b78338a9cd8e857d0cca1f95033fa26e Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Fri, 19 Jun 2026 20:03:51 +0800 Subject: [PATCH 2/2] Add SQL audit rule exception review support --- .../sqle/service/audit_whitelist/index.d.ts | 47 +- .../api/sqle/service/audit_whitelist/index.ts | 75 ++- .../shared/lib/api/sqle/service/common.d.ts | 66 ++ .../lib/api/sqle/service/instance/index.d.ts | 5 + .../lib/api/sqle/service/instance/index.ts | 17 + .../ReportDrawer/RuleExceptionDrawer.tsx | 186 ++++++ .../__snapshots__/index.test.tsx.snap | 608 ++++++++++++++---- .../ReportDrawer/__tests__/index.test.tsx | 61 +- .../src/components/ReportDrawer/index.tsx | 178 ++++- .../src/components/ReportDrawer/index.type.ts | 34 +- .../sqle/src/components/ReportDrawer/style.ts | 35 +- .../sqle/src/locale/en-US/operationRecord.ts | 1 + packages/sqle/src/locale/en-US/whitelist.ts | 35 + .../sqle/src/locale/zh-CN/operationRecord.ts | 1 + packages/sqle/src/locale/zh-CN/whitelist.ts | 33 + .../src/page/OperationRecord/List/index.tsx | 53 +- .../src/page/OperationRecord/index.test.tsx | 10 +- .../sqle/src/page/OperationRecord/index.tsx | 14 +- .../Table/AuditResultDrawer.tsx | 22 + .../Common/AuditResultList/Table/index.tsx | 11 +- .../AuditResultList/Table/index.type.ts | 9 + .../Common/AuditResultList/index.tsx | 19 +- .../Common/AuditResultList/index.type.ts | 1 + .../List/__snapshots__/index.test.tsx.snap | 37 ++ .../sqle/src/page/Whitelist/List/columns.tsx | 138 +++- .../src/page/Whitelist/List/index.test.tsx | 69 ++ .../sqle/src/page/Whitelist/List/index.tsx | 324 ++++++++-- .../__snapshots__/index.test.tsx.snap | 37 ++ .../sqle/src/page/Whitelist/index.test.tsx | 13 + packages/sqle/src/page/Whitelist/index.tsx | 24 +- .../testUtils/mockApi/auditWhiteList/data.ts | 23 +- .../testUtils/mockApi/auditWhiteList/index.ts | 20 +- .../src/testUtils/mockApi/instance/index.ts | 12 + 33 files changed, 1993 insertions(+), 225 deletions(-) create mode 100644 packages/sqle/src/components/ReportDrawer/RuleExceptionDrawer.tsx diff --git a/packages/shared/lib/api/sqle/service/audit_whitelist/index.d.ts b/packages/shared/lib/api/sqle/service/audit_whitelist/index.d.ts index 732f283101..6e6e8fc960 100644 --- a/packages/shared/lib/api/sqle/service/audit_whitelist/index.d.ts +++ b/packages/shared/lib/api/sqle/service/audit_whitelist/index.d.ts @@ -2,7 +2,10 @@ import { IGetAuditWhitelistResV1, ICreateAuditWhitelistReqV1, IBaseRes, - IUpdateAuditWhitelistReqV1 + IUpdateAuditWhitelistReqV1, + ISQLRuleExceptionResV1, + ICreateSQLRuleExceptionReqV1, + IGetSQLRuleExceptionResV1 } from '../common.d'; export interface IGetAuditWhitelistV1Params { @@ -42,3 +45,45 @@ export interface IUpdateAuditWhitelistByIdV1Params } export interface IUpdateAuditWhitelistByIdV1Return extends IBaseRes {} + +export interface ICreateSQLRuleExceptionV1Params + extends ICreateSQLRuleExceptionReqV1 { + project_name: string; +} + +export interface ICreateSQLRuleExceptionV1Return extends IBaseRes { + data?: ISQLRuleExceptionResV1; +} + +export interface IGetSQLRuleExceptionV1Params { + project_name: string; + + fuzzy_search_value?: string; + + filter_instance_id?: string; + + filter_rule_name?: string; + + filter_created_by?: string; + + filter_created_time_from?: string; + + filter_created_time_to?: string; + + filter_sql_fingerprint?: string; + + page_index: string; + + page_size: string; +} + +export interface IGetSQLRuleExceptionV1Return + extends IGetSQLRuleExceptionResV1 {} + +export interface IDeleteSQLRuleExceptionV1Params { + project_name: string; + + sql_rule_exception_id: string; +} + +export interface IDeleteSQLRuleExceptionV1Return extends IBaseRes {} diff --git a/packages/shared/lib/api/sqle/service/audit_whitelist/index.ts b/packages/shared/lib/api/sqle/service/audit_whitelist/index.ts index df283dea6f..475ed74d9b 100644 --- a/packages/shared/lib/api/sqle/service/audit_whitelist/index.ts +++ b/packages/shared/lib/api/sqle/service/audit_whitelist/index.ts @@ -14,7 +14,13 @@ import { IDeleteAuditWhitelistByIdV1Params, IDeleteAuditWhitelistByIdV1Return, IUpdateAuditWhitelistByIdV1Params, - IUpdateAuditWhitelistByIdV1Return + IUpdateAuditWhitelistByIdV1Return, + ICreateSQLRuleExceptionV1Params, + ICreateSQLRuleExceptionV1Return, + IDeleteSQLRuleExceptionV1Params, + IDeleteSQLRuleExceptionV1Return, + IGetSQLRuleExceptionV1Params, + IGetSQLRuleExceptionV1Return } from './index.d'; class AuditWhitelistService extends ServiceBase { @@ -48,6 +54,73 @@ class AuditWhitelistService extends ServiceBase { ); } + public createSQLRuleExceptionV1( + params: ICreateSQLRuleExceptionV1Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + const project_name = paramsData.project_name; + delete paramsData.project_name; + + const config = options || {}; + const headers = config.headers ? config.headers : {}; + config.headers = { + ...headers, + 'Content-Type': 'application/json' + }; + config.transformRequest = [ + (data) => + `{"instance_id":${data.instance_id},"sql_fingerprint":${JSON.stringify( + data.sql_fingerprint ?? '' + )},"rule_name":${JSON.stringify( + data.rule_name ?? '' + )},"rule_desc":${JSON.stringify( + data.rule_desc ?? '' + )},"rule_level":${JSON.stringify( + data.rule_level ?? '' + )},"reason":${JSON.stringify(data.reason ?? '')}}` + ]; + + return this.post( + `/v1/projects/${project_name}/audit_whitelist/rule_exceptions`, + paramsData, + config + ); + } + + public getSQLRuleExceptionV1( + params: IGetSQLRuleExceptionV1Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + const project_name = paramsData.project_name; + delete paramsData.project_name; + + return this.get( + `/v1/projects/${project_name}/audit_whitelist/rule_exceptions`, + paramsData, + options + ); + } + + public deleteSQLRuleExceptionV1( + params: IDeleteSQLRuleExceptionV1Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + const project_name = paramsData.project_name; + delete paramsData.project_name; + + const sql_rule_exception_id = paramsData.sql_rule_exception_id; + delete paramsData.sql_rule_exception_id; + + return this.delete( + `/v1/projects/${project_name}/audit_whitelist/rule_exceptions/${sql_rule_exception_id}`, + paramsData, + options + ); + } + public deleteAuditWhitelistByIdV1( params: IDeleteAuditWhitelistByIdV1Params, options?: AxiosRequestConfig diff --git a/packages/shared/lib/api/sqle/service/common.d.ts b/packages/shared/lib/api/sqle/service/common.d.ts index f432835830..7f62d46649 100644 --- a/packages/shared/lib/api/sqle/service/common.d.ts +++ b/packages/shared/lib/api/sqle/service/common.d.ts @@ -529,6 +529,66 @@ export interface ICreateAuditWhitelistReqV1 { value?: string; } +export interface ICreateSQLRuleExceptionReqV1 { + instance_id?: string; + + sql_fingerprint?: string; + + rule_name?: string; + + rule_desc?: string; + + rule_level?: string; + + reason?: string; +} + +export interface ISQLRuleExceptionResV1 { + sql_rule_exception_id?: number; + + project_name?: string; + + project_id?: string; + + instance_id?: string; + + instance_name?: string; + + sql_fingerprint?: string; + + rule_name?: string; + + rule_desc?: string; + + rule_level?: string; + + reason?: string; + + created_by?: string; + + created_at?: string; + + hit_count?: number; + + last_match_time?: string; + + matched_count?: number; + + match_info?: string; + + hit_info?: string; +} + +export interface IGetSQLRuleExceptionResV1 { + code?: number; + + data?: ISQLRuleExceptionResV1[]; + + message?: string; + + total_nums?: number; +} + export interface ICreateBlacklistReqV1 { content?: string; @@ -3126,6 +3186,12 @@ export interface IAuditTaskSQLResV2 { audit_result?: IAuditResult[]; + skipped_audit_result?: (IAuditResult & Partial)[]; + + sql_fingerprint?: string; + + audit_fingerprint?: string; + audit_status?: string; description?: string; diff --git a/packages/shared/lib/api/sqle/service/instance/index.d.ts b/packages/shared/lib/api/sqle/service/instance/index.d.ts index 633536be3e..fdd95838cb 100644 --- a/packages/shared/lib/api/sqle/service/instance/index.d.ts +++ b/packages/shared/lib/api/sqle/service/instance/index.d.ts @@ -26,6 +26,11 @@ export interface IGetInstanceTipListV1Params { export interface IGetInstanceTipListV1Return extends IGetInstanceTipsResV1 {} +export interface IGetInstanceTipListV2Params + extends IGetInstanceTipListV1Params {} + +export interface IGetInstanceTipListV2Return extends IGetInstanceTipsResV1 {} + export interface IBatchCheckInstanceIsConnectableByNameParams extends IBatchCheckInstanceConnectionsReqV1 { project_name: string; diff --git a/packages/shared/lib/api/sqle/service/instance/index.ts b/packages/shared/lib/api/sqle/service/instance/index.ts index 5a1098780c..e25a927b56 100644 --- a/packages/shared/lib/api/sqle/service/instance/index.ts +++ b/packages/shared/lib/api/sqle/service/instance/index.ts @@ -9,6 +9,8 @@ import { AxiosRequestConfig } from 'axios'; import { IGetInstanceTipListV1Params, IGetInstanceTipListV1Return, + IGetInstanceTipListV2Params, + IGetInstanceTipListV2Return, IBatchCheckInstanceIsConnectableByNameParams, IBatchCheckInstanceIsConnectableByNameReturn, ICheckInstanceIsConnectableByNameV1Params, @@ -41,6 +43,21 @@ class InstanceService extends ServiceBase { ); } + public getInstanceTipListV2( + params: IGetInstanceTipListV2Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + const project_name = paramsData.project_name; + delete paramsData.project_name; + + return this.get( + `/v2/projects/${project_name}/instance_tips`, + paramsData, + options + ); + } + public batchCheckInstanceIsConnectableByName( params: IBatchCheckInstanceIsConnectableByNameParams, options?: AxiosRequestConfig diff --git a/packages/sqle/src/components/ReportDrawer/RuleExceptionDrawer.tsx b/packages/sqle/src/components/ReportDrawer/RuleExceptionDrawer.tsx new file mode 100644 index 0000000000..6ac9fbb843 --- /dev/null +++ b/packages/sqle/src/components/ReportDrawer/RuleExceptionDrawer.tsx @@ -0,0 +1,186 @@ +import { BasicButton, BasicDrawer, BasicInput } from '@actiontech/shared'; +import { DrawerFormLayout } from '@actiontech/shared/lib/data/common'; +import { ResponseCode } from '@actiontech/shared/lib/enum'; +import audit_whitelist from '@actiontech/shared/lib/api/sqle/service/audit_whitelist'; +import instance from '@actiontech/shared/lib/api/sqle/service/instance'; +import { getInstanceTipListV1FunctionalModuleEnum } from '@actiontech/shared/lib/api/sqle/service/instance/index.enum'; +import DBService from '@actiontech/shared/lib/api/base/service/DBService'; +import { ListDBServiceTipsFunctionalModuleEnum } from '@actiontech/shared/lib/api/base/service/DBService/index.enum'; +import { Form, Space, message } from 'antd'; +import { useRequest } from 'ahooks'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + RuleExceptionDrawerProps, + RuleExceptionFormFields +} from './index.type'; + +const DUPLICATE_RULE_EXCEPTION_CODE = 4010; + +type RuleExceptionInstanceTip = { + instance_id?: string; + instance_name?: string; +}; + +const RuleExceptionDrawer: React.FC = ({ + open, + data, + context, + onClose, + onCreated +}) => { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const [messageApi, messageContextHolder] = message.useMessage(); + + const { data: instanceTips, loading: instanceTipsLoading } = useRequest( + () => + context?.projectID + ? DBService.ListDBServiceTips({ + project_uid: context.projectID, + filter_db_type: context?.dbType, + functional_module: + ListDBServiceTipsFunctionalModuleEnum.create_workflow + }).then((res) => + (res.data.data ?? []).map((item) => ({ + instance_id: item.id, + instance_name: item.name + })) + ) + : instance + .getInstanceTipListV2({ + project_name: context?.projectName ?? '', + filter_db_type: context?.dbType, + functional_module: + getInstanceTipListV1FunctionalModuleEnum.create_workflow + }) + .then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + return res.data.data ?? []; + } + return []; + }) + .catch(() => []), + { + ready: open && !!context?.projectName && !context?.instanceId + } + ); + + const resolvedInstanceId = useMemo(() => { + if (context?.instanceId) { + return context.instanceId; + } + const matchedInstance = instanceTips?.find( + (item) => item.instance_name === context?.instanceName + ); + return matchedInstance?.instance_id; + }, [context?.instanceId, context?.instanceName, instanceTips]); + + const lackRequiredContext = + !context?.projectName || + !resolvedInstanceId || + !context?.sqlFingerprint || + !data?.rule_name; + + const { run: submit, loading: submitLoading } = useRequest( + () => form.validateFields(), + { + manual: true, + onSuccess(values) { + audit_whitelist + .createSQLRuleExceptionV1({ + project_name: context!.projectName, + instance_id: `${resolvedInstanceId}`, + sql_fingerprint: context!.sqlFingerprint, + rule_name: data!.rule_name, + rule_desc: data?.desc ?? data?.annotation ?? data?.message, + rule_level: data?.level, + reason: values.reason + }) + .then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + messageApi.success(t('whitelist.ruleException.addSuccess')); + form.resetFields(); + onCreated?.(); + onClose(); + } else if (res.data.code === DUPLICATE_RULE_EXCEPTION_CODE) { + messageApi.warning(t('whitelist.ruleException.duplicateTips')); + onClose(); + } + }); + } + } + ); + + return ( + <> + {messageContextHolder} + + + {t('common.close')} + + + {t('common.submit')} + + + } + > +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + ); +}; + +export default RuleExceptionDrawer; diff --git a/packages/sqle/src/components/ReportDrawer/__tests__/__snapshots__/index.test.tsx.snap b/packages/sqle/src/components/ReportDrawer/__tests__/__snapshots__/index.test.tsx.snap index 1144cef94f..f59777cd70 100644 --- a/packages/sqle/src/components/ReportDrawer/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/sqle/src/components/ReportDrawer/__tests__/__snapshots__/index.test.tsx.snap @@ -69,7 +69,7 @@ exports[`sqle/components/ReportDrawer render snap is empty 1`] = `
+
+

+ 已设例外的规则 +

+
+
+
+ + + + + + + + + + + + +
+
+ + 暂无已设例外的规则 + +
+ +
+
- - - message - + + + message + +
+
+ annotation +
+
+
+
+
+
+

+ 已设例外的规则 +

+
+
+
+ + + + + + + + + + + +
- annotation - - 查看更多 - + 暂无已设例外的规则 +
+
@@ -442,7 +574,7 @@ exports[`sqle/components/ReportDrawer render snap when has delete rule 1`] = `
- - 该规则已删除 -
- - message1 + 该规则已删除 +
+ + + message1 + +
- - - message2 - + + + message2 + +
+
+ annotation +
+
+
+
+ +
+

+ 已设例外的规则 +

+
+
+
+ + + + + + + + + + + +
- annotation - - 查看更多 - + 暂无已设例外的规则 +
+
@@ -720,7 +920,7 @@ exports[`sqle/components/ReportDrawer render snap when has extra 1`] = `
- - - message - + + + message + +
+
+ annotation +
+
+
+
+ +
+

+ 已设例外的规则 +

+
+
+
+ + + + + + + + + + + +
- annotation - - 查看更多 - + 暂无已设例外的规则 +
+
@@ -906,7 +1170,7 @@ exports[`sqle/components/ReportDrawer render snap when loading is true 1`] = `
- - - message - + + + message + +
+
+ annotation +
+
+
+
+ +
+

+ 已设例外的规则 +

+
+
+
+ + + + + + + + + + + +
- annotation - - 查看更多 - + 暂无已设例外的规则 +
+
@@ -1156,7 +1484,7 @@ exports[`sqle/components/ReportDrawer render snap when showSourceFile is true 1`
- - - message - + + + message + +
+
+ annotation +
+
+
+
+ +
+

+ 已设例外的规则 +

+
+
+
+ + + + + + + + + + + +
- annotation - - 查看更多 - + 暂无已设例外的规则 +
+
diff --git a/packages/sqle/src/components/ReportDrawer/__tests__/index.test.tsx b/packages/sqle/src/components/ReportDrawer/__tests__/index.test.tsx index 5c25010db0..22a6b2570b 100644 --- a/packages/sqle/src/components/ReportDrawer/__tests__/index.test.tsx +++ b/packages/sqle/src/components/ReportDrawer/__tests__/index.test.tsx @@ -2,7 +2,7 @@ import ReportDrawer from '..'; import { renderWithTheme } from '../../../testUtils/customRender'; import { DetailReportDrawerProps } from '../index.type'; -import { cleanup } from '@testing-library/react'; +import { cleanup, screen } from '@testing-library/react'; import { mockUseCurrentUser } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentUser'; describe('sqle/components/ReportDrawer', () => { @@ -174,4 +174,63 @@ describe('sqle/components/ReportDrawer', () => { }); expect(baseElement).toMatchSnapshot(); }); + + it('should hide rule exception entry without create permission', () => { + customRender({ + open: true, + title: 'this is a title', + showAnnotation: true, + data: { + sql: 'select 1', + auditResult: [ + { + rule_name: 'rule a', + message: 'message', + level: 'level', + annotation: 'annotation', + db_type: 'mysql' + } + ] + }, + ruleExceptionContext: { + projectName: 'default', + projectID: '700300', + instanceName: 'mysql_local_sqle', + sqlFingerprint: 'fp' + }, + onClose: jest.fn() + }); + + expect(screen.queryByText('添加为单规则例外')).not.toBeInTheDocument(); + }); + + it('should show rule exception entry with create permission', () => { + customRender({ + open: true, + title: 'this is a title', + showAnnotation: true, + data: { + sql: 'select 1', + auditResult: [ + { + rule_name: 'rule a', + message: 'message', + level: 'level', + annotation: 'annotation', + db_type: 'mysql' + } + ] + }, + ruleExceptionContext: { + projectName: 'default', + projectID: '700300', + instanceName: 'mysql_local_sqle', + sqlFingerprint: 'fp' + }, + canCreateRuleException: true, + onClose: jest.fn() + }); + + expect(screen.getByText('添加为单规则例外')).toBeInTheDocument(); + }); }); diff --git a/packages/sqle/src/components/ReportDrawer/index.tsx b/packages/sqle/src/components/ReportDrawer/index.tsx index 927017dd06..f1daf572b0 100644 --- a/packages/sqle/src/components/ReportDrawer/index.tsx +++ b/packages/sqle/src/components/ReportDrawer/index.tsx @@ -1,20 +1,23 @@ import { useTranslation } from 'react-i18next'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { BasicDrawer, BasicTag, EmptyBox, BasicToolTips, SQLRenderer, - BasicTypographyEllipsis + BasicTypographyEllipsis, + BasicButton } from '@actiontech/shared'; import { DetailReportDrawerProps, IAuditResultItem } from './index.type'; import { AuditReportStyleWrapper } from './style'; import AuditResultMessage from '../AuditResultMessage'; -import { Typography, Space } from 'antd'; +import { Typography, Space, Descriptions } from 'antd'; import { ProfileSquareFilled, EnvironmentFilled } from '@actiontech/icons'; import useThemeStyleData from '../../hooks/useThemeStyleData'; import { Spin } from 'antd'; +import RuleExceptionDrawer from './RuleExceptionDrawer'; +import { formatTime } from '@actiontech/shared/lib/utils/Common'; const ReportDrawer = ({ open, @@ -24,16 +27,25 @@ const ReportDrawer = ({ showAnnotation, showSourceFile, loading, - extra + extra, + ruleExceptionContext, + canCreateRuleException = false, + onRuleExceptionCreated }: DetailReportDrawerProps) => { const { t } = useTranslation(); const { sqleTheme } = useThemeStyleData(); + const [selectedRuleException, setSelectedRuleException] = + useState(); const closeModal = () => { onClose(); }; + const closeRuleExceptionDrawer = () => { + setSelectedRuleException(undefined); + }; + const resultDataIsEmpty = useMemo(() => { return ( (Array.isArray(data?.auditResult) && !data?.auditResult.length) || @@ -41,6 +53,23 @@ const ReportDrawer = ({ ); }, [data?.auditResult]); + const skippedAuditResult = useMemo( + () => data?.skippedAuditResult ?? [], + [data?.skippedAuditResult] + ); + + const openOperationRecord = () => { + if (!ruleExceptionContext?.projectName) { + return; + } + + window.open( + `/sqle/project/${ + ruleExceptionContext.projectID ?? ruleExceptionContext.projectName + }/operation-record` + ); + }; + return ( <> { if (!showAnnotation || item.isRuleDeleted) { return ( - + > + + ); } return ( - + > + + +
+ setSelectedRuleException(item)} + > + {t('whitelist.ruleException.addAction')} + +
+
+ ); } ) )} + +
+ + {t('whitelist.ruleException.skippedModule.title')} + +
+ {skippedAuditResult.map((item, index) => ( +
+ + + {item.rule_name || '-'} + + + {item.rule_level || item.level || '-'} + + + {item.rule_desc || + item.message || + item.rule_name || + '-'} + + + {item.created_by || '-'} + + + {formatTime(item.created_at, '-')} + + + {item.reason || '-'} + + + + {t('whitelist.ruleException.viewAudit')} + + + +
+ ))} +
+
+
@@ -185,6 +304,13 @@ const ReportDrawer = ({
+ ); }; diff --git a/packages/sqle/src/components/ReportDrawer/index.type.ts b/packages/sqle/src/components/ReportDrawer/index.type.ts index 8cc0fef080..1f445e588e 100644 --- a/packages/sqle/src/components/ReportDrawer/index.type.ts +++ b/packages/sqle/src/components/ReportDrawer/index.type.ts @@ -1,13 +1,30 @@ -import { IAuditResult } from '@actiontech/shared/lib/api/sqle/service/common'; +import { + IAuditResult, + ISQLRuleExceptionResV1 +} from '@actiontech/shared/lib/api/sqle/service/common'; import { ReactNode } from 'react'; export type IAuditResultItem = IAuditResult & { isRuleDeleted?: boolean; annotation?: string; + desc?: string; +}; + +export type SkippedAuditResultItem = IAuditResult & + Partial; + +export type RuleExceptionContext = { + projectName: string; + projectID?: string; + instanceName?: string; + instanceId?: string; + dbType?: string; + sqlFingerprint?: string; }; export type TypeData = { auditResult: Array; + skippedAuditResult?: Array; sql: string; sqlSourceFile?: string; sqlStartLine?: number; @@ -23,4 +40,19 @@ export interface DetailReportDrawerProps { showSourceFile?: boolean; loading?: boolean; extra?: ReactNode; + ruleExceptionContext?: RuleExceptionContext; + canCreateRuleException?: boolean; + onRuleExceptionCreated?: () => void; } + +export type RuleExceptionFormFields = { + reason: string; +}; + +export type RuleExceptionDrawerProps = { + open: boolean; + data?: IAuditResultItem; + context?: RuleExceptionContext; + onClose: () => void; + onCreated?: () => void; +}; diff --git a/packages/sqle/src/components/ReportDrawer/style.ts b/packages/sqle/src/components/ReportDrawer/style.ts index d611bdc219..cc5856e379 100644 --- a/packages/sqle/src/components/ReportDrawer/style.ts +++ b/packages/sqle/src/components/ReportDrawer/style.ts @@ -5,7 +5,15 @@ export const AuditReportStyleWrapper = styled('div')` height: 100%; .wrapper-item { - height: 50%; + height: 34%; + + &.skipped-rule-wrapper { + height: 28%; + + .wrapper-cont { + height: calc(100% - 68px); + } + } h3 { margin-bottom: 0; @@ -28,10 +36,35 @@ export const AuditReportStyleWrapper = styled('div')` padding: 8px 12px; margin-bottom: 4px; + .rule-exception-action { + display: flex; + justify-content: flex-end; + margin-top: 8px; + } + &:last-child { margin-bottom: 0; } } + + .skipped-rule-item { + background: ${({ theme }) => + theme.sharedTheme.uiToken.colorFillTertiary}; + border: 1px solid + ${({ theme }) => theme.sharedTheme.uiToken.colorBorderSecondary}; + border-radius: 4px; + padding: 12px; + margin-bottom: 8px; + + &:last-child { + margin-bottom: 0; + } + + .ant-descriptions-item-label, + .ant-descriptions-item-content { + padding-bottom: 8px; + } + } } .title-wrap { diff --git a/packages/sqle/src/locale/en-US/operationRecord.ts b/packages/sqle/src/locale/en-US/operationRecord.ts index b4b6300806..10a933333d 100644 --- a/packages/sqle/src/locale/en-US/operationRecord.ts +++ b/packages/sqle/src/locale/en-US/operationRecord.ts @@ -7,6 +7,7 @@ export default { exportButtonText: 'Export', exporting: 'Exporting operation log list...', exportSuccessTips: 'Operation log list exported successfully', + auditContentKeywordsTips: 'Located by audit content: {{keywords}}', filterForm: { operatingTime: 'Operation time', projectName: 'Project name', diff --git a/packages/sqle/src/locale/en-US/whitelist.ts b/packages/sqle/src/locale/en-US/whitelist.ts index 9469d11397..56c9d9a2f7 100644 --- a/packages/sqle/src/locale/en-US/whitelist.ts +++ b/packages/sqle/src/locale/en-US/whitelist.ts @@ -7,6 +7,10 @@ export default { ceTips: "If a user enables a rule but wants to temporarily bypass the triggering of certain rules in actual use, they can enable the platform'S audit whitelist feature.\nCurrently, it supports string matching or SQL fingerprint matching. Statements added to the SQL audit whitelist will not be subject to audit rules when creating a workflow request.", allWhitelist: 'All audit whitelist statements', + view: { + sql: 'Whole SQL exception', + rule: 'Rule exception' + }, table: { sql: 'SQL statement', desc: 'Audit whitelist description', @@ -35,5 +39,36 @@ export default { title: 'Update audit whitelist' }, sql: 'SQL' + }, + ruleException: { + addAction: 'Add as rule exception', + drawerTitle: 'Add rule exception', + addSuccess: 'Rule exception added successfully', + duplicateTips: + 'This rule exception already exists. No need to add it again.', + cancelAction: 'Cancel exception', + deleting: 'Canceling rule exception...', + deleteSuccess: 'Rule exception canceled successfully', + confirmCancel: + 'Confirm canceling this rule exception? The same tuple will trigger this rule again after cancellation.', + project: 'Project', + instance: 'Data source', + sqlFingerprint: 'SQL fingerprint', + ruleName: 'Rule name', + ruleDesc: 'Rule description', + ruleLevel: 'Original rule level', + reason: 'Reason', + createdBy: 'Added by', + createdAt: 'Added at', + matchInfo: 'Hit information', + matchInfoWithTime: 'Hit {{count}} times, last hit {{time}}', + audit: 'Action', + viewAudit: 'View audit', + missingSqlFingerprint: + 'The current audit result does not return a SQL fingerprint, so rule exception cannot be added yet.', + skippedModule: { + title: 'Excepted rules', + empty: 'No excepted rules in this audit result' + } } }; diff --git a/packages/sqle/src/locale/zh-CN/operationRecord.ts b/packages/sqle/src/locale/zh-CN/operationRecord.ts index d042f26e55..ea2e9b497f 100644 --- a/packages/sqle/src/locale/zh-CN/operationRecord.ts +++ b/packages/sqle/src/locale/zh-CN/operationRecord.ts @@ -7,6 +7,7 @@ export default { exportButtonText: '导出', exporting: '正在导出操作记录列表...', exportSuccessTips: '操作记录列表导出成功', + auditContentKeywordsTips: '已按审计内容定位:{{keywords}}', filterForm: { operatingTime: '操作时间', projectName: '操作项目', diff --git a/packages/sqle/src/locale/zh-CN/whitelist.ts b/packages/sqle/src/locale/zh-CN/whitelist.ts index 401ccd8a3f..30fa8f73b1 100644 --- a/packages/sqle/src/locale/zh-CN/whitelist.ts +++ b/packages/sqle/src/locale/zh-CN/whitelist.ts @@ -7,6 +7,10 @@ export default { ceTips: '如果用户开启了某条规则,但在实际使用中又想临时规避某些规则的触发,可以启用平台的审核SQL例外功能。\n目前支持按字符串匹配或按照SQL指纹匹配,添加在SQL审核审核SQL例外中的语句,在提交工单申请时,将不受审核规则的约束。', allWhitelist: '所有审核SQL例外语句', + view: { + sql: '整条 SQL 例外', + rule: '单规则例外' + }, table: { sql: '内容', desc: '描述', @@ -39,5 +43,34 @@ export default { tips: '当修改匹配类型或匹配内容后,该条记录的匹配次数和最后匹配时间将被重置。' }, sql: 'SQL语句' + }, + ruleException: { + addAction: '添加为单规则例外', + drawerTitle: '添加单规则例外', + addSuccess: '添加单规则例外成功', + duplicateTips: '该规则例外已存在,无需重复添加', + cancelAction: '取消例外', + deleting: '正在取消单规则例外...', + deleteSuccess: '取消单规则例外成功', + confirmCancel: + '确认取消这条单规则例外么?取消后同四元组复审将重新触发该规则。', + project: '项目', + instance: '数据源', + sqlFingerprint: 'SQL指纹', + ruleName: '规则名', + ruleDesc: '规则描述', + ruleLevel: '规则原级别', + reason: '添加原因', + createdBy: '添加例外的人', + createdAt: '添加时间', + matchInfo: '命中信息', + matchInfoWithTime: '命中 {{count}} 次,最近命中 {{time}}', + audit: '操作', + viewAudit: '查看审计', + missingSqlFingerprint: '当前审核结果未返回 SQL 指纹,暂不能添加单规则例外', + skippedModule: { + title: '已设例外的规则', + empty: '暂无已设例外的规则' + } } }; diff --git a/packages/sqle/src/page/OperationRecord/List/index.tsx b/packages/sqle/src/page/OperationRecord/List/index.tsx index ac38518185..0269812280 100644 --- a/packages/sqle/src/page/OperationRecord/List/index.tsx +++ b/packages/sqle/src/page/OperationRecord/List/index.tsx @@ -1,5 +1,6 @@ import { useMemo, useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import { useSearchParams } from 'react-router-dom'; import { Space, message } from 'antd'; import { useRequest, useBoolean } from 'ahooks'; import { BasicButton, PageHeader } from '@actiontech/shared'; @@ -30,6 +31,7 @@ import { DownArrowLineOutlined } from '@actiontech/icons'; const OperationRecordList: React.FC = () => { const { t } = useTranslation(); + const [searchParams] = useSearchParams(); const [messageApi, contextHolder] = message.useMessage(); @@ -49,6 +51,25 @@ const OperationRecordList: React.FC = () => { const { updateOperationActions, operationActionOptions } = useOperationActions(); + const defaultFilterInfo = useMemo(() => { + const filterOperateTypeName = searchParams.get('filter_operate_type_name'); + + if (!filterOperateTypeName) { + return {}; + } + + return { + filter_operate_type_name: filterOperateTypeName + }; + }, [searchParams]); + + const auditContentKeywords = useMemo( + () => + searchParams.get('audit_content_keywords')?.split('|').filter(Boolean) ?? + [], + [searchParams] + ); + const { tableFilterInfo, updateTableFilterInfo, @@ -60,7 +81,9 @@ const OperationRecordList: React.FC = () => { } = useTableRequestParams< IOperationRecordList, OperationRecordListFilterParamType - >(); + >({ + defaultFilterInfo + }); const { requestErrorMessage, handleTableRequestError } = useTableRequestError(); @@ -73,16 +96,33 @@ const OperationRecordList: React.FC = () => { () => { const params: IGetOperationRecordListV1Params = { ...pagination, + page_size: auditContentKeywords.length > 0 ? 100 : pagination.page_size, ...tableFilterInfo, filter_operate_project_name: projectName, fuzzy_search_operate_user_name: searchKeyword }; return handleTableRequestError( operationRecord.getOperationRecordListV1(params) - ); + ).then((res) => { + if (auditContentKeywords.length === 0) { + return res; + } + + const filteredData = res.list?.filter((item) => { + return auditContentKeywords.every((keyword) => + item.operation_content?.includes(keyword) + ); + }); + + return { + ...res, + list: filteredData, + total: filteredData?.length ?? 0 + }; + }); }, { - refreshDeps: [pagination, tableFilterInfo] + refreshDeps: [pagination, tableFilterInfo, auditContentKeywords] } ); @@ -168,6 +208,13 @@ const OperationRecordList: React.FC = () => { } /> + {auditContentKeywords.length > 0 && ( +
+ {t('operationRecord.list.auditContentKeywordsTips', { + keywords: auditContentKeywords.join(' / ') + })} +
+ )} { }); test('should render operation record list', async () => { - const { baseElement } = renderWithReduxAndTheme(); + const { baseElement } = renderWithReduxAndTheme( + + + + ); expect(baseElement).toMatchSnapshot(); expect(operationRecordListSpy).toHaveBeenCalledTimes(1); expect(screen.getByText('导出')).toBeInTheDocument(); + expect( + screen.queryByText('操作记录列表为企业版功能') + ).not.toBeInTheDocument(); }); }); diff --git a/packages/sqle/src/page/OperationRecord/index.tsx b/packages/sqle/src/page/OperationRecord/index.tsx index 8c9570555c..5779ac9971 100644 --- a/packages/sqle/src/page/OperationRecord/index.tsx +++ b/packages/sqle/src/page/OperationRecord/index.tsx @@ -1,6 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { EnterpriseFeatureDisplay, PageHeader } from '@actiontech/shared'; -import { Typography } from 'antd'; +import { PageHeader } from '@actiontech/shared'; import OperationRecordList from './List'; const OperationRecord = () => { @@ -12,16 +11,7 @@ const OperationRecord = () => { {/* #endif */} - - {t('operationRecord.ceTips')} - - } - > - - + ); }; diff --git a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/AuditResultDrawer.tsx b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/AuditResultDrawer.tsx index fc1b64fc71..d833d12867 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/AuditResultDrawer.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/AuditResultDrawer.tsx @@ -10,6 +10,11 @@ const AuditResultDrawer: React.FC = ({ open, auditResultRecord, dbType, + projectID, + projectName, + instanceName, + canCreateRuleException, + onRuleExceptionCreated, clickAnalyze }) => { const { t } = useTranslation(); @@ -24,6 +29,7 @@ const AuditResultDrawer: React.FC = ({ onClose={onClose} data={{ auditResult: auditResultRuleInfo, + skippedAuditResult: auditResultRecord?.skipped_audit_result, sql: auditResultRecord?.exec_sql ?? '', sqlSourceFile: auditResultRecord?.sql_source_file ?? '', sqlStartLine: auditResultRecord?.sql_start_line, @@ -39,6 +45,22 @@ const AuditResultDrawer: React.FC = ({ } showAnnotation loading={loading} + ruleExceptionContext={ + projectName && canCreateRuleException + ? { + projectName, + projectID, + instanceName, + dbType, + sqlFingerprint: + auditResultRecord?.sql_fingerprint ?? + auditResultRecord?.audit_fingerprint ?? + auditResultRecord?.exec_sql + } + : undefined + } + canCreateRuleException={canCreateRuleException} + onRuleExceptionCreated={onRuleExceptionCreated} extra={ clickAnalyze(auditResultRecord?.number)}> {t('execWorkflow.audit.table.analyze')} diff --git a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.tsx b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.tsx index 0557bc3e5d..be1ec6a7f5 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.tsx @@ -22,8 +22,12 @@ const AuditResultTable: React.FC = ({ taskID, auditLevelFilterValue, projectID, + projectName, updateTaskRecordCount, - dbType + dbType, + instanceName, + canCreateRuleException, + onRuleExceptionCreated }) => { const [currentAuditResultRecord, setCurrentAuditResultRecord] = useState(); @@ -144,6 +148,11 @@ const AuditResultTable: React.FC = ({ onClose={closeAuditResultDrawer} auditResultRecord={currentAuditResultRecord} dbType={dbType} + projectID={projectID} + projectName={projectName} + instanceName={instanceName} + canCreateRuleException={canCreateRuleException} + onRuleExceptionCreated={onRuleExceptionCreated ?? refresh} clickAnalyze={handleClickAnalyze} /> diff --git a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.type.ts b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.type.ts index 2f2d06a4a7..7ec48b0fdf 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.type.ts +++ b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/Table/index.type.ts @@ -8,6 +8,10 @@ export type AuditResultTableProps = { projectID: string; updateTaskRecordCount?: (taskId: string, sqlNumber: number) => void; dbType?: string; + projectName?: string; + instanceName?: string; + canCreateRuleException?: boolean; + onRuleExceptionCreated?: () => void; }; export type AuditResultDrawerProps = { @@ -15,5 +19,10 @@ export type AuditResultDrawerProps = { onClose: () => void; auditResultRecord?: IAuditTaskSQLResV2; dbType?: string; + projectID?: string; + projectName?: string; + instanceName?: string; + canCreateRuleException?: boolean; + onRuleExceptionCreated?: () => void; clickAnalyze: (sqlNum?: number) => void; }; diff --git a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/index.tsx b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/index.tsx index 3f5b8168e8..a18222129b 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/index.tsx +++ b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/index.tsx @@ -12,7 +12,10 @@ import DownloadRecord from '../DownloadRecord'; import AuditResultTable from './Table'; import AuditResultFilterContainer from '../AuditResultFilterContainer'; import { AuditTaskResV1AuditLevelEnum } from '@actiontech/shared/lib/api/sqle/service/common.enum'; -import { useCurrentProject } from '@actiontech/shared/lib/global'; +import { + useCurrentProject, + useCurrentUser +} from '@actiontech/shared/lib/global'; import useAuditResultFilterParams from '../AuditResultFilterContainer/useAuditResultFilterParams'; import { auditLevelDictionary, @@ -22,10 +25,12 @@ import { const AuditResultList: React.FC = ({ tasks, updateTaskRecordCount, - showTaskTab = true + showTaskTab = true, + onRuleExceptionCreated }) => { const { t } = useTranslation(); - const { projectID } = useCurrentProject(); + const { projectID, projectName, projectArchive } = useCurrentProject(); + const { isAdmin, isProjectManager } = useCurrentUser(); const { noDuplicate, setNoDuplicate, @@ -40,6 +45,10 @@ const AuditResultList: React.FC = ({ [currentTaskID, tasks] ); + const canCreateRuleException = useMemo(() => { + return (isAdmin || isProjectManager(projectName)) && !projectArchive; + }, [isAdmin, isProjectManager, projectName, projectArchive]); + const handleChangeCurrentTask = (taskID?: string) => { setCurrentTaskID(taskID); }; @@ -123,8 +132,12 @@ const AuditResultList: React.FC = ({ noDuplicate={noDuplicate} auditLevelFilterValue={auditLevelFilterValue} projectID={projectID} + projectName={projectName} updateTaskRecordCount={updateTaskRecordCount} dbType={currentTask?.instance_db_type} + instanceName={currentTask?.instance_name} + canCreateRuleException={canCreateRuleException} + onRuleExceptionCreated={onRuleExceptionCreated} /> ); diff --git a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/index.type.ts b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/index.type.ts index bedf27eb8c..38a898d69f 100644 --- a/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/index.type.ts +++ b/packages/sqle/src/page/SqlExecWorkflow/Common/AuditResultList/index.type.ts @@ -4,4 +4,5 @@ export type AuditResultListProps = { tasks: IAuditTaskResV1[]; updateTaskRecordCount?: (taskId: string, sqlNumber: number) => void; showTaskTab?: boolean; + onRuleExceptionCreated?: () => void; }; diff --git a/packages/sqle/src/page/Whitelist/List/__snapshots__/index.test.tsx.snap b/packages/sqle/src/page/Whitelist/List/__snapshots__/index.test.tsx.snap index 6c4af3c67a..3f011f23f9 100644 --- a/packages/sqle/src/page/Whitelist/List/__snapshots__/index.test.tsx.snap +++ b/packages/sqle/src/page/Whitelist/List/__snapshots__/index.test.tsx.snap @@ -43,6 +43,43 @@ exports[`slqe/Whitelist/WhitelistList should render whitelist list 1`] = ` +
+
+ + +
+
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/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(() =>