]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9608 SONAR-9611 Create the project overview bubble chart on the measures page
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 4 Aug 2017 15:17:21 +0000 (17:17 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Mon, 14 Aug 2017 09:44:44 +0000 (11:44 +0200)
16 files changed:
server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.js
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js
server/sonar-web/src/main/js/apps/component-measures/config/bubbles.js
server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.js
server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap
server/sonar-web/src/main/js/apps/component-measures/style.css
server/sonar-web/src/main/js/apps/component-measures/utils.js
server/sonar-web/src/main/js/apps/projects/styles.css
server/sonar-web/src/main/js/apps/projects/visualizations/RatingsLegend.js [deleted file]
server/sonar-web/src/main/js/apps/projects/visualizations/Risk.js
server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.js
server/sonar-web/src/main/js/components/charts/ColorRatingsLegend.js [new file with mode: 0644]
server/sonar-web/src/main/less/components/graphics.less
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 00be69c8786af9c84e732b626f2a9dd5ceee8adc..bd275efccc845d6a0eda3dea3b6e9c34b4cc2909 100644 (file)
@@ -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',
index 4dd6eb92774d0c5f8e7e40164b037deec3ee04d5..6aadddf9fcae72c28ff841262116c0d79ce51b48 100644 (file)
@@ -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,
index 3fd1e2a205548ac50d25c2ad4f9ea20b045afb22..12aa314d30b181cd0a1e86f990e4ba14437ecd87 100644 (file)
  */
 // @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']
+  }
 };
index 495a3fc315853147ff64e47bf441425efea7c3e3..8ef9df9b759d4aa316f85fd73b57c88b3da38edb 100644 (file)
 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<?number>,
     xMetric: Metric,
     yMetric: Metric,
-    sizeMetric: Metric
+    sizeMetric: Metric,
+    colorsMetric: ?Array<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>`;
+    ];
+    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 `<div class="text-left">${inner.join('<br/>')}</div>`;
   }
 
   handleBubbleClick = (component: ComponentEnhanced) => this.props.updateSelected(component.key);
 
-  renderBubbleChart(xMetric: Metric, yMetric: Metric, sizeMetric: Metric) {
+  renderBubbleChart(
+    xMetric: Metric,
+    yMetric: Metric,
+    sizeMetric: Metric,
+    colorsMetric: ?Array<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);
+        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 <EmptyResult />;
-    }
-
-    const { xMetric, yMetric, sizeMetric } = this.getBubbleMetrics(this.props);
+  renderChartHeader(domain: string, sizeMetric: Metric, colorsMetric: ?Array<Metric>) {
+    const title = isProjectOverview(domain)
+      ? translate('component_measures.overview', domain, 'title')
+      : translateWithParameters(
+          'component_measures.domain_x_overview',
+          getLocalizedMetricDomain(domain)
+        );
     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">
+      <div className="measure-overview-bubble-chart-header">
+        <span className="measure-overview-bubble-chart-title">
+          {title}
+        </span>
+        <span className="measure-overview-bubble-chart-legend">
+          <span className="note">
+            {colorsMetric &&
+              <span className="spacer-right">
+                {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])
+                )}
+              </span>}
             {translateWithParameters(
               'component_measures.legend.size_x',
               getLocalizedMetricName(sizeMetric)
             )}
           </span>
+          {colorsMetric && <ColorRatingsLegend className="spacer-top" />}
+        </span>
+      </div>
+    );
+  }
+
+  render() {
+    if (this.props.components.length <= 0) {
+      return <EmptyResult />;
+    }
+    const { domain } = this.props;
+    const { xMetric, yMetric, sizeMetric, colorsMetric } = getBubbleMetrics(
+      domain,
+      this.props.metrics
+    );
+
+    return (
+      <div className="measure-overview-bubble-chart">
+        {this.renderChartHeader(domain, sizeMetric, colorsMetric)}
+        <div className="measure-overview-bubble-chart-content">
+          {this.renderBubbleChart(xMetric, yMetric, sizeMetric, colorsMetric)}
         </div>
-        <div>
-          {this.renderBubbleChart(xMetric, yMetric, sizeMetric)}
-        </div>
-        <div className="measure-details-bubble-chart-axis x">
+        <div className="measure-overview-bubble-chart-axis x">
           {getLocalizedMetricName(xMetric)}
         </div>
-        <div className="measure-details-bubble-chart-axis y">
+        <div className="measure-overview-bubble-chart-axis y">
           {getLocalizedMetricName(yMetric)}
         </div>
       </div>
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 (file)
index 0000000..a3d2a9c
--- /dev/null
@@ -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 (
+      <FacetBox>
+        <FacetItemsList>
+          <FacetItem
+            active={value === selected}
+            disabled={false}
+            key={value}
+            name={
+              <Tooltip overlay={facetName} mouseEnterDelay={0.5}>
+                <strong id={`measure-overview-${value}-name`}>
+                  {facetName}
+                </strong>
+              </Tooltip>
+            }
+            onClick={this.props.onChange}
+            value={value}
+          />
+        </FacetItemsList>
+      </FacetBox>
+    );
+  }
+}
index bf126b8a5b4969b65352b438d837f04161807a7a..441491a518f2e7daee6b3dca498759e557ee92bd 100644 (file)
@@ -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 (
       <div className="search-navigator-facets-list">
+        <ProjectOverviewFacet
+          onChange={this.changeMetric}
+          selected={this.props.selectedMetric}
+          value={PROJECT_OVERVEW}
+        />
         {groupByDomains(this.props.measures).map(domain =>
           <DomainFacet
             key={domain.name}
index ba904466156a72b03e5455b29d17d23fd05d5983..5e7477f09e13f3ef8fc5f2bf4ba28ae0c1bc621f 100644 (file)
@@ -4,6 +4,11 @@ exports[`should display two facets 1`] = `
 <div
   className="search-navigator-facets-list"
 >
