From 6af26307dff6778c601e37cc7926ba15a83bbc2e Mon Sep 17 00:00:00 2001 From: lizhensheng Date: Tue, 12 Sep 2023 19:20:31 +0800 Subject: [PATCH 1/2] [feature]: Support fast SQL auditing --- craco.config.js | 2 +- src/api/common.d.ts | 114 +++++- src/api/instance/index.enum.ts | 4 +- src/api/sql_audit_record/index.d.ts | 73 ++++ src/api/sql_audit_record/index.enum.ts | 7 + src/api/sql_audit_record/index.ts | 144 +++++++ src/hooks/useSQLAuditRecordTag/index.tsx | 51 +++ src/hooks/useStaticStatus/index.data.ts | 9 + src/hooks/useStaticStatus/index.tsx | 30 ++ src/locale/zh-CN/common.ts | 1 + src/locale/zh-CN/index.ts | 2 + src/locale/zh-CN/menu.ts | 1 + src/locale/zh-CN/sqlAudit.ts | 88 +++++ .../AuditResult/AuditResultFilterForm.tsx | 25 +- .../AuditResultCollection.test.tsx.snap | 24 +- .../__snapshots__/index.test.tsx.snap | 16 +- src/page/Order/AuditResult/column.tsx | 67 ++++ src/page/Order/AuditResult/index.tsx | 125 +++--- src/page/Order/AuditResult/index.type.ts | 2 + .../SqlAuditRecord/Create/BaseInfoForm.tsx | 141 +++++++ .../SqlAuditRecord/Create/SQLInfoForm.tsx | 372 ++++++++++++++++++ src/page/SqlAuditRecord/Create/index.tsx | 131 ++++++ src/page/SqlAuditRecord/Create/index.type.ts | 44 +++ src/page/SqlAuditRecord/Detail/index.tsx | 138 +++++++ src/page/SqlAuditRecord/List/CustomTags.tsx | 156 ++++++++ src/page/SqlAuditRecord/List/FilterForm.tsx | 115 ++++++ src/page/SqlAuditRecord/List/column.tsx | 109 +++++ src/page/SqlAuditRecord/List/index.less | 19 + src/page/SqlAuditRecord/List/index.tsx | 137 +++++++ src/page/SqlAuditRecord/List/index.type.ts | 22 ++ src/router/config.tsx | 41 ++ src/scripts/version.ts | 2 +- src/types/router.type.ts | 6 +- 33 files changed, 2135 insertions(+), 83 deletions(-) create mode 100644 src/api/sql_audit_record/index.d.ts create mode 100644 src/api/sql_audit_record/index.enum.ts create mode 100644 src/api/sql_audit_record/index.ts create mode 100644 src/hooks/useSQLAuditRecordTag/index.tsx create mode 100644 src/locale/zh-CN/sqlAudit.ts create mode 100644 src/page/SqlAuditRecord/Create/BaseInfoForm.tsx create mode 100644 src/page/SqlAuditRecord/Create/SQLInfoForm.tsx create mode 100644 src/page/SqlAuditRecord/Create/index.tsx create mode 100644 src/page/SqlAuditRecord/Create/index.type.ts create mode 100644 src/page/SqlAuditRecord/Detail/index.tsx create mode 100644 src/page/SqlAuditRecord/List/CustomTags.tsx create mode 100644 src/page/SqlAuditRecord/List/FilterForm.tsx create mode 100644 src/page/SqlAuditRecord/List/column.tsx create mode 100644 src/page/SqlAuditRecord/List/index.less create mode 100644 src/page/SqlAuditRecord/List/index.tsx create mode 100644 src/page/SqlAuditRecord/List/index.type.ts diff --git a/craco.config.js b/craco.config.js index 878d3c18..abf52cc2 100644 --- a/craco.config.js +++ b/craco.config.js @@ -101,7 +101,7 @@ module.exports = { const res = {}; for (let i = 0; i < 10; i++) { res[`/v${i}`] = { - target: 'http://10.186.60.56:10001', + target: 'http://124.70.158.246:8889', secure: false, changeOrigin: true, ws: true, diff --git a/src/api/common.d.ts b/src/api/common.d.ts index b828ddc6..d49f3252 100644 --- a/src/api/common.d.ts +++ b/src/api/common.d.ts @@ -453,6 +453,14 @@ export interface ICreateRuleTemplateReqV1 { rule_template_name?: string; } +export interface ICreateSQLAuditRecordResV1 { + code?: number; + + data?: ISQLAuditRecordResData; + + message?: string; +} + export interface ICreateSyncInstanceTaskReqV1 { db_type: string; @@ -1229,6 +1237,32 @@ export interface IGetSQLAnalysisDataResItemV1 { table_metas?: ITableMeta[]; } +export interface IGetSQLAuditRecordResV1 { + code?: number; + + data?: ISQLAuditRecord; + + message?: string; +} + +export interface IGetSQLAuditRecordTagTipsResV1 { + code?: number; + + data?: string[]; + + message?: string; +} + +export interface IGetSQLAuditRecordsResV1 { + code?: number; + + data?: ISQLAuditRecord[]; + + message?: string; + + total_nums?: number; +} + export interface IGetSQLEInfoResDataV1 { logo_url?: string; @@ -1275,6 +1309,16 @@ export interface IGetSqlExecutionFailPercentResV1 { message?: string; } +export interface IGetSqlManageListResp { + code?: number; + + data?: ISqlManage[]; + + message?: string; + + total_nums?: number; +} + export interface IGetSyncInstanceTaskListResV1 { code?: number; @@ -2091,6 +2135,32 @@ export interface ISMTPConfigurationResV1 { smtp_username?: string; } +export interface ISQLAuditRecord { + creator?: string; + + instance?: ISQLAuditRecordInstance; + + sql_audit_record_id?: number; + + sql_audit_status?: string; + + tags?: string[]; + + task?: IAuditTaskResV1; +} + +export interface ISQLAuditRecordInstance { + db_host?: string; + + db_port?: string; +} + +export interface ISQLAuditRecordResData { + sql_audit_record_id?: string; + + task?: IAuditTaskResV1; +} + export interface ISQLExplain { classic_result?: IExplainClassicResult; @@ -2141,6 +2211,38 @@ export interface ISqlExecutionFailPercent { percent?: number; } +export interface ISqlManage { + appear_num?: number; + + assignee?: string; + + audit_result?: string; + + first_appear_time?: string; + + id?: number; + + instance?: string; + + last_appear_time?: string; + + remark?: string; + + source?: string; + + sql?: string; + + sql_fingerprint?: string; + + sql_manage_bad_num?: number; + + sql_manage_optimized_num?: number; + + sql_manage_total_num?: number; + + status?: string; +} + export interface IStatisticAuditPlanResV1 { code?: number; @@ -2424,11 +2526,11 @@ export interface IUpdateDingTalkConfigurationReqV1 { } export interface IUpdateFeishuConfigurationReqV1 { - app_id?: string; + app_id: string; - app_secret?: string; + app_secret: string; - is_feishu_notification_enabled?: boolean; + is_feishu_notification_enabled: boolean; } export interface IUpdateInstanceReqV1 { @@ -2507,6 +2609,12 @@ export interface IUpdateSMTPConfigurationReqV1 { smtp_username?: string; } +export interface IUpdateSQLAuditRecordReqV1 { + sql_audit_record_id?: string; + + tags?: string[]; +} + export interface IUpdateSyncInstanceTaskReqV1 { global_rule_template?: string; diff --git a/src/api/instance/index.enum.ts b/src/api/instance/index.enum.ts index 169cf66b..25542130 100644 --- a/src/api/instance/index.enum.ts +++ b/src/api/instance/index.enum.ts @@ -3,5 +3,7 @@ export enum getInstanceTipListV1FunctionalModuleEnum { 'create_audit_plan' = 'create_audit_plan', - 'create_workflow' = 'create_workflow' + 'create_workflow' = 'create_workflow', + + 'sql_manage' = 'sql_manage' } diff --git a/src/api/sql_audit_record/index.d.ts b/src/api/sql_audit_record/index.d.ts new file mode 100644 index 00000000..e613f3da --- /dev/null +++ b/src/api/sql_audit_record/index.d.ts @@ -0,0 +1,73 @@ +import { getSQLAuditRecordsV1FilterSqlAuditStatusEnum } from './index.enum'; + +import { + IGetSQLAuditRecordsResV1, + ICreateSQLAuditRecordResV1, + IGetSQLAuditRecordTagTipsResV1, + IGetSQLAuditRecordResV1, + IUpdateSQLAuditRecordReqV1, + IBaseRes +} from '../common.d'; + +export interface IGetSQLAuditRecordsV1Params { + fuzzy_search_tags?: string; + + filter_sql_audit_status?: getSQLAuditRecordsV1FilterSqlAuditStatusEnum; + + filter_instance_name?: string; + + filter_create_time_from?: string; + + filter_create_time_to?: string; + + page_index: number; + + page_size: number; + + project_name: string; +} + +export interface IGetSQLAuditRecordsV1Return extends IGetSQLAuditRecordsResV1 {} + +export interface ICreateSQLAuditRecordV1Params { + project_name: string; + + instance_name?: string; + + instance_schema?: string; + + db_type?: string; + + sql?: string; + + input_sql_file?: any; + + input_mybatis_xml_file?: any; + + input_zip_file?: any; +} + +export interface ICreateSQLAuditRecordV1Return + extends ICreateSQLAuditRecordResV1 {} + +export interface IGetSQLAuditRecordTagTipsV1Params { + project_name: string; +} + +export interface IGetSQLAuditRecordTagTipsV1Return + extends IGetSQLAuditRecordTagTipsResV1 {} + +export interface IGetSQLAuditRecordV1Params { + project_name: string; + + sql_audit_record_id: string; +} + +export interface IGetSQLAuditRecordV1Return extends IGetSQLAuditRecordResV1 {} + +export interface IUpdateSQLAuditRecordV1Params + extends IUpdateSQLAuditRecordReqV1 { + project_name: string; +} + +export interface IUpdateSQLAuditRecordV1Return extends IBaseRes {} diff --git a/src/api/sql_audit_record/index.enum.ts b/src/api/sql_audit_record/index.enum.ts new file mode 100644 index 00000000..814792f5 --- /dev/null +++ b/src/api/sql_audit_record/index.enum.ts @@ -0,0 +1,7 @@ +/* tslint:disable no-duplicate-string */ + +export enum getSQLAuditRecordsV1FilterSqlAuditStatusEnum { + 'auditing' = 'auditing', + + 'successfully' = 'successfully' +} diff --git a/src/api/sql_audit_record/index.ts b/src/api/sql_audit_record/index.ts new file mode 100644 index 00000000..6bfd8a79 --- /dev/null +++ b/src/api/sql_audit_record/index.ts @@ -0,0 +1,144 @@ +/* tslint:disable no-identical-functions */ +/* tslint:disable no-useless-cast */ +/* tslint:disable no-unnecessary-type-assertion */ +/* tslint:disable no-big-function */ +/* tslint:disable no-duplicate-string */ +import ServiceBase from '../Service.base'; +import { AxiosRequestConfig } from 'axios'; + +import { + IGetSQLAuditRecordsV1Params, + IGetSQLAuditRecordsV1Return, + ICreateSQLAuditRecordV1Params, + ICreateSQLAuditRecordV1Return, + IGetSQLAuditRecordTagTipsV1Params, + IGetSQLAuditRecordTagTipsV1Return, + IGetSQLAuditRecordV1Params, + IGetSQLAuditRecordV1Return, + IUpdateSQLAuditRecordV1Params, + IUpdateSQLAuditRecordV1Return +} from './index.d'; + +class SqlAuditRecordService extends ServiceBase { + public getSQLAuditRecordsV1( + params: IGetSQLAuditRecordsV1Params, + 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_audit_record`, + paramsData, + options + ); + } + + public CreateSQLAuditRecordV1( + params: ICreateSQLAuditRecordV1Params, + options?: AxiosRequestConfig + ) { + const config = options || {}; + const headers = config.headers ? config.headers : {}; + config.headers = { + ...headers, + + 'Content-Type': 'multipart/form-data' + }; + + const paramsData = new FormData(); + + if (params.instance_name != undefined) { + paramsData.append('instance_name', params.instance_name as any); + } + + if (params.instance_schema != undefined) { + paramsData.append('instance_schema', params.instance_schema as any); + } + + if (params.db_type != undefined) { + paramsData.append('db_type', params.db_type as any); + } + + if (params.sql != undefined) { + paramsData.append('sql', params.sql as any); + } + + if (params.input_sql_file != undefined) { + paramsData.append('input_sql_file', params.input_sql_file as any); + } + + if (params.input_mybatis_xml_file != undefined) { + paramsData.append( + 'input_mybatis_xml_file', + params.input_mybatis_xml_file as any + ); + } + + if (params.input_zip_file != undefined) { + paramsData.append('input_zip_file', params.input_zip_file as any); + } + + const project_name = params.project_name; + + return this.post( + `/v1/projects/${project_name}/sql_audit_record`, + paramsData, + config + ); + } + + public GetSQLAuditRecordTagTipsV1( + params: IGetSQLAuditRecordTagTipsV1Params, + 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_audit_record/tag_tips`, + paramsData, + options + ); + } + + public getSQLAuditRecordV1( + params: IGetSQLAuditRecordV1Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + const project_name = paramsData.project_name; + delete paramsData.project_name; + + const sql_audit_record_id = paramsData.sql_audit_record_id; + delete paramsData.sql_audit_record_id; + + return this.get( + `/v1/projects/${project_name}/sql_audit_record/${sql_audit_record_id}`, + paramsData, + options + ); + } + + public updateSQLAuditRecordV1( + params: IUpdateSQLAuditRecordV1Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + const project_name = paramsData.project_name; + delete paramsData.project_name; + + const sql_audit_record_id = paramsData.sql_audit_record_id; + delete paramsData.sql_audit_record_id; + + return this.patch( + `/v1/projects/${project_name}/sql_audit_record/${sql_audit_record_id}`, + paramsData, + options + ); + } +} + +export default new SqlAuditRecordService(); diff --git a/src/hooks/useSQLAuditRecordTag/index.tsx b/src/hooks/useSQLAuditRecordTag/index.tsx new file mode 100644 index 00000000..4d9def27 --- /dev/null +++ b/src/hooks/useSQLAuditRecordTag/index.tsx @@ -0,0 +1,51 @@ +import { useCallback, useState } from 'react'; +import { useBoolean } from 'ahooks'; +import { ResponseCode } from '../../data/common'; +import { Select, Tag } from 'antd'; +import sql_audit_record from '../../api/sql_audit_record'; + +const useSQLAuditRecordTag = () => { + const [auditRecordTags, setSQLAuditRecordTags] = useState([]); + const [loading, { setTrue, setFalse }] = useBoolean(); + + const updateSQLAuditRecordTag = useCallback( + (projectName: string) => { + setTrue(); + sql_audit_record + .GetSQLAuditRecordTagTipsV1({ project_name: projectName }) + .then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + setSQLAuditRecordTags(res.data?.data ?? []); + } else { + setSQLAuditRecordTags([]); + } + }) + .catch(() => { + setSQLAuditRecordTags([]); + }) + .finally(() => { + setFalse(); + }); + }, + [setFalse, setTrue] + ); + + const generateSQLAuditRecordSelectOptions = useCallback(() => { + return auditRecordTags.map((v) => { + return ( + + {v} + + ); + }); + }, [auditRecordTags]); + + return { + auditRecordTags, + loading, + updateSQLAuditRecordTag, + generateSQLAuditRecordSelectOptions, + }; +}; + +export default useSQLAuditRecordTag; diff --git a/src/hooks/useStaticStatus/index.data.ts b/src/hooks/useStaticStatus/index.data.ts index 64bedcf0..6ddf2d4b 100644 --- a/src/hooks/useStaticStatus/index.data.ts +++ b/src/hooks/useStaticStatus/index.data.ts @@ -9,6 +9,7 @@ import { WorkflowTemplateDetailResV1AllowSubmitWhenLessAuditLevelEnum, RuleResV1LevelEnum, } from '../../api/common.enum'; +import { getSQLAuditRecordsV1FilterSqlAuditStatusEnum } from '../../api/sql_audit_record/index.enum'; export const execStatusDictionary: StaticEnumDictionary = { @@ -80,3 +81,11 @@ export const auditLevelDictionary: StaticEnumDictionary = + { + [getSQLAuditRecordsV1FilterSqlAuditStatusEnum.auditing]: + 'sqlAudit.list.auditing', + [getSQLAuditRecordsV1FilterSqlAuditStatusEnum.successfully]: + 'sqlAudit.list.successfully', + }; diff --git a/src/hooks/useStaticStatus/index.tsx b/src/hooks/useStaticStatus/index.tsx index 9976f8a2..53124086 100644 --- a/src/hooks/useStaticStatus/index.tsx +++ b/src/hooks/useStaticStatus/index.tsx @@ -16,7 +16,9 @@ import { orderStatusDictionary, ruleLevelDictionary, auditLevelDictionary, + auditResultRecordFilterStatusEnum, } from './index.data'; +import { getSQLAuditRecordsV1FilterSqlAuditStatusEnum } from '../../api/sql_audit_record/index.enum'; const useStaticStatus = () => { const { t } = useTranslation(); @@ -302,6 +304,33 @@ const useStaticStatus = () => { ); }, [t]); + const getSQLAuditRecordStatusSelectOption = React.useCallback(() => { + return ( + <> + + {t( + auditResultRecordFilterStatusEnum[ + getSQLAuditRecordsV1FilterSqlAuditStatusEnum.auditing + ] + )} + + + {t( + auditResultRecordFilterStatusEnum[ + getSQLAuditRecordsV1FilterSqlAuditStatusEnum.successfully + ] + )} + + + ); + }, [t]); + return { generateAuditStatusSelectOption, generateExecStatusSelectOption, @@ -309,6 +338,7 @@ const useStaticStatus = () => { // generateSqlTaskStatusSelectOption, getRuleLevelStatusSelectOption, getAuditLevelStatusSelectOption, + getSQLAuditRecordStatusSelectOption, }; }; diff --git a/src/locale/zh-CN/common.ts b/src/locale/zh-CN/common.ts index 0b889d9a..1ad821bb 100644 --- a/src/locale/zh-CN/common.ts +++ b/src/locale/zh-CN/common.ts @@ -7,6 +7,7 @@ export default { unknownStatus: '未知状态...', copied: '复制成功', + download: '下载', true: '是', false: '否', diff --git a/src/locale/zh-CN/index.ts b/src/locale/zh-CN/index.ts index 1b8103d0..61b8a990 100644 --- a/src/locale/zh-CN/index.ts +++ b/src/locale/zh-CN/index.ts @@ -24,6 +24,7 @@ import syncDataSource from './syncDataSource'; import operationRecord from './operationRecord'; import customRule from './customRule'; import ruleManager from './ruleManager'; +import sqlAudit from './sqlAudit'; // eslint-disable-next-line import/no-anonymous-default-export export default { @@ -54,5 +55,6 @@ export default { operationRecord, customRule, ruleManager, + sqlAudit, }, }; diff --git a/src/locale/zh-CN/menu.ts b/src/locale/zh-CN/menu.ts index 8507ea39..12c9f647 100644 --- a/src/locale/zh-CN/menu.ts +++ b/src/locale/zh-CN/menu.ts @@ -29,4 +29,5 @@ export default { allInstanceType: '所有数据库类型', syncDataSource: '外部数据源同步', operationRecord: '操作记录', + sqlAudit: 'SQL审核', }; diff --git a/src/locale/zh-CN/sqlAudit.ts b/src/locale/zh-CN/sqlAudit.ts new file mode 100644 index 00000000..68f74c90 --- /dev/null +++ b/src/locale/zh-CN/sqlAudit.ts @@ -0,0 +1,88 @@ +/* eslint-disable import/no-anonymous-default-export */ +export default { + list: { + title: '审核列表', + pageDesc: '审核列表展示您创建的SQL审核记录', + createButtonText: '创建审核', + auditing: '审核中', + successfully: '审核成功', + table: { + title: '所有与我相关的审核', + filterForm: { + instanceName: '数据源', + auditStatus: '审核状态', + businessTag: '业务标签', + auditTime: '审核时间', + }, + updateTagsSuccess: '更新业务标签成功', + columns: { + auditID: '审核ID', + auditStatus: '审核状态', + businessTag: '业务标签', + auditRating: '审核评分', + auditPassRate: '审核通过率(%)', + createUser: '创建人', + auditTime: '审核时间', + instanceName: '数据源', + }, + }, + }, + create: { + title: 'SQL审核', + pageDesc: '您可以在这里获得快速审核SQL', + + baseInfo: { + title: '基本信息', + businessTag: '业务标签', + addTag: '新增业务标签', + addExtraTagPlaceholder: '请输入需要新增的业务标签', + notTags: '暂无标签数据', + }, + + SQLInfo: { + title: 'SQL语句', + auditType: '审核方式', + dbType: '数据库类型', + instanceName: '数据源', + instanceSchema: '数据库', + staticAudit: '静态审核', + dynamicAudit: '动态审核', + uploadType: '选择SQL语句上传方式', + uploadTypeEnum: { + sql: '输入SQL语句', + sqlFile: '上传SQL文件', + xmlFile: '上传Mybatis的XML文件', + zipFile: '上传ZIP文件', + gitRepository: '配置git仓库', + }, + uploadLabelEnum: { + sql: 'SQL语句', + sqlFile: 'SQL文件', + xmlFile: 'Mybatis的XML文件', + zipFile: 'ZIP文件', + }, + auditButton: '审核', + formatterSQL: 'SQL美化', + successTips: '创建审核成功', + }, + + result: { + table: { + number: '序号', + auditLevel: '规则等级', + auditStatus: '审核状态', + auditResult: '审核结果', + auditSql: '审核语句', + describe: '说明', + analyze: '分析', + }, + }, + }, + detail: { + title: 'SQL审核', + pageDesc: '您可以在这里查看审核结果', + auditID: '审核ID', + auditRating: '审核评分', + auditPassRate: '审核通过率', + }, +}; diff --git a/src/page/Order/AuditResult/AuditResultFilterForm.tsx b/src/page/Order/AuditResult/AuditResultFilterForm.tsx index 5c16d5a5..b5291ecf 100644 --- a/src/page/Order/AuditResult/AuditResultFilterForm.tsx +++ b/src/page/Order/AuditResult/AuditResultFilterForm.tsx @@ -3,28 +3,35 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { filterFormButtonLayoutFactory, - FilterFormColLayout, FilterFormLayout, FilterFormRowLayout, } from '../../../data/common'; import useStaticStatus from '../../../hooks/useStaticStatus'; import { FilterFormProps, OrderAuditResultFilterFields } from './index.type'; +const FilterFormColLayout = { + xs: 24, + sm: 12, + xl: 12, + xxl: 8, +}; + const AuditResultFilterForm: React.FC = (props) => { const { t } = useTranslation(); - const { - generateExecStatusSelectOption, - getAuditLevelStatusSelectOption, - } = useStaticStatus(); + const { generateExecStatusSelectOption, getAuditLevelStatusSelectOption } = + useStaticStatus(); return ( {...FilterFormLayout} form={props.form}> - + ( + <> + {menu} + + + setExtraTag(e.target.value)} + /> + + + + )} + > + {[...auditRecordTags, ...extraTags].map((v) => ( + + {v} + + ))} + + + + ); +}; + +export default forwardRef(BaseInfoForm); diff --git a/src/page/SqlAuditRecord/Create/SQLInfoForm.tsx b/src/page/SqlAuditRecord/Create/SQLInfoForm.tsx new file mode 100644 index 00000000..062eb910 --- /dev/null +++ b/src/page/SqlAuditRecord/Create/SQLInfoForm.tsx @@ -0,0 +1,372 @@ +import { + Button, + Col, + Form, + FormItemProps, + Radio, + RadioGroupProps, + Row, + Select, + Space, + Tooltip, + Upload, +} from 'antd'; +import { PageFormLayout } from '../../../data/common'; +import { + AuditTypeEnum, + SQLInfoFormFields, + SQLInfoFormProps, + UploadTypeEnum, +} from './index.type'; +import { useTranslation } from 'react-i18next'; +import MonacoEditor, { MonacoEditorProps } from 'react-monaco-editor'; +import { ComponentType, useEffect } from 'react'; +import useStyles from '../../../theme'; +import useChangeTheme from '../../../hooks/useChangeTheme'; +import useMonacoEditor from '../../../hooks/useMonacoEditor'; +import { getFileFromUploadChangeEvent } from '../../../utils/Common'; +import { useBoolean } from 'ahooks'; +import { + FormatLanguageSupport, + formatterSQL, +} from '../../../utils/FormatterSQL'; +import useInstance from '../../../hooks/useInstance'; +import useDatabaseType from '../../../hooks/useDatabaseType'; +import useInstanceSchema from '../../../hooks/useInstanceSchema'; +import { InfoCircleOutlined } from '@ant-design/icons'; + +const MonacoEditorFunComponent = + MonacoEditor as ComponentType; + +const SQLInfoForm: React.FC = ({ + form, + submit, + projectName, +}) => { + const { t } = useTranslation(); + const theme = useStyles(); + const { currentEditorTheme } = useChangeTheme(); + const { generateInstanceSelectOption, updateInstanceList, instanceList } = + useInstance(); + const { updateDriverNameList, generateDriverSelectOptions } = + useDatabaseType(); + + const { editorDidMount } = useMonacoEditor(form, { + formName: 'sql', + }); + + const uploadType = Form.useWatch('uploadType', form); + const auditType = Form.useWatch('auditType', form); + const instanceName = Form.useWatch('instanceName', form); + + const { updateSchemaList, generateInstanceSchemaSelectOption } = + useInstanceSchema(projectName, instanceName); + + const removeFile = ( + fileName: keyof Pick< + SQLInfoFormFields, + 'sqlFile' | 'mybatisFile' | 'zipFile' + > + ) => { + form.setFieldsValue({ + [fileName]: [], + }); + }; + + const genUploadItem = (type: UploadTypeEnum): FormItemProps => { + const uploadCommonProps: FormItemProps = { + valuePropName: 'fileList', + getValueFromEvent: getFileFromUploadChangeEvent, + rules: [ + { + required: true, + }, + ], + }; + if (type === UploadTypeEnum.sql) { + return { + name: 'sql', + label: t('sqlAudit.create.SQLInfo.uploadLabelEnum.sql'), + initialValue: '/* input your sql */', + wrapperCol: { + ...PageFormLayout.wrapperCol, + className: theme.editor, + }, + rules: [ + { + required: true, + }, + ], + children: ( + + ), + }; + } else if (type === UploadTypeEnum.sqlFile) { + return { + name: 'sqlFile', + label: t('sqlAudit.create.SQLInfo.uploadLabelEnum.sqlFile'), + children: ( + false} + onRemove={removeFile.bind(null, 'sqlFile')} + > + + + ), + ...uploadCommonProps, + }; + } else if (type === UploadTypeEnum.xmlFile) { + return { + name: 'mybatisFile', + label: t('sqlAudit.create.SQLInfo.uploadLabelEnum.xmlFile'), + children: ( + false} + onRemove={removeFile.bind(null, 'mybatisFile')} + > + + + ), + ...uploadCommonProps, + }; + } else if (type === UploadTypeEnum.zipFile) { + return { + name: 'zipFile', + label: t('sqlAudit.create.SQLInfo.uploadLabelEnum.zipFile'), + children: ( + false} + onRemove={removeFile.bind(null, 'zipFile')} + > + + + ), + ...uploadCommonProps, + }; + } else { + return {}; + } + }; + const uploadTypeChange: RadioGroupProps['onChange'] = () => { + form.resetFields([ + 'sql', + 'sqlFile', + 'mybatisFile', + 'zipFile', + 'gitRepository', + ]); + }; + + const auditTypeChange: RadioGroupProps['onChange'] = () => { + form.setFieldsValue({ + instanceName: undefined, + }); + }; + + const [submitLoading, { setTrue: startSubmit, setFalse: submitFinish }] = + useBoolean(); + + const internalSubmit = async () => { + const values = await form.validateFields(); + startSubmit(); + + submit(values).finally(() => { + submitFinish(); + }); + }; + + const formatter = async () => { + const values = form.getFieldsValue(); + const dbType = + auditType === AuditTypeEnum.dynamic + ? instanceList.find((v) => v.instance_name === values.instanceName) + ?.instance_type + : values.dbType; + + const sql = formatterSQL(values.sql, dbType); + form.setFieldsValue({ + sql, + }); + }; + + const handleInstanceNameChange = () => { + form.setFieldsValue({ instanceSchema: undefined }); + }; + + useEffect(() => { + if (auditType === AuditTypeEnum.dynamic) { + updateInstanceList({ project_name: projectName }); + updateSchemaList(); + } else if (auditType === AuditTypeEnum.static) { + updateDriverNameList(); + } + }, [ + auditType, + projectName, + updateDriverNameList, + updateInstanceList, + updateSchemaList, + ]); + + return ( + form={form} {...PageFormLayout} scrollToFirstError> + + + + {t('sqlAudit.create.SQLInfo.staticAudit')} + + + {t('sqlAudit.create.SQLInfo.dynamicAudit')} + + + + + + + + + + + + {t('sqlAudit.create.SQLInfo.uploadTypeEnum.sql')} + + + {t('sqlAudit.create.SQLInfo.uploadTypeEnum.sqlFile')} + + + {t('sqlAudit.create.SQLInfo.uploadTypeEnum.xmlFile')} + + + {t('sqlAudit.create.SQLInfo.uploadTypeEnum.zipFile')} + + + + + + + + + + + + + + + ); +}; + +export default SQLInfoForm; diff --git a/src/page/SqlAuditRecord/Create/index.tsx b/src/page/SqlAuditRecord/Create/index.tsx new file mode 100644 index 00000000..3bc7cc6d --- /dev/null +++ b/src/page/SqlAuditRecord/Create/index.tsx @@ -0,0 +1,131 @@ +import { Theme } from '@mui/material/styles'; +import { useTheme } from '@mui/styles'; +import { Button, Card, PageHeader, Space, message } from 'antd'; +import { useTranslation } from 'react-i18next'; +import BaseInfoForm from './BaseInfoForm'; +import { useForm } from 'antd/lib/form/Form'; +import { + BaseInfoFormFields, + BaseInfoFormRef, + SQLInfoFormFields, + SQLInfoFormProps, +} from './index.type'; +import SQLInfoForm from './SQLInfoForm'; +import { useCurrentProjectName } from '../../ProjectManage/ProjectDetail'; +import AuditResult from '../../Order/AuditResult'; +import sql_audit_record from '../../../api/sql_audit_record'; +import { ICreateSQLAuditRecordV1Params } from '../../../api/sql_audit_record/index.d'; +import { useRef, useState } from 'react'; +import { IAuditTaskResV1, ISQLAuditRecordResData } from '../../../api/common'; +import { ResponseCode } from '../../../data/common'; +import EmptyBox from '../../../components/EmptyBox'; +import FooterButtonWrapper from '../../../components/FooterButtonWrapper'; +import { Link } from '../../../components/Link'; + +const SQLAuditCreate: React.FC = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const [baseForm] = useForm(); + const [sqlInfoForm] = useForm(); + const { projectName } = useCurrentProjectName(); + const [task, setTask] = useState(); + const baseRef = useRef(null); + + const auditSQL: SQLInfoFormProps['submit'] = async (values) => { + const baseValues = await baseForm.validateFields(); + const params: ICreateSQLAuditRecordV1Params = { + project_name: projectName, + sql: values.sql, + input_sql_file: values.sqlFile, + input_mybatis_xml_file: values.mybatisFile, + input_zip_file: values.zipFile, + instance_name: values.instanceName, + instance_schema: values.instanceSchema, + db_type: values.dbType, + }; + + sql_audit_record.CreateSQLAuditRecordV1(params).then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + res.data.data && updateTags(res.data.data, baseValues); + } + }); + }; + + const updateTags = async ( + record: ISQLAuditRecordResData, + values: BaseInfoFormFields + ) => { + sql_audit_record + .updateSQLAuditRecordV1({ + tags: values.tags, + sql_audit_record_id: `${record.task?.task_id}`, + project_name: projectName, + }) + .then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + setTask(record.task); + message.success(t('sqlAudit.create.SQLInfo.successTips')); + } + }); + }; + + const resetForm = () => { + baseRef.current?.reset(); + baseForm.resetFields(); + sqlInfoForm.resetFields(); + }; + + return ( + <> + + {t('sqlAudit.create.pageDesc')} + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + ); +}; + +export default SQLAuditCreate; diff --git a/src/page/SqlAuditRecord/Create/index.type.ts b/src/page/SqlAuditRecord/Create/index.type.ts new file mode 100644 index 00000000..33a4b5aa --- /dev/null +++ b/src/page/SqlAuditRecord/Create/index.type.ts @@ -0,0 +1,44 @@ +import { FormInstance } from 'antd'; + +export type BaseInfoFormFields = { + tags: string[]; +}; + +export type BaseInfoFormProps = { + form: FormInstance; + projectName: string; +}; + +export type BaseInfoFormRef = { + reset: () => void; +}; + +export type SQLInfoFormFields = { + auditType: AuditTypeEnum; + uploadType: UploadTypeEnum; + sql: string; + sqlFile: File[]; + mybatisFile: File[]; + zipFile: File[]; + instanceName: string; + instanceSchema: string; + dbType: string; +}; + +export type SQLInfoFormProps = { + form: FormInstance; + submit: (values: SQLInfoFormFields) => Promise; + projectName: string; +}; + +export enum AuditTypeEnum { + static, + dynamic, +} + +export enum UploadTypeEnum { + sql, + sqlFile, + xmlFile, + zipFile, +} diff --git a/src/page/SqlAuditRecord/Detail/index.tsx b/src/page/SqlAuditRecord/Detail/index.tsx new file mode 100644 index 00000000..8936043e --- /dev/null +++ b/src/page/SqlAuditRecord/Detail/index.tsx @@ -0,0 +1,138 @@ +import { + Button, + Card, + Col, + PageHeader, + Row, + Space, + Tag, + Typography, +} from 'antd'; +import { useTranslation } from 'react-i18next'; +import { Theme } from '@mui/material/styles'; +import { useTheme } from '@mui/styles'; +import { useCurrentProjectName } from '../../ProjectManage/ProjectDetail'; +import AuditResult from '../../Order/AuditResult'; +import { useParams } from 'react-router-dom'; +import task from '../../../api/task'; +import { Link } from '../../../components/Link'; +import { useRequest } from 'ahooks'; +import sql_audit_record from '../../../api/sql_audit_record'; +import { floatRound, floatToPercent } from '../../../utils/Math'; + +const SQLAuditDetail: React.FC = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const { projectName } = useCurrentProjectName(); + const { id } = useParams<{ id: string }>(); + + const downloadReport = () => { + task.downloadAuditTaskSQLReportV1({ + task_id: `${auditRecord?.task?.task_id}`, + no_duplicate: false, + }); + }; + + const { data: auditRecord } = useRequest(() => + sql_audit_record + .getSQLAuditRecordV1({ + project_name: projectName, + sql_audit_record_id: id ?? '', + }) + .then((res) => res.data.data) + ); + + return ( + <> + + {t('sqlAudit.detail.pageDesc')} + + +
+ + +
+ + + + + {t('sqlAudit.detail.auditID')} + + + + + + + {auditRecord?.sql_audit_record_id} + + {auditRecord?.tags?.map( + (v) => + v && ( + + {v} + + ) + )} + + + + + + + {t('sqlAudit.detail.auditPassRate')} + + + + + {floatToPercent(auditRecord?.task?.pass_rate ?? 0)}% + + + + + + {t('sqlAudit.detail.auditRating')} + + + + {floatRound(auditRecord?.task?.score ?? 0)} + + + + + + + + + + + +
+
+ + + +
+
+ + ); +}; + +export default SQLAuditDetail; diff --git a/src/page/SqlAuditRecord/List/CustomTags.tsx b/src/page/SqlAuditRecord/List/CustomTags.tsx new file mode 100644 index 00000000..41518876 --- /dev/null +++ b/src/page/SqlAuditRecord/List/CustomTags.tsx @@ -0,0 +1,156 @@ +import { + Button, + Divider, + Empty, + Input, + InputRef, + Popover, + Space, + Spin, + Tag, + Typography, +} from 'antd'; +import { CustomTagsProps } from './index.type'; +import { PlusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { useMemo, useRef, useState } from 'react'; +import useSQLAuditRecordTag from '../../../hooks/useSQLAuditRecordTag'; +import { useTranslation } from 'react-i18next'; +import useStyles from '../../../theme'; +import EmptyBox from '../../../components/EmptyBox'; + +const CustomTags: React.FC = ({ + tags, + updateTags, + projectName, +}) => { + const styles = useStyles(); + const { t } = useTranslation(); + const removing = useRef(false); + const removeTag = async (tag: string) => { + if (removing.current) { + return; + } + + updateTags(tags.filter((v) => v !== tag)).finally(() => { + removing.current = false; + }); + }; + + const inputRef = useRef(null); + const { loading, updateSQLAuditRecordTag, auditRecordTags } = + useSQLAuditRecordTag(); + const [open, setOpen] = useState(false); + const [extraTag, setExtraTag] = useState(''); + const [extraTags, setExtraTags] = useState([]); + const handelClickAddTagsIcon = () => { + setOpen(true); + updateSQLAuditRecordTag(projectName); + }; + + const content = useMemo(() => { + const addTag = async (tag: string) => { + updateTags(tags.filter((v) => v !== tag)); + setOpen(false); + setExtraTags((v) => []); + setExtraTag(''); + }; + + const createTag = ( + e: React.MouseEvent + ) => { + e.preventDefault(); + if (!extraTag || auditRecordTags.includes(extraTag)) { + return; + } + setExtraTags((v) => [...v, extraTag]); + setExtraTag(''); + setTimeout(() => { + inputRef.current?.focus(); + }, 0); + }; + return ( + +
+ 0} + defaultNode={ + + {t('sqlAudit.create.baseInfo.notTags')} + + } + /> + } + > + {[...extraTags, ...auditRecordTags].map((v) => ( +
addTag(v)} + > + {v} +
+ ))} +
+ + + + setExtraTag(e.target.value)} + /> + + +
+
+ ); + }, [ + auditRecordTags, + extraTag, + extraTags, + loading, + styles.optionsHover, + t, + tags, + updateTags, + ]); + + return ( + + {tags.map((v) => ( + { + e.preventDefault(); + removeTag(v); + }} + > + {v} + + ))} + + + + + + ); +}; + +export default CustomTags; diff --git a/src/page/SqlAuditRecord/List/FilterForm.tsx b/src/page/SqlAuditRecord/List/FilterForm.tsx new file mode 100644 index 00000000..3c10b616 --- /dev/null +++ b/src/page/SqlAuditRecord/List/FilterForm.tsx @@ -0,0 +1,115 @@ +import { Button, Col, DatePicker, Form, Input, Row, Select, Space } from 'antd'; +import { + SQLAuditListFilterFormProps, + SQLAuditListFilterFormFields, +} from './index.type'; +import { + FilterFormLayout, + FilterFormRowLayout, + filterFormButtonLayoutFactory, +} from '../../../data/common'; +import { useTranslation } from 'react-i18next'; +import moment from 'moment'; +import useStaticStatus from '../../../hooks/useStaticStatus'; +import useInstance from '../../../hooks/useInstance'; +import { useEffect } from 'react'; + +const computeDisabledDate = (current: moment.Moment) => { + return current && current > moment().endOf('day'); +}; + +const FilterFormColLayout = { + xs: 24, + sm: 12, + xl: 8, + xxl: 6, +}; + +const SQLAuditListFilterForm: React.FC = ({ + form, + reset, + submit, + projectName, +}) => { + const { t } = useTranslation(); + const { getSQLAuditRecordStatusSelectOption } = useStaticStatus(); + const { updateInstanceList, generateInstanceSelectOption } = useInstance(); + + useEffect(() => { + updateInstanceList({ project_name: projectName }); + }, [projectName, updateInstanceList]); + return ( + form={form} {...FilterFormLayout}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default SQLAuditListFilterForm; diff --git a/src/page/SqlAuditRecord/List/column.tsx b/src/page/SqlAuditRecord/List/column.tsx new file mode 100644 index 00000000..ab8f67eb --- /dev/null +++ b/src/page/SqlAuditRecord/List/column.tsx @@ -0,0 +1,109 @@ +import { Tag, Tooltip } from 'antd'; +import { Link } from '../../../components/Link'; +import { t } from '../../../locale'; +import { TableColumn } from '../../../types/common.type'; +import { formatTime } from '../../../utils/Common'; +import { floatRound, floatToPercent } from '../../../utils/Math'; +import { ISQLAuditRecord } from '../../../api/common'; +import { getSQLAuditRecordsV1FilterSqlAuditStatusEnum } from '../../../api/sql_audit_record/index.enum'; +import CustomTags from './CustomTags'; + +export const SQLAuditListColumn = ( + projectName: string, + updateTags: (id: string, tags: string[]) => Promise +): TableColumn< + ISQLAuditRecord, + 'instance_name' | 'pass_rate' | 'score' | 'audit_time' +> => { + return [ + { + dataIndex: 'sql_audit_record_id', + title: () => t('sqlAudit.list.table.columns.auditID'), + render: (id: string) => { + if (!id) { + return '-'; + } + + return ( + {id} + ); + }, + width: 200, + }, + { + dataIndex: 'instance_name', + title: () => t('sqlAudit.list.table.columns.instanceName'), + render: (_, record) => { + if (!record.task?.instance_name) { + return '-'; + } + + return {record.task?.instance_name}; + }, + width: 200, + }, + { + dataIndex: 'sql_audit_status', + title: () => t('sqlAudit.list.table.columns.auditStatus'), + render: (status: getSQLAuditRecordsV1FilterSqlAuditStatusEnum) => { + if (status === getSQLAuditRecordsV1FilterSqlAuditStatusEnum.auditing) { + return {t('sqlAudit.list.auditing')}; + } else if ( + status === getSQLAuditRecordsV1FilterSqlAuditStatusEnum.successfully + ) { + return {t('sqlAudit.list.successfully')}; + } + + return {t('common.unknownStatus')}; + }, + width: 160, + }, + { + dataIndex: 'tags', + title: () => t('sqlAudit.list.table.columns.businessTag'), + render: (tags: string[], record) => { + if (!Array.isArray(tags)) { + return '-'; + } + + return ( + updateTags(`${record.sql_audit_record_id}`, tags)} + /> + ); + }, + }, + { + dataIndex: 'score', + title: () => t('sqlAudit.list.table.columns.auditRating'), + render: (_, record) => { + const score = record.task?.score; + return typeof score === 'number' ? floatRound(score) : '-'; + }, + width: 100, + }, + { + dataIndex: 'pass_rate', + title: () => t('sqlAudit.list.table.columns.auditPassRate'), + render: (_, record) => { + const rate = record.task?.pass_rate; + return typeof rate === 'number' ? floatToPercent(rate) : '-'; + }, + width: 160, + }, + { + dataIndex: 'creator', + title: () => t('sqlAudit.list.table.columns.createUser'), + }, + { + dataIndex: 'audit_time', + title: () => t('sqlAudit.list.table.columns.auditTime'), + render(_, record) { + return formatTime(record.task?.exec_start_time, '-'); + }, + width: 200, + }, + ]; +}; diff --git a/src/page/SqlAuditRecord/List/index.less b/src/page/SqlAuditRecord/List/index.less new file mode 100644 index 00000000..fb5de869 --- /dev/null +++ b/src/page/SqlAuditRecord/List/index.less @@ -0,0 +1,19 @@ +.custom-tags-wrapper { + .ant-popover-inner-content { + padding: 4px; + + .custom-tag-item { + cursor: pointer; + padding: 8px; + border-radius: 4px; + } + + .ant-divider-horizontal { + margin: 0 0 8px 0; + } + + .add-tag-content { + padding: 0 8px 4px; + } + } +} diff --git a/src/page/SqlAuditRecord/List/index.tsx b/src/page/SqlAuditRecord/List/index.tsx new file mode 100644 index 00000000..04845862 --- /dev/null +++ b/src/page/SqlAuditRecord/List/index.tsx @@ -0,0 +1,137 @@ +import { Button, Card, PageHeader, Space, Table, message } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { Link } from '../../../components/Link'; +import { useCurrentProjectName } from '../../ProjectManage/ProjectDetail'; +import { useRequest } from 'ahooks'; +import { SQLAuditListFilterFormFields } from './index.type'; +import useTable from '../../../hooks/useTable'; +import SQLAuditListFilterForm from './FilterForm'; +import { SyncOutlined } from '@ant-design/icons'; +import { Theme } from '@mui/material/styles'; +import { useTheme } from '@mui/styles'; +import { SQLAuditListColumn } from './column'; +import sql_audit_record from '../../../api/sql_audit_record'; +import { translateTimeForRequest } from '../../../utils/Common'; +import { useCallback } from 'react'; +import { ResponseCode } from '../../../data/common'; + +import './index.less'; + +const SQLAuditList: React.FC = () => { + const { t } = useTranslation(); + const { projectName } = useCurrentProjectName(); + const theme = useTheme(); + + const { + pagination, + filterForm, + filterInfo, + resetFilter, + submitFilter, + tableChange, + } = useTable(); + + const { data, refresh, loading } = useRequest( + () => + sql_audit_record + .getSQLAuditRecordsV1({ + page_index: pagination.pageIndex, + page_size: pagination.pageSize, + project_name: projectName, + fuzzy_search_tags: filterInfo.fuzzy_search_tags, + filter_sql_audit_status: filterInfo.filter_sql_audit_status, + filter_instance_name: filterInfo.filter_instance_name, + filter_create_time_from: translateTimeForRequest( + filterInfo.filter_create_time?.[0] + ), + filter_create_time_to: translateTimeForRequest( + filterInfo.filter_create_time?.[1] + ), + }) + .then((res) => { + return { + list: res.data?.data ?? [], + total: res.data?.total_nums ?? 0, + }; + }), + { + refreshDeps: [pagination, filterInfo, projectName], + } + ); + + const updateTags = useCallback( + async (id: string, tags: string[] = []) => { + sql_audit_record + .updateSQLAuditRecordV1({ + tags, + sql_audit_record_id: id, + project_name: projectName, + }) + .then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + message.success(t('sqlAudit.list.table.updateTagsSuccess')); + refresh(); + } + }); + }, + [projectName, refresh, t] + ); + return ( + <> + + + + } + > + {t('sqlAudit.list.pageDesc')} + + +
+ + {t('sqlAudit.list.table.title')} + + + } + > + + + + + + + + ); +}; + +export default SQLAuditList; diff --git a/src/page/SqlAuditRecord/List/index.type.ts b/src/page/SqlAuditRecord/List/index.type.ts new file mode 100644 index 00000000..2d0b0a6d --- /dev/null +++ b/src/page/SqlAuditRecord/List/index.type.ts @@ -0,0 +1,22 @@ +import { FormInstance } from 'antd'; +import { getSQLAuditRecordsV1FilterSqlAuditStatusEnum } from '../../../api/sql_audit_record/index.enum'; + +//todo +export type SQLAuditListFilterFormFields = { + fuzzy_search_tags: string; + filter_sql_audit_status: getSQLAuditRecordsV1FilterSqlAuditStatusEnum; + filter_instance_name: string; + filter_create_time: moment.Moment[]; +}; + +export type SQLAuditListFilterFormProps = { + form: FormInstance; + submit: () => void; + projectName: string; + reset: () => void; +}; +export type CustomTagsProps = { + updateTags: (tags: string[]) => Promise; + tags: string[]; + projectName: string; +}; diff --git a/src/router/config.tsx b/src/router/config.tsx index a72e43d3..42e22747 100644 --- a/src/router/config.tsx +++ b/src/router/config.tsx @@ -267,6 +267,23 @@ const UpdateSyncTask = React.lazy( ) ); +const SQLAuditList = React.lazy( + () => + import(/* webpackChunkName: "SQLAuditList" */ '../page/SqlAuditRecord/List') +); +const SQLAuditDetail = React.lazy( + () => + import( + /* webpackChunkName: "SQLAuditDetail" */ '../page/SqlAuditRecord/Detail' + ) +); +const SQLAuditCreate = React.lazy( + () => + import( + /* webpackChunkName: "SQLAuditCreate" */ '../page/SqlAuditRecord/Create' + ) +); + export const unAuthRouter: RouteObject[] = [ { path: '/login', @@ -291,6 +308,30 @@ export const projectDetailRouterConfig: RouterConfigItem, }, + { + label: 'menu.sqlAudit', + key: 'sqlAudit', + icon: , + hideChildrenInSliderMenu: true, + path: 'sqlAudit', + children: [ + { + index: true, + element: , + key: 'sqlAuditList', + }, + { + path: 'create', + element: , + key: 'sqlAuditCreate', + }, + { + path: ':id/detail', + element: , + key: 'sqlAuditDetail', + }, + ] as RouterConfigItem[], + }, { label: 'menu.order', key: 'order', diff --git a/src/scripts/version.ts b/src/scripts/version.ts index a559def1..34c7ed8c 100644 --- a/src/scripts/version.ts +++ b/src/scripts/version.ts @@ -1 +1 @@ -export const UI_VERSION="feature/issue-110 72a0e52" \ No newline at end of file +export const UI_VERSION="feature/sql-audit 9b663b4" \ No newline at end of file diff --git a/src/types/router.type.ts b/src/types/router.type.ts index 59582deb..2920c560 100644 --- a/src/types/router.type.ts +++ b/src/types/router.type.ts @@ -57,7 +57,11 @@ export type ProjectDetailRouterItemKeyLiteral = | 'ruleTemplateUpdate' | 'member' | 'projectOverview' - | 'projectRedirect'; + | 'projectRedirect' + | 'sqlAudit' + | 'sqlAuditList' + | 'sqlAuditDetail' + | 'sqlAuditCreate'; export type RouterConfigItem = { role?: Array; From 3cb03ae0b0c9b96bdffedc0db3fa19147ac695b9 Mon Sep 17 00:00:00 2001 From: lizhensheng Date: Wed, 13 Sep 2023 17:25:21 +0800 Subject: [PATCH 2/2] [chore]: rebuild api , Update custom tag and Update test snapshot --- src/api/SqlManage/index.d.ts | 31 +++ src/api/SqlManage/index.enum.ts | 25 +++ src/api/SqlManage/index.ts | 28 +++ src/api/common.d.ts | 4 +- src/api/sql_audit_record/index.d.ts | 2 +- src/api/sql_audit_record/index.ts | 18 +- src/locale/zh-CN/sqlAudit.ts | 2 +- .../Create/__snapshots__/index.test.tsx.snap | 16 +- .../Detail/__snapshots__/index.test.tsx.snap | 16 +- .../__snapshots__/Layout.test.tsx.snap | 184 ++++++++++++++++++ .../__snapshots__/index.test.tsx.snap | 20 ++ .../__snapshots__/index.test.tsx.snap | 92 +++++++++ .../SqlAuditRecord/Create/BaseInfoForm.tsx | 72 +++---- .../SqlAuditRecord/Create/SQLInfoForm.tsx | 7 +- src/page/SqlAuditRecord/Create/index.tsx | 10 +- src/page/SqlAuditRecord/Detail/index.tsx | 6 +- src/page/SqlAuditRecord/List/CustomTags.tsx | 82 +++++--- src/page/SqlAuditRecord/List/column.tsx | 31 +-- src/scripts/version.ts | 2 +- 19 files changed, 530 insertions(+), 118 deletions(-) create mode 100644 src/api/SqlManage/index.d.ts create mode 100644 src/api/SqlManage/index.enum.ts create mode 100644 src/api/SqlManage/index.ts diff --git a/src/api/SqlManage/index.d.ts b/src/api/SqlManage/index.d.ts new file mode 100644 index 00000000..dcb86c06 --- /dev/null +++ b/src/api/SqlManage/index.d.ts @@ -0,0 +1,31 @@ +import { + GetSqlManageListFilterSourceEnum, + GetSqlManageListFilterAuditLevelEnum, + GetSqlManageListFilterStatusEnum +} from './index.enum'; + +import { IGetSqlManageListResp } from '../common.d'; + +export interface IGetSqlManageListParams { + fuzzy_search_sql_fingerprint?: string; + + filter_assignee?: string; + + filter_instance_name?: string; + + filter_source?: GetSqlManageListFilterSourceEnum; + + filter_audit_level?: GetSqlManageListFilterAuditLevelEnum; + + filter_last_audit_start_time_from?: string; + + filter_last_audit_start_time_to?: string; + + filter_status?: GetSqlManageListFilterStatusEnum; + + page_index: number; + + page_size: number; +} + +export interface IGetSqlManageListReturn extends IGetSqlManageListResp {} diff --git a/src/api/SqlManage/index.enum.ts b/src/api/SqlManage/index.enum.ts new file mode 100644 index 00000000..705a4c2c --- /dev/null +++ b/src/api/SqlManage/index.enum.ts @@ -0,0 +1,25 @@ +/* tslint:disable no-duplicate-string */ + +export enum GetSqlManageListFilterSourceEnum { + 'audit_plan' = 'audit_plan', + + 'api_audit' = 'api_audit' +} + +export enum GetSqlManageListFilterAuditLevelEnum { + 'normal' = 'normal', + + 'notice' = 'notice', + + 'warn' = 'warn', + + 'error' = 'error' +} + +export enum GetSqlManageListFilterStatusEnum { + 'unhandled' = 'unhandled', + + 'solved' = 'solved', + + 'ignored' = 'ignored' +} diff --git a/src/api/SqlManage/index.ts b/src/api/SqlManage/index.ts new file mode 100644 index 00000000..d743ed24 --- /dev/null +++ b/src/api/SqlManage/index.ts @@ -0,0 +1,28 @@ +/* tslint:disable no-identical-functions */ +/* tslint:disable no-useless-cast */ +/* tslint:disable no-unnecessary-type-assertion */ +/* tslint:disable no-big-function */ +/* tslint:disable no-duplicate-string */ +import ServiceBase from '../Service.base'; +import { AxiosRequestConfig } from 'axios'; + +import { IGetSqlManageListParams, IGetSqlManageListReturn } from './index.d'; + +class SqlManageService extends ServiceBase { + public GetSqlManageList( + params: IGetSqlManageListParams, + 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`, + paramsData, + options + ); + } +} + +export default new SqlManageService(); diff --git a/src/api/common.d.ts b/src/api/common.d.ts index d49f3252..74b4264f 100644 --- a/src/api/common.d.ts +++ b/src/api/common.d.ts @@ -2136,11 +2136,13 @@ export interface ISMTPConfigurationResV1 { } export interface ISQLAuditRecord { + created_at?: string; + creator?: string; instance?: ISQLAuditRecordInstance; - sql_audit_record_id?: number; + sql_audit_record_id?: string; sql_audit_status?: string; diff --git a/src/api/sql_audit_record/index.d.ts b/src/api/sql_audit_record/index.d.ts index e613f3da..da61d6f2 100644 --- a/src/api/sql_audit_record/index.d.ts +++ b/src/api/sql_audit_record/index.d.ts @@ -38,7 +38,7 @@ export interface ICreateSQLAuditRecordV1Params { db_type?: string; - sql?: string; + sqls?: string; input_sql_file?: any; diff --git a/src/api/sql_audit_record/index.ts b/src/api/sql_audit_record/index.ts index 6bfd8a79..e9471394 100644 --- a/src/api/sql_audit_record/index.ts +++ b/src/api/sql_audit_record/index.ts @@ -16,7 +16,7 @@ import { IGetSQLAuditRecordV1Params, IGetSQLAuditRecordV1Return, IUpdateSQLAuditRecordV1Params, - IUpdateSQLAuditRecordV1Return + IUpdateSQLAuditRecordV1Return, } from './index.d'; class SqlAuditRecordService extends ServiceBase { @@ -29,7 +29,7 @@ class SqlAuditRecordService extends ServiceBase { delete paramsData.project_name; return this.get( - `/v1/projects/${project_name}/sql_audit_record`, + `/v1/projects/${project_name}/sql_audit_records`, paramsData, options ); @@ -44,7 +44,7 @@ class SqlAuditRecordService extends ServiceBase { config.headers = { ...headers, - 'Content-Type': 'multipart/form-data' + 'Content-Type': 'multipart/form-data', }; const paramsData = new FormData(); @@ -61,8 +61,8 @@ class SqlAuditRecordService extends ServiceBase { paramsData.append('db_type', params.db_type as any); } - if (params.sql != undefined) { - paramsData.append('sql', params.sql as any); + if (params.sqls != undefined) { + paramsData.append('sqls', params.sqls as any); } if (params.input_sql_file != undefined) { @@ -83,7 +83,7 @@ class SqlAuditRecordService extends ServiceBase { const project_name = params.project_name; return this.post( - `/v1/projects/${project_name}/sql_audit_record`, + `/v1/projects/${project_name}/sql_audit_records`, paramsData, config ); @@ -98,7 +98,7 @@ class SqlAuditRecordService extends ServiceBase { delete paramsData.project_name; return this.get( - `/v1/projects/${project_name}/sql_audit_record/tag_tips`, + `/v1/projects/${project_name}/sql_audit_records/tag_tips`, paramsData, options ); @@ -116,7 +116,7 @@ class SqlAuditRecordService extends ServiceBase { delete paramsData.sql_audit_record_id; return this.get( - `/v1/projects/${project_name}/sql_audit_record/${sql_audit_record_id}`, + `/v1/projects/${project_name}/sql_audit_records/${sql_audit_record_id}/`, paramsData, options ); @@ -134,7 +134,7 @@ class SqlAuditRecordService extends ServiceBase { delete paramsData.sql_audit_record_id; return this.patch( - `/v1/projects/${project_name}/sql_audit_record/${sql_audit_record_id}`, + `/v1/projects/${project_name}/sql_audit_records/${sql_audit_record_id}/`, paramsData, options ); diff --git a/src/locale/zh-CN/sqlAudit.ts b/src/locale/zh-CN/sqlAudit.ts index 68f74c90..a4ce7bc0 100644 --- a/src/locale/zh-CN/sqlAudit.ts +++ b/src/locale/zh-CN/sqlAudit.ts @@ -30,7 +30,7 @@ export default { create: { title: 'SQL审核', pageDesc: '您可以在这里获得快速审核SQL', - + createTagErrorTips: '当前标签已存在', baseInfo: { title: '基本信息', businessTag: '业务标签', diff --git a/src/page/Order/Create/__snapshots__/index.test.tsx.snap b/src/page/Order/Create/__snapshots__/index.test.tsx.snap index b2c94c66..2dc3f59f 100644 --- a/src/page/Order/Create/__snapshots__/index.test.tsx.snap +++ b/src/page/Order/Create/__snapshots__/index.test.tsx.snap @@ -1046,7 +1046,7 @@ exports[`Order/Create should audit sql when user click audit button 1`] = ` style="margin-left: -12px; margin-right: -12px;" >
+
  • +
  • +
  • +
  • , "title": "menu.projectOverview", }, + Object { + "icon": , + "key": "sqlAudit", + "label": + menu.sqlAudit + , + "title": "menu.sqlAudit", + }, Object { "icon": , "key": "order", @@ -111,6 +121,16 @@ Array [ , "title": "menu.projectOverview", }, + Object { + "icon": , + "key": "sqlAudit", + "label": + menu.sqlAudit + , + "title": "menu.sqlAudit", + }, Object { "icon": , "key": "order", diff --git a/src/page/ProjectManage/ProjectDetail/__snapshots__/index.test.tsx.snap b/src/page/ProjectManage/ProjectDetail/__snapshots__/index.test.tsx.snap index 453a6661..b6618049 100644 --- a/src/page/ProjectManage/ProjectDetail/__snapshots__/index.test.tsx.snap +++ b/src/page/ProjectManage/ProjectDetail/__snapshots__/index.test.tsx.snap @@ -61,6 +61,52 @@ exports[`test ProjectManage/ProjectDetail should match snapshot when bound proje
  • +
  • +
  • = ({ projectName, form }, ref) => { const { t } = useTranslation(); const inputRef = useRef(null); + const [extraTagForm] = Form.useForm<{ extraTag: string }>(); const { auditRecordTags, updateSQLAuditRecordTag } = useSQLAuditRecordTag(); - const [extraTag, setExtraTag] = useState(''); - const [extraTags, setExtraTags] = useState([]); + const [values, setValues] = useState([]); - const addTag = ( + const createTag = async ( e: React.MouseEvent ) => { e.preventDefault(); - if (!extraTag || auditRecordTags.includes(extraTag)) { + const { extraTag } = await extraTagForm.validateFields(); + if (values.includes(extraTag)) { + message.error(t('sqlAudit.create.createTagErrorTips')); return; } - setExtraTags((v) => [...v, extraTag]); - setExtraTag(''); + setValues([...values, extraTag]); + form.setFieldsValue({ + tags: [...values, extraTag], + }); + + extraTagForm.resetFields(); setTimeout(() => { inputRef.current?.focus(); }, 0); @@ -72,10 +79,9 @@ const BaseInfoForm: React.ForwardRefRenderFunction< }; const reset = useCallback(() => { - setExtraTag(''); - setExtraTags([]); + extraTagForm.resetFields(); setValues([]); - }, []); + }, [extraTagForm]); useImperativeHandle(ref, () => ({ reset }), [reset]); @@ -89,16 +95,7 @@ const BaseInfoForm: React.ForwardRefRenderFunction< {...PageFormLayout} scrollToFirstError > - + setExtraTag(e.target.value)} - /> - - +
    + + + + + + + + )} > - {[...auditRecordTags, ...extraTags].map((v) => ( + {auditRecordTags.map((v) => ( {v} diff --git a/src/page/SqlAuditRecord/Create/SQLInfoForm.tsx b/src/page/SqlAuditRecord/Create/SQLInfoForm.tsx index 062eb910..9910aec9 100644 --- a/src/page/SqlAuditRecord/Create/SQLInfoForm.tsx +++ b/src/page/SqlAuditRecord/Create/SQLInfoForm.tsx @@ -346,12 +346,17 @@ const SQLInfoForm: React.FC = ({ onClick={internalSubmit} type="primary" loading={submitLoading} + disabled={submitLoading} > {t('sqlAudit.create.SQLInfo.auditButton')}
  • + {t('sqlAudit.detail.auditID')} @@ -91,7 +91,7 @@ const SQLAuditDetail: React.FC = () => { - + {t('sqlAudit.detail.auditPassRate')} @@ -102,7 +102,7 @@ const SQLAuditDetail: React.FC = () => { - + {t('sqlAudit.detail.auditRating')} diff --git a/src/page/SqlAuditRecord/List/CustomTags.tsx b/src/page/SqlAuditRecord/List/CustomTags.tsx index 41518876..78eb1a71 100644 --- a/src/page/SqlAuditRecord/List/CustomTags.tsx +++ b/src/page/SqlAuditRecord/List/CustomTags.tsx @@ -2,6 +2,7 @@ import { Button, Divider, Empty, + Form, Input, InputRef, Popover, @@ -9,6 +10,7 @@ import { Spin, Tag, Typography, + message, } from 'antd'; import { CustomTagsProps } from './index.type'; import { PlusCircleOutlined, PlusOutlined } from '@ant-design/icons'; @@ -17,6 +19,7 @@ import useSQLAuditRecordTag from '../../../hooks/useSQLAuditRecordTag'; import { useTranslation } from 'react-i18next'; import useStyles from '../../../theme'; import EmptyBox from '../../../components/EmptyBox'; +import { nameRule } from '../../../utils/FormRule'; const CustomTags: React.FC = ({ tags, @@ -25,6 +28,9 @@ const CustomTags: React.FC = ({ }) => { const styles = useStyles(); const { t } = useTranslation(); + const [extraTagForm] = Form.useForm<{ extraTag: string }>(); + const extraTag = Form.useWatch('extraTag', extraTagForm); + const removing = useRef(false); const removeTag = async (tag: string) => { if (removing.current) { @@ -40,39 +46,42 @@ const CustomTags: React.FC = ({ const { loading, updateSQLAuditRecordTag, auditRecordTags } = useSQLAuditRecordTag(); const [open, setOpen] = useState(false); - const [extraTag, setExtraTag] = useState(''); - const [extraTags, setExtraTags] = useState([]); const handelClickAddTagsIcon = () => { setOpen(true); updateSQLAuditRecordTag(projectName); }; const content = useMemo(() => { - const addTag = async (tag: string) => { - updateTags(tags.filter((v) => v !== tag)); - setOpen(false); - setExtraTags((v) => []); - setExtraTag(''); - }; - - const createTag = ( - e: React.MouseEvent + const createTag = async ( + e: React.MouseEvent, + tag?: string ) => { e.preventDefault(); - if (!extraTag || auditRecordTags.includes(extraTag)) { + let realTag = ''; + if (tag) { + realTag = tag; + } else { + const values = await extraTagForm.validateFields(); + realTag = values.extraTag; + } + if (!realTag) { + return; + } + if (tags.includes(realTag)) { + message.error(t('sqlAudit.create.createTagErrorTips')); return; } - setExtraTags((v) => [...v, extraTag]); - setExtraTag(''); - setTimeout(() => { - inputRef.current?.focus(); - }, 0); + updateTags([...tags, realTag]); + + extraTagForm.resetFields(); + + setOpen(false); }; return (
    0} + if={auditRecordTags.length > 0} defaultNode={ = ({ /> } > - {[...extraTags, ...auditRecordTags].map((v) => ( + {auditRecordTags.map((v) => (
    addTag(v)} + onClick={(e) => createTag(e, v)} > {v}
    @@ -96,24 +105,33 @@ const CustomTags: React.FC = ({
    - - setExtraTag(e.target.value)} - /> - - +
    + + + + + + + +
    ); }, [ auditRecordTags, extraTag, - extraTags, + extraTagForm, loading, styles.optionsHover, t, diff --git a/src/page/SqlAuditRecord/List/column.tsx b/src/page/SqlAuditRecord/List/column.tsx index ab8f67eb..27973bde 100644 --- a/src/page/SqlAuditRecord/List/column.tsx +++ b/src/page/SqlAuditRecord/List/column.tsx @@ -11,10 +11,7 @@ import CustomTags from './CustomTags'; export const SQLAuditListColumn = ( projectName: string, updateTags: (id: string, tags: string[]) => Promise -): TableColumn< - ISQLAuditRecord, - 'instance_name' | 'pass_rate' | 'score' | 'audit_time' -> => { +): TableColumn => { return [ { dataIndex: 'sql_audit_record_id', @@ -38,7 +35,15 @@ export const SQLAuditListColumn = ( return '-'; } - return {record.task?.instance_name}; + return record.instance?.db_host && record.instance.db_port ? ( + + {record.task?.instance_name} + + ) : ( + record.task?.instance_name + ); }, width: 200, }, @@ -62,15 +67,13 @@ export const SQLAuditListColumn = ( dataIndex: 'tags', title: () => t('sqlAudit.list.table.columns.businessTag'), render: (tags: string[], record) => { - if (!Array.isArray(tags)) { - return '-'; - } - return ( updateTags(`${record.sql_audit_record_id}`, tags)} + tags={tags ?? []} + updateTags={(realTags) => + updateTags(record.sql_audit_record_id ?? '', realTags) + } /> ); }, @@ -98,10 +101,10 @@ export const SQLAuditListColumn = ( title: () => t('sqlAudit.list.table.columns.createUser'), }, { - dataIndex: 'audit_time', + dataIndex: 'created_at', title: () => t('sqlAudit.list.table.columns.auditTime'), - render(_, record) { - return formatTime(record.task?.exec_start_time, '-'); + render(time: string) { + return formatTime(time, '-'); }, width: 200, }, diff --git a/src/scripts/version.ts b/src/scripts/version.ts index 34c7ed8c..27cdf538 100644 --- a/src/scripts/version.ts +++ b/src/scripts/version.ts @@ -1 +1 @@ -export const UI_VERSION="feature/sql-audit 9b663b4" \ No newline at end of file +export const UI_VERSION = 'feature/issue-110 72a0e52';