updateSelected: (component: string) => void;
}
-export default class BubbleChart extends React.PureComponent<Props> {
+interface State {
+ ratingFilters: { [rating: number]: boolean };
+}
+
+export default class BubbleChart extends React.PureComponent<Props, State> {
+ state: State = {
+ ratingFilters: {}
+ };
+
getMeasureVal = (component: T.ComponentMeasureEnhanced, metric: T.Metric) => {
const measure = component.measures.find(measure => measure.metric.key === metric.key);
if (!measure) {
);
}
+ handleRatingFilterClick = (selection: number) => {
+ this.setState(({ ratingFilters }) => {
+ return { ratingFilters: { ...ratingFilters, [selection]: !ratingFilters[selection] } };
+ });
+ };
+
handleBubbleClick = (component: T.ComponentMeasureEnhanced) =>
this.props.updateSelected(component.refKey || component.key);
}
renderBubbleChart(metrics: { x: T.Metric; y: T.Metric; size: T.Metric; colors?: T.Metric[] }) {
+ const { ratingFilters } = this.state;
+
const items = this.props.components
.map(component => {
const x = this.getMeasureVal(component, metrics.x);
if ((!x && x !== 0) || (!y && y !== 0) || (!size && size !== 0)) {
return undefined;
}
+
+ const colorRating = colors && Math.max(...colors.filter(isDefined));
+
+ // Filter out items that match ratingFilters
+ if (colorRating !== undefined && ratingFilters[colorRating]) {
+ return undefined;
+ }
+
return {
x,
y,
size,
- color:
- colors !== undefined
- ? RATING_COLORS[Math.max(...colors.filter(isDefined)) - 1]
- : undefined,
+ color: colorRating !== undefined ? RATING_COLORS[colorRating - 1] : undefined,
data: component,
tooltip: this.getTooltip(component.name, { x, y, size, colors }, metrics)
};
}
renderChartHeader(domain: string, sizeMetric: T.Metric, colorsMetric?: T.Metric[]) {
+ const { ratingFilters } = this.state;
+
const title = isProjectOverview(domain)
? translate('component_measures.overview', domain, 'title')
: translateWithParameters(
getLocalizedMetricName(sizeMetric)
)}
</span>
- {colorsMetric && <ColorRatingsLegend className="spacer-top" />}
+ {colorsMetric && (
+ <ColorRatingsLegend
+ className="spacer-top"
+ filters={ratingFilters}
+ onRatingClick={this.handleRatingFilterClick}
+ />
+ )}
</span>
</div>
);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.
+ */
+import { shallow } from 'enzyme';
+import { keyBy } from 'lodash';
+import * as React from 'react';
+import { mockComponentMeasure, mockMeasure, mockMetric } from '../../../../helpers/testMocks';
+import { MetricKey } from '../../../../types/metrics';
+import { enhanceComponent } from '../../utils';
+import BubbleChart from '../BubbleChart';
+
+const metrics = keyBy(
+ [
+ mockMetric({ key: MetricKey.ncloc, type: 'NUMBER' }),
+ mockMetric({ key: MetricKey.security_remediation_effort, type: 'NUMBER' }),
+ mockMetric({ key: MetricKey.vulnerabilities, type: 'NUMBER' }),
+ mockMetric({ key: MetricKey.security_rating, type: 'RATING' })
+ ],
+ m => m.key
+);
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should handle filtering', () => {
+ const wrapper = shallowRender();
+
+ wrapper.instance().handleRatingFilterClick(2);
+
+ expect(wrapper.state().ratingFilters).toEqual({ 2: true });
+ expect(wrapper).toMatchSnapshot();
+});
+
+function shallowRender(overrides: Partial<BubbleChart['props']> = {}) {
+ return shallow<BubbleChart>(
+ <BubbleChart
+ component={mockComponentMeasure()}
+ components={[
+ enhanceComponent(
+ mockComponentMeasure(true, {
+ measures: [
+ mockMeasure({ value: '236', metric: MetricKey.ncloc }),
+ mockMeasure({ value: '10', metric: MetricKey.security_remediation_effort }),
+ mockMeasure({ value: '3', metric: MetricKey.vulnerabilities }),
+ mockMeasure({ value: '2', metric: MetricKey.security_rating })
+ ]
+ }),
+ metrics[MetricKey.vulnerabilities],
+ metrics
+ )
+ ]}
+ domain="Security"
+ metrics={metrics}
+ updateSelected={jest.fn()}
+ {...overrides}
+ />
+ );
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should handle filtering 1`] = `
+<div
+ className="measure-overview-bubble-chart"
+>
+ <div
+ className="measure-overview-bubble-chart-header"
+ >
+ <span
+ className="measure-overview-bubble-chart-title"
+ >
+ <span
+ className="text-middle"
+ >
+ component_measures.domain_x_overview.Security
+ </span>
+ <HelpTooltip
+ className="spacer-left"
+ overlay={null}
+ />
+ </span>
+ <span
+ className="measure-overview-bubble-chart-legend"
+ >
+ <span
+ className="note"
+ >
+ <span
+ className="spacer-right"
+ >
+ component_measures.legend.color_x.Security_rating
+ </span>
+ component_measures.legend.size_x.Vulnerabilities
+ </span>
+ <ColorRatingsLegend
+ className="spacer-top"
+ filters={
+ Object {
+ "2": true,
+ }
+ }
+ onRatingClick={[Function]}
+ />
+ </span>
+ </div>
+ <div
+ className="measure-overview-bubble-chart-content"
+ >
+ <BubbleChart
+ displayXGrid={true}
+ displayXTicks={true}
+ displayYGrid={true}
+ displayYTicks={true}
+ formatXTick={[Function]}
+ formatYTick={[Function]}
+ height={500}
+ items={Array []}
+ onBubbleClick={[Function]}
+ padding={
+ Array [
+ 25,
+ 60,
+ 50,
+ 60,
+ ]
+ }
+ sizeRange={
+ Array [
+ 5,
+ 45,
+ ]
+ }
+ />
+ </div>
+ <div
+ className="measure-overview-bubble-chart-axis x"
+ >
+ Ncloc
+ </div>
+ <div
+ className="measure-overview-bubble-chart-axis y"
+ >
+ Security_remediation_effort
+ </div>
+</div>
+`;
+
+exports[`should render correctly 1`] = `
+<div
+ className="measure-overview-bubble-chart"
+>
+ <div
+ className="measure-overview-bubble-chart-header"
+ >
+ <span
+ className="measure-overview-bubble-chart-title"
+ >
+ <span
+ className="text-middle"
+ >
+ component_measures.domain_x_overview.Security
+ </span>
+ <HelpTooltip
+ className="spacer-left"
+ overlay={null}
+ />
+ </span>
+ <span
+ className="measure-overview-bubble-chart-legend"
+ >
+ <span
+ className="note"
+ >
+ <span
+ className="spacer-right"
+ >
+ component_measures.legend.color_x.Security_rating
+ </span>
+ component_measures.legend.size_x.Vulnerabilities
+ </span>
+ <ColorRatingsLegend
+ className="spacer-top"
+ filters={Object {}}
+ onRatingClick={[Function]}
+ />
+ </span>
+ </div>
+ <div
+ className="measure-overview-bubble-chart-content"
+ >
+ <BubbleChart
+ displayXGrid={true}
+ displayXTicks={true}
+ displayYGrid={true}
+ displayYTicks={true}
+ formatXTick={[Function]}
+ formatYTick={[Function]}
+ height={500}
+ items={
+ Array [
+ Object {
+ "color": "#b0d513",
+ "data": Object {
+ "key": "foo:src/index.tsx",
+ "leak": "1.0",
+ "measures": Array [
+ Object {
+ "bestValue": true,
+ "leak": "1.0",
+ "metric": Object {
+ "id": "ncloc",
+ "key": "ncloc",
+ "name": "Ncloc",
+ "type": "NUMBER",
+ },
+ "period": Object {
+ "bestValue": true,
+ "index": 1,
+ "value": "1.0",
+ },
+ "value": "236",
+ },
+ Object {
+ "bestValue": true,
+ "leak": "1.0",
+ "metric": Object {
+ "id": "security_remediation_effort",
+ "key": "security_remediation_effort",
+ "name": "Security_remediation_effort",
+ "type": "NUMBER",
+ },
+ "period": Object {
+ "bestValue": true,
+ "index": 1,
+ "value": "1.0",
+ },
+ "value": "10",
+ },
+ Object {
+ "bestValue": true,
+ "leak": "1.0",
+ "metric": Object {
+ "id": "vulnerabilities",
+ "key": "vulnerabilities",
+ "name": "Vulnerabilities",
+ "type": "NUMBER",
+ },
+ "period": Object {
+ "bestValue": true,
+ "index": 1,
+ "value": "1.0",
+ },
+ "value": "3",
+ },
+ Object {
+ "bestValue": true,
+ "leak": "1.0",
+ "metric": Object {
+ "id": "security_rating",
+ "key": "security_rating",
+ "name": "Security_rating",
+ "type": "RATING",
+ },
+ "period": Object {
+ "bestValue": true,
+ "index": 1,
+ "value": "1.0",
+ },
+ "value": "2",
+ },
+ ],
+ "name": "index.tsx",
+ "path": "src/index.tsx",
+ "qualifier": "FIL",
+ "value": "3",
+ },
+ "size": 3,
+ "tooltip": <div
+ className="text-left"
+ >
+ <React.Fragment>
+ index.tsx
+ <br />
+ </React.Fragment>
+ <React.Fragment>
+ Ncloc: 236
+ <br />
+ </React.Fragment>
+ <React.Fragment>
+ Security_remediation_effort: 10
+ <br />
+ </React.Fragment>
+ <React.Fragment>
+ Vulnerabilities: 3
+ <br />
+ </React.Fragment>
+ <React.Fragment>
+ Security_rating: B
+ </React.Fragment>
+ </div>,
+ "x": 236,
+ "y": 10,
+ },
+ ]
+ }
+ onBubbleClick={[Function]}
+ padding={
+ Array [
+ 25,
+ 60,
+ 50,
+ 60,
+ ]
+ }
+ sizeRange={
+ Array [
+ 5,
+ 45,
+ ]
+ }
+ />
+ </div>
+ <div
+ className="measure-overview-bubble-chart-axis x"
+ >
+ Ncloc
+ </div>
+ <div
+ className="measure-overview-bubble-chart-axis y"
+ >
+ Security_remediation_effort
+ </div>
+</div>
+`;
import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { formatMeasure } from 'sonar-ui-common/helpers/measures';
+import { isDefined } from 'sonar-ui-common/helpers/types';
import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend';
import { RATING_COLORS } from '../../../helpers/constants';
import { getProjectUrl } from '../../../helpers/urls';
projects: Project[];
}
-export default class Risk extends React.PureComponent<Props> {
+interface State {
+ ratingFilters: { [rating: number]: boolean };
+}
+
+export default class Risk extends React.PureComponent<Props, State> {
+ state: State = {
+ ratingFilters: {}
+ };
+
getMetricTooltip(metric: { key: string; type: string }, value?: number) {
const name = translate('metric', metric.key, 'name');
const formattedValue = value != null ? formatMeasure(value, metric.type) : '–';
);
}
- render() {
- const items = this.props.projects.map(project => {
- const x = project.measures[X_METRIC] != null ? Number(project.measures[X_METRIC]) : undefined;
- const y = project.measures[Y_METRIC] != null ? Number(project.measures[Y_METRIC]) : undefined;
- const size =
- project.measures[SIZE_METRIC] != null ? Number(project.measures[SIZE_METRIC]) : undefined;
- const color1 =
- project.measures[COLOR_METRIC_1] != null
- ? Number(project.measures[COLOR_METRIC_1])
- : undefined;
- const color2 =
- project.measures[COLOR_METRIC_2] != null
- ? Number(project.measures[COLOR_METRIC_2])
- : undefined;
- return {
- x: x || 0,
- y: y || 0,
- size: size || 0,
- color:
- color1 != null && color2 != null
- ? RATING_COLORS[Math.max(color1, color2) - 1]
- : undefined,
- key: project.key,
- tooltip: this.getTooltip(project, x, y, size, color1, color2),
- link: getProjectUrl(project.key)
- };
+ handleRatingFilterClick = (selection: number) => {
+ this.setState(({ ratingFilters }) => {
+ return { ratingFilters: { ...ratingFilters, [selection]: !ratingFilters[selection] } };
});
+ };
+
+ render() {
+ const { ratingFilters } = this.state;
+
+ const items = this.props.projects
+ .map(project => {
+ const x =
+ project.measures[X_METRIC] != null ? Number(project.measures[X_METRIC]) : undefined;
+ const y =
+ project.measures[Y_METRIC] != null ? Number(project.measures[Y_METRIC]) : undefined;
+ const size =
+ project.measures[SIZE_METRIC] != null ? Number(project.measures[SIZE_METRIC]) : undefined;
+ const color1 =
+ project.measures[COLOR_METRIC_1] != null
+ ? Number(project.measures[COLOR_METRIC_1])
+ : undefined;
+ const color2 =
+ project.measures[COLOR_METRIC_2] != null
+ ? Number(project.measures[COLOR_METRIC_2])
+ : undefined;
+
+ const colorRating =
+ color1 !== undefined && color2 !== undefined ? Math.max(color1, color2) : undefined;
+
+ // Filter out items that match ratingFilters
+ if (colorRating !== undefined && ratingFilters[colorRating]) {
+ return undefined;
+ }
+
+ return {
+ x: x || 0,
+ y: y || 0,
+ size: size || 0,
+ color: colorRating !== undefined ? RATING_COLORS[colorRating - 1] : undefined,
+ key: project.key,
+ tooltip: this.getTooltip(project, x, y, size, color1, color2),
+ link: getProjectUrl(project.key)
+ };
+ })
+ .filter(isDefined);
const formatXTick = (tick: number) => formatMeasure(tick, X_METRIC_TYPE);
const formatYTick = (tick: number) => formatMeasure(tick, Y_METRIC_TYPE);
'component_measures.legend.size_x',
translate('metric', SIZE_METRIC, 'name')
)}
- <ColorRatingsLegend className="big-spacer-top" />
+ <ColorRatingsLegend
+ className="big-spacer-top"
+ filters={ratingFilters}
+ onRatingClick={this.handleRatingFilterClick}
+ />
</div>
</div>
</div>
import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { formatMeasure } from 'sonar-ui-common/helpers/measures';
+import { isDefined } from 'sonar-ui-common/helpers/types';
import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend';
import { RATING_COLORS } from '../../../helpers/constants';
import { getProjectUrl } from '../../../helpers/urls';
yMetric: Metric;
}
-export default class SimpleBubbleChart extends React.PureComponent<Props> {
+interface State {
+ ratingFilters: { [rating: number]: boolean };
+}
+
+export default class SimpleBubbleChart extends React.PureComponent<Props, State> {
+ state: State = {
+ ratingFilters: {}
+ };
+
getMetricTooltip(metric: Metric, value?: number) {
const name = translate('metric', metric.key, 'name');
const formattedValue = value != null ? formatMeasure(value, metric.type) : '–';
);
}
+ handleRatingFilterClick = (selection: number) => {
+ this.setState(({ ratingFilters }) => {
+ return { ratingFilters: { ...ratingFilters, [selection]: !ratingFilters[selection] } };
+ });
+ };
+
render() {
const { xMetric, yMetric, sizeMetric, colorMetric } = this.props;
+ const { ratingFilters } = this.state;
const items = this.props.projects
.filter(project => colorMetric == null || project.measures[colorMetric] !== null)
? Number(project.measures[sizeMetric.key])
: undefined;
const color = colorMetric ? Number(project.measures[colorMetric]) : undefined;
+
+ // Filter out items that match ratingFilters
+ if (color && ratingFilters[color]) {
+ return undefined;
+ }
+
return {
x: x || 0,
y: y || 0,
tooltip: this.getTooltip(project, x, y, size, color),
link: getProjectUrl(project.key)
};
- });
+ })
+ .filter(isDefined);
const formatXTick = (tick: number) => formatMeasure(tick, xMetric.type);
const formatYTick = (tick: number) => formatMeasure(tick, yMetric.type);
'component_measures.legend.size_x',
translate('metric', sizeMetric.key, 'name')
)}
- {colorMetric != null && <ColorRatingsLegend className="big-spacer-top" />}
+ {colorMetric != null && (
+ <ColorRatingsLegend
+ className="big-spacer-top"
+ filters={ratingFilters}
+ onRatingClick={this.handleRatingFilterClick}
+ />
+ )}
</div>
</div>
</div>
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { ComponentQualifier } from '../../../../types/component';
-import { Project } from '../../types';
+import { mockProject } from '../../../../helpers/mocks/projects';
import Risk from '../Risk';
it('renders', () => {
- const project1: Project = {
- key: 'foo',
- measures: { complexity: '17.2', coverage: '53.5', ncloc: '1734' },
- name: 'Foo',
- qualifier: ComponentQualifier.Project,
- tags: [],
- visibility: 'public'
- };
- expect(
- shallow(<Risk displayOrganizations={false} helpText="foobar" projects={[project1]} />)
- ).toMatchSnapshot();
+ expect(shallowRender()).toMatchSnapshot();
});
+
+it('should handle filtering', () => {
+ const wrapper = shallowRender();
+
+ wrapper.instance().handleRatingFilterClick(2);
+
+ expect(wrapper.state().ratingFilters).toEqual({ 2: true });
+});
+
+function shallowRender(overrides: Partial<Risk['props']> = {}) {
+ const project1 = mockProject({
+ key: 'foo',
+ measures: {
+ complexity: '17.2',
+ coverage: '53.5',
+ ncloc: '1734',
+ sqale_index: '1',
+ reliability_rating: '3',
+ security_rating: '2'
+ },
+ name: 'Foo'
+ });
+ const project2 = mockProject({
+ key: 'bar',
+ name: 'Bar',
+ measures: {}
+ });
+ return shallow<Risk>(
+ <Risk
+ displayOrganizations={false}
+ helpText="foobar"
+ projects={[project1, project2]}
+ {...overrides}
+ />
+ );
+}
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { mockProject } from '../../../../helpers/mocks/projects';
import { ComponentQualifier } from '../../../../types/component';
-import { Project } from '../../types';
import SimpleBubbleChart from '../SimpleBubbleChart';
it('renders', () => {
- const project1: Project = {
- key: 'foo',
- measures: { complexity: '17.2', coverage: '53.5', ncloc: '1734', security_rating: '2' },
- name: 'Foo',
- qualifier: ComponentQualifier.Project,
- tags: [],
- visibility: 'public'
- };
- const app = {
- ...project1,
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should handle filtering', () => {
+ const wrapper = shallowRender();
+
+ wrapper.instance().handleRatingFilterClick(2);
+
+ expect(wrapper.state().ratingFilters).toEqual({ 2: true });
+});
+
+function shallowRender(overrides: Partial<SimpleBubbleChart['props']> = {}) {
+ const project1 = mockProject({
+ measures: { complexity: '17.2', coverage: '53.5', ncloc: '1734', security_rating: '2' }
+ });
+ const app = mockProject({
key: 'app',
measures: { complexity: '23.1', coverage: '87.3', ncloc: '32478', security_rating: '1' },
name: 'App',
qualifier: ComponentQualifier.Application
- };
- expect(
- shallow(
- <SimpleBubbleChart
- colorMetric="security_rating"
- displayOrganizations={false}
- helpText="foobar"
- projects={[app, project1]}
- sizeMetric={{ key: 'ncloc', type: 'INT' }}
- xMetric={{ key: 'complexity', type: 'INT' }}
- yMetric={{ key: 'coverage', type: 'PERCENT' }}
- />
- )
- ).toMatchSnapshot();
-});
+ });
+ return shallow<SimpleBubbleChart>(
+ <SimpleBubbleChart
+ colorMetric="security_rating"
+ displayOrganizations={false}
+ helpText="foobar"
+ projects={[app, project1]}
+ sizeMetric={{ key: 'ncloc', type: 'INT' }}
+ xMetric={{ key: 'complexity', type: 'INT' }}
+ yMetric={{ key: 'coverage', type: 'PERCENT' }}
+ {...overrides}
+ />
+ );
+}
items={
Array [
Object {
- "color": undefined,
+ "color": "#eabe06",
"key": "foo",
"link": Object {
"pathname": "/dashboard",
Foo
</strong>
</div>
+ <div>
+ metric.reliability_rating.name
+ :
+ C
+ </div>
+ <div>
+ metric.security_rating.name
+ :
+ B
+ </div>
+ <div>
+ metric.coverage.name
+ :
+ 53.5%
+ </div>
+ <div>
+ metric.sqale_index.name
+ :
+ work_duration.x_minutes.1
+ </div>
+ <div>
+ metric.ncloc.name
+ :
+ 1.7short_number_suffix.k
+ </div>
+ </div>,
+ "x": 1,
+ "y": 53.5,
+ },
+ Object {
+ "color": undefined,
+ "key": "bar",
+ "link": Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": undefined,
+ "id": "bar",
+ },
+ },
+ "size": 0,
+ "tooltip": <div
+ className="text-left"
+ >
+ <div
+ className="little-spacer-bottom display-flex-center display-flex-space-between"
+ >
+ <strong>
+ Bar
+ </strong>
+ </div>
<div>
metric.reliability_rating.name
:
<div>
metric.coverage.name
:
- 53.5%
+ –
</div>
<div>
metric.sqale_index.name
<div>
metric.ncloc.name
:
- 1.7short_number_suffix.k
+ –
</div>
</div>,
"x": 0,
- "y": 53.5,
+ "y": 0,
},
]
}
component_measures.legend.size_x.metric.ncloc.name
<ColorRatingsLegend
className="big-spacer-top"
+ filters={Object {}}
+ onRatingClick={[Function]}
/>
</div>
</div>
component_measures.legend.size_x.metric.ncloc.name
<ColorRatingsLegend
className="big-spacer-top"
+ filters={Object {}}
+ onRatingClick={[Function]}
/>
</div>
</div>
.color-box-legend.color-box-full .color-box-legend-rect-inner {
opacity: 1;
}
+
+.color-box-legend button {
+ color: var(--baseFontColor);
+ border-bottom: none;
+ display: block;
+}
+
+.color-box-legend button.filtered {
+ opacity: 0.3;
+}
*/
import * as classNames from 'classnames';
import * as React from 'react';
+import { ButtonLink } from 'sonar-ui-common/components/controls/buttons';
+import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
+import { translate } from 'sonar-ui-common/helpers/l10n';
import { formatMeasure } from 'sonar-ui-common/helpers/measures';
import { RATING_COLORS } from '../../helpers/constants';
import './ColorBoxLegend.css';
-interface Props {
+export interface ColorRatingsLegendProps {
className?: string;
+ filters: { [rating: number]: boolean };
+ onRatingClick: (selection: number) => void;
}
-export default function ColorRatingsLegend({ className }: Props) {
+const RATINGS = [1, 2, 3, 4, 5];
+
+export default function ColorRatingsLegend(props: ColorRatingsLegendProps) {
+ const { className, filters } = props;
return (
<div className={classNames('color-box-legend', className)}>
- {[1, 2, 3, 4, 5].map(rating => (
- <div key={rating}>
- <span
- className="color-box-legend-rect"
- style={{ borderColor: RATING_COLORS[rating - 1] }}>
+ {RATINGS.map(rating => (
+ <Tooltip key={rating} overlay={translate('component_measures.legend.help')}>
+ <ButtonLink
+ className={classNames('little-padded-bottom', {
+ filtered: filters[rating]
+ })}
+ onClick={() => props.onRatingClick(rating)}
+ type="button">
<span
- className="color-box-legend-rect-inner"
- style={{ backgroundColor: RATING_COLORS[rating - 1] }}
- />
- </span>
- {formatMeasure(rating, 'RATING')}
- </div>
+ className="color-box-legend-rect"
+ style={{ borderColor: RATING_COLORS[rating - 1] }}>
+ <span
+ className="color-box-legend-rect-inner"
+ style={{ backgroundColor: RATING_COLORS[rating - 1] }}
+ />
+ </span>
+ {formatMeasure(rating, 'RATING')}
+ </ButtonLink>
+ </Tooltip>
))}
</div>
);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { ButtonLink } from 'sonar-ui-common/components/controls/buttons';
+import ColorRatingsLegend, { ColorRatingsLegendProps } from '../ColorRatingsLegend';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should callback when a rating is clicked', () => {
+ const onRatingClick = jest.fn();
+ const wrapper = shallowRender({ onRatingClick });
+
+ wrapper
+ .find(ButtonLink)
+ .at(3)
+ .simulate('click');
+
+ expect(onRatingClick).toBeCalledWith(4);
+});
+
+function shallowRender(overrides: Partial<ColorRatingsLegendProps> = {}) {
+ return shallow(
+ <ColorRatingsLegend filters={{ 2: true }} onRatingClick={jest.fn()} {...overrides} />
+ );
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+ className="color-box-legend"
+>
+ <Tooltip
+ key="1"
+ overlay="component_measures.legend.help"
+ >
+ <ButtonLink
+ className="little-padded-bottom"
+ onClick={[Function]}
+ type="button"
+ >
+ <span
+ className="color-box-legend-rect"
+ style={
+ Object {
+ "borderColor": "#00aa00",
+ }
+ }
+ >
+ <span
+ className="color-box-legend-rect-inner"
+ style={
+ Object {
+ "backgroundColor": "#00aa00",
+ }
+ }
+ />
+ </span>
+ A
+ </ButtonLink>
+ </Tooltip>
+ <Tooltip
+ key="2"
+ overlay="component_measures.legend.help"
+ >
+ <ButtonLink
+ className="little-padded-bottom filtered"
+ onClick={[Function]}
+ type="button"
+ >
+ <span
+ className="color-box-legend-rect"
+ style={
+ Object {
+ "borderColor": "#b0d513",
+ }
+ }
+ >
+ <span
+ className="color-box-legend-rect-inner"
+ style={
+ Object {
+ "backgroundColor": "#b0d513",
+ }
+ }
+ />
+ </span>
+ B
+ </ButtonLink>
+ </Tooltip>
+ <Tooltip
+ key="3"
+ overlay="component_measures.legend.help"
+ >
+ <ButtonLink
+ className="little-padded-bottom"
+ onClick={[Function]}
+ type="button"
+ >
+ <span
+ className="color-box-legend-rect"
+ style={
+ Object {
+ "borderColor": "#eabe06",
+ }
+ }
+ >
+ <span
+ className="color-box-legend-rect-inner"
+ style={
+ Object {
+ "backgroundColor": "#eabe06",
+ }
+ }
+ />
+ </span>
+ C
+ </ButtonLink>
+ </Tooltip>
+ <Tooltip
+ key="4"
+ overlay="component_measures.legend.help"
+ >
+ <ButtonLink
+ className="little-padded-bottom"
+ onClick={[Function]}
+ type="button"
+ >
+ <span
+ className="color-box-legend-rect"
+ style={
+ Object {
+ "borderColor": "#ed7d20",
+ }
+ }
+ >
+ <span
+ className="color-box-legend-rect-inner"
+ style={
+ Object {
+ "backgroundColor": "#ed7d20",
+ }
+ }
+ />
+ </span>
+ D
+ </ButtonLink>
+ </Tooltip>
+ <Tooltip
+ key="5"
+ overlay="component_measures.legend.help"
+ >
+ <ButtonLink
+ className="little-padded-bottom"
+ onClick={[Function]}
+ type="button"
+ >
+ <span
+ className="color-box-legend-rect"
+ style={
+ Object {
+ "borderColor": "#d4333f",
+ }
+ }
+ >
+ <span
+ className="color-box-legend-rect-inner"
+ style={
+ Object {
+ "backgroundColor": "#d4333f",
+ }
+ }
+ />
+ </span>
+ E
+ </ButtonLink>
+ </Tooltip>
+</div>
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.
+ */
+import { Project } from '../../apps/projects/types';
+import { ComponentQualifier } from '../../types/component';
+
+export function mockProject(overrides: Partial<Project> = {}): Project {
+ return {
+ key: 'foo',
+ name: 'Foo',
+ measures: {},
+ qualifier: ComponentQualifier.Project,
+ tags: [],
+ visibility: 'public',
+ ...overrides
+ };
+}
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.legend.help=Click to toggle visibility.
component_measures.no_history=There isn't enough data to generate an activity graph.
component_measures.not_found=The requested measure was not found.
component_measures.empty=No measures.