From f2c95a685a3b950a68122af1e5673978a07df773 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Wed, 26 Jul 2017 16:04:19 +0200 Subject: [PATCH] SONAR-9608 SONAR-9609 Create the new measures page sidebar with all facets --- .../nav/component/ComponentNavMenu.js | 13 ++ .../ComponentNavMenu-test.js.snap | 38 +++- .../src/main/js/app/utils/startReactApp.js | 6 +- .../__snapshots__/utils-test.js.snap | 97 ++++++++ .../__tests__/utils-test.js | 121 ++++++++++ .../apps/component-measures/components/App.js | 147 ++++++++++++ .../components/AppContainer.js | 87 ++++++++ .../components/__tests__/App-test.js | 50 +++++ .../__tests__/__snapshots__/App-test.js.snap | 61 +++++ .../apps/component-measures/config/domains.js | 134 +++++++++++ .../main/js/apps/component-measures/routes.js | 28 +++ .../component-measures/sidebar/DomainFacet.js | 82 +++++++ .../component-measures/sidebar/FacetBox.js | 33 +++ .../component-measures/sidebar/FacetHeader.js | 86 +++++++ .../component-measures/sidebar/FacetItem.js | 71 ++++++ .../sidebar/FacetItemMeasureValue.js | 42 ++++ .../sidebar/FacetItemsList.js | 33 +++ .../component-measures/sidebar/Sidebar.js | 81 +++++++ .../sidebar/__tests__/DomainFacet-test.js | 71 ++++++ .../sidebar/__tests__/FacetHeader-test.js | 55 +++++ .../sidebar/__tests__/FacetItem-test.js | 60 +++++ .../__tests__/FacetItemMeasureValue-test.js | 53 +++++ .../sidebar/__tests__/Sidebar-test.js | 77 +++++++ .../__snapshots__/DomainFacet-test.js.snap | 209 ++++++++++++++++++ .../__snapshots__/FacetHeader-test.js.snap | 148 +++++++++++++ .../__snapshots__/FacetItem-test.js.snap | 105 +++++++++ .../FacetItemMeasureValue-test.js.snap | 56 +++++ .../__snapshots__/Sidebar-test.js.snap | 82 +++++++ .../main/js/apps/component-measures/style.css | 28 +++ .../main/js/apps/component-measures/types.js | 41 ++++ .../main/js/apps/component-measures/utils.js | 109 +++++++++ .../issues/sidebar/components/FacetItem.js | 14 +- .../__snapshots__/FacetItem-test.js.snap | 12 - .../src/main/js/components/measure/Measure.js | 55 ++--- .../less/components/search-navigator.less | 2 +- 35 files changed, 2330 insertions(+), 57 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/__tests__/__snapshots__/utils-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/App.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/component-measures/config/domains.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/routes.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetBox.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetHeader.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetItem.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetItemMeasureValue.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetItemsList.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/DomainFacet-test.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetHeader-test.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetItem-test.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetItemMeasureValue-test.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetHeader-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetItem-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetItemMeasureValue-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/component-measures/style.css create mode 100644 server/sonar-web/src/main/js/apps/component-measures/types.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/utils.js diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js index ebf096e40e3..03e20e597a4 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js @@ -121,6 +121,18 @@ export default class ComponentNavMenu extends React.PureComponent { ); } + renderComponentMeasuresLink() { + return ( +
  • + + {translate('layout.measures')} + +
  • + ); + } + renderComponentMeasuresOldLink() { return (
  • @@ -359,6 +371,7 @@ export default class ComponentNavMenu extends React.PureComponent { {this.renderDashboardLink()} {this.renderIssuesLink()} + {this.renderComponentMeasuresLink()} {this.renderComponentMeasuresOldLink()} {this.renderCodeLink()} {this.renderActivityLink()} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap index 28da18be296..6413b52ceb3 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap @@ -44,7 +44,7 @@ exports[`should work with extensions 1`] = ` style={Object {}} to={ Object { - "pathname": "/component_measures_old", + "pathname": "/component_measures", "query": Object { "id": "foo", }, @@ -54,6 +54,23 @@ exports[`should work with extensions 1`] = ` layout.measures
  • +
  • + + Old Measures + +
  • +
  • + + Old Measures + +
  • { getComponent={() => import('../components/ProjectContainer').then(i => i.default)}> - + + diff --git a/server/sonar-web/src/main/js/apps/component-measures/__tests__/__snapshots__/utils-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/__tests__/__snapshots__/utils-test.js.snap new file mode 100644 index 00000000000..7459d13dc96 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/__snapshots__/utils-test.js.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`groupByDomains should correctly group by domains 1`] = ` +Array [ + Object { + "measures": Array [ + Object { + "leak": "0.0999999999999943", + "metric": Object { + "domain": "Coverage", + "key": "coverage", + "name": "Coverage", + "type": "PERCENT", + }, + "periods": Array [ + Object { + "index": 1, + "value": "0.0999999999999943", + }, + ], + "value": "99.3", + }, + Object { + "leak": "70", + "metric": Object { + "domain": "Coverage", + "key": "lines_to_cover", + "name": "Lines to Cover", + "type": "INT", + }, + "periods": Array [ + Object { + "index": 1, + "value": "70", + }, + ], + "value": "431", + }, + ], + "name": "Coverage", + }, + Object { + "measures": Array [ + Object { + "leak": "0.0", + "metric": Object { + "domain": "Duplications", + "key": "duplicated_lines_density", + "name": "Duplicated Lines (%)", + "type": "PERCENT", + }, + "periods": Array [ + Object { + "index": 1, + "value": "0.0", + }, + ], + "value": "3.2", + }, + ], + "name": "Duplications", + }, +] +`; + +exports[`sortMeasures should sort based on the config 1`] = ` +Array [ + Object { + "metric": Object { + "key": "bugs", + "name": "bugs", + "type": "INT", + }, + }, + Object { + "metric": Object { + "key": "new_bugs", + "name": "new_bugs", + "type": "INT", + }, + }, + Object { + "metric": Object { + "key": "reliability_remediation_effort", + "name": "new_bugs", + "type": "INT", + }, + }, + Object { + "metric": Object { + "key": "new_reliability_remediation_effort", + "name": "bugs", + "type": "INT", + }, + }, +] +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.js b/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.js new file mode 100644 index 00000000000..62eaf3c7a56 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.js @@ -0,0 +1,121 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 * as utils from '../utils'; + +const MEASURES = [ + { + metric: { + key: 'lines_to_cover', + type: 'INT', + name: 'Lines to Cover', + domain: 'Coverage' + }, + value: '431', + periods: [{ index: 1, value: '70' }], + leak: '70' + }, + { + metric: { + key: 'coverage', + type: 'PERCENT', + name: 'Coverage', + domain: 'Coverage' + }, + value: '99.3', + periods: [{ index: 1, value: '0.0999999999999943' }], + leak: '0.0999999999999943' + }, + { + metric: { + key: 'duplicated_lines_density', + type: 'PERCENT', + name: 'Duplicated Lines (%)', + domain: 'Duplications' + }, + value: '3.2', + periods: [{ index: 1, value: '0.0' }], + leak: '0.0' + } +]; + +describe('filterMeasures', () => { + it('should exclude banned measures', () => { + expect( + utils.filterMeasures([ + { metric: { key: 'bugs', name: 'Bugs', type: 'INT' } }, + { metric: { key: 'critical_violations', name: 'Critical Violations', type: 'INT' } } + ]) + ).toHaveLength(1); + }); +}); + +describe('sortMeasures', () => { + it('should sort based on the config', () => { + expect( + utils.sortMeasures('Reliability', [ + { metric: { key: 'reliability_remediation_effort', name: 'new_bugs', type: 'INT' } }, + { metric: { key: 'new_reliability_remediation_effort', name: 'bugs', type: 'INT' } }, + { metric: { key: 'new_bugs', name: 'new_bugs', type: 'INT' } }, + { metric: { key: 'bugs', name: 'bugs', type: 'INT' } } + ]) + ).toMatchSnapshot(); + }); +}); + +describe('groupByDomains', () => { + it('should correctly group by domains', () => { + expect(utils.groupByDomains(MEASURES)).toMatchSnapshot(); + }); + + it('should be memoized', () => { + expect(utils.groupByDomains(MEASURES)).toBe(utils.groupByDomains(MEASURES)); + }); +}); + +describe('parseQuery', () => { + it('should correctly parse the url query', () => { + expect(utils.parseQuery({})).toEqual({ metric: '', view: utils.DEFAULT_VIEW }); + expect(utils.parseQuery({ metric: 'foo', view: 'tree' })).toEqual({ + metric: 'foo', + view: 'tree' + }); + }); + + it('should be memoized', () => { + const query = { metric: 'foo', view: 'tree' }; + expect(utils.parseQuery(query)).toBe(utils.parseQuery(query)); + }); +}); + +describe('serializeQuery', () => { + it('should correctly serialize the query', () => { + expect(utils.serializeQuery({ metric: '', view: 'list' })).toEqual({}); + expect(utils.serializeQuery({ metric: 'foo', view: 'tree' })).toEqual({ + metric: 'foo', + view: 'tree' + }); + }); + + it('should be memoized', () => { + const query = { metric: 'foo', view: 'tree' }; + expect(utils.serializeQuery(query)).toBe(utils.serializeQuery(query)); + }); +}); 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 new file mode 100644 index 00000000000..02d5b590ef4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.js @@ -0,0 +1,147 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 Helmet from 'react-helmet'; +import Sidebar from '../sidebar/Sidebar'; +import { parseQuery, serializeQuery } from '../utils'; +import { translate } from '../../../helpers/l10n'; +import type { Component, Query, Period } from '../types'; +import type { RawQuery } from '../../../helpers/query'; +import type { Metrics } from '../../../store/metrics/actions'; +import type { MeasureEnhanced } from '../../../components/measure/types'; +import '../style.css'; + +type Props = {| + component: Component, + location: { pathname: string, query: RawQuery }, + fetchMeasures: ( + Component, + Metrics + ) => Promise<{ measures: Array, periods: Array }>, + fetchMetrics: () => void, + metrics: Metrics, + router: { + push: ({ pathname: string, query?: RawQuery }) => void + } +|}; + +type State = {| + loading: boolean, + measures: Array, + periods: Array +|}; + +export default class App extends React.PureComponent { + mounted: boolean; + props: Props; + state: State; + + constructor(props: Props) { + super(props); + this.state = { + loading: true, + measures: [], + periods: [] + }; + } + + componentDidMount() { + this.mounted = true; + this.props.fetchMetrics(); + this.fetchMeasures(this.props); + + const footer = document.getElementById('footer'); + if (footer) { + footer.classList.add('search-navigator-footer'); + } + } + + componentWillReceiveProps(nextProps: Props) { + if ( + nextProps.component.key !== this.props.component.key || + nextProps.metrics !== this.props.metrics + ) { + this.fetchMeasures(nextProps); + } + } + + fetchMeasures = ({ component, fetchMeasures, metrics }: Props) => { + this.setState({ loading: true }); + fetchMeasures(component, metrics).then( + ({ measures, periods }) => { + if (this.mounted) { + this.setState({ loading: false, measures, periods }); + } + }, + () => this.setState({ loading: false }) + ); + }; + + updateQuery = (newQuery: Query) => { + const query = serializeQuery({ + ...parseQuery(this.props.location.query), + ...newQuery + }); + this.props.router.push({ + pathname: this.props.location.pathname, + query: { + ...query, + id: this.props.component.key + } + }); + }; + + render() { + if (this.state.loading) { + return ; + } + const query = parseQuery(this.props.location.query); + return ( +
    + + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    Page Actions
    +
    +
    + +
    Main
    +
    +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js new file mode 100644 index 00000000000..063a5bc2124 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js @@ -0,0 +1,87 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 { connect } from 'react-redux'; +import { withRouter } from 'react-router'; +import App from './App'; +import throwGlobalError from '../../../app/utils/throwGlobalError'; +import { getComponent, getMetrics, getMetricByKey } from '../../../store/rootReducer'; +import { fetchMetrics } from '../../../store/rootActions'; +import { getMeasuresAndMeta } from '../../../api/measures'; +import { getLeakValue } from '../utils'; +import type { Component } from '../types'; +import type { Metrics } from '../../../store/metrics/actions'; +import type { Measure, MeasureEnhanced } from '../../../components/measure/types'; + +const mapStateToProps = (state, ownProps) => ({ + component: getComponent(state, ownProps.location.query.id), + metrics: getMetrics(state) +}); + +const banQualityGate = (component: Component, measures: Array): Array => { + let newMeasures = [...measures]; + if (!['VW', 'SVW', 'APP'].includes(component.qualifier)) { + newMeasures = newMeasures.filter(measure => measure.metric !== 'alert_status'); + } + if (component.qualifier === 'APP') { + newMeasures = newMeasures.filter( + measure => + measure.metric !== 'releasability_rating' && measure.metric !== 'releasability_effort' + ); + } + return newMeasures; +}; + +const fetchMeasures = (component: Component, metrics: Metrics) => ( + dispatch, + getState +): Promise> => { + const metricKeys = metrics.filter(key => { + const metric = getMetricByKey(getState(), key); + return !metric.hidden && !['DATA', 'DISTRIB'].includes(metric.type); + }); + + if (metricKeys.length <= 0) { + return Promise.resolve([]); + } + + return getMeasuresAndMeta(component.key, metricKeys, { additionalFields: 'periods' }).then(r => { + const measures: Array = banQualityGate(component, r.component.measures) + .map(measure => { + const metric = getMetricByKey(getState(), measure.metric); + const leak = getLeakValue(measure); + return { value: measure.value, periods: measure.periods, metric, leak }; + }) + .filter(measure => { + const hasValue = measure.value != null; + const hasLeakValue = measure.leak != null; + return hasValue || hasLeakValue; + }); + + const newBugs = measures.find(measure => measure.metric.key === 'new_bugs'); + const applicationPeriods = newBugs ? [{ index: 1 }] : []; + const periods = component.qualifier === 'APP' ? applicationPeriods : r.periods; + return { measures, periods }; + }, throwGlobalError); +}; + +const mapDispatchToProps = { fetchMeasures, fetchMetrics }; + +export default connect(mapStateToProps, mapDispatchToProps)(withRouter(App)); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.js new file mode 100644 index 00000000000..a17ab39c8f1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.js @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 React from 'react'; +import { shallow } from 'enzyme'; +import App from '../App'; + +const METRICS = [ + { key: 'lines_to_cover', type: 'INT', name: 'Lines to Cover', domain: 'Coverage' }, + { key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' }, + { + key: 'duplicated_lines_density', + type: 'PERCENT', + name: 'Duplicated Lines (%)', + domain: 'Duplications' + }, + { key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' } +]; + +const PROPS = { + component: { key: 'foo' }, + location: { pathname: '/component_measures', query: {} }, + fetchMeasures: () => {}, + fetchMetrics: () => {}, + metrics: METRICS, + router: { push: () => {} } +}; + +it('should render correctly', () => { + const wrapper = shallow(); + expect(wrapper.find('.spinner')).toHaveLength(1); + wrapper.setState({ loading: false }); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap new file mode 100644 index 00000000000..c74cb34eff9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + Page Actions +
    +
    +
    +
    + Main +
    +
    +
    +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/config/domains.js b/server/sonar-web/src/main/js/apps/component-measures/config/domains.js new file mode 100644 index 00000000000..bd683bd18fb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/config/domains.js @@ -0,0 +1,134 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 +export const domains = { + Reliability: { + order: [ + 'bugs', + 'new_bugs', + 'reliability_rating', + 'reliability_remediation_effort', + 'new_reliability_remediation_effort' + ] + }, + + Security: { + order: [ + 'vulnerabilities', + 'new_vulnerabilities', + 'security_rating', + 'security_remediation_effort', + 'new_security_remediation_effort' + ] + }, + + Maintainability: { + order: [ + 'code_smells', + 'new_code_smells', + 'sqale_rating', + 'sqale_index', + 'new_technical_debt', + 'sqale_debt_ratio', + 'new_sqale_debt_ratio', + 'effort_to_reach_maintainability_rating_a' + ] + }, + + Coverage: { + order: [ + 'coverage', + 'new_coverage', + 'line_coverage', + 'new_line_coverage', + 'branch_coverage', + 'new_branch_coverage', + 'uncovered_lines', + 'new_uncovered_lines', + 'uncovered_conditions', + 'new_uncovered_conditions', + 'new_lines_to_cover', + + 'lines_to_cover', + + 'tests', + 'test_success', + 'test_errors', + 'test_failures', + 'skipped_tests', + 'test_success_density', + 'test_execution_time' + ] + }, + + Duplications: { + order: [ + 'duplicated_lines_density', + 'new_duplicated_lines_density', + 'duplicated_blocks', + 'new_duplicated_blocks', + 'duplicated_lines', + 'new_duplicated_lines', + 'duplicated_files' + ] + }, + + Size: { + order: [ + 'ncloc', + 'lines', + 'new_lines', + 'statements', + 'functions', + 'classes', + 'files', + 'directories' + ] + }, + + Complexity: { + order: ['complexity', 'function_complexity', 'file_complexity', 'class_complexity'] + }, + + Releasability: { + order: ['alert_status'] + }, + + Issues: { + order: [ + 'violations', + 'new_violations', + 'blocker_violations', + 'new_blocker_violations', + 'critical_violations', + 'new_critical_violations', + 'major_violations', + 'new_major_violations', + 'minor_violations', + 'new_minor_violations', + 'info_violations', + 'new_info_violations', + 'open_issues', + 'reopened_issues', + 'confirmed_issues', + 'false_positive_issues' + ] + } +}; diff --git a/server/sonar-web/src/main/js/apps/component-measures/routes.js b/server/sonar-web/src/main/js/apps/component-measures/routes.js new file mode 100644 index 00000000000..02a7323043e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/routes.js @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ +const routes = [ + { + getIndexRoute(_, callback) { + import('./components/AppContainer').then(i => callback(null, { component: i.default })); + } + } +]; + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js new file mode 100644 index 00000000000..ac2083422d1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js @@ -0,0 +1,82 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 FacetBox from './FacetBox'; +import FacetHeader from './FacetHeader'; +import FacetItem from './FacetItem'; +import FacetItemsList from './FacetItemsList'; +import FacetItemMeasureValue from './FacetItemMeasureValue'; +import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; +import Tooltip from '../../../components/controls/Tooltip'; +import { filterMeasures, sortMeasures } from '../utils'; +import { getLocalizedMetricDomain, getLocalizedMetricName } from '../../../helpers/l10n'; +import type { MeasureEnhanced } from '../../../components/measure/types'; + +type Props = {| + onChange: (metric: string) => void, + onToggle: (property: string) => void, + open: boolean, + domain: { name: string, measures: Array }, + selected: string +|}; + +export default class DomainFacet extends React.PureComponent { + props: Props; + + handleHeaderClick = () => this.props.onToggle(this.props.domain.name); + + render() { + const { domain, selected } = this.props; + const measures = sortMeasures(domain.name, filterMeasures(domain.measures)); + return ( + + measure.metric.key === selected) ? 1 : 0} + /> + + {this.props.open && + + {measures.map(measure => + + + + {getLocalizedMetricName(measure.metric)} + + + } + onClick={this.props.onChange} + stat={} + value={measure.metric.key} + /> + )} + } + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetBox.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetBox.js new file mode 100644 index 00000000000..92b7afb58db --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetBox.js @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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'; + +type Props = {| + children?: React.Element<*> +|}; + +export default function FacetBox(props: Props) { + return ( +
    + {props.children} +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetHeader.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetHeader.js new file mode 100644 index 00000000000..2cc9d19f98b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetHeader.js @@ -0,0 +1,86 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 +/* eslint-disable max-len */ +import React from 'react'; + +type Props = {| + name: string, + onClick?: () => void, + open: boolean, + values?: number +|}; + +export default class FacetHeader extends React.PureComponent { + props: Props; + + static defaultProps = { + open: true + }; + + handleClick = (event: Event & { currentTarget: HTMLElement }) => { + event.preventDefault(); + event.currentTarget.blur(); + if (this.props.onClick) { + this.props.onClick(); + } + }; + + renderCheckbox() { + return ( + + {this.props.open + ? + : } + + ); + } + + renderValueIndicator() { + if (this.props.open || !this.props.values) { + return null; + } + return ( + + {this.props.values} + + ); + } + + render() { + return ( +
    + {this.props.onClick + ? + {this.renderCheckbox()} {this.props.name} {this.renderValueIndicator()} + + : + {this.props.name} + } +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetItem.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetItem.js new file mode 100644 index 00000000000..e84480b1317 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetItem.js @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 classNames from 'classnames'; + +type Props = {| + active: boolean, + disabled: boolean, + halfWidth: boolean, + name: string | React.Element<*>, + onClick: string => void, + stat: string | React.Element<*>, + value: string +|}; + +export default class FacetItem extends React.PureComponent { + props: Props; + + static defaultProps = { + disabled: false, + halfWidth: false + }; + + handleClick = (event: Event & { currentTarget: HTMLElement }) => { + event.preventDefault(); + this.props.onClick(this.props.value); + }; + + render() { + const className = classNames('facet', 'search-navigator-facet', { + active: this.props.active, + 'search-navigator-facet-half': this.props.halfWidth + }); + + return this.props.disabled + ? + + {this.props.name} + + + {this.props.stat} + + + : + + {this.props.name} + + + {this.props.stat} + + ; + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetItemMeasureValue.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetItemMeasureValue.js new file mode 100644 index 00000000000..ede45d98793 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetItemMeasureValue.js @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 Measure from '../../../components/measure/Measure'; +import { isDiffMetric } from '../../../helpers/measures'; +import type { MeasureEnhanced } from '../../../components/measure/types'; + +export default function FacetItemMeasureValue({ measure }: { measure: MeasureEnhanced }) { + if (isDiffMetric(measure.metric.key)) { + return ( +
    + +
    + ); + } + + return ( +
    + +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetItemsList.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetItemsList.js new file mode 100644 index 00000000000..5d36d9934e3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetItemsList.js @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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'; + +type Props = {| + children?: Array> +|}; + +export default function FacetItemsList(props: Props) { + return ( +
    + {props.children} +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js new file mode 100644 index 00000000000..f707eb462a5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js @@ -0,0 +1,81 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 DomainFacet from './DomainFacet'; +import { groupByDomains } from '../utils'; +import type { MeasureEnhanced } from '../../../components/measure/types'; +import type { Query } from '../types'; + +type Props = {| + measures: Array, + selectedMetric: string, + updateQuery: Query => void +|}; + +type State = {| + closedFacets: { [string]: boolean }, + measuresByDomains: Array<{ name: string, measures: Array }> +|}; + +export default class Sidebar extends React.PureComponent { + props: Props; + state: State; + + constructor(props: Props) { + super(props); + this.state = { + closedFacets: {}, + measuresByDomains: groupByDomains(props.measures) + }; + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.measures !== this.props.measures) { + this.setState({ measuresByDomains: groupByDomains(nextProps.measures) }); + } + } + + toggleFacet = (name: string) => { + this.setState(({ closedFacets }) => ({ + closedFacets: { ...closedFacets, [name]: !closedFacets[name] } + })); + }; + + changeMetric = (metric: string) => this.props.updateQuery({ metric }); + + render() { + const { measuresByDomains } = this.state; + return ( +
    + {measuresByDomains.map(domain => + + )} +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/DomainFacet-test.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/DomainFacet-test.js new file mode 100644 index 00000000000..09bdd46af59 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/DomainFacet-test.js @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 { shallow } from 'enzyme'; +import DomainFacet from '../DomainFacet'; + +const DOMAIN = { + name: 'Reliability', + measures: [ + { + metric: { + key: 'bugs', + type: 'INT', + name: 'Bugs', + domain: 'Reliability' + }, + value: '5', + periods: [{ index: 1, value: '5' }], + leak: '5' + }, + { + metric: { + key: 'new_bugs', + type: 'INT', + name: 'New Bugs', + domain: 'Reliability' + }, + periods: [{ index: 1, value: '5' }], + leak: '5' + } + ] +}; + +const PROPS = { + onChange: () => {}, + onToggle: () => {}, + open: true, + domain: DOMAIN, + selected: 'foo' +}; + +it('should display facet item list', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('should display facet item list with bugs selected', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('should render closed', () => { + const wrapper = shallow(); + expect(wrapper.find('FacetItemsList')).toHaveLength(0); +}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetHeader-test.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetHeader-test.js new file mode 100644 index 00000000000..0fe3347edbd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetHeader-test.js @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 { shallow } from 'enzyme'; +import { click } from '../../../../helpers/testUtils'; +import FacetHeader from '../FacetHeader'; + +it('should render open facet with value', () => { + expect( + shallow() + ).toMatchSnapshot(); +}); + +it('should render open facet without value', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('should render closed facet with value', () => { + expect( + shallow() + ).toMatchSnapshot(); +}); + +it('should render closed facet without value', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('should render without link', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('should call onClick', () => { + const onClick = jest.fn(); + const wrapper = shallow(); + click(wrapper.find('a')); + expect(onClick).toHaveBeenCalled(); +}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetItem-test.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetItem-test.js new file mode 100644 index 00000000000..e8c8bfe4d54 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetItem-test.js @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 { shallow } from 'enzyme'; +import { click } from '../../../../helpers/testUtils'; +import FacetItem from '../FacetItem'; + +const renderFacetItem = (props: {}) => + shallow( + + ); + +it('should render active', () => { + expect(renderFacetItem({ active: true })).toMatchSnapshot(); +}); + +it('should render inactive', () => { + expect(renderFacetItem({ active: false })).toMatchSnapshot(); +}); + +it('should render stat', () => { + expect(renderFacetItem({ stat: 13 })).toMatchSnapshot(); +}); + +it('should render disabled', () => { + expect(renderFacetItem({ disabled: true })).toMatchSnapshot(); +}); + +it('should render half width', () => { + expect(renderFacetItem({ halfWidth: true })).toMatchSnapshot(); +}); + +it('should render effort stat', () => { + expect(renderFacetItem({ facetMode: 'effort', stat: 1234 })).toMatchSnapshot(); +}); + +it('should call onClick', () => { + const onClick = jest.fn(); + const wrapper = renderFacetItem({ onClick }); + click(wrapper, { currentTarget: { dataset: { value: 'bar' } } }); + expect(onClick).toHaveBeenCalled(); +}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetItemMeasureValue-test.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetItemMeasureValue-test.js new file mode 100644 index 00000000000..d2a6ec5769a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetItemMeasureValue-test.js @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 { shallow } from 'enzyme'; +import FacetItemMeasureValue from '../FacetItemMeasureValue'; + +const MEASURE = { + metric: { + key: 'bugs', + type: 'INT', + name: 'Bugs', + domain: 'Reliability' + }, + value: '5', + periods: [{ index: 1, value: '5' }], + leak: '5' +}; +const LEAK_MEASURE = { + metric: { + key: 'new_bugs', + type: 'INT', + name: 'New Bugs', + domain: 'Reliability' + }, + periods: [{ index: 1, value: '5' }], + leak: '5' +}; + +it('should display measure value', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('should display leak measure value', () => { + expect(shallow()).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.js new file mode 100644 index 00000000000..aa993cfc71b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.js @@ -0,0 +1,77 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 React from 'react'; +import { shallow } from 'enzyme'; +import Sidebar from '../Sidebar'; + +const MEASURES = [ + { + metric: { + key: 'lines_to_cover', + type: 'INT', + name: 'Lines to Cover', + domain: 'Coverage' + }, + value: '431', + periods: [{ index: 1, value: '70' }], + leak: '70' + }, + { + metric: { + key: 'coverage', + type: 'PERCENT', + name: 'Coverage', + domain: 'Coverage' + }, + value: '99.3', + periods: [{ index: 1, value: '0.0999999999999943' }], + leak: '0.0999999999999943' + }, + { + metric: { + key: 'duplicated_lines_density', + type: 'PERCENT', + name: 'Duplicated Lines (%)', + domain: 'Duplications' + }, + value: '3.2', + periods: [{ index: 1, value: '0.0' }], + leak: '0.0' + } +]; + +const PROPS = { + measures: MEASURES, + selectedMetric: 'foo', + updateQuery: () => {} +}; + +it('should display two facets', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('should correctly toggle facets', () => { + const wrapper = shallow(); + expect(wrapper.state('closedFacets').bugs).toBeUndefined(); + wrapper.instance().toggleFacet('bugs'); + expect(wrapper.state('closedFacets').bugs).toBeTruthy(); + wrapper.instance().toggleFacet('bugs'); + expect(wrapper.state('closedFacets').bugs).toBeFalsy(); +}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap new file mode 100644 index 00000000000..6ca87d36a3f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap @@ -0,0 +1,209 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should display facet item list 1`] = ` + + + + + + + Bugs + + + } + onClick={[Function]} + stat={ + + } + value="bugs" + /> + + + + New Bugs + + + } + onClick={[Function]} + stat={ + + } + value="new_bugs" + /> + + +`; + +exports[`should display facet item list with bugs selected 1`] = ` + + + + + + + Bugs + + + } + onClick={[Function]} + stat={ + + } + value="bugs" + /> + + + + New Bugs + + + } + onClick={[Function]} + stat={ + + } + value="new_bugs" + /> + + +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetHeader-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetHeader-test.js.snap new file mode 100644 index 00000000000..bd6c0e5681c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetHeader-test.js.snap @@ -0,0 +1,148 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render closed facet with value 1`] = ` + +`; + +exports[`should render closed facet without value 1`] = ` + +`; + +exports[`should render open facet with value 1`] = ` + +`; + +exports[`should render open facet without value 1`] = ` + +`; + +exports[`should render without link 1`] = ` +
    + + foo + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetItem-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetItem-test.js.snap new file mode 100644 index 00000000000..82e72d3b0af --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetItem-test.js.snap @@ -0,0 +1,105 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render active 1`] = ` + + + foo + + + +`; + +exports[`should render disabled 1`] = ` + + + foo + + + +`; + +exports[`should render effort stat 1`] = ` + + + foo + + + 1234 + + +`; + +exports[`should render half width 1`] = ` + + + foo + + + +`; + +exports[`should render inactive 1`] = ` + + + foo + + + +`; + +exports[`should render stat 1`] = ` + + + foo + + + 13 + + +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetItemMeasureValue-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetItemMeasureValue-test.js.snap new file mode 100644 index 00000000000..9bd6a3e7e00 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetItemMeasureValue-test.js.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should display leak measure value 1`] = ` +
    + +
    +`; + +exports[`should display measure value 1`] = ` +
    + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap new file mode 100644 index 00000000000..ba904466156 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap @@ -0,0 +1,82 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should display two facets 1`] = ` +
    + + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/style.css b/server/sonar-web/src/main/js/apps/component-measures/style.css new file mode 100644 index 00000000000..fc294c55dee --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/style.css @@ -0,0 +1,28 @@ +.measures-domains-leak-header { + float: right; + background-color: #fbf3d5; + border: 1px solid #eae3c7; + padding: 4px 10px; + white-space: nowrap; +} + +.domain-measures-leak { + background-color: #fbf3d5; + border: 1px solid #eae3c7; + padding: 4px 4px; + margin: -5px -5px; +} + +.domain-measures-value .rating { + width: 18px; + height: 18px; + line-height: 18px; + margin-top: -2px; + margin-bottom: -2px; + font-size: 12px; +} + +.domain-measures-leak .rating { + margin-top: -3px; + margin-bottom: -3px; +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/types.js b/server/sonar-web/src/main/js/apps/component-measures/types.js new file mode 100644 index 00000000000..501e60ed0c1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/types.js @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ +export type Component = { + isFavorite?: boolean, + isRecentlyBrowsed?: boolean, + key: string, + match?: string, + name: string, + organization?: string, + project?: string, + qualifier: string +}; + +export type Query = { + metric: ?string, + view: string +}; + +export type Period = { + index: number, + date: string, + mode: string, + parameter?: string +}; diff --git a/server/sonar-web/src/main/js/apps/component-measures/utils.js b/server/sonar-web/src/main/js/apps/component-measures/utils.js new file mode 100644 index 00000000000..2622792dcda --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/utils.js @@ -0,0 +1,109 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 { groupBy, memoize, sortBy, toPairs } from 'lodash'; +import { getLocalizedMetricName } from '../../helpers/l10n'; +import { cleanQuery, parseAsString, serializeString } from '../../helpers/query'; +import { domains } from './config/domains'; +import type { Query } from './types'; +import type { RawQuery } from '../../helpers/query'; +import type { Measure, MeasureEnhanced } from '../../components/measure/types'; + +export const DEFAULT_VIEW = 'list'; +const KNOWN_DOMAINS = [ + 'Releasability', + 'Reliability', + 'Security', + 'Maintainability', + 'Coverage', + 'Duplications', + 'Size', + 'Complexity' +]; +const BANNED_MEASURES = [ + 'blocker_violations', + 'new_blocker_violations', + 'critical_violations', + 'new_critical_violations', + 'major_violations', + 'new_major_violations', + 'minor_violations', + 'new_minor_violations', + 'info_violations', + 'new_info_violations' +]; + +export function filterMeasures(measures: Array): Array { + return measures.filter(measure => !BANNED_MEASURES.includes(measure.metric.key)); +} + +export function sortMeasures( + domainName: string, + measures: Array +): Array { + const config = domains[domainName] || {}; + const configOrder = config.order || []; + return sortBy(measures, [ + measure => { + const idx = configOrder.indexOf(measure.metric.key); + return idx >= 0 ? idx : configOrder.length; + }, + measure => getLocalizedMetricName(measure.metric) + ]); +} + +export function getLeakValue(measure: ?Measure): ?string { + if (!measure || !measure.periods) { + return null; + } + const period = measure.periods.find(period => period.index === 1); + return period ? period.value : null; +} + +export const groupByDomains = memoize((measures: Array): Array<{ + name: string, + measures: Array +}> => { + const domains = toPairs(groupBy(measures, measure => measure.metric.domain)).map(r => { + const [name, measures] = r; + const sortedMeasures = sortBy(measures, measure => getLocalizedMetricName(measure.metric)); + return { name, measures: sortedMeasures }; + }); + + return sortBy(domains, [ + domain => { + const idx = KNOWN_DOMAINS.indexOf(domain.name); + return idx >= 0 ? idx : KNOWN_DOMAINS.length; + }, + 'name' + ]); +}); + +export const parseQuery = memoize((urlQuery: RawQuery): Query => ({ + metric: parseAsString(urlQuery['metric']), + view: parseAsString(urlQuery['view']) || DEFAULT_VIEW +})); + +export const serializeQuery = memoize((query: Query): RawQuery => { + return cleanQuery({ + metric: serializeString(query.metric), + view: query.view === DEFAULT_VIEW ? null : serializeString(query.view) + }); +}); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js index 58e64edbad7..88c5a0e7c77 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js @@ -64,17 +64,19 @@ export default class FacetItem extends React.PureComponent { {this.props.name} - - {formattedStat} - + {formattedStat != null && + + {formattedStat} + }
    : {this.props.name} - - {formattedStat} - + {formattedStat != null && + + {formattedStat} + } ; } } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap index 1d5c7a04b95..b8db949fded 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap @@ -12,9 +12,6 @@ exports[`should render active 1`] = ` > foo - `; @@ -27,9 +24,6 @@ exports[`should render disabled 1`] = ` > foo - `; @@ -65,9 +59,6 @@ exports[`should render half width 1`] = ` > foo - `; @@ -83,9 +74,6 @@ exports[`should render inactive 1`] = ` > foo - `; diff --git a/server/sonar-web/src/main/js/components/measure/Measure.js b/server/sonar-web/src/main/js/components/measure/Measure.js index a2b1e4dd22f..3ce72c83ee0 100644 --- a/server/sonar-web/src/main/js/components/measure/Measure.js +++ b/server/sonar-web/src/main/js/components/measure/Measure.js @@ -28,45 +28,18 @@ import type { MeasureEnhanced } from './types'; type Props = { className?: string, - measure: MeasureEnhanced, - decimals?: ?number + decimals?: ?number, + measure: MeasureEnhanced }; -export default class Measure extends React.PureComponent { - props: Props; +export default function Measure({ className, decimals, measure }: Props) { + const metric = measure.metric; - renderRating() { - const { measure } = this.props; - const metric = measure.metric; - const value = isDiffMetric(metric.key) ? measure.leak : measure.value; - const tooltip = getRatingTooltip(metric.key, value); - const rating = ; - - if (tooltip) { - return ( - - - {rating} - - - ); - } - - return rating; + if (metric.type === 'LEVEL') { + return ; } - render() { - const { className, decimals, measure } = this.props; - const metric = measure.metric; - - if (metric.type === 'RATING') { - return this.renderRating(); - } - - if (metric.type === 'LEVEL') { - return ; - } - + if (metric.type !== 'RATING') { const formattedValue = isDiffMetric(metric.key) ? formatLeak(measure.leak, metric, { decimals }) : formatMeasure(measure.value, metric.type, { decimals }); @@ -76,4 +49,18 @@ export default class Measure extends React.PureComponent { ); } + + const value = isDiffMetric(metric.key) ? measure.leak : measure.value; + const tooltip = getRatingTooltip(metric.key, value); + const rating = ; + if (tooltip) { + return ( + + + {rating} + + + ); + } + return rating; } diff --git a/server/sonar-web/src/main/less/components/search-navigator.less b/server/sonar-web/src/main/less/components/search-navigator.less index 97961b89988..17075deae12 100644 --- a/server/sonar-web/src/main/less/components/search-navigator.less +++ b/server/sonar-web/src/main/less/components/search-navigator.less @@ -164,7 +164,7 @@ top: 0; right: 0; margin-left: 5px; - padding: 4px 5px; + padding: 5px 5px; background-color: @barBackgroundColor; color: @secondFontColor; font-size: @smallFontSize; -- 2.39.5