Merge pull request 'sonar' (#747) from Eeeros/forgeplus-react:feature_sonar into feature_sonar

1
This commit is contained in:
Eeeros 2024-06-20 09:13:39 +08:00
commit 60ee84c8d4
33 changed files with 1292 additions and 5 deletions

View File

@ -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",

View File

@ -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";
//响应前的设置

View File

@ -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({

View File

@ -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){
() => (<Data {...props}/>)
}
></Route>
<Route path="/:owner/:projectsId/service/quality/detail"
render={
() => (<QualityDetail {...props}/>)
}
></Route>
<Route path="/:owner/:projectsId/service/quality"
render={
() => (<Quality {...props}/>)
}
></Route>
{/* 个人建站服务 */}
{projectDetail && projectDetail.web_site && projectDetail.author.type === "User" && <Route path="/:owner/:projectsId/service/pages"
render={

View File

@ -64,6 +64,16 @@ function Main(props){
<Link to={`/${owner}/${projectsId}/service/reposyncer`} className="btnhover">查看详情</Link>
</span>
</li>
<li>
<div className="servername" style={{marginBottom: '8px'}}>
<img src={require('./img/logo4.png')} alt="" style={{width: '24px'}}/>
<Link to={`/${owner}/${projectsId}/service/quality`}>代码质量分析工具</Link>
</div>
<p className="task-hide-2 serverdesc">提供自动化的静态代码检查能力能够识别代码中的缺陷安全漏洞代码异味同时产出代码覆盖率和重复度的详细报告</p>
<span className="serverbtn">
<Link to={`/${owner}/${projectsId}/service/quality`} className="btnhover">查看详情</Link>
</span>
</li>
{/* 是站点仓库则显示,否则隐藏 */}
{projectDetail && projectDetail.web_site && projectDetail.author.type === "User" && <li>
<span className="servername">

69
src/forge/Server/api.js Normal file
View File

@ -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',
});
}

View File

@ -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
}

10
src/forge/Server/fetch.js Normal file
View File

@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

View File

@ -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 && <Drawer
placement="bottom"
visible={visible}
closable={false}
onClose={onClose}
height={460}
maskStyle={{backgroundColor:'rgba(0,0,0,0.09)'}}
>
<div className="drawer-head">
{ issue.message }
<img src={close} alt="" onClick={onClose}/>
</div>
<div className="drawer-content">
<p className="content-title">
{ issue.message }
<span>{ issue.rule }</span>
</p>
<p className="content-info">
<span>{ issueDetail.type }</span>
<span>{ issueDetail.severity }</span>
<span>生效时间 { moment(issue.creationDate).format('YYYY年MM月DD日') }</span>
<span>{ issueDetail.langName }</span>
</p>
<Divider dashed />
<div className="content-detail" dangerouslySetInnerHTML={{ __html: issueDetail.htmlDesc }}></div>
</div>
</Drawer>
)
}
export default ProblemDetail;

View File

@ -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;
}
}
}

View File

