From 3d7ad29b570e76695fd4b53ba6d1ad127e5c92f3 Mon Sep 17 00:00:00 2001 From: Grégoire Aubert Date: Fri, 4 Aug 2017 11:27:20 +0200 Subject: SONAR-9608 SONAR-9637 Add the overview bubble charts on the measures page --- .../js/apps/component-measures/components/App.js | 27 ++- .../components/LeakPeriodLegend.js | 16 +- .../components/MeasureContentContainer.js | 4 +- .../components/MeasureOverview.js | 181 +++++++++++++++++++++ .../components/MeasureOverviewContainer.js | 126 ++++++++++++++ .../component-measures/components/PageActions.js | 2 +- .../components/__tests__/App-test.js | 12 ++ .../component-measures/drilldown/BubbleChart.js | 151 +++++++++++++++++ .../component-measures/drilldown/ComponentsList.js | 4 +- .../drilldown/EmptyComponentsList.js | 30 ---- .../component-measures/drilldown/EmptyResult.js | 30 ++++ .../component-measures/drilldown/TreeMapView.js | 4 +- .../src/main/js/apps/component-measures/style.css | 40 +++++ .../src/main/js/apps/component-measures/utils.js | 5 +- 14 files changed, 579 insertions(+), 53 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.js delete mode 100644 server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyComponentsList.js create mode 100644 server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyResult.js (limited to 'server') 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 773235ee8ca..2a4e2ad6b62 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 @@ -21,8 +21,9 @@ import React from 'react'; import Helmet from 'react-helmet'; import MeasureContentContainer from './MeasureContentContainer'; +import MeasureOverviewContainer from './MeasureOverviewContainer'; import Sidebar from '../sidebar/Sidebar'; -import { parseQuery, serializeQuery } from '../utils'; +import { hasBubbleChart, parseQuery, serializeQuery } from '../utils'; import { translate } from '../../../helpers/l10n'; import type { Component, Query, Period } from '../types'; import type { RawQuery } from '../../../helpers/query'; @@ -132,8 +133,10 @@ export default class App extends React.PureComponent { if (isLoading) { return ; } + const { component, fetchMeasures, metrics } = this.props; + const { leakPeriod } = this.state; const query = parseQuery(this.props.location.query); - const metric = this.props.metrics[query.metric]; + const metric = metrics[query.metric]; return (
@@ -156,15 +159,27 @@ export default class App extends React.PureComponent { } + {metric == null && + hasBubbleChart(query.metric) && + }
); } 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 index 1d36e84a078..dbf0ae7ffd0 100644 --- 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 @@ -19,29 +19,31 @@ */ // @flow import React from 'react'; +import classNames from 'classnames'; 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 -}: { +type Props = { + className?: string, component: Component, period: Period -}) { +}; + +export default function LeakPeriodLegend({ className, component, period }: Props) { + const leakClass = classNames('domain-measures-leak-header', className); if (component.qualifier === 'APP') { return ( -
+
{translate('issues.leak_period')}
); } const label = ( -
+
{translateWithParameters('overview.leak_period_x', getPeriodLabel(period))}
); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js index 08378cccae7..f9948e1028d 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js @@ -24,7 +24,7 @@ import type { Component, Period, Query } from '../types'; import type { MeasureEnhanced } from '../../../components/measure/types'; import type { Metric } from '../../../store/metrics/actions'; -type Props = { +type Props = {| className?: string, currentUser: { isLoggedIn: boolean }, rootComponent: Component, @@ -38,7 +38,7 @@ type Props = { selected: ?string, updateQuery: Query => void, view: string -}; +|}; type State = { component: ?Component, diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js new file mode 100644 index 00000000000..cab6f21fc1a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js @@ -0,0 +1,181 @@ +/* + * 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 Breadcrumbs from './Breadcrumbs'; +import BubbleChart from '../drilldown/BubbleChart'; +import Favorite from '../../../components/controls/Favorite'; +import LeakPeriodLegend from './LeakPeriodLegend'; +import PageActions from './PageActions'; +import SourceViewer from '../../../components/SourceViewer/SourceViewer'; +import { getComponentLeaves } from '../../../api/components'; +import { enhanceComponent, isFileType } from '../utils'; +import { bubbles } from '../config/bubbles'; +import type { Component, ComponentEnhanced, Paging, Period } from '../types'; +import type { Metric } from '../../../store/metrics/actions'; + +type Props = {| + className?: string, + component: Component, + currentUser: { isLoggedIn: boolean }, + domain: string, + leakPeriod: Period, + loading: boolean, + metrics: { [string]: Metric }, + rootComponent: Component, + updateLoading: ({ [string]: boolean }) => void, + updateSelected: string => void +|}; + +type State = { + components: Array, + paging?: Paging +}; + +const BUBBLES_LIMIT = 500; + +export default class MeasureOverview extends React.PureComponent { + mounted: boolean; + props: Props; + state: State = { + components: [], + paging: null + }; + + componentDidMount() { + this.mounted = true; + this.fetchComponents(this.props); + } + + componentWillReceiveProps(nextProps: Props) { + if ( + nextProps.component !== this.props.component || + nextProps.metrics !== this.props.metrics || + nextProps.domain !== this.props.domain + ) { + this.fetchComponents(nextProps); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + getBubbleMetrics = ({ domain, metrics }: Props) => { + const conf = bubbles[domain]; + return { + xMetric: metrics[conf.x], + yMetric: metrics[conf.y], + sizeMetric: metrics[conf.size] + }; + }; + + fetchComponents = (props: Props) => { + const { component, metrics } = props; + if (isFileType(component)) { + return this.setState({ components: [], paging: null }); + } + const { xMetric, yMetric, sizeMetric } = this.getBubbleMetrics(props); + const metricsKey = [xMetric.key, yMetric.key, sizeMetric.key]; + const options = { + s: 'metric', + metricSort: sizeMetric.key, + asc: false, + ps: BUBBLES_LIMIT + }; + + this.props.updateLoading({ bubbles: true }); + getComponentLeaves(component.key, metricsKey, options).then( + r => { + if (this.mounted) { + this.setState({ + components: r.components.map(component => enhanceComponent(component, null, metrics)), + paging: r.paging + }); + this.props.updateLoading({ bubbles: false }); + } + }, + () => this.props.updateLoading({ bubbles: false }) + ); + }; + + renderContent() { + const { component } = this.props; + if (isFileType(component)) { + return ( +
+ +
+ ); + } + + return ( + + ); + } + + render() { + const { component, currentUser, leakPeriod, rootComponent } = this.props; + const isLoggedIn = currentUser && currentUser.isLoggedIn; + const isFile = isFileType(component); + return ( +
+
+
+
+ + {component.key !== rootComponent.key && + isLoggedIn && + } + +
+
+
+
+
+ {leakPeriod != null && + } +
+ {!this.props.loading && this.renderContent()} +
+
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js new file mode 100644 index 00000000000..b674dbc9e8a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js @@ -0,0 +1,126 @@ +/* + * 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 MeasureOverview from './MeasureOverview'; +import { getComponentShow } from '../../../api/components'; +import type { Component, Period, Query } from '../types'; +import type { Metric } from '../../../store/metrics/actions'; + +type Props = {| + className?: string, + rootComponent: Component, + currentUser: { isLoggedIn: boolean }, + domain: string, + leakPeriod: Period, + metrics: { [string]: Metric }, + selected: ?string, + updateQuery: Query => void +|}; + +type State = { + component: ?Component, + loading: { + component: boolean, + bubbles: boolean + } +}; + +export default class MeasureOverviewContainer extends React.PureComponent { + mounted: boolean; + props: Props; + state: State = { + component: null, + loading: { + component: false, + bubbles: false + } + }; + + componentDidMount() { + this.mounted = true; + this.fetchComponent(this.props); + } + + componentWillReceiveProps(nextProps: Props) { + const { component } = this.state; + const componentChanged = + !component || + nextProps.rootComponent.key !== component.key || + nextProps.selected !== component.key; + if (componentChanged || nextProps.domain !== this.props.domain) { + this.fetchComponent(nextProps); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchComponent = ({ rootComponent, selected }: Props) => { + if (!selected || rootComponent.key === selected) { + this.setState({ component: rootComponent }); + this.updateLoading({ component: false }); + return; + } + this.updateLoading({ component: true }); + getComponentShow(selected).then( + ({ component }) => { + if (this.mounted) { + this.setState({ component }); + this.updateLoading({ component: false }); + } + }, + () => this.updateLoading({ component: false }) + ); + }; + + updateLoading = (loading: { [string]: boolean }) => { + if (this.mounted) { + this.setState(state => ({ loading: { ...state.loading, ...loading } })); + } + }; + + updateSelected = (component: string) => + this.props.updateQuery({ + selected: component !== this.props.rootComponent.key ? component : null + }); + + render() { + if (!this.state.component) { + return null; + } + + return ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js b/server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js index 297d351d79d..3ac96ea7a8b 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js @@ -29,7 +29,7 @@ type Props = {| loading: boolean, isFile: ?boolean, paging: ?Paging, - view: string + view?: string |}; export default class PageActions extends React.PureComponent { 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 7561113bd27..7df3340638f 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 @@ -54,3 +54,15 @@ it('should render correctly', () => { wrapper.setState({ loading: false }); expect(wrapper).toMatchSnapshot(); }); + +it('should render a measure overview', () => { + const wrapper = shallow( + + ); + expect(wrapper.find('.spinner')).toHaveLength(1); + wrapper.setState({ loading: false }); + expect(wrapper.find('MeasureOverviewContainer')).toHaveLength(1); +}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.js new file mode 100644 index 00000000000..495a3fc3158 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.js @@ -0,0 +1,151 @@ +/* + * 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 EmptyResult from './EmptyResult'; +import OriginalBubbleChart from '../../../components/charts/BubbleChart'; +import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; +import { + getLocalizedMetricDomain, + getLocalizedMetricName, + translateWithParameters +} from '../../../helpers/l10n'; +import { bubbles } from '../config/bubbles'; +import type { Component, ComponentEnhanced } from '../types'; +import type { Metric } from '../../../store/metrics/actions'; + +const HEIGHT = 500; + +type Props = {| + component: Component, + components: Array, + domain: string, + metrics: { [string]: Metric }, + updateSelected: string => void +|}; + +export default class BubbleChart extends React.PureComponent { + props: Props; + + getBubbleMetrics = ({ domain, metrics }: Props) => { + const conf = bubbles[domain]; + return { + xMetric: metrics[conf.x], + yMetric: metrics[conf.y], + sizeMetric: metrics[conf.size] + }; + }; + + getMeasureVal = (component: ComponentEnhanced, metric: Metric) => { + const measure = component.measures.find(measure => measure.metric.key === metric.key); + if (measure) { + return Number(isDiffMetric(metric.key) ? measure.leak : measure.value); + } + }; + + getTooltip( + componentName: string, + x: number, + y: number, + size: number, + xMetric: Metric, + yMetric: Metric, + sizeMetric: Metric + ) { + const inner = [ + componentName, + `${xMetric.name}: ${formatMeasure(x, xMetric.type)}`, + `${yMetric.name}: ${formatMeasure(y, yMetric.type)}`, + `${sizeMetric.name}: ${formatMeasure(size, sizeMetric.type)}` + ].join('
'); + return `
${inner}
`; + } + + handleBubbleClick = (component: ComponentEnhanced) => this.props.updateSelected(component.key); + + renderBubbleChart(xMetric: Metric, yMetric: Metric, sizeMetric: Metric) { + const items = this.props.components + .map(component => { + const x = this.getMeasureVal(component, xMetric); + const y = this.getMeasureVal(component, yMetric); + const size = this.getMeasureVal(component, sizeMetric); + if ((!x && x !== 0) || (!y && y !== 0) || (!size && size !== 0)) { + return null; + } + return { + x, + y, + size, + link: component, + tooltip: this.getTooltip(component.name, x, y, size, xMetric, yMetric, sizeMetric) + }; + }) + .filter(Boolean); + + const formatXTick = tick => formatMeasure(tick, xMetric.type); + const formatYTick = tick => formatMeasure(tick, yMetric.type); + + return ( + + ); + } + + render() { + if (this.props.components.length <= 0) { + return ; + } + + const { xMetric, yMetric, sizeMetric } = this.getBubbleMetrics(this.props); + return ( +
+
+ + {translateWithParameters( + 'component_measures.domain_x_overview', + getLocalizedMetricDomain(this.props.domain) + )} + + + {translateWithParameters( + 'component_measures.legend.size_x', + getLocalizedMetricName(sizeMetric) + )} + +
+
+ {this.renderBubbleChart(xMetric, yMetric, sizeMetric)} +
+
+ {getLocalizedMetricName(xMetric)} +
+
+ {getLocalizedMetricName(yMetric)} +
+
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js index ccc417f5d30..45be86a7f1d 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js @@ -20,7 +20,7 @@ // @flow import React from 'react'; import ComponentsListRow from './ComponentsListRow'; -import EmptyComponentsList from './EmptyComponentsList'; +import EmptyResult from './EmptyResult'; import { complementary } from '../config/complementary'; import { getLocalizedMetricName } from '../../../helpers/l10n'; import type { Component } from '../types'; @@ -42,7 +42,7 @@ export default function ComponentsList({ selectedComponent }: Props) { if (!components.length) { - return ; + return ; } const otherMetrics = (complementary[metric.key] || []).map(key => metrics[key]); diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyComponentsList.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyComponentsList.js deleted file mode 100644 index 01f19c0bff5..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyComponentsList.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 ( -
- {translate('no_results')} -
- ); -} diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyResult.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyResult.js new file mode 100644 index 00000000000..3b237ad7dd8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyResult.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 EmptyResult() { + return ( +
+ {translate('no_results')} +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js index 529e95d25c4..5555bb37041 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js @@ -64,7 +64,6 @@ export default class TreeMapView extends React.PureComponent { const colorMeasure = component.measures.find(measure => measure.metric.key === metric.key); const sizeMeasure = component.measures.find(measure => measure.metric.key !== metric.key); if (colorMeasure == null || sizeMeasure == null) { - // $FlowFixMe Null values are filtered just after return null; } const colorValue = isDiffMetric(colorMeasure.metric.key) @@ -74,7 +73,6 @@ export default class TreeMapView extends React.PureComponent { ? sizeMeasure.leak : sizeMeasure.value; if (sizeValue == null) { - // $FlowFixMe Null values are filtered just after return null; } return { @@ -93,7 +91,7 @@ export default class TreeMapView extends React.PureComponent { link: getComponentUrl(component.key) }; }) - .filter(component => component != null); + .filter(Boolean); }; getLevelColorScale = () => 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 4d41d32f2f3..72834db9921 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 @@ -93,3 +93,43 @@ .measure-favorite svg { vertical-align: middle; } + +.measure-details-bubble-chart { + position: relative; + padding: 0 0 30px 60px; + border: 1px solid #e6e6e6; + background-color: #fff; +} + +.measure-details-bubble-chart-header { + padding: 16px; + margin-left: -60px; + border-bottom: 1px solid #e6e6e6; +} + +.measure-details-bubble-chart-legend { + position: absolute; + width: 100%; + left: 0; + text-align: center; +} + +.measure-details-bubble-chart-axis { + position: absolute; + color: #777; + font-size: 12px; +} + +.measure-details-bubble-chart-axis.x { + left: 50%; + bottom: 10px; + width: 500px; + margin-left: -250px; + text-align: center; +} + +.measure-details-bubble-chart-axis.y { + top: 50%; + left: -20px; + transform: rotate(-90deg); +} 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 5f00fa61bf7..8d672b97def 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 @@ -74,11 +74,12 @@ export function sortMeasures( export const enhanceComponent = ( component: Component, - metric: Metric, + metric: ?Metric, metrics: { [string]: Metric } ): ComponentEnhanced => { const enhancedMeasures = component.measures.map(measure => enhanceMeasure(measure, metrics)); - const measure = enhancedMeasures.find(measure => measure.metric.key === metric.key); + // $FlowFixMe metric can't be null since there is a guard for it + const measure = metric && 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 }; -- cgit v1.2.3