From: Stas Vilchik Date: Wed, 21 Feb 2018 12:32:25 +0000 (+0100) Subject: review source viewer measures overlay in react (#3084) X-Git-Tag: 7.5~1642 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=d6d2b5824be0c018ad4fc606f01abfb9f266c804;p=sonarqube.git review source viewer measures overlay in react (#3084) --- 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 { return getJSON('/api/issues/search', query); } -export function getFacets(query: RequestData, facets: string[]): Promise { +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 { + 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 (
(this.node = node)}> - + {sourceRemoved && (
{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.js deleted file mode 100644 index 253f8db02ac..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js +++ /dev/null @@ -1,222 +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. - */ -// @flow -import React from 'react'; -import { Link } from 'react-router'; -import QualifierIcon from '../shared/QualifierIcon'; -import FavoriteContainer from '../controls/FavoriteContainer'; -import { getPathUrlAsString, getProjectUrl, getComponentIssuesUrl } 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 - }; -*/ - - showMeasures = (e /*: SyntheticInputEvent */) => { - e.preventDefault(); - this.props.showMeasures(); - }; - - openInWorkspace = (e /*: SyntheticInputEvent */) => { - e.preventDefault(); - const { key } = this.props.component; - const Workspace = require('../workspace/main').default; - Workspace.openComponent({ key, branch: this.props.branch }); - }; - - render() { - const { - key, - measures, - path, - project, - projectName, - q, - subProject, - subProjectName, - uuid - } = this.props.component; - const isUnitTest = q === 'UTS'; - const workspace = false; - let rawSourcesLink = - window.baseUrl + `/api/sources/raw?key=${encodeURIComponent(this.props.component.key)}`; - if (this.props.branch) { - rawSourcesLink += `&branch=${encodeURIComponent(this.props.branch)}`; - } - - // TODO favorite - return ( -
-
-
- - - {subProject != null && ( - - )} - -
- {collapsedDirFromPath(path)} - {fileFromPath(path)} - {this.props.component.canMarkAsFavorite && ( - - )} -
-
-
- - - -
- {isUnitTest && ( -
- - {formatMeasure(measures.tests, 'SHORT_INT')} - - - {translate('metric.tests.name')} - -
- )} - - {!isUnitTest && ( -
- - {formatMeasure(measures.lines, 'SHORT_INT')} - - - {translate('metric.lines.name')} - -
- )} - -
- - - {measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0} - - - - {translate('metric.violations.name')} - -
- - {measures.coverage != null && ( -
- - {formatMeasure(measures.coverage, 'PERCENT')} - - - {translate('metric.coverage.name')} - -
- )} - - {measures.duplicationDensity != null && ( -
- - {formatMeasure(measures.duplicationDensity, 'PERCENT')} - - - {translate('duplications')} - -
- )} -
-
- ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx new file mode 100644 index 00000000000..a29348b9312 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx @@ -0,0 +1,226 @@ +/* + * 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 MeasuresOverlay from './components/MeasuresOverlay'; +import { SourceViewerFile } from '../../app/types'; +import QualifierIcon from '../shared/QualifierIcon'; +import FavoriteContainer from '../controls/FavoriteContainer'; +import { + getPathUrlAsString, + getProjectUrl, + getComponentIssuesUrl, + getBaseUrl +} from '../../helpers/urls'; +import { collapsedDirFromPath, fileFromPath } from '../../helpers/path'; +import { translate } from '../../helpers/l10n'; +import { formatMeasure } from '../../helpers/measures'; + +interface Props { + branch: string | undefined; + sourceViewerFile: SourceViewerFile; +} + +interface State { + measuresOverlay: boolean; +} + +export default class SourceViewerHeader extends React.PureComponent { + state: State = { measuresOverlay: false }; + + handleShowMeasuresClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + this.setState({ measuresOverlay: true }); + }; + + handleMeasuresOverlayClose = () => { + this.setState({ measuresOverlay: false }); + }; + + openInWorkspace = (event: React.SyntheticEvent) => { + event.preventDefault(); + const { key } = this.props.sourceViewerFile; + const Workspace = require('../workspace/main').default; + Workspace.openComponent({ key, branch: this.props.branch }); + }; + + render() { + const { + key, + measures, + path, + project, + projectName, + q, + subProject, + subProjectName, + uuid + } = this.props.sourceViewerFile; + const isUnitTest = q === 'UTS'; + const workspace = false; + let rawSourcesLink = + getBaseUrl() + `/api/sources/raw?key=${encodeURIComponent(this.props.sourceViewerFile.key)}`; + if (this.props.branch) { + rawSourcesLink += `&branch=${encodeURIComponent(this.props.branch)}`; + } + + // TODO favorite + return ( +
+
+
+ + + {subProject != null && ( + + )} + +
+ {collapsedDirFromPath(path)} + {fileFromPath(path)} + {this.props.sourceViewerFile.canMarkAsFavorite && ( + + )} +
+
+
+ + + +
+ {isUnitTest && ( +
+ + {formatMeasure(measures.tests, 'SHORT_INT')} + + + {translate('metric.tests.name')} + +
+ )} + + {!isUnitTest && ( +
+ + {formatMeasure(measures.lines, 'SHORT_INT')} + + + {translate('metric.lines.name')} + +
+ )} + +
+ + + {measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0} + + + + {translate('metric.violations.name')} + +
+ + {measures.coverage != null && ( +
+ + {formatMeasure(measures.coverage, 'PERCENT')} + + + {translate('metric.coverage.name')} + +
+ )} + + {measures.duplicationDensity != null && ( +
+ + {formatMeasure(measures.duplicationDensity, 'PERCENT')} + + + {translate('duplications')} + +
+ )} +
+
+ ); + } +} 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 { + 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) => { + event.preventDefault(); + event.currentTarget.blur(); + this.props.onClose(); + }; + + handleAllMeasuresClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.setState({ showAllMeasures: true }); + }; + + renderMeasure = (measure: MeasureEnhanced | undefined) => { + return measure ? : null; + }; + + renderLines = () => { + const { measures } = this.state; + + return ( +
+
+
+
+ {this.renderMeasure(measures.lines)} + {this.renderMeasure(measures.ncloc)} + {this.renderMeasure(measures.comment_lines)} + {this.renderMeasure(measures.comment_lines_density)} +
+
+ +
+
+ {this.renderMeasure(measures.cognitive_complexity)} + {this.renderMeasure(measures.complexity)} + {this.renderMeasure(measures.function_complexity)} +
+
+
+
+ ); + }; + + renderBigMeasure = (measure: MeasureEnhanced | undefined) => { + return measure ? ( +
+ + + + {getLocalizedMetricName(measure.metric, true)} +
+ ) : null; + }; + + renderIssues = () => { + const { measures, severitiesFacet, tagsFacet, typesFacet } = this.state; + return ( +
+
+
+ {this.renderBigMeasure(measures.violations)} + {this.renderBigMeasure(measures.sqale_index)} +
+ {measures.violations && + !measures.violations.value && ( + <> + {typesFacet && ( +
+
+ {sortBy(typesFacet, f => TYPES.indexOf(f.val)).map(f => ( +
+ + + {translate('issue.type', f.val)} + + + {formatMeasure(f.count, 'SHORT_INT')} + +
+ ))} +
+
+ )} + {severitiesFacet && ( +
+
+ {sortBy(severitiesFacet, f => SEVERITIES.indexOf(f.val)).map(f => ( +
+ + + + + {formatMeasure(f.count, 'SHORT_INT')} + +
+ ))} +
+
+ )} + {tagsFacet && ( +
+
+ {tagsFacet.map(f => ( +
+ + + {f.val} + + + {formatMeasure(f.count, 'SHORT_INT')} + +
+ ))} +
+
+ )} + + )} +
+
+ ); + }; + + renderCoverage = () => { + const { coverage } = this.state.measures; + if (!coverage) { + return null; + } + return ( +
+
+
+
+ +
+
+ + + + {getLocalizedMetricName(coverage.metric)} +
+
+ +
+
+ {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)} +
+
+
+
+ ); + }; + + renderDuplications = () => { + const { duplicated_lines_density: duplications } = this.state.measures; + if (!duplications) { + return null; + } + return ( +
+
+
+
+ +
+
+ + + + + {getLocalizedMetricName(duplications.metric, true)} + +
+
+ +
+
+ {this.renderMeasure(this.state.measures.duplicated_blocks)} + {this.renderMeasure(this.state.measures.duplicated_lines)} +
+
+
+
+ ); + }; + + renderTests = () => { + const { measures } = this.state; + return ( +
+
+
+
+
+ {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)} +
+
+
+
+
+ ); + }; + + renderDomain = (domain: string, measures: MeasureEnhanced[]) => { + return ( +
+
+
+
+ {domain} +
+ {sortBy(measures.filter(measure => measure.value !== undefined), measure => + getLocalizedMetricName(measure.metric) + ).map(measure => this.renderMeasure(measure))} +
+
+
+ ); + }; + + 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 ( +
+
+ {odd.map(domain => this.renderDomain(domain, domains[domain]))} +
+
+ {even.map(domain => this.renderDomain(domain, domains[domain]))} +
+
+ ); + }; + + render() { + const { branch, sourceViewerFile } = this.props; + const { loading } = this.state; + + return ( + +
+
+
+ + + {sourceViewerFile.projectName} + + + {sourceViewerFile.subProject && ( + <> + + + {sourceViewerFile.subProjectName} + + + )} +
+ +
+ + {sourceViewerFile.path} +
+
+ + {loading ? ( + + ) : ( + <> + {sourceViewerFile.q === 'UTS' ? ( + <> + {this.renderTests()} + + + ) : ( +
+ {this.renderLines()} + {this.renderIssues()} + {this.renderCoverage()} + {this.renderDuplications()} +
+ )} + + )} + +
+ {this.state.showAllMeasures ? ( + this.renderAllMeasures() + ) : ( + + {translate('component_viewer.show_all_measures')} + + )} +
+
+ +
+ +
+
+ ); + } +} 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 { + 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 ( +
+ +
+ {testCase.status !== 'ERROR' && + testCase.status !== 'FAILURE' && + coveredFiles !== undefined && ( + <> +
+ {translate('component_viewer.transition.covers')} +
+ {coveredFiles.length > 0 + ? coveredFiles.map(coveredFile => ( +
+ {coveredFile.longName} + + {translateWithParameters( + 'component_viewer.x_lines_are_covered', + coveredFile.coveredLines + )} + +
+ )) + : translate('none')} + + )} + + {testCase.status !== 'OK' && ( + <> +
{translate('component_viewer.details')}
+ {testCase.message &&
{testCase.message}
} +
{testCase.stacktrace}
+ + )} +
+
+
+ ); + } +} 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 ( +
+ + {['bugs', 'vulnerabilities', 'code_smells'].includes(measure.metric.key) && ( + + )} + {getLocalizedMetricName(measure.metric)} + + + + +
+ ); +} 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 { + handleTestCaseClick = (event: React.SyntheticEvent) => { + 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 ( + + + + + + {status !== 'SKIPPED' && `${testCase.durationInMs}ms`} + + + {hasAdditionalData ? ( + + {testCase.name} + + ) : ( + testCase.name + )} + + {testCase.coveredLines} + + ); + } +} 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 { + 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) => { + 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 ( +
+
+
+
+ + + + + + + {sortTestCases(testCases, sort, sortAsc).map(testCase => ( + + ))} + +
+ {translate('component_viewer.measure_section.unit_tests')} +
+ + {translate('component_viewer.tests.ordered_by')} + + + {translate('component_viewer.tests.duration')} + + + + {translate('component_viewer.tests.test_name')} + + + + {translate('component_viewer.tests.status')} + +
+ {translate('component_viewer.covered_lines')} +
+
+
+
+ {selectedTest && } +
+ ); + } +} + +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( + + ); + 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( + + ); + 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(); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); +}); + +it('should render ERROR test', async () => { + const wrapper = shallow( + + ); + 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( + + ) + ).toMatchSnapshot(); +}); + +it('should render issues icon', () => { + expect( + shallow( + + ) + ).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(); + 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( + + ); + 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`] = ` + +
+
+
+ + + Project Name + + + + + Sub-Project Name + + +
+
+ + src/file.js +
+
+ +
+
+
+
+
+ + + + +
+
+
+
+ + +
+
+
+
+
+
+
+
+ + + + + sqale_index + +
+
+
+
+
+
+
+
+ +
+
+ + + + + coverage + +
+
+
+
+ + +
+
+
+
+
+
+
+
+ +
+
+ + + + + duplicated_lines_density + +
+
+
+
+ + +
+
+
+
+
+
+ +
+
+ +
+
+`; + +exports[`should render source file 2`] = ` + +
+
+
+ + + Project Name + + + + + Sub-Project Name + + +
+
+ + src/file.js +
+
+ +
+
+
+
+
+ + + + +
+
+
+
+ + +
+
+
+
+
+
+
+
+ + + + + sqale_index + +
+
+
+
+
+
+
+
+ +
+
+ + + + + coverage + +
+
+
+
+ + +
+
+
+
+
+
+
+
+ +
+
+ + + + + duplicated_lines_density + +
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + Complexity + +
+ + +
+
+
+
+
+
+
+ + Size + +
+ + + + + + + + +
+
+
+
+
+
+
+ + Coverage + +
+ + + + +
+
+
+
+
+
+
+ + Reliability + +
+ + +
+
+
+
+
+
+
+
+
+ + Security + +
+ + +
+
+
+
+
+
+
+ + Tests + +
+ + + + +
+
+
+
+
+
+
+ + Issues + +
+ + +
+
+
+
+
+
+
+ + Duplications + +
+ + + + +
+
+
+
+
+
+
+ + Maintainability + +
+ + + +
+
+
+
+
+
+
+
+ +
+
+`; + +exports[`should render test file 1`] = ` + +
+
+
+ + + Project Name + + + + + Sub-Project Name + + +
+
+ + src/file.js +
+
+ + +
+
+
+
+
+ + + + +
+
+
+
+
+ +
+
+ +
+
+ +
+
+`; 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`] = ` +
+ +
+ +
+ component_viewer.details +
+
+          Something failed
+        
+
+      
+    
+
+
+`; + +exports[`should render OK test 1`] = ` +
+ +
+ +
+ component_viewer.transition.covers +
+
+ + src/file.js + + + component_viewer.x_lines_are_covered.3 + +
+
+
+
+
+`; 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`] = ` +
+ + Coverage + + + + +
+`; + +exports[`should render issues icon 1`] = ` +
+ + + Bugs + + + + +
+`; 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`] = ` + + + + + + 1ms + + + + should work + + + + 3 + + +`; 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`] = ` +
+
+
+
+ + + + + + + + + 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", + } + } + /> + +
+ component_viewer.measure_section.unit_tests +
+ + component_viewer.tests.ordered_by + + + component_viewer.tests.duration + + + + component_viewer.tests.test_name + + + + component_viewer.tests.status + +
+ component_viewer.covered_lines +
+
+
+
+
+`; + +exports[`should render 2`] = ` +
+
+
+
+ + + + + + + + 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", + } + } + /> + + +
+ component_viewer.measure_section.unit_tests +
+ + component_viewer.tests.ordered_by + + + component_viewer.tests.duration + + + + component_viewer.tests.test_name + + + + component_viewer.tests.status + +
+ component_viewer.covered_lines +
+
+
+
+
+`; 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}} -
- {{#eachEven measuresToDisplay}} -
-
-
-
- {{name}} -
- {{#each metrics}} -
- {{#eq key 'bugs'}}{{issueTypeIcon 'BUG'}} {{/eq}}{{#eq key 'vulnerabilities'}}{{issueTypeIcon 'VULNERABILITY'}} {{/eq}}{{#eq key 'code_smells'}}{{issueTypeIcon 'CODE_SMELL'}} {{/eq}}{{name}} -  {{value}} -
- {{/each}} -
-
-
- {{/eachEven}} -
- -
- {{#eachOdd measuresToDisplay}} -
-
-
-
- {{name}} -
- {{#each metrics}} -
- {{#eq key 'bugs'}}{{issueTypeIcon 'BUG'}} {{/eq}}{{#eq key 'vulnerabilities'}}{{issueTypeIcon 'VULNERABILITY'}} {{/eq}}{{#eq key 'code_smells'}}{{issueTypeIcon 'CODE_SMELL'}} {{/eq}}{{name}} -  {{value}} -
- {{/each}} -
-
-
- {{/eachOdd}} -
-{{/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}} -
-
- -
-
- {{measures.coverage}} - {{t 'metric.coverage.name'}} -
-
- - {{#any measures.covered_lines measures.lines_to_cover measures.covered_conditions measures.conditions_to_cover}} -
-
-
- Covered by Tests -
-
- Lines - {{formatMeasure measures.covered_lines 'INT'}}/{{measures.lines_to_cover}} -
- {{#if measures.conditions_to_cover}} -
- Conditions - {{default measures.covered_conditions 0}}/{{measures.conditions_to_cover}} -
- {{/if}} -
-
- {{/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}} -
-
-
- -
-
- {{measures.duplicated_lines_density}} - {{t 'metric.duplicated_lines_density.short_name'}} -
-
- -
-
-
- {{t 'metric.duplicated_blocks.name'}} - {{measures.duplicated_blocks}} -
-
- {{t 'metric.duplicated_lines.name'}} - {{measures.duplicated_lines}} -
-
-
-
-{{/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 @@ -
-
-
- {{default issuesCount 0}} - {{t 'metric.violations.name'}} -
-
- {{formatMeasure measures.issuesRemediationEffort 'SHORT_WORK_DUR'}} - {{t 'metric.sqale_index.short_name'}} -
-
- - {{#if issuesCount}} -
-
- {{#each typesFacet}} -
- {{issueTypeIcon val}} {{issueType val}} - {{formatMeasure count 'SHORT_INT'}} -
- {{/each}} -
-
- -
-
- {{#each severitiesFacet}} -
- {{severityHelper val}} - {{formatMeasure count 'SHORT_INT'}} -
- {{/each}} -
-
- -
-
- {{#each tagsFacet}} -
-  {{val}} - {{formatMeasure count 'SHORT_INT'}} -
- {{/each}} -
-
- {{/if}} -
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 @@ -
-
-
- {{t 'metric.lines.name'}} - {{measures.lines}} -
-
- {{t 'metric.ncloc.name'}} - {{measures.ncloc}} -
-
- {{t 'metric.comment_lines_density.short_name'}} - {{measures.comment_lines_density}} / {{measures.comment_lines}} -
-
-
- -
-
-
- {{t 'metric.complexity.name'}} - {{measures.complexity}} -
-
- {{t 'metric.function_complexity.name'}} - {{measures.function_complexity}} -
-
-
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 @@ -
-
-
- - - - - - {{#each tests}} - - {{#eq status 'SKIPPED'}} - - - - - {{else}} - {{#ifTestData this}} - - - - - {{else}} - - - - {{/ifTestData}} - {{/eq}} - - {{/each}} -
- {{t 'component_viewer.measure_section.unit_tests'}}
- {{t 'component_viewer.tests.ordered_by'}} - - {{t 'component_viewer.tests.duration'}} - / - - {{t 'component_viewer.tests.test_name'}} - / - - {{t 'component_viewer.tests.status'}} -
{{t 'component_viewer.covered_lines'}}
{{testStatusIcon status}}{{name}}{{testStatusIcon status}}{{durationInMs}}ms{{name}}{{coveredLines}}{{testStatusIcon status}}{{durationInMs}}ms{{name}}
-
-
-
- -{{#if selectedTest}} -
-
- {{#notEq selectedTest.status 'ERROR'}} - {{#notEq selectedTest.status 'FAILURE'}} -
{{t 'component_viewer.transition.covers'}}
- {{#each coveredFiles}} -
- {{longName}} - {{tp 'component_viewer.x_lines_are_covered' coveredLines}} -
- {{else}} - {{t 'none'}} - {{/each}} - {{/notEq}} - {{/notEq}} - - {{#notEq selectedTest.status 'OK'}} -
{{t 'component_viewer.details'}}
- {{#if selectedTest.message}} -
{{selectedTest.message}}
- {{/if}} -
{{selectedTest.stacktrace}}
- {{/notEq}} -
-
-{{/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 @@ -
-
-
-
- {{t 'metric.tests.name'}} - {{measures.tests}} -
- {{#notNull measures.test_success_density}} -
- {{t 'metric.test_success_density.name'}} - {{measures.test_success_density}} -
- {{/notNull}} - {{#notNull measures.test_failures}} -
- {{t 'metric.test_failures.name'}} - {{measures.test_failures}} -
- {{/notNull}} - {{#notNull measures.test_errors}} -
- {{t 'metric.test_errors.name'}} - {{measures.test_errors}} -
- {{/notNull}} - {{#notNull measures.skipped_tests}} -
- {{t 'metric.skipped_tests.name'}} - {{measures.skipped_tests}} -
- {{/notNull}} - {{#notNull measures.test_execution_time}} -
- {{t 'metric.test_execution_time.name'}} - {{measures.test_execution_time}} -
- {{/notNull}} -
-
-
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 @@ - - - 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 {'–'}; } if (metricType === 'LEVEL') { - return ; + return ; } if (metricType !== 'RATING') { @@ -47,7 +55,7 @@ export default function Measure({ className, decimals, metricKey, metricType, va } const tooltip = getRatingTooltip(metricKey, Number(value)); - const rating = ; + const rating = ; if (tooltip) { return ( 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()).toMatchSnapshot(); + expect( + shallow() + ).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 ; +} 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