@ -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 = `<span class="${className}">${specialText}</span>`;
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 <div className="bug-details">
<ProblemDetail visible={detailVisible} issue={selectedIssue} issueDetail={issueInfo} onClose={() => setDetailVisible(false)}></ProblemDetail>
<div className="left-list">
<img className="back-icon pointer" src={ backImg } alt="" onClick={() => history.goBack()}/>
<Divider dashed />
{
issues && Object.entries(issues).map(([key, item]) => <div key={ key }>
<div className="doc-title" title={ key.split(`:`)[1] }><img src={ folderImg } alt="" /><span>{ key.split(`:`)[1] }</span></div>
{
item.map(e => (
<div key={ e.key } className={`issue ${ e.key === selectedIssue?.key ? 'selected-issue' : '' }`} onClick={() => setSelectedIssue(e)}>
<p>{ e.message }</p>
<p className="issue-typeline"><img src={issueTypeIcon[e.type]} alt="" /><span className="issue-type">{ issueType[e.type] }</span>{ e.flows.length > 0 && <span className="issue-duplication">+{e.flows.length} </span> }</p>
{
e.key === selectedIssue?.key && e.flows && e.flows.length > 0 && e.flows.map((flow, index) => {
return <div className={`repeat-item ${ flow.locations[0].textRange.startLine === selectedFlow ? 'selected-repeat' : '' }`} key={index} onClick={ e => selectAndJumpRepeat(flow.locations[0].textRange.startLine) }>
<span>{ index }</span>
<span>{ flow.locations[0].msg }</span>
</div>
})
}
</div>
))
}
</div>
)
}
{ pageNum * pageSize < total && <div className="load-more" onClick={() => setPageNum(pageNum + 1)}>查看更多</div> }
</div>
{
snippetsDetail && <div className="snippets" ref={ codeRef }>
<div className="head"><img src={ folderImg } alt="" />{ snippetsDetail.component.path }</div>
<div className="content">
<tbody>
{snippetsDetail.sources.map((item, index) => (
<><tr key={index} id={ `line-${ item.line }` }>
<td className="td-index">{item.line}</td>
<td className="td-code">
<div>
<pre dangerouslySetInnerHTML={{ __html: item.code }} className={`code ${ selectedFlow === item.line ? 'selected-flow' : '' }`} onClick={e => selectRepeat(e, item.line)}/>
{issues[selectedIssue.component] && issues[selectedIssue.component].map(e => {
if (e.line === item.line) {
return <div className={`issue-info ${selectedIssue.key === item.issue?.key ? 'selected-issue' : ''}`} onClick={() => setSelectedIssue(e)}>
{e.message}
<span onClick={() => detail()}>问题分析</span>
</div>;
}
})}
</div>
</td>
</tr>
{ snippetsDetail.sources[index + 1] && (snippetsDetail.sources[index + 1].line - item.line > 1) && <tr><td colSpan={2} className="td-ellipsis">......</td></tr>}
</>
))}
</tbody>
</div>
</div>
}
</div>
}
export default issueDetail;

View File

@ -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;
}
}

View File

