diff --git a/packages/base/src/page/Nav/SideMenu/UserMenu/Modal/VersionModal/test/__snapshots__/BasicVersionModal.test.tsx.snap b/packages/base/src/page/Nav/SideMenu/UserMenu/Modal/VersionModal/test/__snapshots__/BasicVersionModal.test.tsx.snap index b30049a529..8e6e5a31e5 100644 --- a/packages/base/src/page/Nav/SideMenu/UserMenu/Modal/VersionModal/test/__snapshots__/BasicVersionModal.test.tsx.snap +++ b/packages/base/src/page/Nav/SideMenu/UserMenu/Modal/VersionModal/test/__snapshots__/BasicVersionModal.test.tsx.snap @@ -110,7 +110,7 @@ exports[`base/Nav/SideMenu/UserMenu/BasicVersionModal render snap when open is t class="ant-typography css-dev-only-do-not-override-txh9fw" > UI: - dev-zjrc-error-p0p1 93c9fa7e2 + dev-zjrc-sql-audit-report-summary f4fab7b02
UI: - dev-zjrc-error-p0p1 93c9fa7e2 + dev-zjrc-sql-audit-report-summary f4fab7b02
UI: - dev-zjrc-error-p0p1 93c9fa7e2 + dev-zjrc-sql-audit-report-summary f4fab7b02
UI: - dev-zjrc-error-p0p1 93c9fa7e2 + dev-zjrc-sql-audit-report-summary f4fab7b02
UI: - dev-zjrc-error-p0p1 93c9fa7e2 + dev-zjrc-sql-audit-report-summary f4fab7b02
UI: - dev-zjrc-error-p0p1 93c9fa7e2 + dev-zjrc-sql-audit-report-summary f4fab7b02
( + `/v1/tasks/audits/${task_id}/audit_summary`, + paramsData, + options + ); + } + public DownloadAuditFile( params: IDownloadAuditFileParams, options?: AxiosRequestConfig diff --git a/packages/sqle/src/locale/en-US/sqlAudit.ts b/packages/sqle/src/locale/en-US/sqlAudit.ts index bc0e371278..310e3cbe3d 100644 --- a/packages/sqle/src/locale/en-US/sqlAudit.ts +++ b/packages/sqle/src/locale/en-US/sqlAudit.ts @@ -42,7 +42,41 @@ export default { download: 'Download audit result', auditID: 'Audit ID', auditRating: 'Audit rating', - auditPassRate: 'Audit pass rate' + auditPassRate: 'Audit pass rate', + sourceHeader: { + instanceName: 'Data source', + address: 'Address', + instanceDbType: 'Instance type', + instanceSchema: 'Schema', + sqlSource: 'SQL source', + sourceFiles: 'Source files', + sqlSourceEnum: { + form_data: 'Form', + sql_file: 'SQL file', + mybatis_xml_file: 'MyBatis', + zip_file: 'ZIP', + git_repository: 'GIT' + } + }, + levelHitSummary: { + title: 'Level hit summary', + sqlCount: 'SQL count', + loading: 'Loading…', + loadFailed: 'Failed to load level hit summary' + }, + ruleHitDetails: { + title: 'Rule hit details', + empty: 'No rule hits', + loading: 'Loading…', + loadFailed: 'Failed to load rule hit details', + columns: { + rule: 'Rule', + level: 'Level', + hitCount: 'Hit count', + message: 'Description', + sqlNumbers: 'Related SQL' + } + } }, create: { title: 'Create SQL audit', diff --git a/packages/sqle/src/locale/zh-CN/sqlAudit.ts b/packages/sqle/src/locale/zh-CN/sqlAudit.ts index ebfeb15550..bdd8035586 100644 --- a/packages/sqle/src/locale/zh-CN/sqlAudit.ts +++ b/packages/sqle/src/locale/zh-CN/sqlAudit.ts @@ -42,7 +42,41 @@ export default { download: '下载审核结果', auditID: '审核ID', auditRating: '审核评分', - auditPassRate: '审核通过率' + auditPassRate: '审核通过率', + sourceHeader: { + instanceName: '数据源', + address: '地址', + instanceDbType: '实例类型', + instanceSchema: 'Schema', + sqlSource: 'SQL 来源', + sourceFiles: '来源文件', + sqlSourceEnum: { + form_data: '表单', + sql_file: 'SQL 文件', + mybatis_xml_file: 'MyBatis', + zip_file: 'ZIP', + git_repository: 'GIT' + } + }, + levelHitSummary: { + title: '等级命中数', + sqlCount: 'SQL 条数', + loading: '加载中…', + loadFailed: '等级命中数加载失败' + }, + ruleHitDetails: { + title: '命中规则细则', + empty: '暂无规则命中', + loading: '加载中…', + loadFailed: '规则命中细则加载失败', + columns: { + rule: '规则', + level: '等级', + hitCount: '命中次数', + message: '说明', + sqlNumbers: '涉及 SQL' + } + } }, create: { title: '创建SQL审核', diff --git a/packages/sqle/src/page/SqlAudit/Detail/BasicInfoWrapper/index.tsx b/packages/sqle/src/page/SqlAudit/Detail/BasicInfoWrapper/index.tsx index 1756929da7..a9bf132ff6 100644 --- a/packages/sqle/src/page/SqlAudit/Detail/BasicInfoWrapper/index.tsx +++ b/packages/sqle/src/page/SqlAudit/Detail/BasicInfoWrapper/index.tsx @@ -1,25 +1,122 @@ +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { BasicInfoStyleWrapper } from './style'; import { BasicTag } from '@actiontech/shared'; import { getSQLAuditRecordsV1FilterSqlAuditStatusEnum } from '@actiontech/shared/lib/api/sqle/service/sql_audit_record/index.enum'; import SqlAuditStatusTag from '../../List/component/SqlAuditStatusTag'; -import { IAuditTaskResV1 } from '@actiontech/shared/lib/api/sqle/service/common'; +import { + IAuditTaskResV1, + ISQLAuditRecordInstance +} from '@actiontech/shared/lib/api/sqle/service/common'; +import { AuditTaskResV1SqlSourceEnum } from '@actiontech/shared/lib/api/sqle/service/common.enum'; interface IBasicInfoWrapper { id: string; tags: string[]; status?: string | getSQLAuditRecordsV1FilterSqlAuditStatusEnum; task?: IAuditTaskResV1; + instance?: ISQLAuditRecordInstance; } -const BasicInfoWrapper = ({ id, tags, status }: IBasicInfoWrapper) => { +const EMPTY = '-'; + +const displayText = (value?: string | null) => { + const trimmed = (value ?? '').trim(); + return trimmed || EMPTY; +}; + +const BasicInfoWrapper = ({ + id, + tags, + status, + task, + instance +}: IBasicInfoWrapper) => { const { t } = useTranslation(); + const address = useMemo(() => { + const host = (instance?.db_host ?? '').trim(); + const port = (instance?.db_port ?? '').trim(); + if (!host || !port) { + return EMPTY; + } + return `${host}:${port}`; + }, [instance?.db_host, instance?.db_port]); + + const sqlSourceLabel = useMemo(() => { + const source = task?.sql_source; + if (!source) { + return EMPTY; + } + const map: Partial> = { + [AuditTaskResV1SqlSourceEnum.form_data]: t( + 'sqlAudit.detail.sourceHeader.sqlSourceEnum.form_data' + ), + [AuditTaskResV1SqlSourceEnum.sql_file]: t( + 'sqlAudit.detail.sourceHeader.sqlSourceEnum.sql_file' + ), + [AuditTaskResV1SqlSourceEnum.mybatis_xml_file]: t( + 'sqlAudit.detail.sourceHeader.sqlSourceEnum.mybatis_xml_file' + ), + [AuditTaskResV1SqlSourceEnum.zip_file]: t( + 'sqlAudit.detail.sourceHeader.sqlSourceEnum.zip_file' + ), + [AuditTaskResV1SqlSourceEnum.git_repository]: t( + 'sqlAudit.detail.sourceHeader.sqlSourceEnum.git_repository' + ) + }; + return map[source as AuditTaskResV1SqlSourceEnum] ?? EMPTY; + }, [t, task?.sql_source]); + + const sourceFiles = useMemo(() => { + const names = (task?.audit_files ?? []) + .map((f) => (f.file_name ?? '').trim()) + .filter(Boolean); + return names.length ? names.join('、') : EMPTY; + }, [task?.audit_files]); + + const sourceFields: Array<{ + testId: string; + label: string; + value: string; + }> = [ + { + testId: 'sql-audit-report-header-instance_name', + label: t('sqlAudit.detail.sourceHeader.instanceName'), + value: displayText(task?.instance_name) + }, + { + testId: 'sql-audit-report-header-address', + label: t('sqlAudit.detail.sourceHeader.address'), + value: address + }, + { + testId: 'sql-audit-report-header-instance_db_type', + label: t('sqlAudit.detail.sourceHeader.instanceDbType'), + value: displayText(task?.instance_db_type) + }, + { + testId: 'sql-audit-report-header-instance_schema', + label: t('sqlAudit.detail.sourceHeader.instanceSchema'), + value: displayText(task?.instance_schema) + }, + { + testId: 'sql-audit-report-header-sql_source', + label: t('sqlAudit.detail.sourceHeader.sqlSource'), + value: sqlSourceLabel + }, + { + testId: 'sql-audit-report-header-source_files', + label: t('sqlAudit.detail.sourceHeader.sourceFiles'), + value: sourceFiles + } + ]; + return (

- {t('sqlAudit.detail.auditID')}: {id ?? '-'} + {t('sqlAudit.detail.auditID')}: {id ?? EMPTY}

@@ -29,7 +126,7 @@ const BasicInfoWrapper = ({ id, tags, status }: IBasicInfoWrapper) => { status={status as getSQLAuditRecordsV1FilterSqlAuditStatusEnum} /> ) : ( - '-' + EMPTY )}
@@ -42,9 +139,20 @@ const BasicInfoWrapper = ({ id, tags, status }: IBasicInfoWrapper) => { ); }) - : '-'} + : EMPTY}
+
+ {sourceFields.map((field) => ( +
+ {field.label}:{field.value} +
+ ))} +
); }; diff --git a/packages/sqle/src/page/SqlAudit/Detail/BasicInfoWrapper/style.ts b/packages/sqle/src/page/SqlAudit/Detail/BasicInfoWrapper/style.ts index cf8da90139..0524c78949 100644 --- a/packages/sqle/src/page/SqlAudit/Detail/BasicInfoWrapper/style.ts +++ b/packages/sqle/src/page/SqlAudit/Detail/BasicInfoWrapper/style.ts @@ -41,4 +41,18 @@ export const BasicInfoStyleWrapper = styled('div')` } } } + + .source-fields-cont { + display: flex; + flex-wrap: wrap; + gap: 8px 16px; + margin-top: 12px; + + .source-field-item { + font-size: 13px; + font-weight: 500; + line-height: 20px; + color: ${({ theme }) => theme.sharedTheme.uiToken.colorTextSecondary}; + } + } `; diff --git a/packages/sqle/src/page/SqlAudit/Detail/LevelHitSummary/index.tsx b/packages/sqle/src/page/SqlAudit/Detail/LevelHitSummary/index.tsx new file mode 100644 index 0000000000..350e06cc7a --- /dev/null +++ b/packages/sqle/src/page/SqlAudit/Detail/LevelHitSummary/index.tsx @@ -0,0 +1,109 @@ +import { useTranslation } from 'react-i18next'; +import { IAuditLevelSummary } from '@actiontech/shared/lib/api/sqle/service/common'; +import { LevelHitSummaryStyleWrapper } from './style'; + +type LevelHitSummaryProps = { + levelSummary?: IAuditLevelSummary | null; + loading?: boolean; + error?: boolean; +}; + +const toCount = (value?: number) => + typeof value === 'number' && Number.isFinite(value) ? value : 0; + +const LevelHitSummary = ({ + levelSummary, + loading, + error +}: LevelHitSummaryProps) => { + const { t } = useTranslation(); + + const errorCount = toCount(levelSummary?.error_count); + const errorP0 = toCount(levelSummary?.error_p0_count); + const errorP1 = toCount(levelSummary?.error_p1_count); + const warnCount = toCount(levelSummary?.warn_count); + const noticeCount = toCount(levelSummary?.notice_count); + const normalCount = toCount(levelSummary?.normal_count); + const sqlCount = toCount(levelSummary?.sql_count); + const errorPriorityParts = [ + errorP0 > 0 ? `P0 ${errorP0}` : '', + errorP1 > 0 ? `P1 ${errorP1}` : '' + ].filter(Boolean); + const errorPriorityText = + errorPriorityParts.length > 0 ? `(${errorPriorityParts.join(',')})` : ''; + + return ( + +
+ {t('sqlAudit.detail.levelHitSummary.title')} +
+ {error ? ( +
+ {t('sqlAudit.detail.levelHitSummary.loadFailed')} +
+ ) : loading && !levelSummary ? ( +
+ {t('sqlAudit.detail.levelHitSummary.loading')} +
+ ) : ( +
+ + {t('components.auditResultMessage.level.error')} + {errorCount} + {errorPriorityText ? ( + + {errorPriorityText} + + ) : null} + + + {t('components.auditResultMessage.level.warn')} + {warnCount} + + + {t('components.auditResultMessage.level.notice')} + {noticeCount} + + + {t('components.auditResultMessage.level.normal')} + {normalCount} + + + {t('sqlAudit.detail.levelHitSummary.sqlCount')} + {sqlCount} + +
+ )} +
+ ); +}; + +export default LevelHitSummary; diff --git a/packages/sqle/src/page/SqlAudit/Detail/LevelHitSummary/style.ts b/packages/sqle/src/page/SqlAudit/Detail/LevelHitSummary/style.ts new file mode 100644 index 0000000000..6ec5d7b4cd --- /dev/null +++ b/packages/sqle/src/page/SqlAudit/Detail/LevelHitSummary/style.ts @@ -0,0 +1,51 @@ +import { styled } from '@mui/material/styles'; + +export const LevelHitSummaryStyleWrapper = styled('div')` + padding: 16px 40px; + border-bottom: 1px solid + ${({ theme }) => theme.sharedTheme.uiToken.colorBorderSecondary}; + + .section-title { + font-size: 14px; + font-weight: 600; + line-height: 22px; + color: ${({ theme }) => theme.sharedTheme.uiToken.colorText}; + margin-bottom: 12px; + } + + .level-hit-items { + display: flex; + flex-wrap: wrap; + gap: 8px 20px; + } + + .level-hit-item { + font-size: 13px; + font-weight: 500; + line-height: 20px; + color: ${({ theme }) => theme.sharedTheme.uiToken.colorTextSecondary}; + + .level-hit-value { + font-weight: 600; + color: ${({ theme }) => theme.sharedTheme.uiToken.colorText}; + margin-left: 4px; + } + + .level-hit-sub { + margin-left: 6px; + color: ${({ theme }) => theme.sharedTheme.uiToken.colorTextSecondary}; + } + } + + .level-hit-error { + display: inline-flex; + align-items: baseline; + flex-wrap: wrap; + } + + .level-hit-empty, + .level-hit-error-msg { + font-size: 13px; + color: ${({ theme }) => theme.sharedTheme.uiToken.colorTextSecondary}; + } +`; diff --git a/packages/sqle/src/page/SqlAudit/Detail/RuleHitDetails/index.tsx b/packages/sqle/src/page/SqlAudit/Detail/RuleHitDetails/index.tsx new file mode 100644 index 0000000000..300a16dd48 --- /dev/null +++ b/packages/sqle/src/page/SqlAudit/Detail/RuleHitDetails/index.tsx @@ -0,0 +1,98 @@ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { IAuditRuleHitDetail } from '@actiontech/shared/lib/api/sqle/service/common'; +import { resolveAuditLevelDisplayMeta } from '../../../../components/AuditResultMessage/errorPriorityDisplay'; +import { RuleHitDetailsStyleWrapper } from './style'; + +type RuleHitDetailsProps = { + ruleHitDetails?: IAuditRuleHitDetail[] | null; + loading?: boolean; + error?: boolean; +}; + +const EMPTY = '-'; + +const RuleHitDetails = ({ + ruleHitDetails, + loading, + error +}: RuleHitDetailsProps) => { + const { t } = useTranslation(); + + const rows = useMemo(() => ruleHitDetails ?? [], [ruleHitDetails]); + + return ( + +
+ {t('sqlAudit.detail.ruleHitDetails.title')} +
+ {error ? ( +
+ {t('sqlAudit.detail.ruleHitDetails.loadFailed')} +
+ ) : loading && !ruleHitDetails ? ( +
+ {t('sqlAudit.detail.ruleHitDetails.loading')} +
+ ) : rows.length === 0 ? ( +
+ {t('sqlAudit.detail.ruleHitDetails.empty')} +
+ ) : ( + + + + + + + + + + + + {rows.map((item, index) => { + const displayName = + (item.message ?? '').trim() || + (item.rule_name ?? '').trim() || + EMPTY; + const meta = resolveAuditLevelDisplayMeta( + item.level, + item.error_priority + ); + const levelLabel = t(meta.labelKey); + const hitCount = + typeof item.hit_count === 'number' && + Number.isFinite(item.hit_count) + ? item.hit_count + : 0; + const sqlNumbers = (item.sql_numbers ?? []) + .map((n) => `#${n}`) + .join(', '); + const rowKey = `${item.rule_name ?? displayName}-${index}`; + + return ( + + + + + + + + ); + })} + +
{t('sqlAudit.detail.ruleHitDetails.columns.rule')}{t('sqlAudit.detail.ruleHitDetails.columns.level')}{t('sqlAudit.detail.ruleHitDetails.columns.hitCount')}{t('sqlAudit.detail.ruleHitDetails.columns.message')}{t('sqlAudit.detail.ruleHitDetails.columns.sqlNumbers')}
{displayName}{levelLabel} + {hitCount} + {(item.message ?? '').trim() || EMPTY}{sqlNumbers || EMPTY}
+ )} +
+ ); +}; + +export default RuleHitDetails; diff --git a/packages/sqle/src/page/SqlAudit/Detail/RuleHitDetails/style.ts b/packages/sqle/src/page/SqlAudit/Detail/RuleHitDetails/style.ts new file mode 100644 index 0000000000..bae177dd38 --- /dev/null +++ b/packages/sqle/src/page/SqlAudit/Detail/RuleHitDetails/style.ts @@ -0,0 +1,46 @@ +import { styled } from '@mui/material/styles'; + +export const RuleHitDetailsStyleWrapper = styled('div')` + padding: 16px 40px; + border-bottom: 1px solid + ${({ theme }) => theme.sharedTheme.uiToken.colorBorderSecondary}; + + .section-title { + font-size: 14px; + font-weight: 600; + line-height: 22px; + color: ${({ theme }) => theme.sharedTheme.uiToken.colorText}; + margin-bottom: 12px; + } + + .rule-hit-table { + width: 100%; + border-collapse: collapse; + + th, + td { + padding: 8px 12px; + text-align: left; + font-size: 13px; + line-height: 20px; + border-bottom: 1px solid + ${({ theme }) => theme.sharedTheme.uiToken.colorBorderSecondary}; + } + + th { + font-weight: 600; + color: ${({ theme }) => theme.sharedTheme.uiToken.colorTextSecondary}; + } + + td { + color: ${({ theme }) => theme.sharedTheme.uiToken.colorText}; + font-weight: 500; + } + } + + .rule-hit-empty, + .rule-hit-error-msg { + font-size: 13px; + color: ${({ theme }) => theme.sharedTheme.uiToken.colorTextSecondary}; + } +`; diff --git a/packages/sqle/src/page/SqlAudit/Detail/__snapshots__/index.test.tsx.snap b/packages/sqle/src/page/SqlAudit/Detail/__snapshots__/index.test.tsx.snap index b4d0a4d7b0..80f8e1f72b 100644 --- a/packages/sqle/src/page/SqlAudit/Detail/__snapshots__/index.test.tsx.snap +++ b/packages/sqle/src/page/SqlAudit/Detail/__snapshots__/index.test.tsx.snap @@ -84,7 +84,7 @@ exports[`sqle/SqlAudit/Detail should match snap shot 1`] = ` style="height: 60px;" />

+
+
+ 数据源 + : + mysql-1 +
+
+ 地址 + : + 10.196.62.23:3333 +
+
+ 实例类型 + : + MySQL +
+
+ Schema + : + test +
+
+ SQL 来源 + : + 表单 +
+
+ 来源文件 + : + - +
+
+ +
+
+ 等级命中数 +
+
+ + 错误 + + 0 + + + + 告警 + + 0 + + + + 提示 + + 0 + + + + 普通 + + 0 + + + + SQL 条数 + + 0 + + +
+
+
+
+ 命中规则细则 +
+
+ 暂无规则命中 +
{ it('should match snap shot', async () => { const getSqlAuditRecordDetailSpy = sqlAuditRecord.getSQLAuditRecord(); const getAuditTaskSqlSpy = task.getAuditTaskSQLs(); + const getAuditTaskSummarySpy = task.getAuditTaskSummary(); const { baseElement } = renderWithThemeAndRedux( @@ -40,11 +41,13 @@ describe('sqle/SqlAudit/Detail', () => { expect(getSqlAuditRecordDetailSpy).toHaveBeenCalledTimes(1); expect(getAuditTaskSqlSpy).toHaveBeenCalledTimes(1); await act(async () => jest.advanceTimersByTime(3000)); + expect(getAuditTaskSummarySpy).toHaveBeenCalled(); expect(baseElement).toMatchSnapshot(); }); it('should hide create button when project is archived', async () => { sqlAuditRecord.getSQLAuditRecord(); + task.getAuditTaskSummary(); mockUseCurrentProjectSpy.mockClear(); mockUseCurrentProjectSpy.mockImplementation(() => ({ ...mockProjectInfo, diff --git a/packages/sqle/src/page/SqlAudit/Detail/index.tsx b/packages/sqle/src/page/SqlAudit/Detail/index.tsx index cd13bb166b..9d57b968a7 100644 --- a/packages/sqle/src/page/SqlAudit/Detail/index.tsx +++ b/packages/sqle/src/page/SqlAudit/Detail/index.tsx @@ -4,6 +4,8 @@ import { useRequest } from 'ahooks'; import { Spin } from 'antd'; import { BasicButton, PageHeader } from '@actiontech/shared'; import BasicInfoWrapper from './BasicInfoWrapper'; +import LevelHitSummary from './LevelHitSummary'; +import RuleHitDetails from './RuleHitDetails'; import { useCurrentProject } from '@actiontech/shared/lib/global'; import sql_audit_record from '@actiontech/shared/lib/api/sqle/service/sql_audit_record'; import task from '@actiontech/shared/lib/api/sqle/service/task'; @@ -42,6 +44,7 @@ const SqlAuditDetail = () => { | undefined; const nestedPriority = (nestedTask?.audit_error_priority ?? '').trim(); const needTaskPriorityEnrich = !!nestedTask?.task_id && !nestedPriority; + const taskId = nestedTask?.task_id ? `${nestedTask.task_id}` : ''; const { data: enrichedTaskPriority, loading: taskPriorityLoading } = useRequest( @@ -58,12 +61,31 @@ const SqlAuditDetail = () => { } ); + /** + * 任务级汇总:只依赖 task_id,与 SQL 明细分页 / 等级筛选无联动(AC-4)。 + */ + const { + data: auditSummary, + loading: summaryLoading, + error: summaryError + } = useRequest( + () => + task + .getAuditTaskSummaryV1({ task_id: taskId }) + .then((res) => res.data.data), + { + ready: !!taskId, + refreshDeps: [taskId] + } + ); + const basicInfoData = useMemo(() => { return { id: pluginAuditRecord?.sql_audit_record_id ?? '', tags: pluginAuditRecord?.tags ?? [], status: pluginAuditRecord?.sql_audit_status, - task: pluginAuditRecord?.task + task: pluginAuditRecord?.task, + instance: pluginAuditRecord?.instance }; }, [pluginAuditRecord]); @@ -127,6 +149,20 @@ const SqlAuditDetail = () => { style={{ height: '60px' }} /> + {taskId ? ( + <> + + + + ) : null} + createSpySuccessResponse({ + data: { + level_summary: { + error_count: 0, + error_p0_count: 0, + error_p1_count: 0, + warn_count: 0, + notice_count: 0, + normal_count: 0, + sql_count: 0 + }, + rule_hit_details: [] + } + }) + ); + return spy; + } + public getAuditFileList() { const spy = jest.spyOn(task, 'getAuditFileList'); spy.mockImplementation(() =>