diff --git a/packages/sqle/src/components/AuditResultMessage/ResultIconRender.tsx b/packages/sqle/src/components/AuditResultMessage/ResultIconRender.tsx index 96f1f922c1..b447ef3561 100644 --- a/packages/sqle/src/components/AuditResultMessage/ResultIconRender.tsx +++ b/packages/sqle/src/components/AuditResultMessage/ResultIconRender.tsx @@ -1,5 +1,4 @@ import React, { useMemo } from 'react'; - import { Space } from 'antd'; import { CheckCircleFilled, @@ -7,36 +6,70 @@ import { InfoHexagonFilled, CloseCircleFilled } from '@actiontech/icons'; +import { useTranslation } from 'react-i18next'; +import { ResultIconRenderProps } from './index.type'; + +const IconLevelDictionary = { + normal: , + notice: , + warn: , + error: +}; +/** @deprecated use ResultIconRenderProps instead */ export type IResultIconRender = { iconLevels: string[]; }; -const ResultIconRender = (props: IResultIconRender) => { - const { iconLevels } = props; +const ResultIconRender = (props: ResultIconRenderProps) => { + const { auditResultInfo, iconLevels } = props; + const { t } = useTranslation(); - const iconData = useMemo(() => { - return Array.from(new Set(iconLevels.filter((icon: string) => icon))); + const legacyIconData = useMemo(() => { + return Array.from( + new Set((iconLevels ?? []).filter((icon: string) => icon)) + ); }, [iconLevels]); - const renderIcon = useMemo(() => { - return { - normal: , - notice: , - warn: , - error: - }; - }, []); + const auditResultIconData = useMemo(() => { + return Array.from( + new Set(auditResultInfo?.map((v) => v.level)?.filter(Boolean)) + ); + }, [auditResultInfo]); + + if (auditResultInfo) { + if (auditResultInfo.some((item) => item.executionFailed)) { + return ( + + + {t('components.auditResultMessage.hasException')} + + ); + } + + return ( + + {auditResultIconData.length + ? auditResultIconData.map((icon) => ( + + {IconLevelDictionary[ + icon as keyof typeof IconLevelDictionary + ] ?? null} + + )) + : IconLevelDictionary.normal} + + ); + } return ( - {iconData.map((icon) => { - return ( - - {renderIcon[icon as keyof typeof renderIcon] ?? null} - - ); - })} + {legacyIconData.map((icon) => ( + + {IconLevelDictionary[icon as keyof typeof IconLevelDictionary] ?? + null} + + ))} ); }; diff --git a/packages/sqle/src/components/AuditResultMessage/index.type.ts b/packages/sqle/src/components/AuditResultMessage/index.type.ts index beff128de8..b5add0c48a 100644 --- a/packages/sqle/src/components/AuditResultMessage/index.type.ts +++ b/packages/sqle/src/components/AuditResultMessage/index.type.ts @@ -8,3 +8,13 @@ export type AuditResultMessageProps = { isRuleDeleted?: boolean; auditStatus?: string; }; + +export type AuditResultInfoItem = { + level: string; + executionFailed: boolean; +}; + +export type ResultIconRenderProps = { + iconLevels?: string[]; + auditResultInfo?: AuditResultInfoItem[]; +}; diff --git a/packages/sqle/src/components/AuditResultMessage/style.ts b/packages/sqle/src/components/AuditResultMessage/style.ts index 5b76010805..f95d9b1780 100644 --- a/packages/sqle/src/components/AuditResultMessage/style.ts +++ b/packages/sqle/src/components/AuditResultMessage/style.ts @@ -1,7 +1,12 @@ import { styled } from '@mui/material/styles'; +import { BasicTag } from '@actiontech/shared'; import { AuditResultMessageProps } from './index.type'; +export const ResultIconTagStyleWrapper = styled(BasicTag)` + width: fit-content; +`; + export const AuditResultMessageStyleWrapper = styled('div')` display: flex; align-items: center; diff --git a/packages/sqle/src/locale/en-US/components.ts b/packages/sqle/src/locale/en-US/components.ts index 0180d8cf1f..56cdbad269 100644 --- a/packages/sqle/src/locale/en-US/components.ts +++ b/packages/sqle/src/locale/en-US/components.ts @@ -2,6 +2,8 @@ export default { auditResultMessage: { auditPassed: 'Audit passed', - ruleDeleted: 'The rule has been deleted' + ruleDeleted: 'The rule has been deleted', + auditing: 'Auditing', + hasException: 'Audit exception occurred' } }; diff --git a/packages/sqle/src/locale/en-US/managementConf.ts b/packages/sqle/src/locale/en-US/managementConf.ts index ba29a61570..042f74225a 100644 --- a/packages/sqle/src/locale/en-US/managementConf.ts +++ b/packages/sqle/src/locale/en-US/managementConf.ts @@ -155,6 +155,10 @@ export default { firstAppearanceTime: 'First appearance time', occurrenceCount: 'Occurrence count', status: 'Status', + auditStatus: 'Audit status', + auditResult: 'Audit result', + auditResultTooltip: 'Shows the latest audit result', + audited: 'Audited', explanation: { text: 'Explanation', operator: 'Add explanation' diff --git a/packages/sqle/src/locale/zh-CN/components.ts b/packages/sqle/src/locale/zh-CN/components.ts index aa52708436..7f593216d0 100644 --- a/packages/sqle/src/locale/zh-CN/components.ts +++ b/packages/sqle/src/locale/zh-CN/components.ts @@ -2,6 +2,8 @@ export default { auditResultMessage: { auditPassed: '审核通过', - ruleDeleted: '该规则已删除' + ruleDeleted: '该规则已删除', + auditing: '审核中', + hasException: '审核存在异常' } }; diff --git a/packages/sqle/src/locale/zh-CN/managementConf.ts b/packages/sqle/src/locale/zh-CN/managementConf.ts index 71b2c965b4..57b44c7646 100644 --- a/packages/sqle/src/locale/zh-CN/managementConf.ts +++ b/packages/sqle/src/locale/zh-CN/managementConf.ts @@ -151,6 +151,10 @@ export default { firstAppearanceTime: '最早一次出现时间', occurrenceCount: '出现次数', status: '状态', + auditStatus: '审核状态', + auditResult: '审核结果', + auditResultTooltip: '展示最新的审核结果', + audited: '已审核', explanation: { text: '说明', operator: '添加说明' diff --git a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/AuditStatusTag.tsx b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/AuditStatusTag.tsx new file mode 100644 index 0000000000..9a2f3e76f1 --- /dev/null +++ b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/AuditStatusTag.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from 'react-i18next'; +import { ResultIconTagStyleWrapper } from '../../../../components/AuditResultMessage/style'; +import { AUDITED, BEING_AUDITED } from './utils'; + +type AuditStatusTagProps = { + status?: string; +}; + +const AuditStatusTag: React.FC = ({ status }) => { + const { t } = useTranslation(); + + if (status === BEING_AUDITED) { + return ( + + {t('components.auditResultMessage.auditing')} + + ); + } + + if (status === AUDITED) { + return ( + + {t('managementConf.detail.scanTypeSqlCollection.column.audited')} + + ); + } + + return <>{status || '-'}; +}; + +export default AuditStatusTag; diff --git a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/__tests__/__snapshots__/index.test.tsx.snap b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/__tests__/__snapshots__/index.test.tsx.snap index 6630a8ffad..ef901f3e91 100644 --- a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/__tests__/__snapshots__/index.test.tsx.snap @@ -645,7 +645,15 @@ exports[`test ScanTypeSqlCollection should match snapshot 2`] = ` class="ant-table-cell" scope="col" > - 审核结果 + 审核状态 + + + + 审核结果 + + +
+   +
+ + + - + @@ -1202,10 +1224,10 @@ LIMIT data-testid="trigger-open-report-drawer" >
- - - - 审核通过 - +
@@ -1407,6 +1424,11 @@ LIMIT + + - + @@ -1414,10 +1436,10 @@ LIMIT data-testid="trigger-open-report-drawer" >
- - - - 审核通过 - +
@@ -1589,6 +1606,11 @@ LIMIT + + - + @@ -1596,10 +1618,10 @@ LIMIT data-testid="trigger-open-report-drawer" >
- - - - 审核通过 - +
@@ -1745,6 +1762,11 @@ LIMIT + + - + @@ -1752,32 +1774,23 @@ LIMIT data-testid="trigger-open-report-drawer" >
- - - - - - - 语法错误或者解析器不支持,请人工确认SQL正确性 - + +
@@ -1906,6 +1919,11 @@ LIMIT + + - + @@ -1913,10 +1931,10 @@ LIMIT data-testid="trigger-open-report-drawer" >
- - - - 审核通过 - +
@@ -2105,6 +2118,11 @@ LIMIT + + - + @@ -2112,10 +2130,10 @@ LIMIT data-testid="trigger-open-report-drawer" >
- - - - 审核通过 - +
@@ -2795,7 +2808,15 @@ exports[`test ScanTypeSqlCollection should open report drawer and set current au class="ant-table-cell" scope="col" > - 审核结果 + 审核状态 + + + + 审核结果 + + +
+   +
+ + + - + @@ -3352,10 +3387,10 @@ LIMIT data-testid="trigger-open-report-drawer" >
- - - - 审核通过 - +
@@ -3557,6 +3587,11 @@ LIMIT + + - + @@ -3564,10 +3599,10 @@ LIMIT data-testid="trigger-open-report-drawer" >
- - - - 审核通过 - +
@@ -3739,6 +3769,11 @@ LIMIT + + - + @@ -3746,10 +3781,10 @@ LIMIT data-testid="trigger-open-report-drawer" >
- - - - 审核通过 - +
@@ -3895,6 +3925,11 @@ LIMIT + + - + @@ -3902,32 +3937,23 @@ LIMIT data-testid="trigger-open-report-drawer" >
- - - - - - - 语法错误或者解析器不支持,请人工确认SQL正确性 - + +
@@ -4056,6 +4082,11 @@ LIMIT + + - + @@ -4063,10 +4094,10 @@ LIMIT data-testid="trigger-open-report-drawer" >
- - - - 审核通过 - +
@@ -4255,6 +4281,11 @@ LIMIT + + - + @@ -4262,10 +4293,10 @@ LIMIT data-testid="trigger-open-report-drawer" >
- - - - 审核通过 - +
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 a7306f3295..6fe5d7a42b 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 @@ -7,6 +7,8 @@ import { mockProjectInfo } from '@actiontech/shared/lib/testUtil/mockHook/data'; import { mockUseCurrentUser } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentUser'; import { getAllBySelector } from '@actiontech/shared/lib/testUtil/customQuery'; import rule_template from '../../../../../testUtils/mockApi/rule_template'; +import { createSpySuccessResponse } from '@actiontech/shared/lib/testUtil/mockApi'; +import { mockAuditPlanSQLData } from '../../../../../testUtils/mockApi/instanceAuditPlan/data'; describe('test ScanTypeSqlCollection', () => { let getInstanceAuditPlanSQLMetaSpy: jest.SpyInstance; @@ -130,4 +132,56 @@ describe('test ScanTypeSqlCollection', () => { await act(async () => jest.advanceTimersByTime(3000)); expect(baseElement).toMatchSnapshot(); }); + + it('should polling request when sql audit status is being_audited', async () => { + getInstanceAuditPlanSQLDataSpy + .mockImplementationOnce(() => { + return createSpySuccessResponse({ + data: { + rows: [ + { + ...mockAuditPlanSQLData?.rows?.[0], + audit_results: '[]', + audit_status: 'being_audited' + } + ] + } + }); + }) + .mockImplementationOnce(() => { + return createSpySuccessResponse({ + data: { + rows: [ + { + ...mockAuditPlanSQLData?.rows?.[0] + } + ] + } + }); + }); + + customRender(); + await act(async () => jest.advanceTimersByTime(3000)); + expect(getInstanceAuditPlanSQLDataSpy).toHaveBeenCalledTimes(1); + await act(async () => jest.advanceTimersByTime(3000)); + expect(getInstanceAuditPlanSQLDataSpy).toHaveBeenCalledTimes(2); + }); + + it('should stop polling request when sql audit status is not being_audited', async () => { + getInstanceAuditPlanSQLDataSpy.mockImplementation(() => { + return createSpySuccessResponse({ + data: { + rows: [ + { + ...mockAuditPlanSQLData?.rows?.[0] + } + ] + } + }); + }); + + customRender(); + await act(async () => jest.advanceTimersByTime(3000)); + expect(getInstanceAuditPlanSQLDataSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/indx.tsx b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/indx.tsx index cf12b713d4..4858255e59 100644 --- a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/indx.tsx +++ b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/indx.tsx @@ -8,7 +8,7 @@ import { } from '@actiontech/shared/lib/components/ActiontechTable'; import { useTranslation } from 'react-i18next'; import ReportDrawer from '../../../../components/ReportDrawer'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState, ReactNode } 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'; @@ -34,14 +34,19 @@ import { getErrorMessage } from '@actiontech/shared/lib/utils/Common'; import ResultIconRender from '../../../../components/AuditResultMessage/ResultIconRender'; -import AuditResultMessage from '../../../../components/AuditResultMessage'; +import AuditStatusTag from './AuditStatusTag'; +import { + BEING_AUDITED, + buildTableHeadWithAuditStatus, + parseAuditResult +} from './utils'; import { IGetInstanceAuditPlanSQLDataV1Params, IGetInstanceAuditPlanSQLExportV1Params } 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 { message, Tooltip } from 'antd'; import { Link } from 'react-router-dom'; const ScanTypeSqlCollection: React.FC = ({ @@ -64,6 +69,8 @@ const ScanTypeSqlCollection: React.FC = ({ const [currentAuditResultRecord, setCurrentAuditResultRecord] = useState(); const [messageApi, messageContextHolder] = message.useMessage(); + const [polling, { setFalse: finishPollRequest, setTrue: startPollRequest }] = + useBoolean(); const { tableChange, @@ -86,10 +93,13 @@ const ScanTypeSqlCollection: React.FC = ({ { setTrue: openReportDrawer, setFalse: closeReportDrawer } ] = useBoolean(false); - const onClickAuditResult = (record: ScanTypeSqlTableDataSourceItem) => { - openReportDrawer(); - setCurrentAuditResultRecord(record); - }; + const onClickAuditResult = useCallback( + (record: ScanTypeSqlTableDataSourceItem) => { + openReportDrawer(); + setCurrentAuditResultRecord(record); + }, + [openReportDrawer] + ); const { data: tableMetas, @@ -165,7 +175,8 @@ const ScanTypeSqlCollection: React.FC = ({ data: tableRows, loading: getTableRowLoading, refresh: refreshTableRows, - error: getTableRowError + error: getTableRowError, + cancel } = useRequest( () => { const params: IGetInstanceAuditPlanSQLDataV1Params = { @@ -186,7 +197,21 @@ const ScanTypeSqlCollection: React.FC = ({ }, { refreshDeps: [pagination, tableFilterInfo, sortInfo], - ready: activeTabKey === auditPlanId + ready: activeTabKey === auditPlanId, + pollingInterval: 1000, + pollingErrorRetryCount: 3, + onSuccess: (res) => { + if (res.data?.some((i) => i?.audit_status === BEING_AUDITED)) { + startPollRequest(); + } else { + cancel(); + finishPollRequest(); + } + }, + onError: () => { + cancel(); + finishPollRequest(); + } } ); @@ -267,6 +292,90 @@ const ScanTypeSqlCollection: React.FC = ({ }; }, [username, auditPlanType]); + const loading = getFilterMetaListLoading || (getTableRowLoading && !polling); + + const tableHead = useMemo( + () => + buildTableHeadWithAuditStatus(tableMetas?.head, { + auditStatus: t( + 'managementConf.detail.scanTypeSqlCollection.column.auditStatus' + ), + auditResult: t( + 'managementConf.detail.scanTypeSqlCollection.column.auditResult' + ) + }), + [tableMetas?.head, t] + ); + + const columns = useMemo(() => { + if (!tableHead.length) { + return []; + } + + const builtColumns = sortableTableColumnFactory(tableHead, { + columnClassName: (type) => + type === 'sql' ? 'ellipsis-column-large-width' : undefined, + customRender: (text, record, fieldName, type) => { + if (fieldName === 'audit_status') { + return ; + } + + if (fieldName === 'audit_results') { + return ( +
onClickAuditResult(record)} + > + +
+ ); + } + + if (!text) { + return '-'; + } + + if (type === 'time') { + return formatTime(text, '-'); + } + + if (type === 'sql') { + return ( + onClickAuditResult(record)} + sql={text} + rows={1} + showCopyIcon + cuttingLength={200} + /> + ); + } + + return text; + } + }); + + return builtColumns.map((column) => { + if (column.dataIndex === 'audit_results') { + return { + ...column, + title: ( + + {column.title as ReactNode} + + ) + }; + } + return column; + }); + }, [onClickAuditResult, sortableTableColumnFactory, t, tableHead]); + return ( @@ -284,67 +393,8 @@ const ScanTypeSqlCollection: React.FC = ({ rowKey="id" setting={tableSetting} errorMessage={getTableRowError && getErrorMessage(getTableRowError)} - loading={getFilterMetaListLoading || getTableRowLoading} - columns={sortableTableColumnFactory(tableMetas?.head ?? [], { - columnClassName: (type) => - type === 'sql' ? 'ellipsis-column-large-width' : undefined, - customRender: (text, record, fieldName, type) => { - if (fieldName === 'audit_results') { - let results: IAuditResult[] = []; - try { - results = JSON.parse(text ?? '[]') as IAuditResult[]; - } catch (error) { - results = []; - } - return ( -
onClickAuditResult(record)} - > - {results?.length > 1 ? ( - { - return item.level ?? ''; - })} - /> - ) : ( - - )} -
- ); - } - - if (!text) { - return '-'; - } - - if (type === 'time') { - return formatTime(text, '-'); - } - - if (type === 'sql') { - return ( - onClickAuditResult(record)} - sql={text} - rows={1} - showCopyIcon - cuttingLength={200} - /> - ); - } - - return text; - } - })} + loading={loading} + columns={columns} dataSource={tableRows?.data} onChange={tableChange} pagination={{ diff --git a/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/utils.ts b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/utils.ts new file mode 100644 index 0000000000..a4e6ef0935 --- /dev/null +++ b/packages/sqle/src/page/SqlManagementConf/Detail/ScanTypeSqlCollection/utils.ts @@ -0,0 +1,91 @@ +import { + IAuditResult, + IAuditPlanSQLHeadV1 +} from '@actiontech/shared/lib/api/sqle/service/common'; +import { AuditResultInfoItem } from '../../../../components/AuditResultMessage/index.type'; + +export const BEING_AUDITED = 'being_audited'; +export const AUDITED = 'audited'; + +const AUDIT_STATUS_FIELD = 'audit_status'; +const AUDIT_RESULTS_FIELD = 'audit_results'; +const PRIORITY_FIELD = 'priority'; + +type AuditResultJsonItem = IAuditResult & { + execution_failed?: boolean; +}; + +export const parseAuditResult = ( + resultString: string +): AuditResultInfoItem[] => { + let results: AuditResultJsonItem[] = []; + try { + const parsed = JSON.parse(resultString ?? '[]') as + | AuditResultJsonItem[] + | null; + results = Array.isArray(parsed) ? parsed : []; + } catch { + results = []; + } + return results.map((item) => ({ + level: item.level ?? '', + executionFailed: !!item.execution_failed + })); +}; + +export type AuditColumnLabels = { + auditStatus: string; + auditResult: string; +}; + +/** + * 在后端返回的 head 中注入 `audit_status` 列,并按以下顺序排列: + * ... → audit_status → priority → audit_results → ... + * 即:保证 `audit_status` 出现在 `priority` 之前;若 `priority` 不存在,则 + * 退化为出现在 `audit_results` 之前。 + * + * 同时统一覆盖 `audit_results` 列标题为「审核结果」,避免后端返回的旧文案。 + */ +export const buildTableHeadWithAuditStatus = ( + head: IAuditPlanSQLHeadV1[] | undefined, + labels: AuditColumnLabels +): IAuditPlanSQLHeadV1[] => { + if (!head?.length) { + return []; + } + + const withoutAuditStatus = head + .filter((item) => item.field_name !== AUDIT_STATUS_FIELD) + .map((item) => { + if (item.field_name === AUDIT_RESULTS_FIELD) { + return { ...item, desc: labels.auditResult }; + } + return item; + }); + + const auditStatusColumn: IAuditPlanSQLHeadV1 = { + field_name: AUDIT_STATUS_FIELD, + desc: labels.auditStatus, + sortable: false + }; + + const priorityIndex = withoutAuditStatus.findIndex( + (item) => item.field_name === PRIORITY_FIELD + ); + const insertIndex = + priorityIndex !== -1 + ? priorityIndex + : withoutAuditStatus.findIndex( + (item) => item.field_name === AUDIT_RESULTS_FIELD + ); + + if (insertIndex === -1) { + return withoutAuditStatus; + } + + return [ + ...withoutAuditStatus.slice(0, insertIndex), + auditStatusColumn, + ...withoutAuditStatus.slice(insertIndex) + ]; +};