diff --git a/package.json b/package.json index f0c3872de..d6b36ce01 100644 --- a/package.json +++ b/package.json @@ -200,7 +200,7 @@ "eslintConfig": { "extends": "react-app" }, - "proxy": "https://testforgeplus.trustie.net", + "proxy": "http://172.20.32.202:4000", "port": "3007", "devDependencies": { "@babel/core": "^7.23.6", diff --git a/src/AppConfig.js b/src/AppConfig.js index e102edbe1..308ff1a6a 100644 --- a/src/AppConfig.js +++ b/src/AppConfig.js @@ -32,7 +32,7 @@ export function initAxiosInterceptors(props) { // 判断网络是否连接 initOnlineOfflineListener(); - var proxy = "https://testforgeplus.trustie.net"; + var proxy = "http://172.20.32.202:4000/"; // var proxy = "https://www.gitlink.org.cn"; //响应前的设置 diff --git a/src/common/util/ShareUtil.js b/src/common/util/ShareUtil.js index 3eb42e755..2778b8d2d 100644 --- a/src/common/util/ShareUtil.js +++ b/src/common/util/ShareUtil.js @@ -3,7 +3,7 @@ const host = window.location.protocol + '//' + window.location.host const wx = window.wx const appId = 'wxf8debe756b4a0309' -let settings = localStorage.chromesetting&&JSON.parse(localStorage.chromesetting); +let settings = localStorage.chromesetting && localStorage.chromesetting !== 'undefined' && JSON.parse(localStorage.chromesetting); let actionUrl = settings && settings.common.zone +'/api'; const service = axios.create({ diff --git a/src/forge/Server/Index.jsx b/src/forge/Server/Index.jsx index 9a6f57e4f..9b47adeae 100644 --- a/src/forge/Server/Index.jsx +++ b/src/forge/Server/Index.jsx @@ -23,6 +23,17 @@ const Pages = Loadable({ loader: () => import('./pages'), loading: Loading, }) +// 代码质量分析 +const Quality = Loadable({ + loader: () => import('./quality/index'), + loading: Loading, +}) +// 代码质量分析详情 +const QualityDetail = Loadable({ + loader: () => import('./quality/detail'), + loading: Loading, +}) + function ServerIndex(props){ const { projectDetail } = props; const { owner , projectsId } = props.match.params; @@ -47,6 +58,16 @@ function ServerIndex(props){ () => () } > + () + } + > + () + } + > {/* 个人建站服务 */} {projectDetail && projectDetail.web_site && projectDetail.author.type === "User" && 查看详情 +
  • +
    + + 代码质量分析工具 +
    +

    提供自动化的静态代码检查能力,能够识别代码中的缺陷、安全漏洞、代码异味,同时产出代码覆盖率和重复度的详细报告

    + + 查看详情 + +
  • {/* 是站点仓库则显示,否则隐藏 */} {projectDetail && projectDetail.web_site && projectDetail.author.type === "User" &&
  • diff --git a/src/forge/Server/api.js b/src/forge/Server/api.js new file mode 100644 index 000000000..acf0679af --- /dev/null +++ b/src/forge/Server/api.js @@ -0,0 +1,69 @@ +import fetch from './fetch'; + +// 获取列表 +export function getList(params, owner, repo) { + return fetch({ + url: `/api/v1/${owner}/${repo}/sonarqubes/issues_search.json?s=FILE_LINE`, + // url: `/api/issues/search?components=kingchanx-fluid-cloudnative_fluid&s=FILE_LINE&issueStatuses=CONFIRMED%2COPEN&&additionalFields=_all&timeZone=Asia%2FShanghai`, + method: 'get', + params + }); +} + + +// 代码片段 +export function getCodeSnippets(key, owner, repo) { + return fetch({ + url: `/api/v1/${owner}/${repo}/sonarqubes/sources_issue_snippet.json?issueKey=${ key }`, + method: 'get', + }); +} + +// 问题分析 +export function getProblemDetail(key, owner, repo) { + return fetch({ + url: `/api/v1/${owner}/${repo}/sonarqubes/rules_show.json?key=${ key }`, + method: 'get', + }); +} + +// 数据汇总 +export function getProblemData(owner, repo , keys) { + return fetch({ + url: `/api/v1/${owner}/${repo}/sonarqubes/measures_component.json?component=${ owner }-${ repo }&metricKeys=${ keys }`, + method: 'get', + }); +} + +// 初始化sonar +export function initSonar(open, owner, repo) { + return fetch({ + url: `/api/v1/${owner}/${repo}/sonarqubes/sonar_initialize.json?has_actions=${ open }`, + method: 'post', + }); +} + +// 开始分析 +export function startAlalyze( branch , owner, repo) { + return fetch({ + url: `/api/v1/${owner}/${repo}/sonarqubes/insert_file.json?branch=${ branch }`, + method: 'post', + }); +} + +// // 获取action任务列表 +// export function getActionList(owner, repo ) { +// return fetch({ +// url: `/api/v1/${owner}/${repo}/actions`, +// method: 'get', +// }); +// } + +// 获取action任务列表 +export function getActionList(owner, repo ) { + return fetch({ + url: `/api/v1/${owner}/${repo}/actions/runs?workflow=SonarScanner.yaml`, + method: 'get', + }); +} + diff --git a/src/forge/Server/constants/quality.js b/src/forge/Server/constants/quality.js new file mode 100644 index 000000000..c44461377 --- /dev/null +++ b/src/forge/Server/constants/quality.js @@ -0,0 +1,35 @@ +import bugs from '../img/quality/bugs.png' +import vulnerabilities from '../img/quality/vulnerabilities.png' +import code_smells from '../img/quality/code_smells.png' +import low from '../img/quality/low.png' +import medium from '../img/quality/medium.png' +import high from '../img/quality/high.png' + +export const issueType = { + BUG: 'BUG', + VULNERABILITY: '漏洞', + CODE_SMELL: '异味' +} + +export const issueTypeKeys = { + BUG: 'bugs', + VULNERABILITY: 'vulnerabilities', + CODE_SMELL: 'code_smells' +} +export const maintainability = { + LOW: '低', + MEDIUM: '中', + HIGH: '高' +} + +export const issueTypeIcon = { + BUG: bugs, + VULNERABILITY: vulnerabilities, + CODE_SMELL: code_smells +} + +export const maintainabilityIcon = { + LOW: low, + MEDIUM: medium, + HIGH: high +} diff --git a/src/forge/Server/fetch.js b/src/forge/Server/fetch.js new file mode 100644 index 000000000..95a7d849b --- /dev/null +++ b/src/forge/Server/fetch.js @@ -0,0 +1,10 @@ +import javaFetch from '../javaFetch'; + +let settings = localStorage.chromesetting && JSON.parse(localStorage.chromesetting); +// let actionUrl = settings && settings.common.softbot; +let actionUrl = 'http://172.20.32.202:9999'; + +const service = javaFetch(); +// export const httpUrl = actionUrl; +// export const main_site_url = settings && settings.common.main_site_url +export default service; \ No newline at end of file diff --git a/src/forge/Server/img/close.png b/src/forge/Server/img/close.png new file mode 100644 index 000000000..de4a9da8d Binary files /dev/null and b/src/forge/Server/img/close.png differ diff --git a/src/forge/Server/img/logo3-1.png b/src/forge/Server/img/logo3-1.png new file mode 100644 index 000000000..c2a6c85f0 Binary files /dev/null and b/src/forge/Server/img/logo3-1.png differ diff --git a/src/forge/Server/img/logo3.png b/src/forge/Server/img/logo3.png index c2a6c85f0..66c617f97 100644 Binary files a/src/forge/Server/img/logo3.png and b/src/forge/Server/img/logo3.png differ diff --git a/src/forge/Server/img/logo4-1.png b/src/forge/Server/img/logo4-1.png new file mode 100644 index 000000000..37e43bacc Binary files /dev/null and b/src/forge/Server/img/logo4-1.png differ diff --git a/src/forge/Server/img/logo4.png b/src/forge/Server/img/logo4.png new file mode 100644 index 000000000..cf9d0317f Binary files /dev/null and b/src/forge/Server/img/logo4.png differ diff --git a/src/forge/Server/img/quality/back.png b/src/forge/Server/img/quality/back.png new file mode 100644 index 000000000..460e55a99 Binary files /dev/null and b/src/forge/Server/img/quality/back.png differ diff --git a/src/forge/Server/img/quality/bugs.png b/src/forge/Server/img/quality/bugs.png new file mode 100644 index 000000000..a921c64b5 Binary files /dev/null and b/src/forge/Server/img/quality/bugs.png differ diff --git a/src/forge/Server/img/quality/code_lines.png b/src/forge/Server/img/quality/code_lines.png new file mode 100644 index 000000000..7ffec82cf Binary files /dev/null and b/src/forge/Server/img/quality/code_lines.png differ diff --git a/src/forge/Server/img/quality/code_smells.png b/src/forge/Server/img/quality/code_smells.png new file mode 100644 index 000000000..ca7434e39 Binary files /dev/null and b/src/forge/Server/img/quality/code_smells.png differ diff --git a/src/forge/Server/img/quality/cover.png b/src/forge/Server/img/quality/cover.png new file mode 100644 index 000000000..44a53a6cb Binary files /dev/null and b/src/forge/Server/img/quality/cover.png differ diff --git a/src/forge/Server/img/quality/folder.png b/src/forge/Server/img/quality/folder.png new file mode 100644 index 000000000..2ce02b8ac Binary files /dev/null and b/src/forge/Server/img/quality/folder.png differ diff --git a/src/forge/Server/img/quality/high.png b/src/forge/Server/img/quality/high.png new file mode 100644 index 000000000..ce67e4739 Binary files /dev/null and b/src/forge/Server/img/quality/high.png differ diff --git a/src/forge/Server/img/quality/label.png b/src/forge/Server/img/quality/label.png new file mode 100644 index 000000000..e2c06300e Binary files /dev/null and b/src/forge/Server/img/quality/label.png differ diff --git a/src/forge/Server/img/quality/low.png b/src/forge/Server/img/quality/low.png new file mode 100644 index 000000000..25c563d8e Binary files /dev/null and b/src/forge/Server/img/quality/low.png differ diff --git a/src/forge/Server/img/quality/medium.png b/src/forge/Server/img/quality/medium.png new file mode 100644 index 000000000..1b8814cec Binary files /dev/null and b/src/forge/Server/img/quality/medium.png differ diff --git a/src/forge/Server/img/quality/repeat.png b/src/forge/Server/img/quality/repeat.png new file mode 100644 index 000000000..2b0bad6c0 Binary files /dev/null and b/src/forge/Server/img/quality/repeat.png differ diff --git a/src/forge/Server/img/quality/vulnerabilities.png b/src/forge/Server/img/quality/vulnerabilities.png new file mode 100644 index 000000000..6030bdd26 Binary files /dev/null and b/src/forge/Server/img/quality/vulnerabilities.png differ diff --git a/src/forge/Server/quality/component/ProblemDetail.jsx b/src/forge/Server/quality/component/ProblemDetail.jsx new file mode 100644 index 000000000..bbca4f733 --- /dev/null +++ b/src/forge/Server/quality/component/ProblemDetail.jsx @@ -0,0 +1,41 @@ +import React, { useEffect, useState } from 'react'; +import { Drawer , Divider } from 'antd'; +import './index.scss' +import close from '../../img/close.png' +import moment from 'moment'; + +function ProblemDetail({visible, issue, issueDetail, onClose}){ + + onClose + + return( + issue && issueDetail && +
    + { issue.message } + +
    +
    +

    + { issue.message } + { issue.rule } +

    +

    + { issueDetail.type } + { issueDetail.severity } + 生效时间 { moment(issue.creationDate).format('YYYY年MM月DD日') } + { issueDetail.langName } +

    + +
    +
    +
    + ) +} +export default ProblemDetail; diff --git a/src/forge/Server/quality/component/index.scss b/src/forge/Server/quality/component/index.scss new file mode 100644 index 000000000..6170527c7 --- /dev/null +++ b/src/forge/Server/quality/component/index.scss @@ -0,0 +1,44 @@ +.drawer-head { + height: 60px; + background: #252D41; + font-weight: 400; + font-size: 16px; + color: #FFFFFF; + padding: 20px 40px; + img { + width: 24px; + margin-left: auto; + float: right; + cursor: pointer; + } +} +.drawer-content { + height: 400px; + padding: 20px 40px; + overflow-y: auto; + .content-title { + font-weight: bold; + font-size: 18px; + color: #111010; + span { + float: right; + } + } + .content-info { + font-weight: 400; + font-size: 14px; + color: #5D6790; + margin-top: 15px; + span:nth-of-type(2) { + &::before { + content: "|"; + color: rgba(187,189,197,0.61); + margin-right: 13px; + margin-left: 13px; + } + } + span:nth-of-type(n + 3) { + margin-left: 70px; + } + } +} \ No newline at end of file diff --git a/src/forge/Server/quality/detail.jsx b/src/forge/Server/quality/detail.jsx new file mode 100644 index 000000000..8adbadd68 --- /dev/null +++ b/src/forge/Server/quality/detail.jsx @@ -0,0 +1,229 @@ +import React, {useState, useEffect, useRef } from "react"; +import { getList, getCodeSnippets, getProblemDetail } from "../api"; +import './detail.scss'; +import ProblemDetail from "./component/ProblemDetail"; +import { issueType, issueTypeIcon } from "../constants/quality" +import folderImg from '../img/quality/folder.png' +import backImg from '../img/quality/back.png' +import { Divider } from 'antd'; + +function issueDetail(props){ + const { owner , projectsId } = props.match.params; + + const [ snippetsDetail, setSnippetsDetail ] = useState(null); + const [ issues, setIssues ] = useState({}); + const [ selectedIssue, setSelectedIssue ] = useState(null); + const [ selectedFlow, setSelectedFlow ] = useState(null); + const [ detailVisible, setDetailVisible ] = useState(true); + const [ issueInfo, setIssueInfo ] = useState(null); + const [reload, setReload] = useState(); + const [pageNum, setPageNum] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [total, setTotal] = useState() + const { projectDetail, history } = props; + const codeRef = useRef(null); + + useEffect(()=>{ + // 更新网页标题 + if(projectDetail){ + if (history.location.state?.baseInfo) { + handleIssues(history.location.state?.baseInfo) + } else { + getIssueList() + } + if (history.location.state?.selectedIssue) { + setSelectedIssue(history.location.state?.selectedIssue) + } + } + }, [projectDetail, pageNum]) + + useEffect(() => { + if (selectedIssue) { + getCodeSnippets(selectedIssue.key, owner, projectsId).then(res => { + processIssues(res[selectedIssue.component].sources, issues[selectedIssue.component]) + setSnippetsDetail(res[selectedIssue.component]); + }) + } + },[selectedIssue]) + + useEffect(()=>{ + },[reload]) + + const getIssueList = (owner, name) => { + let params = { + p: pageNum, + ps: pageSize + }; + getList(params).then(res => { + handleIssues(res) + }) + }; + + const handleIssues = (data) => { + let newIssues = data.issues + let issuesHandled = JSON.parse(JSON.stringify(issues)) + setTotal(data.total) + newIssues.forEach(e => { + if (issuesHandled[e.component]) { + issuesHandled[e.component].push(e) + } else { + issuesHandled[e.component] = [e] + } + }); + setIssues(issuesHandled) + } + + const addSpecialClassToRange = (code, textRange, className) => { + let textOnly = code.replace(/<[^>]+>/g, ''); // Remove HTML tags to get text only + let start = 0; + let end = textOnly.length; + + if (textRange.startLine === textRange.endLine) { + start = textRange.startOffset; + end = textRange.endOffset; + } + + let specialText = textOnly.slice(start, end); + let highlightedText = `${specialText}`; + + let currentIndex = 0; + let finalCode = ''; + let tag = false; + let tagBuffer = ''; + + for (let i = 0; i < code.length; i++) { + let char = code[i]; + + if (char === '<') { + tag = true; + tagBuffer += char; + } else if (char === '>') { + tagBuffer += char; + tag = false; + finalCode += tagBuffer; + tagBuffer = ''; + } else if (tag) { + tagBuffer += char; + } else { + if (currentIndex === start) { + finalCode += highlightedText; + } + if (currentIndex >= start && currentIndex < end) { + currentIndex++; + continue; + } + finalCode += char; + currentIndex++; + } + } + + return finalCode; + } + + function processIssues(data, issues) { + return data.map(item => { + // 处理 issue 的 textRange + issues.forEach(issue => { + if (item.line === issue.line) { + item.code = addSpecialClassToRange(item.code, issue.textRange, 'special'); + // 将 issue 添加到 data 项 + item.issue = issue + } + }); + // 处理选中 issue 的 flows 中的 textRange + selectedIssue.flows.forEach(flow => { + flow.locations.forEach(location => { + if (item.line === location.textRange.startLine) { + item.code = addSpecialClassToRange(item.code, location.textRange, 'special1'); + } + }); + }); + return item; + }); + } + + // 点击右侧列表重复选中 + function selectRepeat(e, line) { + if (e.target.className === 'special1') { + setSelectedFlow(line) + } + } + + // 点击左侧列表跳转到对应位置 + function selectAndJumpRepeat(line) { + setSelectedFlow(line) + const element = document.getElementById(`line-${ line }`); + if (element) { + // 滚动到目标元素 + element.scrollIntoView({ behavior: 'smooth', block: "center" }); + } + } + + function detail() { + getProblemDetail( selectedIssue.rule, owner, projectsId).then(res => { + setIssueInfo(res?.rule) + setDetailVisible(true) + }) + } + + return
    + setDetailVisible(false)}> +
    + history.goBack()}/> + + { + issues && Object.entries(issues).map(([key, item]) =>
    +
    { key.split(`:`)[1] }
    + { + item.map(e => ( +
    setSelectedIssue(e)}> +

    { e.message }

    +

    { issueType[e.type] }{ e.flows.length > 0 && +{e.flows.length} }

    + { + e.key === selectedIssue?.key && e.flows && e.flows.length > 0 && e.flows.map((flow, index) => { + return
    selectAndJumpRepeat(flow.locations[0].textRange.startLine) }> + { index } + { flow.locations[0].msg } +
    + }) + } +
    + )) + } +
    + ) + } + { pageNum * pageSize < total &&
    setPageNum(pageNum + 1)}>查看更多
    } +
    + { + snippetsDetail &&
    +
    { snippetsDetail.component.path }
    +
    + + {snippetsDetail.sources.map((item, index) => ( + <> + {item.line} + +
    +
     selectRepeat(e, item.line)}/>
    +                    {issues[selectedIssue.component] && issues[selectedIssue.component].map(e => {
    +                      if (e.line === item.line) {
    +                        return 
    setSelectedIssue(e)}> + {e.message} + detail()}>问题分析 +
    ; + } + })} +
    + + + { snippetsDetail.sources[index + 1] && (snippetsDetail.sources[index + 1].line - item.line > 1) && ......} + + ))} + +
    +
    + } +
    +} +export default issueDetail; \ No newline at end of file diff --git a/src/forge/Server/quality/detail.scss b/src/forge/Server/quality/detail.scss new file mode 100644 index 000000000..8a4a10dc8 --- /dev/null +++ b/src/forge/Server/quality/detail.scss @@ -0,0 +1,461 @@ +.bug-details { + display: flex; + max-height: 80vh; + .left-list { + overflow-y: auto; + width: 350px; + flex-shrink: 0; + .back-icon { + width: 24px; + } + .ant-divider { + padding-right: 10px; + width: 330px; + } + .load-more { + text-align: center; + padding: 4px; + margin-right: 10px; + margin-right: 10px; + background: #FAFCFF; + border-radius: 3px 3px 3px 3px; + border: 1px solid rgba(42,97,255,0.23); + font-size: 14px; + color: #111010; + cursor: pointer; + } + .doc-title { + display: flex; + align-items: center; + img { + width: 18px; + margin-right: 10px; + } + span { + display: inline-block; + width: 240px; + overflow: hidden; + white-space: nowrap; /* 防止文本换行 */ + overflow: hidden; /* 隐藏溢出的文本 */ + text-overflow: ellipsis; /* 显示省略号 */ + direction: rtl; /* 文字从右到左排列 */ + text-align: left; /* 文字从左边开始剪裁 */ + } + margin-top: 15px; + margin-bottom: 20px; + } + .issue { + padding: 15px 20px; + background: #FAFCFF; + border-radius: 3px 3px 3px 3px; + border: 1px solid rgba(42,97,255,0.23); + margin-bottom: 15px; + margin-right: 10px; + word-break: break-word; + font-family: PingFang SC, PingFang SC; + font-size: 14px; + color: #111010; + cursor: pointer; + &:hover { + border: 1px solid #466AFF; + } + .issue-typeline { + display: flex; + align-items: center; + img { + width: 14px; + margin-right: 5px; + } + } + .issue-type { + font-weight: 400; + color: #5D6790; + } + .issue-duplication { + margin-left: 5px; + padding: 0 8px; + background: #57575B; + border-radius: 4px 4px 4px 4px; + font-weight: 400; + color: #FFFFFF; + } + .repeat-item { + span { + border-radius: 2px 2px 2px 2px; + color: #FFFFFF; + } + span:nth-of-type(1) { + display: inline-block; + text-align: center; + line-height: 22px; + width: 22px; + height: 22px; + background: #C7797E; + } + span:nth-of-type(2) { + display: inline-block; + line-height: 22px; + padding: 0 7px; + margin-left: 9px; + height: 22px; + background: #A8A8A8; + } + } + .selected-repeat { + span:nth-of-type(1) { + background: #D4333F; + } + span:nth-of-type(2) { + background: #475760; + } + } + } + .selected-issue { + background: #FFFFFF; + border: 1px solid #2A61FF; + .issue-duplication { + background: #D4333F; + } + } + } + .snippets { + width: 100%; + overflow-y: auto; + .head { + width: 100%; + height: 42px; + background: #FAFCFF; + border: 1px solid #ECEEEF; + font-weight: bold; + font-size: 16px; + color: #111010; + padding-left: 20px; + line-height: 42px; + display: flex; + align-items: center; + img { + width: 18px; + margin-right: 10px; + } + } + .content { + display: flex; + border: 1px solid #ECEEEF; + tbody { + width: 100%; + } + tr:hover { + background: rgb(239, 242, 249); + } + .td-index { + width: 40px; + padding: 0 20px; + } + .td-code { + border-left: 1px solid rgb(221, 221, 221); + padding-left: 20px; + width: 100%; + .code { + position: relative; + white-space: pre-wrap; + overflow-wrap: anywhere; + tab-size: 4; + } + .issue-info { + padding: 15px 20px; + border-radius: 4px; + border: 1px solid rgb(197, 205, 223); + margin-right: 10px; + margin-bottom: 10px; + cursor: pointer; + word-break: break-word; + span { + font-weight: 400; + font-size: 14px; + color: #466AFF; + margin-left: 16px; + cursor: pointer; + } + } + .selected-issue { + border: 1px solid rgb(253, 162, 155); + } + .special { + text-decoration: underline 2px wavy rgb(253, 162, 155); + text-decoration-skip-ink: none; + } + .special1 { + background-color: rgba(253, 162, 155, 0.15); + cursor: pointer; + } + .selected-flow { + .special1 { + background-color: rgba(253, 162, 155, 0.5); + } + } + } + .td-ellipsis { + text-align: center; + border-top: 1px solid #ECEEEF; + border-bottom: 1px solid #ECEEEF; + } + } + .cd { + font-family: "Ubuntu Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-style: italic; + color: rgb(95, 96, 102); + } + .k { + font-family: "Ubuntu Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-weight: 700; + color: rgb(162, 34, 160); + } + .s { + color: rgb(36, 117, 35); + } + } +} +.issue-total { + .nodata { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + img { + width: 400px; + } + p { + margin-top: 20px; + font-size: 16px; + color: #788498; + } + } + .container { + background: #FFFFFF; + box-shadow: 0px 0px 6px 1px rgba(7,70,165,0.08); + border-radius: 4px 4px 4px 4px; + border: 2px solid #FFFFFF; + } + .count-title { + font-weight: bold; + font-size: 20px; + color: #333333; + margin: 28px 0; + } + .issue-list { + display: flex; + .show-more-list { + max-height: 2300px; + } + .show-less-list { + max-height: 1400px; + } + .left-content { + width: 218px; + flex-shrink: 0; + transition: max-height 0.4s; + overflow: hidden; + .clear-button { + margin: 17px 0 0 19px; + padding: 0 7px; + height: 28px; + border-radius: 3px 3px 3px 3px; + border: 1px solid #D4333F; + font-weight: 400; + font-size: 14px; + color: #D4333F; + cursor: pointer; + } + .filters { + .filter-title { + font-weight: bold; + font-size: 17px; + color: #000000; + padding: 20px; + line-height: 28px; + .clear-button { + float: right; + margin: 0; + } + } + .filter-item { + padding: 0 20px; + margin: 2px 0; + display: flex; + justify-content: space-between; + align-items: center; + height: 50px; + cursor: pointer; + &:hover { + background: #466bff0c; + } + .item-type { + display: flex; + align-items: center; + img { + width: 16px; + margin-right: 5px; + } + } + } + .selected-item { + background: #466bff17; + } + .ant-divider { + margin: 20px; + margin-bottom: 0px; + width: auto; + min-width: unset; + } + .show-more { + cursor: pointer; + font-size: 14px; + color: #5D6790; + margin-left: 20px; + margin-top: 4px; + } + } + } + .right-content { + margin-left: 17px; + flex-grow: 1; + padding: 20px; + padding-top: 10px; + .doc-title { + font-weight: bold; + font-size: 16px; + color: #111010; + margin: 19px 0; + } + .issue { + padding: 15px 20px; + background: #FAFCFF; + border-radius: 3px 3px 3px 3px; + border: 1px solid rgba(42,97,255,0.23); + margin-bottom: 12px; + .content-info { + font-weight: 400; + font-size: 14px; + color: #5D6790; + margin-top: 10px; + display: flex; + > span:nth-child(2), > span:nth-child(3) { + &::before { + content: "|"; + color: rgba(187,189,197,0.61); + margin-right: 13px; + margin-left: 13px; + } + } + span:nth-of-type(n + 4) { + margin-left: auto; + } + } + .issue-message { + font-weight: bold; + font-size: 15px; + color: #111010; + word-break: break-word; + span:nth-of-type(1) { + font-weight: 400; + font-size: 14px; + color: #466AFF; + margin-left: 16px; + cursor: pointer; + } + span:nth-of-type(2) { + float: right; + } + } + } + } + } + .total-info { + padding: 20px; + p { + font-weight: bold; + font-size: 18px; + color: #333333; + } + .info-list { + display: flex; + .total-item { + display: flex; + flex-direction: column; + min-height: 53px; + font-weight: 400; + font-size: 15px; + color: #525151; + margin-top: 18px; + > span:nth-child(2) { + font-weight: bold; + font-size: 18px; + color: #111010; + } + .item-more { + margin-left: 5px; + font-weight: 400; + font-size: 12px; + color: #525151; + span { + color: #466AFF; + } + } + } + .total-item:not(:last-child) { + margin-right: 40px; + } + .total-item:nth-child(n + 2) { + margin-left: 64px; + position: relative; + &::after { + position: absolute; + left: -40px; + content: ''; + width: 0px; + height: 100%; + border-left: 1px solid rgba(187,189,197,0.61); + } + } + } + } +} + +.headBox{ + height:60px; + line-height: 60px; + background-color:#fafcff; + border:1px solid rgba(42, 97, 255, 0.23); + border-radius:3px 3px 0px 0px; + color:#333333; + position: relative; +} + +.nullStoreBox{ + background-color:#fafcff; + border-radius:4px 4px 0px 0px; + text-align: center; + color:#333333; + padding-bottom: 65px; + .loBox{width: 68px;} + .introBox{ + color:#666666; + width: 57%; + margin: 15px auto; + } + .borBox{ + width: 45%; + margin: 0 auto 20px; + border-bottom: 1px solid rgba(90, 117, 193, 0.23); + } +} + +.icon-text { + display: flex; + align-items: center; + img { + width: 16px; + margin-right: 5px; + } +} \ No newline at end of file diff --git a/src/forge/Server/quality/index.jsx b/src/forge/Server/quality/index.jsx new file mode 100644 index 000000000..1b9c69efb --- /dev/null +++ b/src/forge/Server/quality/index.jsx @@ -0,0 +1,350 @@ +import React, { useState, useEffect, Fragment } from "react"; +import { getList, getProblemDetail, getProblemData, initSonar, startAlalyze, getActionList } from "../api"; +import './detail.scss'; +import ProblemDetail from "./component/ProblemDetail"; +import { issueType, maintainability, issueTypeIcon, maintainabilityIcon } from "../constants/quality"; +import { Divider, Pagination, Button, message, Spin } from 'antd'; +import { formatNumber, formatNumberWithCommas } from '../../Utils/utils'; +import logo from '../img/logo4-1.png'; +import labelIcon from '../img/quality/label.png'; +import code_lines from '../img/quality/code_lines.png'; +import repeat from '../img/quality/repeat.png'; +import cover from '../img/quality/cover.png'; +import nodata from '../../Images/nodata.png' +import { Link } from "react-router-dom"; + + +const actionStatus = { + success: 1, + failure: 2, + waiting: 5, + running: 6, + none: 7 +} + +function IssueList(props) { + const [issues, setIssues] = useState({}); + const [baseInfo, setBaseInfo] = useState({}); + const [baseData, setBaseData] = useState({}); + const [listFilter, setListFilter] = useState({}); + const [selectedIssue, setSelectedIssue] = useState(null); + const [detailVisible, setDetailVisible] = useState(true); + const [issueInfo, setIssueInfo] = useState({}); + const [selectedType, setSelectedType] = useState([]); + const [selectedSeverity, setSelectedSeverity] = useState([]); + const [selectedTag, setSelectedTag] = useState([]); + const [pageNum, setPageNum] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [loading, setLoading] = useState(false); + const [pageLoading, setPageLoading] = useState(true); + const [analysisStatus, setAnalysisStatus] = useState(null); + const [tagShowMore, setTagShowMore] = useState(false); + const [tagShowMoreDelay, setTagShowMoreDelay] = useState(false); + const { projectDetail, history } = props; + const metricKeys = 'lines,coverage,lines_to_cover,duplicated_lines_density,maintainability_issues,vulnerabilities,bugs,code_smells'; + const facets = 'impactSeverities,tags,types'; + const { owner , projectsId } = props.match.params; + + useEffect(() => { + let newContainer = document.getElementsByClassName("newContainer")[0]; + newContainer.style.backgroundColor = '#F7F9FCFF'; + return () => { + newContainer.style.backgroundColor = '#FFFFFF'; + }; + }, []); + + useEffect(() => { + if (projectDetail) { + const { author, name } = projectDetail; + document.title = `代码质量分析-${author.name}/${name}`; + setPageLoading(true) + getActionList(owner, projectsId).then(res => { + setPageLoading(false) + if (res.runs.length === 0) { + setAnalysisStatus(actionStatus.none) + } else { + setAnalysisStatus(res.runs[0].status) + if (res.runs[0].status) { + getData() + getIssueList() + } + } + }) + } + }, [projectDetail]); + + const getData = () => { + getProblemData(owner, projectsId, metricKeys).then(res => { + if (res.status === 0) { + const measures = res.component.measures; + const result = {}; + measures.forEach(measure => { + result[measure.metric] = measure.value; + }); + setBaseData(result); + } + }); + } + + const getIssueList = () => { + let params = { + facets: facets, + types: selectedType.join(','), + impactSeverities: selectedSeverity.join(','), + tags: selectedTag.join(','), + p: pageNum, + ps: pageSize + }; + setPageLoading(true) + getList(params, owner, projectsId).then(res => { + setPageLoading(false) + const issues = res.issues.reduce((acc, issue) => { + if (acc[issue.component]) { + acc[issue.component].push(issue); + } else { + acc[issue.component] = [issue]; + } + return acc; + }, {}); + + const filter = res.facets.reduce((acc, facet) => { + acc[facet.property] = facet.values; + return acc; + }, {}); + + setListFilter(filter); + setBaseInfo(res); + setIssues(issues); + }); + }; + + useEffect(() => { + if (pageNum === 1) { + getIssueList() + } else { + setPageNum(1) + } + }, [selectedType, selectedSeverity, selectedTag]); + + useEffect(() => { + if (projectDetail && analysisStatus === actionStatus.success) { + getIssueList() + } + }, [pageNum, pageSize]); + + useEffect(() => { + if (!tagShowMore) { + setTimeout(() => { + setTagShowMoreDelay(tagShowMore) + }, 450); + } else { + setTagShowMoreDelay(tagShowMore) + } + }, [tagShowMore]) + + const detail = (e) => { + setSelectedIssue(e); + getProblemDetail(e.rule, owner, projectsId).then(res => { + setIssueInfo(res?.rule); + setDetailVisible(true); + }); + }; + + const toggleSelection = (state, setter, value) => { + window.scrollTo(0,0); + const index = state.indexOf(value); + if (index > -1) { + state.splice(index, 1); + setter([...state]); + } else { + setter([...state, value]); + } + }; + + const clearAllSelection = () => { + setSelectedType([]) + setSelectedSeverity([]) + setSelectedTag([]) + } + + // 筛选列表 + const renderFilterItems = (items, state, setter, label, showMoreButton = false) => { + const displayedItems = showMoreButton && items && !tagShowMoreDelay ? items.slice(0, 10) : items; + + return (
    +
    + {label} + { state.length > 0 && } +
    + {displayedItems && displayedItems.map(e => ( +
    toggleSelection(state, setter, e.val)} + > + {label === "问题类型" ? issueType[e.val] : maintainability[e.val] || e.val} + {e.count} +
    + ))} + { showMoreButton && items.length > 10 && { setTagShowMore(!tagShowMore) }}> { tagShowMore ? '收起' : '显示更多'} } + +
    ) + }; + + // 总览项 + const renderTotalItem = (label, value, icon) => ( +
    + {label} + {value} +
    + ); + + // 总览项(带代码行数) + const renderDetailedItem = (label, value, percent, moreValue, icon) => ( +
    + {label} + + {percent}% + {moreValue}{formatNumber(value)} + +
    + ); + + const toDetail = (e) => { + history.push({ + pathname: `/${owner}/${projectsId}/service/quality/detail`, + state: { + baseInfo, + selectedIssue: e + } + }) + } + + const openAnalysis = () => { + setLoading(true) + initSonar(true, owner, projectsId).then(res => { + if (res.status === 0) { + startAlalyze(projectDetail.default_branch, owner, projectsId).then(res1=> { + if (res1.status === 0) { + message.success('创建分析成功,请等待分析完成') + setAnalysisStatus(actionStatus.waiting) + } + setLoading(false) + }) + } else { + setLoading(false) + } + }) + } + + // 开始按钮状态 + const getStatusButton = () => { + switch (analysisStatus) { + case actionStatus.none: + return + case actionStatus.waiting: + case actionStatus.running: + return + case actionStatus.failure: + return
    +

    分析失败,请重试

    + +
    + default: + return <> + } + } + + return ( + +
    + setDetailVisible(false)} + /> + { + analysisStatus !== actionStatus.success && +
    sonar代码质量分析工具
    +
    + +

    欢迎使用代码质量分析工具

    +
    代码质量分析工具通过静态代码分析帮助开发者识别和修复代码库潜在的缺陷、代码异味、复杂度过高的代码段以及潜在的安全漏洞。同时能够评估代码的结构和风格,提供关于代码覆盖率、重复率等关键指标的深入洞察。
    +
    + { + getStatusButton() + } +
    +
    + } + { + analysisStatus === actionStatus.success &&
    +
    +

    代码分析结果

    +
    + {renderTotalItem("Bugs", baseData.bugs, issueTypeIcon.BUG)} + {renderTotalItem("漏洞", formatNumber(baseData.vulnerabilities), issueTypeIcon.VULNERABILITY)} + {renderTotalItem("异味", formatNumber(baseData.code_smells), issueTypeIcon.CODE_SMELL)} + {renderDetailedItem("重复率", baseData.lines || '-', baseData.duplicated_lines_density || '-', "代码", repeat)} + {renderDetailedItem("覆盖率", baseData.lines_to_cover || '-', baseData.coverage || '-', "代码", cover)} + {renderTotalItem("代码行数", formatNumberWithCommas(baseData.lines || '-'), code_lines)} +
    +
    + {issues &&
    发现{ baseData.maintainability_issues && JSON.parse(baseData.maintainability_issues).total }个问题
    } +
    +
    + { (selectedType.length > 0 || selectedSeverity.length > 0 || selectedTag .length > 0) && } + {renderFilterItems(listFilter.types, selectedType, setSelectedType, "问题类型")} + {renderFilterItems(listFilter.impactSeverities, selectedSeverity, setSelectedSeverity, "严重程度")} + {renderFilterItems(listFilter.tags, selectedTag, setSelectedTag, "标签", true)} +
    +
    + {issues && baseInfo.total > 0 && Object.entries(issues).map(([key, item]) => ( +
    +
    + {key.split(':')[1]} +
    + {item.map(e => ( +
    toDetail(e)} + > +

    + {e.message} + { ele.stopPropagation();detail(e)}}>问题分析 + L{e.line} +

    +

    + {issueType[e.type]} + {maintainability[e.impacts[0].severity]} + {e.debt}工作 + + {e.tags && e.tags.length > 0 && } + {e.tags && e.tags.map((tag, index) => ( + + {tag}{index < e.tags.length - 1 ? ',' : ''} + + ))} + +

    +
    + ))} +
    + ))} + { baseInfo.total > 0 && setPageNum(e)} onShowSizeChange={(current, size) => setPageSize(size)} /> } + {!baseInfo.total &&
    + +

    暂无问题

    +
    } +
    +
    +
    + } +
    +
    + ); +} + +export default IssueList; diff --git a/src/forge/Server/reposyncer/component/storeList.jsx b/src/forge/Server/reposyncer/component/storeList.jsx index 0580435ef..86992e01b 100644 --- a/src/forge/Server/reposyncer/component/storeList.jsx +++ b/src/forge/Server/reposyncer/component/storeList.jsx @@ -2,7 +2,7 @@ import React, { Fragment, useEffect, useState} from "react"; import { Button, Modal, Icon, message, Select, Input, Form, Radio, Table, Divider, Badge, Spin, Checkbox } from "antd"; import { Link } from "react-router-dom"; import gitlinkLogo from '../../img/gitlink.jpg'; -import logo from '../../img/logo3.png'; +import logo from '../../img/logo3-1.png'; import '../index.scss'; import axios from "axios"; import AddTaskModal from './addTaskModal'; diff --git a/src/forge/Utils/utils.js b/src/forge/Utils/utils.js new file mode 100644 index 000000000..085bfd5c6 --- /dev/null +++ b/src/forge/Utils/utils.js @@ -0,0 +1,16 @@ +export function formatNumber(num) { + if (num === undefined || num === null) return + if (num >= 1e9) { + return (num / 1e9).toFixed(1) + 'B'; // 十亿 + } else if (num >= 1e6) { + return (num / 1e6).toFixed(1) + 'M'; // 百万 + } else if (num >= 1e3) { + return (num / 1e3).toFixed(1) + 'K'; // 千 + } else { + return num.toString(); // 小于 1000 的数字原样返回 + } +} +export function formatNumberWithCommas(num) { + if (num === undefined || num === null) return + return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); +} \ No newline at end of file diff --git a/src/services/project.js b/src/services/project.js index 4dc0eb859..fa81bc0b5 100644 --- a/src/services/project.js +++ b/src/services/project.js @@ -7,6 +7,7 @@ let baseZoneUrl = `${zoneUrl}/api` if (__CLIENT__ ) { let settings = localStorage.chromesetting && localStorage.chromesetting !== 'undefined' &&JSON.parse(localStorage.chromesetting); baseUrl = settings ? `${settings && settings.common.main_site_url}/api` : '/api'; + // baseUrl = settings ? `http://172.20.32.202:4000/api` : '/api'; baseZoneUrl = settings ? `${settings && settings.common.zone}/api` : `${zoneUrl}/api`; } @@ -39,7 +40,7 @@ export async function getProjectFunc (owner, projectsId) { export async function getProjectDetailFunc (owner, projectsId) { setHead() - const url = `${baseUrl}/${owner}/${projectsId}/detail.json`; + const url = `${baseUrl}/${owner}/${projectsId}/detail.json?debug=true`; return axiosInstance.get(url); }