diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-07-26 16:04:19 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-08-14 11:44:44 +0200 |
commit | f2c95a685a3b950a68122af1e5673978a07df773 (patch) | |
tree | a81639fe40a98b6d746369658d8a3f88082fd3a2 /server/sonar-web/src | |
parent | cadc6bb011393ff5092f83f146bd9b74bad21460 (diff) | |
download | sonarqube-f2c95a685a3b950a68122af1e5673978a07df773.tar.gz sonarqube-f2c95a685a3b950a68122af1e5673978a07df773.zip |
SONAR-9608 SONAR-9609 Create the new measures page sidebar with all facets
Diffstat (limited to 'server/sonar-web/src')
35 files changed, 2330 insertions, 57 deletions
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 ( + <li> + <Link + to={{ pathname: '/component_measures', query: { id: this.props.component.key } }} + activeClassName="active"> + {translate('layout.measures')} + </Link> + </li> + ); + } + renderComponentMeasuresOldLink() { return ( <li> @@ -359,6 +371,7 @@ export default class ComponentNavMenu extends React.PureComponent { <NavBarTabs> {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", }, @@ -61,6 +61,23 @@ exports[`should work with extensions 1`] = ` style={Object {}} to={ Object { + "pathname": "/component_measures_old", + "query": Object { + "id": "foo", + }, + } + } + > + Old Measures + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { "pathname": "/code", "query": Object { "id": "foo", @@ -243,7 +260,7 @@ exports[`should work with multiple extensions 1`] = ` style={Object {}} to={ Object { - "pathname": "/component_measures_old", + "pathname": "/component_measures", "query": Object { "id": "foo", }, @@ -260,6 +277,23 @@ exports[`should work with multiple extensions 1`] = ` style={Object {}} to={ Object { + "pathname": "/component_measures_old", + "query": Object { + "id": "foo", + }, + } + } + > + Old Measures + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { "pathname": "/code", "query": Object { "id": "foo", diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js index 52dc397468c..4c93e5e341d 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.js +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js @@ -45,7 +45,8 @@ import backgroundTasksRoutes from '../../apps/background-tasks/routes'; import codeRoutes from '../../apps/code/routes'; import codingRulesRoutes from '../../apps/coding-rules/routes'; import componentRoutes from '../../apps/component/routes'; -import componentMeasuresRoutes from '../../apps/component-measures-old/routes'; +import componentMeasuresRoutes from '../../apps/component-measures/routes'; +import componentMeasuresOldRoutes from '../../apps/component-measures-old/routes'; import customMeasuresRoutes from '../../apps/custom-measures/routes'; import groupsRoutes from '../../apps/groups/routes'; import issuesRoutes from '../../apps/issues/routes'; @@ -170,7 +171,8 @@ const startReactApp = () => { getComponent={() => import('../components/ProjectContainer').then(i => i.default)}> <Route path="code" childRoutes={codeRoutes} /> - <Route path="component_measures_old" childRoutes={componentMeasuresRoutes} /> + <Route path="component_measures" childRoutes={componentMeasuresRoutes} /> + <Route path="component_measures_old" childRoutes={componentMeasuresOldRoutes} /> <Route path="custom_measures" childRoutes={customMeasuresRoutes} /> <Route path="dashboard" childRoutes={overviewRoutes} /> <Route path="project"> 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<MeasureEnhanced>, periods: Array<Period> }>, + fetchMetrics: () => void, + metrics: Metrics, + router: { + push: ({ pathname: string, query?: RawQuery }) => void + } +|}; + +type State = {| + loading: boolean, + measures: Array<MeasureEnhanced>, + periods: Array<Period> +|}; + +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 <i className="spinner spinner-margin" />; + } + const query = parseQuery(this.props.location.query); + return ( + <div className="layout-page" id="component-measures"> + <Helmet title={translate('layout.measures')} /> + + <div className="layout-page-side-outer"> + <div className="layout-page-side" style={{ top: 95 }}> + <div className="layout-page-side-inner"> + <div className="layout-page-filters"> + <Sidebar + measures={this.state.measures} + selectedMetric={query.metric} + updateQuery={this.updateQuery} + /> + </div> + </div> + </div> + </div> + + <div className="layout-page-main"> + <div className="layout-page-header-panel layout-page-main-header issues-main-header"> + <div className="layout-page-header-panel-inner layout-page-main-header-inner"> + <div className="layout-page-main-inner">Page Actions</div> + </div> + </div> + + <div className="layout-page-main-inner">Main</div> + </div> + </div> + ); + } +} 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<Measure>): Array<Measure> => { + 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<Array<MeasureEnhanced>> => { + 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<MeasureEnhanced> = 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(<App {...PROPS} />); + 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`] = ` +<div + className="layout-page" + id="component-measures" +> + <HelmetWrapper + encodeSpecialCharacters={true} + title="layout.measures" + /> + <div + className="layout-page-side-outer" + > + <div + className="layout-page-side" + style={ + Object { + "top": 95, + } + } + > + <div + className="layout-page-side-inner" + > + <div + className="layout-page-filters" + > + <Sidebar + measures={Array []} + selectedMetric="" + updateQuery={[Function]} + /> + </div> + </div> + </div> + </div> + <div + className="layout-page-main" + > + <div + className="layout-page-header-panel layout-page-main-header issues-main-header" + > + <div + className="layout-page-header-panel-inner layout-page-main-header-inner" + > + <div + className="layout-page-main-inner" + > + Page Actions + </div> + </div> + </div> + <div + className="layout-page-main-inner" + > + Main + </div> + </div> +</div> +`; 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<MeasureEnhanced> }, + 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 ( + <FacetBox> + <FacetHeader + name={getLocalizedMetricDomain(domain.name)} + onClick={this.handleHeaderClick} + open={this.props.open} + values={measures.find(measure => measure.metric.key === selected) ? 1 : 0} + /> + + {this.props.open && + <FacetItemsList> + {measures.map(measure => + <FacetItem + active={measure.metric.key === selected} + disabled={false} + key={measure.metric.key} + name={ + <Tooltip overlay={getLocalizedMetricName(measure.metric)} mouseEnterDelay={1}> + <span id={`measure-${measure.metric.key}-name`}> + <IssueTypeIcon query={measure.metric.key} className="little-spacer-right" /> + {getLocalizedMetricName(measure.metric)} + </span> + </Tooltip> + } + onClick={this.props.onChange} + stat={<FacetItemMeasureValue measure={measure} />} + value={measure.metric.key} + /> + )} + </FacetItemsList>} + </FacetBox> + ); + } +} 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 ( + <div className="search-navigator-facet-box"> + {props.children} + </div> + ); +} 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 ( + <svg viewBox="0 0 1792 1792" width="10" height="10" style={{ paddingTop: 3 }}> + {this.props.open + ? <path + style={{ fill: 'currentColor ' }} + d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z" + /> + : <path + style={{ fill: 'currentColor ' }} + d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z" + />} + </svg> + ); + } + + renderValueIndicator() { + if (this.props.open || !this.props.values) { + return null; + } + return ( + <span className="spacer-left badge is-rounded"> + {this.props.values} + </span> + ); + } + + render() { + return ( + <div> + {this.props.onClick + ? <a className="search-navigator-facet-header" href="#" onClick={this.handleClick}> + {this.renderCheckbox()} {this.props.name} {this.renderValueIndicator()} + </a> + : <span className="search-navigator-facet-header"> + {this.props.name} + </span>} + </div> + ); + } +} 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 + ? <span className={className}> + <span className="facet-name"> + {this.props.name} + </span> + <span className="facet-stat"> + {this.props.stat} + </span> + </span> + : <a className={className} href="#" onClick={this.handleClick}> + <span className="facet-name"> + {this.props.name} + </span> + <span className="facet-stat"> + {this.props.stat} + </span> + </a>; + } +} 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 ( + <div + id={`measure-${measure.metric.key}-leak`} + className="domain-measures-value domain-measures-leak"> + <Measure measure={measure} /> + </div> + ); + } + + return ( + <div id={`measure-${measure.metric.key}-value`} className="domain-measures-value"> + <Measure measure={measure} /> + </div> + ); +} 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<React.Element<*>> +|}; + +export default function FacetItemsList(props: Props) { + return ( + <div className="search-navigator-facet-list"> + {props.children} + </div> + ); +} 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<MeasureEnhanced>, + selectedMetric: string, + updateQuery: Query => void +|}; + +type State = {| + closedFacets: { [string]: boolean }, + measuresByDomains: Array<{ name: string, measures: Array<MeasureEnhanced> }> +|}; + +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 ( + <div className="search-navigator-facets-list"> + {measuresByDomains.map(domain => + <DomainFacet + key={domain.name} + domain={domain} + onChange={this.changeMetric} + onToggle={this.toggleFacet} + open={!this.state.closedFacets[domain.name]} + selected={this.props.selectedMetric} + /> + )} + </div> + ); + } +} 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(<DomainFacet {...PROPS} />)).toMatchSnapshot(); +}); + +it('should display facet item list with bugs selected', () => { + expect(shallow(<DomainFacet {...PROPS} selected="bugs" />)).toMatchSnapshot(); +}); + +it('should render closed', () => { + const wrapper = shallow(<DomainFacet {...PROPS} open={false} />); + 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(<FacetHeader name="foo" onClick={jest.fn()} open={true} values={1} />) + ).toMatchSnapshot(); +}); + +it('should render open facet without value', () => { + expect(shallow(<FacetHeader name="foo" onClick={jest.fn()} open={true} />)).toMatchSnapshot(); +}); + +it('should render closed facet with value', () => { + expect( + shallow(<FacetHeader name="foo" onClick={jest.fn()} open={false} values={1} />) + ).toMatchSnapshot(); +}); + +it('should render closed facet without value', () => { + expect(shallow(<FacetHeader name="foo" onClick={jest.fn()} open={false} />)).toMatchSnapshot(); +}); + +it('should render without link', () => { + expect(shallow(<FacetHeader name="foo" open={false} />)).toMatchSnapshot(); +}); + +it('should call onClick', () => { + const onClick = jest.fn(); + const wrapper = shallow(<FacetHeader name="foo" onClick={onClick} open={false} />); + 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( + <FacetItem active={false} name="foo" onClick={jest.fn()} stat={''} value="bar" {...props} /> + ); + +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(<FacetItemMeasureValue measure={MEASURE} />)).toMatchSnapshot(); +}); + +it('should display leak measure value', () => { + expect(shallow(<FacetItemMeasureValue measure={LEAK_MEASURE} />)).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(<Sidebar {...PROPS} />)).toMatchSnapshot(); +}); + +it('should correctly toggle facets', () => { + const wrapper = shallow(<Sidebar {...PROPS} />); + 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`] = ` +<FacetBox> + <FacetHeader + name="Reliability" + onClick={[Function]} + open={true} + values={0} + /> + <FacetItemsList> + <FacetItem + active={false} + disabled={false} + halfWidth={false} + name={ + <Tooltip + mouseEnterDelay={1} + overlay="Bugs" + placement="bottom" + > + <span + id="measure-bugs-name" + > + <IssueTypeIcon + className="little-spacer-right" + query="bugs" + /> + Bugs + </span> + </Tooltip> + } + onClick={[Function]} + stat={ + <FacetItemMeasureValue + measure={ + Object { + "leak": "5", + "metric": Object { + "domain": "Reliability", + "key": "bugs", + "name": "Bugs", + "type": "INT", + }, + "periods": Array [ + Object { + "index": 1, + "value": "5", + }, + ], + "value": "5", + } + } + /> + } + value="bugs" + /> + <FacetItem + active={false} + disabled={false} + halfWidth={false} + name={ + <Tooltip + mouseEnterDelay={1} + overlay="New Bugs" + placement="bottom" + > + <span + id="measure-new_bugs-name" + > + <IssueTypeIcon + className="little-spacer-right" + query="new_bugs" + /> + New Bugs + </span> + </Tooltip> + } + onClick={[Function]} + stat={ + <FacetItemMeasureValue + measure={ + Object { + "leak": "5", + "metric": Object { + "domain": "Reliability", + "key": "new_bugs", + "name": "New Bugs", + "type": "INT", + }, + "periods": Array [ + Object { + "index": 1, + "value": "5", + }, + ], + } + } + /> + } + value="new_bugs" + /> + </FacetItemsList> +</FacetBox> +`; + +exports[`should display facet item list with bugs selected 1`] = ` +<FacetBox> + <FacetHeader + name="Reliability" + onClick={[Function]} + open={true} + values={1} + /> + <FacetItemsList> + <FacetItem + active={true} + disabled={false} + halfWidth={false} + name={ + <Tooltip + mouseEnterDelay={1} + overlay="Bugs" + placement="bottom" + > + <span + id="measure-bugs-name" + > + <IssueTypeIcon + className="little-spacer-right" + query="bugs" + /> + Bugs + </span> + </Tooltip> + } + onClick={[Function]} + stat={ + <FacetItemMeasureValue + measure={ + Object { + "leak": "5", + "metric": Object { + "domain": "Reliability", + "key": "bugs", + "name": "Bugs", + "type": "INT", + }, + "periods": Array [ + Object { + "index": 1, + "value": "5", + }, + ], + "value": "5", + } + } + /> + } + value="bugs" + /> + <FacetItem + active={false} + disabled={false} + halfWidth={false} + name={ + <Tooltip + mouseEnterDelay={1} + overlay="New Bugs" + placement="bottom" + > + <span + id="measure-new_bugs-name" + > + <IssueTypeIcon + className="little-spacer-right" + query="new_bugs" + /> + New Bugs + </span> + </Tooltip> + } + onClick={[Function]} + stat={ + <FacetItemMeasureValue + measure={ + Object { + "leak": "5", + "metric": Object { + "domain": "Reliability", + "key": "new_bugs", + "name": "New Bugs", + "type": "INT", + }, + "periods": Array [ + Object { + "index": 1, + "value": "5", + }, + ], + } + } + /> + } + value="new_bugs" + /> + </FacetItemsList> +</FacetBox> +`; 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`] = ` +<div> + <a + className="search-navigator-facet-header" + href="#" + onClick={[Function]} + > + <svg + height="10" + style={ + Object { + "paddingTop": 3, + } + } + viewBox="0 0 1792 1792" + width="10" + > + <path + d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z" + style={ + Object { + "fill": "currentColor ", + } + } + /> + </svg> + + foo + + <span + className="spacer-left badge is-rounded" + > + 1 + </span> + </a> +</div> +`; + +exports[`should render closed facet without value 1`] = ` +<div> + <a + className="search-navigator-facet-header" + href="#" + onClick={[Function]} + > + <svg + height="10" + style={ + Object { + "paddingTop": 3, + } + } + viewBox="0 0 1792 1792" + width="10" + > + <path + d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z" + style={ + Object { + "fill": "currentColor ", + } + } + /> + </svg> + + foo + + </a> +</div> +`; + +exports[`should render open facet with value 1`] = ` +<div> + <a + className="search-navigator-facet-header" + href="#" + onClick={[Function]} + > + <svg + height="10" + style={ + Object { + "paddingTop": 3, + } + } + viewBox="0 0 1792 1792" + width="10" + > + <path + d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z" + style={ + Object { + "fill": "currentColor ", + } + } + /> + </svg> + + foo + + </a> +</div> +`; + +exports[`should render open facet without value 1`] = ` +<div> + <a + className="search-navigator-facet-header" + href="#" + onClick={[Function]} + > + <svg + height="10" + style={ + Object { + "paddingTop": 3, + } + } + viewBox="0 0 1792 1792" + width="10" + > + <path + d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z" + style={ + Object { + "fill": "currentColor ", + } + } + /> + </svg> + + foo + + </a> +</div> +`; + +exports[`should render without link 1`] = ` +<div> + <span + className="search-navigator-facet-header" + > + foo + </span> +</div> +`; 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`] = ` +<a + className="facet search-navigator-facet active" + href="#" + onClick={[Function]} +> + <span + className="facet-name" + > + foo + </span> + <span + className="facet-stat" + /> +</a> +`; + +exports[`should render disabled 1`] = ` +<span + className="facet search-navigator-facet" +> + <span + className="facet-name" + > + foo + </span> + <span + className="facet-stat" + /> +</span> +`; + +exports[`should render effort stat 1`] = ` +<a + className="facet search-navigator-facet" + href="#" + onClick={[Function]} +> + <span + className="facet-name" + > + foo + </span> + <span + className="facet-stat" + > + 1234 + </span> +</a> +`; + +exports[`should render half width 1`] = ` +<a + className="facet search-navigator-facet search-navigator-facet-half" + href="#" + onClick={[Function]} +> + <span + className="facet-name" + > + foo + </span> + <span + className="facet-stat" + /> +</a> +`; + +exports[`should render inactive 1`] = ` +<a + className="facet search-navigator-facet" + href="#" + onClick={[Function]} +> + <span + className="facet-name" + > + foo + </span> + <span + className="facet-stat" + /> +</a> +`; + +exports[`should render stat 1`] = ` +<a + className="facet search-navigator-facet" + href="#" + onClick={[Function]} +> + <span + className="facet-name" + > + foo + </span> + <span + className="facet-stat" + > + 13 + </span> +</a> +`; 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`] = ` +<div + className="domain-measures-value domain-measures-leak" + id="measure-new_bugs-leak" +> + <Measure + measure={ + Object { + "leak": "5", + "metric": Object { + "domain": "Reliability", + "key": "new_bugs", + "name": "New Bugs", + "type": "INT", + }, + "periods": Array [ + Object { + "index": 1, + "value": "5", + }, + ], + } + } + /> +</div> +`; + +exports[`should display measure value 1`] = ` +<div + className="domain-measures-value" + id="measure-bugs-value" +> + <Measure + measure={ + Object { + "leak": "5", + "metric": Object { + "domain": "Reliability", + "key": "bugs", + "name": "Bugs", + "type": "INT", + }, + "periods": Array [ + Object { + "index": 1, + "value": "5", + }, + ], + "value": "5", + } + } + /> +</div> +`; 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`] = ` +<div + className="search-navigator-facets-list" +> + <DomainFacet + domain={ + 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", + } + } + onChange={[Function]} + onToggle={[Function]} + open={true} + selected="foo" + /> + <DomainFacet + domain={ + 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", + } + } + onChange={[Function]} + onToggle={[Function]} + open={true} + selected="foo" + /> +</div> +`; 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<MeasureEnhanced>): Array<MeasureEnhanced> { + return measures.filter(measure => !BANNED_MEASURES.includes(measure.metric.key)); +} + +export function sortMeasures( + domainName: string, + measures: Array<MeasureEnhanced> +): Array<MeasureEnhanced> { + 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<MeasureEnhanced>): Array<{ + name: string, + measures: Array<MeasureEnhanced> +}> => { + 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 { <span className="facet-name"> {this.props.name} </span> - <span className="facet-stat"> - {formattedStat} - </span> + {formattedStat != null && + <span className="facet-stat"> + {formattedStat} + </span>} </span> : <a className={className} data-value={this.props.value} href="#" onClick={this.handleClick}> <span className="facet-name"> {this.props.name} </span> - <span className="facet-stat"> - {formattedStat} - </span> + {formattedStat != null && + <span className="facet-stat"> + {formattedStat} + </span>} </a>; } } 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 </span> - <span - className="facet-stat" - /> </a> `; @@ -27,9 +24,6 @@ exports[`should render disabled 1`] = ` > foo </span> - <span - className="facet-stat" - /> </span> `; @@ -65,9 +59,6 @@ exports[`should render half width 1`] = ` > foo </span> - <span - className="facet-stat" - /> </a> `; @@ -83,9 +74,6 @@ exports[`should render inactive 1`] = ` > foo </span> - <span - className="facet-stat" - /> </a> `; 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 = <Rating value={value} />; - - if (tooltip) { - return ( - <Tooltips overlay={tooltip}> - <span className={this.props.className}> - {rating} - </span> - </Tooltips> - ); - } - - return rating; + if (metric.type === 'LEVEL') { + return <Level className={className} level={measure.value} />; } - render() { - const { className, decimals, measure } = this.props; - const metric = measure.metric; - - if (metric.type === 'RATING') { - return this.renderRating(); - } - - if (metric.type === 'LEVEL') { - return <Level className={className} level={measure.value} />; - } - + 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 { </span> ); } + + const value = isDiffMetric(metric.key) ? measure.leak : measure.value; + const tooltip = getRatingTooltip(metric.key, value); + const rating = <Rating value={value} />; + if (tooltip) { + return ( + <Tooltips overlay={tooltip}> + <span className={className}> + {rating} + </span> + </Tooltips> + ); + } + 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; |