diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-08-04 17:17:21 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-08-14 11:44:44 +0200 |
commit | bb393dd277ab06c8451513a6339171e800fc9944 (patch) | |
tree | 8d452b784eed096a3362fb5730d94a5ddd0b82a1 | |
parent | 21cd227ec55a54d22edfddbb45ec011359bdba3e (diff) | |
download | sonarqube-bb393dd277ab06c8451513a6339171e800fc9944.tar.gz sonarqube-bb393dd277ab06c8451513a6339171e800fc9944.zip |
SONAR-9608 SONAR-9611 Create the project overview bubble chart on the measures page
15 files changed, 262 insertions, 93 deletions
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<?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 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 ( + <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> + ); + } +} 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 ( <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} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap index ba904466156..5e7477f09e1 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap @@ -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 { 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 0f9c831dfb7..5327c58edd2 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 @@ -98,16 +98,22 @@ } .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 { 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 8d672b97def..41b4186466d 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 @@ -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) }); 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') )} - <RatingsLegend /> + <ColorRatingsLegend className="big-spacer-top" /> </div> </div> ); 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 && <RatingsLegend />} + {colorMetric != null && <ColorRatingsLegend className="big-spacer-top" />} </div> </div> ); 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 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 ( - <div className="projects-visualizations-ratings"> + <div className={classNames('color-ratings-legend', className)}> {[1, 2, 3, 4, 5].map(rating => <div key={rating}> <span - className="projects-visualizations-ratings-rect" + className="color-ratings-legend-rect" style={{ borderColor: RATING_COLORS[rating - 1] }}> <span - className="projects-visualizations-ratings-rect-inner" + className="color-ratings-legend-rect-inner" style={{ backgroundColor: RATING_COLORS[rating - 1] }} /> </span> 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 |