@ -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 (<div className={`filters`}>
<div className="filter-title">
<span>{label}</span>
{ state.length > 0 && <button className="clear-button" onClick={() => setter([])}>清空</button> }
</div>
{displayedItems && displayedItems.map(e => (
<div
className={`filter-item${state.includes(e.val) ? ' selected-item' : ''}`}
key={e.val}
onClick={() => toggleSelection(state, setter, e.val)}
>
<span className="item-type"> <img src={ label === "问题类型" ? issueTypeIcon[e.val] : maintainabilityIcon[e.val] || labelIcon } alt="" /> {label === "问题类型" ? issueType[e.val] : maintainability[e.val] || e.val}</span>
<span>{e.count}</span>
</div>
))}
{ showMoreButton && items.length > 10 && <span className="show-more" onClick={() => { setTagShowMore(!tagShowMore) }}> { tagShowMore ? '收起' : '显示更多'} </span> }
<Divider dashed />
</div>)
};
//
const renderTotalItem = (label, value, icon) => (
<div className="total-item">
<span className="icon-text"> <img src={ icon} alt="" /> {label}</span>
<span className="ml20">{value}</span>
</div>
);
//
const renderDetailedItem = (label, value, percent, moreValue, icon) => (
<div className="total-item">
<span className="icon-text"> <img src={ icon} alt="" /> {label}</span>
<span className="ml20">
{percent}%
<span className="item-more">{moreValue}<span>{formatNumber(value)}</span></span>
</span>
</div>
);
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 <Button type="primary" style={{width: '112px', height: '36px'}} onClick={openAnalysis} loading={loading}>开始体验</Button>
case actionStatus.waiting:
case actionStatus.running:
return <Button type="primary" style={{height: '36px'}} disabled>代码正在分析中请稍候</Button>
case actionStatus.failure:
return <div>
<p>分析失败请重试</p>
<Button type="primary" style={{width: '112px', height: '36px'}} onClick={openAnalysis} loading={loading}>开始分析</Button>
</div>
default:
return <></>
}
}
return (
<Spin spinning={pageLoading}>
<div className="issue-total">
<ProblemDetail
visible={detailVisible}
issue={selectedIssue}
issueDetail={issueInfo}
onClose={() => setDetailVisible(false)}
/>
{
analysisStatus !== actionStatus.success && <Fragment>
<div className="headBox font-16 pl15">sonar代码质量分析工具</div>
<div className="nullStoreBox mt25">
<img src={logo} alt="" className="loBox mt50"/>
<p className="font-22 mt10">欢迎使用代码质量分析工具</p>
<div className="introBox font-15">代码质量分析工具通过静态代码分析帮助开发者识别和修复代码库潜在的缺陷代码异味复杂度过高的代码段以及潜在的安全漏洞同时能够评估代码的结构和风格提供关于代码覆盖率重复率等关键指标的深入洞察</div>
<div className="borBox"></div>
{
getStatusButton()
}
</div>
</Fragment>
}
{
analysisStatus === actionStatus.success && <div>
<div className="container total-info">
<p>代码分析结果</p>
<div className="info-list">
{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)}
</div>
</div>
{issues && <div className="count-title">发现{ baseData.maintainability_issues && JSON.parse(baseData.maintainability_issues).total }个问题</div>}
<div className="issue-list">
<div className={`container left-content ${tagShowMore ? 'show-more-list' : 'show-less-list'}`}>
{ (selectedType.length > 0 || selectedSeverity.length > 0 || selectedTag .length > 0) && <button className="clear-button" onClick={clearAllSelection}>清空所有条件</button>}
{renderFilterItems(listFilter.types, selectedType, setSelectedType, "问题类型")}
{renderFilterItems(listFilter.impactSeverities, selectedSeverity, setSelectedSeverity, "严重程度")}
{renderFilterItems(listFilter.tags, selectedTag, setSelectedTag, "标签", true)}
</div>
<div className="container right-content">
{issues && baseInfo.total > 0 && Object.entries(issues).map(([key, item]) => (
<div key={key}>
<div className="doc-title" title={key.split(':')[1]}>
{key.split(':')[1]}
</div>
{item.map(e => (
<div
key={e.key}
className={`issue`}
onClick={() => toDetail(e)}
>
<p className="issue-message">
{e.message}
<span onClick={(ele) => { ele.stopPropagation();detail(e)}}>问题分析</span>
<span>L{e.line}</span>
</p>
<p className="content-info">
<span className="icon-text"> <img src={ issueTypeIcon[e.type]} alt="" /> {issueType[e.type]}</span>
<span className="icon-text"> <img src={ maintainabilityIcon[e.impacts[0].severity] } alt="" /> {maintainability[e.impacts[0].severity]}</span>
<span>{e.debt}工作</span>
<span className="icon-text">
{e.tags && e.tags.length > 0 && <img src={ labelIcon } alt="" />}
{e.tags && e.tags.map((tag, index) => (
<span key={tag}>
{tag}{index < e.tags.length - 1 ? ',' : ''}
</span>
))}
</span>
</p>
</div>
))}
</div>
))}
{ baseInfo.total > 0 && <Pagination showSizeChanger showQuickJumper current={pageNum} total={baseInfo.total} onChange={e => setPageNum(e)} onShowSizeChange={(current, size) => setPageSize(size)} /> }
{!baseInfo.total && <div className="nodata">
<img src={nodata} alt="" />
<p>暂无问题</p>
</div>}
</div>
</div>
</div>
}
</div>
</Spin>
);
}
export default IssueList;

View File

@ -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';

16
src/forge/Utils/utils.js Normal file
View File

@ -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, ",");
}

View File

@ -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);
}