cont.
|
|
@ -28,6 +28,12 @@ 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;
|
||||
|
|
@ -52,7 +58,12 @@ function ServerIndex(props){
|
|||
() => (<Data {...props}/>)
|
||||
}
|
||||
></Route>
|
||||
<Route path="/:owner/:projectsId/service/test"
|
||||
<Route path="/:owner/:projectsId/service/quality/detail"
|
||||
render={
|
||||
() => (<QualityDetail {...props}/>)
|
||||
}
|
||||
></Route>
|
||||
<Route path="/:owner/:projectsId/service/quality"
|
||||
render={
|
||||
() => (<Quality {...props}/>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ function Main(props){
|
|||
}
|
||||
}, [projectDetail])
|
||||
|
||||
useEffect(()=>{
|
||||
if(current_user && !current_user.login){
|
||||
props.history.push(`/login?go_page=/${owner}/${projectsId}/service`);
|
||||
}else{
|
||||
setHas_trace_user(current_user.has_trace_user);
|
||||
}
|
||||
},[current_user])
|
||||
// useEffect(()=>{
|
||||
// if(current_user && !current_user.login){
|
||||
// props.history.push(`/login?go_page=/${owner}/${projectsId}/service`);
|
||||
// }else{
|
||||
// setHas_trace_user(current_user.has_trace_user);
|
||||
// }
|
||||
// },[current_user])
|
||||
|
||||
function onOk(){
|
||||
setVisible(false);
|
||||
|
|
@ -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/logo3.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">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import fetch from './fetch';
|
||||
|
||||
// 获取列表
|
||||
export function getList(params) {
|
||||
export function getList(params, owner, repo) {
|
||||
return fetch({
|
||||
url: `/api/issues/search?components=kingchanx-fluid-cloudnative_fluid&s=FILE_LINE&issueStatuses=CONFIRMED%2COPEN&ps=100&&additionalFields=_all&timeZone=Asia%2FShanghai`,
|
||||
// url: `/api/v1/${owner}/${repo}/sonarqubes/issues_search.json`,
|
||||
url: `/api/issues/search?components=kingchanx-fluid-cloudnative_fluid&s=FILE_LINE&issueStatuses=CONFIRMED%2COPEN&&additionalFields=_all&timeZone=Asia%2FShanghai`,
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
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: '漏洞',
|
||||
|
|
@ -13,4 +20,16 @@ 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
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 654 B |
|
After Width: | Height: | Size: 397 B |
|
After Width: | Height: | Size: 370 B |
|
After Width: | Height: | Size: 468 B |
|
After Width: | Height: | Size: 641 B |
|
After Width: | Height: | Size: 390 B |
|
After Width: | Height: | Size: 417 B |
|
After Width: | Height: | Size: 270 B |
|
After Width: | Height: | Size: 363 B |
|
After Width: | Height: | Size: 339 B |
|
After Width: | Height: | Size: 439 B |
|
After Width: | Height: | Size: 337 B |
|
|
@ -2,40 +2,40 @@ import React, {useState, useEffect, useRef } from "react";
|
|||
import { getList, getCodeSnippets, getProblemDetail } from "../api";
|
||||
import './detail.scss';
|
||||
import ProblemDetail from "./component/ProblemDetail";
|
||||
import { issueType } from "../constants/quality"
|
||||
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(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 { projectDetail } = props;
|
||||
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){
|
||||
const { author, name} = projectDetail;
|
||||
document.title = `reposyncer-${author.name}/${name}`;
|
||||
if (history.location.state?.baseInfo) {
|
||||
handleIssues(history.location.state?.baseInfo)
|
||||
} else {
|
||||
getIssueList()
|
||||
}
|
||||
if (history.location.state?.selectedIssue) {
|
||||
setSelectedIssue(history.location.state?.selectedIssue)
|
||||
}
|
||||
}
|
||||
getList().then(res => {
|
||||
let issues = res.issues
|
||||
let issuesHandled = {}
|
||||
issues.forEach(e => {
|
||||
if (issuesHandled[e.component]) {
|
||||
issuesHandled[e.component].push(e)
|
||||
} else {
|
||||
issuesHandled[e.component] = [e]
|
||||
}
|
||||
});
|
||||
setIssues(issuesHandled)
|
||||
})
|
||||
}, [projectDetail])
|
||||
}, [projectDetail, pageNum])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIssue) {
|
||||
|
|
@ -49,7 +49,31 @@ function issueDetail(props){
|
|||
useEffect(()=>{
|
||||
},[reload])
|
||||
|
||||
function addSpecialClassToRange(code, textRange, className) {
|
||||
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;
|
||||
|
|
@ -145,14 +169,16 @@ function issueDetail(props){
|
|||
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] }>{ key.split(`:`)[1] }</div>
|
||||
<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><span className="issue-type">{ issueType[e.type] }</span>{ e.flows.length > 0 && <span className="issue-duplication">+{e.flows.length} </span> }</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) }>
|
||||
|
|
@ -167,10 +193,11 @@ function issueDetail(props){
|
|||
</div>
|
||||
)
|
||||
}
|
||||
{ pageNum * pageSize < total && <div className="load-more" onClick={() => setPageNum(pageNum + 1)}>查看更多</div> }
|
||||
</div>
|
||||
{
|
||||
snippetsDetail && <div className="snippets" ref={ codeRef }>
|
||||
<div className="head">{ snippetsDetail.component.path }</div>
|
||||
<div className="head"><img src={ folderImg } alt="" />{ snippetsDetail.component.path }</div>
|
||||
<div className="content">
|
||||
<tbody>
|
||||
{snippetsDetail.sources.map((item, index) => (
|
||||
|
|
|
|||
|
|
@ -5,14 +5,42 @@
|
|||
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 {
|
||||
width: 250px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap; /* 防止文本换行 */
|
||||
overflow: hidden; /* 隐藏溢出的文本 */
|
||||
text-overflow: ellipsis; /* 显示省略号 */
|
||||
direction: rtl; /* 文字从右到左排列 */
|
||||
text-align: left; /* 文字从左边开始剪裁 */
|
||||
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;
|
||||
}
|
||||
|
|
@ -23,7 +51,6 @@
|
|||
border: 1px solid rgba(42,97,255,0.23);
|
||||
margin-bottom: 15px;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
word-break: break-word;
|
||||
font-family: PingFang SC, PingFang SC;
|
||||
font-size: 14px;
|
||||
|
|
@ -32,6 +59,14 @@
|
|||
&: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;
|
||||
|
|
@ -96,6 +131,12 @@
|
|||
color: #111010;
|
||||
padding-left: 20px;
|
||||
line-height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
img {
|
||||
width: 18px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
|
|
@ -174,36 +215,69 @@
|
|||
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;
|
||||
.left-content {
|
||||
width: 218px;
|
||||
flex-shrink: 0;
|
||||
.filter-title {
|
||||
font-weight: bold;
|
||||
font-size: 17px;
|
||||
color: #000000;
|
||||
padding: 20px;
|
||||
}
|
||||
.filter-item {
|
||||
padding: 0 20px;
|
||||
margin: 2px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
.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;
|
||||
&:hover {
|
||||
background: #466bff0c;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
.selected-item {
|
||||
background: #466bff17;
|
||||
}
|
||||
.ant-divider {
|
||||
margin: 20px;
|
||||
width: auto;
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
.right-content {
|
||||
|
|
@ -228,6 +302,7 @@
|
|||
font-size: 14px;
|
||||
color: #5D6790;
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
> span:nth-child(2), > span:nth-child(3) {
|
||||
&::before {
|
||||
content: "|";
|
||||
|
|
@ -237,7 +312,7 @@
|
|||
}
|
||||
}
|
||||
span:nth-of-type(n + 4) {
|
||||
float: right;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
.issue-message {
|
||||
|
|
@ -261,6 +336,11 @@
|
|||
}
|
||||
.total-info {
|
||||
padding: 20px;
|
||||
p {
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
color: #333333;
|
||||
}
|
||||
.info-list {
|
||||
display: flex;
|
||||
.total-item {
|
||||
|
|
@ -270,6 +350,7 @@
|
|||
font-weight: 400;
|
||||
font-size: 15px;
|
||||
color: #525151;
|
||||
margin-top: 18px;
|
||||
> span:nth-child(2) {
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
|
|
@ -295,10 +376,48 @@
|
|||
left: -40px;
|
||||
content: '';
|
||||
width: 0px;
|
||||
height: 53px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +1,48 @@
|
|||
import React, {useState, useEffect, useRef } from "react";
|
||||
import React, { useState, useEffect, Fragment } from "react";
|
||||
import { getList, getCodeSnippets, getProblemDetail, getProblemData } from "../api";
|
||||
import './detail.scss';
|
||||
import ProblemDetail from "./component/ProblemDetail";
|
||||
import { issueType, issueTypeKeys, maintainability } from "../constants/quality"
|
||||
import { Divider } from 'antd';
|
||||
import { formatNumber } from '../../Utils/utils'
|
||||
import { issueType, maintainability, issueTypeIcon, maintainabilityIcon } from "../constants/quality";
|
||||
import { Divider, Pagination, Button } 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 { Link } from "react-router-dom";
|
||||
|
||||
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 { 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;
|
||||
|
||||
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 { projectDetail } = props;
|
||||
const metricKeys = 'lines,coverage,lines_to_cover,duplicated_lines_density,maintainability_issues,vulnerabilities,bugs,code_smells'
|
||||
const facets = 'impactSeverities,tags,types'
|
||||
useEffect(() => {
|
||||
let newContainer = document.getElementsByClassName("newContainer")[0]
|
||||
newContainer.style.backgroundColor = '#F7F9FCFF'
|
||||
let newContainer = document.getElementsByClassName("newContainer")[0];
|
||||
newContainer.style.backgroundColor = '#F7F9FCFF';
|
||||
return () => {
|
||||
newContainer.style.backgroundColor = '#FFFFFF'
|
||||
}
|
||||
},[])
|
||||
newContainer.style.backgroundColor = '#FFFFFF';
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
// 更新网页标题
|
||||
if(projectDetail){
|
||||
const { author, name} = projectDetail;
|
||||
document.title = `reposyncer-${author.name}/${name}`;
|
||||
useEffect(() => {
|
||||
if (projectDetail) {
|
||||
const { author, name } = projectDetail;
|
||||
document.title = `代码质量分析-${author.name}/${name}`;
|
||||
getIssueList(author.name, name);
|
||||
}
|
||||
getProblemData('kingchanx-fluid-cloudnative_fluid', metricKeys).then(res => {
|
||||
const measures = res.component.measures;
|
||||
|
|
@ -40,181 +50,195 @@ function issueList(props){
|
|||
measures.forEach(measure => {
|
||||
result[measure.metric] = measure.value;
|
||||
});
|
||||
setBaseData(result)
|
||||
})
|
||||
getIssueList()
|
||||
}, [projectDetail])
|
||||
setBaseData(result);
|
||||
});
|
||||
}, [projectDetail]);
|
||||
|
||||
function getIssueList() {
|
||||
const getIssueList = (owner, name) => {
|
||||
let params = {
|
||||
facets: facets,
|
||||
types: selectedType.join(','),
|
||||
impactSeverities: selectedSeverity.join(','),
|
||||
tags: selectedTag.join(','),
|
||||
}
|
||||
getList(params).then(res => {
|
||||
let issues = res.issues
|
||||
let issuesHandled = {}
|
||||
issues.forEach(e => {
|
||||
if (issuesHandled[e.component]) {
|
||||
issuesHandled[e.component].push(e)
|
||||
p: pageNum,
|
||||
ps: pageSize
|
||||
};
|
||||
getList(params, owner, name).then(res => {
|
||||
const issues = res.issues.reduce((acc, issue) => {
|
||||
if (acc[issue.component]) {
|
||||
acc[issue.component].push(issue);
|
||||
} else {
|
||||
issuesHandled[e.component] = [e]
|
||||
acc[issue.component] = [issue];
|
||||
}
|
||||
});
|
||||
const filter = {}
|
||||
res.facets.forEach(e => {
|
||||
filter[e.property] = e.values
|
||||
})
|
||||
setListFilter(filter)
|
||||
setBaseInfo(res)
|
||||
setIssues(issuesHandled)
|
||||
})
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const filter = res.facets.reduce((acc, facet) => {
|
||||
acc[facet.property] = facet.values;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
setListFilter(filter);
|
||||
setBaseInfo(res);
|
||||
setIssues(issues);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getIssueList()
|
||||
},[selectedType, selectedSeverity, selectedTag])
|
||||
setPageNum(1)
|
||||
}, [selectedType, selectedSeverity, selectedTag]);
|
||||
|
||||
function detail(e) {
|
||||
setSelectedIssue(e)
|
||||
useEffect(() => {
|
||||
getIssueList();
|
||||
}, [pageNum, pageSize]);
|
||||
|
||||
const detail = (e) => {
|
||||
setSelectedIssue(e);
|
||||
getProblemDetail(e.rule).then(res => {
|
||||
setIssueInfo(res?.rule)
|
||||
setDetailVisible(true)
|
||||
setIssueInfo(res?.rule);
|
||||
setDetailVisible(true);
|
||||
});
|
||||
};
|
||||
|
||||
const toggleSelection = (state, setter, value) => {
|
||||
const index = state.indexOf(value);
|
||||
if (index > -1) {
|
||||
state.splice(index, 1);
|
||||
setter([...state]);
|
||||
} else {
|
||||
setter([...state, value]);
|
||||
}
|
||||
};
|
||||
|
||||
const renderFilterItems = (items, state, setter, label) => (
|
||||
<div className="filters">
|
||||
<div className="filter-title">
|
||||
<span>{label}</span>
|
||||
{ state.length > 0 && <button className="clear-button" onClick={() => setter([])}>清空</button> }
|
||||
</div>
|
||||
{items && items.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>
|
||||
))}
|
||||
<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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function selectType(e) {
|
||||
let index = selectedType.indexOf(e)
|
||||
if (index > -1) {
|
||||
selectedType.splice(index, 1)
|
||||
setSelectedType([...selectedType])
|
||||
} else {
|
||||
setSelectedType([...selectedType, e])
|
||||
}
|
||||
}
|
||||
|
||||
function selectSeverity(e) {
|
||||
let index = selectedSeverity.indexOf(e)
|
||||
if (index > -1) {
|
||||
selectedSeverity.splice(index, 1)
|
||||
setSelectedSeverity([...selectedSeverity])
|
||||
} else {
|
||||
setSelectedSeverity([...selectedSeverity, e])
|
||||
}
|
||||
}
|
||||
|
||||
function selectTags(e) {
|
||||
let index = selectedTag.indexOf(e)
|
||||
if (index > -1) {
|
||||
selectedTag.splice(index, 1)
|
||||
setSelectedTag([...selectedTag])
|
||||
} else {
|
||||
setSelectedTag([...selectedTag, e])
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="issue-total">
|
||||
<ProblemDetail visible={detailVisible} issue={selectedIssue} issueDetail={issueInfo} onClose={() => setDetailVisible(false)}></ProblemDetail>
|
||||
<div className="container total-info">
|
||||
<p>代码分析结果</p>
|
||||
<div className="info-list">
|
||||
<div className="total-item">
|
||||
<span>Bugs</span>
|
||||
<span>{ baseData.bugs }</span>
|
||||
</div>
|
||||
<div className="total-item">
|
||||
<span>漏洞</span>
|
||||
<span>{ formatNumber(baseData.vulnerabilities) }</span>
|
||||
</div>
|
||||
<div className="total-item">
|
||||
<span>异味</span>
|
||||
<span>{ formatNumber(baseData.code_smells) }</span>
|
||||
</div>
|
||||
<div className="total-item">
|
||||
<span>重复率</span>
|
||||
<span>{ baseData.duplicated_lines_density }% <span className="item-more">代码<span>{ formatNumber(baseData.lines) }</span>行</span></span>
|
||||
</div>
|
||||
<div className="total-item">
|
||||
<span>覆盖率</span>
|
||||
<span>{ baseData.coverage }% <span className="item-more">代码<span>{ formatNumber(baseData.lines_to_cover) }</span>行</span></span>
|
||||
</div>
|
||||
<div className="total-item">
|
||||
<span>代码行数</span>
|
||||
<span>{ baseData.lines && baseData.lines.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{issues && <p>发现{ baseInfo.total }个问题</p>}
|
||||
<div className="issue-list">
|
||||
<div className="container left-content">
|
||||
<div className="type">
|
||||
<div className="filter-title">
|
||||
<span>问题类型 {}</span>
|
||||
return (
|
||||
<div className="issue-total">
|
||||
<ProblemDetail
|
||||
visible={detailVisible}
|
||||
issue={selectedIssue}
|
||||
issueDetail={issueInfo}
|
||||
onClose={() => setDetailVisible(false)}
|
||||
/>
|
||||
{
|
||||
false && <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>
|
||||
<Button type="primary" style={{width: '112px', height: '36px'}}>开始体验</Button>
|
||||
</div>
|
||||
{
|
||||
listFilter.types && listFilter.types.map(e => (
|
||||
<div className={`filter-item${ selectedType.includes(e.val) ? ' selected-item' : '' }`} key={e.val} onClick={() => selectType(e.val)}>
|
||||
<span>{ issueType[e.val] }</span>
|
||||
<span>{ e.count }</span>
|
||||
</div>))
|
||||
}
|
||||
<Divider dashed />
|
||||
</div>
|
||||
<div className="severity">
|
||||
<div className="filter-title">
|
||||
<span>严重程度</span>
|
||||
</div>
|
||||
{
|
||||
listFilter.impactSeverities && listFilter.impactSeverities.map(e => (
|
||||
<div className={`filter-item${ selectedSeverity.includes(e.val) ? ' selected-item' : '' }`} key={e.val} onClick={() => selectSeverity(e.val)}>
|
||||
<span>{ maintainability[e.val] }</span>
|
||||
<span>{ e.count }</span>
|
||||
</div>))
|
||||
}
|
||||
<Divider dashed />
|
||||
</div>
|
||||
<div className="tags">
|
||||
<div className="filter-title">
|
||||
<span>标签</span>
|
||||
</div>
|
||||
{
|
||||
listFilter.tags && listFilter.tags.map(e => (
|
||||
<div className={`filter-item${ selectedTag.includes(e.val) ? ' selected-item' : '' }`} key={e.val} onClick={() => selectTags(e.val)}>
|
||||
<span>{ e.val }</span>
|
||||
<span>{ e.count }</span>
|
||||
</div>))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="container right-content">
|
||||
{
|
||||
issues && 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 ${ e.key === selectedIssue?.key ? 'selected-issue' : '' }`} onClick={() => setSelectedIssue(e)}>
|
||||
<p className="issue-message">{ e.message } <span onClick={() => detail(e)}>问题分析</span><span>L{ e.line }</span></p>
|
||||
<p className="content-info">
|
||||
<span>{ issueType[e.type] }</span>
|
||||
<span>{ maintainability[e.impacts[0].severity] }</span>
|
||||
<span>{ e.debt }工作</span>
|
||||
<span>
|
||||
{
|
||||
e.tags && e.tags.map((tag, index) => (
|
||||
<span key={tag}>{ tag }{ index < e.tags.length - 1 ? ',' : '' }</span>
|
||||
))
|
||||
}
|
||||
</span>
|
||||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
{
|
||||
true && <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>
|
||||
))
|
||||
}
|
||||
</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">
|
||||
{ (selectedType.length > 0 || selectedSeverity.length > 0 || selectedTag .length > 0) && <button className="clear-button" onClick={() => setter([])}>清空所有条件</button>}
|
||||
{renderFilterItems(listFilter.types, selectedType, setSelectedType, "问题类型")}
|
||||
{renderFilterItems(listFilter.impactSeverities, selectedSeverity, setSelectedSeverity, "严重程度")}
|
||||
{renderFilterItems(listFilter.tags, selectedTag, setSelectedTag, "标签")}
|
||||
</div>
|
||||
<div className="container right-content">
|
||||
{issues && 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={() => 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>
|
||||
))}
|
||||
<Pagination showSizeChanger showQuickJumper current={pageNum} total={baseInfo.total} onChange={e => setPageNum(e)} onShowSizeChange={(current, size) => setPageSize(size)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default issueList;
|
||||
|
||||
export default IssueList;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -9,4 +9,8 @@ export function formatNumber(num) {
|
|||
} else {
|
||||
return num.toString(); // 小于 1000 的数字原样返回
|
||||
}
|
||||
}
|
||||
export function formatNumberWithCommas(num) {
|
||||
if (num === undefined || num === null) return
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||