aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/projects
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2020-12-03 11:56:59 +0100
committersonartech <sonartech@sonarsource.com>2020-12-04 20:06:50 +0000
commit17439910a22be35c8f4a9d601ab8a4b524df287a (patch)
tree8c582c96c295832b7ea66a598065f1513efdb9f4 /server/sonar-web/src/main/js/apps/projects
parent9676f33bfcdeb599e26de963a938e33e753261fe (diff)
downloadsonarqube-17439910a22be35c8f4a9d601ab8a4b524df287a.tar.gz
sonarqube-17439910a22be35c8f4a9d601ab8a4b524df287a.zip
SONAR-11556 Make bubblechart legend actionable
Diffstat (limited to 'server/sonar-web/src/main/js/apps/projects')
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx87
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx51
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx57
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap60
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap2
6 files changed, 218 insertions, 74 deletions
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx
index 161202d0c6b..e40860b2279 100644
--- a/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx
@@ -23,6 +23,7 @@ import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
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';
@@ -45,7 +46,15 @@ interface Props {
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) : '–';
@@ -102,33 +111,51 @@ export default class Risk extends React.PureComponent<Props> {
);
}
- 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);
@@ -166,7 +193,11 @@ export default class Risk extends React.PureComponent<Props> {
'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>
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx
index 55f0ec925dd..cf7fe2f6c14 100644
--- a/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx
@@ -23,6 +23,7 @@ import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
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';
@@ -46,7 +47,15 @@ interface Props {
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) : '–';
@@ -96,8 +105,15 @@ export default class SimpleBubbleChart extends React.PureComponent<Props> {
);
}
+ 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)
@@ -111,6 +127,12 @@ export default class SimpleBubbleChart extends React.PureComponent<Props> {
? 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,
@@ -120,7 +142,8 @@ export default class SimpleBubbleChart extends React.PureComponent<Props> {
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);
@@ -159,7 +182,13 @@ export default class SimpleBubbleChart extends React.PureComponent<Props> {
'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>
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx
index b212b3be785..76f088fc9c6 100644
--- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx
@@ -19,20 +19,45 @@
*/
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}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx
index 00a64d12e9e..831439af07e 100644
--- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx
@@ -19,37 +19,42 @@
*/
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}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap
index 381818cef6b..e62a6b6a11e 100644
--- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap
@@ -13,7 +13,7 @@ exports[`renders 1`] = `
items={
Array [
Object {
- "color": undefined,
+ "color": "#eabe06",
"key": "foo",
"link": Object {
"pathname": "/dashboard",
@@ -36,6 +36,56 @@ exports[`renders 1`] = `
<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>
<div>
@@ -46,7 +96,7 @@ exports[`renders 1`] = `
<div>
metric.coverage.name
:
- 53.5%
+ –
</div>
<div>
metric.sqale_index.name
@@ -56,11 +106,11 @@ exports[`renders 1`] = `
<div>
metric.ncloc.name
:
- 1.7short_number_suffix.k
+ –
</div>
</div>,
"x": 0,
- "y": 53.5,
+ "y": 0,
},
]
}
@@ -120,6 +170,8 @@ exports[`renders 1`] = `
component_measures.legend.size_x.metric.ncloc.name
<ColorRatingsLegend
className="big-spacer-top"
+ filters={Object {}}
+ onRatingClick={[Function]}
/>
</div>
</div>
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap
index 7d0403e0300..bc2daa21d57 100644
--- a/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap
@@ -162,6 +162,8 @@ exports[`renders 1`] = `
component_measures.legend.size_x.metric.ncloc.name
<ColorRatingsLegend
className="big-spacer-top"
+ filters={Object {}}
+ onRatingClick={[Function]}
/>
</div>
</div>