diff options
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 |