]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11538 Explain bubble charts only take into account 1st 500 files
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Tue, 24 Aug 2021 08:44:41 +0000 (10:44 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 26 Aug 2021 20:04:29 +0000 (20:04 +0000)
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureOverview-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureOverview-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx
server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/BubbleChart-test.tsx
server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/BubbleChart-test.tsx.snap
server/sonar-web/src/main/js/apps/component-measures/utils.ts
server/sonar-web/src/main/js/helpers/mocks/component.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/testMocks.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 9df6b91c619e069f8f5e351855def3ae9368aa7c..abc99393977d58c94c4f11325886208645d6eea8 100644 (file)
@@ -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<Props, State> {
   mounted = false;
   state: State = { components: [] };
@@ -92,7 +96,7 @@ export default class MeasureOverview extends React.PureComponent<Props, State> {
       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<Props, State> {
         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<Props, State> {
   };
 
   renderContent() {
-    const { branchLike, component } = this.props;
+    const { branchLike, component, domain, metrics } = this.props;
+    const { paging } = this.state;
+
     if (isFileType(component)) {
       return (
         <div className="measure-details-viewer">
@@ -130,21 +134,21 @@ export default class MeasureOverview extends React.PureComponent<Props, State> {
 
     return (
       <BubbleChart
-        component={this.props.component}
+        component={component}
         components={this.state.components}
-        domain={this.props.domain}
-        metrics={this.props.metrics}
+        domain={domain}
+        metrics={metrics}
+        paging={paging}
         updateSelected={this.props.updateSelected}
       />
     );
   }
 
   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 (
-      <div className={this.props.className}>
+      <div className={className}>
         <div className="layout-page-header-panel layout-page-main-header">
           <A11ySkipTarget anchor="measures_main" />
 
@@ -165,7 +169,6 @@ export default class MeasureOverview extends React.PureComponent<Props, State> {
                   <PageActions
                     componentQualifier={rootComponent.qualifier}
                     current={this.state.components.length}
-                    total={paging && paging.total}
                   />
                 }
               />
@@ -178,8 +181,8 @@ export default class MeasureOverview extends React.PureComponent<Props, State> {
               <LeakPeriodLegend className="pull-right" component={component} period={leakPeriod} />
             )}
           </div>
-          <DeferredSpinner loading={this.props.loading} />
-          {!this.props.loading && this.renderContent()}
+          <DeferredSpinner loading={loading} />
+          {!loading && this.renderContent()}
         </div>
       </div>
     );
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 (file)
index 0000000..77923e3
--- /dev/null
@@ -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<MeasureOverview['props']> = {}) {
+  return shallow<MeasureOverview>(
+    <MeasureOverview
+      component={mockComponentMeasure()}
+      domain="Reliability"
+      loading={false}
+      metrics={keyBy(
+        [
+          mockMetric({ key: MetricKey.ncloc, type: 'INT' }),
+          mockMetric({
+            key: MetricKey.reliability_remediation_effort,
+            type: 'INT'
+          }),
+          mockMetric({ key: MetricKey.bugs, type: 'INT' }),
+          mockMetric({
+            key: MetricKey.reliability_rating,
+            type: 'DATA'
+          })
+        ],
+        m => 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 (file)
index 0000000..170556d
--- /dev/null
@@ -0,0 +1,428 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+<div>
+  <div
+    className="layout-page-header-panel layout-page-main-header"
+  >
+    <A11ySkipTarget
+      anchor="measures_main"
+    />
+    <div
+      className="layout-page-header-panel-inner layout-page-main-header-inner"
+    >
+      <div
+        className="layout-page-main-inner"
+      >
+        <MeasureContentHeader
+          left={
+            <Breadcrumbs
+              backToFirst={true}
+              className="text-ellipsis"
+              component={
+                Object {
+                  "key": "foo",
+                  "measures": Array [
+                    Object {
+                      "bestValue": false,
+                      "metric": "bugs",
+                      "value": "12",
+                    },
+                  ],
+                  "name": "Foo",
+                  "qualifier": "TRK",
+                }
+              }
+              handleSelect={[MockFunction]}
+              rootComponent={
+                Object {
+                  "key": "foo",
+                  "measures": Array [
+                    Object {
+                      "bestValue": false,
+                      "metric": "bugs",
+                      "value": "12",
+                    },
+                  ],
+                  "name": "Foo",
+                  "qualifier": "TRK",
+                }
+              }
+            />
+          }
+          right={
+            <PageActions
+              componentQualifier="TRK"
+              current={0}
+            />
+          }
+        />
+      </div>
+    </div>
+  </div>
+  <div
+    className="layout-page-main-inner measure-details-content"
+  >
+    <div
+      className="clearfix big-spacer-bottom"
+    />
+    <DeferredSpinner
+      loading={false}
+    />
+    <BubbleChart
+      component={
+        Object {
+          "key": "foo",
+          "measures": Array [
+            Object {
+              "bestValue": false,
+              "metric": "bugs",
+              "value": "12",
+            },
+          ],
+          "name": "Foo",
+          "qualifier": "TRK",
+        }
+      }
+      components={Array []}
+      domain="Reliability"
+      metrics={
+        Object {
+          "bugs": Object {
+            "id": "bugs",
+            "key": "bugs",
+            "name": "Bugs",
+            "type": "INT",
+          },
+          "ncloc": Object {
+            "id": "ncloc",
+            "key": "ncloc",
+            "name": "Ncloc",
+            "type": "INT",
+          },
+          "reliability_rating": Object {
+            "id": "reliability_rating",
+            "key": "reliability_rating",
+            "name": "Reliability_rating",
+            "type": "DATA",
+          },
+          "reliability_remediation_effort": Object {
+            "id": "reliability_remediation_effort",
+            "key": "reliability_remediation_effort",
+            "name": "Reliability_remediation_effort",
+            "type": "INT",
+          },
+        }
+      }
+      updateSelected={[MockFunction]}
+    />
+  </div>
+</div>
+`;
+
+exports[`should render correctly: has leak period 1`] = `
+<div>
+  <div
+    className="layout-page-header-panel layout-page-main-header"
+  >
+    <A11ySkipTarget
+      anchor="measures_main"
+    />
+    <div
+      className="layout-page-header-panel-inner layout-page-main-header-inner"
+    >
+      <div
+        className="layout-page-main-inner"
+      >
+        <MeasureContentHeader
+          left={
+            <Breadcrumbs
+              backToFirst={true}
+              branchLike={
+                Object {
+                  "analysisDate": "2018-01-01",
+                  "excludedFromPurge": true,
+                  "isMain": false,
+                  "name": "branch-6.7",
+                }
+              }
+              className="text-ellipsis"
+              component={
+                Object {
+                  "key": "foo",
+                  "measures": Array [
+                    Object {
+                      "bestValue": false,
+                      "metric": "bugs",
+                      "value": "12",
+                    },
+                  ],
+                  "name": "Foo",
+                  "qualifier": "TRK",
+                }
+              }
+              handleSelect={[MockFunction]}
+              rootComponent={
+                Object {
+                  "key": "foo",
+                  "measures": Array [
+                    Object {
+                      "bestValue": false,
+                      "metric": "bugs",
+                      "value": "12",
+                    },
+                  ],
+                  "name": "Foo",
+                  "qualifier": "TRK",
+                }
+              }
+            />
+          }
+          right={
+            <PageActions
+              componentQualifier="TRK"
+              current={0}
+            />
+          }
+        />
+      </div>
+    </div>
+  </div>
+  <div
+    className="layout-page-main-inner measure-details-content"
+  >
+    <div
+      className="clearfix big-spacer-bottom"
+    >
+      <InjectIntl(LeakPeriodLegend)
+        className="pull-right"
+        component={
+          Object {
+            "key": "foo",
+            "measures": Array [
+              Object {
+                "bestValue": false,
+                "metric": "bugs",
+                "value": "12",
+              },
+            ],
+            "name": "Foo",
+            "qualifier": "TRK",
+          }
+        }
+        period={
+          Object {
+            "date": "2019-04-23T02:12:32+0100",
+            "index": 0,
+            "mode": "previous_version",
+          }
+        }
+      />
+    </div>
+    <DeferredSpinner
+      loading={false}
+    />
+    <BubbleChart
+      component={
+        Object {
+          "key": "foo",
+          "measures": Array [
+            Object {
+              "bestValue": false,
+              "metric": "bugs",
+              "value": "12",
+            },
+          ],
+          "name": "Foo",
+          "qualifier": "TRK",
+        }
+      }
+      components={Array []}
+      domain="Reliability"
+      metrics={
+        Object {
+          "bugs": Object {
+            "id": "bugs",
+            "key": "bugs",
+            "name": "Bugs",
+            "type": "INT",
+          },
+          "ncloc": Object {
+            "id": "ncloc",
+            "key": "ncloc",
+            "name": "Ncloc",
+            "type": "INT",
+          },
+          "reliability_rating": Object {
+            "id": "reliability_rating",
+            "key": "reliability_rating",
+            "name": "Reliability_rating",
+            "type": "DATA",
+          },
+          "reliability_remediation_effort": Object {
+            "id": "reliability_remediation_effort",
+            "key": "reliability_remediation_effort",
+            "name": "Reliability_remediation_effort",
+            "type": "INT",
+          },
+        }
+      }
+      updateSelected={[MockFunction]}
+    />
+  </div>
+</div>
+`;
+
+exports[`should render correctly: is file 1`] = `
+<div>
+  <div
+    className="layout-page-header-panel layout-page-main-header"
+  >
+    <A11ySkipTarget
+      anchor="measures_main"
+    />
+    <div
+      className="layout-page-header-panel-inner layout-page-main-header-inner"
+    >
+      <div
+        className="layout-page-main-inner"
+      >
+        <MeasureContentHeader
+          left={
+            <Breadcrumbs
+              backToFirst={true}
+              className="text-ellipsis"
+              component={
+                Object {
+                  "key": "foo:src/index.tsx",
+                  "measures": Array [
+                    Object {
+                      "bestValue": false,
+                      "metric": "bugs",
+                      "value": "1",
+                    },
+                  ],
+                  "name": "index.tsx",
+                  "path": "src/index.tsx",
+                  "qualifier": "FIL",
+                }
+              }
+              handleSelect={[MockFunction]}
+              rootComponent={
+                Object {
+                  "key": "foo",
+                  "measures": Array [
+                    Object {
+                      "bestValue": false,
+                      "metric": "bugs",
+                      "value": "12",
+                    },
+                  ],
+                  "name": "Foo",
+                  "qualifier": "TRK",
+                }
+              }
+            />
+          }
+          right={
+            <PageActions
+              componentQualifier="TRK"
+              current={0}
+            />
+          }
+        />
+      </div>
+    </div>
+  </div>
+  <div
+    className="layout-page-main-inner measure-details-content"
+  >
+    <div
+      className="clearfix big-spacer-bottom"
+    />
+    <DeferredSpinner
+      loading={false}
+    />
+    <div
+      className="measure-details-viewer"
+    >
+      <SourceViewer
+        component="foo:src/index.tsx"
+      />
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correctly: loading 1`] = `
+<div>
+  <div
+    className="layout-page-header-panel layout-page-main-header"
+  >
+    <A11ySkipTarget
+      anchor="measures_main"
+    />
+    <div
+      className="layout-page-header-panel-inner layout-page-main-header-inner"
+    >
+      <div
+        className="layout-page-main-inner"
+      >
+        <MeasureContentHeader
+          left={
+            <Breadcrumbs
+              backToFirst={true}
+              className="text-ellipsis"
+              component={
+                Object {
+                  "key": "foo",
+                  "measures": Array [
+                    Object {
+                      "bestValue": false,
+                      "metric": "bugs",
+                      "value": "12",
+                    },
+                  ],
+                  "name": "Foo",
+                  "qualifier": "TRK",
+                }
+              }
+              handleSelect={[MockFunction]}
+              rootComponent={
+                Object {
+                  "key": "foo",
+                  "measures": Array [
+                    Object {
+                      "bestValue": false,
+                      "metric": "bugs",
+                      "value": "12",
+                    },
+                  ],
+                  "name": "Foo",
+                  "qualifier": "TRK",
+                }
+              }
+            />
+          }
+          right={
+            <PageActions
+              componentQualifier="TRK"
+              current={0}
+            />
+          }
+        />
+      </div>
+    </div>
+  </div>
+  <div
+    className="layout-page-main-inner measure-details-content"
+  >
+    <div
+      className="clearfix big-spacer-bottom"
+    />
+    <DeferredSpinner
+      loading={true}
+    />
+  </div>
+</div>
+`;
index 14997bd80e2001c9a50eaddec4fa6b4569e1871f..2fb770d4722b45c2e9054629aec0a8834d371288 100644 (file)
@@ -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<T.Metric>;
+  paging?: T.Paging;
   updateSelected: (component: string) => void;
 }
 
@@ -170,6 +176,7 @@ export default class BubbleChart extends React.PureComponent<Props, State> {
 
   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<Props, State> {
     return (
       <div className="measure-overview-bubble-chart-header">
         <span className="measure-overview-bubble-chart-title">
-          <span className="text-middle">{title}</span>
-          <HelpTooltip className="spacer-left" overlay={this.getDescription(domain)} />
+          <div className="display-flex-center">
+            {title}
+            <HelpTooltip className="spacer-left" overlay={this.getDescription(domain)} />
+          </div>
+
+          {paging?.total && paging?.total > BUBBLES_FETCH_LIMIT && (
+            <div className="note spacer-top">
+              ({translate('component_measures.legend.only_first_500_files')})
+            </div>
+          )}
         </span>
         <span className="measure-overview-bubble-chart-legend">
           <span className="note">
index 7c1eca4cf52554c582470fea3f963a4123535dbf..4673a98f9434e84ec2111199786992bb0a76cf22 100644 (file)
@@ -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<BubbleChart['props']> = {}) {
       ]}
       domain="Security"
       metrics={metrics}
+      paging={mockPaging({ total: 100 })}
       updateSelected={jest.fn()}
       {...overrides}
     />
index cadea7ed64db03c99bc38f62de35cee8c5db6d01..e8c8d464e908738c5f60e158bf96e5ca470657e1 100644 (file)
@@ -10,15 +10,15 @@ exports[`should render correctly: all on x=0 1`] = `
     <span
       className="measure-overview-bubble-chart-title"
     >
-      <span
-        className="text-middle"
+      <div
+        className="display-flex-center"
       >
         component_measures.domain_x_overview.Security
-      </span>
-      <HelpTooltip
-        className="spacer-left"
-        overlay={null}
-      />
+        <HelpTooltip
+          className="spacer-left"
+          overlay={null}
+        />
+      </div>
     </span>
     <span
       className="measure-overview-bubble-chart-legend"
@@ -66,7 +66,7 @@ exports[`should render correctly: all on x=0 1`] = `
                     "id": "ncloc",
                     "key": "ncloc",
                     "name": "Ncloc",
-                    "type": "NUMBER",
+                    "type": "INT",
                   },
                   "period": Object {
                     "bestValue": true,
@@ -82,7 +82,7 @@ exports[`should render correctly: all on x=0 1`] = `
                     "id": "security_remediation_effort",
                     "key": "security_remediation_effort",
                     "name": "Security_remediation_effort",
-                    "type": "NUMBER",
+                    "type": "DATA",
                   },
                   "period": Object {
                     "bestValue": true,
@@ -98,7 +98,7 @@ exports[`should render correctly: all on x=0 1`] = `
                     "id": "vulnerabilities",
                     "key": "vulnerabilities",
                     "name": "Vulnerabilities",
-                    "type": "NUMBER",
+                    "type": "INT",
                   },
                   "period": Object {
                     "bestValue": true,
@@ -204,16 +204,211 @@ exports[`should render correctly: default 1`] = `
     <span
       className="measure-overview-bubble-chart-title"
     >
-      <span
-        className="text-middle"
+      <div
+        className="display-flex-center"
       >
         component_measures.domain_x_overview.Security
+        <HelpTooltip
+          className="spacer-left"
+          overlay={null}
+        />
+      </div>
+    </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>
-      <HelpTooltip
-        className="spacer-left"
-        overlay={null}
+      <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": "INT",
+                  },
+                  "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": "DATA",
+                  },
+                  "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": "INT",
+                  },
+                  "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>
+`;
+
+exports[`should render correctly: only showing first 500 files 1`] = `
+<div
+  className="measure-overview-bubble-chart"
+>
+  <div
+    className="measure-overview-bubble-chart-header"
+  >
+    <span
+      className="measure-overview-bubble-chart-title"
+    >
+      <div
+        className="display-flex-center"
+      >
+        component_measures.domain_x_overview.Security
+        <HelpTooltip
+          className="spacer-left"
+          overlay={null}
+        />
+      </div>
+      <div
+        className="note spacer-top"
+      >
+        (
+        component_measures.legend.only_first_500_files
+        )
+      </div>
+    </span>
     <span
       className="measure-overview-bubble-chart-legend"
     >
@@ -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,
index d2e7763e86b98c67e84a02acf247f423265995ea..a36aaf2a6133319f85c24a478c6ee20142e214af 100644 (file)
@@ -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 (file)
index 0000000..4bcbde0
--- /dev/null
@@ -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> = {}
+): T.ComponentMeasureEnhanced {
+  return {
+    ...mockComponentMeasure(false, overrides as T.ComponentMeasure),
+    leak: undefined,
+    measures: [mockMeasureEnhanced()],
+    value: undefined,
+    ...overrides
+  };
+}
index 5dbbc378f681b5604cd5f2e5598450467bd48027..d4bdd52018446e96a5c559683a6379c9f4390a66 100644 (file)
@@ -790,3 +790,12 @@ export function mockRef(
     }
   } as React.RefObject<HTMLElement>;
 }
+
+export function mockPaging(overrides: Partial<T.Paging> = {}): T.Paging {
+  return {
+    pageIndex: 1,
+    pageSize: 100,
+    total: 1000,
+    ...overrides
+  };
+}
index 9b9fd4368d5e5c2de0a7d81facae5fd6016d31b4..90bcc63c6960b2d495a4db77b2a3b92e257b01ac 100644 (file)
@@ -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.