+  <ProjectOverviewFacet
+    onChange={[Function]}
+    selected="foo"
+    value="project_overview"
+  />
   <DomainFacet
     domain={
       Object {
index 0f9c831dfb755566a938a1d21c686c1b3e382b35..5327c58edd22e2a7e8b624a87b676afc85ee9ca3 100644 (file)
 }
 
 .measure-details-bubble-chart-header {
+  display: flex;
+  align-items: center;
   padding: 16px;
   margin-left: -60px;
   border-bottom: 1px solid #e6e6e6;
 }
 
-.measure-details-bubble-chart-legend {
+.measure-details-bubble-chart-title {
   position: absolute;
-  width: 100%;
-  left: 0;
+}
+
+.measure-details-bubble-chart-legend {
+  display: flex;
+  flex-direction: column;
   text-align: center;
+  flex-grow: 1;
 }
 
 .measure-details-bubble-chart-axis {
index 8d672b97def7c3aab7bd3c6b5ec1af0d85fbb394..41b4186466d0678ad87c5bf11a55d6bac79da487 100644 (file)
@@ -29,7 +29,9 @@ import type { RawQuery } from '../../helpers/query';
 import type { Metric } from '../../store/metrics/actions';
 import type { MeasureEnhanced } from '../../components/measure/types';
 
+export const PROJECT_OVERVEW = 'project_overview';
 export const DEFAULT_VIEW = 'list';
+export const DEFAULT_METRIC = PROJECT_OVERVEW;
 const KNOWN_DOMAINS = [
   'Releasability',
   'Reliability',
@@ -112,15 +114,27 @@ export const hasTreemap = (metricType: string): boolean =>
 
 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)
   });
index 1e16c1a15650a594943d3246ad747474a25e846a..dd0ef5e696d721fdbe408ce9a773dac3abe2a3cc 100644 (file)
   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/RatingsLegend.js b/server/sonar-web/src/main/js/apps/projects/visualizations/RatingsLegend.js
deleted file mode 100644 (file)
index b22d972..0000000
+++ /dev/null
@@ -1,43 +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 { formatMeasure } from '../../../helpers/measures';
-import { RATING_COLORS } from '../../../helpers/constants';
-
-export default function RatingsLegend() {
-  return (
-    <div className="projects-visualizations-ratings">
-      {[1, 2, 3, 4, 5].map(rating =>
-        <div key={rating}>
-          <span
-            className="projects-visualizations-ratings-rect"
-            style={{ borderColor: RATING_COLORS[rating - 1] }}>
-            <span
-              className="projects-visualizations-ratings-rect-inner"
-              style={{ backgroundColor: RATING_COLORS[rating - 1] }}
-            />
-          </span>
-          {formatMeasure(rating, 'RATING')}
-        </div>
-      )}
-    </div>
-  );
-}
index fc881790b54cefe05bd07a1c1587c6bb60e49cc2..d69b6bfce49a026c486b856be92a088b77d8e53f 100644 (file)
@@ -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')
           )}
-          <RatingsLegend />
+          <ColorRatingsLegend className="big-spacer-top" />
         </div>
       </div>
     );
index ede73dbf87b369805eacf4b7cd9160f2dd1a086c..44ddf7360406e03cbe61c6d3b184d7a1258ce16c 100644 (file)
@@ -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 && <RatingsLegend />}
+          {colorMetric != null && <ColorRatingsLegend className="big-spacer-top" />}
         </div>
       </div>
     );
diff --git a/server/sonar-web/src/main/js/components/charts/ColorRatingsLegend.js b/server/sonar-web/src/main/js/components/charts/ColorRatingsLegend.js
new file mode 100644 (file)
index 0000000..9e17ff0
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * 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 { formatMeasure } from '../../helpers/measures';
+import { RATING_COLORS } from '../../helpers/constants';
+
+export default function ColorRatingsLegend({ className }: { className?: string }) {
+  return (
+    <div className={classNames('color-ratings-legend', className)}>
+      {[1, 2, 3, 4, 5].map(rating =>
+        <div key={rating}>
+          <span
+            className="color-ratings-legend-rect"
+            style={{ borderColor: RATING_COLORS[rating - 1] }}>
+            <span
+              className="color-ratings-legend-rect-inner"
+              style={{ backgroundColor: RATING_COLORS[rating - 1] }}
+            />
+          </span>
+          {formatMeasure(rating, 'RATING')}
+        </div>
+      )}
+    </div>
+  );
+}
index 47b0390b2f0f36101dd2a302a212a7c1bdf94b5e..8df1fcb6b7018d147ebbf1815021b069ec62b1e3 100644 (file)
   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
  */
index 621bde0875e12eda18a45d02def4256ea71189a4..2cb9c8049deb60ad2587bb04a0bf5734164983b6 100644 (file)
@@ -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