forked from Gitlink/forgeplus-react
523 lines
20 KiB
JavaScript
523 lines
20 KiB
JavaScript
import React, { useState, useEffect, Fragment, useRef } from "react";
|
||
import { getList, getProblemDetail, getProblemData, initSonar, startAlalyze, getActionList, getAnalizeUrl } from "../api";
|
||
import './detail.scss';
|
||
import ProblemDetail from "./component/ProblemDetail";
|
||
import { issueType, maintainability, issueTypeIcon, maintainabilityIcon, labelIcon } 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 code_lines from '../img/quality/code_lines.svg';
|
||
import repeat from '../img/quality/repeat.svg';
|
||
import cover from '../img/quality/cover.svg';
|
||
import nodata from '../../Images/nodata.png'
|
||
import arrow from '../img/quality/triangular-arrow.png'
|
||
import files from '../img/quality/files.svg'
|
||
import classes from '../img/quality/class.svg'
|
||
import funcs from '../img/quality/funcs.svg'
|
||
import rightArrow from '../img/quality/right-arrow.svg'
|
||
import { ReactSVG } from 'react-svg'
|
||
import folderImg from '../img/quality/folder.png'
|
||
import CallTable from "./component/CallTable";
|
||
|
||
const actionStatus = {
|
||
success: 1,
|
||
failure: 2,
|
||
waiting: 5,
|
||
running: 6,
|
||
none: 7
|
||
}
|
||
|
||
const codeAnalyze = {
|
||
classes: 1,
|
||
files: 2,
|
||
func: 3
|
||
}
|
||
|
||
const analizeIcon = {
|
||
[codeAnalyze.classes]: classes,
|
||
[codeAnalyze.files]: files,
|
||
[codeAnalyze.func]: funcs
|
||
}
|
||
|
||
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 [codeSelected, setCodeSelected] = useState(false);
|
||
const [url, setUrl] = useState(null);
|
||
const { projectDetail, history, changeClass } = 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;
|
||
const iframeRef = useRef(null);
|
||
const timeRef = useRef();
|
||
|
||
useEffect(() => {
|
||
if (projectDetail) {
|
||
const { author, name } = projectDetail;
|
||
document.title = `代码质量分析-${author.name}/${name}`;
|
||
if ( !projectDetail.has_actions ) {
|
||
setAnalysisStatus(actionStatus.none)
|
||
return
|
||
}
|
||
setPageLoading(true)
|
||
getAction()
|
||
}
|
||
}, [projectDetail]);
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
clearTimer()
|
||
}
|
||
},[])
|
||
|
||
useEffect(() => {
|
||
const iframe = iframeRef.current;
|
||
if (!iframe || !url) return; // 确保 iframe 和 url 已存在
|
||
|
||
const handleLoad = () => {
|
||
try {
|
||
const iframeDocument = iframe.contentWindow?.document;
|
||
if (iframeDocument) {
|
||
const contentHeight = Math.max(
|
||
iframeDocument.body.scrollHeight, iframeDocument.body.offsetHeight,
|
||
iframeDocument.documentElement.clientHeight, iframeDocument.documentElement.scrollHeight, iframeDocument.documentElement.offsetHeight
|
||
);
|
||
iframe.style.height = contentHeight + 'px';
|
||
}
|
||
} catch (e) {
|
||
iframe.style.height = `${document.getElementsByClassName('issue-list')[0] ? document.getElementsByClassName('right-content')[0].clientHeight : document.body.clientHeight }px`;
|
||
console.error("调整同源 iframe 大小出错 (或者它实际是跨域的):", e);
|
||
}
|
||
};
|
||
|
||
iframe.addEventListener('load', handleLoad);
|
||
// 可选: 如果 iframe 内部尺寸变化,重新计算 (更复杂)
|
||
|
||
return () => {
|
||
iframe.removeEventListener('load', handleLoad);
|
||
};
|
||
}, [url]); // 如果 URL 改变,重新绑定监听器
|
||
|
||
const clearTimer = () => {
|
||
if (timeRef.current) {
|
||
clearTimeout(timeRef.current);
|
||
timeRef.current = undefined; // 清空引用
|
||
}
|
||
}
|
||
|
||
const getAction = () => {
|
||
getActionList(owner, projectsId).then(res => {
|
||
setPageLoading(false)
|
||
if (res.status !== 0) {
|
||
if (res.runs.length === 0) {
|
||
setAnalysisStatus(actionStatus.none)
|
||
} else {
|
||
if (res.runs[0].status === actionStatus.success) {
|
||
changeClass(true)
|
||
}
|
||
if ([actionStatus.waiting, actionStatus.running].includes(res.runs[0].status)) {
|
||
if (timeRef.current) clearTimer();
|
||
timeRef.current = setTimeout(() => {
|
||
getAction()
|
||
}, 3000);
|
||
}
|
||
setAnalysisStatus(res.runs[0].status)
|
||
if (res.runs[0].status) {
|
||
getData()
|
||
getIssueList()
|
||
}
|
||
}
|
||
} else {
|
||
message.error(res.message)
|
||
}
|
||
})
|
||
}
|
||
|
||
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 = () => {
|
||
const isBLOCKED = selectedSeverity.join(',') === 'BLOCKED' //手动添加阻塞,当只选BLOCKED时筛选为空
|
||
let params = {
|
||
facets: facets,
|
||
types: selectedType.join(','),
|
||
impactSeverities: selectedSeverity.join(',').replace('BLOCKED', ''),
|
||
tags: selectedTag.join(','),
|
||
p: pageNum,
|
||
ps: pageSize
|
||
};
|
||
if (!timeRef.current) {
|
||
setPageLoading(true)
|
||
}
|
||
getList(params, owner, projectsId).then(res => {
|
||
setPageLoading(false)
|
||
const issues = res.issues && !isBLOCKED ? 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 ? res.facets.reduce((acc, facet) => {
|
||
if (facet.property === 'impactSeverities') {
|
||
facet.values.unshift({
|
||
count: 0,
|
||
val: "BLOCKED"
|
||
})
|
||
}
|
||
|
||
if (isBLOCKED && facet.property !== 'impactSeverities') {
|
||
if (facet.property === 'tags') facet.values = []
|
||
facet.values.map(e => {
|
||
e.count = 0
|
||
})
|
||
}
|
||
|
||
acc[facet.property] = facet.values;
|
||
return acc;
|
||
}, {}) : [];
|
||
|
||
if (isBLOCKED) res.total = 0
|
||
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) => {
|
||
setCodeSelected(undefined)
|
||
setUrl(undefined)
|
||
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, 8) : 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, index) => (
|
||
<div
|
||
className={`filter-item${state.includes(e.val) ? ' selected-item' : ''}`}
|
||
key={e.val}
|
||
onClick={() => toggleSelection(state, setter, e.val)}
|
||
>
|
||
<span className="item-type">
|
||
{
|
||
label === "缺陷预警提示" ? <img className="type-icon" src={ maintainabilityIcon[e.val] } alt="" /> :
|
||
<ReactSVG className="type-icon" src={ label === "代码缺陷及风格检查类型" ? issueTypeIcon[e.val] : labelIcon[index % 5] } />
|
||
}
|
||
{label === "代码缺陷及风格检查类型" ? issueType[e.val] : maintainability[e.val] || e.val}
|
||
</span>
|
||
<span>{e.count}</span>
|
||
</div>
|
||
))}
|
||
{ showMoreButton && items && 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="item-text"> <img src={ icon } alt="" /> {label}</span>
|
||
<span className="ml20 item-value">{value}</span>
|
||
</div>
|
||
);
|
||
|
||
// 总览项(带代码行数)
|
||
const renderDetailedItem = (label, value, percent, moreValue, icon) => (
|
||
<div className="total-item">
|
||
<span className="item-text"> <img src={ icon} alt="" /> {label}</span>
|
||
<span className="item-more">({moreValue}<span>{formatNumber(value)}</span>行)</span>
|
||
<span className="item-value">
|
||
{percent}%
|
||
</span>
|
||
</div>
|
||
);
|
||
|
||
const renderAnalizeItem = (label, type) => {
|
||
return <div
|
||
className={`filter-item${codeSelected === type ? ' selected-item' : ''}`}
|
||
onClick={() => toIframe(type)}
|
||
>
|
||
<span className="item-type"> <ReactSVG className="type-icon" src={analizeIcon[type]} />{ label }</span>
|
||
<img className="type-icon" src={rightArrow} alt="" />
|
||
</div>
|
||
}
|
||
|
||
const toDetail = (e) => {
|
||
history.push({
|
||
pathname: `/${owner}/${projectsId}/service/quality/detail`,
|
||
state: {
|
||
baseInfo,
|
||
selectedIssue: e
|
||
}
|
||
})
|
||
}
|
||
|
||
const toIframe = (type) => {
|
||
setCodeSelected(type)
|
||
clearAllSelection()
|
||
getAnalizeUrl(owner, projectsId).then(res => {
|
||
if (res.doxygen_url) {
|
||
if (type !== codeAnalyze.func) {
|
||
setUrl(`${ res.doxygen_url }/${ type === codeAnalyze.files ? 'files' : 'annotated' }.html`)
|
||
}
|
||
}
|
||
})
|
||
|
||
}
|
||
|
||
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)
|
||
if (timeRef.current) clearTimer();
|
||
timeRef.current = setTimeout(() => {
|
||
getAction()
|
||
}, 3000);
|
||
}
|
||
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 loading>代码正在分析中,请稍候</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="page-quality">
|
||
{
|
||
analysisStatus === actionStatus.success && <div className="quality-title">
|
||
<span>代码质量静态扫描</span>
|
||
<span>护航研发效能</span>
|
||
</div>
|
||
}
|
||
|
||
<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">
|
||
<div className="sub-container">
|
||
<p>软件质量统计结果,全方位精准定位问题</p>
|
||
<p>深度剖析代码细节,精准定位潜在问题,助力团队提升开发效率,打造高质量软件,为项目稳定运行保驾护航,开启高效开发新时代。</p>
|
||
<img className="triangular-arrow" src={ arrow } alt="" />
|
||
<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 className="issue-list">
|
||
{/* 左边筛选框 */}
|
||
<div className={`left-content ${tagShowMore ? 'show-more-list' : 'show-less-list'}`}>
|
||
<div className="count">
|
||
{issues && <div className="count-title">共{ baseData.maintainability_issues && JSON.parse(baseData.maintainability_issues).total }个问题</div>}
|
||
{ (selectedType.length > 0 || selectedSeverity.length > 0 || selectedTag .length > 0) && <button className="clear-button" onClick={clearAllSelection}>清空条件</button>}
|
||
</div>
|
||
{renderFilterItems(listFilter.types, selectedType, setSelectedType, "代码缺陷及风格检查类型")}
|
||
{renderFilterItems(listFilter.impactSeverities, selectedSeverity, setSelectedSeverity, "缺陷预警提示")}
|
||
{/* 代码执行分析功能固定写死 */}
|
||
<div className={`filters`}>
|
||
<div className="filter-title">
|
||
<span>代码执行分析功能</span>
|
||
</div>
|
||
{ renderAnalizeItem('classes类溯源', codeAnalyze.classes) }
|
||
{ renderAnalizeItem('files文件溯源', codeAnalyze.files) }
|
||
{ renderAnalizeItem('调用请求量', codeAnalyze.func) }
|
||
<Divider dashed />
|
||
</div>
|
||
{renderFilterItems(listFilter.tags, selectedTag, setSelectedTag, "标签", true)}
|
||
</div>
|
||
{/* 右边详情 */}
|
||
<div className="right-content">
|
||
{
|
||
!codeSelected && <div className={`content-warap ${ issues && baseInfo.total > 0 && 'has-issues' }`}>
|
||
{issues && baseInfo.total > 0 && Object.entries(issues).map(([key, item]) => (
|
||
<div key={key}>
|
||
<div className="doc-title" title={key.split(':')[1]}>
|
||
<img src={ folderImg } alt="" />
|
||
{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 && <ReactSVG className="svg-icon" src={labelIcon[0]} />}
|
||
{e.tags && e.tags.map((tag, index) => (
|
||
<span key={tag}>
|
||
{tag}{index < e.tags.length - 1 ? ',' : ''}
|
||
</span>
|
||
))}
|
||
</span>
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
))}
|
||
{!baseInfo.total && <div className="nodata">
|
||
<img src={nodata} alt="" />
|
||
<p>暂无问题</p>
|
||
</div>}
|
||
</div>
|
||
}
|
||
{
|
||
codeSelected && (
|
||
codeSelected !== codeAnalyze.func ? <iframe
|
||
className="iframe"
|
||
ref={iframeRef}
|
||
src={url}
|
||
style={{ width: '100%', border: 'none' }} // 高度将由 JS 设置
|
||
scrolling="no"
|
||
title="代码质量扫描结果"
|
||
></iframe> : <CallTable {...props}></CallTable>
|
||
)
|
||
}
|
||
{ !codeSelected && baseInfo.total > 0 && <Pagination showSizeChanger showQuickJumper current={pageNum} total={baseInfo.total} onChange={e => setPageNum(e)} onShowSizeChange={(current, size) => setPageSize(size)} /> }
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
</Spin>
|
||
);
|
||
}
|
||
|
||
export default IssueList;
|