aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-07-26 16:04:19 +0200
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-08-14 11:44:44 +0200
commitf2c95a685a3b950a68122af1e5673978a07df773 (patch)
treea81639fe40a98b6d746369658d8a3f88082fd3a2 /server/sonar-web/src
parentcadc6bb011393ff5092f83f146bd9b74bad21460 (diff)
downloadsonarqube-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')
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js13
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap38
-rw-r--r--server/sonar-web/src/main/js/app/utils/startReactApp.js6
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/__tests__/__snapshots__/utils-test.js.snap97
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.js121
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/App.js147
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js87
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.js50
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap61
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/config/domains.js134
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/routes.js28
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js82
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetBox.js33
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetHeader.js86
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetItem.js71
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetItemMeasureValue.js42
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetItemsList.js33
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js81
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/DomainFacet-test.js71
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetHeader-test.js55
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetItem-test.js60
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/FacetItemMeasureValue-test.js53
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/Sidebar-test.js77
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap209
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetHeader-test.js.snap148
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetItem-test.js.snap105
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/FacetItemMeasureValue-test.js.snap56
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap82
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/style.css28
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/types.js41
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/utils.js109
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js14
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap12
-rw-r--r--server/sonar-web/src/main/js/components/measure/Measure.js55
-rw-r--r--server/sonar-web/src/main/less/components/search-navigator.less2
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;