Browse Source

review source viewer measures overlay in react (#3084)

tags/7.5
Stas Vilchik 6 years ago
parent
commit
d6d2b5824b
No account linked to committer's email address
36 changed files with 3376 additions and 726 deletions
  1. 12
    2
      server/sonar-web/src/main/js/api/issues.ts
  2. 10
    10
      server/sonar-web/src/main/js/api/tests.ts
  3. 43
    0
      server/sonar-web/src/main/js/app/types.ts
  4. 3
    4
      server/sonar-web/src/main/js/apps/component-measures/components/App.js
  5. 1
    15
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
  6. 47
    43
      server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
  7. 459
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx
  8. 115
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx
  9. 57
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayMeasure.tsx
  10. 62
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCase.tsx
  11. 181
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCases.tsx
  12. 172
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx
  13. 55
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayCoveredFiles-test.tsx
  14. 48
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayMeasure-test.tsx
  15. 42
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCase-test.tsx
  16. 74
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCases-test.tsx
  17. 1535
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap
  18. 76
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap
  19. 53
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayMeasure-test.tsx.snap
  20. 34
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCase-test.tsx.snap
  21. 247
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCases-test.tsx.snap
  22. 0
    1
      server/sonar-web/src/main/js/components/SourceViewer/styles.css
  23. 0
    282
      server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js
  24. 0
    42
      server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-all.hbs
  25. 0
    36
      server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-coverage.hbs
  26. 0
    30
      server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-duplications.hbs
  27. 0
    47
      server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-issues.hbs
  28. 0
    29
      server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-lines.hbs
  29. 0
    72
      server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-test-cases.hbs
  30. 0
    40
      server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-tests.hbs
  31. 0
    68
      server/sonar-web/src/main/js/components/SourceViewer/views/templates/source-viewer-measures.hbs
  32. 12
    4
      server/sonar-web/src/main/js/components/measure/Measure.tsx
  33. 3
    1
      server/sonar-web/src/main/js/components/measure/__tests__/Measure-test.tsx
  34. 30
    0
      server/sonar-web/src/main/js/components/shared/TestStatusIcon.tsx
  35. 4
    0
      server/sonar-web/src/main/js/helpers/measures.ts
  36. 1
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 12
- 2
server/sonar-web/src/main/js/api/issues.ts View File

@@ -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(),

+ 10
- 10
server/sonar-web/src/main/js/api/tests.ts View File

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

+ 43
- 0
server/sonar-web/src/main/js/app/types.ts View File

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

+ 3
- 4
server/sonar-web/src/main/js/apps/component-measures/components/App.js View File

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

+ 1
- 15
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js View File

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

server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js → server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx View File

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

+ 459
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx View File

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

+ 115
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx View File

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

+ 57
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayMeasure.tsx View File

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

+ 62
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCase.tsx View File

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

+ 181
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCases.tsx View File

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

+ 172
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx View File

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

+ 55
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayCoveredFiles-test.tsx View File

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

+ 48
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayMeasure-test.tsx View File

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

+ 42
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCase-test.tsx View File

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

+ 74
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCases-test.tsx View File

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

+ 1535
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap
File diff suppressed because it is too large
View File


+ 76
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap View File

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

+ 53
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayMeasure-test.tsx.snap View File

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

+ 34
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCase-test.tsx.snap View File

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

+ 247
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCases-test.tsx.snap View File

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

+ 0
- 1
server/sonar-web/src/main/js/components/SourceViewer/styles.css View File

@@ -567,7 +567,6 @@

.measure-big .measure-value {
font-size: 22px;
font-weight: 300;
}

.measure-big .rating {

+ 0
- 282
server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js View File

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

+ 0
- 42
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-all.hbs View File

@@ -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">&nbsp;{{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">&nbsp;{{value}}</span>
</div>
{{/each}}
</div>
</div>
</div>
{{/eachOdd}}
</div>
{{/notEmpty}}


+ 0
- 36
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-coverage.hbs View File

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

+ 0
- 30
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-duplications.hbs View File

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

+ 0
- 47
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-issues.hbs View File

@@ -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>&nbsp;{{val}}</span>
<span class="measure-value">{{formatMeasure count 'SHORT_INT'}}</span>
</div>
{{/each}}
</div>
</div>
{{/if}}
</div>

+ 0
- 29
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-lines.hbs View File

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

+ 0
- 72
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-test-cases.hbs View File

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

+ 0
- 40
server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-tests.hbs View File

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

+ 0
- 68
server/sonar-web/src/main/js/components/SourceViewer/views/templates/source-viewer-measures.hbs View File

@@ -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'}}&nbsp;<a href="{{dashboardUrl project}}">{{projectName}}</a>
{{#if subProjectName}}
&nbsp;&nbsp;&nbsp;
{{qualifierIcon 'TRK'}}&nbsp;<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">&nbsp;</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>

+ 12
- 4
server/sonar-web/src/main/js/components/measure/Measure.tsx View File

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

+ 3
- 1
server/sonar-web/src/main/js/components/measure/__tests__/Measure-test.tsx View File

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

+ 30
- 0
server/sonar-web/src/main/js/components/shared/TestStatusIcon.tsx View File

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

+ 4
- 0
server/sonar-web/src/main/js/helpers/measures.ts View File

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

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

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

Loading…
Cancel
Save