aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-07-27 17:21:25 +0200
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-08-14 11:44:44 +0200
commitd7b669175e4e40341f6f1553ebe8ed84a9980ce2 (patch)
treec631f0dac9d40e88042d4d2ca8f4643b9f780134 /server/sonar-web
parenta0ea568244db7b77b165c7ecf7dca83620f8edbd (diff)
downloadsonarqube-d7b669175e4e40341f6f1553ebe8ed84a9980ce2.tar.gz
sonarqube-d7b669175e4e40341f6f1553ebe8ed84a9980ce2.zip
SONAR-9608 SONAR-9634 Create the measure list view
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures-old/details/MeasureDetailsHeader.js2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.js14
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/App.js61
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js53
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js61
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js160
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js86
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.js32
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.js20
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.js61
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js78
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap72
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap30
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap149
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentCell.js106
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsList.js84
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsListRow.js70
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/drilldown/EmptyComponentsList.js30
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js193
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/drilldown/MeasureCell.js39
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js27
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/config/complementary.js38
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/style.css47
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/types.js25
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/utils.js25
-rw-r--r--server/sonar-web/src/main/js/components/measure/utils.js16
-rw-r--r--server/sonar-web/src/main/js/components/shared/ComplexityDistribution.js (renamed from server/sonar-web/src/main/js/components/shared/complexity-distribution.js)7
-rw-r--r--server/sonar-web/src/main/js/store/metrics/actions.js4
-rw-r--r--server/sonar-web/src/main/js/store/metrics/reducer.js4
-rw-r--r--server/sonar-web/src/main/js/store/rootReducer.js2
30 files changed, 1473 insertions, 123 deletions
diff --git a/server/sonar-web/src/main/js/apps/component-measures-old/details/MeasureDetailsHeader.js b/server/sonar-web/src/main/js/apps/component-measures-old/details/MeasureDetailsHeader.js
index f2281c24eba..8a6254d140b 100644
--- a/server/sonar-web/src/main/js/apps/component-measures-old/details/MeasureDetailsHeader.js
+++ b/server/sonar-web/src/main/js/apps/component-measures-old/details/MeasureDetailsHeader.js
@@ -25,7 +25,7 @@ import LeakPeriodLegend from '../components/LeakPeriodLegend';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import HistoryIcon from '../../../components/icons-components/HistoryIcon';
import Tooltip from '../../../components/controls/Tooltip';
-import { ComplexityDistribution } from '../../../components/shared/complexity-distribution';
+import ComplexityDistribution from '../../../components/shared/ComplexityDistribution';
import { isDiffMetric } from '../../../helpers/measures';
import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
import { getComponentMeasureHistory } from '../../../helpers/urls';
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
index 62eaf3c7a56..00be69c8786 100644
--- 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
@@ -92,30 +92,32 @@ describe('groupByDomains', () => {
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({
+ expect(utils.parseQuery({})).toEqual({ metric: '', selected: '', view: utils.DEFAULT_VIEW });
+ expect(utils.parseQuery({ metric: 'foo', selected: 'bar', view: 'tree' })).toEqual({
metric: 'foo',
+ selected: 'bar',
view: 'tree'
});
});
it('should be memoized', () => {
- const query = { metric: 'foo', view: 'tree' };
+ const query = { metric: 'foo', selected: 'bar', 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({
+ expect(utils.serializeQuery({ metric: '', selected: '', view: 'list' })).toEqual({});
+ expect(utils.serializeQuery({ metric: 'foo', selected: 'bar', view: 'tree' })).toEqual({
metric: 'foo',
+ selected: 'bar',
view: 'tree'
});
});
it('should be memoized', () => {
- const query = { metric: 'foo', view: 'tree' };
+ const query = { metric: 'foo', selected: 'bar', 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
index 02d5b590ef4..7b9ac879874 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/App.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.js
@@ -20,12 +20,13 @@
// @flow
import React from 'react';
import Helmet from 'react-helmet';
+import MeasureContent from './MeasureContent';
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 { Metric } from '../../../store/metrics/actions';
import type { MeasureEnhanced } from '../../../components/measure/types';
import '../style.css';
@@ -34,10 +35,11 @@ type Props = {|
location: { pathname: string, query: RawQuery },
fetchMeasures: (
Component,
- Metrics
- ) => Promise<{ measures: Array<MeasureEnhanced>, periods: Array<Period> }>,
+ Array<string>
+ ) => Promise<{ component: Component, measures: Array<MeasureEnhanced>, leakPeriod: ?Period }>,
fetchMetrics: () => void,
- metrics: Metrics,
+ metrics: { [string]: Metric },
+ metricsKey: Array<string>,
router: {
push: ({ pathname: string, query?: RawQuery }) => void
}
@@ -46,7 +48,7 @@ type Props = {|
type State = {|
loading: boolean,
measures: Array<MeasureEnhanced>,
- periods: Array<Period>
+ leakPeriod: ?Period
|};
export default class App extends React.PureComponent {
@@ -59,7 +61,7 @@ export default class App extends React.PureComponent {
this.state = {
loading: true,
measures: [],
- periods: []
+ leakPeriod: null
};
}
@@ -83,12 +85,27 @@ export default class App extends React.PureComponent {
}
}
- fetchMeasures = ({ component, fetchMeasures, metrics }: Props) => {
+ componentWillUnmount() {
+ this.mounted = false;
+ const footer = document.getElementById('footer');
+ if (footer) {
+ footer.classList.remove('search-navigator-footer');
+ }
+ }
+
+ fetchMeasures = ({ component, fetchMeasures, metrics, metricsKey }: Props) => {
this.setState({ loading: true });
- fetchMeasures(component, metrics).then(
- ({ measures, periods }) => {
+ const filterdKeys = metricsKey.filter(
+ key => !metrics[key].hidden && !['DATA', 'DISTRIB'].includes(metrics[key].type)
+ );
+ fetchMeasures(component.key, filterdKeys).then(
+ ({ measures, leakPeriod }) => {
if (this.mounted) {
- this.setState({ loading: false, measures, periods });
+ this.setState({
+ loading: false,
+ leakPeriod,
+ measures: measures.filter(measure => measure.value != null || measure.leak != null)
+ });
}
},
() => this.setState({ loading: false })
@@ -110,10 +127,12 @@ export default class App extends React.PureComponent {
};
render() {
- if (this.state.loading) {
+ const isLoading = this.state.loading || this.props.metricsKey.length <= 0;
+ if (isLoading) {
return <i className="spinner spinner-margin" />;
}
const query = parseQuery(this.props.location.query);
+ const metric = this.props.metrics[query.metric];
return (
<div className="layout-page" id="component-measures">
<Helmet title={translate('layout.measures')} />
@@ -132,15 +151,17 @@ export default class App extends React.PureComponent {
</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>
+ {metric != null &&
+ <MeasureContent
+ className="layout-page-main-inner"
+ rootComponent={this.props.component}
+ fetchMeasures={this.props.fetchMeasures}
+ leakPeriod={this.state.leakPeriod}
+ metric={metric}
+ metrics={this.props.metrics}
+ selected={query.selected}
+ updateQuery={this.updateQuery}
+ />}
</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
index 063a5bc2124..f8979c7eea7 100644
--- 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
@@ -22,21 +22,27 @@ 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 {
+ getComponent,
+ getMetrics,
+ getMetricByKey,
+ getMetricsKey
+} 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 { getLeakPeriod } from '../../../helpers/periods';
+import { enhanceMeasure } from '../../../components/measure/utils';
+import type { Component, Period } from '../types';
import type { Measure, MeasureEnhanced } from '../../../components/measure/types';
const mapStateToProps = (state, ownProps) => ({
component: getComponent(state, ownProps.location.query.id),
- metrics: getMetrics(state)
+ metrics: getMetrics(state),
+ metricsKey: getMetricsKey(state)
});
-const banQualityGate = (component: Component, measures: Array<Measure>): Array<Measure> => {
- let newMeasures = [...measures];
+const banQualityGate = (component: Component): Array<Measure> => {
+ let newMeasures = [...component.measures];
if (!['VW', 'SVW', 'APP'].includes(component.qualifier)) {
newMeasures = newMeasures.filter(measure => measure.metric !== 'alert_status');
}
@@ -49,36 +55,23 @@ const banQualityGate = (component: Component, measures: Array<Measure>): Array<M
return newMeasures;
};
-const fetchMeasures = (component: Component, metrics: Metrics) => (
+const fetchMeasures = (component: string, metrics: Array<string>) => (
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([]);
+): Promise<{ component: Component, measures: Array<MeasureEnhanced>, leakPeriod: ?Period }> => {
+ if (metrics.length <= 0) {
+ return Promise.resolve({ component: {}, measures: [], leakPeriod: null });
}
- 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;
- });
+ return getMeasuresAndMeta(component, metrics, { additionalFields: 'periods' }).then(r => {
+ const measures: Array<MeasureEnhanced> = banQualityGate(r.component).map(measure =>
+ enhanceMeasure(measure, getMetricByKey(getState(), measure.metric))
+ );
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 };
+ const periods = r.component.qualifier === 'APP' ? applicationPeriods : r.periods;
+ return { component: r.component, measures, leakPeriod: getLeakPeriod(periods) };
}, throwGlobalError);
};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js b/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js
new file mode 100644
index 00000000000..1d36e84a078
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js
@@ -0,0 +1,61 @@
+/*
+ * 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 moment from 'moment';
+import Tooltip from '../../../components/controls/Tooltip';
+import { getPeriodLabel, getPeriodDate } from '../../../helpers/periods';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import type { Component, Period } from '../types';
+
+export default function LeakPeriodLegend({
+ component,
+ period
+}: {
+ component: Component,
+ period: Period
+}) {
+ if (component.qualifier === 'APP') {
+ return (
+ <div className="domain-measures-leak-header">
+ {translate('issues.leak_period')}
+ </div>
+ );
+ }
+
+ const label = (
+ <div className="domain-measures-leak-header">
+ {translateWithParameters('overview.leak_period_x', getPeriodLabel(period))}
+ </div>
+ );
+
+ if (period.mode === 'days') {
+ return label;
+ }
+
+ const date = getPeriodDate(period);
+ const fromNow = moment(date).fromNow();
+ const tooltip = fromNow + ', ' + moment(date).format('LL');
+ return (
+ <Tooltip placement="left" overlay={tooltip}>
+ {label}
+ </Tooltip>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js
new file mode 100644
index 00000000000..68bc0bb3eac
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js
@@ -0,0 +1,160 @@
+/*
+ * 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 DeferredSpinner from '../../../components/common/DeferredSpinner';
+import ListView from './drilldown/ListView';
+import MeasureHeader from './MeasureHeader';
+import MetricNotFound from './MetricNotFound';
+import type { Component, Period, Query } from '../types';
+import type { MeasureEnhanced } from '../../../components/measure/types';
+import type { Metric } from '../../../store/metrics/actions';
+
+type Props = {
+ className?: string,
+ rootComponent: Component,
+ fetchMeasures: (
+ Component,
+ Array<string>
+ ) => Promise<{ component: Component, measures: Array<MeasureEnhanced> }>,
+ leakPeriod?: Period,
+ metric: Metric,
+ metrics: { [string]: Metric },
+ selected: ?string,
+ updateQuery: Query => void
+};
+
+type State = {
+ component: ?Component,
+ loading: {
+ measure: boolean,
+ components: boolean
+ },
+ measure: ?MeasureEnhanced,
+ secondaryMeasure: ?MeasureEnhanced
+};
+
+export default class MeasureContent extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State = {
+ component: null,
+ loading: {
+ measure: false,
+ components: false
+ },
+ measure: null,
+ secondaryMeasure: null
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchMeasure(this.props);
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ const { component } = this.state;
+ const componentChanged =
+ !component ||
+ nextProps.rootComponent.key !== component.key ||
+ nextProps.selected !== component.key;
+ if (componentChanged || nextProps.metric !== this.props.metric) {
+ this.fetchMeasure(nextProps);
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchMeasure = ({ rootComponent, fetchMeasures, metric, selected }: Props) => {
+ this.updateLoading({ measure: true });
+
+ const metricKeys = [metric.key];
+ if (metric.key === 'ncloc') {
+ metricKeys.push('ncloc_language_distribution');
+ } else if (metric.key === 'function_complexity') {
+ metricKeys.push('function_complexity_distribution');
+ } else if (metric.key === 'file_complexity') {
+ metricKeys.push('file_complexity_distribution');
+ }
+
+ fetchMeasures(selected || rootComponent.key, metricKeys).then(
+ ({ component, measures }) => {
+ if (this.mounted) {
+ const measure = measures.find(measure => measure.metric.key === metric.key);
+ const secondaryMeasure = measures.find(measure => measure.metric.key !== metric.key);
+ this.setState({ component, measure, secondaryMeasure });
+ this.updateLoading({ measure: false });
+ }
+ },
+ () => this.updateLoading({ measure: false })
+ );
+ };
+
+ handleSelect = (component: Component) => this.props.updateQuery({ selected: component.key });
+
+ updateLoading = (loading: { [string]: boolean }) => {
+ if (this.mounted) {
+ this.setState(state => ({ loading: { ...state.loading, ...loading } }));
+ }
+ };
+
+ render() {
+ const { metric } = this.props;
+ const { loading, measure } = this.state;
+
+ return (
+ <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
+ <DeferredSpinner
+ className="pull-right"
+ loading={loading.measure || loading.components}
+ />
+ </div>
+ </div>
+ </div>
+ {metric != null && measure != null
+ ? <div className="layout-page-main-inner">
+ <MeasureHeader
+ component={this.state.component}
+ leakPeriod={this.props.leakPeriod}
+ measure={measure}
+ secondaryMeasure={this.state.secondaryMeasure}
+ />
+ <ListView
+ component={this.state.component}
+ handleSelect={this.handleSelect}
+ leakPeriod={this.props.leakPeriod}
+ loading={loading.components}
+ metric={metric}
+ metrics={this.props.metrics}
+ selectedComponent={this.props.selected}
+ updateLoading={this.updateLoading}
+ />
+ </div>
+ : <MetricNotFound className="layout-page-main-inner" />}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js
new file mode 100644
index 00000000000..c58748cda20
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.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.
+ */
+import React from 'react';
+import { Link } from 'react-router';
+import ComplexityDistribution from '../../../components/shared/ComplexityDistribution';
+import HistoryIcon from '../../../components/icons-components/HistoryIcon';
+import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
+import LanguageDistribution from '../../../components/charts/LanguageDistribution';
+import LeakPeriodLegend from './LeakPeriodLegend';
+import Measure from '../../../components/measure/Measure';
+import Tooltip from '../../../components/controls/Tooltip';
+import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
+import { getComponentMeasureHistory } from '../../../helpers/urls';
+import { isDiffMetric } from '../../../helpers/measures';
+import type { Component, Period } from '../types';
+import type { MeasureEnhanced } from '../../../components/measure/types';
+
+type Props = {
+ component: Component,
+ leakPeriod?: Period,
+ measure: MeasureEnhanced,
+ secondaryMeasure: ?MeasureEnhanced
+};
+
+export default function MeasureHeader({ component, leakPeriod, measure, secondaryMeasure }: Props) {
+ const metric = measure.metric;
+ const isDiff = isDiffMetric(metric.key);
+ return (
+ <div className="measure-details-header big-spacer-bottom">
+ <div className="measure-details-metric">
+ <IssueTypeIcon query={metric.key} className="little-spacer-right text-text-bottom" />
+ {getLocalizedMetricName(metric)}
+ <span className="measure-details-value spacer-left">
+ <strong>
+ {isDiff
+ ? <Measure className="domain-measures-leak" measure={measure} metric={metric} />
+ : <Measure measure={measure} metric={metric} />}
+ </strong>
+ </span>
+ {!isDiff &&
+ <Tooltip placement="right" overlay={translate('component_measures.show_metric_history')}>
+ <Link
+ className="js-show-history spacer-left button button-small button-compact"
+ to={getComponentMeasureHistory(component.key, metric.key)}>
+ <HistoryIcon />
+ </Link>
+ </Tooltip>}
+ {secondaryMeasure &&
+ secondaryMeasure.metric.key === 'ncloc_language_distribution' &&
+ <div className="measure-details-secondary">
+ <LanguageDistribution distribution={secondaryMeasure.value} />
+ </div>}
+
+ {secondaryMeasure &&
+ secondaryMeasure.metric.key === 'function_complexity_distribution' &&
+ <div className="measure-details-secondary">
+ <ComplexityDistribution distribution={secondaryMeasure.value} of="function" />
+ </div>}
+
+ {secondaryMeasure &&
+ secondaryMeasure.metric.key === 'file_complexity_distribution' &&
+ <div className="measure-details-secondary">
+ <ComplexityDistribution distribution={secondaryMeasure.value} of="file" />
+ </div>}
+ </div>
+ {leakPeriod != null && <LeakPeriodLegend component={component} period={leakPeriod} />}
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.js b/server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.js
new file mode 100644
index 00000000000..2dff010b0a0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MetricNotFound.js
@@ -0,0 +1,32 @@
+/*
+ * 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 { translate } from '../../../helpers/l10n';
+
+export default function MetricNotFound({ className }: { className?: string }) {
+ return (
+ <div className={className}>
+ <div className="alert alert-danger">
+ {translate('component_measures.not_found')}
+ </div>
+ </div>
+ );
+}
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
index a17ab39c8f1..7561113bd27 100644
--- 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
@@ -21,24 +21,30 @@ 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' },
- {
+const METRICS = {
+ lines_to_cover: {
+ key: 'lines_to_cover',
+ type: 'INT',
+ name: 'Lines to Cover',
+ domain: 'Coverage'
+ },
+ coverage: { key: 'coverage', type: 'PERCENT', name: 'Coverage', domain: 'Coverage' },
+ duplicated_lines_density: {
key: 'duplicated_lines_density',
type: 'PERCENT',
name: 'Duplicated Lines (%)',
domain: 'Duplications'
},
- { key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' }
-];
+ new_bugs: { key: 'new_bugs', type: 'INT', name: 'New Bugs', domain: 'Reliability' }
+};
const PROPS = {
component: { key: 'foo' },
- location: { pathname: '/component_measures', query: {} },
+ location: { pathname: '/component_measures', query: { metric: 'coverage' } },
fetchMeasures: () => {},
fetchMetrics: () => {},
metrics: METRICS,
+ metricsKey: ['lines_to_cover', 'coverage', 'duplicated_lines_density', 'new_bugs'],
router: { push: () => {} }
};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.js
new file mode 100644
index 00000000000..4f7fce011d7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.js
@@ -0,0 +1,61 @@
+/*
+ * 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 LeakPeriodLegend from '../LeakPeriodLegend';
+
+const PROJECT = {
+ key: 'foo',
+ qualifier: 'TRK'
+};
+
+const APP = {
+ key: 'bar',
+ qualifier: 'APP'
+};
+
+const PERIOD = {
+ date: '2017-05-16T13:50:02+0200',
+ index: 1,
+ mode: 'previous_version',
+ parameter: '6,4'
+};
+
+const PERIOD_DAYS = {
+ date: '2017-05-16T13:50:02+0200',
+ index: 1,
+ mode: 'days',
+ parameter: '18'
+};
+
+jest.mock('moment', () => () => ({
+ format: () => 'March 1, 2017 9:36 AM',
+ fromNow: () => 'a month ago',
+ toDate: () => 'date'
+}));
+
+it('should render correctly', () => {
+ expect(shallow(<LeakPeriodLegend component={PROJECT} period={PERIOD} />)).toMatchSnapshot();
+ expect(shallow(<LeakPeriodLegend component={PROJECT} period={PERIOD_DAYS} />)).toMatchSnapshot();
+});
+
+it('should render correctly for APP', () => {
+ expect(shallow(<LeakPeriodLegend component={APP} period={PERIOD} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js
new file mode 100644
index 00000000000..8914cf0a86e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js
@@ -0,0 +1,78 @@
+/*
+ * 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 MeasureHeader from '../MeasureHeader';
+
+const MEASURE = {
+ value: '3.0',
+ periods: [{ index: 1, value: '0.0' }],
+ metric: {
+ key: 'reliability_rating',
+ type: 'RATING',
+ name: 'Reliability Rating'
+ },
+ leak: '0.0'
+};
+
+const LEAK_MEASURE = {
+ periods: [{ index: 1, value: '3.0' }],
+ metric: {
+ key: 'new_reliability_rating',
+ type: 'RATING',
+ name: 'Reliability Rating on New Code'
+ },
+ leak: '3.0'
+};
+
+const SECONDARY = {
+ value: 'java=175123;js=26382',
+ metric: {
+ key: 'ncloc_language_distribution',
+ type: 'DATA',
+ name: 'Lines of Code Per Language'
+ },
+ leak: null
+};
+
+const PROPS = {
+ component: { key: 'foo' },
+ leakPeriod: {
+ date: '2017-05-16T13:50:02+0200',
+ index: 1,
+ mode: 'previous_version',
+ parameter: '6,4'
+ },
+ measure: MEASURE,
+ secondaryMeasure: null
+};
+
+it('should render correctly', () => {
+ expect(shallow(<MeasureHeader {...PROPS} />)).toMatchSnapshot();
+});
+
+it('should render correctly for leak', () => {
+ expect(shallow(<MeasureHeader {...PROPS} measure={LEAK_MEASURE} />)).toMatchSnapshot();
+});
+
+it('should display secondary measure too', () => {
+ const wrapper = shallow(<MeasureHeader {...PROPS} secondaryMeasure={SECONDARY} />);
+ expect(wrapper.find('LanguageDistribution')).toHaveLength(1);
+});
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
index c74cb34eff9..1eeed263913 100644
--- 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
@@ -28,34 +28,60 @@ exports[`should render correctly 1`] = `
>
<Sidebar
measures={Array []}
- selectedMetric=""
+ selectedMetric="coverage"
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>
+ <MeasureContent
+ className="layout-page-main-inner"
+ fetchMeasures={[Function]}
+ leakPeriod={null}
+ metric={
+ Object {
+ "domain": "Coverage",
+ "key": "coverage",
+ "name": "Coverage",
+ "type": "PERCENT",
+ }
+ }
+ metrics={
+ Object {
+ "coverage": Object {
+ "domain": "Coverage",
+ "key": "coverage",
+ "name": "Coverage",
+ "type": "PERCENT",
+ },
+ "duplicated_lines_density": Object {
+ "domain": "Duplications",
+ "key": "duplicated_lines_density",
+ "name": "Duplicated Lines (%)",
+ "type": "PERCENT",
+ },
+ "lines_to_cover": Object {
+ "domain": "Coverage",
+ "key": "lines_to_cover",
+ "name": "Lines to Cover",
+ "type": "INT",
+ },
+ "new_bugs": Object {
+ "domain": "Reliability",
+ "key": "new_bugs",
+ "name": "New Bugs",
+ "type": "INT",
+ },
+ }
+ }
+ rootComponent={
+ Object {
+ "key": "foo",
+ }
+ }
+ selected=""
+ updateQuery={[Function]}
+ />
</div>
`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap
new file mode 100644
index 00000000000..0f0e328d85e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.js.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Tooltip
+ overlay="a month ago, March 1, 2017 9:36 AM"
+ placement="left"
+>
+ <div
+ className="domain-measures-leak-header"
+ >
+ overview.leak_period_x.overview.period.previous_version.6,4
+ </div>
+</Tooltip>
+`;
+
+exports[`should render correctly 2`] = `
+<div
+ className="domain-measures-leak-header"
+>
+ overview.leak_period_x.overview.period.days.18
+</div>
+`;
+
+exports[`should render correctly for APP 1`] = `
+<div
+ className="domain-measures-leak-header"
+>
+ issues.leak_period
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap
new file mode 100644
index 00000000000..3e66e8fa4fc
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap
@@ -0,0 +1,149 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+ className="measure-details-header big-spacer-bottom"
+>
+ <div
+ className="measure-details-metric"
+ >
+ <IssueTypeIcon
+ className="little-spacer-right text-text-bottom"
+ query="reliability_rating"
+ />
+ Reliability Rating
+ <span
+ className="measure-details-value spacer-left"
+ >
+ <strong>
+ <Measure
+ measure={
+ Object {
+ "leak": "0.0",
+ "metric": Object {
+ "key": "reliability_rating",
+ "name": "Reliability Rating",
+ "type": "RATING",
+ },
+ "periods": Array [
+ Object {
+ "index": 1,
+ "value": "0.0",
+ },
+ ],
+ "value": "3.0",
+ }
+ }
+ metric={
+ Object {
+ "key": "reliability_rating",
+ "name": "Reliability Rating",
+ "type": "RATING",
+ }
+ }
+ />
+ </strong>
+ </span>
+ <Tooltip
+ overlay="component_measures.show_metric_history"
+ placement="right"
+ >
+ <Link
+ className="js-show-history spacer-left button button-small button-compact"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/activity",
+ "query": Object {
+ "custom_metrics": "reliability_rating",
+ "graph": "custom",
+ "id": "foo",
+ },
+ }
+ }
+ >
+ <IconHistory />
+ </Link>
+ </Tooltip>
+ </div>
+ <LeakPeriodLegend
+ component={
+ Object {
+ "key": "foo",
+ }
+ }
+ period={
+ Object {
+ "date": "2017-05-16T13:50:02+0200",
+ "index": 1,
+ "mode": "previous_version",
+ "parameter": "6,4",
+ }
+ }
+ />
+</div>
+`;
+
+exports[`should render correctly for leak 1`] = `
+<div
+ className="measure-details-header big-spacer-bottom"
+>
+ <div
+ className="measure-details-metric"
+ >
+ <IssueTypeIcon
+ className="little-spacer-right text-text-bottom"
+ query="new_reliability_rating"
+ />
+ Reliability Rating on New Code
+ <span
+ className="measure-details-value spacer-left"
+ >
+ <strong>
+ <Measure
+ className="domain-measures-leak"
+ measure={
+ Object {
+ "leak": "3.0",
+ "metric": Object {
+ "key": "new_reliability_rating",
+ "name": "Reliability Rating on New Code",
+ "type": "RATING",
+ },
+ "periods": Array [
+ Object {
+ "index": 1,
+ "value": "3.0",
+ },
+ ],
+ }
+ }
+ metric={
+ Object {
+ "key": "new_reliability_rating",
+ "name": "Reliability Rating on New Code",
+ "type": "RATING",
+ }
+ }
+ />
+ </strong>
+ </span>
+ </div>
+ <LeakPeriodLegend
+ component={
+ Object {
+ "key": "foo",
+ }
+ }
+ period={
+ Object {
+ "date": "2017-05-16T13:50:02+0200",
+ "index": 1,
+ "mode": "previous_version",
+ "parameter": "6,4",
+ }
+ }
+ />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentCell.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentCell.js
new file mode 100644
index 00000000000..a3b54e1955e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentCell.js
@@ -0,0 +1,106 @@
+/*
+ * 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';
+import QualifierIcon from '../../../../components/shared/QualifierIcon';
+import { splitPath } from '../../../../helpers/path';
+import { getComponentUrl } from '../../../../helpers/urls';
+import type { Component } from '../../types';
+
+type Props = {
+ component: Component,
+ isSelected: boolean,
+ onClick: Component => void
+};
+
+export default class ComponentCell extends React.PureComponent {
+ props: Props;
+
+ handleClick = (e: MouseEvent) => {
+ const isLeftClickEvent = e.button === 0;
+ const isModifiedEvent = !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
+
+ if (isLeftClickEvent && !isModifiedEvent) {
+ e.preventDefault();
+ this.props.onClick();
+ }
+ };
+
+ renderInner() {
+ const { component } = this.props;
+ let head = '';
+ let tail = component.name;
+
+ if (['DIR', 'FIL', 'UTS'].includes(component.qualifier)) {
+ const parts = splitPath(component.path);
+ head = parts.head;
+ tail = parts.tail;
+ }
+ return (
+ <span title={component.refKey || component.key}>
+ <QualifierIcon qualifier={component.qualifier} />
+ &nbsp;
+ {head.length > 0 &&
+ <span className="note">
+ {head}/
+ </span>}
+ <span>{tail}</span>
+ </span>
+ );
+ }
+
+ render() {
+ const { component } = this.props;
+ const linkClassName = classNames('link-no-underline', {
+ selected: this.props.isSelected
+ });
+
+ return (
+ <td style={{ maxWidth: 0 }}>
+ <div
+ style={{
+ maxWidth: '100%',
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis'
+ }}>
+ {component.refId == null || component.qualifier === 'DEV_PRJ'
+ ? <a
+ id={'component-measures-component-link-' + component.key}
+ className={linkClassName}
+ href={getComponentUrl(component.key)}
+ onClick={this.handleClick}>
+ {this.renderInner()}
+ </a>
+ : <a
+ id={'component-measures-component-link-' + component.key}
+ className={linkClassName}
+ href={getComponentUrl(component.refKey || component.key)}>
+ <span className="big-spacer-right">
+ <i className="icon-detach" />
+ </span>
+ {this.renderInner()}
+ </a>}
+ </div>
+ </td>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsList.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsList.js
new file mode 100644
index 00000000000..ba8d42d84c9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsList.js
@@ -0,0 +1,84 @@
+/*
+ * 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 ComponentsListRow from './ComponentsListRow';
+import EmptyComponentsList from './EmptyComponentsList';
+import { complementary } from '../../config/complementary';
+import { getLocalizedMetricName } from '../../../../helpers/l10n';
+import type { Component } from '../../types';
+import type { Metric } from '../../../../store/metrics/actions';
+
+type Props = {
+ components: Array<Component>,
+ onClick: Component => void,
+ metric: Metric,
+ metrics: { [string]: Metric },
+ selectedComponent?: ?string
+};
+
+export default function ComponentsList({
+ components,
+ onClick,
+ metrics,
+ metric,
+ selectedComponent
+}: Props) {
+ if (!components.length) {
+ return <EmptyComponentsList />;
+ }
+
+ const otherMetrics = (complementary[metric.key] || []).map(key => metrics[key]);
+ return (
+ <table className="data zebra zebra-hover">
+ {otherMetrics.length > 0 &&
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th className="text-right">
+ <span className="small">
+ {getLocalizedMetricName(metric)}
+ </span>
+ </th>
+ {otherMetrics.map(metric =>
+ <th key={metric.key} className="text-right">
+ <span className="small">
+ {getLocalizedMetricName(metric)}
+ </span>
+ </th>
+ )}
+ </tr>
+ </thead>}
+
+ <tbody>
+ {components.map(component =>
+ <ComponentsListRow
+ key={component.id}
+ component={component}
+ otherMetrics={otherMetrics}
+ isSelected={component.key === selectedComponent}
+ metric={metric}
+ onClick={onClick}
+ />
+ )}
+ </tbody>
+ </table>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsListRow.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsListRow.js
new file mode 100644
index 00000000000..deab1a98453
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsListRow.js
@@ -0,0 +1,70 @@
+/*
+ * 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 ComponentCell from './ComponentCell';
+import MeasureCell from './MeasureCell';
+import type { Component } from '../../types';
+import type { Metric } from '../../../../store/metrics/actions';
+
+type Props = {
+ component: Component,
+ isSelected: boolean,
+ onClick: Component => void,
+ otherMetrics: Array<Metric>,
+ metric: Metric
+};
+
+export default class ComponentsListRow extends React.PureComponent {
+ props: Props;
+
+ handleClick = () => this.props.onClick(this.props.component);
+
+ render() {
+ const { component } = this.props;
+ const otherMeasures = this.props.otherMetrics.map(metric => {
+ const measure = component.measures.find(measure => measure.metric === metric.key);
+ return { ...measure, metric };
+ });
+ return (
+ <tr>
+ <ComponentCell
+ component={component}
+ isSelected={this.props.isSelected}
+ onClick={this.handleClick}
+ />
+
+ <MeasureCell component={component} metric={this.props.metric} />
+
+ {otherMeasures.map(measure =>
+ <MeasureCell
+ key={measure.metric.key}
+ component={{
+ ...component,
+ value: measure.value,
+ leak: measure.leak
+ }}
+ metric={measure.metric}
+ />
+ )}
+ </tr>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/EmptyComponentsList.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/EmptyComponentsList.js
new file mode 100644
index 00000000000..bcdeee0e710
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/EmptyComponentsList.js
@@ -0,0 +1,30 @@
+/*
+ * 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 { translate } from '../../../../helpers/l10n';
+
+export default function EmptyComponentsList() {
+ return (
+ <div className="note">
+ {translate('no_results')}
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js
new file mode 100644
index 00000000000..d567f772759
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js
@@ -0,0 +1,193 @@
+/*
+ * 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 moment from 'moment';
+import ComponentsList from './ComponentsList';
+import ListFooter from '../../../../components/controls/ListFooter';
+import SourceViewer from '../../../../components/SourceViewer/SourceViewer';
+import { getComponentTree } from '../../../../api/components';
+import { complementary } from '../../config/complementary';
+import { isDiffMetric } from '../../../../helpers/measures';
+import { enhanceComponent } from '../../utils';
+import type { Component, ComponentEnhanced, Paging, Period } from '../../types';
+import type { Metric } from '../../../../store/metrics/actions';
+
+type Props = {
+ component: Component,
+ handleSelect: Component => void,
+ leakPeriod?: Period,
+ loading: boolean,
+ metric: Metric,
+ metrics: { [string]: Metric },
+ selectedComponent: ?string,
+ updateLoading: ({ [string]: boolean }) => void
+};
+
+type State = {
+ components: Array<ComponentEnhanced>,
+ metric: ?Metric,
+ paging?: Paging
+};
+
+export default class ListView extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State = {
+ components: [],
+ metric: null,
+ paging: null
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchComponents(this.props);
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ if (nextProps.component !== this.props.component || nextProps.metric !== this.props.metric) {
+ this.fetchComponents(nextProps);
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ getComponentRequestParams = (metric: Metric, options: Object = {}) => {
+ const metricKeys = [metric.key, ...(complementary[metric.key] || [])];
+ let opts: Object = {
+ asc: metric.direction === 1,
+ ps: 100,
+ metricSortFilter: 'withMeasuresOnly',
+ metricSort: metric.key
+ };
+ if (isDiffMetric(metric.key)) {
+ opts = {
+ ...opts,
+ s: 'metricPeriod,name',
+ metricPeriodSort: 1
+ };
+ } else {
+ opts = {
+ ...opts,
+ s: 'metric,name'
+ };
+ }
+ return { metricKeys, opts: { ...opts, ...options } };
+ };
+
+ fetchComponents = ({ component, metric, selectedComponent }: Props) => {
+ if (selectedComponent) {
+ this.setState({ metric });
+ return;
+ }
+ const { metricKeys, opts } = this.getComponentRequestParams(metric);
+ this.props.updateLoading({ components: true });
+ getComponentTree('leaves', component.key, metricKeys, opts).then(
+ r => {
+ if (this.mounted) {
+ this.setState({
+ components: r.components.map(component => enhanceComponent(component, metric)),
+ metric,
+ paging: r.paging
+ });
+ }
+ this.props.updateLoading({ components: false });
+ },
+ () => this.props.updateLoading({ components: false })
+ );
+ };
+
+ fetchMoreComponents = () => {
+ const { component, metric } = this.props;
+ const { paging } = this.state;
+ if (!paging) {
+ return;
+ }
+ const { metricKeys, opts } = this.getComponentRequestParams(metric, {
+ p: paging.pageIndex + 1
+ });
+ this.props.updateLoading({ components: true });
+ getComponentTree('leaves', component.key, metricKeys, opts).then(
+ r => {
+ if (this.mounted) {
+ this.setState(state => ({
+ components: [
+ ...state.components,
+ ...r.components.map(component => enhanceComponent(component, metric))
+ ],
+ metric,
+ paging: r.paging
+ }));
+ }
+ this.props.updateLoading({ components: false });
+ },
+ () => this.props.updateLoading({ components: false })
+ );
+ };
+
+ render() {
+ const { components, metric, paging } = this.state;
+ if (metric == null) {
+ return null;
+ }
+
+ const { leakPeriod, selectedComponent } = this.props;
+ if (selectedComponent) {
+ const leakPeriodDate =
+ isDiffMetric(metric.key) && leakPeriod != null ? moment(leakPeriod.date).toDate() : null;
+
+ let filterLine;
+ if (leakPeriodDate != null) {
+ filterLine = line => {
+ if (line.scmDate) {
+ const scmDate = moment(line.scmDate).toDate();
+ return scmDate >= leakPeriodDate;
+ } else {
+ return false;
+ }
+ };
+ }
+ return (
+ <div className="measure-details-viewer">
+ <SourceViewer component={selectedComponent} filterLine={filterLine} />
+ </div>
+ );
+ }
+
+ return (
+ <div>
+ <ComponentsList
+ components={components}
+ metrics={this.props.metrics}
+ metric={metric}
+ onClick={this.props.handleSelect}
+ />
+ {paging &&
+ <ListFooter
+ count={components.length}
+ total={paging.total}
+ loadMore={this.fetchMoreComponents}
+ />}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/MeasureCell.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/MeasureCell.js
new file mode 100644
index 00000000000..be6348e5105
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/MeasureCell.js
@@ -0,0 +1,39 @@
+/*
+ * 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 type { Component } from '../../types';
+import type { Metric } from '../../../../store/metrics/actions';
+
+type Props = {
+ component: Component,
+ metric: Metric
+};
+
+export default function MeasureCell({ component, metric }: Props) {
+ return (
+ <td className="thin nowrap text-right">
+ <span id={'component-measures-component-measure-' + component.key + '-' + metric.key}>
+ <Measure measure={{ metric, value: component.value, leak: component.leak }} />
+ </span>
+ </td>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js b/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js
new file mode 100644
index 00000000000..3fd1e2a2055
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js
@@ -0,0 +1,27 @@
+/*
+ * 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 bubbles = {
+ Reliability: { x: 'ncloc', y: 'reliability_remediation_effort', size: 'bugs' },
+ Security: { x: 'ncloc', y: 'security_remediation_effort', size: 'vulnerabilities' },
+ Maintainability: { x: 'ncloc', y: 'sqale_index', size: 'code_smells' },
+ Coverage: { x: 'complexity', y: 'coverage', size: 'uncovered_lines' },
+ Duplications: { x: 'ncloc', y: 'duplicated_lines', size: 'duplicated_blocks' }
+};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/config/complementary.js b/server/sonar-web/src/main/js/apps/component-measures/config/complementary.js
new file mode 100644
index 00000000000..e50c2238aac
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/config/complementary.js
@@ -0,0 +1,38 @@
+/*
+ * 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 complementary = {
+ coverage: ['uncovered_lines', 'uncovered_conditions'],
+ line_coverage: ['uncovered_lines'],
+ branch_coverage: ['uncovered_conditions'],
+ uncovered_lines: ['line_coverage'],
+ uncovered_conditions: ['branch_coverage'],
+
+ new_coverage: ['new_uncovered_lines', 'new_uncovered_conditions'],
+ new_line_coverage: ['new_uncovered_lines'],
+ new_branch_coverage: ['new_uncovered_conditions'],
+ new_uncovered_lines: ['new_line_coverage'],
+ new_uncovered_conditions: ['new_branch_coverage'],
+
+ duplicated_lines_density: ['duplicated_lines'],
+ new_duplicated_lines_density: ['new_duplicated_lines'],
+ duplicated_lines: ['duplicated_lines_density'],
+ new_duplicated_lines: ['new_duplicated_lines_density']
+};
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
index fc294c55dee..1e3aa63a269 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/style.css
+++ b/server/sonar-web/src/main/js/apps/component-measures/style.css
@@ -1,19 +1,45 @@
-.measures-domains-leak-header {
- float: right;
+.domain-measures-leak {
background-color: #fbf3d5;
border: 1px solid #eae3c7;
- padding: 4px 10px;
- white-space: nowrap;
+ padding: 4px 6px;
}
-.domain-measures-leak {
- background-color: #fbf3d5;
- border: 1px solid #eae3c7;
+.facet .domain-measures-leak {
padding: 4px 4px;
margin: -5px -5px;
}
-.domain-measures-value .rating {
+.domain-measures-leak-header {
+ background-color: #fbf3d5;
+ border: 1px solid #eae3c7;
+ padding: 4px 10px;
+ white-space: nowrap;
+}
+
+.measure-details-header {
+ display: flex;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.measure-details-metric {
+ display: flex;
+ align-items: center;
+}
+
+.measure-details-secondary {
+ display: inline-block;
+ width: 260px;
+ margin-left: 4px;
+}
+
+.measure-details-secondary .bar-chart {
+ margin-top: -10px;
+}
+
+.domain-measures-value .rating,
+.measure-details-value .rating {
width: 18px;
height: 18px;
line-height: 18px;
@@ -21,8 +47,3 @@
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
index 501e60ed0c1..340baf37de1 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/types.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/types.js
@@ -17,7 +17,9 @@
* 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 = {
+import type { Measure, MeasureEnhanced } from '../../components/measure/types';
+
+type ComponentIntern = {
isFavorite?: boolean,
isRecentlyBrowsed?: boolean,
key: string,
@@ -28,9 +30,18 @@ export type Component = {
qualifier: string
};
-export type Query = {
- metric: ?string,
- view: string
+export type Component = ComponentIntern & { measures?: Array<Measure> };
+
+export type ComponentEnhanced = ComponentIntern & {
+ value?: ?string,
+ leak?: ?string,
+ measures: Array<MeasureEnhanced>
+};
+
+export type Paging = {
+ pageIndex: number,
+ pageSize: number,
+ total: number
};
export type Period = {
@@ -39,3 +50,9 @@ export type Period = {
mode: string,
parameter?: string
};
+
+export type Query = {
+ metric: ?string,
+ selected: ?string,
+ view: 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
index 2622792dcda..f6fcfdabc3d 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/utils.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/utils.js
@@ -22,9 +22,12 @@ 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 { bubbles } from './config/bubbles';
+import { enhanceMeasure } from '../../components/measure/utils';
+import type { Component, ComponentEnhanced, Query } from './types';
import type { RawQuery } from '../../helpers/query';
-import type { Measure, MeasureEnhanced } from '../../components/measure/types';
+import type { Metric } from '../../store/metrics/actions';
+import type { MeasureEnhanced } from '../../components/measure/types';
export const DEFAULT_VIEW = 'list';
const KNOWN_DOMAINS = [
@@ -69,13 +72,13 @@ export function sortMeasures(
]);
}
-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 enhanceComponent = (component: Component, metric: Metric): ComponentEnhanced => {
+ const enhancedMeasures = component.measures.map(measure => enhanceMeasure(measure, metric));
+ const measure = enhancedMeasures.find(measure => measure.metric.key === metric.key);
+ const value = measure ? measure.value : null;
+ const leak = measure ? measure.leak : null;
+ return { ...component, value, leak, measures: enhancedMeasures };
+};
export const groupByDomains = memoize((measures: Array<MeasureEnhanced>): Array<{
name: string,
@@ -96,14 +99,18 @@ export const groupByDomains = memoize((measures: Array<MeasureEnhanced>): Array<
]);
});
+export const hasBubbleChart = (domainName: string): boolean => bubbles[domainName] != null;
+
export const parseQuery = memoize((urlQuery: RawQuery): Query => ({
metric: parseAsString(urlQuery['metric']),
+ selected: parseAsString(urlQuery['selected']),
view: parseAsString(urlQuery['view']) || DEFAULT_VIEW
}));
export const serializeQuery = memoize((query: Query): RawQuery => {
return cleanQuery({
metric: serializeString(query.metric),
+ selected: serializeString(query.selected),
view: query.view === DEFAULT_VIEW ? null : serializeString(query.view)
});
});
diff --git a/server/sonar-web/src/main/js/components/measure/utils.js b/server/sonar-web/src/main/js/components/measure/utils.js
index bf98a58ee7b..74cc616d680 100644
--- a/server/sonar-web/src/main/js/components/measure/utils.js
+++ b/server/sonar-web/src/main/js/components/measure/utils.js
@@ -24,10 +24,18 @@ import {
getRatingTooltip as nextGetRatingTooltip,
isDiffMetric
} from '../../helpers/measures';
+import type { Measure, MeasureEnhanced } from './types';
import type { Metric } from '../../store/metrics/actions';
const KNOWN_RATINGS = ['sqale_rating', 'reliability_rating', 'security_rating'];
+export const enhanceMeasure = (measure: Measure, metric: Metric): MeasureEnhanced => ({
+ value: measure.value,
+ periods: measure.periods,
+ metric,
+ leak: getLeakValue(measure)
+});
+
export function formatLeak(value: ?string, metric: Metric, options: Object) {
if (isDiffMetric(metric.key)) {
return formatMeasure(value, metric.type, options);
@@ -36,6 +44,14 @@ export function formatLeak(value: ?string, metric: Metric, options: Object) {
}
}
+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 function getRatingTooltip(metricKey: string, value: ?string) {
const finalMetricKey = isDiffMetric(metricKey) ? metricKey.substr(4) : metricKey;
if (KNOWN_RATINGS.includes(finalMetricKey)) {
diff --git a/server/sonar-web/src/main/js/components/shared/complexity-distribution.js b/server/sonar-web/src/main/js/components/shared/ComplexityDistribution.js
index 7853ee91800..0dd2ccc4514 100644
--- a/server/sonar-web/src/main/js/components/shared/complexity-distribution.js
+++ b/server/sonar-web/src/main/js/components/shared/ComplexityDistribution.js
@@ -25,7 +25,7 @@ import { translateWithParameters } from '../../helpers/l10n';
const HEIGHT = 80;
-export class ComplexityDistribution extends React.PureComponent {
+export default class ComplexityDistribution extends React.PureComponent {
static propTypes = {
distribution: PropTypes.string.isRequired,
of: PropTypes.string.isRequired
@@ -61,11 +61,8 @@ export class ComplexityDistribution extends React.PureComponent {
};
render() {
- // TODO remove inline styling
return (
- <div
- className="overview-bar-chart"
- style={{ height: HEIGHT, paddingTop: 10, paddingBottom: 15 }}>
+ <div className="overview-bar-chart" style={{ height: HEIGHT }}>
{this.renderBarChart()}
</div>
);
diff --git a/server/sonar-web/src/main/js/store/metrics/actions.js b/server/sonar-web/src/main/js/store/metrics/actions.js
index d76e54233fb..c144c2e1527 100644
--- a/server/sonar-web/src/main/js/store/metrics/actions.js
+++ b/server/sonar-web/src/main/js/store/metrics/actions.js
@@ -32,11 +32,9 @@ export type Metric = {
type: string
};
-export type Metrics = Array<Metric>;
-
export const RECEIVE_METRICS = 'RECEIVE_METRICS';
-export const receiveMetrics = (metrics: Metrics) => ({
+export const receiveMetrics = (metrics: Array<Metric>) => ({
type: RECEIVE_METRICS,
metrics
});
diff --git a/server/sonar-web/src/main/js/store/metrics/reducer.js b/server/sonar-web/src/main/js/store/metrics/reducer.js
index 0c0950c6864..edc8750186c 100644
--- a/server/sonar-web/src/main/js/store/metrics/reducer.js
+++ b/server/sonar-web/src/main/js/store/metrics/reducer.js
@@ -44,6 +44,6 @@ const keys = (state: StateKeys = [], action = {}) => {
export default combineReducers({ byKey, keys });
-export const getMetrics = (state: State) => state.keys;
-
+export const getMetrics = (state: State) => state.byKey;
export const getMetricByKey = (state: State, key: string) => state.byKey[key];
+export const getMetricsKey = (state: State) => state.keys;
diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js
index e38f544dabf..7da4acd3dfc 100644
--- a/server/sonar-web/src/main/js/store/rootReducer.js
+++ b/server/sonar-web/src/main/js/store/rootReducer.js
@@ -93,6 +93,8 @@ export const getMetrics = state => fromMetrics.getMetrics(state.metrics);
export const getMetricByKey = (state, key) => fromMetrics.getMetricByKey(state.metrics, key);
+export const getMetricsKey = state => fromMetrics.getMetricsKey(state.metrics);
+
export const getGlobalNotifications = state => fromNotifications.getGlobal(state.notifications);
export const getProjectsWithNotifications = state =>