From bb393dd277ab06c8451513a6339171e800fc9944 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Fri, 4 Aug 2017 17:17:21 +0200 Subject: [PATCH] SONAR-9608 SONAR-9611 Create the project overview bubble chart on the measures page --- .../__tests__/utils-test.js | 6 +- .../components/MeasureOverview.js | 20 ++- .../apps/component-measures/config/bubbles.js | 29 ++++- .../drilldown/BubbleChart.js | 122 +++++++++++++----- .../sidebar/ProjectOverviewFacet.js | 61 +++++++++ .../component-measures/sidebar/Sidebar.js | 8 +- .../__snapshots__/Sidebar-test.js.snap | 5 + .../main/js/apps/component-measures/style.css | 12 +- .../main/js/apps/component-measures/utils.js | 18 ++- .../src/main/js/apps/projects/styles.css | 25 ---- .../js/apps/projects/visualizations/Risk.js | 4 +- .../visualizations/SimpleBubbleChart.js | 4 +- .../charts/ColorRatingsLegend.js} | 13 +- .../src/main/less/components/graphics.less | 24 ++++ .../resources/org/sonar/l10n/core.properties | 4 + 15 files changed, 262 insertions(+), 93 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.js rename server/sonar-web/src/main/js/{apps/projects/visualizations/RatingsLegend.js => components/charts/ColorRatingsLegend.js} (75%) 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 00be69c8786..bd275efccc8 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,7 +92,11 @@ describe('groupByDomains', () => { describe('parseQuery', () => { it('should correctly parse the url query', () => { - expect(utils.parseQuery({})).toEqual({ metric: '', selected: '', view: utils.DEFAULT_VIEW }); + expect(utils.parseQuery({})).toEqual({ + metric: 'project_overview', + selected: '', + view: utils.DEFAULT_VIEW + }); expect(utils.parseQuery({ metric: 'foo', selected: 'bar', view: 'tree' })).toEqual({ metric: 'foo', selected: 'bar', 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 index 4dd6eb92774..6aadddf9fca 100644 --- 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 @@ -26,8 +26,7 @@ import MeasureFavoriteContainer from './MeasureFavoriteContainer'; 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 { enhanceComponent, getBubbleMetrics, isFileType } from '../utils'; import type { Component, ComponentEnhanced, Paging, Period } from '../types'; import type { Metric } from '../../../store/metrics/actions'; @@ -78,22 +77,19 @@ export default class MeasureOverview extends React.PureComponent { 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 { xMetric, yMetric, sizeMetric, colorsMetric } = getBubbleMetrics( + props.domain, + props.metrics + ); const metricsKey = [xMetric.key, yMetric.key, sizeMetric.key]; + if (colorsMetric) { + metricsKey.push(colorsMetric.map(metric => metric.key)); + } const options = { s: 'metric', metricSort: sizeMetric.key, 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 index 3fd1e2a2055..12aa314d30b 100644 --- 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 @@ -19,9 +19,30 @@ */ // @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' }, + Reliability: { + x: 'ncloc', + y: 'reliability_remediation_effort', + size: 'bugs', + colors: ['reliability_rating'] + }, + Security: { + x: 'ncloc', + y: 'security_remediation_effort', + size: 'vulnerabilities', + colors: ['security_rating'] + }, + Maintainability: { + x: 'ncloc', + y: 'sqale_index', + size: 'code_smells', + colors: ['sqale_rating'] + }, Coverage: { x: 'complexity', y: 'coverage', size: 'uncovered_lines' }, - Duplications: { x: 'ncloc', y: 'duplicated_lines', size: 'duplicated_blocks' } + Duplications: { x: 'ncloc', y: 'duplicated_lines', size: 'duplicated_blocks' }, + project_overview: { + x: 'sqale_index', + y: 'coverage', + size: 'ncloc', + colors: ['reliability_rating', 'security_rating'] + } }; 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 index 495a3fc3158..8ef9df9b759 100644 --- 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 @@ -21,13 +21,16 @@ import React from 'react'; import EmptyResult from './EmptyResult'; import OriginalBubbleChart from '../../../components/charts/BubbleChart'; +import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend'; import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; import { getLocalizedMetricDomain, getLocalizedMetricName, + translate, translateWithParameters } from '../../../helpers/l10n'; -import { bubbles } from '../config/bubbles'; +import { getBubbleMetrics, isProjectOverview } from '../utils'; +import { RATING_COLORS } from '../../../helpers/constants'; import type { Component, ComponentEnhanced } from '../types'; import type { Metric } from '../../../store/metrics/actions'; @@ -44,15 +47,6 @@ type Props = {| 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) { @@ -65,27 +59,45 @@ export default class BubbleChart extends React.PureComponent { x: number, y: number, size: number, + colors: ?Array, xMetric: Metric, yMetric: Metric, - sizeMetric: Metric + sizeMetric: Metric, + colorsMetric: ?Array ) { const inner = [ componentName, `${xMetric.name}: ${formatMeasure(x, xMetric.type)}`, `${yMetric.name}: ${formatMeasure(y, yMetric.type)}`, `${sizeMetric.name}: ${formatMeasure(size, sizeMetric.type)}` - ].join('
'); - return `
${inner}
`; + ]; + if (colors && colorsMetric) { + colorsMetric.forEach((metric, idx) => { + // $FlowFixMe colors is always defined at this point + const colorValue = colors[idx]; + if (colorValue || colorValue === 0) { + inner.push(`${metric.name}: ${formatMeasure(colorValue, metric.type)}`); + } + }); + } + return `
${inner.join('
')}
`; } handleBubbleClick = (component: ComponentEnhanced) => this.props.updateSelected(component.key); - renderBubbleChart(xMetric: Metric, yMetric: Metric, sizeMetric: Metric) { + renderBubbleChart( + xMetric: Metric, + yMetric: Metric, + sizeMetric: Metric, + colorsMetric: ?Array + ) { 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); + const colors = + colorsMetric && colorsMetric.map(metric => this.getMeasureVal(component, metric)); if ((!x && x !== 0) || (!y && y !== 0) || (!size && size !== 0)) { return null; } @@ -93,8 +105,20 @@ export default class BubbleChart extends React.PureComponent { x, y, size, + color: + colors != null ? RATING_COLORS[Math.max(...colors.filter(Boolean)) - 1] : undefined, link: component, - tooltip: this.getTooltip(component.name, x, y, size, xMetric, yMetric, sizeMetric) + tooltip: this.getTooltip( + component.name, + x, + y, + size, + colors, + xMetric, + yMetric, + sizeMetric, + colorsMetric + ) }; }) .filter(Boolean); @@ -114,35 +138,63 @@ export default class BubbleChart extends React.PureComponent { ); } - render() { - if (this.props.components.length <= 0) { - return ; - } - - const { xMetric, yMetric, sizeMetric } = this.getBubbleMetrics(this.props); + renderChartHeader(domain: string, sizeMetric: Metric, colorsMetric: ?Array) { + const title = isProjectOverview(domain) + ? translate('component_measures.overview', domain, 'title') + : translateWithParameters( + 'component_measures.domain_x_overview', + getLocalizedMetricDomain(domain) + ); return ( -
-
- - {translateWithParameters( - 'component_measures.domain_x_overview', - getLocalizedMetricDomain(this.props.domain) - )} - - +
+ + {title} + + + + {colorsMetric && + + {translateWithParameters( + 'component_measures.legend.color_x', + colorsMetric.length > 1 + ? translateWithParameters( + 'component_measures.legend.worse_of_x_y', + ...colorsMetric.map(metric => getLocalizedMetricName(metric)) + ) + : getLocalizedMetricName(colorsMetric[0]) + )} + } {translateWithParameters( 'component_measures.legend.size_x', getLocalizedMetricName(sizeMetric) )} + {colorsMetric && } + +
+ ); + } + + render() { + if (this.props.components.length <= 0) { + return ; + } + const { domain } = this.props; + const { xMetric, yMetric, sizeMetric, colorsMetric } = getBubbleMetrics( + domain, + this.props.metrics + ); + + return ( +
+ {this.renderChartHeader(domain, sizeMetric, colorsMetric)} +
+ {this.renderBubbleChart(xMetric, yMetric, sizeMetric, colorsMetric)}
-
- {this.renderBubbleChart(xMetric, yMetric, sizeMetric)} -
-
+
{getLocalizedMetricName(xMetric)}
-
+
{getLocalizedMetricName(yMetric)}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.js new file mode 100644 index 00000000000..a3d2a9c5b9a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.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 FacetBox from '../../../components/facet/FacetBox'; +import FacetItem from '../../../components/facet/FacetItem'; +import FacetItemsList from '../../../components/facet/FacetItemsList'; +import Tooltip from '../../../components/controls/Tooltip'; +import { translate } from '../../../helpers/l10n'; + +type Props = {| + onChange: (metric: string) => void, + selected: string, + value: string +|}; + +export default class ProjectOverviewFacet extends React.PureComponent { + props: Props; + + render() { + const { value, selected } = this.props; + const facetName = translate('component_measures.overview', value, 'facet'); + return ( + + + + + {facetName} + + + } + onClick={this.props.onChange} + value={value} + /> + + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js index bf126b8a5b4..441491a518f 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js @@ -19,8 +19,9 @@ */ // @flow import React from 'react'; +import ProjectOverviewFacet from './ProjectOverviewFacet'; import DomainFacet from './DomainFacet'; -import { groupByDomains } from '../utils'; +import { groupByDomains, PROJECT_OVERVEW } from '../utils'; import type { MeasureEnhanced } from '../../../components/measure/types'; import type { Query } from '../types'; @@ -56,6 +57,11 @@ export default class Sidebar extends React.PureComponent { render() { return (
+ {groupByDomains(this.props.measures).map(domain => + export const hasBubbleChart = (domainName: string): boolean => bubbles[domainName] != null; +export const getBubbleMetrics = (domain: string, metrics: { [string]: Metric }) => { + const conf = bubbles[domain]; + return { + xMetric: metrics[conf.x], + yMetric: metrics[conf.y], + sizeMetric: metrics[conf.size], + colorsMetric: conf.colors ? conf.colors.map(color => metrics[color]) : null + }; +}; + +export const isProjectOverview = (metric: string) => metric === PROJECT_OVERVEW; + export const parseQuery = memoize((urlQuery: RawQuery): Query => ({ - metric: parseAsString(urlQuery['metric']), + metric: parseAsString(urlQuery['metric']) || DEFAULT_METRIC, selected: parseAsString(urlQuery['selected']), view: parseAsString(urlQuery['view']) || DEFAULT_VIEW })); export const serializeQuery = memoize((query: Query): RawQuery => { return cleanQuery({ - metric: serializeString(query.metric), + metric: query.metric === DEFAULT_METRIC ? null : 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/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css index 1e16c1a1565..dd0ef5e696d 100644 --- a/server/sonar-web/src/main/js/apps/projects/styles.css +++ b/server/sonar-web/src/main/js/apps/projects/styles.css @@ -268,31 +268,6 @@ font-style: italic; } -.projects-visualizations-ratings { - display: flex; - justify-content: center; - margin-top: 16px; -} - -.projects-visualizations-ratings > *:not(:first-child) { - margin-left: 24px; -} - -.projects-visualizations-ratings-rect { - display: inline-block; - vertical-align: top; - margin-top: 1px; - margin-right: 4px; - border: 1px solid; -} - -.projects-visualizations-ratings-rect-inner { - display: block; - width: 8px; - height: 8px; - opacity: 0.2; -} - .measure-details-bubble-chart-axis { position: absolute; color: #777; diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.js b/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.js index fc881790b54..d69b6bfce49 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.js +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.js @@ -19,7 +19,7 @@ */ // @flow import React from 'react'; -import RatingsLegend from './RatingsLegend'; +import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend'; import BubbleChart from '../../../components/charts/BubbleChart'; import { formatMeasure } from '../../../helpers/measures'; import { translate, translateWithParameters } from '../../../helpers/l10n'; @@ -130,7 +130,7 @@ export default class Risk extends React.PureComponent { 'component_measures.legend.size_x', translate('metric', SIZE_METRIC, 'name') )} - +
); diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.js b/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.js index ede73dbf87b..44ddf736040 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.js +++ b/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.js @@ -19,7 +19,7 @@ */ // @flow import React from 'react'; -import RatingsLegend from './RatingsLegend'; +import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend'; import BubbleChart from '../../../components/charts/BubbleChart'; import { formatMeasure } from '../../../helpers/measures'; import { translate, translateWithParameters } from '../../../helpers/l10n'; @@ -130,7 +130,7 @@ export default class SimpleBubbleChart extends React.PureComponent { 'component_measures.legend.size_x', translate('metric', sizeMetric.key, 'name') )} - {colorMetric != null && } + {colorMetric != null && }
); diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/RatingsLegend.js b/server/sonar-web/src/main/js/components/charts/ColorRatingsLegend.js similarity index 75% rename from server/sonar-web/src/main/js/apps/projects/visualizations/RatingsLegend.js rename to server/sonar-web/src/main/js/components/charts/ColorRatingsLegend.js index b22d9729157..9e17ff01add 100644 --- a/server/sonar-web/src/main/js/apps/projects/visualizations/RatingsLegend.js +++ b/server/sonar-web/src/main/js/components/charts/ColorRatingsLegend.js @@ -19,19 +19,20 @@ */ // @flow import React from 'react'; -import { formatMeasure } from '../../../helpers/measures'; -import { RATING_COLORS } from '../../../helpers/constants'; +import classNames from 'classnames'; +import { formatMeasure } from '../../helpers/measures'; +import { RATING_COLORS } from '../../helpers/constants'; -export default function RatingsLegend() { +export default function ColorRatingsLegend({ className }: { className?: string }) { return ( -
+
{[1, 2, 3, 4, 5].map(rating =>
diff --git a/server/sonar-web/src/main/less/components/graphics.less b/server/sonar-web/src/main/less/components/graphics.less index 47b0390b2f0..8df1fcb6b70 100644 --- a/server/sonar-web/src/main/less/components/graphics.less +++ b/server/sonar-web/src/main/less/components/graphics.less @@ -263,6 +263,30 @@ text-anchor: end; } +.color-ratings-legend { + display: flex; + justify-content: center; + + & > *:not(:first-child) { + margin-left: 24px; + } + + .color-ratings-legend-rect { + display: inline-block; + vertical-align: top; + margin-top: 1px; + margin-right: 4px; + border: 1px solid; + } + + .color-ratings-legend-rect-inner { + display: block; + width: 8px; + height: 8px; + opacity: 0.2; + } +} + /* * Bar Chart */ diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 621bde0875e..2cb9c8049de 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2894,6 +2894,7 @@ component_measures.tab.treemap=Treemap component_measures.tab.history=History component_measures.legend.color_x=Color: {0} component_measures.legend.size_x=Size: {0} +component_measures.legend.worse_of_x_y=Worse of {0} and {1} component_measures.x_of_y={0} of {1} component_measures.no_history=There is no historical data. component_measures.not_found=The requested measure was not found. @@ -2901,6 +2902,9 @@ component_measures.to_select_files=to select files component_measures.to_navigate=to navigate component_measures.to_navigate_back=to navigate back +component_measures.overview.project_overview.facet=Project Overview +component_measures.overview.project_overview.title=Risk + #------------------------------------------------------------------------------ # # ABOUT PAGE -- 2.39.5