aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-08-04 11:27:20 +0200
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-08-14 11:44:44 +0200
commit3d7ad29b570e76695fd4b53ba6d1ad127e5c92f3 (patch)
treedb3b14fa3a6107698f76a8125db9731a2d5075da /server
parente26ecd2e39df60a3ac0634a8b797a5ebec218b4c (diff)
downloadsonarqube-3d7ad29b570e76695fd4b53ba6d1ad127e5c92f3.tar.gz
sonarqube-3d7ad29b570e76695fd4b53ba6d1ad127e5c92f3.zip
SONAR-9608 SONAR-9637 Add the overview bubble charts on the measures page
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/App.js27
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.js16
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js181
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js126
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/App-test.js12
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.js151
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyResult.js (renamed from server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyComponentsList.js)2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/style.css40
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/utils.js5
13 files changed, 550 insertions, 24 deletions
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 <i className="spinner spinner-margin" />;
}
+ 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 (
<div className="layout-page" id="component-measures">
<Helmet title={translate('layout.measures')} />
@@ -156,15 +159,27 @@ export default class App extends React.PureComponent {
<MeasureContentContainer
className="layout-page-main"
currentUser={this.props.currentUser}
- rootComponent={this.props.component}
- fetchMeasures={this.props.fetchMeasures}
- leakPeriod={this.state.leakPeriod}
+ rootComponent={component}
+ fetchMeasures={fetchMeasures}
+ leakPeriod={leakPeriod}
metric={metric}
- metrics={this.props.metrics}
+ metrics={metrics}
selected={query.selected}
updateQuery={this.updateQuery}
view={query.view}
/>}
+ {metric == null &&
+ hasBubbleChart(query.metric) &&
+ <MeasureOverviewContainer
+ className="layout-page-main"
+ rootComponent={component}
+ currentUser={this.props.currentUser}
+ domain={query.metric}
+ leakPeriod={leakPeriod}
+ metrics={metrics}
+ selected={query.selected}
+ updateQuery={this.updateQuery}
+ />}
</div>
);
}
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 (
- <div className="domain-measures-leak-header">
+ <div className={leakClass}>
{translate('issues.leak_period')}
</div>
);
}
const label = (
- <div className="domain-measures-leak-header">
+ <div className={leakClass}>
{translateWithParameters('overview.leak_period_x', getPeriodLabel(period))}
</div>
);
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<ComponentEnhanced>,
+ 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 (
+ <div className="measure-details-viewer">
+ <SourceViewer component={component.key} />
+ </div>
+ );
+ }
+
+ return (
+ <BubbleChart
+ component={this.props.component}
+ components={this.state.components}
+ domain={this.props.domain}
+ metrics={this.props.metrics}
+ updateSelected={this.props.updateSelected}
+ />
+ );
+ }
+
+ render() {
+ const { component, currentUser, leakPeriod, rootComponent } = this.props;
+ const isLoggedIn = currentUser && currentUser.isLoggedIn;
+ const isFile = isFileType(component);
+ return (
+ <div className={this.props.className}>
+ <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 clearfix">
+ <Breadcrumbs
+ className="measure-breadcrumbs spacer-right text-ellipsis"
+ component={component}
+ handleSelect={this.props.updateSelected}
+ rootComponent={rootComponent}
+ />
+ {component.key !== rootComponent.key &&
+ isLoggedIn &&
+ <Favorite
+ favorite={component.isFavorite === true}
+ component={component.key}
+ className="measure-favorite spacer-right"
+ />}
+ <PageActions
+ current={this.state.components.length}
+ loading={this.props.loading}
+ isFile={isFile}
+ paging={this.state.paging}
+ />
+ </div>
+ </div>
+ </div>
+ <div className="layout-page-main-inner">
+ <div className="clearfix big-spacer-bottom">
+ {leakPeriod != null &&
+ <LeakPeriodLegend className="pull-right" component={component} period={leakPeriod} />}
+ </div>
+ {!this.props.loading && this.renderContent()}
+ </div>
+ </div>
+ );
+ }
+}
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 (
+ <MeasureOverview
+ className={this.props.className}
+ component={this.state.component}
+ currentUser={this.props.currentUser}
+ domain={this.props.domain}
+ loading={this.state.loading.component || this.state.loading.bubbles}
+ leakPeriod={this.props.leakPeriod}
+ metrics={this.props.metrics}
+ rootComponent={this.props.rootComponent}
+ updateLoading={this.updateLoading}
+ updateSelected={this.updateSelected}
+ />
+ );
+ }
+}
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(
+ <App
+ {...PROPS}
+ location={{ pathname: '/component_measures', query: { metric: 'Reliability' } }}
+ />
+ );
+ 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<ComponentEnhanced>,
+ 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('<br>');
+ return `<div class="text-left">${inner}</div>`;
+ }
+
+ 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 (
+ <OriginalBubbleChart
+ items={items}
+ height={HEIGHT}
+ padding={[25, 60, 50, 60]}
+ formatXTick={formatXTick}
+ formatYTick={formatYTick}
+ onBubbleClick={this.handleBubbleClick}
+ />
+ );
+ }
+
+ render() {
+ if (this.props.components.length <= 0) {
+ return <EmptyResult />;
+ }
+
+ const { xMetric, yMetric, sizeMetric } = this.getBubbleMetrics(this.props);
+ return (
+ <div className="measure-details-bubble-chart">
+ <div className="measure-details-bubble-chart-header">
+ <span>
+ {translateWithParameters(
+ 'component_measures.domain_x_overview',
+ getLocalizedMetricDomain(this.props.domain)
+ )}
+ </span>
+ <span className="measure-details-bubble-chart-legend">
+ {translateWithParameters(
+ 'component_measures.legend.size_x',
+ getLocalizedMetricName(sizeMetric)
+ )}
+ </span>
+ </div>
+ <div>
+ {this.renderBubbleChart(xMetric, yMetric, sizeMetric)}
+ </div>
+ <div className="measure-details-bubble-chart-axis x">
+ {getLocalizedMetricName(xMetric)}
+ </div>
+ <div className="measure-details-bubble-chart-axis y">
+ {getLocalizedMetricName(yMetric)}
+ </div>
+ </div>
+ );
+ }
+}
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 <EmptyComponentsList />;
+ return <EmptyResult />;
}
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/EmptyResult.js
index 01f19c0bff5..3b237ad7dd8 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyComponentsList.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/EmptyResult.js
@@ -21,7 +21,7 @@
import React from 'react';
import { translate } from '../../../helpers/l10n';
-export default function EmptyComponentsList() {
+export default function EmptyResult() {
return (
<div className="note">
{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 };