From: Wouter Admiraal Date: Tue, 24 Aug 2021 08:44:41 +0000 (+0200) Subject: SONAR-11538 Explain bubble charts only take into account 1st 500 files X-Git-Tag: 9.1.0.47736~86 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=3127e9e6821be46440f1ad89b5e56c47f10fd38b;p=sonarqube.git SONAR-11538 Explain bubble charts only take into account 1st 500 files --- diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx index 9df6b91c619..abc99393977 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx @@ -26,7 +26,13 @@ import PageActions from '../../../components/ui/PageActions'; import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; import { BranchLike } from '../../../types/branch-like'; import BubbleChart from '../drilldown/BubbleChart'; -import { enhanceComponent, getBubbleMetrics, hasFullMeasures, isFileType } from '../utils'; +import { + BUBBLES_FETCH_LIMIT, + enhanceComponent, + getBubbleMetrics, + hasFullMeasures, + isFileType +} from '../utils'; import Breadcrumbs from './Breadcrumbs'; import LeakPeriodLegend from './LeakPeriodLegend'; import MeasureContentHeader from './MeasureContentHeader'; @@ -50,8 +56,6 @@ interface State { paging?: T.Paging; } -const BUBBLES_LIMIT = 500; - export default class MeasureOverview extends React.PureComponent { mounted = false; state: State = { components: [] }; @@ -92,7 +96,7 @@ export default class MeasureOverview extends React.PureComponent { s: 'metric', metricSort: size.key, asc: false, - ps: BUBBLES_LIMIT + ps: BUBBLES_FETCH_LIMIT }; this.props.updateLoading({ bubbles: true }); @@ -101,9 +105,7 @@ export default class MeasureOverview extends React.PureComponent { if (domain === this.props.domain) { if (this.mounted) { this.setState({ - components: r.components.map(component => - enhanceComponent(component, undefined, metrics) - ), + components: r.components.map(c => enhanceComponent(c, undefined, metrics)), paging: r.paging }); } @@ -115,7 +117,9 @@ export default class MeasureOverview extends React.PureComponent { }; renderContent() { - const { branchLike, component } = this.props; + const { branchLike, component, domain, metrics } = this.props; + const { paging } = this.state; + if (isFileType(component)) { return (
@@ -130,21 +134,21 @@ export default class MeasureOverview extends React.PureComponent { return ( ); } render() { - const { branchLike, component, leakPeriod, rootComponent } = this.props; - const { paging } = this.state; + const { branchLike, className, component, leakPeriod, loading, rootComponent } = this.props; const displayLeak = hasFullMeasures(branchLike); return ( -
+
@@ -165,7 +169,6 @@ export default class MeasureOverview extends React.PureComponent { } /> @@ -178,8 +181,8 @@ export default class MeasureOverview extends React.PureComponent { )}
- - {!this.props.loading && this.renderContent()} + + {!loading && this.renderContent()}
); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureOverview-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureOverview-test.tsx new file mode 100644 index 00000000000..77923e3f982 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureOverview-test.tsx @@ -0,0 +1,132 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { getComponentLeaves } from '../../../../api/components'; +import { mockBranch } from '../../../../helpers/mocks/branch-like'; +import { mockComponentMeasureEnhanced } from '../../../../helpers/mocks/component'; +import { + mockComponentMeasure, + mockMeasure, + mockMeasureEnhanced, + mockMetric, + mockPeriod +} from '../../../../helpers/testMocks'; +import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { MetricKey } from '../../../../types/metrics'; +import { BUBBLES_FETCH_LIMIT } from '../../utils'; +import MeasureOverview from '../MeasureOverview'; + +jest.mock('../../../../api/components', () => ({ + getComponentLeaves: jest + .fn() + .mockResolvedValue({ components: [], paging: { total: 200, pageIndex: 1, pageSize: 100 } }) +})); + +beforeEach(jest.clearAllMocks); + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ loading: true })).toMatchSnapshot('loading'); + expect(shallowRender({ leakPeriod: mockPeriod(), branchLike: mockBranch() })).toMatchSnapshot( + 'has leak period' + ); + expect(shallowRender({ component: mockComponentMeasure(true) })).toMatchSnapshot('is file'); +}); + +it('should correctly enhance leaf components', async () => { + (getComponentLeaves as jest.Mock).mockResolvedValueOnce({ + components: [ + mockComponentMeasure(false, { measures: [mockMeasure({ metric: MetricKey.bugs })] }) + ] + }); + const updateLoading = jest.fn(); + const wrapper = shallowRender({ updateLoading }); + + expect(updateLoading).toBeCalledWith({ bubbles: true }); + expect(getComponentLeaves).toHaveBeenCalledWith( + 'foo', + [ + MetricKey.ncloc, + MetricKey.reliability_remediation_effort, + MetricKey.bugs, + MetricKey.reliability_rating + ], + expect.objectContaining({ metricSort: MetricKey.bugs, s: 'metric', ps: BUBBLES_FETCH_LIMIT }) + ); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().components).toEqual([ + mockComponentMeasureEnhanced({ + measures: [ + mockMeasureEnhanced({ + leak: '1.0', + metric: mockMetric({ key: MetricKey.bugs, type: 'INT' }) + }) + ] + }) + ]); + expect(updateLoading).toHaveBeenLastCalledWith({ bubbles: false }); +}); + +it('should not enhance file components', () => { + shallowRender({ component: mockComponentMeasure(true) }); + expect(getComponentLeaves).not.toHaveBeenCalled(); +}); + +it('should correctly flag itself as (un)mounted', () => { + const wrapper = shallowRender(); + const instance = wrapper.instance(); + expect(instance.mounted).toBe(true); + wrapper.unmount(); + expect(instance.mounted).toBe(false); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + m.key + )} + updateLoading={jest.fn()} + updateSelected={jest.fn()} + rootComponent={mockComponentMeasure()} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureOverview-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureOverview-test.tsx.snap new file mode 100644 index 00000000000..170556dc4d0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureOverview-test.tsx.snap @@ -0,0 +1,428 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` +
+
+ +
+
+ + } + right={ + + } + /> +
+
+
+
+
+ + +
+
+`; + +exports[`should render correctly: has leak period 1`] = ` +
+
+ +
+
+ + } + right={ + + } + /> +
+
+
+
+
+ +
+ + +
+
+`; + +exports[`should render correctly: is file 1`] = ` +
+
+ +
+
+ + } + right={ + + } + /> +
+
+
+
+
+ +
+ +
+
+
+`; + +exports[`should render correctly: loading 1`] = ` +
+
+ +
+
+ + } + right={ + + } + /> +
+
+
+
+
+ +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx index 14997bd80e2..2fb770d4722 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx @@ -30,7 +30,12 @@ import { } from '../../../helpers/l10n'; import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; import { isDefined } from '../../../helpers/types'; -import { getBubbleMetrics, getBubbleYDomain, isProjectOverview } from '../utils'; +import { + BUBBLES_FETCH_LIMIT, + getBubbleMetrics, + getBubbleYDomain, + isProjectOverview +} from '../utils'; import EmptyResult from './EmptyResult'; const HEIGHT = 500; @@ -40,6 +45,7 @@ interface Props { components: T.ComponentMeasureEnhanced[]; domain: string; metrics: T.Dict; + paging?: T.Paging; updateSelected: (component: string) => void; } @@ -170,6 +176,7 @@ export default class BubbleChart extends React.PureComponent { renderChartHeader(domain: string, sizeMetric: T.Metric, colorsMetric?: T.Metric[]) { const { ratingFilters } = this.state; + const { paging } = this.props; const title = isProjectOverview(domain) ? translate('component_measures.overview', domain, 'title') @@ -180,8 +187,16 @@ export default class BubbleChart extends React.PureComponent { return (
- {title} - +
+ {title} + +
+ + {paging?.total && paging?.total > BUBBLES_FETCH_LIMIT && ( +
+ ({translate('component_measures.legend.only_first_500_files')}) +
+ )}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/BubbleChart-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/BubbleChart-test.tsx index 7c1eca4cf52..4673a98f943 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/BubbleChart-test.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/BubbleChart-test.tsx @@ -21,16 +21,21 @@ import { shallow } from 'enzyme'; import { keyBy } from 'lodash'; import * as React from 'react'; import OriginalBubbleChart from '../../../../components/charts/BubbleChart'; -import { mockComponentMeasure, mockMeasure, mockMetric } from '../../../../helpers/testMocks'; +import { + mockComponentMeasure, + mockMeasure, + mockMetric, + mockPaging +} 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.ncloc, type: 'INT' }), + mockMetric({ key: MetricKey.security_remediation_effort, type: 'DATA' }), + mockMetric({ key: MetricKey.vulnerabilities, type: 'INT' }), mockMetric({ key: MetricKey.security_rating, type: 'RATING' }) ], m => m.key @@ -38,6 +43,9 @@ const metrics = keyBy( it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ paging: mockPaging({ total: 1000 }) })).toMatchSnapshot( + 'only showing first 500 files' + ); expect( shallowRender({ components: [ @@ -88,6 +96,7 @@ function shallowRender(overrides: Partial = {}) { ]} domain="Security" metrics={metrics} + paging={mockPaging({ total: 100 })} updateSelected={jest.fn()} {...overrides} /> diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/BubbleChart-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/BubbleChart-test.tsx.snap index cadea7ed64d..e8c8d464e90 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/BubbleChart-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/BubbleChart-test.tsx.snap @@ -10,15 +10,15 @@ exports[`should render correctly: all on x=0 1`] = ` - component_measures.domain_x_overview.Security - - + +
- component_measures.domain_x_overview.Security + +
+ + + + + component_measures.legend.color_x.Security_rating + + component_measures.legend.size_x.Vulnerabilities - +
+
+ + + index.tsx +
+
+ + Ncloc: 236 +
+
+ + Security_remediation_effort: 10 +
+
+ + Vulnerabilities: 3 +
+
+ + Security_rating: B + +
, + "x": 236, + "y": 10, + }, + ] + } + onBubbleClick={[Function]} + padding={ + Array [ + 25, + 60, + 50, + 60, + ] + } + sizeRange={ + Array [ + 5, + 45, + ] + } + /> +
+
+ Ncloc +
+
+ Security_remediation_effort +
+
+`; + +exports[`should render correctly: only showing first 500 files 1`] = ` +
+
+ +
+ component_measures.domain_x_overview.Security + +
+
+ ( + component_measures.legend.only_first_500_files + ) +
+
@@ -260,7 +455,7 @@ exports[`should render correctly: default 1`] = ` "id": "ncloc", "key": "ncloc", "name": "Ncloc", - "type": "NUMBER", + "type": "INT", }, "period": Object { "bestValue": true, @@ -276,7 +471,7 @@ exports[`should render correctly: default 1`] = ` "id": "security_remediation_effort", "key": "security_remediation_effort", "name": "Security_remediation_effort", - "type": "NUMBER", + "type": "DATA", }, "period": Object { "bestValue": true, @@ -292,7 +487,7 @@ exports[`should render correctly: default 1`] = ` "id": "vulnerabilities", "key": "vulnerabilities", "name": "Vulnerabilities", - "type": "NUMBER", + "type": "INT", }, "period": Object { "bestValue": true, diff --git a/server/sonar-web/src/main/js/apps/component-measures/utils.ts b/server/sonar-web/src/main/js/apps/component-measures/utils.ts index d2e7763e86b..a36aaf2a613 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/utils.ts +++ b/server/sonar-web/src/main/js/apps/component-measures/utils.ts @@ -31,6 +31,7 @@ import { domains } from './config/domains'; export type View = 'list' | 'tree' | 'treemap'; +export const BUBBLES_FETCH_LIMIT = 500; export const PROJECT_OVERVEW = 'project_overview'; export const DEFAULT_VIEW: View = 'tree'; export const DEFAULT_METRIC = PROJECT_OVERVEW; diff --git a/server/sonar-web/src/main/js/helpers/mocks/component.ts b/server/sonar-web/src/main/js/helpers/mocks/component.ts new file mode 100644 index 00000000000..4bcbde0cb11 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/mocks/component.ts @@ -0,0 +1,32 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { mockComponentMeasure, mockMeasureEnhanced } from '../testMocks'; + +export function mockComponentMeasureEnhanced( + overrides: Partial = {} +): T.ComponentMeasureEnhanced { + return { + ...mockComponentMeasure(false, overrides as T.ComponentMeasure), + leak: undefined, + measures: [mockMeasureEnhanced()], + value: undefined, + ...overrides + }; +} diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index 5dbbc378f68..d4bdd520184 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -790,3 +790,12 @@ export function mockRef( } } as React.RefObject; } + +export function mockPaging(overrides: Partial = {}): T.Paging { + return { + pageIndex: 1, + pageSize: 100, + total: 1000, + ...overrides + }; +} 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 9b9fd4368d5..90bcc63c696 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3169,6 +3169,7 @@ 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.legend.only_first_500_files=Only showing data for the first 500 files 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.