aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorstanislavh <stanislav.honcharov@sonarsource.com>2023-05-25 13:37:09 +0200
committersonartech <sonartech@sonarsource.com>2023-05-26 20:03:09 +0000
commit915462cb80552f7dbf99a06faa358df8401687c9 (patch)
tree9aa9e6958ebc36e26a7f41f6fb06217f5acebbb9
parent9443d2e8130ee0f81b0c5775f3bee1e197df79a0 (diff)
downloadsonarqube-915462cb80552f7dbf99a06faa358df8401687c9.tar.gz
sonarqube-915462cb80552f7dbf99a06faa358df8401687c9.zip
SONAR-19385 Add BubbleChart to components library
-rw-r--r--server/sonar-web/design-system/package.json5
-rw-r--r--server/sonar-web/design-system/src/components/BubbleChart.tsx445
-rw-r--r--server/sonar-web/design-system/src/components/Tooltip.tsx23
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/BubbleChart-test.tsx91
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/SearchSelectDropdown-test.tsx6
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/Tooltip-test.tsx9
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap2
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/__snapshots__/Histogram-test.tsx.snap3
-rw-r--r--server/sonar-web/design-system/src/components/index.ts1
-rw-r--r--server/sonar-web/design-system/src/index.ts3
-rw-r--r--server/sonar-web/design-system/src/types/charts.ts (renamed from server/sonar-web/src/main/js/components/charts/BubbleChart.css)41
-rw-r--r--server/sonar-web/design-system/src/types/index.ts23
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx14
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/BubbleChart-test.tsx86
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/BubbleChart-test.tsx.snap628
-rw-r--r--server/sonar-web/src/main/js/components/charts/BubbleChart.tsx377
-rw-r--r--server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx165
-rw-r--r--server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap127
-rw-r--r--server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/LanguageDistribution-test.tsx.snap1
-rw-r--r--server/sonar-web/yarn.lock5
20 files changed, 668 insertions, 1387 deletions
diff --git a/server/sonar-web/design-system/package.json b/server/sonar-web/design-system/package.json
index ff65cf5ab5b..07cbc22b88c 100644
--- a/server/sonar-web/design-system/package.json
+++ b/server/sonar-web/design-system/package.json
@@ -54,11 +54,13 @@
"clipboard": "2.0.11",
"d3-array": "3.2.3",
"d3-scale": "4.0.2",
+ "d3-selection": "3.0.0",
"d3-shape": "3.2.0",
+ "d3-zoom": "3.0.0",
"date-fns": "2.29.3",
"lodash": "4.17.21",
"react": "17.0.2",
- "react-day-picker": "8.6.0",
+ "react-day-picker": "8.7.1",
"react-dom": "17.0.2",
"react-helmet-async": "1.3.0",
"react-highlight-words": "0.20.0",
@@ -66,6 +68,7 @@
"react-modal": "3.16.1",
"react-router-dom": "6.10.0",
"react-select": "5.7.2",
+ "react-virtualized": "9.22.3",
"tailwindcss": "3.3.1"
},
"babelMacros": {
diff --git a/server/sonar-web/design-system/src/components/BubbleChart.tsx b/server/sonar-web/design-system/src/components/BubbleChart.tsx
new file mode 100644
index 00000000000..e35aba51308
--- /dev/null
+++ b/server/sonar-web/design-system/src/components/BubbleChart.tsx
@@ -0,0 +1,445 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import classNames from 'classnames';
+import { max, min } from 'd3-array';
+import { ScaleLinear, scaleLinear } from 'd3-scale';
+import { select } from 'd3-selection';
+import { D3ZoomEvent, ZoomBehavior, zoom, zoomIdentity } from 'd3-zoom';
+import { sortBy, uniq } from 'lodash';
+import * as React from 'react';
+import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
+import tw from 'twin.macro';
+import { themeColor, themeContrast } from '../helpers';
+import { BubbleColorVal } from '../types/charts';
+import { Note } from './Text';
+import Tooltip from './Tooltip';
+import { ButtonSecondary } from './buttons';
+
+const TICKS_COUNT = 5;
+
+interface BubbleItem<T> {
+ color?: BubbleColorVal;
+ data?: T;
+ key?: string;
+ size: number;
+ tooltip?: React.ReactNode;
+ x: number;
+ y: number;
+}
+
+export interface BubbleChartProps<T> {
+ displayXGrid?: boolean;
+ displayXTicks?: boolean;
+ displayYGrid?: boolean;
+ displayYTicks?: boolean;
+ formatXTick: (tick: number) => string;
+ formatYTick: (tick: number) => string;
+ height: number;
+ items: Array<BubbleItem<T>>;
+ onBubbleClick?: (ref?: T) => void;
+ padding: [number, number, number, number];
+ sizeDomain?: [number, number];
+ sizeRange?: [number, number];
+ xDomain?: [number, number];
+ yDomain?: [number, number];
+ zoomLabel?: string;
+ zoomResetLabel?: string;
+ zoomTooltipText?: string;
+}
+
+type Scale = ScaleLinear<number, number>;
+
+BubbleChart.defaultProps = {
+ displayXGrid: true,
+ displayXTicks: true,
+ displayYGrid: true,
+ displayYTicks: true,
+ formatXTick: (d: number) => String(d),
+ formatYTick: (d: number) => String(d),
+ padding: [10, 10, 10, 10],
+ sizeRange: [5, 45],
+};
+
+export function BubbleChart<T>(props: BubbleChartProps<T>) {
+ const {
+ padding,
+ height,
+ items,
+ xDomain,
+ yDomain,
+ sizeDomain,
+ sizeRange,
+ zoomResetLabel = 'Reset',
+ zoomTooltipText,
+ zoomLabel = 'Zoom',
+ displayXTicks,
+ displayYTicks,
+ displayXGrid,
+ displayYGrid,
+ formatXTick,
+ formatYTick,
+ } = props;
+ const [transform, setTransform] = React.useState({ x: 0, y: 0, k: 1 });
+ const nodeRef = React.useRef<SVGSVGElement>();
+ const zoomRef = React.useRef<ZoomBehavior<Element, unknown>>();
+ const zoomLevelLabel = `${Math.floor(transform.k * 100)}%`;
+
+ if (zoomRef.current && nodeRef.current) {
+ const rect = nodeRef.current.getBoundingClientRect();
+ zoomRef.current.translateExtent([
+ [0, 0],
+ [rect.width, rect.height],
+ ]);
+ }
+
+ const zoomed = React.useCallback(
+ (event: D3ZoomEvent<SVGSVGElement, void>) => {
+ const { x, y, k } = event.transform;
+ setTransform({
+ x: x + padding[3] * (k - 1),
+ y: y + padding[0] * (k - 1),
+ k,
+ });
+ },
+ [padding]
+ );
+
+ const boundNode = React.useCallback(
+ (node: SVGSVGElement) => {
+ nodeRef.current = node;
+ zoomRef.current = zoom().scaleExtent([1, 10]).on('zoom', zoomed);
+ select(nodeRef.current).call(zoomRef.current);
+ },
+ [zoomed]
+ );
+
+ const resetZoom = React.useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
+ e.stopPropagation();
+ e.preventDefault();
+ if (zoomRef.current && nodeRef.current) {
+ select(nodeRef.current).call(zoomRef.current.transform, zoomIdentity);
+ }
+ }, []);
+
+ const getXRange = React.useCallback(
+ (xScale: Scale, sizeScale: Scale, availableWidth: number) => {
+ const [x1, x2] = xScale.range();
+ const minX = min(items, (d) => xScale(d.x) - sizeScale(d.size)) ?? 0;
+ const maxX = max(items, (d) => xScale(d.x) + sizeScale(d.size)) ?? 0;
+ const dMinX = minX < 0 ? x1 - minX : x1;
+ const dMaxX = maxX > x2 ? maxX - x2 : 0;
+ return [dMinX, availableWidth - dMaxX];
+ },
+ [items]
+ );
+
+ const getYRange = React.useCallback(
+ (yScale: Scale, sizeScale: Scale, availableHeight: number) => {
+ const [y1, y2] = yScale.range();
+ const minY = min(items, (d) => yScale(d.y) - sizeScale(d.size)) ?? 0;
+ const maxY = max(items, (d) => yScale(d.y) + sizeScale(d.size)) ?? 0;
+ const dMinY = minY < 0 ? y2 - minY : y2;
+ const dMaxY = maxY > y1 ? maxY - y1 : 0;
+ return [availableHeight - dMaxY, dMinY];
+ },
+ [items]
+ );
+
+ const getTicks = React.useCallback(
+ (scale: Scale, format: (d: number) => string) => {
+ const zoomAmount = Math.ceil(transform.k);
+ const ticks = scale.ticks(TICKS_COUNT * zoomAmount).map((tick) => format(tick));
+ const uniqueTicksCount = uniq(ticks).length;
+ const ticksCount =
+ uniqueTicksCount < TICKS_COUNT * zoomAmount
+ ? uniqueTicksCount - 1
+ : TICKS_COUNT * zoomAmount;
+ return scale.ticks(ticksCount);
+ },
+ [transform]
+ );
+
+ const renderXGrid = React.useCallback(
+ (ticks: number[], xScale: Scale, yScale: Scale) => {
+ if (!displayXGrid) {
+ return null;
+ }
+
+ const lines = ticks.map((tick, index) => {
+ const x = xScale(tick);
+ const [y1, y2] = yScale.range();
+ return (
+ <BubbleChartGrid
+ // eslint-disable-next-line react/no-array-index-key
+ key={index}
+ x1={x * transform.k + transform.x}
+ x2={x * transform.k + transform.x}
+ y1={y1 * transform.k}
+ y2={transform.k > 1 ? 0 : y2}
+ />
+ );
+ });
+
+ return <g>{lines}</g>;
+ },
+ [transform, displayXGrid]
+ );
+
+ const renderYGrid = React.useCallback(
+ (ticks: number[], xScale: Scale, yScale: Scale) => {
+ if (!displayYGrid) {
+ return null;
+ }
+
+ const lines = ticks.map((tick, index) => {
+ const y = yScale(tick);
+ const [x1, x2] = xScale.range();
+ return (
+ <BubbleChartGrid
+ // eslint-disable-next-line react/no-array-index-key
+ key={index}
+ x1={transform.k > 1 ? 0 : x1}
+ x2={x2 * transform.k}
+ y1={y * transform.k + transform.y}
+ y2={y * transform.k + transform.y}
+ />
+ );
+ });
+
+ return <g>{lines}</g>;
+ },
+ [displayYGrid, transform]
+ );
+
+ const renderXTicks = React.useCallback(
+ (xTicks: number[], xScale: Scale, yScale: Scale) => {
+ if (!displayXTicks) {
+ return null;
+ }
+
+ const ticks = xTicks.map((tick, index) => {
+ const x = xScale(tick) * transform.k + transform.x;
+ const y = yScale.range()[0];
+ const innerText = formatXTick(tick);
+ // as we modified the `x` using `transform`, check that it is inside the range again
+ return x > 0 && x < xScale.range()[1] ? (
+ // eslint-disable-next-line react/no-array-index-key
+ <BubbleChartTick dy="1.5em" key={index} style={{ '--align': 'middle' }} x={x} y={y}>
+ {innerText}
+ </BubbleChartTick>
+ ) : null;
+ });
+
+ return <g>{ticks}</g>;
+ },
+ [displayXTicks, formatXTick, transform]
+ );
+
+ const renderYTicks = React.useCallback(
+ (yTicks: number[], xScale: Scale, yScale: Scale) => {
+ if (!displayYTicks) {
+ return null;
+ }
+
+ const ticks = yTicks.map((tick, index) => {
+ const x = xScale.range()[0];
+ const y = yScale(tick) * transform.k + transform.y;
+ const innerText = formatYTick(tick);
+ // as we modified the `y` using `transform`, check that it is inside the range again
+ return y > 0 && y < yScale.range()[0] ? (
+ <BubbleChartTick
+ dx="-0.5em"
+ dy="0.3em"
+ // eslint-disable-next-line react/no-array-index-key
+ key={index}
+ style={{ '--align': 'end' }}
+ x={x}
+ y={y}
+ >
+ {innerText}
+ </BubbleChartTick>
+ ) : null;
+ });
+
+ return <g>{ticks}</g>;
+ },
+ [displayYTicks, formatYTick, transform]
+ );
+
+ const renderChart = (width: number) => {
+ const availableWidth = width - padding[1] - padding[3];
+ const availableHeight = height - padding[0] - padding[2];
+
+ const xScale = scaleLinear()
+ .domain(xDomain ?? [0, max(items, (d) => d.x) ?? 0])
+ .range([0, availableWidth])
+ .nice();
+ const yScale = scaleLinear()
+ .domain(yDomain ?? [0, max(items, (d) => d.y) ?? 0])
+ .range([availableHeight, 0])
+ .nice();
+ const sizeScale = scaleLinear()
+ .domain(sizeDomain ?? [0, max(items, (d) => d.size) ?? 0])
+ .range(sizeRange ?? []);
+
+ const xScaleOriginal = xScale.copy();
+ const yScaleOriginal = yScale.copy();
+
+ xScale.range(getXRange(xScale, sizeScale, availableWidth));
+ yScale.range(getYRange(yScale, sizeScale, availableHeight));
+
+ const bubbles = sortBy(items, (b) => -b.size).map((item, index) => {
+ return (
+ <Bubble
+ color={item.color}
+ data={item.data}
+ key={item.key ?? index}
+ onClick={props.onBubbleClick}
+ r={sizeScale(item.size)}
+ scale={1 / transform.k}
+ tooltip={item.tooltip}
+ x={xScale(item.x)}
+ y={yScale(item.y)}
+ />
+ );
+ });
+
+ const xTicks = getTicks(xScale, props.formatXTick);
+ const yTicks = getTicks(yScale, props.formatYTick);
+
+ return (
+ <svg className={classNames('bubble-chart')} height={height} ref={boundNode} width={width}>
+ <defs>
+ <clipPath id="graph-region">
+ <rect
+ // Extend clip by 2 pixels: one for clipRect border, and one for Bubble borders
+ height={availableHeight + 4}
+ width={availableWidth + 4}
+ x={-2}
+ y={-2}
+ />
+ </clipPath>
+ </defs>
+ <g transform={`translate(${padding[3]}, ${padding[0]})`}>
+ <g clipPath="url(#graph-region)">
+ {renderXGrid(xTicks, xScale, yScale)}
+ {renderYGrid(yTicks, xScale, yScale)}
+ <g transform={`translate(${transform.x}, ${transform.y}) scale(${transform.k})`}>
+ {bubbles}
+ </g>
+ </g>
+ {renderXTicks(xTicks, xScale, yScaleOriginal)}
+ {renderYTicks(yTicks, xScaleOriginal, yScale)}
+ </g>
+ </svg>
+ );
+ };
+
+ return (
+ <div>
+ <div className="sw-flex sw-items-center sw-justify-end sw-h-control sw-mb-4">
+ <Tooltip overlay={zoomTooltipText}>
+ <span>
+ <Note className="sw-body-sm-highlight">{zoomLabel}</Note>
+ {': '}
+ {zoomLevelLabel}
+ </span>
+ </Tooltip>
+ {zoomLevelLabel !== '100%' && (
+ <ButtonSecondary
+ className="sw-ml-2"
+ disabled={zoomLevelLabel === '100%'}
+ onClick={resetZoom}
+ >
+ {zoomResetLabel}
+ </ButtonSecondary>
+ )}
+ </div>
+ <AutoSizer disableHeight={true}>{(size) => renderChart(size.width)}</AutoSizer>
+ </div>
+ );
+}
+
+interface BubbleProps<T> {
+ color?: BubbleColorVal;
+ data?: T;
+ onClick?: (ref?: T) => void;
+ r: number;
+ scale: number;
+ tooltip?: string | React.ReactNode;
+ x: number;
+ y: number;
+}
+
+function Bubble<T>(props: BubbleProps<T>) {
+ const theme = useTheme();
+ const { color, data, onClick, r, scale, tooltip, x, y } = props;
+ const handleClick = React.useCallback(
+ (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.stopPropagation();
+ event.preventDefault();
+ onClick?.(data);
+ },
+ [data, onClick]
+ );
+
+ const circle = (
+ <a href="#" onClick={handleClick}>
+ <BubbleStyled
+ r={r}
+ style={
+ color && {
+ fill: themeColor(`bubble.${color}`)({ theme }),
+ stroke: themeContrast(`bubble.${color}`)({ theme }),
+ }
+ }
+ transform={`translate(${x}, ${y}) scale(${scale})`}
+ />
+ </a>
+ );
+
+ return <Tooltip overlay={tooltip}>{circle}</Tooltip>;
+}
+
+const BubbleStyled = styled.circle`
+ ${tw`sw-cursor-pointer`}
+
+ transition: fill-opacity 0.2s ease;
+ fill: ${themeColor('bubbleDefault')};
+ stroke: ${themeContrast('bubbleDefault')};
+
+ &:hover {
+ fill-opacity: 0.8;
+ }
+`;
+
+const BubbleChartGrid = styled.line`
+ shape-rendering: crispedges;
+ stroke: ${themeColor('bubbleChartLine')};
+`;
+
+const BubbleChartTick = styled.text`
+ ${tw`sw-body-sm`}
+ ${tw`sw-select-none`}
+ fill: ${themeColor('pageContentLight')};
+ text-anchor: var(--align);
+`;
diff --git a/server/sonar-web/design-system/src/components/Tooltip.tsx b/server/sonar-web/design-system/src/components/Tooltip.tsx
index cb51f9dbb70..2a5d3925957 100644
--- a/server/sonar-web/design-system/src/components/Tooltip.tsx
+++ b/server/sonar-web/design-system/src/components/Tooltip.tsx
@@ -20,7 +20,7 @@
import { keyframes, ThemeContext } from '@emotion/react';
import styled from '@emotion/styled';
import classNames from 'classnames';
-import { throttle } from 'lodash';
+import { throttle, uniqueId } from 'lodash';
import React from 'react';
import { createPortal, findDOMNode } from 'react-dom';
import tw from 'twin.macro';
@@ -81,6 +81,7 @@ export class TooltipInner extends React.Component<TooltipProps, State> {
tooltipNode?: HTMLElement | null;
mounted = false;
mouseIn = false;
+ id: string;
static defaultProps = {
mouseEnterDelay: 0.1,
@@ -94,7 +95,7 @@ export class TooltipInner extends React.Component<TooltipProps, State> {
placement: props.placement,
visible: props.visible !== undefined ? props.visible : false,
};
-
+ this.id = uniqueId('tooltip-');
this.throttledPositionTooltip = throttle(this.positionTooltip, THROTTLE_SCROLL_DELAY);
}
@@ -289,6 +290,17 @@ export class TooltipInner extends React.Component<TooltipProps, State> {
}
};
+ handleFocus = () => {
+ this.setState({ visible: true });
+ if (this.props.onShow) {
+ this.props.onShow();
+ }
+ };
+
+ handleBlur = () => {
+ this.setState({ visible: false });
+ };
+
handleOverlayPointerEnter = () => {
this.mouseIn = true;
};
@@ -355,6 +367,13 @@ export class TooltipInner extends React.Component<TooltipProps, State> {
{React.cloneElement(this.props.children, {
onPointerEnter: this.handleChildPointerEnter,
onPointerLeave: this.handleChildPointerLeave,
+ onFocus: this.handleFocus,
+ onBlur: this.handleBlur,
+ // aria-describedby is the semantically correct property to use, but it's not
+ // always well supported. We sometimes need to handle this differently, depending
+ // on the triggering element. We should NOT use aria-labelledby, as this can
+ // have unintended effects (e.g., this can mess up buttons that need a tooltip).
+ 'aria-describedby': this.id,
})}
{this.isVisible() && (
<TooltipPortal>
diff --git a/server/sonar-web/design-system/src/components/__tests__/BubbleChart-test.tsx b/server/sonar-web/design-system/src/components/__tests__/BubbleChart-test.tsx
new file mode 100644
index 00000000000..cdbe8cb2686
--- /dev/null
+++ b/server/sonar-web/design-system/src/components/__tests__/BubbleChart-test.tsx
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { screen } from '@testing-library/react';
+import { AutoSizerProps } from 'react-virtualized';
+import { renderWithRouter } from '../../helpers/testUtils';
+import { FCProps } from '../../types/misc';
+import { BubbleChart } from '../BubbleChart';
+
+jest.mock('react-virtualized/dist/commonjs/AutoSizer', () => ({
+ AutoSizer: ({ children }: AutoSizerProps) => children({ width: 100, height: NaN }),
+}));
+
+jest.mock('d3-zoom', () => ({
+ zoom: jest.fn().mockReturnValue({ scaleExtent: jest.fn().mockReturnValue({ on: jest.fn() }) }),
+}));
+
+jest.mock('d3-selection', () => ({
+ select: jest.fn().mockReturnValue({ call: jest.fn() }),
+}));
+
+it('should display bubbles with correct chart structure', () => {
+ renderBubbleChart();
+ expect(screen.getAllByRole('link')).toHaveLength(2);
+ expect(screen.getByText('5')).toBeInTheDocument();
+});
+
+it('should allow click on bubbles', async () => {
+ const onBubbleClick = jest.fn();
+ const { user } = renderBubbleChart({ onBubbleClick });
+ await user.click(screen.getAllByRole('link')[0]);
+ expect(onBubbleClick).toHaveBeenCalledWith('foo');
+});
+
+it('should navigate between bubbles by tab', async () => {
+ const { user } = renderBubbleChart();
+ await user.tab();
+ expect(screen.getAllByRole('link')[0]).toHaveFocus();
+ await user.tab();
+ expect(screen.getAllByRole('link')[1]).toHaveFocus();
+});
+
+it('should not display ticks and grid', () => {
+ renderBubbleChart({
+ displayXGrid: false,
+ displayYGrid: false,
+ displayXTicks: false,
+ displayYTicks: false,
+ });
+
+ expect(screen.queryByText('5')).not.toBeInTheDocument();
+});
+
+it('renders empty graph', () => {
+ renderBubbleChart({
+ items: [],
+ });
+
+ expect(screen.queryByRole('link')).not.toBeInTheDocument();
+});
+
+function renderBubbleChart(props: Partial<FCProps<typeof BubbleChart>> = {}) {
+ return renderWithRouter(
+ <BubbleChart
+ height={100}
+ items={[
+ { x: 1, y: 10, size: 7, data: 'foo' },
+ { x: 2, y: 30, size: 5, color: 3, data: 'bar' },
+ ]}
+ padding={[0, 0, 0, 0]}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/design-system/src/components/__tests__/SearchSelectDropdown-test.tsx b/server/sonar-web/design-system/src/components/__tests__/SearchSelectDropdown-test.tsx
index 2ff49675d18..135a9b9f2d1 100644
--- a/server/sonar-web/design-system/src/components/__tests__/SearchSelectDropdown-test.tsx
+++ b/server/sonar-web/design-system/src/components/__tests__/SearchSelectDropdown-test.tsx
@@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { screen } from '@testing-library/react';
+import { act, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from '../../helpers/testUtils';
import { FCProps } from '../../types/misc';
@@ -65,7 +65,9 @@ it('should handle key navigation', async () => {
expect(screen.queryByText('different')).not.toBeInTheDocument();
await user.keyboard('{Escape}');
expect(await screen.findByText('different')).toBeInTheDocument();
- await user.keyboard('{Escape}');
+ await act(async () => {
+ await user.keyboard('{Escape}');
+ });
expect(screen.queryByText('different')).not.toBeInTheDocument();
await user.tab({ shift: true });
await user.keyboard('{ArrowDown}');
diff --git a/server/sonar-web/design-system/src/components/__tests__/Tooltip-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Tooltip-test.tsx
index 95ce51cc9b0..76385492db0 100644
--- a/server/sonar-web/design-system/src/components/__tests__/Tooltip-test.tsx
+++ b/server/sonar-web/design-system/src/components/__tests__/Tooltip-test.tsx
@@ -82,6 +82,15 @@ describe('TooltipInner', () => {
expect(screen.getByRole('tooltip')).toHaveClass('bottom');
});
+ it('should be opened/hidden using tab navigation', async () => {
+ const { user } = setupWithProps({}, <a href="#">Link</a>);
+
+ await user.tab();
+ expect(await screen.findByRole('tooltip')).toBeInTheDocument();
+ await user.tab();
+ expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
+ });
+
function setupWithProps(
props: Partial<TooltipInner['props']> = {},
children = <div role="note" />
diff --git a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap
index 66320711540..5b27cf29f1f 100644
--- a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap
+++ b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap
@@ -189,6 +189,7 @@ exports[`should show full size when multiline with no editting 1`] = `
class="fs-mask emotion-0 emotion-1"
>
<button
+ aria-describedby="tooltip-1"
class="sw-select-none emotion-2 emotion-3 emotion-4 emotion-5"
data-clipboard-text="foo
bar"
@@ -429,6 +430,7 @@ exports[`should show reduced size when single line with no editting 1`] = `
class="code-snippet-highlighted-oneline fs-mask emotion-0 emotion-1"
>
<button
+ aria-describedby="tooltip-2"
class="sw-select-none emotion-2 emotion-3 emotion-4 emotion-5"
data-clipboard-text="foobar"
type="button"
diff --git a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Histogram-test.tsx.snap b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Histogram-test.tsx.snap
index 166209b1591..8ad1037bf83 100644
--- a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Histogram-test.tsx.snap
+++ b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/Histogram-test.tsx.snap
@@ -290,6 +290,7 @@ exports[`renders correctly with yValues, yTicks, and yTooltips 1`] = `
y="10"
/>
<text
+ aria-describedby="tooltip-1"
class="emotion-2 emotion-3"
dx="1em"
dy="0.3em"
@@ -319,6 +320,7 @@ exports[`renders correctly with yValues, yTicks, and yTooltips 1`] = `
y="28"
/>
<text
+ aria-describedby="tooltip-2"
class="emotion-2 emotion-3"
dx="1em"
dy="0.3em"
@@ -348,6 +350,7 @@ exports[`renders correctly with yValues, yTicks, and yTooltips 1`] = `
y="46"
/>
<text
+ aria-describedby="tooltip-3"
class="emotion-2 emotion-3"
dx="1em"
dy="0.3em"
diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts
index 16fe359ece8..f1880121fde 100644
--- a/server/sonar-web/design-system/src/components/index.ts
+++ b/server/sonar-web/design-system/src/components/index.ts
@@ -23,6 +23,7 @@ export * from './Avatar';
export { Badge } from './Badge';
export { BarChart } from './BarChart';
export { Breadcrumbs } from './Breadcrumbs';
+export * from './BubbleChart';
export * from './Card';
export * from './Checkbox';
export * from './CodeSnippet';
diff --git a/server/sonar-web/design-system/src/index.ts b/server/sonar-web/design-system/src/index.ts
index 9c4c8f50bfb..b37e6479619 100644
--- a/server/sonar-web/design-system/src/index.ts
+++ b/server/sonar-web/design-system/src/index.ts
@@ -21,5 +21,4 @@
export * from './components';
export * from './helpers';
export * from './theme';
-export * from './types/measures';
-export * from './types/theme';
+export * from './types';
diff --git a/server/sonar-web/src/main/js/components/charts/BubbleChart.css b/server/sonar-web/design-system/src/types/charts.ts
index 77bc547c408..7be16a1b5b3 100644
--- a/server/sonar-web/src/main/js/components/charts/BubbleChart.css
+++ b/server/sonar-web/design-system/src/types/charts.ts
@@ -17,40 +17,17 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-.bubble-chart text {
- user-select: none;
-}
-
-.bubble-chart-bubble {
- fill: var(--blue);
- fill-opacity: 0.2;
- stroke: var(--blue);
- cursor: pointer;
- transition: fill-opacity 0.2s ease;
-}
-
-.bubble-chart-bubble:hover {
- fill-opacity: 0.8;
-}
-
-.bubble-chart-grid {
- shape-rendering: crispedges;
- stroke: #eee;
-}
-.bubble-chart-tick {
- fill: var(--secondFontColor);
- font-size: var(--smallFontSize);
- text-anchor: middle;
+export interface ChartPoint {
+ x: Date;
+ y: number | string | undefined;
}
-.bubble-chart-tick-y {
- text-anchor: end;
+export interface ChartSerie {
+ data: ChartPoint[];
+ name: string;
+ translatedName: string;
+ type: string;
}
-.bubble-chart-zoom {
- position: absolute;
- right: 20px;
- top: 20px;
- z-index: var(--aboveNormalZIndex);
-}
+export type BubbleColorVal = 1 | 2 | 3 | 4 | 5;
diff --git a/server/sonar-web/design-system/src/types/index.ts b/server/sonar-web/design-system/src/types/index.ts
new file mode 100644
index 00000000000..60861305544
--- /dev/null
+++ b/server/sonar-web/design-system/src/types/index.ts
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.
+ */
+
+export * from './charts';
+export * from './measures';
+export * from './theme';
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 11ffbd4bc52..c57f2004d5e 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
@@ -17,13 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { BubbleColorVal, BubbleChart as OriginalBubbleChart } from 'design-system';
import * as React from 'react';
-import theme from '../../../app/theme';
-import OriginalBubbleChart from '../../../components/charts/BubbleChart';
import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend';
import Link from '../../../components/common/Link';
import HelpTooltip from '../../../components/controls/HelpTooltip';
-import { RATING_COLORS } from '../../../helpers/constants';
import {
getLocalizedMetricDomain,
getLocalizedMetricName,
@@ -140,8 +138,7 @@ export default class BubbleChart extends React.PureComponent<Props, State> {
const x = this.getMeasureVal(component, metrics.x);
const y = this.getMeasureVal(component, metrics.y);
const size = this.getMeasureVal(component, metrics.size);
- const colors =
- metrics.colors && metrics.colors.map((metric) => this.getMeasureVal(component, metric));
+ const colors = metrics.colors?.map((metric) => this.getMeasureVal(component, metric));
if ((!x && x !== 0) || (!y && y !== 0) || (!size && size !== 0)) {
return undefined;
}
@@ -157,10 +154,7 @@ export default class BubbleChart extends React.PureComponent<Props, State> {
x,
y,
size,
- color:
- colorRating !== undefined
- ? RATING_COLORS[colorRating - 1]
- : { fill: theme.colors.primary, stroke: theme.colors.primary },
+ color: (colorRating as BubbleColorVal) ?? 0,
data: component,
tooltip: this.getTooltip(component, { x, y, size, colors }, metrics),
};
@@ -185,7 +179,7 @@ export default class BubbleChart extends React.PureComponent<Props, State> {
height={HEIGHT}
items={items}
onBubbleClick={this.handleBubbleClick}
- padding={[25, 60, 50, 60]}
+ padding={[0, 4, 50, 60]}
yDomain={getBubbleYDomain(this.props.domain)}
xDomain={xDomain}
/>
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 16901b5bc49..cdcca493f8c 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
@@ -17,63 +17,69 @@
* 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 userEvent from '@testing-library/user-event';
+import { MetricsEnum } from 'design-system';
import { keyBy } from 'lodash';
-import * as React from 'react';
-import OriginalBubbleChart from '../../../../components/charts/BubbleChart';
+import React from 'react';
+import { AutoSizerProps } from 'react-virtualized';
+import { byRole, byText } from 'testing-library-selector';
import { mockComponentMeasure } from '../../../../helpers/mocks/component';
import { mockMeasure, mockMetric, mockPaging } from '../../../../helpers/testMocks';
-import { MetricKey } from '../../../../types/metrics';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { MetricKey, MetricType } from '../../../../types/metrics';
import { enhanceComponent } from '../../utils';
import BubbleChart from '../BubbleChart';
+jest.mock('react-virtualized/dist/commonjs/AutoSizer', () => ({
+ AutoSizer: ({ children }: AutoSizerProps) => children({ width: 100, height: NaN }),
+}));
+
+jest.mock('d3-zoom', () => ({
+ zoom: jest.fn().mockReturnValue({ scaleExtent: jest.fn().mockReturnValue({ on: jest.fn() }) }),
+}));
+
+jest.mock('d3-selection', () => ({
+ select: jest.fn().mockReturnValue({ call: jest.fn() }),
+}));
+
const metrics = keyBy(
[
- 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' }),
+ mockMetric({ key: MetricKey.ncloc, type: MetricType.Integer }),
+ mockMetric({ key: MetricKey.security_remediation_effort, type: MetricType.Data }),
+ mockMetric({ key: MetricKey.vulnerabilities, type: MetricType.Integer }),
+ mockMetric({ key: MetricKey.security_rating, type: MetricType.Rating }),
],
(m) => m.key
);
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot('default');
- expect(shallowRender({ paging: mockPaging({ total: 1000 }) })).toMatchSnapshot(
- 'only showing first 500 files'
- );
- expect(
- shallowRender({
- components: [
- enhanceComponent(
- mockComponentMeasure(true, {
- measures: [
- mockMeasure({ value: '0', metric: MetricKey.ncloc }),
- mockMeasure({ value: '0', metric: MetricKey.security_remediation_effort }),
- mockMeasure({ value: '0', metric: MetricKey.vulnerabilities }),
- mockMeasure({ value: '0', metric: MetricKey.security_rating }),
- ],
- }),
- metrics[MetricKey.vulnerabilities],
- metrics
- ),
- ],
- })
- ).toMatchSnapshot('all on x=0');
+const ui = {
+ show500Files: byText(/component_measures.legend.only_first_500_files/),
+ bubbles: byRole('link', { name: '' }),
+ filterCheckbox: (name: string) => byRole('checkbox', { name }),
+};
+
+it('should render correctly', async () => {
+ renderBubbleChart();
+
+ expect(await ui.show500Files.find()).toBeInTheDocument();
+ expect(ui.bubbles.getAll()).toHaveLength(1);
});
-it('should handle filtering', () => {
- const wrapper = shallowRender();
- expect(wrapper.find(OriginalBubbleChart).props().items).toHaveLength(1);
+it('should filter by rating', async () => {
+ const user = userEvent.setup();
+ renderBubbleChart();
- wrapper.instance().handleRatingFilterClick(2);
+ expect(await ui.bubbles.findAll()).toHaveLength(1);
+ Object.keys(MetricsEnum).forEach((rating) => {
+ expect(ui.filterCheckbox(rating).get()).toBeInTheDocument();
+ });
- expect(wrapper.state().ratingFilters).toEqual({ 2: true });
- expect(wrapper.find(OriginalBubbleChart).props().items).toHaveLength(0);
+ await user.click(ui.filterCheckbox(MetricsEnum.C).get());
+ expect(ui.bubbles.getAll()).toHaveLength(1);
});
-function shallowRender(overrides: Partial<BubbleChart['props']> = {}) {
- return shallow<BubbleChart>(
+function renderBubbleChart(overrides: Partial<BubbleChart['props']> = {}) {
+ return renderComponent(
<BubbleChart
componentKey="foo"
components={[
@@ -92,7 +98,7 @@ function shallowRender(overrides: Partial<BubbleChart['props']> = {}) {
]}
domain="Security"
metrics={metrics}
- paging={mockPaging({ total: 100 })}
+ paging={mockPaging({ total: 1000 })}
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
deleted file mode 100644
index 5c1419f015f..00000000000
--- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/BubbleChart-test.tsx.snap
+++ /dev/null
@@ -1,628 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: all on x=0 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>
- </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={{}}
- onRatingClick={[Function]}
- />
- </span>
- </div>
- <div
- className="measure-overview-bubble-chart-content"
- >
- <div
- className="text-center small spacer-top spacer-bottom"
- >
- <ForwardRef(Link)
- to={
- {
- "pathname": "/component_measures",
- "search": "?id=foo&metric=vulnerabilities&view=list",
- }
- }
- >
- component_measures.overview.see_data_as_list
- </ForwardRef(Link)>
- </div>
- <BubbleChart
- displayXGrid={true}
- displayXTicks={true}
- displayYGrid={true}
- displayYTicks={true}
- formatXTick={[Function]}
- formatYTick={[Function]}
- height={500}
- items={
- [
- {
- "color": undefined,
- "data": {
- "key": "foo:src/index.tsx",
- "leak": "1.0",
- "measures": [
- {
- "bestValue": true,
- "leak": "1.0",
- "metric": {
- "id": "ncloc",
- "key": "ncloc",
- "name": "ncloc",
- "type": "INT",
- },
- "period": {
- "bestValue": true,
- "index": 1,
- "value": "1.0",
- },
- "value": "0",
- },
- {
- "bestValue": true,
- "leak": "1.0",
- "metric": {
- "id": "security_remediation_effort",
- "key": "security_remediation_effort",
- "name": "security_remediation_effort",
- "type": "DATA",
- },
- "period": {
- "bestValue": true,
- "index": 1,
- "value": "1.0",
- },
- "value": "0",
- },
- {
- "bestValue": true,
- "leak": "1.0",
- "metric": {
- "id": "vulnerabilities",
- "key": "vulnerabilities",
- "name": "vulnerabilities",
- "type": "INT",
- },
- "period": {
- "bestValue": true,
- "index": 1,
- "value": "1.0",
- },
- "value": "0",
- },
- {
- "bestValue": true,
- "leak": "1.0",
- "metric": {
- "id": "security_rating",
- "key": "security_rating",
- "name": "security_rating",
- "type": "RATING",
- },
- "period": {
- "bestValue": true,
- "index": 1,
- "value": "1.0",
- },
- "value": "0",
- },
- ],
- "name": "index.tsx",
- "path": "src/index.tsx",
- "qualifier": "FIL",
- "value": "0",
- },
- "size": 0,
- "tooltip": <div
- className="text-left"
- >
- <React.Fragment>
- index.tsx
- <br />
- </React.Fragment>
- <React.Fragment>
- ncloc: 0
- <br />
- </React.Fragment>
- <React.Fragment>
- security_remediation_effort: 0
- <br />
- </React.Fragment>
- <React.Fragment>
- vulnerabilities: 0
- <br />
- </React.Fragment>
- <React.Fragment>
- security_rating: \`
- </React.Fragment>
- </div>,
- "x": 0,
- "y": 0,
- },
- ]
- }
- onBubbleClick={[Function]}
- padding={
- [
- 25,
- 60,
- 50,
- 60,
- ]
- }
- sizeRange={
- [
- 5,
- 45,
- ]
- }
- xDomain={
- [
- 0,
- 100,
- ]
- }
- />
- </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: default 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>
- </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={{}}
- onRatingClick={[Function]}
- />
- </span>
- </div>
- <div
- className="measure-overview-bubble-chart-content"
- >
- <div
- className="text-center small spacer-top spacer-bottom"
- >
- <ForwardRef(Link)
- to={
- {
- "pathname": "/component_measures",
- "search": "?id=foo&metric=vulnerabilities&view=list",
- }
- }
- >
- component_measures.overview.see_data_as_list
- </ForwardRef(Link)>
- </div>
- <BubbleChart
- displayXGrid={true}
- displayXTicks={true}
- displayYGrid={true}
- displayYTicks={true}
- formatXTick={[Function]}
- formatYTick={[Function]}
- height={500}
- items={
- [
- {
- "color": {
- "fill": "#C6E056",
- "fillTransparent": "rgba(198, 224, 86, 0.2)",
- "stroke": "#809E00",
- },
- "data": {
- "key": "foo:src/index.tsx",
- "leak": "1.0",
- "measures": [
- {
- "bestValue": true,
- "leak": "1.0",
- "metric": {
- "id": "ncloc",
- "key": "ncloc",
- "name": "ncloc",
- "type": "INT",
- },
- "period": {
- "bestValue": true,
- "index": 1,
- "value": "1.0",
- },
- "value": "236",
- },
- {
- "bestValue": true,
- "leak": "1.0",
- "metric": {
- "id": "security_remediation_effort",
- "key": "security_remediation_effort",
- "name": "security_remediation_effort",
- "type": "DATA",
- },
- "period": {
- "bestValue": true,
- "index": 1,
- "value": "1.0",
- },
- "value": "10",
- },
- {
- "bestValue": true,
- "leak": "1.0",
- "metric": {
- "id": "vulnerabilities",
- "key": "vulnerabilities",
- "name": "vulnerabilities",
- "type": "INT",
- },
- "period": {
- "bestValue": true,
- "index": 1,
- "value": "1.0",
- },
- "value": "3",
- },
- {
- "bestValue": true,
- "leak": "1.0",
- "metric": {
- "id": "security_rating",
- "key": "security_rating",
- "name": "security_rating",
- "type": "RATING",
- },
- "period": {
- "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={
- [
- 25,
- 60,
- 50,
- 60,
- ]
- }
- sizeRange={
- [
- 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"
- >
- <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={{}}
- onRatingClick={[Function]}
- />
- </span>
- </div>
- <div
- className="measure-overview-bubble-chart-content"
- >
- <div
- className="text-center small spacer-top spacer-bottom"
- >
- <ForwardRef(Link)
- to={
- {
- "pathname": "/component_measures",
- "search": "?id=foo&metric=vulnerabilities&view=list",
- }
- }
- >
- component_measures.overview.see_data_as_list
- </ForwardRef(Link)>
- </div>
- <BubbleChart
- displayXGrid={true}
- displayXTicks={true}
- displayYGrid={true}
- displayYTicks={true}
- formatXTick={[Function]}
- formatYTick={[Function]}
- height={500}
- items={
- [
- {
- "color": {
- "fill": "#C6E056",
- "fillTransparent": "rgba(198, 224, 86, 0.2)",
- "stroke": "#809E00",
- },
- "data": {
- "key": "foo:src/index.tsx",
- "leak": "1.0",
- "measures": [
- {
- "bestValue": true,
- "leak": "1.0",
- "metric": {
- "id": "ncloc",
- "key": "ncloc",
- "name": "ncloc",
- "type": "INT",
- },
- "period": {
- "bestValue": true,
- "index": 1,
- "value": "1.0",
- },
- "value": "236",
- },
- {
- "bestValue": true,
- "leak": "1.0",
- "metric": {
- "id": "security_remediation_effort",
- "key": "security_remediation_effort",
- "name": "security_remediation_effort",
- "type": "DATA",
- },
- "period": {
- "bestValue": true,
- "index": 1,
- "value": "1.0",
- },
- "value": "10",
- },
- {
- "bestValue": true,
- "leak": "1.0",
- "metric": {
- "id": "vulnerabilities",
- "key": "vulnerabilities",
- "name": "vulnerabilities",
- "type": "INT",
- },
- "period": {
- "bestValue": true,
- "index": 1,
- "value": "1.0",
- },
- "value": "3",
- },
- {
- "bestValue": true,
- "leak": "1.0",
- "metric": {
- "id": "security_rating",
- "key": "security_rating",
- "name": "security_rating",
- "type": "RATING",
- },
- "period": {
- "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={
- [
- 25,
- 60,
- 50,
- 60,
- ]
- }
- sizeRange={
- [
- 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>
-`;
diff --git a/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx b/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx
deleted file mode 100644
index e9124b73452..00000000000
--- a/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx
+++ /dev/null
@@ -1,377 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 classNames from 'classnames';
-import { max, min } from 'd3-array';
-import { scaleLinear, ScaleLinear } from 'd3-scale';
-import { select } from 'd3-selection';
-import { D3ZoomEvent, zoom, ZoomBehavior, zoomIdentity } from 'd3-zoom';
-import { sortBy, uniq } from 'lodash';
-import * as React from 'react';
-import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
-import { translate } from '../../helpers/l10n';
-import Link from '../common/Link';
-import Tooltip from '../controls/Tooltip';
-import './BubbleChart.css';
-
-const TICKS_COUNT = 5;
-
-interface BubbleItem<T> {
- color: { fill: string; stroke: string; hover?: string };
- key?: string;
- data?: T;
- size: number;
- tooltip?: React.ReactNode;
- x: number;
- y: number;
-}
-
-interface Props<T> {
- displayXGrid?: boolean;
- displayXTicks?: boolean;
- displayYGrid?: boolean;
- displayYTicks?: boolean;
- formatXTick: (tick: number) => string;
- formatYTick: (tick: number) => string;
- height: number;
- items: BubbleItem<T>[];
- onBubbleClick?: (ref?: T) => void;
- padding: [number, number, number, number];
- sizeDomain?: [number, number];
- sizeRange?: [number, number];
- xDomain?: [number, number];
- yDomain?: [number, number];
-}
-
-interface State {
- transform: { x: number; y: number; k: number };
-}
-
-type Scale = ScaleLinear<number, number>;
-
-export default class BubbleChart<T> extends React.PureComponent<Props<T>, State> {
- private node?: Element;
- private zoom?: ZoomBehavior<Element, unknown>;
-
- static defaultProps = {
- displayXGrid: true,
- displayXTicks: true,
- displayYGrid: true,
- displayYTicks: true,
- formatXTick: (d: number) => String(d),
- formatYTick: (d: number) => String(d),
- padding: [10, 10, 10, 10],
- sizeRange: [5, 45],
- };
-
- constructor(props: Props<T>) {
- super(props);
- this.state = { transform: { x: 0, y: 0, k: 1 } };
- }
-
- componentDidUpdate() {
- if (this.zoom && this.node) {
- const rect = this.node.getBoundingClientRect();
- this.zoom.translateExtent([
- [0, 0],
- [rect.width, rect.height],
- ]);
- }
- }
-
- boundNode = (node: SVGSVGElement) => {
- this.node = node;
- this.zoom = zoom().scaleExtent([1, 10]).on('zoom', this.zoomed);
- select(this.node).call(this.zoom as any);
- };
-
- zoomed = (event: D3ZoomEvent<SVGSVGElement, void>) => {
- const { padding } = this.props;
- const { x, y, k } = event.transform;
- this.setState({
- transform: {
- x: x + padding[3] * (k - 1),
- y: y + padding[0] * (k - 1),
- k,
- },
- });
- };
-
- resetZoom = (e: React.MouseEvent) => {
- e.stopPropagation();
- e.preventDefault();
- if (this.zoom && this.node) {
- select(this.node).call(this.zoom.transform as any, zoomIdentity);
- }
- };
-
- getXRange(xScale: Scale, sizeScale: Scale, availableWidth: number) {
- const minX = min(this.props.items, (d) => xScale(d.x) - sizeScale(d.size)) || 0;
- const maxX = max(this.props.items, (d) => xScale(d.x) + sizeScale(d.size)) || 0;
- const dMinX = minX < 0 ? xScale.range()[0] - minX : xScale.range()[0];
- const dMaxX = maxX > xScale.range()[1] ? maxX - xScale.range()[1] : 0;
- return [dMinX, availableWidth - dMaxX];
- }
-
- getYRange(yScale: Scale, sizeScale: Scale, availableHeight: number) {
- const minY = min(this.props.items, (d) => yScale(d.y) - sizeScale(d.size)) || 0;
- const maxY = max(this.props.items, (d) => yScale(d.y) + sizeScale(d.size)) || 0;
- const dMinY = minY < 0 ? yScale.range()[1] - minY : yScale.range()[1];
- const dMaxY = maxY > yScale.range()[0] ? maxY - yScale.range()[0] : 0;
- return [availableHeight - dMaxY, dMinY];
- }
-
- getTicks(scale: Scale, format: (d: number) => string) {
- const zoomAmount = Math.ceil(this.state.transform.k);
- const ticks = scale.ticks(TICKS_COUNT * zoomAmount).map((tick) => format(tick));
- const uniqueTicksCount = uniq(ticks).length;
- const ticksCount =
- uniqueTicksCount < TICKS_COUNT * zoomAmount ? uniqueTicksCount - 1 : TICKS_COUNT * zoomAmount;
- return scale.ticks(ticksCount);
- }
-
- getZoomLevelLabel = () => Math.floor(this.state.transform.k * 100) + '%';
-
- renderXGrid = (ticks: number[], xScale: Scale, yScale: Scale) => {
- if (!this.props.displayXGrid) {
- return null;
- }
-
- const { transform } = this.state;
- const lines = ticks.map((tick, index) => {
- const x = xScale(tick);
- const y1 = yScale.range()[0];
- const y2 = yScale.range()[1];
- return (
- <line
- className="bubble-chart-grid"
- // eslint-disable-next-line react/no-array-index-key
- key={index}
- x1={x * transform.k + transform.x}
- x2={x * transform.k + transform.x}
- y1={y1 * transform.k}
- y2={transform.k > 1 ? 0 : y2}
- />
- );
- });
-
- return <g>{lines}</g>;
- };
-
- renderYGrid = (ticks: number[], xScale: Scale, yScale: Scale) => {
- if (!this.props.displayYGrid) {
- return null;
- }
-
- const { transform } = this.state;
- const lines = ticks.map((tick, index) => {
- const y = yScale(tick);
- const x1 = xScale.range()[0];
- const x2 = xScale.range()[1];
- return (
- <line
- className="bubble-chart-grid"
- // eslint-disable-next-line react/no-array-index-key
- key={index}
- x1={transform.k > 1 ? 0 : x1}
- x2={x2 * transform.k}
- y1={y * transform.k + transform.y}
- y2={y * transform.k + transform.y}
- />
- );
- });
-
- return <g>{lines}</g>;
- };
-
- renderXTicks = (xTicks: number[], xScale: Scale, yScale: Scale) => {
- if (!this.props.displayXTicks) {
- return null;
- }
-
- const { transform } = this.state;
- const ticks = xTicks.map((tick, index) => {
- const x = xScale(tick) * transform.k + transform.x;
- const y = yScale.range()[0];
- const innerText = this.props.formatXTick(tick);
- // as we modified the `x` using `transform`, check that it is inside the range again
- return x > 0 && x < xScale.range()[1] ? (
- // eslint-disable-next-line react/no-array-index-key
- <text className="bubble-chart-tick" dy="1.5em" key={index} x={x} y={y}>
- {innerText}
- </text>
- ) : null;
- });
-
- return <g>{ticks}</g>;
- };
-
- renderYTicks = (yTicks: number[], xScale: Scale, yScale: Scale) => {
- if (!this.props.displayYTicks) {
- return null;
- }
-
- const { transform } = this.state;
- const ticks = yTicks.map((tick, index) => {
- const x = xScale.range()[0];
- const y = yScale(tick) * transform.k + transform.y;
- const innerText = this.props.formatYTick(tick);
- // as we modified the `y` using `transform`, check that it is inside the range again
- return y > 0 && y < yScale.range()[0] ? (
- <text
- className="bubble-chart-tick bubble-chart-tick-y"
- dx="-0.5em"
- dy="0.3em"
- // eslint-disable-next-line react/no-array-index-key
- key={index}
- x={x}
- y={y}
- >
- {innerText}
- </text>
- ) : null;
- });
-
- return <g>{ticks}</g>;
- };
-
- renderChart = (width: number) => {
- const { transform } = this.state;
- const availableWidth = width - this.props.padding[1] - this.props.padding[3];
- const availableHeight = this.props.height - this.props.padding[0] - this.props.padding[2];
-
- const xScale = scaleLinear()
- .domain(this.props.xDomain || [0, max(this.props.items, (d) => d.x) || 0])
- .range([0, availableWidth])
- .nice();
- const yScale = scaleLinear()
- .domain(this.props.yDomain || [0, max(this.props.items, (d) => d.y) || 0])
- .range([availableHeight, 0])
- .nice();
- const sizeScale = scaleLinear()
- .domain(this.props.sizeDomain || [0, max(this.props.items, (d) => d.size) || 0])
- .range(this.props.sizeRange || []);
-
- const xScaleOriginal = xScale.copy();
- const yScaleOriginal = yScale.copy();
-
- xScale.range(this.getXRange(xScale, sizeScale, availableWidth));
- yScale.range(this.getYRange(yScale, sizeScale, availableHeight));
-
- const bubbles = sortBy(this.props.items, (b) => -b.size).map((item, index) => {
- return (
- <Bubble
- color={item.color}
- data={item.data}
- key={item.key || index}
- onClick={this.props.onBubbleClick}
- r={sizeScale(item.size)}
- scale={1 / transform.k}
- tooltip={item.tooltip}
- x={xScale(item.x)}
- y={yScale(item.y)}
- />
- );
- });
-
- const xTicks = this.getTicks(xScale, this.props.formatXTick);
- const yTicks = this.getTicks(yScale, this.props.formatYTick);
-
- return (
- <svg
- className={classNames('bubble-chart')}
- height={this.props.height}
- ref={this.boundNode}
- width={width}
- >
- <defs>
- <clipPath id="graph-region">
- <rect
- // Extend clip by 2 pixels: one for clipRect border, and one for Bubble borders
- height={availableHeight + 4}
- width={availableWidth + 4}
- x={-2}
- y={-2}
- />
- </clipPath>
- </defs>
- <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
- <g clipPath="url(#graph-region)">
- {this.renderXGrid(xTicks, xScale, yScale)}
- {this.renderYGrid(yTicks, xScale, yScale)}
- <g transform={`translate(${transform.x}, ${transform.y}) scale(${transform.k})`}>
- {bubbles}
- </g>
- </g>
- {this.renderXTicks(xTicks, xScale, yScaleOriginal)}
- {this.renderYTicks(yTicks, xScaleOriginal, yScale)}
- </g>
- </svg>
- );
- };
-
- render() {
- return (
- <div>
- <div className="bubble-chart-zoom">
- <Tooltip overlay={translate('component_measures.bubble_chart.zoom_level')}>
- <Link onClick={this.resetZoom} to="#">
- {this.getZoomLevelLabel()}
- </Link>
- </Tooltip>
- </div>
- <AutoSizer disableHeight={true}>{(size) => this.renderChart(size.width)}</AutoSizer>
- </div>
- );
- }
-}
-
-interface BubbleProps<T> {
- color: { fill: string; stroke: string; hover?: string };
- onClick?: (ref?: T) => void;
- data?: T;
- r: number;
- scale: number;
- tooltip?: string | React.ReactNode;
- x: number;
- y: number;
-}
-
-function Bubble<T>(props: BubbleProps<T>) {
- const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
- if (props.onClick) {
- e.stopPropagation();
- e.preventDefault();
- props.onClick(props.data);
- }
- };
-
- const circle = (
- <a onClick={handleClick} href="#">
- <circle
- className="bubble-chart-bubble"
- r={props.r}
- style={{ fill: props.color.fill, stroke: props.color.stroke }}
- transform={`translate(${props.x}, ${props.y}) scale(${props.scale})`}
- />
- </a>
- );
-
- return <Tooltip overlay={props.tooltip || undefined}>{circle}</Tooltip>;
-}
diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx b/server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx
deleted file mode 100644
index 955891f2fbf..00000000000
--- a/server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { select } from 'd3-selection';
-import { D3ZoomEvent, zoom } from 'd3-zoom';
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { AutoSizer, AutoSizerProps } from 'react-virtualized/dist/commonjs/AutoSizer';
-import Link from '../../../components/common/Link';
-import { mockComponentMeasureEnhanced } from '../../../helpers/mocks/component';
-import { mockHtmlElement } from '../../../helpers/mocks/dom';
-import { click, mockEvent } from '../../../helpers/testUtils';
-import { ComponentMeasureEnhanced } from '../../../types/types';
-import BubbleChart from '../BubbleChart';
-
-jest.mock('react-virtualized/dist/commonjs/AutoSizer', () => ({
- AutoSizer: ({ children }: AutoSizerProps) => children({ width: 100, height: NaN }),
-}));
-
-jest.mock('d3-selection', () => ({
- select: jest.fn().mockReturnValue({ call: jest.fn() }),
-}));
-
-jest.mock('d3-zoom', () => {
- return {
- zoomidentity: { k: 1, tx: 0, ty: 0 },
- zoom: jest.fn(),
- };
-});
-
-beforeEach(jest.clearAllMocks);
-
-it('should display bubbles', () => {
- const wrapper = shallowRender();
- wrapper
- .find(AutoSizer)
- .dive()
- .find('Bubble')
- .forEach((bubble) => {
- expect(bubble.dive()).toMatchSnapshot();
- });
-});
-
-it('should render bubble links', () => {
- const wrapper = shallowRender({
- items: [
- { x: 1, y: 10, size: 7, color: { fill: 'blue', stroke: 'blue' } },
- { x: 2, y: 30, size: 5, color: { fill: 'green', stroke: 'green' } },
- ],
- });
- wrapper
- .find(AutoSizer)
- .dive()
- .find('Bubble')
- .forEach((bubble) => {
- expect(bubble.dive()).toMatchSnapshot();
- });
-});
-
-it('should render bubbles with click handlers', () => {
- const onBubbleClick = jest.fn();
- const wrapper = shallowRender({ onBubbleClick });
- wrapper
- .find(AutoSizer)
- .dive()
- .find('Bubble')
- .forEach((bubble) => {
- click(bubble.dive().find('a'));
- expect(bubble.dive()).toMatchSnapshot();
- });
- expect(onBubbleClick).toHaveBeenCalledTimes(2);
- expect(onBubbleClick).toHaveBeenLastCalledWith(mockComponentMeasureEnhanced());
-});
-
-it('should correctly handle zooming', () => {
- class ZoomBehaviorMock {
- on = () => this;
- scaleExtent = () => this;
- translateExtent = () => this;
- }
-
- const call = jest.fn();
- const zoomBehavior = new ZoomBehaviorMock();
- (select as jest.Mock).mockReturnValueOnce({ call });
- (zoom as jest.Mock).mockReturnValueOnce(zoomBehavior);
-
- return new Promise<void>((resolve, reject) => {
- const wrapper = shallowRender({ padding: [5, 5, 5, 5] });
- wrapper.instance().boundNode(
- mockHtmlElement<SVGSVGElement>({
- getBoundingClientRect: () => ({ width: 100, height: 100 } as DOMRect),
- })
- );
-
- // Call zoom event handler.
- const mockZoomEvent = { transform: { x: 10, y: 10, k: 20 } } as D3ZoomEvent<
- SVGSVGElement,
- void
- >;
- wrapper.instance().zoomed(mockZoomEvent);
- expect(wrapper.state().transform).toEqual({
- x: 105,
- y: 105,
- k: 20,
- });
-
- // Reset Zoom levels.
- const resetZoomClick = wrapper.find('div.bubble-chart-zoom').find(Link).props().onClick;
- if (!resetZoomClick) {
- reject();
- return;
- }
-
- const stopPropagation = jest.fn();
- const preventDefault = jest.fn();
- resetZoomClick(mockEvent({ stopPropagation, preventDefault }));
- expect(stopPropagation).toHaveBeenCalled();
- expect(preventDefault).toHaveBeenCalled();
- expect(call).toHaveBeenCalledWith(zoomBehavior);
-
- resolve();
- });
-});
-
-function shallowRender(props: Partial<BubbleChart<ComponentMeasureEnhanced>['props']> = {}) {
- return shallow<BubbleChart<ComponentMeasureEnhanced>>(
- <BubbleChart
- height={100}
- items={[
- {
- x: 1,
- y: 10,
- size: 7,
- data: mockComponentMeasureEnhanced(),
- color: { fill: 'blue', stroke: 'blue' },
- },
- {
- x: 2,
- y: 30,
- size: 5,
- data: mockComponentMeasureEnhanced(),
- color: { fill: 'red', stroke: 'red' },
- },
- ]}
- padding={[0, 0, 0, 0]}
- {...props}
- />
- );
-}
diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap b/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap
deleted file mode 100644
index d59030a480e..00000000000
--- a/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap
+++ /dev/null
@@ -1,127 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display bubbles 1`] = `
-<Tooltip>
- <a
- href="#"
- onClick={[Function]}
- >
- <circle
- className="bubble-chart-bubble"
- r={45}
- style={
- {
- "fill": "blue",
- "stroke": "blue",
- }
- }
- transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
- />
- </a>
-</Tooltip>
-`;
-
-exports[`should display bubbles 2`] = `
-<Tooltip>
- <a
- href="#"
- onClick={[Function]}
- >
- <circle
- className="bubble-chart-bubble"
- r={33.57142857142858}
- style={
- {
- "fill": "red",
- "stroke": "red",
- }
- }
- transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
- />
- </a>
-</Tooltip>
-`;
-
-exports[`should render bubble links 1`] = `
-<Tooltip>
- <a
- href="#"
- onClick={[Function]}
- >
- <circle
- className="bubble-chart-bubble"
- r={45}
- style={
- {
- "fill": "blue",
- "stroke": "blue",
- }
- }
- transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
- />
- </a>
-</Tooltip>
-`;
-
-exports[`should render bubble links 2`] = `
-<Tooltip>
- <a
- href="#"
- onClick={[Function]}
- >
- <circle
- className="bubble-chart-bubble"
- r={33.57142857142858}
- style={
- {
- "fill": "green",
- "stroke": "green",
- }
- }
- transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
- />
- </a>
-</Tooltip>
-`;
-
-exports[`should render bubbles with click handlers 1`] = `
-<Tooltip>
- <a
- href="#"
- onClick={[Function]}
- >
- <circle
- className="bubble-chart-bubble"
- r={45}
- style={
- {
- "fill": "blue",
- "stroke": "blue",
- }
- }
- transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
- />
- </a>
-</Tooltip>
-`;
-
-exports[`should render bubbles with click handlers 2`] = `
-<Tooltip>
- <a
- href="#"
- onClick={[Function]}
- >
- <circle
- className="bubble-chart-bubble"
- r={33.57142857142858}
- style={
- {
- "fill": "red",
- "stroke": "red",
- }
- }
- transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
- />
- </a>
-</Tooltip>
-`;
diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/LanguageDistribution-test.tsx.snap b/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/LanguageDistribution-test.tsx.snap
index 5fed7969987..8d594aa389e 100644
--- a/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/LanguageDistribution-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/LanguageDistribution-test.tsx.snap
@@ -35,6 +35,7 @@ exports[`should render correctly 1`] = `
y="13"
/>
<text
+ aria-describedby="tooltip-1"
class="emotion-2 emotion-3"
dx="1em"
dy="0.3em"
diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock
index 5e21aa42090..36d303c96f0 100644
--- a/server/sonar-web/yarn.lock
+++ b/server/sonar-web/yarn.lock
@@ -6192,11 +6192,13 @@ __metadata:
clipboard: 2.0.11
d3-array: 3.2.3
d3-scale: 4.0.2
+ d3-selection: 3.0.0
d3-shape: 3.2.0
+ d3-zoom: 3.0.0
date-fns: 2.29.3
lodash: 4.17.21
react: 17.0.2
- react-day-picker: 8.6.0
+ react-day-picker: 8.7.1
react-dom: 17.0.2
react-helmet-async: 1.3.0
react-highlight-words: 0.20.0
@@ -6204,6 +6206,7 @@ __metadata:
react-modal: 3.16.1
react-router-dom: 6.10.0
react-select: 5.7.2
+ react-virtualized: 9.22.3
tailwindcss: 3.3.1
languageName: unknown
linkType: soft