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',
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';
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,
*/
// @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']
+ }
};
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';
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) {
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;
}
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);
);
}
- 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>
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
*/
// @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';
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}
<div
className="search-navigator-facets-list"
>
+ <ProjectOverviewFacet
+ onChange={[Function]}
+ selected="foo"
+ value="project_overview"
+ />
<DomainFacet
domain={
Object {
}
.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 {
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',
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)
});
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;
+++ /dev/null
-/*
- * 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>
- );
-}
*/
// @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';
'component_measures.legend.size_x',
translate('metric', SIZE_METRIC, 'name')
)}
- <RatingsLegend />
+ <ColorRatingsLegend className="big-spacer-top" />
</div>
</div>
);
*/
// @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';
'component_measures.legend.size_x',
translate('metric', sizeMetric.key, 'name')
)}
- {colorMetric != null && <RatingsLegend />}
+ {colorMetric != null && <ColorRatingsLegend className="big-spacer-top" />}
</div>
</div>
);
--- /dev/null
+/*
+ * 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>
+ );
+}
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
*/
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.
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