aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2018-02-21 13:32:25 +0100
committerGitHub <noreply@github.com>2018-02-21 13:32:25 +0100
commitd6d2b5824be0c018ad4fc606f01abfb9f266c804 (patch)
treef6e8fde0634860a56c9f6d43107187cf21a0b2f3
parentcaf48455f693b76d06ebd2328d8498442cb0c1ab (diff)
downloadsonarqube-d6d2b5824be0c018ad4fc606f01abfb9f266c804.tar.gz
sonarqube-d6d2b5824be0c018ad4fc606f01abfb9f266c804.zip
review source viewer measures overlay in react (#3084)
-rw-r--r--server/sonar-web/src/main/js/api/issues.ts14
-rw-r--r--server/sonar-web/src/main/js/api/tests.ts20
-rw-r--r--server/sonar-web/src/main/js/app/types.ts43
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/App.js7
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js16
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx (renamed from server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js)90
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx459
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx115
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayMeasure.tsx57
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCase.tsx62
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCases.tsx181
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx172
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayCoveredFiles-test.tsx55
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayMeasure-test.tsx48
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCase-test.tsx42
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCases-test.tsx74
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap1535
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap76
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayMeasure-test.tsx.snap53
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCase-test.tsx.snap34
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCases-test.tsx.snap247
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/styles.css1
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js282
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-all.hbs42
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-coverage.hbs36
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-duplications.hbs30
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-issues.hbs47
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-lines.hbs29
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-test-cases.hbs72
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-tests.hbs40
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/views/templates/source-viewer-measures.hbs68
-rw-r--r--server/sonar-web/src/main/js/components/measure/Measure.tsx16
-rw-r--r--server/sonar-web/src/main/js/components/measure/__tests__/Measure-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/shared/TestStatusIcon.tsx30
-rw-r--r--server/sonar-web/src/main/js/helpers/measures.ts4
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties1
36 files changed, 3376 insertions, 726 deletions
diff --git a/server/sonar-web/src/main/js/api/issues.ts b/server/sonar-web/src/main/js/api/issues.ts
index de740e4190c..9830e627e54 100644
--- a/server/sonar-web/src/main/js/api/issues.ts
+++ b/server/sonar-web/src/main/js/api/issues.ts
@@ -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(),
diff --git a/server/sonar-web/src/main/js/api/tests.ts b/server/sonar-web/src/main/js/api/tests.ts
index e39aa2ff45e..1ac56218630 100644
--- a/server/sonar-web/src/main/js/api/tests.ts
+++ b/server/sonar-web/src/main/js/api/tests.ts
@@ -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);
}
diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts
index 4ec38c6b961..5d552e722a2 100644
--- a/server/sonar-web/src/main/js/app/types.ts
+++ b/server/sonar-web/src/main/js/app/types.ts
@@ -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;
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.js b/server/sonar-web/src/main/js/apps/component-measures/components/App.js
index dbd68e8f276..4198c6689fd 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/App.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.js
@@ -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) {
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
index 2022165e43f..6f267452fa0 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
@@ -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')}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
index 253f8db02ac..a29348b9312 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx
new file mode 100644
index 00000000000..131c58b1a4f
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx
new file mode 100644
index 00000000000..f89a9aa408b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayCoveredFiles.tsx
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayMeasure.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayMeasure.tsx
new file mode 100644
index 00000000000..7a41a9ef001
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayMeasure.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCase.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCase.tsx
new file mode 100644
index 00000000000..17b3e455ba0
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCase.tsx
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCases.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCases.tsx
new file mode 100644
index 00000000000..64dca78addc
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCases.tsx
@@ -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]);
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx
new file mode 100644
index 00000000000..1f94f1375d3
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayCoveredFiles-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayCoveredFiles-test.tsx
new file mode 100644
index 00000000000..4be9b29f714
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayCoveredFiles-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayMeasure-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayMeasure-test.tsx
new file mode 100644
index 00000000000..c16c841c841
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayMeasure-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCase-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCase-test.tsx
new file mode 100644
index 00000000000..6c51c7c3433
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCase-test.tsx
@@ -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');
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCases-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCases-test.tsx
new file mode 100644
index 00000000000..72226708c6f
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCases-test.tsx
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap
new file mode 100644
index 00000000000..bd7eb4c902b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap
@@ -0,0 +1,1535 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render source file 1`] = `
+<Modal
+ contentLabel=""
+ large={true}
+ onRequestClose={[MockFunction]}
+>
+ <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
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": "branch",
+ "id": "project-key",
+ },
+ }
+ }
+ >
+ Project Name
+ </Link>
+ <React.Fragment>
+ <QualifierIcon
+ className="big-spacer-left little-spacer-right"
+ qualifier="BRC"
+ />
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": "branch",
+ "id": "sub-project-key",
+ },
+ }
+ }
+ >
+ Sub-Project Name
+ </Link>
+ </React.Fragment>
+ </div>
+ <div
+ className="source-viewer-header-component-name"
+ >
+ <QualifierIcon
+ className="little-spacer-right"
+ qualifier="FIL"
+ />
+ src/file.js
+ </div>
+ </div>
+ <React.Fragment>
+ <div
+ className="source-viewer-measures"
+ >
+ <div
+ className="source-viewer-measures-section"
+ >
+ <div
+ className="source-viewer-measures-card"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <MeasuresOverlayMeasure
+ key="lines"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "lines",
+ "type": "INT",
+ },
+ "value": "18",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="ncloc"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "ncloc",
+ "type": "INT",
+ },
+ "value": "8",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="comment_lines"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "comment_lines",
+ "type": "INT",
+ },
+ "value": "2",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="comment_lines_density"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "comment_lines_density",
+ "type": "PERCENT",
+ },
+ "value": "20.0",
+ }
+ }
+ />
+ </div>
+ </div>
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <MeasuresOverlayMeasure
+ key="cognitive_complexity"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Complexity",
+ "key": "cognitive_complexity",
+ "type": "INT",
+ },
+ "value": "0",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="complexity"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Complexity",
+ "key": "complexity",
+ "type": "INT",
+ },
+ "value": "1",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ className="source-viewer-measures-section"
+ >
+ <div
+ className="source-viewer-measures-card"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measure measure-big"
+ data-metric="sqale_index"
+ >
+ <span
+ className="measure-value"
+ >
+ <Measure
+ metricKey="sqale_index"
+ metricType="WORK_DUR"
+ value="40"
+ />
+ </span>
+ <span
+ className="measure-name"
+ >
+ sqale_index
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ className="source-viewer-measures-section"
+ >
+ <div
+ className="source-viewer-measures-card"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-chart"
+ >
+ <CoverageRating
+ size="big"
+ value="75.0"
+ />
+ </div>
+ <div
+ className="measure measure-big"
+ data-metric="coverage"
+ >
+ <span
+ className="measure-value"
+ >
+ <Measure
+ metricKey="coverage"
+ metricType="PERCENT"
+ value="75.0"
+ />
+ </span>
+ <span
+ className="measure-name"
+ >
+ coverage
+ </span>
+ </div>
+ </div>
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <MeasuresOverlayMeasure
+ key="uncovered_lines"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Coverage",
+ "key": "uncovered_lines",
+ "type": "INT",
+ },
+ "value": "1",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="lines_to_cover"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Coverage",
+ "key": "lines_to_cover",
+ "type": "INT",
+ },
+ "value": "4",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ className="source-viewer-measures-section"
+ >
+ <div
+ className="source-viewer-measures-card"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-chart"
+ >
+ <DuplicationsRating
+ muted={false}
+ size="big"
+ value={0}
+ />
+ </div>
+ <div
+ className="measure measure-big"
+ data-metric="duplicated_lines_density"
+ >
+ <span
+ className="measure-value"
+ >
+ <Measure
+ metricKey="duplicated_lines_density"
+ metricType="PERCENT"
+ value="0.0"
+ />
+ </span>
+ <span
+ className="measure-name"
+ >
+ duplicated_lines_density
+ </span>
+ </div>
+ </div>
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <MeasuresOverlayMeasure
+ key="duplicated_blocks"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Duplications",
+ "key": "duplicated_blocks",
+ "type": "INT",
+ },
+ "value": "3",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="duplicated_lines"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Duplications",
+ "key": "duplicated_lines",
+ "type": "INT",
+ },
+ "value": "0",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </React.Fragment>
+ <div
+ className="spacer-top"
+ >
+ <a
+ className="js-show-all-measures"
+ href="#"
+ onClick={[Function]}
+ >
+ component_viewer.show_all_measures
+ </a>
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ className="button-link"
+ onClick={[Function]}
+ type="button"
+ >
+ close
+ </button>
+ </footer>
+</Modal>
+`;
+
+exports[`should render source file 2`] = `
+<Modal
+ contentLabel=""
+ large={true}
+ onRequestClose={[MockFunction]}
+>
+ <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
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": "branch",
+ "id": "project-key",
+ },
+ }
+ }
+ >
+ Project Name
+ </Link>
+ <React.Fragment>
+ <QualifierIcon
+ className="big-spacer-left little-spacer-right"
+ qualifier="BRC"
+ />
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": "branch",
+ "id": "sub-project-key",
+ },
+ }
+ }
+ >
+ Sub-Project Name
+ </Link>
+ </React.Fragment>
+ </div>
+ <div
+ className="source-viewer-header-component-name"
+ >
+ <QualifierIcon
+ className="little-spacer-right"
+ qualifier="FIL"
+ />
+ src/file.js
+ </div>
+ </div>
+ <React.Fragment>
+ <div
+ className="source-viewer-measures"
+ >
+ <div
+ className="source-viewer-measures-section"
+ >
+ <div
+ className="source-viewer-measures-card"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <MeasuresOverlayMeasure
+ key="lines"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "lines",
+ "type": "INT",
+ },
+ "value": "18",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="ncloc"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "ncloc",
+ "type": "INT",
+ },
+ "value": "8",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="comment_lines"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "comment_lines",
+ "type": "INT",
+ },
+ "value": "2",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="comment_lines_density"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "comment_lines_density",
+ "type": "PERCENT",
+ },
+ "value": "20.0",
+ }
+ }
+ />
+ </div>
+ </div>
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <MeasuresOverlayMeasure
+ key="cognitive_complexity"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Complexity",
+ "key": "cognitive_complexity",
+ "type": "INT",
+ },
+ "value": "0",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="complexity"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Complexity",
+ "key": "complexity",
+ "type": "INT",
+ },
+ "value": "1",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ className="source-viewer-measures-section"
+ >
+ <div
+ className="source-viewer-measures-card"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measure measure-big"
+ data-metric="sqale_index"
+ >
+ <span
+ className="measure-value"
+ >
+ <Measure
+ metricKey="sqale_index"
+ metricType="WORK_DUR"
+ value="40"
+ />
+ </span>
+ <span
+ className="measure-name"
+ >
+ sqale_index
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ className="source-viewer-measures-section"
+ >
+ <div
+ className="source-viewer-measures-card"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-chart"
+ >
+ <CoverageRating
+ size="big"
+ value="75.0"
+ />
+ </div>
+ <div
+ className="measure measure-big"
+ data-metric="coverage"
+ >
+ <span
+ className="measure-value"
+ >
+ <Measure
+ metricKey="coverage"
+ metricType="PERCENT"
+ value="75.0"
+ />
+ </span>
+ <span
+ className="measure-name"
+ >
+ coverage
+ </span>
+ </div>
+ </div>
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <MeasuresOverlayMeasure
+ key="uncovered_lines"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Coverage",
+ "key": "uncovered_lines",
+ "type": "INT",
+ },
+ "value": "1",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="lines_to_cover"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Coverage",
+ "key": "lines_to_cover",
+ "type": "INT",
+ },
+ "value": "4",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ className="source-viewer-measures-section"
+ >
+ <div
+ className="source-viewer-measures-card"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-chart"
+ >
+ <DuplicationsRating
+ muted={false}
+ size="big"
+ value={0}
+ />
+ </div>
+ <div
+ className="measure measure-big"
+ data-metric="duplicated_lines_density"
+ >
+ <span
+ className="measure-value"
+ >
+ <Measure
+ metricKey="duplicated_lines_density"
+ metricType="PERCENT"
+ value="0.0"
+ />
+ </span>
+ <span
+ className="measure-name"
+ >
+ duplicated_lines_density
+ </span>
+ </div>
+ </div>
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <MeasuresOverlayMeasure
+ key="duplicated_blocks"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Duplications",
+ "key": "duplicated_blocks",
+ "type": "INT",
+ },
+ "value": "3",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="duplicated_lines"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Duplications",
+ "key": "duplicated_lines",
+ "type": "INT",
+ },
+ "value": "0",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </React.Fragment>
+ <div
+ className="spacer-top"
+ >
+ <div
+ className="source-viewer-measures source-viewer-measures-secondary js-all-measures"
+ >
+ <div
+ className="source-viewer-measures-section source-viewer-measures-section-big"
+ >
+ <div
+ className="source-viewer-measures-card"
+ key="Complexity"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <div
+ className="measure measure-one-line measure-big"
+ >
+ <span
+ className="measure-name"
+ >
+ Complexity
+ </span>
+ </div>
+ <MeasuresOverlayMeasure
+ key="cognitive_complexity"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Complexity",
+ "key": "cognitive_complexity",
+ "type": "INT",
+ },
+ "value": "0",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="complexity"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Complexity",
+ "key": "complexity",
+ "type": "INT",
+ },
+ "value": "1",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="source-viewer-measures-card"
+ key="Size"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <div
+ className="measure measure-one-line measure-big"
+ >
+ <span
+ className="measure-name"
+ >
+ Size
+ </span>
+ </div>
+ <MeasuresOverlayMeasure
+ key="classes"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "classes",
+ "type": "INT",
+ },
+ "value": "1",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="comment_lines"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "comment_lines",
+ "type": "INT",
+ },
+ "value": "2",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="comment_lines_density"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "comment_lines_density",
+ "type": "PERCENT",
+ },
+ "value": "20.0",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="files"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "files",
+ "type": "INT",
+ },
+ "value": "1",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="functions"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "functions",
+ "type": "INT",
+ },
+ "value": "1",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="lines"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "lines",
+ "type": "INT",
+ },
+ "value": "18",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="ncloc"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "ncloc",
+ "type": "INT",
+ },
+ "value": "8",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="statements"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Size",
+ "key": "statements",
+ "type": "INT",
+ },
+ "value": "3",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="source-viewer-measures-card"
+ key="Coverage"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <div
+ className="measure measure-one-line measure-big"
+ >
+ <span
+ className="measure-name"
+ >
+ Coverage
+ </span>
+ </div>
+ <MeasuresOverlayMeasure
+ key="coverage"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Coverage",
+ "key": "coverage",
+ "type": "PERCENT",
+ },
+ "value": "75.0",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="line_coverage"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Coverage",
+ "key": "line_coverage",
+ "type": "PERCENT",
+ },
+ "value": "75.0",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="lines_to_cover"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Coverage",
+ "key": "lines_to_cover",
+ "type": "INT",
+ },
+ "value": "4",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="uncovered_lines"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Coverage",
+ "key": "uncovered_lines",
+ "type": "INT",
+ },
+ "value": "1",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="source-viewer-measures-card"
+ key="Reliability"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <div
+ className="measure measure-one-line measure-big"
+ >
+ <span
+ className="measure-name"
+ >
+ Reliability
+ </span>
+ </div>
+ <MeasuresOverlayMeasure
+ key="bugs"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Reliability",
+ "key": "bugs",
+ "type": "INT",
+ },
+ "value": "0",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="reliability_rating"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Reliability",
+ "key": "reliability_rating",
+ "type": "RATING",
+ },
+ "value": "1.0",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ className="source-viewer-measures-section source-viewer-measures-section-big"
+ >
+ <div
+ className="source-viewer-measures-card"
+ key="Security"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <div
+ className="measure measure-one-line measure-big"
+ >
+ <span
+ className="measure-name"
+ >
+ Security
+ </span>
+ </div>
+ <MeasuresOverlayMeasure
+ key="security_rating"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Security",
+ "key": "security_rating",
+ "type": "RATING",
+ },
+ "value": "1.0",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="vulnerabilities"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Security",
+ "key": "vulnerabilities",
+ "type": "INT",
+ },
+ "value": "0",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="source-viewer-measures-card"
+ key="Tests"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <div
+ className="measure measure-one-line measure-big"
+ >
+ <span
+ className="measure-name"
+ >
+ Tests
+ </span>
+ </div>
+ <MeasuresOverlayMeasure
+ key="skipped_tests"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Tests",
+ "key": "skipped_tests",
+ "type": "INT",
+ },
+ "value": "0",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="test_errors"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Tests",
+ "key": "test_errors",
+ "type": "INT",
+ },
+ "value": "1",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="test_failures"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Tests",
+ "key": "test_failures",
+ "type": "INT",
+ },
+ "value": "0",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="test_success_density"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Tests",
+ "key": "test_success_density",
+ "type": "PERCENT",
+ },
+ "value": "100.0",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="source-viewer-measures-card"
+ key="Issues"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <div
+ className="measure measure-one-line measure-big"
+ >
+ <span
+ className="measure-name"
+ >
+ Issues
+ </span>
+ </div>
+ <MeasuresOverlayMeasure
+ key="false_positive_issues"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Issues",
+ "key": "false_positive_issues",
+ "type": "INT",
+ },
+ "value": "0",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="wont_fix_issues"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Issues",
+ "key": "wont_fix_issues",
+ "type": "INT",
+ },
+ "value": "0",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="source-viewer-measures-card"
+ key="Duplications"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <div
+ className="measure measure-one-line measure-big"
+ >
+ <span
+ className="measure-name"
+ >
+ Duplications
+ </span>
+ </div>
+ <MeasuresOverlayMeasure
+ key="duplicated_blocks"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Duplications",
+ "key": "duplicated_blocks",
+ "type": "INT",
+ },
+ "value": "3",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="duplicated_files"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Duplications",
+ "key": "duplicated_files",
+ "type": "INT",
+ },
+ "value": "1",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="duplicated_lines"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Duplications",
+ "key": "duplicated_lines",
+ "type": "INT",
+ },
+ "value": "0",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="duplicated_lines_density"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Duplications",
+ "key": "duplicated_lines_density",
+ "type": "PERCENT",
+ },
+ "value": "0.0",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="source-viewer-measures-card"
+ key="Maintainability"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <div
+ className="measure measure-one-line measure-big"
+ >
+ <span
+ className="measure-name"
+ >
+ Maintainability
+ </span>
+ </div>
+ <MeasuresOverlayMeasure
+ key="code_smells"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Maintainability",
+ "key": "code_smells",
+ "type": "INT",
+ },
+ "value": "2",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="sqale_debt_ratio"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Maintainability",
+ "key": "sqale_debt_ratio",
+ "type": "PERCENT",
+ },
+ "value": "16.7",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="sqale_index"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Maintainability",
+ "key": "sqale_index",
+ "type": "WORK_DUR",
+ },
+ "value": "40",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ className="button-link"
+ onClick={[Function]}
+ type="button"
+ >
+ close
+ </button>
+ </footer>
+</Modal>
+`;
+
+exports[`should render test file 1`] = `
+<Modal
+ contentLabel=""
+ large={true}
+ onRequestClose={[MockFunction]}
+>
+ <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
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": "branch",
+ "id": "project-key",
+ },
+ }
+ }
+ >
+ Project Name
+ </Link>
+ <React.Fragment>
+ <QualifierIcon
+ className="big-spacer-left little-spacer-right"
+ qualifier="BRC"
+ />
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": "branch",
+ "id": "sub-project-key",
+ },
+ }
+ }
+ >
+ Sub-Project Name
+ </Link>
+ </React.Fragment>
+ </div>
+ <div
+ className="source-viewer-header-component-name"
+ >
+ <QualifierIcon
+ className="little-spacer-right"
+ qualifier="UTS"
+ />
+ src/file.js
+ </div>
+ </div>
+ <React.Fragment>
+ <React.Fragment>
+ <div
+ className="source-viewer-measures"
+ >
+ <div
+ className="source-viewer-measures-section"
+ >
+ <div
+ className="source-viewer-measures-card"
+ >
+ <div
+ className="measures"
+ >
+ <div
+ className="measures-list"
+ >
+ <MeasuresOverlayMeasure
+ key="test_success_density"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Tests",
+ "key": "test_success_density",
+ "type": "PERCENT",
+ },
+ "value": "100.0",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="test_failures"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Tests",
+ "key": "test_failures",
+ "type": "INT",
+ },
+ "value": "0",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="test_errors"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Tests",
+ "key": "test_errors",
+ "type": "INT",
+ },
+ "value": "1",
+ }
+ }
+ />
+ <MeasuresOverlayMeasure
+ key="skipped_tests"
+ measure={
+ Object {
+ "metric": Object {
+ "domain": "Tests",
+ "key": "skipped_tests",
+ "type": "INT",
+ },
+ "value": "0",
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <MeasuresOverlayTestCases
+ branch="branch"
+ componentKey="component-key"
+ />
+ </React.Fragment>
+ </React.Fragment>
+ <div
+ className="spacer-top"
+ >
+ <a
+ className="js-show-all-measures"
+ href="#"
+ onClick={[Function]}
+ >
+ component_viewer.show_all_measures
+ </a>
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ className="button-link"
+ onClick={[Function]}
+ type="button"
+ >
+ close
+ </button>
+ </footer>
+</Modal>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap
new file mode 100644
index 00000000000..c578435451a
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayMeasure-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayMeasure-test.tsx.snap
new file mode 100644
index 00000000000..4612ee4034a
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayMeasure-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCase-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCase-test.tsx.snap
new file mode 100644
index 00000000000..02553e953e9
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCase-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCases-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCases-test.tsx.snap
new file mode 100644
index 00000000000..78316c348a8
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayTestCases-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/styles.css b/server/sonar-web/src/main/js/components/SourceViewer/styles.css
index 2c98827855f..3bcdcdf4415 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/styles.css
+++ b/server/sonar-web/src/main/js/components/SourceViewer/styles.css
@@ -567,7 +567,6 @@
.measure-big .measure-value {
font-size: 22px;
- font-weight: 300;
}
.measure-big .rating {
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js b/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js
deleted file mode 100644
index 925e533b87f..00000000000
--- a/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js
+++ /dev/null
@@ -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 || []
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-all.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-all.hbs
deleted file mode 100644
index cac99fea52d..00000000000
--- a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-all.hbs
+++ /dev/null
@@ -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}}
-
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-coverage.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-coverage.hbs
deleted file mode 100644
index 2598e962d77..00000000000
--- a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-coverage.hbs
+++ /dev/null
@@ -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}}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-duplications.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-duplications.hbs
deleted file mode 100644
index 86ddc9e1edc..00000000000
--- a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-duplications.hbs
+++ /dev/null
@@ -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}}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-issues.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-issues.hbs
deleted file mode 100644
index 30a39146750..00000000000
--- a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-issues.hbs
+++ /dev/null
@@ -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>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-lines.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-lines.hbs
deleted file mode 100644
index 28464266c79..00000000000
--- a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-lines.hbs
+++ /dev/null
@@ -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>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-test-cases.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-test-cases.hbs
deleted file mode 100644
index 894ab99d1a7..00000000000
--- a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-test-cases.hbs
+++ /dev/null
@@ -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}}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-tests.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-tests.hbs
deleted file mode 100644
index c9b33c392ac..00000000000
--- a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-tests.hbs
+++ /dev/null
@@ -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>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/source-viewer-measures.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/source-viewer-measures.hbs
deleted file mode 100644
index ebcc2e79b13..00000000000
--- a/server/sonar-web/src/main/js/components/SourceViewer/views/templates/source-viewer-measures.hbs
+++ /dev/null
@@ -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>
diff --git a/server/sonar-web/src/main/js/components/measure/Measure.tsx b/server/sonar-web/src/main/js/components/measure/Measure.tsx
index 3437fa9616f..8cccb3cd5d6 100644
--- a/server/sonar-web/src/main/js/components/measure/Measure.tsx
+++ b/server/sonar-web/src/main/js/components/measure/Measure.tsx
@@ -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}>
diff --git a/server/sonar-web/src/main/js/components/measure/__tests__/Measure-test.tsx b/server/sonar-web/src/main/js/components/measure/__tests__/Measure-test.tsx
index 84aaf15fdb3..cbfe52cb4bf 100644
--- a/server/sonar-web/src/main/js/components/measure/__tests__/Measure-test.tsx
+++ b/server/sonar-web/src/main/js/components/measure/__tests__/Measure-test.tsx
@@ -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();
});
diff --git a/server/sonar-web/src/main/js/components/shared/TestStatusIcon.tsx b/server/sonar-web/src/main/js/components/shared/TestStatusIcon.tsx
new file mode 100644
index 00000000000..c54fca5654b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/shared/TestStatusIcon.tsx
@@ -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)} />;
+}
diff --git a/server/sonar-web/src/main/js/helpers/measures.ts b/server/sonar-web/src/main/js/helpers/measures.ts
index da64a3d6654..928fc79a103 100644
--- a/server/sonar-web/src/main/js/helpers/measures.ts
+++ b/server/sonar-web/src/main/js/helpers/measures.ts
@@ -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));
+}
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 780c65b1206..8144c5c5190 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -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