aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx44
-rw-r--r--server/sonar-web/src/main/js/components/charts/TreeMap.css95
-rw-r--r--server/sonar-web/src/main/js/components/charts/TreeMap.tsx122
-rw-r--r--server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx146
-rw-r--r--server/sonar-web/src/main/js/components/charts/__tests__/TreeMap-test.tsx67
-rw-r--r--server/sonar-web/yarn.lock1
7 files changed, 27 insertions, 450 deletions
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
index 426abce1033..4cbf1d5d3f6 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
@@ -335,5 +335,5 @@ export default AppWithComponentContext;
const StyledMain = withTheme(styled.main`
background-color: ${themeColor('filterbar')};
background-color: ${themeColor('pageBlock')};
- border: ${themeBorder('default', 'pageBlockBorder')}l;
+ border: ${themeBorder('default', 'pageBlockBorder')};
`);
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx
index 44f4c45ed8c..07e89c39453 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx
@@ -18,16 +18,16 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { scaleLinear, scaleOrdinal } from 'd3-scale';
+import { TreeMap, TreeMapItem } from 'design-system';
import * as React from 'react';
import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
import { colors } from '../../../app/theme';
import ColorBoxLegend from '../../../components/charts/ColorBoxLegend';
import ColorGradientLegend from '../../../components/charts/ColorGradientLegend';
-import TreeMap, { TreeMapItem } from '../../../components/charts/TreeMap';
import QualifierIcon from '../../../components/icons/QualifierIcon';
import { getComponentMeasureUniqueKey } from '../../../helpers/component';
import { RATING_COLORS } from '../../../helpers/constants';
-import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
+import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
import { isDefined } from '../../../helpers/types';
import { MetricKey } from '../../../types/metrics';
@@ -41,7 +41,7 @@ interface Props {
}
interface State {
- treemapItems: TreeMapItem[];
+ treemapItems: Array<TreeMapItem<ComponentMeasureIntern>>;
}
const HEIGHT = 500;
@@ -68,7 +68,10 @@ export default class TreeMapView extends React.PureComponent<Props, State> {
}
}
- getTreemapComponents = ({ components, metric }: Props) => {
+ getTreemapComponents = ({
+ components,
+ metric,
+ }: Props): Array<TreeMapItem<ComponentMeasureIntern>> => {
const colorScale = this.getColorScale(metric);
return components
.map((component) => {
@@ -92,11 +95,12 @@ export default class TreeMapView extends React.PureComponent<Props, State> {
if (sizeValue < 1) {
return undefined;
}
+
return {
+ key: getComponentMeasureUniqueKey(component) ?? '',
color: colorValue ? (colorScale as Function)(colorValue) : undefined,
gradient: !colorValue ? NA_GRADIENT : undefined,
icon: <QualifierIcon fill={colors.baseFontColor} qualifier={component.qualifier} />,
- key: getComponentMeasureUniqueKey(component) ?? '',
label: [component.name, component.branch].filter((s) => !!s).join(' / '),
size: sizeValue,
measureValue: colorValue,
@@ -108,7 +112,7 @@ export default class TreeMapView extends React.PureComponent<Props, State> {
sizeMetric: sizeMeasure.metric,
sizeValue,
}),
- component,
+ sourceData: component,
};
})
.filter(isDefined);
@@ -161,6 +165,12 @@ export default class TreeMapView extends React.PureComponent<Props, State> {
);
};
+ handleSelect(node: TreeMapItem<ComponentMeasureIntern>) {
+ if (node.sourceData) {
+ this.props.handleSelect(node.sourceData);
+ }
+ }
+
renderLegend() {
const { metric } = this.props;
const colorScale = this.getColorScale(metric);
@@ -198,29 +208,25 @@ export default class TreeMapView extends React.PureComponent<Props, State> {
<div className="measure-details-treemap">
<div className="display-flex-start note spacer-bottom">
<span>
- {translateWithParameters(
- 'component_measures.legend.color_x',
- getLocalizedMetricName(metric)
- )}
+ <strong className="sw-mr-1">{translate('component_measures.legend.color')}</strong>
+ {getLocalizedMetricName(metric)}
</span>
<span className="spacer-left flex-1">
- {translateWithParameters(
- 'component_measures.legend.size_x',
- translate(
- 'metric',
- sizeMeasure && sizeMeasure.metric ? sizeMeasure.metric.key : MetricKey.ncloc,
- 'name'
- )
+ <strong className="sw-mr-1">{translate('component_measures.legend.size')}</strong>
+ {translate(
+ 'metric',
+ sizeMeasure && sizeMeasure.metric ? sizeMeasure.metric.key : MetricKey.ncloc,
+ 'name'
)}
</span>
<span>{this.renderLegend()}</span>
</div>
<AutoSizer disableHeight={true}>
{({ width }) => (
- <TreeMap
+ <TreeMap<ComponentMeasureIntern>
height={HEIGHT}
items={treemapItems}
- onRectangleClick={this.props.handleSelect}
+ onRectangleClick={this.handleSelect.bind(this)}
width={width}
/>
)}
diff --git a/server/sonar-web/src/main/js/components/charts/TreeMap.css b/server/sonar-web/src/main/js/components/charts/TreeMap.css
deleted file mode 100644
index f0d15a225a5..00000000000
--- a/server/sonar-web/src/main/js/components/charts/TreeMap.css
+++ /dev/null
@@ -1,95 +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.
- */
-.sonar-d3 .treemap-container {
- position: relative;
-}
-
-.sonar-d3 .treemap-cell {
- position: absolute;
- border-right: 1px solid #fff;
- border-bottom: 1px solid #fff;
- box-sizing: border-box;
- text-align: center;
- overflow: hidden;
-}
-
-.sonar-d3 .treemap-cell:focus {
- outline: none;
-}
-
-.sonar-d3 .treemap-inner {
- display: inline-flex;
- vertical-align: middle;
- align-items: center;
- justify-content: center;
- flex-wrap: wrap;
- padding: var(--gridSize);
- box-sizing: border-box;
- line-height: 1.2;
- background: rgba(0, 0, 0, 0.6);
- border-radius: 2px;
-}
-
-.sonar-d3 .treemap-inner .treemap-icon {
- flex-shrink: 0;
-}
-
-.sonar-d3 .treemap-inner .treemap-icon svg {
- margin-top: 2px;
-}
-
-.sonar-d3 .treemap-inner .treemap-icon svg path {
- fill: var(--barBackgroundColor) !important;
-}
-
-.sonar-d3 .treemap-inner .treemap-text {
- flex-shrink: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- text-align: center;
- color: var(--barBackgroundColor);
-}
-
-.sonar-d3 .treemap-inner .treemap-text-suffix {
- color: var(--barBorderColor);
- font-size: var(--smallFontSize);
-}
-
-.sonar-d3 .treemap-link {
- position: absolute;
- z-index: var(--normalZIndex);
- top: 5px;
- right: 5px;
- line-height: 14px;
- font-size: var(--smallFontSize);
- border-bottom: none;
-}
-
-.sonar-d3 .treemap-link:hover {
- color: #d1eafb;
-}
-
-.sonar-d3 .treemap-link i,
-.sonar-d3 .treemap-link i:before {
- vertical-align: top;
- font-size: inherit;
- line-height: inherit;
-}
diff --git a/server/sonar-web/src/main/js/components/charts/TreeMap.tsx b/server/sonar-web/src/main/js/components/charts/TreeMap.tsx
deleted file mode 100644
index 746411f6b37..00000000000
--- a/server/sonar-web/src/main/js/components/charts/TreeMap.tsx
+++ /dev/null
@@ -1,122 +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 { hierarchy as d3Hierarchy, treemap as d3Treemap } from 'd3-hierarchy';
-import * as React from 'react';
-import { formatMeasure, localizeMetric } from '../../helpers/measures';
-import { Location } from '../../helpers/urls';
-import { ComponentMeasureEnhanced } from '../../types/types';
-import './TreeMap.css';
-import TreeMapRect from './TreeMapRect';
-
-export interface TreeMapItem {
- color?: string;
- gradient?: string;
- icon?: React.ReactNode;
- key: string;
- label: string;
- link?: string | Location;
- measureValue?: string;
- metric?: { key: string; type: string };
- size: number;
- tooltip?: React.ReactNode;
- component: ComponentMeasureEnhanced;
-}
-
-interface HierarchicalTreemapItem extends TreeMapItem {
- children?: TreeMapItem[];
-}
-
-interface Props {
- height: number;
- items: TreeMapItem[];
- onRectangleClick?: (item: ComponentMeasureEnhanced) => void;
- width: number;
-}
-
-export default class TreeMap extends React.PureComponent<Props> {
- mostCommitPrefix = (labels: string[]) => {
- const sortedLabels = labels.slice(0).sort();
- const firstLabel = sortedLabels[0];
- const firstLabelLength = firstLabel.length;
- const lastLabel = sortedLabels[sortedLabels.length - 1];
- let i = 0;
- while (i < firstLabelLength && firstLabel.charAt(i) === lastLabel.charAt(i)) {
- i++;
- }
- const prefix = firstLabel.substr(0, i);
- const prefixTokens = prefix.split(/[\s\\/]/);
- const lastPrefixPart = prefixTokens[prefixTokens.length - 1];
- return prefix.substr(0, prefix.length - lastPrefixPart.length);
- };
-
- handleClick = (component: ComponentMeasureEnhanced) => {
- if (this.props.onRectangleClick) {
- this.props.onRectangleClick(component);
- }
- };
-
- render() {
- const { items, height, width } = this.props;
- const hierarchy = d3Hierarchy({ children: items } as HierarchicalTreemapItem)
- .sum((d) => d.size)
- .sort((a, b) => (b.value || 0) - (a.value || 0));
-
- const treemap = d3Treemap<TreeMapItem>().round(true).size([width, height]);
-
- const nodes = treemap(hierarchy).leaves();
- const prefix = this.mostCommitPrefix(items.map((item) => item.label));
- const halfWidth = width / 2;
- return (
- <div className="sonar-d3">
- <div className="treemap-container" style={{ width, height }}>
- {nodes.map((node) => (
- <TreeMapRect
- fill={node.data.color}
- gradient={node.data.gradient}
- height={node.y1 - node.y0}
- icon={node.data.icon}
- itemKey={node.data.key}
- key={node.data.key}
- label={node.data.label}
- link={node.data.link}
- onClick={() => this.handleClick(node.data.component)}
- placement={node.x0 === 0 || node.x1 < halfWidth ? 'right' : 'left'}
- prefix={prefix}
- value={
- node.data.metric && (
- <>
- {formatMeasure(node.data.measureValue, node.data.metric.type)}
- <span className="little-spacer-left">
- {localizeMetric(node.data.metric.key)}
- </span>
- </>
- )
- }
- tooltip={node.data.tooltip}
- width={node.x1 - node.x0}
- x={node.x0}
- y={node.y0}
- />
- ))}
- </div>
- </div>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx b/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx
deleted file mode 100644
index af9e761fe73..00000000000
--- a/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx
+++ /dev/null
@@ -1,146 +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 { scaleLinear } from 'd3-scale';
-import * as React from 'react';
-import { convertToTo, Location } from '../../helpers/urls';
-import Link from '../common/Link';
-import Tooltip, { Placement } from '../controls/Tooltip';
-import LinkIcon from '../icons/LinkIcon';
-
-const SIZE_SCALE = scaleLinear().domain([3, 15]).range([11, 18]).clamp(true);
-
-interface Props {
- fill?: string;
- gradient?: string;
- height: number;
- icon?: React.ReactNode;
- itemKey: string;
- label: string;
- link?: string | Location;
- onClick?: (item: string) => void;
- placement?: Placement;
- prefix: string;
- tooltip?: React.ReactNode;
- value?: React.ReactNode;
- width: number;
- x: number;
- y: number;
-}
-
-const TEXT_VISIBLE_AT_WIDTH = 80;
-const TEXT_VISIBLE_AT_HEIGHT = 50;
-const ICON_VISIBLE_AT_WIDTH = 60;
-const ICON_VISIBLE_AT_HEIGHT = 30;
-export default class TreeMapRect extends React.PureComponent<Props> {
- handleLinkClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
- event.stopPropagation();
- };
-
- handleRectClick = () => {
- if (this.props.onClick) {
- this.props.onClick(this.props.itemKey);
- }
- };
-
- renderLink = () => {
- const { link, height, width } = this.props;
- const hasMinSize = width >= 24 && height >= 24 && (width >= 48 || height >= 50);
- if (!hasMinSize || link == null) {
- return null;
- }
-
- return (
- <Link className="treemap-link" onClick={this.handleLinkClick} to={convertToTo(link)}>
- <LinkIcon />
- </Link>
- );
- };
-
- renderCell = () => {
- const cellStyles = {
- left: this.props.x,
- top: this.props.y,
- width: this.props.width,
- height: this.props.height,
- backgroundColor: this.props.fill,
- backgroundImage: this.props.gradient,
- backgroundSize: '12px 12px',
- fontSize: SIZE_SCALE(this.props.width / this.props.label.length),
- lineHeight: `${this.props.height}px`,
- cursor: this.props.onClick != null ? 'pointer' : 'default',
- };
- const isTextVisible =
- this.props.width >= TEXT_VISIBLE_AT_WIDTH && this.props.height >= TEXT_VISIBLE_AT_HEIGHT;
- const isIconVisible =
- this.props.width >= ICON_VISIBLE_AT_WIDTH && this.props.height >= ICON_VISIBLE_AT_HEIGHT;
-
- return (
- <div
- className="treemap-cell"
- onClick={this.handleRectClick}
- // eslint-disable-next-line jsx-a11y/role-has-required-aria-props
- role="treeitem"
- style={cellStyles}
- tabIndex={0}
- >
- {isTextVisible && (
- <div className="treemap-inner" style={{ maxWidth: this.props.width }}>
- {this.props.prefix || this.props.value ? (
- <div className="treemap-text">
- <div>
- {isIconVisible && (
- <span className={classNames('treemap-icon', { 'spacer-right': isTextVisible })}>
- {this.props.icon}
- </span>
- )}
-
- {this.props.prefix && (
- <>
- {this.props.prefix}
- <br />
- </>
- )}
-
- {this.props.label.substring(this.props.prefix.length)}
- </div>
-
- <div className="treemap-text-suffix little-spacer-top">{this.props.value}</div>
- </div>
- ) : (
- <div className="treemap-text">{this.props.label}</div>
- )}
- </div>
- )}
- {this.renderLink()}
- </div>
- );
- };
-
- render() {
- const { placement, tooltip } = this.props;
- return (
- <Tooltip overlay={tooltip || undefined} placement={placement || 'left'}>
- {this.renderCell()}
- </Tooltip>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/TreeMap-test.tsx b/server/sonar-web/src/main/js/components/charts/__tests__/TreeMap-test.tsx
deleted file mode 100644
index 0aa96a6b2f8..00000000000
--- a/server/sonar-web/src/main/js/components/charts/__tests__/TreeMap-test.tsx
+++ /dev/null
@@ -1,67 +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 { mount } from 'enzyme';
-import * as React from 'react';
-import { mockComponentMeasureEnhanced } from '../../../helpers/mocks/component';
-import TreeMap from '../TreeMap';
-import TreeMapRect from '../TreeMapRect';
-
-it('should render correctly', () => {
- const items = [
- {
- key: '1',
- size: 10,
- color: '#777',
- label: 'SonarQube :: Server',
- component: mockComponentMeasureEnhanced(),
- },
- {
- key: '2',
- size: 30,
- color: '#777',
- label: 'SonarQube :: Web',
- component: mockComponentMeasureEnhanced(),
- },
- {
- key: '3',
- size: 20,
- gradient: '#777',
- label: 'SonarQube :: Search',
- metric: { key: 'coverage', type: 'PERCENT' },
- component: mockComponentMeasureEnhanced(),
- },
- ];
- const onRectClick = jest.fn();
- const chart = mount(
- <TreeMap height={100} items={items} onRectangleClick={onRectClick} width={100} />
- );
- const rects = chart.find(TreeMapRect);
- expect(rects).toHaveLength(3);
-
- const event: React.MouseEvent<HTMLAnchorElement> = {
- stopPropagation: jest.fn(),
- } as any;
-
- (rects.first().instance() as TreeMapRect).handleLinkClick(event);
- expect(event.stopPropagation).toHaveBeenCalled();
-
- (rects.first().instance() as TreeMapRect).handleRectClick();
- expect(onRectClick).toHaveBeenCalledWith(expect.objectContaining({ key: 'foo' }));
-});
diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock
index 36d303c96f0..598b2b41906 100644
--- a/server/sonar-web/yarn.lock
+++ b/server/sonar-web/yarn.lock
@@ -6191,6 +6191,7 @@ __metadata:
classnames: 2.3.2
clipboard: 2.0.11
d3-array: 3.2.3
+ d3-hierarchy: 3.1.2
d3-scale: 4.0.2
d3-selection: 3.0.0
d3-shape: 3.2.0