From 1b6dcc529ae3ef54db0cfe7ac80c926d20799d56 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Tue, 21 Aug 2018 11:31:32 +0200 Subject: [PATCH] SONAR-11164 Add measures page for short-lived branches and PR --- .../__tests__/utils-test.ts | 2 +- .../component-measures/components/App.tsx | 146 ++++++++++++------ .../components/MeasureHeader.tsx | 14 +- .../components/MeasuresEmpty.tsx | 29 ++++ .../components/__tests__/App-test.tsx | 14 +- .../__tests__/MeasureHeader-test.tsx | 20 ++- .../__snapshots__/MeasureHeader-test.tsx.snap | 39 ++++- .../sidebar/DomainFacet.tsx | 25 +-- .../component-measures/sidebar/Sidebar.tsx | 17 +- .../sidebar/__tests__/DomainFacet-test.tsx | 37 ++--- .../sidebar/__tests__/Sidebar-test.tsx | 1 + .../__snapshots__/Sidebar-test.tsx.snap | 2 + .../main/js/apps/component-measures/utils.ts | 29 +++- .../resources/org/sonar/l10n/core.properties | 1 + 14 files changed, 268 insertions(+), 108 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/MeasuresEmpty.tsx diff --git a/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts index 74987c7eaa5..a3d14ff95c4 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts @@ -102,7 +102,7 @@ describe('groupByDomains', () => { describe('parseQuery', () => { it('should correctly parse the url query', () => { expect(utils.parseQuery({})).toEqual({ - metric: 'project_overview', + metric: utils.DEFAULT_METRIC, selected: '', view: utils.DEFAULT_VIEW }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx index ff78d3a724a..236c96619a2 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx @@ -22,10 +22,21 @@ import * as key from 'keymaster'; import { InjectedRouter } from 'react-router'; import Helmet from 'react-helmet'; import MeasureContentContainer from './MeasureContentContainer'; +import MeasuresEmpty from './MeasuresEmpty'; import MeasureOverviewContainer from './MeasureOverviewContainer'; import Sidebar from '../sidebar/Sidebar'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; -import { isProjectOverview, hasBubbleChart, parseQuery, serializeQuery, Query } from '../utils'; +import { + isProjectOverview, + hasBubbleChart, + parseQuery, + serializeQuery, + Query, + hasFullMeasures, + getMeasuresPageMetricKeys, + groupByDomains, + sortMeasures +} from '../utils'; import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches'; import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; import { @@ -33,14 +44,13 @@ import { translateWithParameters, translate } from '../../../helpers/l10n'; -import { getDisplayMetrics } from '../../../helpers/measures'; import { RawQuery } from '../../../helpers/query'; import { BranchLike, ComponentMeasure, + CurrentUser, MeasureEnhanced, Metric, - CurrentUser, Period } from '../../../app/types'; import '../../../components/search-navigator.css'; @@ -117,8 +127,8 @@ export default class App extends React.PureComponent { fetchMeasures = ({ branchLike, component, fetchMeasures, metrics }: Props) => { this.setState({ loading: true }); - const filteredKeys = getDisplayMetrics(Object.values(metrics)).map(metric => metric.key); + const filteredKeys = getMeasuresPageMetricKeys(metrics, branchLike); fetchMeasures(component.key, filteredKeys, branchLike).then( ({ measures, leakPeriod }) => { if (this.mounted) { @@ -139,6 +149,34 @@ export default class App extends React.PureComponent { ); }; + getHelmetTitle = (query: Query, displayOverview: boolean, metric?: Metric) => { + if (displayOverview && query.metric) { + return isProjectOverview(query.metric) + ? translate('component_measures.overview.project_overview.facet') + : translateWithParameters( + 'component_measures.domain_x_overview', + getLocalizedMetricDomain(query.metric) + ); + } + return metric ? metric.name : translate('layout.measures'); + }; + + getSelectedMetric = (query: Query, displayOverview: boolean) => { + if (displayOverview) { + return undefined; + } + const metric = this.props.metrics[query.metric]; + if (!metric) { + const domainMeasures = groupByDomains(this.state.measures); + const firstMeasure = + domainMeasures[0] && sortMeasures(domainMeasures[0].name, domainMeasures[0].measures)[0]; + if (firstMeasure && typeof firstMeasure !== 'string') { + return firstMeasure.metric; + } + } + return metric; + }; + updateQuery = (newQuery: Partial) => { const query = serializeQuery({ ...parseQuery(this.props.location.query), @@ -154,16 +192,51 @@ export default class App extends React.PureComponent { }); }; - getHelmetTitle = (metric?: Metric) => { - if (metric && hasBubbleChart(metric.key)) { - return isProjectOverview(metric.key) - ? translate('component_measures.overview.project_overview.facet') - : translateWithParameters( - 'component_measures.domain_x_overview', - getLocalizedMetricDomain(metric.key) - ); + renderContent = (displayOverview: boolean, query: Query, metric?: Metric) => { + const { branchLike, component, fetchMeasures, metrics } = this.props; + const { leakPeriod, measures } = this.state; + + if (measures.length === 0) { + return ; } - return metric ? metric.name : translate('layout.measures'); + + if (displayOverview) { + return ( + + ); + } + + if (!metric) { + return ; + } + + return ( + + ); }; render() { @@ -171,14 +244,16 @@ export default class App extends React.PureComponent { if (isLoading) { return ; } - const { branchLike, component, fetchMeasures, metrics } = this.props; - const { leakPeriod } = this.state; + const { branchLike } = this.props; + const { measures } = this.state; const query = parseQuery(this.props.location.query); - const metric = metrics[query.metric]; + const hasOverview = hasFullMeasures(branchLike); + const displayOverview = hasOverview && hasBubbleChart(query.metric); + const metric = this.getSelectedMetric(query, displayOverview); return (
- + {({ top }) => ( @@ -186,8 +261,9 @@ export default class App extends React.PureComponent {
@@ -196,37 +272,7 @@ export default class App extends React.PureComponent { )} - {metric && ( - - )} - {!metric && - hasBubbleChart(query.metric) && ( - - )} + {this.renderContent(displayOverview, query, metric)}
); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx index 61840e12d62..2d524d52332 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx @@ -28,7 +28,8 @@ import Tooltip from '../../../components/controls/Tooltip'; import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; import { getMeasureHistoryUrl } from '../../../helpers/urls'; import { isDiffMetric } from '../../../helpers/measures'; -import { MeasureEnhanced, Metric, ComponentMeasure, BranchLike, Period } from '../../../app/types'; +import { BranchLike, ComponentMeasure, MeasureEnhanced, Metric, Period } from '../../../app/types'; +import { hasFullMeasures } from '../utils'; interface Props { branchLike?: BranchLike; @@ -42,7 +43,9 @@ interface Props { export default function MeasureHeader(props: Props) { const { branchLike, component, leakPeriod, measure, metric, secondaryMeasure } = props; const isDiff = isDiffMetric(metric.key); - const hasHistory = component.qualifier !== 'FIL' && component.qualifier !== 'UTS'; + const hasHistory = + component.qualifier !== 'FIL' && component.qualifier !== 'UTS' && hasFullMeasures(branchLike); + const displayLeak = hasFullMeasures(branchLike); return (
@@ -79,9 +82,10 @@ export default function MeasureHeader(props: Props) { )}
- {leakPeriod && ( - - )} + {displayLeak && + leakPeriod && ( + + )}
{secondaryMeasure && diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasuresEmpty.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasuresEmpty.tsx new file mode 100644 index 00000000000..ea25a2d2f69 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasuresEmpty.tsx @@ -0,0 +1,29 @@ +/* + * 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 { translate } from '../../../helpers/l10n'; + +export default function MeasuresEmpty() { + return ( +
+
{translate('component_measures.empty')}
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx index f0202595073..c6505527b5e 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import App from '../App'; +import { waitAndUpdate } from '../../../../helpers/testUtils'; const COMPONENT = { key: 'foo', name: 'Foo', qualifier: 'TRK' }; @@ -48,21 +49,24 @@ const PROPS = { component: COMPONENT, currentUser: { isLoggedIn: false }, location: { pathname: '/component_measures', query: { metric: 'coverage' } }, - fetchMeasures: jest.fn().mockResolvedValue({ component: COMPONENT, measures: [] }), + fetchMeasures: jest.fn().mockResolvedValue({ + component: COMPONENT, + measures: [{ metric: 'coverage', value: '80.0' }] + }), fetchMetrics: jest.fn(), metrics: METRICS, metricsKey: ['lines_to_cover', 'coverage', 'duplicated_lines_density', 'new_bugs'], router: { push: jest.fn() } as any }; -it('should render correctly', () => { +it('should render correctly', async () => { const wrapper = shallow(); expect(wrapper.find('.spinner')).toHaveLength(1); - wrapper.setState({ loading: false }); + await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); }); -it('should render a measure overview', () => { +it('should render a measure overview', async () => { const wrapper = shallow( { /> ); expect(wrapper.find('.spinner')).toHaveLength(1); - wrapper.setState({ loading: false }); + await waitAndUpdate(wrapper); expect(wrapper.find('MeasureOverviewContainer')).toHaveLength(1); }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.tsx index 965af1b5474..2df222654bd 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.tsx @@ -81,10 +81,24 @@ it('should render correctly for leak', () => { ).toMatchSnapshot(); }); -it('should render with branch', () => { - const shortBranch = { isMain: false, name: 'feature', mergeBranch: '', type: 'SHORT' }; +it('should render with long living branch', () => { + const longBranch = { isMain: false, name: 'branch-6.7', type: 'LONG' }; expect( - shallow().find('Link') + shallow().find('Link') + ).toMatchSnapshot(); +}); + +it('should render with short living branch', () => { + const shortBranch = { isMain: false, name: 'feature', mergeBranch: 'master', type: 'SHORT' }; + expect( + shallow( + + ) ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap index 2ccafb98328..c74f60aa659 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.tsx.snap @@ -128,7 +128,7 @@ exports[`should render correctly for leak 1`] = `
`; -exports[`should render with branch 1`] = ` +exports[`should render with long living branch 1`] = ` `; +exports[`should render with short living branch 1`] = ` +
+
+
+ + Reliability Rating on New Code + + + + + +
+
+
+
+`; + exports[`should work with measure without value 1`] = `
void; onToggle: (property: string) => void; open: boolean; @@ -48,24 +49,28 @@ interface Props { } export default class DomainFacet extends React.PureComponent { + getValues = () => { + const { domain, selected } = this.props; + const measureSelected = domain.measures.find(measure => measure.metric.key === selected); + const overviewSelected = domain.name === selected && this.hasOverview(domain.name); + if (measureSelected) { + return [getLocalizedMetricName(measureSelected.metric)]; + } + return overviewSelected ? [translate('component_measures.domain_overview')] : []; + }; + handleHeaderClick = () => { this.props.onToggle(this.props.domain.name); }; hasFacetSelected = (domain: { name: string }, measures: MeasureEnhanced[], selected: string) => { const measureSelected = measures.find(measure => measure.metric.key === selected); - const overviewSelected = domain.name === selected && hasBubbleChart(domain.name); + const overviewSelected = domain.name === selected && this.hasOverview(domain.name); return measureSelected || overviewSelected; }; - getValues = () => { - const { domain, selected } = this.props; - const measureSelected = domain.measures.find(measure => measure.metric.key === selected); - const overviewSelected = domain.name === selected && hasBubbleChart(domain.name); - if (measureSelected) { - return [getLocalizedMetricName(measureSelected.metric)]; - } - return overviewSelected ? [translate('component_measures.domain_overview')] : []; + hasOverview = (domain: string) => { + return this.props.hasOverview && hasBubbleChart(domain); }; renderItemFacetStat = (item: MeasureEnhanced) => { @@ -115,7 +120,7 @@ export default class DomainFacet extends React.PureComponent { renderOverviewFacet = () => { const { domain, selected } = this.props; - if (!hasBubbleChart(domain.name)) { + if (!this.hasOverview(domain.name)) { return null; } return ( diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx index 027d3593f4f..09683497982 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx @@ -24,9 +24,10 @@ import { getDefaultView, groupByDomains, KNOWN_DOMAINS, PROJECT_OVERVEW, Query } import { MeasureEnhanced } from '../../../app/types'; interface Props { + hasOverview: boolean; measures: MeasureEnhanced[]; selectedMetric: string; - updateQuery: (query: Query) => void; + updateQuery: (query: Partial) => void; } interface State { @@ -73,16 +74,20 @@ export default class Sidebar extends React.PureComponent { this.props.updateQuery({ metric, ...this.resetSelection(metric) }); render() { + const { hasOverview } = this.props; return (
- + {hasOverview && ( + + )} {groupByDomains(this.props.measures).map(domain => ( {}, onToggle: () => {}, open: true, - domain: DOMAIN, selected: 'foo' }; @@ -71,6 +72,16 @@ it('should render closed', () => { expect(wrapper.find('FacetItemsList')).toHaveLength(0); }); +it('should render without overview', () => { + const wrapper = shallow(); + expect( + wrapper + .find('FacetItem') + .filterWhere(node => node.getElement().key === 'Reliability') + .exists() + ).toBe(false); +}); + it('should not display subtitles of new measures if there is none', () => { const domain = { name: 'Reliability', @@ -82,17 +93,7 @@ it('should not display subtitles of new measures if there is none', () => { ] }; - expect( - shallow( - {}} - onToggle={() => {}} - open={true} - selected={'foo'} - /> - ) - ).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); it('should not display subtitles of new measures if there is none, even on last line', () => { @@ -106,15 +107,5 @@ it('should not display subtitles of new measures if there is none, even on last ] }; - expect( - shallow( - {}} - onToggle={() => {}} - open={true} - selected={'foo'} - /> - ) - ).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.tsx index 9b629994ac3..4814927ffe5 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.tsx @@ -61,6 +61,7 @@ const MEASURES = [ ]; const PROPS = { + hasOverview: true, measures: MEASURES, selectedMetric: 'duplicated_lines_density', updateQuery: () => {} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap index 09f7788831c..d626c8f7948 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap @@ -49,6 +49,7 @@ exports[`should display two facets 1`] = ` "name": "Coverage", } } + hasOverview={true} key="Coverage" onChange={[Function]} onToggle={[Function]} @@ -80,6 +81,7 @@ exports[`should display two facets 1`] = ` "name": "Duplications", } } + hasOverview={true} key="Duplications" onChange={[Function]} onToggle={[Function]} diff --git a/server/sonar-web/src/main/js/apps/component-measures/utils.ts b/server/sonar-web/src/main/js/apps/component-measures/utils.ts index 74befd184f0..26b41a1a503 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/utils.ts +++ b/server/sonar-web/src/main/js/apps/component-measures/utils.ts @@ -25,10 +25,13 @@ import { ComponentMeasure, ComponentMeasureEnhanced, Metric, - MeasureEnhanced + MeasureEnhanced, + BranchLike } from '../../app/types'; import { enhanceMeasure } from '../../components/measure/utils'; import { cleanQuery, parseAsString, RawQuery, serializeString } from '../../helpers/query'; +import { isLongLivingBranch, isMainBranch } from '../../helpers/branches'; +import { getDisplayMetrics } from '../../helpers/measures'; export const PROJECT_OVERVEW = 'project_overview'; export const DEFAULT_VIEW = 'list'; @@ -146,13 +149,33 @@ export function hasTreemap(metric: string, type: string): boolean { } export function hasBubbleChart(domainName: string): boolean { - return bubbles[domainName] != null; + return bubbles[domainName] !== undefined; } export function hasFacetStat(metric: string): boolean { return metric !== 'alert_status'; } +export function hasFullMeasures(branch?: BranchLike) { + return !branch || isLongLivingBranch(branch) || isMainBranch(branch); +} + +export function getMeasuresPageMetricKeys(metrics: { [key: string]: Metric }, branch?: BranchLike) { + if (!hasFullMeasures(branch)) { + return [ + 'new_coverage', + 'new_lines_to_cover', + 'new_uncovered_lines', + 'new_line_coverage', + 'new_conditions_to_cover', + 'new_uncovered_conditions', + 'new_branch_coverage' + ]; + } + + return getDisplayMetrics(Object.values(metrics)).map(metric => metric.key); +} + export function getBubbleMetrics(domain: string, metrics: { [key: string]: Metric }) { const conf = bubbles[domain]; return { @@ -182,7 +205,7 @@ const parseView = (metric: string, rawView?: string) => { }; export interface Query { - metric?: string; + metric: string; selected?: string; view: string; } 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 1256fbfbe45..efd5ce43150 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2521,6 +2521,7 @@ component_measures.legend.size_x=Size: {0} component_measures.legend.worse_of_x_y=Worse of {0} and {1} component_measures.no_history=There is no historical data. component_measures.not_found=The requested measure was not found. +component_measures.empty=No measures. component_measures.to_select_files=to select files component_measures.to_navigate=to navigate component_measures.to_navigate_files=to next/previous file -- 2.39.5