diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2020-12-03 11:56:59 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2020-12-04 20:06:50 +0000 |
commit | 17439910a22be35c8f4a9d601ab8a4b524df287a (patch) | |
tree | 8c582c96c295832b7ea66a598065f1513efdb9f4 /server/sonar-web/src/main/js/apps/projects | |
parent | 9676f33bfcdeb599e26de963a938e33e753261fe (diff) | |
download | sonarqube-17439910a22be35c8f4a9d601ab8a4b524df287a.tar.gz sonarqube-17439910a22be35c8f4a9d601ab8a4b524df287a.zip |
SONAR-11556 Make bubblechart legend actionable
Diffstat (limited to 'server/sonar-web/src/main/js/apps/projects')
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> |