@@ -17,6 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { FacetValue } from '../app/types'; | |||
import { getJSON, post, postJSON, RequestData } from '../helpers/request'; | |||
import { RawIssue } from '../helpers/issues'; | |||
@@ -30,7 +31,10 @@ export interface IssueResponse { | |||
interface IssuesResponse { | |||
components?: { key: string; name: string; uuid: string }[]; | |||
debtTotal?: number; | |||
facets: Array<{}>; | |||
facets: Array<{ | |||
property: string; | |||
values: { count: number; val: string }[]; | |||
}>; | |||
issues: RawIssue[]; | |||
paging: { | |||
pageIndex: number; | |||
@@ -45,7 +49,13 @@ export function searchIssues(query: RequestData): Promise<IssuesResponse> { | |||
return getJSON('/api/issues/search', query); | |||
} | |||
export function getFacets(query: RequestData, facets: string[]): Promise<any> { | |||
export function getFacets( | |||
query: RequestData, | |||
facets: string[] | |||
): Promise<{ | |||
facets: Array<{ property: string; values: FacetValue[] }>; | |||
response: IssuesResponse; | |||
}> { | |||
const data = { | |||
...query, | |||
facets: facets.join(), |
@@ -18,21 +18,21 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
import { Paging, TestCase, CoveredFile } from '../app/types'; | |||
import { getJSON } from '../helpers/request'; | |||
export interface GetTestsParameters { | |||
export function getTests(parameters: { | |||
branch?: string; | |||
p?: number; | |||
ps?: number; | |||
sourceFileKey?: string; | |||
sourceFileLineNumber?: number; | |||
testFileKey: string; | |||
} | |||
export function getTests(parameters: GetTestsParameters) { | |||
testId?: string; | |||
}): Promise<{ paging: Paging; tests: TestCase[] }> { | |||
return getJSON('/api/tests/list', parameters).catch(throwGlobalError); | |||
} | |||
export interface GetCoveredFilesParameters { | |||
testId: string; | |||
} | |||
export function getCoveredFiles(parameters: GetCoveredFilesParameters) { | |||
return getJSON('/api/tests/covered_files', parameters).catch(throwGlobalError); | |||
export function getCoveredFiles(data: { testId: string }): Promise<CoveredFile[]> { | |||
return getJSON('/api/tests/covered_files', data).then(r => r.files, throwGlobalError); | |||
} |
@@ -331,3 +331,46 @@ export interface PermissionTemplate { | |||
withProjectCreator?: boolean; | |||
}>; | |||
} | |||
export interface TestCase { | |||
coveredLines: number; | |||
durationInMs: number; | |||
fileId: string; | |||
fileKey: string; | |||
fileName: string; | |||
id: string; | |||
message?: string; | |||
name: string; | |||
stacktrace?: string; | |||
status: string; | |||
} | |||
export interface CoveredFile { | |||
key: string; | |||
longName: string; | |||
coveredLines: number; | |||
} | |||
export interface FacetValue { | |||
count: number; | |||
val: string; | |||
} | |||
export interface SourceViewerFile { | |||
canMarkAsFavorite?: boolean; | |||
key: string; | |||
measures: { | |||
coverage?: string; | |||
duplicationDensity?: string; | |||
issues?: string; | |||
lines?: string; | |||
tests?: string; | |||
}; | |||
path: string; | |||
project: string; | |||
projectName: string; | |||
q: string; | |||
subProject?: string; | |||
subProjectName?: string; | |||
uuid: string; | |||
} |
@@ -28,6 +28,7 @@ import ScreenPositionHelper from '../../../components/common/ScreenPositionHelpe | |||
import { hasBubbleChart, parseQuery, serializeQuery } from '../utils'; | |||
import { getBranchName } from '../../../helpers/branches'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getDisplayMetrics } from '../../../helpers/measures'; | |||
/*:: import type { Component, Query, Period } from '../types'; */ | |||
/*:: import type { RawQuery } from '../../../helpers/query'; */ | |||
/*:: import type { Metric } from '../../../store/metrics/actions'; */ | |||
@@ -106,11 +107,9 @@ export default class App extends React.PureComponent { | |||
} | |||
} | |||
fetchMeasures = ({ branch, component, fetchMeasures, metrics, metricsKey } /*: Props */) => { | |||
fetchMeasures = ({ branch, component, fetchMeasures, metrics } /*: Props */) => { | |||
this.setState({ loading: true }); | |||
const filteredKeys = metricsKey.filter( | |||
key => !metrics[key].hidden && !['DATA', 'DISTRIB'].includes(metrics[key].type) | |||
); | |||
const filteredKeys = getDisplayMetrics(Object.values(metrics)).map(metric => metric.key); | |||
fetchMeasures(component.key, filteredKeys, getBranchName(branch)).then( | |||
({ measures, leakPeriod }) => { | |||
if (this.mounted) { |
@@ -27,7 +27,6 @@ import CoveragePopupView from './popups/coverage-popup'; | |||
import DuplicationPopupView from './popups/duplication-popup'; | |||
import LineActionsPopupView from './popups/line-actions-popup'; | |||
import SCMPopupView from './popups/scm-popup'; | |||
import MeasuresOverlay from './views/measures-overlay'; | |||
import loadIssues from './helpers/loadIssues'; | |||
import getCoverageStatus from './helpers/getCoverageStatus'; | |||
import { | |||
@@ -464,15 +463,6 @@ export default class SourceViewerBase extends React.PureComponent { | |||
}); | |||
}; | |||
showMeasures = () => { | |||
const measuresOverlay = new MeasuresOverlay({ | |||
branch: this.props.branch, | |||
component: this.state.component, | |||
large: true | |||
}); | |||
measuresOverlay.render(); | |||
}; | |||
handleCoverageClick = (line /*: SourceLine */, element /*: HTMLElement */) => { | |||
getTests(this.props.component, line.line, this.props.branch).then(tests => { | |||
const popup = new CoveragePopupView({ | |||
@@ -691,11 +681,7 @@ export default class SourceViewerBase extends React.PureComponent { | |||
return ( | |||
<div className={className} ref={node => (this.node = node)}> | |||
<SourceViewerHeader | |||
branch={this.props.branch} | |||
component={this.state.component} | |||
showMeasures={this.showMeasures} | |||
/> | |||
<SourceViewerHeader branch={this.props.branch} sourceViewerFile={this.state.component} /> | |||
{sourceRemoved && ( | |||
<div className="alert alert-warning spacer-top"> | |||
{translate('code_viewer.no_source_code_displayed_due_to_source_removed')} |
@@ -17,49 +17,46 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import MeasuresOverlay from './components/MeasuresOverlay'; | |||
import { SourceViewerFile } from '../../app/types'; | |||
import QualifierIcon from '../shared/QualifierIcon'; | |||
import FavoriteContainer from '../controls/FavoriteContainer'; | |||
import { getPathUrlAsString, getProjectUrl, getComponentIssuesUrl } from '../../helpers/urls'; | |||
import { | |||
getPathUrlAsString, | |||
getProjectUrl, | |||
getComponentIssuesUrl, | |||
getBaseUrl | |||
} from '../../helpers/urls'; | |||
import { collapsedDirFromPath, fileFromPath } from '../../helpers/path'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { formatMeasure } from '../../helpers/measures'; | |||
export default class SourceViewerHeader extends React.PureComponent { | |||
/*:: props: { | |||
branch?: string, | |||
component: { | |||
canMarkAsFavorite: boolean, | |||
key: string, | |||
measures: { | |||
coverage?: string, | |||
duplicationDensity?: string, | |||
issues?: string, | |||
lines?: string, | |||
tests?: string | |||
}, | |||
path: string, | |||
project: string, | |||
projectName: string, | |||
q: string, | |||
subProject?: string, | |||
subProjectName?: string, | |||
uuid: string | |||
}, | |||
showMeasures: () => void | |||
interface Props { | |||
branch: string | undefined; | |||
sourceViewerFile: SourceViewerFile; | |||
} | |||
interface State { | |||
measuresOverlay: boolean; | |||
} | |||
export default class SourceViewerHeader extends React.PureComponent<Props, State> { | |||
state: State = { measuresOverlay: false }; | |||
handleShowMeasuresClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
this.setState({ measuresOverlay: true }); | |||
}; | |||
*/ | |||
showMeasures = (e /*: SyntheticInputEvent */) => { | |||
e.preventDefault(); | |||
this.props.showMeasures(); | |||
handleMeasuresOverlayClose = () => { | |||
this.setState({ measuresOverlay: false }); | |||
}; | |||
openInWorkspace = (e /*: SyntheticInputEvent */) => { | |||
e.preventDefault(); | |||
const { key } = this.props.component; | |||
openInWorkspace = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
const { key } = this.props.sourceViewerFile; | |||
const Workspace = require('../workspace/main').default; | |||
Workspace.openComponent({ key, branch: this.props.branch }); | |||
}; | |||
@@ -75,11 +72,11 @@ export default class SourceViewerHeader extends React.PureComponent { | |||
subProject, | |||
subProjectName, | |||
uuid | |||
} = this.props.component; | |||
} = this.props.sourceViewerFile; | |||
const isUnitTest = q === 'UTS'; | |||
const workspace = false; | |||
let rawSourcesLink = | |||
window.baseUrl + `/api/sources/raw?key=${encodeURIComponent(this.props.component.key)}`; | |||
getBaseUrl() + `/api/sources/raw?key=${encodeURIComponent(this.props.sourceViewerFile.key)}`; | |||
if (this.props.branch) { | |||
rawSourcesLink += `&branch=${encodeURIComponent(this.props.branch)}`; | |||
} | |||
@@ -91,8 +88,8 @@ export default class SourceViewerHeader extends React.PureComponent { | |||
<div className="component-name"> | |||
<div className="component-name-parent"> | |||
<a | |||
href={getPathUrlAsString(getProjectUrl(project, this.props.branch))} | |||
className="link-with-icon"> | |||
className="link-with-icon" | |||
href={getPathUrlAsString(getProjectUrl(project, this.props.branch))}> | |||
<QualifierIcon qualifier="TRK" /> <span>{projectName}</span> | |||
</a> | |||
</div> | |||
@@ -100,8 +97,8 @@ export default class SourceViewerHeader extends React.PureComponent { | |||
{subProject != null && ( | |||
<div className="component-name-parent"> | |||
<a | |||
href={getPathUrlAsString(getProjectUrl(subProject, this.props.branch))} | |||
className="link-with-icon"> | |||
className="link-with-icon" | |||
href={getPathUrlAsString(getProjectUrl(subProject, this.props.branch))}> | |||
<QualifierIcon qualifier="BRC" /> <span>{subProjectName}</span> | |||
</a> | |||
</div> | |||
@@ -110,7 +107,7 @@ export default class SourceViewerHeader extends React.PureComponent { | |||
<div className="component-name-path"> | |||
<QualifierIcon qualifier={q} /> <span>{collapsedDirFromPath(path)}</span> | |||
<span className="component-name-file">{fileFromPath(path)}</span> | |||
{this.props.component.canMarkAsFavorite && ( | |||
{this.props.sourceViewerFile.canMarkAsFavorite && ( | |||
<FavoriteContainer className="component-name-favorite" componentKey={key} /> | |||
)} | |||
</div> | |||
@@ -125,18 +122,25 @@ export default class SourceViewerHeader extends React.PureComponent { | |||
/> | |||
<ul className="dropdown-menu dropdown-menu-right"> | |||
<li> | |||
<a className="js-measures" href="#" onClick={this.showMeasures}> | |||
<a className="js-measures" href="#" onClick={this.handleShowMeasuresClick}> | |||
{translate('component_viewer.show_details')} | |||
</a> | |||
{this.state.measuresOverlay && ( | |||
<MeasuresOverlay | |||
branch={this.props.branch} | |||
onClose={this.handleMeasuresOverlayClose} | |||
sourceViewerFile={this.props.sourceViewerFile} | |||
/> | |||
)} | |||
</li> | |||
<li> | |||
<a | |||
className="js-new-window" | |||
target="_blank" | |||
href={getPathUrlAsString({ | |||
pathname: '/component', | |||
query: { branch: this.props.branch, id: this.props.component.key } | |||
})}> | |||
query: { branch: this.props.branch, id: this.props.sourceViewerFile.key } | |||
})} | |||
target="_blank"> | |||
{translate('component_viewer.new_window')} | |||
</a> | |||
</li> |
@@ -0,0 +1,459 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import { keyBy, sortBy, groupBy } from 'lodash'; | |||
import MeasuresOverlayMeasure from './MeasuresOverlayMeasure'; | |||
import MeasuresOverlayTestCases from './MeasuresOverlayTestCases'; | |||
import { getFacets } from '../../../api/issues'; | |||
import { getMeasures } from '../../../api/measures'; | |||
import { getAllMetrics } from '../../../api/metrics'; | |||
import { FacetValue, SourceViewerFile } from '../../../app/types'; | |||
import Modal from '../../controls/Modal'; | |||
import Measure from '../../measure/Measure'; | |||
import QualifierIcon from '../../shared/QualifierIcon'; | |||
import SeverityHelper from '../../shared/SeverityHelper'; | |||
import CoverageRating from '../../ui/CoverageRating'; | |||
import DuplicationsRating from '../../ui/DuplicationsRating'; | |||
import IssueTypeIcon from '../../ui/IssueTypeIcon'; | |||
import { SEVERITIES, TYPES } from '../../../helpers/constants'; | |||
import { translate, getLocalizedMetricName } from '../../../helpers/l10n'; | |||
import { | |||
formatMeasure, | |||
MeasureEnhanced, | |||
getDisplayMetrics, | |||
enhanceMeasuresWithMetrics | |||
} from '../../../helpers/measures'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
interface Props { | |||
branch: string | undefined; | |||
onClose: () => void; | |||
sourceViewerFile: SourceViewerFile; | |||
} | |||
interface Measures { | |||
[metricKey: string]: MeasureEnhanced; | |||
} | |||
interface State { | |||
loading: boolean; | |||
measures: Measures; | |||
severitiesFacet?: FacetValue[]; | |||
showAllMeasures: boolean; | |||
tagsFacet?: FacetValue[]; | |||
typesFacet?: FacetValue[]; | |||
} | |||
export default class MeasuresOverlay extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { loading: true, measures: {}, showAllMeasures: false }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchData(); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
fetchData = () => { | |||
Promise.all([this.fetchMeasures(), this.fetchIssues()]).then( | |||
([measures, facets]) => { | |||
if (this.mounted) { | |||
this.setState({ loading: false, measures, ...facets }); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
); | |||
}; | |||
fetchMeasures = () => { | |||
return getAllMetrics().then(metrics => { | |||
const metricKeys = getDisplayMetrics(metrics).map(metric => metric.key); | |||
// eslint-disable-next-line promise/no-nesting | |||
return getMeasures(this.props.sourceViewerFile.key, metricKeys, this.props.branch).then( | |||
measures => { | |||
const withMetrics = enhanceMeasuresWithMetrics(measures, metrics).filter( | |||
measure => measure.metric | |||
); | |||
return keyBy(withMetrics, measure => measure.metric.key); | |||
} | |||
); | |||
}); | |||
}; | |||
fetchIssues = () => { | |||
return getFacets( | |||
{ | |||
branch: this.props.branch, | |||
componentKeys: this.props.sourceViewerFile.key, | |||
resolved: 'false' | |||
}, | |||
['types', 'severities', 'tags'] | |||
).then(({ facets }) => { | |||
const severitiesFacet = facets.find(f => f.property === 'severities'); | |||
const tagsFacet = facets.find(f => f.property === 'tags'); | |||
const typesFacet = facets.find(f => f.property === 'types'); | |||
return { | |||
severitiesFacet: severitiesFacet && severitiesFacet.values, | |||
tagsFacet: tagsFacet && tagsFacet.values, | |||
typesFacet: typesFacet && typesFacet.values | |||
}; | |||
}); | |||
}; | |||
handleCloseClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.props.onClose(); | |||
}; | |||
handleAllMeasuresClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.setState({ showAllMeasures: true }); | |||
}; | |||
renderMeasure = (measure: MeasureEnhanced | undefined) => { | |||
return measure ? <MeasuresOverlayMeasure key={measure.metric.key} measure={measure} /> : null; | |||
}; | |||
renderLines = () => { | |||
const { measures } = this.state; | |||
return ( | |||
<div className="source-viewer-measures-section"> | |||
<div className="source-viewer-measures-card"> | |||
<div className="measures"> | |||
<div className="measures-list"> | |||
{this.renderMeasure(measures.lines)} | |||
{this.renderMeasure(measures.ncloc)} | |||
{this.renderMeasure(measures.comment_lines)} | |||
{this.renderMeasure(measures.comment_lines_density)} | |||
</div> | |||
</div> | |||
<div className="measures"> | |||
<div className="measures-list"> | |||
{this.renderMeasure(measures.cognitive_complexity)} | |||
{this.renderMeasure(measures.complexity)} | |||
{this.renderMeasure(measures.function_complexity)} | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
}; | |||
renderBigMeasure = (measure: MeasureEnhanced | undefined) => { | |||
return measure ? ( | |||
<div className="measure measure-big" data-metric={measure.metric.key}> | |||
<span className="measure-value"> | |||
<Measure | |||
metricKey={measure.metric.key} | |||
metricType={measure.metric.type} | |||
value={measure.value} | |||
/> | |||
</span> | |||
<span className="measure-name">{getLocalizedMetricName(measure.metric, true)}</span> | |||
</div> | |||
) : null; | |||
}; | |||
renderIssues = () => { | |||
const { measures, severitiesFacet, tagsFacet, typesFacet } = this.state; | |||
return ( | |||
<div className="source-viewer-measures-section"> | |||
<div className="source-viewer-measures-card"> | |||
<div className="measures"> | |||
{this.renderBigMeasure(measures.violations)} | |||
{this.renderBigMeasure(measures.sqale_index)} | |||
</div> | |||
{measures.violations && | |||
!measures.violations.value && ( | |||
<> | |||
{typesFacet && ( | |||
<div className="measures"> | |||
<div className="measures-list"> | |||
{sortBy(typesFacet, f => TYPES.indexOf(f.val)).map(f => ( | |||
<div className="measure measure-one-line" key={f.val}> | |||
<span className="measure-name"> | |||
<IssueTypeIcon className="little-spacer-right" query={f.val} /> | |||
{translate('issue.type', f.val)} | |||
</span> | |||
<span className="measure-value"> | |||
{formatMeasure(f.count, 'SHORT_INT')} | |||
</span> | |||
</div> | |||
))} | |||
</div> | |||
</div> | |||
)} | |||
{severitiesFacet && ( | |||
<div className="measures"> | |||
<div className="measures-list"> | |||
{sortBy(severitiesFacet, f => SEVERITIES.indexOf(f.val)).map(f => ( | |||
<div className="measure measure-one-line" key={f.val}> | |||
<span className="measure-name"> | |||
<SeverityHelper severity={f.val} /> | |||
</span> | |||
<span className="measure-value"> | |||
{formatMeasure(f.count, 'SHORT_INT')} | |||
</span> | |||
</div> | |||
))} | |||
</div> | |||
</div> | |||
)} | |||
{tagsFacet && ( | |||
<div className="measures"> | |||
<div className="measures-list"> | |||
{tagsFacet.map(f => ( | |||
<div className="measure measure-one-line" key={f.val}> | |||
<span className="measure-name"> | |||
<i className="icon-tags little-spacer-right" /> | |||
{f.val} | |||
</span> | |||
<span className="measure-value"> | |||
{formatMeasure(f.count, 'SHORT_INT')} | |||
</span> | |||
</div> | |||
))} | |||
</div> | |||
</div> | |||
)} | |||
</> | |||
)} | |||
</div> | |||
</div> | |||
); | |||
}; | |||
renderCoverage = () => { | |||
const { coverage } = this.state.measures; | |||
if (!coverage) { | |||
return null; | |||
} | |||
return ( | |||
<div className="source-viewer-measures-section"> | |||
<div className="source-viewer-measures-card"> | |||
<div className="measures"> | |||
<div className="measures-chart"> | |||
<CoverageRating size="big" value={coverage.value} /> | |||
</div> | |||
<div className="measure measure-big" data-metric={coverage.metric.key}> | |||
<span className="measure-value"> | |||
<Measure | |||
metricKey={coverage.metric.key} | |||
metricType={coverage.metric.type} | |||
value={coverage.value} | |||
/> | |||
</span> | |||
<span className="measure-name">{getLocalizedMetricName(coverage.metric)}</span> | |||
</div> | |||
</div> | |||
<div className="measures"> | |||
<div className="measures-list"> | |||
{this.renderMeasure(this.state.measures.uncovered_lines)} | |||
{this.renderMeasure(this.state.measures.lines_to_cover)} | |||
{this.renderMeasure(this.state.measures.uncovered_conditions)} | |||
{this.renderMeasure(this.state.measures.conditions_to_cover)} | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
}; | |||
renderDuplications = () => { | |||
const { duplicated_lines_density: duplications } = this.state.measures; | |||
if (!duplications) { | |||
return null; | |||
} | |||
return ( | |||
<div className="source-viewer-measures-section"> | |||
<div className="source-viewer-measures-card"> | |||
<div className="measures"> | |||
<div className="measures-chart"> | |||
<DuplicationsRating | |||
muted={duplications.value === undefined} | |||
size="big" | |||
value={Number(duplications.value || 0)} | |||
/> | |||
</div> | |||
<div className="measure measure-big" data-metric={duplications.metric.key}> | |||
<span className="measure-value"> | |||
<Measure | |||
metricKey={duplications.metric.key} | |||
metricType={duplications.metric.type} | |||
value={duplications.value} | |||
/> | |||
</span> | |||
<span className="measure-name"> | |||
{getLocalizedMetricName(duplications.metric, true)} | |||
</span> | |||
</div> | |||
</div> | |||
<div className="measures"> | |||
<div className="measures-list"> | |||
{this.renderMeasure(this.state.measures.duplicated_blocks)} | |||
{this.renderMeasure(this.state.measures.duplicated_lines)} | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
}; | |||
renderTests = () => { | |||
const { measures } = this.state; | |||
return ( | |||
<div className="source-viewer-measures"> | |||
<div className="source-viewer-measures-section"> | |||
<div className="source-viewer-measures-card"> | |||
<div className="measures"> | |||
<div className="measures-list"> | |||
{this.renderMeasure(measures.tests)} | |||
{this.renderMeasure(measures.test_success_density)} | |||
{this.renderMeasure(measures.test_failures)} | |||
{this.renderMeasure(measures.test_errors)} | |||
{this.renderMeasure(measures.skipped_tests)} | |||
{this.renderMeasure(measures.test_execution_time)} | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
}; | |||
renderDomain = (domain: string, measures: MeasureEnhanced[]) => { | |||
return ( | |||
<div className="source-viewer-measures-card" key={domain}> | |||
<div className="measures"> | |||
<div className="measures-list"> | |||
<div className="measure measure-one-line measure-big"> | |||
<span className="measure-name">{domain}</span> | |||
</div> | |||
{sortBy(measures.filter(measure => measure.value !== undefined), measure => | |||
getLocalizedMetricName(measure.metric) | |||
).map(measure => this.renderMeasure(measure))} | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
}; | |||
renderAllMeasures = () => { | |||
const domains = groupBy(Object.values(this.state.measures), measure => measure.metric.domain); | |||
const domainKeys = Object.keys(domains); | |||
const odd = domainKeys.filter((_, index) => index % 2 === 1); | |||
const even = domainKeys.filter((_, index) => index % 2 === 0); | |||
return ( | |||
<div className="source-viewer-measures source-viewer-measures-secondary js-all-measures"> | |||
<div className="source-viewer-measures-section source-viewer-measures-section-big"> | |||
{odd.map(domain => this.renderDomain(domain, domains[domain]))} | |||
</div> | |||
<div className="source-viewer-measures-section source-viewer-measures-section-big"> | |||
{even.map(domain => this.renderDomain(domain, domains[domain]))} | |||
</div> | |||
</div> | |||
); | |||
}; | |||
render() { | |||
const { branch, sourceViewerFile } = this.props; | |||
const { loading } = this.state; | |||
return ( | |||
<Modal contentLabel="" large={true} onRequestClose={this.props.onClose}> | |||
<div className="modal-container source-viewer-measures-modal"> | |||
<div className="source-viewer-header-component source-viewer-measures-component"> | |||
<div className="source-viewer-header-component-project"> | |||
<QualifierIcon className="little-spacer-right" qualifier="TRK" /> | |||
<Link to={getProjectUrl(sourceViewerFile.project, branch)}> | |||
{sourceViewerFile.projectName} | |||
</Link> | |||
{sourceViewerFile.subProject && ( | |||
<> | |||
<QualifierIcon className="big-spacer-left little-spacer-right" qualifier="BRC" /> | |||
<Link to={getProjectUrl(sourceViewerFile.subProject, branch)}> | |||
{sourceViewerFile.subProjectName} | |||
</Link> | |||
</> | |||
)} | |||
</div> | |||
<div className="source-viewer-header-component-name"> | |||
<QualifierIcon className="little-spacer-right" qualifier={sourceViewerFile.q} /> | |||
{sourceViewerFile.path} | |||
</div> | |||
</div> | |||
{loading ? ( | |||
<i className="spinner" /> | |||
) : ( | |||
<> | |||
{sourceViewerFile.q === 'UTS' ? ( | |||
<> | |||
{this.renderTests()} | |||
<MeasuresOverlayTestCases branch={branch} componentKey={sourceViewerFile.key} /> | |||
</> | |||
) : ( | |||
<div className="source-viewer-measures"> | |||
{this.renderLines()} | |||
{this.renderIssues()} | |||
{this.renderCoverage()} | |||
{this.renderDuplications()} | |||
</div> | |||
)} | |||
</> | |||
)} | |||
<div className="spacer-top"> | |||
{this.state.showAllMeasures ? ( | |||
this.renderAllMeasures() | |||
) : ( | |||
<a className="js-show-all-measures" href="#" onClick={this.handleAllMeasuresClick}> | |||
{translate('component_viewer.show_all_measures')} | |||
</a> | |||
)} | |||
</div> | |||
</div> | |||
<footer className="modal-foot"> | |||
<button className="button-link" onClick={this.handleCloseClick} type="button"> | |||
{translate('close')} | |||
</button> | |||
</footer> | |||
</Modal> | |||
); | |||
} | |||
} |
@@ -0,0 +1,115 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import { getCoveredFiles } from '../../../api/tests'; | |||
import { TestCase, CoveredFile } from '../../../app/types'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import DeferredSpinner from '../../common/DeferredSpinner'; | |||
interface Props { | |||
testCase: TestCase; | |||
} | |||
interface State { | |||
coveredFiles?: CoveredFile[]; | |||
loading: boolean; | |||
} | |||
export default class MeasuresOverlayCoveredFiles extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { loading: true }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchCoveredFiles(); | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if (this.props.testCase.id !== prevProps.testCase.id) { | |||
this.fetchCoveredFiles(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
fetchCoveredFiles = () => { | |||
this.setState({ loading: true }); | |||
getCoveredFiles({ testId: this.props.testCase.id }).then( | |||
coveredFiles => { | |||
if (this.mounted) { | |||
this.setState({ coveredFiles, loading: false }); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
); | |||
}; | |||
render() { | |||
const { testCase } = this.props; | |||
const { loading, coveredFiles } = this.state; | |||
return ( | |||
<div className="source-viewer-measures-section source-viewer-measures-section-big js-selected-test"> | |||
<DeferredSpinner loading={loading}> | |||
<div className="source-viewer-measures-card source-viewer-measures-card-fixed-height"> | |||
{testCase.status !== 'ERROR' && | |||
testCase.status !== 'FAILURE' && | |||
coveredFiles !== undefined && ( | |||
<> | |||
<div className="bubble-popup-title"> | |||
{translate('component_viewer.transition.covers')} | |||
</div> | |||
{coveredFiles.length > 0 | |||
? coveredFiles.map(coveredFile => ( | |||
<div className="bubble-popup-section" key={coveredFile.key}> | |||
<Link to={getProjectUrl(coveredFile.key)}>{coveredFile.longName}</Link> | |||
<span className="note spacer-left"> | |||
{translateWithParameters( | |||
'component_viewer.x_lines_are_covered', | |||
coveredFile.coveredLines | |||
)} | |||
</span> | |||
</div> | |||
)) | |||
: translate('none')} | |||
</> | |||
)} | |||
{testCase.status !== 'OK' && ( | |||
<> | |||
<div className="bubble-popup-title">{translate('component_viewer.details')}</div> | |||
{testCase.message && <pre>{testCase.message}</pre>} | |||
<pre>{testCase.stacktrace}</pre> | |||
</> | |||
)} | |||
</div> | |||
</DeferredSpinner> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,57 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { Metric } from '../../../app/types'; | |||
import Measure from '../../measure/Measure'; | |||
import IssueTypeIcon from '../../ui/IssueTypeIcon'; | |||
import { getLocalizedMetricName } from '../../../helpers/l10n'; | |||
export interface MeasureWithMetric { | |||
metric: Metric; | |||
value?: string; | |||
} | |||
interface Props { | |||
measure: MeasureWithMetric; | |||
} | |||
export default function MeasuresOverlayMeasure({ measure }: Props) { | |||
return ( | |||
<div | |||
className="measure measure-one-line" | |||
data-metric={measure.metric.key} | |||
key={measure.metric.key}> | |||
<span className="measure-name"> | |||
{['bugs', 'vulnerabilities', 'code_smells'].includes(measure.metric.key) && ( | |||
<IssueTypeIcon className="little-spacer-right" query={measure.metric.key} /> | |||
)} | |||
{getLocalizedMetricName(measure.metric)} | |||
</span> | |||
<span className="measure-value"> | |||
<Measure | |||
metricKey={measure.metric.key} | |||
metricType={measure.metric.type} | |||
small={true} | |||
value={measure.value} | |||
/> | |||
</span> | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,62 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { TestCase } from '../../../app/types'; | |||
import TestStatusIcon from '../../shared/TestStatusIcon'; | |||
interface Props { | |||
onClick: (testId: string) => void; | |||
testCase: TestCase; | |||
} | |||
export default class MeasuresOverlayTestCase extends React.PureComponent<Props> { | |||
handleTestCaseClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.props.onClick(this.props.testCase.id); | |||
}; | |||
render() { | |||
const { testCase } = this.props; | |||
const { status } = testCase; | |||
const hasAdditionalData = status !== 'OK' || (status === 'OK' && testCase.coveredLines); | |||
return ( | |||
<tr> | |||
<td className="source-viewer-test-status"> | |||
<TestStatusIcon status={status} /> | |||
</td> | |||
<td className="source-viewer-test-duration note"> | |||
{status !== 'SKIPPED' && `${testCase.durationInMs}ms`} | |||
</td> | |||
<td className="source-viewer-test-name"> | |||
{hasAdditionalData ? ( | |||
<a className="js-show-test" href="#" onClick={this.handleTestCaseClick}> | |||
{testCase.name} | |||
</a> | |||
) : ( | |||
testCase.name | |||
)} | |||
</td> | |||
<td className="source-viewer-test-covered-lines note">{testCase.coveredLines}</td> | |||
</tr> | |||
); | |||
} | |||
} |
@@ -0,0 +1,181 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import { orderBy } from 'lodash'; | |||
import MeasuresOverlayCoveredFiles from './MeasuresOverlayCoveredFiles'; | |||
import MeasuresOverlayTestCase from './MeasuresOverlayTestCase'; | |||
import { getTests } from '../../../api/tests'; | |||
import { TestCase } from '../../../app/types'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
branch: string | undefined; | |||
componentKey: string; | |||
} | |||
interface State { | |||
loading: boolean; | |||
selectedTestId?: string; | |||
sort?: string; | |||
sortAsc?: boolean; | |||
testCases?: TestCase[]; | |||
} | |||
export default class MeasuresOverlayTestCases extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { loading: true }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchTests(); | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if ( | |||
prevProps.branch !== this.props.branch || | |||
prevProps.componentKey !== this.props.componentKey | |||
) { | |||
this.fetchTests(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
fetchTests = () => { | |||
// TODO implement pagination one day... | |||
this.setState({ loading: true }); | |||
getTests({ branch: this.props.branch, ps: 500, testFileKey: this.props.componentKey }).then( | |||
({ tests: testCases }) => { | |||
if (this.mounted) { | |||
this.setState({ loading: false, testCases }); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
); | |||
}; | |||
handleTestCasesSortClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
const { sort } = event.currentTarget.dataset; | |||
if (sort) { | |||
this.setState((state: State) => ({ | |||
sort, | |||
sortAsc: sort === state.sort ? !state.sortAsc : true | |||
})); | |||
} | |||
}; | |||
handleTestCaseClick = (selectedTestId: string) => { | |||
this.setState({ selectedTestId }); | |||
}; | |||
render() { | |||
const { selectedTestId, sort = 'name', sortAsc = true, testCases } = this.state; | |||
if (!testCases) { | |||
return null; | |||
} | |||
const selectedTest = testCases.find(test => test.id === selectedTestId); | |||
return ( | |||
<div className="source-viewer-measures"> | |||
<div className="source-viewer-measures-section source-viewer-measures-section-big"> | |||
<div className="source-viewer-measures-card source-viewer-measures-card-fixed-height js-test-list"> | |||
<div className="measures"> | |||
<table className="source-viewer-tests-list"> | |||
<tbody> | |||
<tr> | |||
<td className="source-viewer-test-status note" colSpan={3}> | |||
{translate('component_viewer.measure_section.unit_tests')} | |||
<br /> | |||
<span className="spacer-right"> | |||
{translate('component_viewer.tests.ordered_by')} | |||
</span> | |||
<a | |||
className={classNames('js-sort-tests-by-duration', { | |||
'active-link': sort === 'duration' | |||
})} | |||
data-sort="duration" | |||
href="#" | |||
onClick={this.handleTestCasesSortClick}> | |||
{translate('component_viewer.tests.duration')} | |||
</a> | |||
<span className="slash-separator" /> | |||
<a | |||
className={classNames('js-sort-tests-by-name', { | |||
'active-link': sort === 'name' | |||
})} | |||
data-sort="name" | |||
href="#" | |||
onClick={this.handleTestCasesSortClick}> | |||
{translate('component_viewer.tests.test_name')} | |||
</a> | |||
<span className="slash-separator" /> | |||
<a | |||
className={classNames('js-sort-tests-by-status', { | |||
'active-link': sort === 'status' | |||
})} | |||
data-sort="status" | |||
href="#" | |||
onClick={this.handleTestCasesSortClick}> | |||
{translate('component_viewer.tests.status')} | |||
</a> | |||
</td> | |||
<td className="source-viewer-test-covered-lines note"> | |||
{translate('component_viewer.covered_lines')} | |||
</td> | |||
</tr> | |||
{sortTestCases(testCases, sort, sortAsc).map(testCase => ( | |||
<MeasuresOverlayTestCase | |||
key={testCase.id} | |||
onClick={this.handleTestCaseClick} | |||
testCase={testCase} | |||
/> | |||
))} | |||
</tbody> | |||
</table> | |||
</div> | |||
</div> | |||
</div> | |||
{selectedTest && <MeasuresOverlayCoveredFiles testCase={selectedTest} />} | |||
</div> | |||
); | |||
} | |||
} | |||
function sortTestCases(testCases: TestCase[], sort: string, sortAsc: boolean) { | |||
const mainOrder = sortAsc ? 'asc' : 'desc'; | |||
if (sort === 'duration') { | |||
return orderBy(testCases, ['durationInMs', 'name'], [mainOrder, 'asc']); | |||
} else if (sort === 'status') { | |||
return orderBy(testCases, ['status', 'name'], [mainOrder, 'asc']); | |||
} else { | |||
return orderBy(testCases, ['name'], [mainOrder]); | |||
} | |||
} |
@@ -0,0 +1,172 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import MeasuresOverlay from '../MeasuresOverlay'; | |||
import { SourceViewerFile } from '../../../../app/types'; | |||
import { waitAndUpdate, click } from '../../../../helpers/testUtils'; | |||
jest.mock('../../../../api/issues', () => ({ | |||
getFacets: () => | |||
Promise.resolve({ | |||
facets: [ | |||
{ | |||
property: 'types', | |||
values: [ | |||
{ val: 'CODE_SMELL', count: 2 }, | |||
{ val: 'BUG', count: 1 }, | |||
{ val: 'VULNERABILITY', count: 0 } | |||
] | |||
}, | |||
{ | |||
property: 'severities', | |||
values: [ | |||
{ val: 'MAJOR', count: 1 }, | |||
{ val: 'INFO', count: 2 }, | |||
{ val: 'MINOR', count: 3 }, | |||
{ val: 'CRITICAL', count: 4 }, | |||
{ val: 'BLOCKER', count: 5 } | |||
] | |||
}, | |||
{ | |||
property: 'tags', | |||
values: [ | |||
{ val: 'bad-practice', count: 1 }, | |||
{ val: 'cert', count: 3 }, | |||
{ val: 'design', count: 1 } | |||
] | |||
} | |||
] | |||
}) | |||
})); | |||
jest.mock('../../../../api/measures', () => ({ | |||
getMeasures: () => | |||
Promise.resolve([ | |||
{ metric: 'vulnerabilities', value: '0' }, | |||
{ metric: 'complexity', value: '1' }, | |||
{ metric: 'test_errors', value: '1' }, | |||
{ metric: 'comment_lines_density', value: '20.0' }, | |||
{ metric: 'wont_fix_issues', value: '0' }, | |||
{ metric: 'uncovered_lines', value: '1' }, | |||
{ metric: 'functions', value: '1' }, | |||
{ metric: 'duplicated_files', value: '1' }, | |||
{ metric: 'duplicated_blocks', value: '3' }, | |||
{ metric: 'line_coverage', value: '75.0' }, | |||
{ metric: 'duplicated_lines_density', value: '0.0' }, | |||
{ metric: 'comment_lines', value: '2' }, | |||
{ metric: 'ncloc', value: '8' }, | |||
{ metric: 'reliability_rating', value: '1.0' }, | |||
{ metric: 'false_positive_issues', value: '0' }, | |||
{ metric: 'reliability_remediation_effort', value: '0' }, | |||
{ metric: 'code_smells', value: '2' }, | |||
{ metric: 'security_rating', value: '1.0' }, | |||
{ metric: 'test_success_density', value: '100.0' }, | |||
{ metric: 'cognitive_complexity', value: '0' }, | |||
{ metric: 'files', value: '1' }, | |||
{ metric: 'duplicated_lines', value: '0' }, | |||
{ metric: 'lines', value: '18' }, | |||
{ metric: 'classes', value: '1' }, | |||
{ metric: 'bugs', value: '0' }, | |||
{ metric: 'lines_to_cover', value: '4' }, | |||
{ metric: 'sqale_index', value: '40' }, | |||
{ metric: 'sqale_debt_ratio', value: '16.7' }, | |||
{ metric: 'coverage', value: '75.0' }, | |||
{ metric: 'security_remediation_effort', value: '0' }, | |||
{ metric: 'statements', value: '3' }, | |||
{ metric: 'skipped_tests', value: '0' }, | |||
{ metric: 'test_failures', value: '0' } | |||
]) | |||
})); | |||
jest.mock('../../../../api/metrics', () => ({ | |||
getAllMetrics: () => | |||
Promise.resolve([ | |||
{ key: 'vulnerabilities', type: 'INT', domain: 'Security' }, | |||
{ key: 'complexity', type: 'INT', domain: 'Complexity' }, | |||
{ key: 'test_errors', type: 'INT', domain: 'Tests' }, | |||
{ key: 'comment_lines_density', type: 'PERCENT', domain: 'Size' }, | |||
{ key: 'wont_fix_issues', type: 'INT', domain: 'Issues' }, | |||
{ key: 'uncovered_lines', type: 'INT', domain: 'Coverage' }, | |||
{ key: 'functions', type: 'INT', domain: 'Size' }, | |||
{ key: 'duplicated_files', type: 'INT', domain: 'Duplications' }, | |||
{ key: 'duplicated_blocks', type: 'INT', domain: 'Duplications' }, | |||
{ key: 'line_coverage', type: 'PERCENT', domain: 'Coverage' }, | |||
{ key: 'duplicated_lines_density', type: 'PERCENT', domain: 'Duplications' }, | |||
{ key: 'comment_lines', type: 'INT', domain: 'Size' }, | |||
{ key: 'ncloc', type: 'INT', domain: 'Size' }, | |||
{ key: 'reliability_rating', type: 'RATING', domain: 'Reliability' }, | |||
{ key: 'false_positive_issues', type: 'INT', domain: 'Issues' }, | |||
{ key: 'code_smells', type: 'INT', domain: 'Maintainability' }, | |||
{ key: 'security_rating', type: 'RATING', domain: 'Security' }, | |||
{ key: 'test_success_density', type: 'PERCENT', domain: 'Tests' }, | |||
{ key: 'cognitive_complexity', type: 'INT', domain: 'Complexity' }, | |||
{ key: 'files', type: 'INT', domain: 'Size' }, | |||
{ key: 'duplicated_lines', type: 'INT', domain: 'Duplications' }, | |||
{ key: 'lines', type: 'INT', domain: 'Size' }, | |||
{ key: 'classes', type: 'INT', domain: 'Size' }, | |||
{ key: 'bugs', type: 'INT', domain: 'Reliability' }, | |||
{ key: 'lines_to_cover', type: 'INT', domain: 'Coverage' }, | |||
{ key: 'sqale_index', type: 'WORK_DUR', domain: 'Maintainability' }, | |||
{ key: 'sqale_debt_ratio', type: 'PERCENT', domain: 'Maintainability' }, | |||
{ key: 'coverage', type: 'PERCENT', domain: 'Coverage' }, | |||
{ key: 'statements', type: 'INT', domain: 'Size' }, | |||
{ key: 'skipped_tests', type: 'INT', domain: 'Tests' }, | |||
{ key: 'test_failures', type: 'INT', domain: 'Tests' }, | |||
// next two must be filtered out | |||
{ key: 'data', type: 'DATA' }, | |||
{ key: 'hidden', hidden: true } | |||
]) | |||
})); | |||
const sourceViewerFile: SourceViewerFile = { | |||
key: 'component-key', | |||
measures: {}, | |||
path: 'src/file.js', | |||
project: 'project-key', | |||
projectName: 'Project Name', | |||
q: 'FIL', | |||
subProject: 'sub-project-key', | |||
subProjectName: 'Sub-Project Name', | |||
uuid: 'abcd123' | |||
}; | |||
it('should render source file', async () => { | |||
const wrapper = shallow( | |||
<MeasuresOverlay branch="branch" onClose={jest.fn()} sourceViewerFile={sourceViewerFile} /> | |||
); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('.js-show-all-measures')); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should render test file', async () => { | |||
const wrapper = shallow( | |||
<MeasuresOverlay | |||
branch="branch" | |||
onClose={jest.fn()} | |||
sourceViewerFile={{ ...sourceViewerFile, q: 'UTS' }} | |||
/> | |||
); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,55 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import MeasuresOverlayCoveredFiles from '../MeasuresOverlayCoveredFiles'; | |||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | |||
jest.mock('../../../../api/tests', () => ({ | |||
getCoveredFiles: () => | |||
Promise.resolve([{ key: 'project:src/file.js', longName: 'src/file.js', coveredLines: 3 }]) | |||
})); | |||
const testCase = { | |||
coveredLines: 3, | |||
durationInMs: 1, | |||
fileId: 'abcd', | |||
fileKey: 'project:test.js', | |||
fileName: 'test.js', | |||
id: 'test-abcd', | |||
name: 'should work', | |||
status: 'OK' | |||
}; | |||
it('should render OK test', async () => { | |||
const wrapper = shallow(<MeasuresOverlayCoveredFiles testCase={testCase} />); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should render ERROR test', async () => { | |||
const wrapper = shallow( | |||
<MeasuresOverlayCoveredFiles | |||
testCase={{ ...testCase, status: 'ERROR', message: 'Something failed' }} | |||
/> | |||
); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,48 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import MeasuresOverlayMeasure from '../MeasuresOverlayMeasure'; | |||
it('should render', () => { | |||
expect( | |||
shallow( | |||
<MeasuresOverlayMeasure | |||
measure={{ | |||
metric: { id: '1', key: 'coverage', name: 'Coverage', type: 'PERCENT' }, | |||
value: '72' | |||
}} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should render issues icon', () => { | |||
expect( | |||
shallow( | |||
<MeasuresOverlayMeasure | |||
measure={{ | |||
metric: { id: '1', key: 'bugs', name: 'Bugs', type: 'INT' }, | |||
value: '2' | |||
}} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,42 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import MeasuresOverlayTestCase from '../MeasuresOverlayTestCase'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
const testCase = { | |||
coveredLines: 3, | |||
durationInMs: 1, | |||
fileId: 'abcd', | |||
fileKey: 'project:test.js', | |||
fileName: 'test.js', | |||
id: 'test-abcd', | |||
name: 'should work', | |||
status: 'OK' | |||
}; | |||
it('should render', () => { | |||
const onClick = jest.fn(); | |||
const wrapper = shallow(<MeasuresOverlayTestCase onClick={onClick} testCase={testCase} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('a')); | |||
expect(onClick).toBeCalledWith('test-abcd'); | |||
}); |
@@ -0,0 +1,74 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import MeasuresOverlayTestCases from '../MeasuresOverlayTestCases'; | |||
import { waitAndUpdate, click } from '../../../../helpers/testUtils'; | |||
jest.mock('../../../../api/tests', () => ({ | |||
getTests: () => | |||
Promise.resolve({ | |||
tests: [ | |||
{ | |||
id: 'AWGub2mGGZxsAttCZwQy', | |||
name: 'testAdd_WhichFails', | |||
fileKey: 'test:fake-project-for-tests:src/test/java/bar/SimplestTest.java', | |||
fileName: 'src/test/java/bar/SimplestTest.java', | |||
status: 'FAILURE', | |||
durationInMs: 6, | |||
coveredLines: 3, | |||
message: 'expected:<9> but was:<2>', | |||
stacktrace: | |||
'java.lang.AssertionError: expected:<9> but was:<2>\n\tat org.junit.Assert.fail(Assert.java:93)\n\tat org.junit.Assert.failNotEquals(Assert.java:647)' | |||
}, | |||
{ | |||
id: 'AWGub2mGGZxsAttCZwQz', | |||
name: 'testAdd_InError', | |||
fileKey: 'test:fake-project-for-tests:src/test/java/bar/SimplestTest.java', | |||
fileName: 'src/test/java/bar/SimplestTest.java', | |||
status: 'ERROR', | |||
durationInMs: 2, | |||
coveredLines: 3 | |||
}, | |||
{ | |||
id: 'AWGub2mFGZxsAttCZwQx', | |||
name: 'testAdd', | |||
fileKey: 'test:fake-project-for-tests:src/test/java/bar/SimplestTest.java', | |||
fileName: 'src/test/java/bar/SimplestTest.java', | |||
status: 'OK', | |||
durationInMs: 8, | |||
coveredLines: 3 | |||
} | |||
] | |||
}) | |||
})); | |||
it('should render', async () => { | |||
const wrapper = shallow( | |||
<MeasuresOverlayTestCases branch="branch" componentKey="component-key" /> | |||
); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('.js-sort-tests-by-duration'), { | |||
currentTarget: { blur() {}, dataset: { sort: 'duration' } } | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,76 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render ERROR test 1`] = ` | |||
<div | |||
className="source-viewer-measures-section source-viewer-measures-section-big js-selected-test" | |||
> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
> | |||
<div | |||
className="source-viewer-measures-card source-viewer-measures-card-fixed-height" | |||
> | |||
<React.Fragment> | |||
<div | |||
className="bubble-popup-title" | |||
> | |||
component_viewer.details | |||
</div> | |||
<pre> | |||
Something failed | |||
</pre> | |||
<pre /> | |||
</React.Fragment> | |||
</div> | |||
</DeferredSpinner> | |||
</div> | |||
`; | |||
exports[`should render OK test 1`] = ` | |||
<div | |||
className="source-viewer-measures-section source-viewer-measures-section-big js-selected-test" | |||
> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
> | |||
<div | |||
className="source-viewer-measures-card source-viewer-measures-card-fixed-height" | |||
> | |||
<React.Fragment> | |||
<div | |||
className="bubble-popup-title" | |||
> | |||
component_viewer.transition.covers | |||
</div> | |||
<div | |||
className="bubble-popup-section" | |||
key="project:src/file.js" | |||
> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "project:src/file.js", | |||
}, | |||
} | |||
} | |||
> | |||
src/file.js | |||
</Link> | |||
<span | |||
className="note spacer-left" | |||
> | |||
component_viewer.x_lines_are_covered.3 | |||
</span> | |||
</div> | |||
</React.Fragment> | |||
</div> | |||
</DeferredSpinner> | |||
</div> | |||
`; |
@@ -0,0 +1,53 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render 1`] = ` | |||
<div | |||
className="measure measure-one-line" | |||
data-metric="coverage" | |||
key="coverage" | |||
> | |||
<span | |||
className="measure-name" | |||
> | |||
Coverage | |||
</span> | |||
<span | |||
className="measure-value" | |||
> | |||
<Measure | |||
metricKey="coverage" | |||
metricType="PERCENT" | |||
small={true} | |||
value="72" | |||
/> | |||
</span> | |||
</div> | |||
`; | |||
exports[`should render issues icon 1`] = ` | |||
<div | |||
className="measure measure-one-line" | |||
data-metric="bugs" | |||
key="bugs" | |||
> | |||
<span | |||
className="measure-name" | |||
> | |||
<IssueTypeIcon | |||
className="little-spacer-right" | |||
query="bugs" | |||
/> | |||
Bugs | |||
</span> | |||
<span | |||
className="measure-value" | |||
> | |||
<Measure | |||
metricKey="bugs" | |||
metricType="INT" | |||
small={true} | |||
value="2" | |||
/> | |||
</span> | |||
</div> | |||
`; |
@@ -0,0 +1,34 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render 1`] = ` | |||
<tr> | |||
<td | |||
className="source-viewer-test-status" | |||
> | |||
<TestStatusIcon | |||
status="OK" | |||
/> | |||
</td> | |||
<td | |||
className="source-viewer-test-duration note" | |||
> | |||
1ms | |||
</td> | |||
<td | |||
className="source-viewer-test-name" | |||
> | |||
<a | |||
className="js-show-test" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
should work | |||
</a> | |||
</td> | |||
<td | |||
className="source-viewer-test-covered-lines note" | |||
> | |||
3 | |||
</td> | |||
</tr> | |||
`; |
@@ -0,0 +1,247 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render 1`] = ` | |||
<div | |||
className="source-viewer-measures" | |||
> | |||
<div | |||
className="source-viewer-measures-section source-viewer-measures-section-big" | |||
> | |||
<div | |||
className="source-viewer-measures-card source-viewer-measures-card-fixed-height js-test-list" | |||
> | |||
<div | |||
className="measures" | |||
> | |||
<table | |||
className="source-viewer-tests-list" | |||
> | |||
<tbody> | |||
<tr> | |||
<td | |||
className="source-viewer-test-status note" | |||
colSpan={3} | |||
> | |||
component_viewer.measure_section.unit_tests | |||
<br /> | |||
<span | |||
className="spacer-right" | |||
> | |||
component_viewer.tests.ordered_by | |||
</span> | |||
<a | |||
className="js-sort-tests-by-duration" | |||
data-sort="duration" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
component_viewer.tests.duration | |||
</a> | |||
<span | |||
className="slash-separator" | |||
/> | |||
<a | |||
className="js-sort-tests-by-name active-link" | |||
data-sort="name" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
component_viewer.tests.test_name | |||
</a> | |||
<span | |||
className="slash-separator" | |||
/> | |||
<a | |||
className="js-sort-tests-by-status" | |||
data-sort="status" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
component_viewer.tests.status | |||
</a> | |||
</td> | |||
<td | |||
className="source-viewer-test-covered-lines note" | |||
> | |||
component_viewer.covered_lines | |||
</td> | |||
</tr> | |||
<MeasuresOverlayTestCase | |||
key="AWGub2mFGZxsAttCZwQx" | |||
onClick={[Function]} | |||
testCase={ | |||
Object { | |||
"coveredLines": 3, | |||
"durationInMs": 8, | |||
"fileKey": "test:fake-project-for-tests:src/test/java/bar/SimplestTest.java", | |||
"fileName": "src/test/java/bar/SimplestTest.java", | |||
"id": "AWGub2mFGZxsAttCZwQx", | |||
"name": "testAdd", | |||
"status": "OK", | |||
} | |||
} | |||
/> | |||
<MeasuresOverlayTestCase | |||
key="AWGub2mGGZxsAttCZwQz" | |||
onClick={[Function]} | |||
testCase={ | |||
Object { | |||
"coveredLines": 3, | |||
"durationInMs": 2, | |||
"fileKey": "test:fake-project-for-tests:src/test/java/bar/SimplestTest.java", | |||
"fileName": "src/test/java/bar/SimplestTest.java", | |||
"id": "AWGub2mGGZxsAttCZwQz", | |||
"name": "testAdd_InError", | |||
"status": "ERROR", | |||
} | |||
} | |||
/> | |||
<MeasuresOverlayTestCase | |||
key="AWGub2mGGZxsAttCZwQy" | |||
onClick={[Function]} | |||
testCase={ | |||
Object { | |||
"coveredLines": 3, | |||
"durationInMs": 6, | |||
"fileKey": "test:fake-project-for-tests:src/test/java/bar/SimplestTest.java", | |||
"fileName": "src/test/java/bar/SimplestTest.java", | |||
"id": "AWGub2mGGZxsAttCZwQy", | |||
"message": "expected:<9> but was:<2>", | |||
"name": "testAdd_WhichFails", | |||
"stacktrace": "java.lang.AssertionError: expected:<9> but was:<2> | |||
at org.junit.Assert.fail(Assert.java:93) | |||
at org.junit.Assert.failNotEquals(Assert.java:647)", | |||
"status": "FAILURE", | |||
} | |||
} | |||
/> | |||
</tbody> | |||
</table> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render 2`] = ` | |||
<div | |||
className="source-viewer-measures" | |||
> | |||
<div | |||
className="source-viewer-measures-section source-viewer-measures-section-big" | |||
> | |||
<div | |||
className="source-viewer-measures-card source-viewer-measures-card-fixed-height js-test-list" | |||
> | |||
<div | |||
className="measures" | |||
> | |||
<table | |||
className="source-viewer-tests-list" | |||
> | |||
<tbody> | |||
<tr> | |||
<td | |||
className="source-viewer-test-status note" | |||
colSpan={3} | |||
> | |||
component_viewer.measure_section.unit_tests | |||
<br /> | |||
<span | |||
className="spacer-right" | |||
> | |||
component_viewer.tests.ordered_by | |||
</span> | |||
<a | |||
className="js-sort-tests-by-duration active-link" | |||
data-sort="duration" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
component_viewer.tests.duration | |||
</a> | |||
<span | |||
className="slash-separator" | |||
/> | |||
<a | |||
className="js-sort-tests-by-name" | |||
data-sort="name" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
component_viewer.tests.test_name | |||
</a> | |||
<span | |||
className="slash-separator" | |||
/> | |||
<a | |||
className="js-sort-tests-by-status" | |||
data-sort="status" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
component_viewer.tests.status | |||
</a> | |||
</td> | |||
<td | |||
className="source-viewer-test-covered-lines note" | |||
> | |||
component_viewer.covered_lines | |||
</td> | |||
</tr> | |||
<MeasuresOverlayTestCase | |||
key="AWGub2mGGZxsAttCZwQz" | |||
onClick={[Function]} | |||
testCase={ | |||
Object { | |||
"coveredLines": 3, | |||
"durationInMs": 2, | |||
"fileKey": "test:fake-project-for-tests:src/test/java/bar/SimplestTest.java", | |||
"fileName": "src/test/java/bar/SimplestTest.java", | |||
"id": "AWGub2mGGZxsAttCZwQz", | |||
"name": "testAdd_InError", | |||
"status": "ERROR", | |||
} | |||
} | |||
/> | |||
<MeasuresOverlayTestCase | |||
key="AWGub2mGGZxsAttCZwQy" | |||
onClick={[Function]} | |||
testCase={ | |||
Object { | |||
"coveredLines": 3, | |||
"durationInMs": 6, | |||
"fileKey": "test:fake-project-for-tests:src/test/java/bar/SimplestTest.java", | |||
"fileName": "src/test/java/bar/SimplestTest.java", | |||
"id": "AWGub2mGGZxsAttCZwQy", | |||
"message": "expected:<9> but was:<2>", | |||
"name": "testAdd_WhichFails", | |||
"stacktrace": "java.lang.AssertionError: expected:<9> but was:<2> | |||
at org.junit.Assert.fail(Assert.java:93) | |||
at org.junit.Assert.failNotEquals(Assert.java:647)", | |||
"status": "FAILURE", | |||
} | |||
} | |||
/> | |||
<MeasuresOverlayTestCase | |||
key="AWGub2mFGZxsAttCZwQx" | |||
onClick={[Function]} | |||
testCase={ | |||
Object { | |||
"coveredLines": 3, | |||
"durationInMs": 8, | |||
"fileKey": "test:fake-project-for-tests:src/test/java/bar/SimplestTest.java", | |||
"fileName": "src/test/java/bar/SimplestTest.java", | |||
"id": "AWGub2mFGZxsAttCZwQx", | |||
"name": "testAdd", | |||
"status": "OK", | |||
} | |||
} | |||
/> | |||
</tbody> | |||
</table> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
`; |
@@ -567,7 +567,6 @@ | |||
.measure-big .measure-value { | |||
font-size: 22px; | |||
font-weight: 300; | |||
} | |||
.measure-big .rating { |
@@ -1,282 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import $ from 'jquery'; | |||
import { select } from 'd3-selection'; | |||
import { arc as d3Arc, pie as d3Pie } from 'd3-shape'; | |||
import { groupBy, sortBy, toPairs } from 'lodash'; | |||
import Template from './templates/source-viewer-measures.hbs'; | |||
import ModalView from '../../common/modals'; | |||
import { searchIssues } from '../../../api/issues'; | |||
import { getMeasures } from '../../../api/measures'; | |||
import { getAllMetrics } from '../../../api/metrics'; | |||
import { getTests, getCoveredFiles } from '../../../api/tests'; | |||
import * as theme from '../../../app/theme'; | |||
import { getLocalizedMetricName, getLocalizedMetricDomain } from '../../../helpers/l10n'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
const severityComparator = severity => { | |||
const SEVERITIES_ORDER = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']; | |||
return SEVERITIES_ORDER.indexOf(severity); | |||
}; | |||
export default ModalView.extend({ | |||
template: Template, | |||
testsOrder: ['ERROR', 'FAILURE', 'OK', 'SKIPPED'], | |||
initialize() { | |||
this.testsScroll = 0; | |||
const requests = [this.requestMeasures(), this.requestIssues()]; | |||
if (this.options.component.q === 'UTS') { | |||
requests.push(this.requestTests()); | |||
} | |||
Promise.all(requests).then(() => this.render()); | |||
}, | |||
events() { | |||
return { | |||
...ModalView.prototype.events.apply(this, arguments), | |||
'click .js-sort-tests-by-duration': 'sortTestsByDuration', | |||
'click .js-sort-tests-by-name': 'sortTestsByName', | |||
'click .js-sort-tests-by-status': 'sortTestsByStatus', | |||
'click .js-show-test': 'showTest', | |||
'click .js-show-all-measures': 'showAllMeasures' | |||
}; | |||
}, | |||
initPieChart() { | |||
const trans = function(left, top) { | |||
return `translate(${left}, ${top})`; | |||
}; | |||
const defaults = { | |||
size: 40, | |||
thickness: 8, | |||
color: '#1f77b4', | |||
baseColor: theme.barBorderColor | |||
}; | |||
this.$('.js-pie-chart').each(function() { | |||
const data = [$(this).data('value'), $(this).data('max') - $(this).data('value')]; | |||
const options = { ...defaults, ...$(this).data() }; | |||
const radius = options.size / 2; | |||
const container = select(this); | |||
const svg = container | |||
.append('svg') | |||
.attr('width', options.size) | |||
.attr('height', options.size); | |||
const plot = svg.append('g').attr('transform', trans(radius, radius)); | |||
const arc = d3Arc() | |||
.innerRadius(radius - options.thickness) | |||
.outerRadius(radius); | |||
const pie = d3Pie() | |||
.sort(null) | |||
.value(d => d); | |||
const colors = function(i) { | |||
return i === 0 ? options.color : options.baseColor; | |||
}; | |||
const sectors = plot.selectAll('path').data(pie(data)); | |||
sectors | |||
.enter() | |||
.append('path') | |||
.style('fill', (d, i) => colors(i)) | |||
.attr('d', arc); | |||
}); | |||
}, | |||
onRender() { | |||
ModalView.prototype.onRender.apply(this, arguments); | |||
this.initPieChart(); | |||
this.$('.js-test-list').scrollTop(this.testsScroll); | |||
}, | |||
calcAdditionalMeasures(measures) { | |||
measures.issuesRemediationEffort = | |||
(Number(measures.sqale_index_raw) || 0) + | |||
(Number(measures.reliability_remediation_effort_raw) || 0) + | |||
(Number(measures.security_remediation_effort_raw) || 0); | |||
if (measures.lines_to_cover && measures.uncovered_lines) { | |||
measures.covered_lines = measures.lines_to_cover_raw - measures.uncovered_lines_raw; | |||
} | |||
if (measures.conditions_to_cover && measures.uncovered_conditions) { | |||
measures.covered_conditions = measures.conditions_to_cover - measures.uncovered_conditions; | |||
} | |||
return measures; | |||
}, | |||
prepareMetrics(metrics) { | |||
metrics = metrics | |||
.filter(metric => metric.value != null) | |||
.map(metric => ({ ...metric, name: getLocalizedMetricName(metric) })); | |||
return sortBy( | |||
toPairs(groupBy(metrics, 'domain')).map(domain => { | |||
return { | |||
name: getLocalizedMetricDomain(domain[0]), | |||
metrics: domain[1] | |||
}; | |||
}), | |||
'name' | |||
); | |||
}, | |||
requestMeasures() { | |||
return getAllMetrics().then(metrics => { | |||
const metricsToRequest = metrics | |||
.filter(metric => metric.type !== 'DATA' && !metric.hidden) | |||
.map(metric => metric.key); | |||
return getMeasures(this.options.component.key, metricsToRequest, this.options.branch).then( | |||
measures => { | |||
let nextMeasures = this.options.component.measures || {}; | |||
measures.forEach(measure => { | |||
const metric = metrics.find(metric => metric.key === measure.metric); | |||
nextMeasures[metric.key] = formatMeasure(measure.value, metric.type); | |||
nextMeasures[metric.key + '_raw'] = measure.value; | |||
metric.value = nextMeasures[metric.key]; | |||
}); | |||
nextMeasures = this.calcAdditionalMeasures(nextMeasures); | |||
this.measures = nextMeasures; | |||
this.measuresToDisplay = this.prepareMetrics(metrics); | |||
}, | |||
() => {} | |||
); | |||
}); | |||
}, | |||
requestIssues() { | |||
const options = { | |||
branch: this.options.branch, | |||
componentKeys: this.options.component.key, | |||
resolved: false, | |||
ps: 1, | |||
facets: 'types,severities,tags' | |||
}; | |||
return searchIssues(options).then( | |||
data => { | |||
const typesFacet = data.facets.find(facet => facet.property === 'types').values; | |||
const typesOrder = ['BUG', 'VULNERABILITY', 'CODE_SMELL']; | |||
const sortedTypesFacet = sortBy(typesFacet, v => typesOrder.indexOf(v.val)); | |||
const severitiesFacet = data.facets.find(facet => facet.property === 'severities').values; | |||
const sortedSeveritiesFacet = sortBy(severitiesFacet, facet => | |||
severityComparator(facet.val) | |||
); | |||
const tagsFacet = data.facets.find(facet => facet.property === 'tags').values; | |||
this.tagsFacet = tagsFacet; | |||
this.typesFacet = sortedTypesFacet; | |||
this.severitiesFacet = sortedSeveritiesFacet; | |||
this.issuesCount = data.total; | |||
}, | |||
() => {} | |||
); | |||
}, | |||
requestTests() { | |||
return getTests({ branch: this.options.branch, testFileKey: this.options.component.key }).then( | |||
data => { | |||
this.tests = data.tests; | |||
this.testSorting = 'status'; | |||
this.testAsc = true; | |||
this.sortTests(test => `${this.testsOrder.indexOf(test.status)}_______${test.name}`); | |||
}, | |||
() => {} | |||
); | |||
}, | |||
sortTests(condition) { | |||
let tests = this.tests; | |||
if (Array.isArray(tests)) { | |||
tests = sortBy(tests, condition); | |||
if (!this.testAsc) { | |||
tests.reverse(); | |||
} | |||
this.tests = tests; | |||
} | |||
}, | |||
sortTestsByDuration() { | |||
if (this.testSorting === 'duration') { | |||
this.testAsc = !this.testAsc; | |||
} | |||
this.sortTests('durationInMs'); | |||
this.testSorting = 'duration'; | |||
this.render(); | |||
}, | |||
sortTestsByName() { | |||
if (this.testSorting === 'name') { | |||
this.testAsc = !this.testAsc; | |||
} | |||
this.sortTests('name'); | |||
this.testSorting = 'name'; | |||
this.render(); | |||
}, | |||
sortTestsByStatus() { | |||
if (this.testSorting === 'status') { | |||
this.testAsc = !this.testAsc; | |||
} | |||
this.sortTests(test => `${this.testsOrder.indexOf(test.status)}_______${test.name}`); | |||
this.testSorting = 'status'; | |||
this.render(); | |||
}, | |||
showTest(e) { | |||
const testId = $(e.currentTarget).data('id'); | |||
this.testsScroll = $(e.currentTarget) | |||
.scrollParent() | |||
.scrollTop(); | |||
getCoveredFiles({ testId }).then( | |||
data => { | |||
this.coveredFiles = data.files; | |||
this.selectedTest = this.tests.find(test => test.id === testId); | |||
this.render(); | |||
}, | |||
() => {} | |||
); | |||
}, | |||
showAllMeasures() { | |||
this.$('.js-all-measures').removeClass('hidden'); | |||
this.$('.js-show-all-measures').remove(); | |||
}, | |||
serializeData() { | |||
return { | |||
...ModalView.prototype.serializeData.apply(this, arguments), | |||
...this.options.component, | |||
measures: this.measures, | |||
measuresToDisplay: this.measuresToDisplay, | |||
tests: this.tests, | |||
tagsFacet: this.tagsFacet, | |||
typesFacet: this.typesFacet, | |||
severitiesFacet: this.severitiesFacet, | |||
issuesCount: this.issuesCount, | |||
testSorting: this.testSorting, | |||
selectedTest: this.selectedTest, | |||
coveredFiles: this.coveredFiles || [] | |||
}; | |||
} | |||
}); |
@@ -1,42 +0,0 @@ | |||
{{#notEmpty measuresToDisplay}} | |||
<div class="source-viewer-measures-section source-viewer-measures-section-big"> | |||
{{#eachEven measuresToDisplay}} | |||
<div class="source-viewer-measures-card"> | |||
<div class="measures"> | |||
<div class="measures-list"> | |||
<div class="measure measure-one-line measure-big"> | |||
<span class="measure-name">{{name}}</span> | |||
</div> | |||
{{#each metrics}} | |||
<div class="measure measure-one-line" data-metric="{{key}}"> | |||
<span class="measure-name">{{#eq key 'bugs'}}{{issueTypeIcon 'BUG'}} {{/eq}}{{#eq key 'vulnerabilities'}}{{issueTypeIcon 'VULNERABILITY'}} {{/eq}}{{#eq key 'code_smells'}}{{issueTypeIcon 'CODE_SMELL'}} {{/eq}}{{name}}</span> | |||
<span class="measure-value"> {{value}}</span> | |||
</div> | |||
{{/each}} | |||
</div> | |||
</div> | |||
</div> | |||
{{/eachEven}} | |||
</div> | |||
<div class="source-viewer-measures-section source-viewer-measures-section-big"> | |||
{{#eachOdd measuresToDisplay}} | |||
<div class="source-viewer-measures-card"> | |||
<div class="measures"> | |||
<div class="measures-list"> | |||
<div class="measure measure-one-line measure-big"> | |||
<span class="measure-name">{{name}}</span> | |||
</div> | |||
{{#each metrics}} | |||
<div class="measure measure-one-line" data-metric="{{key}}"> | |||
<span class="measure-name">{{#eq key 'bugs'}}{{issueTypeIcon 'BUG'}} {{/eq}}{{#eq key 'vulnerabilities'}}{{issueTypeIcon 'VULNERABILITY'}} {{/eq}}{{#eq key 'code_smells'}}{{issueTypeIcon 'CODE_SMELL'}} {{/eq}}{{name}}</span> | |||
<span class="measure-value"> {{value}}</span> | |||
</div> | |||
{{/each}} | |||
</div> | |||
</div> | |||
</div> | |||
{{/eachOdd}} | |||
</div> | |||
{{/notEmpty}} | |||
@@ -1,36 +0,0 @@ | |||
{{#if measures.coverage}} | |||
<div class="measures"> | |||
<div class="measures-chart"> | |||
<span class="js-pie-chart" | |||
data-value="{{measures.coverage_raw}}" | |||
data-max="100" | |||
data-color="#00aa00" | |||
data-base-color="#d4333f" | |||
data-size="47"></span> | |||
</div> | |||
<div class="measure measure-big" data-metric="coverage"> | |||
<span class="measure-value">{{measures.coverage}}</span> | |||
<span class="measure-name">{{t 'metric.coverage.name'}}</span> | |||
</div> | |||
</div> | |||
{{#any measures.covered_lines measures.lines_to_cover measures.covered_conditions measures.conditions_to_cover}} | |||
<div class="measures"> | |||
<div class="measures-list"> | |||
<div class="measure measure-one-line"> | |||
<span class="measure-name">Covered by Tests</span> | |||
</div> | |||
<div class="measure measure-one-line" data-metric="lines_to_cover"> | |||
<span class="measure-name">Lines</span> | |||
<span class="measure-value">{{formatMeasure measures.covered_lines 'INT'}}/{{measures.lines_to_cover}}</span> | |||
</div> | |||
{{#if measures.conditions_to_cover}} | |||
<div class="measure measure-one-line" data-metric="conditions_to_cover"> | |||
<span class="measure-name">Conditions</span> | |||
<span class="measure-value">{{default measures.covered_conditions 0}}/{{measures.conditions_to_cover}}</span> | |||
</div> | |||
{{/if}} | |||
</div> | |||
</div> | |||
{{/any}} | |||
{{/if}} |
@@ -1,30 +0,0 @@ | |||
{{#notNull measures.duplicated_lines_density}} | |||
<div class="source-viewer-measures-card"> | |||
<div class="measures"> | |||
<div class="measures-chart"> | |||
<span class="js-pie-chart" | |||
data-value="{{measures.duplicated_lines_density_raw}}" | |||
data-max="100" | |||
data-size="50" | |||
data-color="#797979"></span> | |||
</div> | |||
<div class="measure measure-big" data-metric="duplicated_lines_density"> | |||
<span class="measure-value">{{measures.duplicated_lines_density}}</span> | |||
<span class="measure-name">{{t 'metric.duplicated_lines_density.short_name'}}</span> | |||
</div> | |||
</div> | |||
<div class="measures"> | |||
<div class="measures-list"> | |||
<div class="measure measure-one-line" data-metric="duplicated_blocks"> | |||
<span class="measure-name">{{t 'metric.duplicated_blocks.name'}}</span> | |||
<span class="measure-value">{{measures.duplicated_blocks}}</span> | |||
</div> | |||
<div class="measure measure-one-line" data-metric="duplicated_lines"> | |||
<span class="measure-name">{{t 'metric.duplicated_lines.name'}}</span> | |||
<span class="measure-value">{{measures.duplicated_lines}}</span> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
{{/notNull}} |
@@ -1,47 +0,0 @@ | |||
<div class="source-viewer-measures-card"> | |||
<div class="measures"> | |||
<div class="measure measure-big" data-metric="violations"> | |||
<span class="measure-value">{{default issuesCount 0}}</span> | |||
<span class="measure-name">{{t 'metric.violations.name'}}</span> | |||
</div> | |||
<div class="measure measure-big" data-metric="sqale_index"> | |||
<span class="measure-value">{{formatMeasure measures.issuesRemediationEffort 'SHORT_WORK_DUR'}}</span> | |||
<span class="measure-name">{{t 'metric.sqale_index.short_name'}}</span> | |||
</div> | |||
</div> | |||
{{#if issuesCount}} | |||
<div class="measures"> | |||
<div class="measures-list"> | |||
{{#each typesFacet}} | |||
<div class="measure measure-one-line"> | |||
<span class="measure-name">{{issueTypeIcon val}} {{issueType val}}</span> | |||
<span class="measure-value">{{formatMeasure count 'SHORT_INT'}}</span> | |||
</div> | |||
{{/each}} | |||
</div> | |||
</div> | |||
<div class="measures"> | |||
<div class="measures-list"> | |||
{{#each severitiesFacet}} | |||
<div class="measure measure-one-line"> | |||
<span class="measure-name">{{severityHelper val}}</span> | |||
<span class="measure-value">{{formatMeasure count 'SHORT_INT'}}</span> | |||
</div> | |||
{{/each}} | |||
</div> | |||
</div> | |||
<div class="measures"> | |||
<div class="measures-list"> | |||
{{#each tagsFacet}} | |||
<div class="measure measure-one-line"> | |||
<span class="measure-name"><i class="icon-tags"></i> {{val}}</span> | |||
<span class="measure-value">{{formatMeasure count 'SHORT_INT'}}</span> | |||
</div> | |||
{{/each}} | |||
</div> | |||
</div> | |||
{{/if}} | |||
</div> |
@@ -1,29 +0,0 @@ | |||
<div class="measures"> | |||
<div class="measures-list"> | |||
<div class="measure measure-one-line" data-metric="lines"> | |||
<span class="measure-name">{{t 'metric.lines.name'}}</span> | |||
<span class="measure-value">{{measures.lines}}</span> | |||
</div> | |||
<div class="measure measure-one-line" data-metric="ncloc"> | |||
<span class="measure-name">{{t 'metric.ncloc.name'}}</span> | |||
<span class="measure-value">{{measures.ncloc}}</span> | |||
</div> | |||
<div class="measure measure-one-line" data-metric="comment_lines"> | |||
<span class="measure-name">{{t 'metric.comment_lines_density.short_name'}}</span> | |||
<span class="measure-value">{{measures.comment_lines_density}} / {{measures.comment_lines}}</span> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="measures"> | |||
<div class="measures-list"> | |||
<div class="measure measure-one-line" data-metric="complexity"> | |||
<span class="measure-name">{{t 'metric.complexity.name'}}</span> | |||
<span class="measure-value">{{measures.complexity}}</span> | |||
</div> | |||
<div class="measure measure-one-line" data-metric="function_complexity"> | |||
<span class="measure-name">{{t 'metric.function_complexity.name'}}</span> | |||
<span class="measure-value">{{measures.function_complexity}}</span> | |||
</div> | |||
</div> | |||
</div> |
@@ -1,72 +0,0 @@ | |||
<div class="source-viewer-measures-section source-viewer-measures-section-big"> | |||
<div class="source-viewer-measures-card source-viewer-measures-card-fixed-height js-test-list"> | |||
<div class="measures"> | |||
<table class="source-viewer-tests-list"> | |||
<tr> | |||
<td class="source-viewer-test-status note" colspan="3"> | |||
{{t 'component_viewer.measure_section.unit_tests'}}<br> | |||
{{t 'component_viewer.tests.ordered_by'}} | |||
<a class="js-sort-tests-by-duration {{#eq testSorting 'duration'}}active-link{{/eq}}"> | |||
{{t 'component_viewer.tests.duration'}}</a> | |||
/ | |||
<a class="js-sort-tests-by-name {{#eq testSorting 'name'}}active-link{{/eq}}"> | |||
{{t 'component_viewer.tests.test_name'}}</a> | |||
/ | |||
<a class="js-sort-tests-by-status {{#eq testSorting 'status'}}active-link{{/eq}}"> | |||
{{t 'component_viewer.tests.status'}}</a> | |||
</td> | |||
<td class="source-viewer-test-covered-lines note">{{t 'component_viewer.covered_lines'}}</td> | |||
</tr> | |||
{{#each tests}} | |||
<tr> | |||
{{#eq status 'SKIPPED'}} | |||
<td class="source-viewer-test-status note">{{testStatusIcon status}}</td> | |||
<td class="source-viewer-test-duration note"></td> | |||
<td class="source-viewer-test-name">{{name}}</td> | |||
<td class="source-viewer-test-covered-lines note"></td> | |||
{{else}} | |||
{{#ifTestData this}} | |||
<td class="source-viewer-test-status note">{{testStatusIcon status}}</td> | |||
<td class="source-viewer-test-duration note">{{durationInMs}}ms</td> | |||
<td class="source-viewer-test-name"><a class="js-show-test" data-id="{{id}}">{{name}}</a></td> | |||
<td class="source-viewer-test-covered-lines note">{{coveredLines}}</td> | |||
{{else}} | |||
<td class="source-viewer-test-status note">{{testStatusIcon status}}</td> | |||
<td class="source-viewer-test-duration note">{{durationInMs}}ms</td> | |||
<td class="source-viewer-test-name">{{name}}</td> | |||
{{/ifTestData}} | |||
{{/eq}} | |||
</tr> | |||
{{/each}} | |||
</table> | |||
</div> | |||
</div> | |||
</div> | |||
{{#if selectedTest}} | |||
<div class="source-viewer-measures-section source-viewer-measures-section-big js-selected-test"> | |||
<div class="source-viewer-measures-card source-viewer-measures-card-fixed-height"> | |||
{{#notEq selectedTest.status 'ERROR'}} | |||
{{#notEq selectedTest.status 'FAILURE'}} | |||
<div class="bubble-popup-title">{{t 'component_viewer.transition.covers'}}</div> | |||
{{#each coveredFiles}} | |||
<div class="bubble-popup-section"> | |||
<a target="_blank" href="{{dashboardUrl key}}" title="{{longName}}">{{longName}}</a> | |||
<span class="note">{{tp 'component_viewer.x_lines_are_covered' coveredLines}}</span> | |||
</div> | |||
{{else}} | |||
{{t 'none'}} | |||
{{/each}} | |||
{{/notEq}} | |||
{{/notEq}} | |||
{{#notEq selectedTest.status 'OK'}} | |||
<div class="bubble-popup-title">{{t 'component_viewer.details'}}</div> | |||
{{#if selectedTest.message}} | |||
<pre>{{selectedTest.message}}</pre> | |||
{{/if}} | |||
<pre>{{selectedTest.stacktrace}}</pre> | |||
{{/notEq}} | |||
</div> | |||
</div> | |||
{{/if}} |
@@ -1,40 +0,0 @@ | |||
<div class="source-viewer-measures-card"> | |||
<div class="measures"> | |||
<div class="measures-list"> | |||
<div class="measure measure-big" data-metric="tests"> | |||
<span class="measure-name">{{t 'metric.tests.name'}}</span> | |||
<span class="measure-value">{{measures.tests}}</span> | |||
</div> | |||
{{#notNull measures.test_success_density}} | |||
<div class="measure measure-one-line" data-metric="test_success_density"> | |||
<span class="measure-name">{{t 'metric.test_success_density.name'}}</span> | |||
<span class="measure-value">{{measures.test_success_density}}</span> | |||
</div> | |||
{{/notNull}} | |||
{{#notNull measures.test_failures}} | |||
<div class="measure measure-one-line" data-metric="test_failures"> | |||
<span class="measure-name">{{t 'metric.test_failures.name'}}</span> | |||
<span class="measure-value">{{measures.test_failures}}</span> | |||
</div> | |||
{{/notNull}} | |||
{{#notNull measures.test_errors}} | |||
<div class="measure measure-one-line" data-metric="test_errors"> | |||
<span class="measure-name">{{t 'metric.test_errors.name'}}</span> | |||
<span class="measure-value">{{measures.test_errors}}</span> | |||
</div> | |||
{{/notNull}} | |||
{{#notNull measures.skipped_tests}} | |||
<div class="measure measure-one-line" data-metric="skipped_tests"> | |||
<span class="measure-name">{{t 'metric.skipped_tests.name'}}</span> | |||
<span class="measure-value">{{measures.skipped_tests}}</span> | |||
</div> | |||
{{/notNull}} | |||
{{#notNull measures.test_execution_time}} | |||
<div class="measure measure-one-line" data-metric="test_execution_time"> | |||
<span class="measure-name">{{t 'metric.test_execution_time.name'}}</span> | |||
<span class="measure-value">{{measures.test_execution_time}}</span> | |||
</div> | |||
{{/notNull}} | |||
</div> | |||
</div> | |||
</div> |
@@ -1,68 +0,0 @@ | |||
<div class="modal-container source-viewer-measures-modal"> | |||
<div class="source-viewer-header-component source-viewer-measures-component"> | |||
{{#unless removed}} | |||
{{#if projectName}} | |||
<div class="source-viewer-header-component-project"> | |||
{{qualifierIcon 'TRK'}} <a href="{{dashboardUrl project}}">{{projectName}}</a> | |||
{{#if subProjectName}} | |||
| |||
{{qualifierIcon 'TRK'}} <a href="{{dashboardUrl subProject}}">{{subProjectName}}</a> | |||
{{/if}} | |||
</div> | |||
{{/if}} | |||
<div class="source-viewer-header-component-name"> | |||
{{qualifierIcon q}} {{default path longName}} | |||
</div> | |||
{{else}} | |||
<div class="source-viewer-header-component-project removed">{{removedMessage}}</div> | |||
{{/unless}} | |||
</div> | |||
{{#eq q 'UTS'}} | |||
<div class="source-viewer-measures"> | |||
<div class="source-viewer-measures-section"> | |||
{{> '_source-viewer-measures-tests'}} | |||
</div> | |||
</div> | |||
<div class="source-viewer-measures"> | |||
{{> '_source-viewer-measures-test-cases'}} | |||
</div> | |||
{{else}} | |||
<div class="source-viewer-measures"> | |||
<div class="source-viewer-measures-section"> | |||
<div class="source-viewer-measures-card"> | |||
{{> '_source-viewer-measures-lines'}} | |||
</div> | |||
</div> | |||
<div class="source-viewer-measures-section"> | |||
{{> '_source-viewer-measures-issues'}} | |||
</div> | |||
{{#if measures.coverage}} | |||
<div class="source-viewer-measures-section"> | |||
<div class="source-viewer-measures-card"> | |||
{{> '_source-viewer-measures-coverage'}} | |||
</div> | |||
</div> | |||
{{/if}} | |||
<div class="source-viewer-measures-section"> | |||
{{> '_source-viewer-measures-duplications'}} | |||
</div> | |||
</div> | |||
{{/eq}} | |||
<div class="spacer-bottom"> </div> | |||
<a class="js-show-all-measures">{{t 'component_viewer.show_all_measures'}}</a> | |||
<div class="source-viewer-measures source-viewer-measures-secondary js-all-measures hidden"> | |||
{{> '_source-viewer-measures-all'}} | |||
</div> | |||
</div> | |||
<div class="modal-foot"> | |||
<a class="js-modal-close" href="#">{{t 'close'}}</a> | |||
</div> |
@@ -27,18 +27,26 @@ import { formatMeasure } from '../../helpers/measures'; | |||
interface Props { | |||
className?: string; | |||
decimals?: number | null; | |||
value?: string; | |||
metricKey: string; | |||
metricType: string; | |||
small?: boolean; | |||
value: string | undefined; | |||
} | |||
export default function Measure({ className, decimals, metricKey, metricType, value }: Props) { | |||
export default function Measure({ | |||
className, | |||
decimals, | |||
metricKey, | |||
metricType, | |||
small, | |||
value | |||
}: Props) { | |||
if (value === undefined) { | |||
return <span>{'–'}</span>; | |||
} | |||
if (metricType === 'LEVEL') { | |||
return <Level className={className} level={value} />; | |||
return <Level className={className} level={value} small={small} />; | |||
} | |||
if (metricType !== 'RATING') { | |||
@@ -47,7 +55,7 @@ export default function Measure({ className, decimals, metricKey, metricType, va | |||
} | |||
const tooltip = getRatingTooltip(metricKey, Number(value)); | |||
const rating = <Rating value={value} />; | |||
const rating = <Rating small={small} value={value} />; | |||
if (tooltip) { | |||
return ( | |||
<Tooltips overlay={tooltip}> |
@@ -58,5 +58,7 @@ it('renders unknown RATING', () => { | |||
}); | |||
it('renders undefined measure', () => { | |||
expect(shallow(<Measure metricKey="foo" metricType="PERCENT" />)).toMatchSnapshot(); | |||
expect( | |||
shallow(<Measure metricKey="foo" metricType="PERCENT" value={undefined} />) | |||
).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,30 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
interface Props { | |||
className?: string; | |||
status: string; | |||
} | |||
export default function TestStatusIcon({ className, status }: Props) { | |||
return <i className={classNames('icon-test-status-' + status.toLowerCase(), className)} />; | |||
} |
@@ -365,3 +365,7 @@ export function getRatingTooltip(metricKey: string, value: number | string): str | |||
? getMaintainabilityRatingTooltip(Number(value)) | |||
: translate('metric', finalMetricKey, 'tooltip', ratingLetter); | |||
} | |||
export function getDisplayMetrics(metrics: Metric[]) { | |||
return metrics.filter(metric => !metric.hidden && !['DATA', 'DISTRIB'].includes(metric.type)); | |||
} |
@@ -1915,6 +1915,7 @@ metric.usability.description=Usability | |||
metric.usability.name=Usability | |||
metric.violations.description=Issues | |||
metric.violations.name=Issues | |||
metric.violations.short_name=Issues | |||
metric.vulnerabilities.description=Vulnerabilities | |||
metric.vulnerabilities.name=Vulnerabilities | |||
metric.wont_fix_issues.description=Won't fix issues |