From 573b5f22cd242e67ea420342f750115c6c386155 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 + .../lib/api/sqle/service/SqlManage/index.d.ts | 26 +++- .../api/sqle/service/SqlManage/index.enum.ts | 20 ++++ .../lib/api/sqle/service/SqlManage/index.ts | 51 ++++++++ .../shared/lib/api/sqle/service/common.d.ts | 74 ++++++++++++ packages/sqle/src/data/EmitterKey.ts | 1 + .../sqle/src/locale/en-US/managementConf.ts | 13 ++ .../sqle/src/locale/en-US/sqlManagement.ts | 21 ++++ .../sqle/src/locale/zh-CN/managementConf.ts | 12 ++ .../sqle/src/locale/zh-CN/sqlManagement.ts | 33 +++++ .../page/SqlAnalyze/SqlAnalyze/SqlAnalyze.tsx | 30 +++-- .../src/page/SqlAnalyze/SqlAnalyze/index.tsx | 2 + .../SqlManage/RemediationCompare.tsx | 113 ++++++++++++++++++ .../src/page/SqlAnalyze/SqlManage/index.tsx | 17 ++- .../SQLEEIndex/RemediationStatusTag.tsx | 36 ++++++ .../Detail/ScanTypeSqlCollection/style.ts | 55 +++++++++ .../SqlManagementRemediationReport/index.tsx | 70 +++++++++++ packages/sqle/src/router/config.tsx | 4 + 19 files changed, 564 insertions(+), 16 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 75c3595578..40443b36a2 100644 --- a/packages/base/src/locale/en-US/dmsMenu.ts +++ b/packages/base/src/locale/en-US/dmsMenu.ts @@ -63,6 +63,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 232d307dd4..f5a4f89abf 100644 --- a/packages/base/src/locale/zh-CN/dmsMenu.ts +++ b/packages/base/src/locale/zh-CN/dmsMenu.ts @@ -65,6 +65,7 @@ export default { title: '全局设置', userCenter: '用户中心', reportStatistics: '报表统计', + sqlManagementRemediationReport: 'SQL 管控整改报表', viewRule: '查看规则', ruleManage: '规则管理', system: '系统设置', 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 47e4da1ef4..c369d280f1 100644 --- a/packages/shared/lib/api/sqle/service/SqlManage/index.d.ts +++ b/packages/shared/lib/api/sqle/service/SqlManage/index.d.ts @@ -44,7 +44,9 @@ import { ISqlManageCodingReq, IPostSqlManageCodingResp, IGetSqlManageSqlAnalysisResp, - ISqlManageAnalysisChartResp + ISqlManageAnalysisChartResp, + IGetSqlManageRemediationResp, + IGetSqlManageRemediationOverviewResp } from '../common.d'; export interface IGetGlobalSqlManageListParams { @@ -162,6 +164,8 @@ export interface IExportSqlManageV1Params { sort_order?: exportSqlManageV1SortOrderEnum; } +export interface IExportGlobalSqlManageRemediationV1Params {} + export interface IGetSqlManageRuleTipsParams { project_name: string; } @@ -247,6 +251,26 @@ 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 {} + export interface IExportSqlManageV2Params { project_name: string; 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 0d04710fc9..331d8041db 100644 --- a/packages/shared/lib/api/sqle/service/SqlManage/index.enum.ts +++ b/packages/shared/lib/api/sqle/service/SqlManage/index.enum.ts @@ -102,6 +102,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', @@ -136,6 +144,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 535cf514a6..f70533e1b5 100644 --- a/packages/shared/lib/api/sqle/service/SqlManage/index.ts +++ b/packages/shared/lib/api/sqle/service/SqlManage/index.ts @@ -18,6 +18,7 @@ import { IBatchUpdateSqlManageParams, IBatchUpdateSqlManageReturn, IExportSqlManageV1Params, + IExportGlobalSqlManageRemediationV1Params, IGetSqlManageRuleTipsParams, IGetSqlManageRuleTipsReturn, ISendSqlManageParams, @@ -28,6 +29,10 @@ import { IGetSqlManageSqlAnalysisChartV1Return, IGetSqlManageListV2Params, IGetSqlManageListV2Return, + IGetSqlManageRemediationV1Params, + IGetSqlManageRemediationV1Return, + IGetSqlManageRemediationOverviewV1Params, + IGetSqlManageRemediationOverviewV1Return, IExportSqlManageV2Params, IGetSqlManageListV3Params, IGetSqlManageListV3Return @@ -118,6 +123,19 @@ 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 GetSqlManageRuleTips( params: IGetSqlManageRuleTipsParams, options?: AxiosRequestConfig @@ -199,6 +217,39 @@ class SqlManageService extends ServiceBase { ); } + 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 + ); + } + public exportSqlManageV2( params: IExportSqlManageV2Params, 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 57804bfc7f..c4ce25b8c6 100644 --- a/packages/shared/lib/api/sqle/service/common.d.ts +++ b/packages/shared/lib/api/sqle/service/common.d.ts @@ -5103,3 +5103,77 @@ export interface ICompleteWorkflowReq { workflow_id?: string; } + +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 1650a2d028..58f4ec1475 100644 --- a/packages/sqle/src/data/EmitterKey.ts +++ b/packages/sqle/src/data/EmitterKey.ts @@ -27,6 +27,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 a52c99dd8c..0e97c50c07 100644 --- a/packages/sqle/src/locale/en-US/sqlManagement.ts +++ b/packages/sqle/src/locale/en-US/sqlManagement.ts @@ -11,6 +11,19 @@ export default { } } }, + 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', @@ -67,9 +80,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 d49f3a62d6..1252395e15 100644 --- a/packages/sqle/src/locale/zh-CN/managementConf.ts +++ b/packages/sqle/src/locale/zh-CN/managementConf.ts @@ -116,6 +116,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 9d1f95c2e4..8591d7e73f 100644 --- a/packages/sqle/src/locale/zh-CN/sqlManagement.ts +++ b/packages/sqle/src/locale/zh-CN/sqlManagement.ts @@ -11,6 +11,18 @@ export default { } } }, + remediationReport: { + pageTitle: 'SQL 管控整改报表', + description: + '导出全局范围内 SQL 管控整改追踪数据,Excel 包含概览、规则维度汇总和明细。', + exportButton: '导出 SQL 管控整改报表', + exporting: '正在导出 SQL 管控整改报表', + exportSuccessTips: 'SQL 管控整改报表导出成功', + scopeTitle: '导出范围', + scopeContent: + '全局范围:覆盖当前用户有平台管理权限的全部项目 SQL 管控整改数据', + permissionTips: '仅平台超管 / 全局运维可见并可导出。' + }, statistics: { SQLTotalNum: 'SQL总数', problemSQlNum: '问题SQL数', @@ -92,9 +104,17 @@ export default { occurrenceCount: '出现数量', personInCharge: '负责人', status: '状态', + remediationStatus: '整改状态', comment: '备注', endpoints: '端点信息' }, + remediationStatus: { + resolved: '已整改', + partially_fixed: '部分整改', + unchanged: '未变化', + deteriorated: '恶化', + newly_discovered: '新发现' + }, filter: { time: '时间范围', status: { @@ -127,5 +147,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 e33b5b516a..ed3a2d9bf3 100644 --- a/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/SqlAnalyze.tsx +++ b/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/SqlAnalyze.tsx @@ -12,6 +12,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(); const { @@ -20,6 +21,7 @@ const SqlAnalyze: React.FC = (props) => { errorMessage, loading = false, performanceStatistics, + remediationCompare, errorType = 'error', sqlExecPlanCostDataSource, getSqlExecPlanCostDataSource, @@ -69,25 +71,27 @@ const SqlAnalyze: React.FC = (props) => { return ; }; const getSegmentedOption = useMemo(() => { + const options = [ + { + label: t('sqlAnalyze.sqlExplain'), + value: 'sql' + } + ]; + if (remediationCompare) { + options.push({ + label: t('sqlAnalyze.remediationCompare'), + value: 'remediation' + }); + } if ( !( Array.isArray(tableMetas?.table_meta_items) && tableMetas?.table_meta_items.length ) ) { - return [ - { - label: t('sqlAnalyze.sqlExplain'), - value: 'sql' - } - ]; + return options; } - return [ - { - label: t('sqlAnalyze.sqlExplain'), - value: 'sql' - } - ].concat( + return options.concat( (tableMetas?.table_meta_items ?? []).map((table) => { return { label: t('sqlAnalyze.tableTitle', { @@ -97,7 +101,7 @@ const SqlAnalyze: React.FC = (props) => { }; }) ); - }, [tableMetas?.table_meta_items, t]); + }, [remediationCompare, tableMetas?.table_meta_items, t]); return ( diff --git a/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/index.tsx b/packages/sqle/src/page/SqlAnalyze/SqlAnalyze/index.tsx index 417d3dcd60..b0aa92c9d5 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, @@ -17,6 +18,7 @@ export type SqlAnalyzeProps = { tableMetas?: ITableMetas; sqlExplain?: ISQLExplain; performanceStatistics?: IPerformanceStatistics; + remediationCompare?: ISqlManageRemediation; loading?: boolean; sqlExecPlanCostDataSource?: IChartPoint[]; getSqlExecPlanCostDataSourceLoading?: boolean; 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..9960cbb8cb --- /dev/null +++ b/packages/sqle/src/page/SqlAnalyze/SqlManage/RemediationCompare.tsx @@ -0,0 +1,113 @@ +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 filterRuleNames = (names?: Array) => + names?.filter((name): name is string => !!name); + +const ruleNames = (rules?: ISqlManageRemediation['rule_diff']) => ({ + resolved: filterRuleNames(rules?.resolved?.map((rule) => rule.rule_name)), + newRules: filterRuleNames(rules?.new?.map((rule) => rule.rule_name)), + unchanged: filterRuleNames(rules?.unchanged?.map((rule) => rule.rule_name)) +}); + +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 AuditResultList: React.FC<{ + results?: NonNullable; +}> = ({ results }) => { + if (!results?.length) { + return ; + } + + return ( + + {results.map((result, index) => ( + + ))} + + ); +}; + +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 bdf209f9a5..00f3b9a748 100644 --- a/packages/sqle/src/page/SqlAnalyze/SqlManage/index.tsx +++ b/packages/sqle/src/page/SqlAnalyze/SqlManage/index.tsx @@ -5,6 +5,7 @@ import { ResponseCode } from '../../../data/common'; import SqlManage from '@actiontech/shared/lib/api/sqle/service/SqlManage'; import { IPerformanceStatistics, + ISqlManageRemediation, ISQLExplain, ITableMetas } from '@actiontech/shared/lib/api/sqle/service/common'; @@ -30,6 +31,8 @@ const SQLManageAnalyze = () => { const [tableMetas, setTableMetas] = useState(); const [performanceStatistics, setPerformancesStatistics] = useState(); + const [remediationCompare, setRemediationCompare] = + useState(); const [ loading, @@ -53,11 +56,17 @@ const SQLManageAnalyze = () => { async (affectRowsEnabled = false) => { startGetSqlAnalyze(); try { - const res = await SqlManage.GetSqlManageSqlAnalysisV1({ + const [res, remediationRes] = await Promise.all([ + SqlManage.GetSqlManageSqlAnalysisV1({ sql_manage_id: urlParams.sqlManageId ?? '', project_name: projectName, affectRowsEnabled - }); + }), + SqlManage.GetSqlManageRemediationV1({ + sql_manage_id: urlParams.sqlManageId ?? '', + project_name: projectName + }) + ]); if (res.data.code === ResponseCode.SUCCESS) { if (affectRowsEnabled) { setPerformanceInfoLoaded(); @@ -70,6 +79,9 @@ const SQLManageAnalyze = () => { setSqlExplain(data?.sql_explain); setTableMetas(data?.table_metas); setPerformancesStatistics(data?.performance_statistics); + if (remediationRes.data.code === ResponseCode.SUCCESS) { + setRemediationCompare(remediationRes.data.data); + } setOptimizationCreationParams({ instance_name: queryParams?.instance_name, schema_name: queryParams?.schema, @@ -132,6 +144,7 @@ const SQLManageAnalyze = () => { sqlExplain={sqlExplain} errorMessage={errorMessage} performanceStatistics={performanceStatistics} + remediationCompare={remediationCompare} loading={loading} sqlExecPlanCostDataSource={data} getSqlExecPlanCostDataSourceLoading={ 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..52c8e25b84 --- /dev/null +++ b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/RemediationStatusTag.tsx @@ -0,0 +1,36 @@ +import { BasicTag } from '@actiontech/dms-kit'; +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}` as any)} + + ); +}; + +export default RemediationStatusTag; diff --git a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/style.ts b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/style.ts index 52aabf0add..81fb7b9b9c 100644 --- a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/style.ts +++ b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/style.ts @@ -2,6 +2,61 @@ import { styled } from '@mui/material'; import { ActiontechTable } from '@actiontech/dms-kit/es/components/ActiontechTable'; export const ScanTypeSqlCollectionStyleWrapper = styled('section')` + .remediation-overview-card { + margin-bottom: 16px; + } + + .remediation-overview-metrics { + display: flex; + flex-wrap: wrap; + margin: -8px; + } + + .remediation-overview-metric { + min-width: 160px; + margin: 8px; + padding-right: 24px; + + strong { + display: block; + color: ${({ theme }) => 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/SqlManagementRemediationReport/index.tsx b/packages/sqle/src/page/SqlManagementRemediationReport/index.tsx new file mode 100644 index 0000000000..da6cff3388 --- /dev/null +++ b/packages/sqle/src/page/SqlManagementRemediationReport/index.tsx @@ -0,0 +1,70 @@ +import { BasicButton, PageHeader } from '@actiontech/dms-kit'; +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 37b30cc5b8..eb8acb5c46 100644 --- a/packages/sqle/src/router/config.tsx +++ b/packages/sqle/src/router/config.tsx @@ -196,6 +196,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') ); From 3b1cef1b3dd1672800f29935927549029df516ac Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Mon, 22 Jun 2026 16:52:55 +0800 Subject: [PATCH 2/2] feat: refine SQL remediation tracking UI Fixes #2988 --- .../components/AuditResultMessage/index.tsx | 3 +- packages/sqle/src/data/ModalName.ts | 1 + .../sqle/src/locale/en-US/sqlManagement.ts | 19 ++++ .../sqle/src/locale/zh-CN/sqlManagement.ts | 4 + .../SqlManage/RemediationCompare.test.tsx | 51 +++++++++++ .../Modals/RemediationDetailDrawer/index.tsx | 87 +++++++++++++++++++ .../component/SQLEEIndex/Modals/index.tsx | 3 + .../SQLEEIndex/RemediationStatusTag.tsx | 23 +++-- .../component/SQLEEIndex/column.tsx | 26 ++++++ .../component/SQLEEIndex/index.test.tsx | 2 +- .../__tests__/index.test.tsx | 2 + 11 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 packages/sqle/src/page/SqlAnalyze/SqlManage/RemediationCompare.test.tsx create mode 100644 packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modals/RemediationDetailDrawer/index.tsx diff --git a/packages/sqle/src/components/AuditResultMessage/index.tsx b/packages/sqle/src/components/AuditResultMessage/index.tsx index 370a05f597..bb4568a2f2 100644 --- a/packages/sqle/src/components/AuditResultMessage/index.tsx +++ b/packages/sqle/src/components/AuditResultMessage/index.tsx @@ -41,7 +41,8 @@ const AuditResultMessage = ({ } }, [auditResult]); const renderMessage = useMemo(() => { - const { level, message } = auditResult || {}; + const { level, message, rule_name } = auditResult || {}; + if (rule_name) return rule_name; if (message) return message; if (passStatusLevelData.includes(level ?? '')) return t('components.auditResultMessage.auditPassed'); diff --git a/packages/sqle/src/data/ModalName.ts b/packages/sqle/src/data/ModalName.ts index 8e15f02595..2beb59d903 100644 --- a/packages/sqle/src/data/ModalName.ts +++ b/packages/sqle/src/data/ModalName.ts @@ -16,6 +16,7 @@ export enum ModalName { Assignment_Member_Batch = 'Assignment_Member_Batch', Change_Status_Single = 'Change_Status_Single', View_Audit_Result_Drawer = 'View_Audit_Result_Drawer', + View_Remediation_Detail_Drawer = 'View_Remediation_Detail_Drawer', Change_SQL_Priority = 'Change_SQL_Priority', Push_To_Coding = 'Push_To_Coding', diff --git a/packages/sqle/src/locale/en-US/sqlManagement.ts b/packages/sqle/src/locale/en-US/sqlManagement.ts index 0e97c50c07..de59246acb 100644 --- a/packages/sqle/src/locale/en-US/sqlManagement.ts +++ b/packages/sqle/src/locale/en-US/sqlManagement.ts @@ -75,6 +75,8 @@ export default { highPriority: 'High priority', lowPriority: 'Low priority', auditResult: 'Audit result', + firstAuditResult: 'Initial audit result', + currentAuditResult: 'Current audit result', firstOccurrence: 'First occurrence time', lastOccurrence: 'Last occurrence time', occurrenceCount: 'Occurrence count', @@ -121,5 +123,22 @@ export default { statusReport: { title: 'SQL audit result' } + }, + remediationCompare: { + tab: 'Remediation compare', + drawerTitle: 'Remediation status details', + title: 'Remediation compare', + description: + 'Compare the first audit result with the latest audit result and show rule changes.', + loadFailed: 'Failed to load remediation details. Please try again later.', + firstAuditMissing: + 'No first audit snapshot is available. The latest result is shown as newly discovered issues.', + firstAuditResult: 'First audit result', + latestAuditResult: 'Latest audit result', + ruleDiffTitle: 'Rule differences', + resolved: 'Resolved rules', + new: 'New rules', + unchanged: 'Unchanged rules', + emptyRules: 'No matched rules' } }; diff --git a/packages/sqle/src/locale/zh-CN/sqlManagement.ts b/packages/sqle/src/locale/zh-CN/sqlManagement.ts index 8591d7e73f..5c6dad6e3f 100644 --- a/packages/sqle/src/locale/zh-CN/sqlManagement.ts +++ b/packages/sqle/src/locale/zh-CN/sqlManagement.ts @@ -99,6 +99,8 @@ export default { highPriority: '高优先', lowPriority: '低优先', auditResult: '审核结果', + firstAuditResult: '最初审核结果', + currentAuditResult: '当前审核结果', firstOccurrence: '初次出现时间', lastOccurrence: '最后一次出现时间', occurrenceCount: '出现数量', @@ -150,8 +152,10 @@ export default { }, remediationCompare: { tab: '整改对比', + drawerTitle: '整改状态详情', title: '整改对比', description: '对比首次审核与最末次审核结果,展示规则整改变化', + loadFailed: '整改详情加载失败,请稍后重试。', firstAuditMissing: '无首次审核快照,当前按最末次结果展示新发现问题。', firstAuditResult: '首次审核结果', latestAuditResult: '最末次审核结果', diff --git a/packages/sqle/src/page/SqlAnalyze/SqlManage/RemediationCompare.test.tsx b/packages/sqle/src/page/SqlAnalyze/SqlManage/RemediationCompare.test.tsx new file mode 100644 index 0000000000..5e0f64e342 --- /dev/null +++ b/packages/sqle/src/page/SqlAnalyze/SqlManage/RemediationCompare.test.tsx @@ -0,0 +1,51 @@ +import { screen } from '@testing-library/react'; +import { renderWithTheme } from '../../../testUtils/customRender'; +import RemediationCompare from './RemediationCompare'; + +describe('sqle/page/SqlAnalyze/SqlManage/RemediationCompare', () => { + it('renders multiple first and latest audit results by rule name', () => { + renderWithTheme( + + ); + + expect(screen.getAllByText('first_rule_without_primary_key')).toHaveLength(2); + expect(screen.getByText('first_rule_without_comment')).toBeInTheDocument(); + expect(screen.getAllByText('latest_rule_without_index')).toHaveLength(2); + expect(screen.getAllByText('latest_rule_without_limit')).toHaveLength(2); + expect(screen.queryByText('首次命中:建表建议设置主键')).toBeNull(); + expect(screen.queryByText('最末次命中:查询建议添加 LIMIT')).toBeNull(); + }); +}); diff --git a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modals/RemediationDetailDrawer/index.tsx b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modals/RemediationDetailDrawer/index.tsx new file mode 100644 index 0000000000..18aba6d4ba --- /dev/null +++ b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modals/RemediationDetailDrawer/index.tsx @@ -0,0 +1,87 @@ +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Alert, Empty, Spin } from 'antd'; +import { BasicDrawer } from '@actiontech/shared'; +import { useCurrentProject } from '@actiontech/shared/lib/global'; +import SqlManage from '@actiontech/shared/lib/api/sqle/service/SqlManage'; +import { ResponseCode } from '@actiontech/shared/lib/enum'; +import { useRequest } from 'ahooks'; +import { ModalName } from '../../../../../../data/ModalName'; +import RemediationCompare from '../../../../../SqlAnalyze/SqlManage/RemediationCompare'; +import useSqlManagementRedux from '../../hooks/useSqlManagementRedux'; + +const RemediationDetailDrawer = () => { + const { t } = useTranslation(); + const { projectName } = useCurrentProject(); + + const { + open: visible, + selectSqlManagement: selectedData, + setSelectData, + updateModalStatus + } = useSqlManagementRedux(ModalName.View_Remediation_Detail_Drawer); + + const closeModal = () => { + updateModalStatus(ModalName.View_Remediation_Detail_Drawer, false); + setSelectData(null); + }; + + const { + data: remediationDetail, + loading, + error, + run, + mutate + } = useRequest( + (sqlManageId: string) => + SqlManage.GetSqlManageRemediationV1({ + project_name: projectName, + sql_manage_id: sqlManageId + }).then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + return res.data.data; + } + return undefined; + }), + { + manual: true + } + ); + + useEffect(() => { + if (visible && selectedData?.id) { + run(selectedData.id.toString()); + } + if (!visible) { + mutate(undefined); + } + }, [mutate, run, selectedData?.id, visible]); + + return ( + + + {error ? ( + + ) : remediationDetail ? ( + + ) : ( + + )} + + + ); +}; + +export default RemediationDetailDrawer; diff --git a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modals/index.tsx b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modals/index.tsx index e424cbe58a..b5b4d3cad3 100644 --- a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modals/index.tsx +++ b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/Modals/index.tsx @@ -11,6 +11,7 @@ import EmitterKey from '../../../../../data/EmitterKey'; import AddWhitelist from '../../../../Whitelist/Drawer/AddWhitelist'; import ChangePriority from './ChangePriority'; import PushToCodingModal from './PushToCoding'; +import RemediationDetailDrawer from './RemediationDetailDrawer'; const SqlManagementModals = () => { const { initModalStatus } = useSqlManagementRedux(); @@ -21,6 +22,7 @@ const SqlManagementModals = () => { [ModalName.Assignment_Member_Batch]: false, [ModalName.Change_Status_Single]: false, [ModalName.View_Audit_Result_Drawer]: false, + [ModalName.View_Remediation_Detail_Drawer]: false, [ModalName.Change_SQL_Priority]: false, [ModalName.Push_To_Coding]: false }); @@ -41,6 +43,7 @@ const SqlManagementModals = () => { + ); }; diff --git a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/RemediationStatusTag.tsx b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/RemediationStatusTag.tsx index 52c8e25b84..965613566a 100644 --- a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/RemediationStatusTag.tsx +++ b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/RemediationStatusTag.tsx @@ -1,5 +1,6 @@ import { BasicTag } from '@actiontech/dms-kit'; import { t } from '../../../../locale'; +import { CSSProperties } from 'react'; export const remediationStatusOptions = [ 'resolved', @@ -7,13 +8,20 @@ export const remediationStatusOptions = [ 'unchanged', 'deteriorated', 'newly_discovered' -]; +] as const; + +export type RemediationStatus = (typeof remediationStatusOptions)[number]; type RemediationStatusTagProps = { - status?: string; + status?: RemediationStatus; + onClick?: () => void; +}; + +const clickableStyle: CSSProperties = { + cursor: 'pointer' }; -const remediationStatusColor: Record = { +const remediationStatusColor: Record = { resolved: 'green', partially_fixed: 'blue', unchanged: 'gray', @@ -22,12 +30,17 @@ const remediationStatusColor: Record = { }; const RemediationStatusTag: React.FC = ({ - status + status, + onClick }) => { const currentStatus = status || 'unchanged'; return ( - + {t(`sqlManagement.table.remediationStatus.${currentStatus}` as any)} ); diff --git a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/column.tsx b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/column.tsx index 0764712fa1..f4e092ae13 100644 --- a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/column.tsx +++ b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/column.tsx @@ -13,6 +13,9 @@ import { BasicToolTip, CustomAvatar, EditText } from '@actiontech/dms-kit'; import { SQLRenderer, TypedLink } from '@actiontech/shared'; import { Avatar, Space } from 'antd'; import StatusTag from './StatusTag'; +import RemediationStatusTag, { + RemediationStatus +} from './RemediationStatusTag'; import { BasicTag, basicTooltipCommonProps } from '@actiontech/dms-kit'; import { BasicTypographyEllipsis } from '@actiontech/shared'; import { SqlManageAuditStatusEnum } from '@actiontech/shared/lib/api/sqle/service/common.enum'; @@ -31,6 +34,7 @@ export type ExtraFilterMetaType = ISqlManage & { filter_instance_id?: string; filter_audit_level?: string; filter_rule_name?: string; + filter_remediation_status?: string; time?: string; }; export const ExtraFilterMeta: () => ActiontechTableFilterMeta< @@ -97,6 +101,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 + } ] ]); }; @@ -341,6 +354,19 @@ const sqlManagementColumn: ( ); } }, + { + dataIndex: 'remediation_status', + title: () => t('sqlManagement.table.column.remediationStatus'), + width: 140, + render: (status, record) => ( + { + openModal(ModalName.View_Remediation_Detail_Drawer, record); + }} + /> + ) + }, { dataIndex: 'status', title: () => t('sqlManagement.table.column.status'), diff --git a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/index.test.tsx b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/index.test.tsx index 089fa08935..8d8ce378d8 100644 --- a/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/index.test.tsx +++ b/packages/sqle/src/page/SqlManagement/component/SQLEEIndex/index.test.tsx @@ -181,7 +181,7 @@ describe('page/SqlManagement/SQLEEIndex', () => { fireEvent.click(screen.getByText('筛选')); expect(screen.getByText('审核规则')).toBeInTheDocument(); const searchInput = getAllBySelector('.ant-select-selection-search-input'); - fireEvent.mouseDown(searchInput[searchInput.length - 1]); + fireEvent.mouseDown(searchInput[searchInput.length - 2]); await act(async () => jest.advanceTimersByTime(3000)); expect(baseElement).toMatchSnapshot(); const options = getAllBySelector('.ant-select-item-option'); diff --git a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/__tests__/index.test.tsx b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/__tests__/index.test.tsx index 554f6b36c3..678545e831 100644 --- a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/__tests__/index.test.tsx +++ b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/__tests__/index.test.tsx @@ -47,6 +47,8 @@ describe('test ScanTypeSqlCollection', () => { instanceType={instanceType} exportDone={jest.fn()} exportPending={jest.fn()} + remediationExportDone={jest.fn()} + remediationExportPending={jest.fn()} auditPlanType="default" instanceName="test" />