import { CustomIcon } from './icons/Icon';
interface Props {
+ ariaLabel?: string;
checked: boolean;
children?: React.ReactNode;
className?: string;
}
export function Checkbox({
+ ariaLabel,
checked,
disabled,
children,
<CheckboxContainer className={className} disabled={disabled}>
{right && children}
<AccessibleCheckbox
- aria-label={title}
+ aria-label={ariaLabel ?? title}
checked={checked}
disabled={disabled ?? loading}
id={id}
--- /dev/null
+/*
+ * 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 tw from 'twin.macro';
+import { BubbleColorVal } from '../types/charts';
+import { Checkbox } from './Checkbox';
+import Tooltip from './Tooltip';
+
+import { themeBorder, themeColor, themeContrast } from '../helpers';
+
+export interface ColorFilterOption {
+ ariaLabel?: string;
+ backgroundColor?: string;
+ borderColor?: string;
+ label: React.ReactNode;
+ overlay?: React.ReactNode;
+ selected: boolean;
+ value: string | number;
+}
+
+interface ColorLegendProps {
+ className?: string;
+ colors: ColorFilterOption[];
+ onColorClick: (color: ColorFilterOption) => void;
+}
+
+export function ColorsLegend(props: ColorLegendProps) {
+ const { className, colors } = props;
+ const theme = useTheme();
+
+ return (
+ <ColorsLegendWrapper className={className}>
+ {colors.map((color, idx) => (
+ <li className="sw-ml-4" key={color.value}>
+ <Tooltip overlay={color.overlay}>
+ <div>
+ <Checkbox
+ ariaLabel={color.ariaLabel}
+ checked={color.selected}
+ onCheck={() => {
+ props.onColorClick(color);
+ }}
+ >
+ <ColorRating
+ style={
+ color.selected
+ ? {
+ backgroundColor:
+ color.borderColor ??
+ themeColor(`bubble.${(idx + 1) as BubbleColorVal}`)({ theme }),
+ borderColor:
+ color.backgroundColor ??
+ themeContrast(`bubble.${(idx + 1) as BubbleColorVal}`)({ theme }),
+ }
+ : {}
+ }
+ >
+ {color.label}
+ </ColorRating>
+ </Checkbox>
+ </div>
+ </Tooltip>
+ </li>
+ ))}
+ </ColorsLegendWrapper>
+ );
+}
+
+const ColorsLegendWrapper = styled.ul`
+ ${tw`sw-flex`}
+`;
+
+const ColorRating = styled.div`
+ width: 20px;
+ height: 20px;
+ line-height: 20px;
+ border-radius: 50%;
+ border: ${themeBorder()};
+ ${tw`sw-flex sw-justify-center`}
+ ${tw`sw-ml-1`}
+`;
export class DeferredSpinner extends React.PureComponent<Props, State> {
timer?: number;
-
+ static displayName = 'DeferredSpinner';
state: State = { showSpinner: false };
componentDidMount() {
--- /dev/null
+/*
+ * 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 { render } from '../../helpers/testUtils';
+import { FCProps } from '../../types/misc';
+import { ColorsLegend } from '../ColorsLegend';
+
+const colors = [
+ {
+ selected: true,
+ overlay: 'Overlay A',
+ label: 'A',
+ value: '1',
+ },
+ {
+ selected: true,
+ overlay: 'Overlay B',
+ label: 'B',
+ value: '2',
+ },
+];
+
+it('should render correctly', () => {
+ renderColorLegend();
+ expect(screen.getByRole('checkbox', { name: 'A' })).toBeInTheDocument();
+ expect(screen.getByRole('checkbox', { name: 'B' })).toBeInTheDocument();
+});
+
+it('should react when a rating is clicked', () => {
+ const onColorClick = jest.fn();
+ renderColorLegend({ onColorClick });
+
+ screen.getByRole('checkbox', { name: 'A' }).click();
+ expect(onColorClick).toHaveBeenCalledWith(colors[0]);
+});
+
+function renderColorLegend(props: Partial<FCProps<typeof ColorsLegend>> = {}) {
+ return render(<ColorsLegend colors={colors} onColorClick={jest.fn()} {...props} />);
+}
export * from './Card';
export * from './Checkbox';
export * from './CodeSnippet';
+export * from './ColorsLegend';
export * from './CoverageIndicator';
export * from './DatePicker';
export * from './DateRangePicker';
import PromotionNotification from './promotion-notification/PromotionNotification';
import UpdateNotification from './update-notification/UpdateNotification';
-const TEMP_PAGELIST_WITH_NEW_BACKGROUND = ['/dashboard', '/security_hotspots'];
+const TEMP_PAGELIST_WITH_NEW_BACKGROUND = [
+ '/dashboard',
+ '/security_hotspots',
+ '/component_measures',
+];
export default function GlobalContainer() {
// it is important to pass `location` down to `GlobalNav` to trigger render on url change
if (displayOverview) {
return (
- <StyledMain className="sw-rounded-1 sw-p-6 sw-mb-4">
+ <StyledMain className="sw-rounded-1 sw-mb-4">
<MeasureOverviewContainer
branchLike={branchLike}
domain={query.metric}
export default AppWithComponentContext;
const StyledMain = withTheme(styled.main`
- background-color: ${themeColor('filterbar')};
background-color: ${themeColor('pageBlock')};
border: ${themeBorder('default', 'pageBlockBorder')};
`);
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import styled from '@emotion/styled';
+import { themeBorder } from 'design-system';
import * as React from 'react';
interface Props {
export default function MeasureContentHeader({ left, right }: Props) {
return (
- <div>
+ <StyledHeader className="sw-py-3 sw-px-6 sw-flex sw-justify-between sw-items-center">
<div>{left}</div>
<div>{right}</div>
- </div>
+ </StyledHeader>
);
}
+
+const StyledHeader = styled.div`
+ border-bottom: ${themeBorder('default', 'pageBlockBorder')};
+`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { DeferredSpinner } from 'design-system';
import * as React from 'react';
import { getComponentLeaves } from '../../../api/components';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
-import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import PageActions from '../../../components/ui/PageActions';
import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like';
import { BranchLike } from '../../../types/branch-like';
Paging,
Period,
} from '../../../types/types';
-import BubbleChart from '../drilldown/BubbleChart';
+import BubbleChartView from '../drilldown/BubbleChartView';
import { BUBBLES_FETCH_LIMIT, enhanceComponent, getBubbleMetrics, hasFullMeasures } from '../utils';
import LeakPeriodLegend from './LeakPeriodLegend';
import MeasureContentHeader from './MeasureContentHeader';
);
};
- renderContent() {
+ renderContent(isFile: boolean) {
const { branchLike, component, domain, metrics } = this.props;
const { paging } = this.state;
- if (isFile(component.qualifier)) {
+ if (isFile) {
return (
<div className="measure-details-viewer">
<SourceViewer
}
return (
- <BubbleChart
- componentKey={component.key}
+ <BubbleChartView
+ component={component}
branchLike={branchLike}
components={this.state.components}
domain={domain}
render() {
const { branchLike, className, component, leakPeriod, loading, rootComponent } = this.props;
const displayLeak = hasFullMeasures(branchLike);
+ const isFileComponent = isFile(component.qualifier);
+
return (
<div className={className}>
<A11ySkipTarget anchor="measures_main" />
/>
}
right={
- <PageActions
- componentQualifier={rootComponent.qualifier}
- current={this.state.components.length}
- />
+ <>
+ <PageActions
+ componentQualifier={rootComponent.qualifier}
+ current={this.state.components.length}
+ />
+ {leakPeriod && displayLeak && (
+ <LeakPeriodLegend
+ className="pull-right"
+ component={component}
+ period={leakPeriod}
+ />
+ )}
+ </>
}
/>
- {leakPeriod && displayLeak && (
- <LeakPeriodLegend className="pull-right" component={component} period={leakPeriod} />
- )}
- <DeferredSpinner loading={loading} />
-
- {!loading && this.renderContent()}
+ <div className="sw-p-6">
+ <DeferredSpinner loading={loading} />
+ {!loading && this.renderContent(isFileComponent)}
+ </div>
</div>
);
}
+++ /dev/null
-/*
- * 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 { BubbleColorVal, BubbleChart as OriginalBubbleChart } from 'design-system';
-import * as React from 'react';
-import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend';
-import Link from '../../../components/common/Link';
-import HelpTooltip from '../../../components/controls/HelpTooltip';
-import {
- getLocalizedMetricDomain,
- getLocalizedMetricName,
- translate,
- translateWithParameters,
-} from '../../../helpers/l10n';
-import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
-import { isDefined } from '../../../helpers/types';
-import { getComponentDrilldownUrl } from '../../../helpers/urls';
-import { BranchLike } from '../../../types/branch-like';
-import { isProject } from '../../../types/component';
-import { MetricKey } from '../../../types/metrics';
-import {
- ComponentMeasureEnhanced,
- ComponentMeasureIntern,
- Dict,
- Metric,
- Paging,
-} from '../../../types/types';
-import {
- BUBBLES_FETCH_LIMIT,
- getBubbleMetrics,
- getBubbleYDomain,
- isProjectOverview,
-} from '../utils';
-import EmptyResult from './EmptyResult';
-
-const HEIGHT = 500;
-
-interface Props {
- componentKey: string;
- components: ComponentMeasureEnhanced[];
- branchLike?: BranchLike;
- domain: string;
- metrics: Dict<Metric>;
- paging?: Paging;
- updateSelected: (component: ComponentMeasureIntern) => void;
-}
-
-interface State {
- ratingFilters: { [rating: number]: boolean };
-}
-
-export default class BubbleChart extends React.PureComponent<Props, State> {
- state: State = {
- ratingFilters: {},
- };
-
- getMeasureVal = (component: ComponentMeasureEnhanced, metric: Metric) => {
- const measure = component.measures.find((measure) => measure.metric.key === metric.key);
- if (!measure) {
- return undefined;
- }
- return Number(isDiffMetric(metric.key) ? measure.leak : measure.value);
- };
-
- getTooltip(
- component: ComponentMeasureEnhanced,
- values: { x: number; y: number; size: number; colors?: Array<number | undefined> },
- metrics: { x: Metric; y: Metric; size: Metric; colors?: Metric[] }
- ) {
- const inner = [
- [component.name, isProject(component.qualifier) ? component.branch : undefined]
- .filter((s) => !!s)
- .join(' / '),
- `${metrics.x.name}: ${formatMeasure(values.x, metrics.x.type)}`,
- `${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`,
- `${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}`,
- ].filter((s) => !!s);
- const { colors: valuesColors } = values;
- const { colors: metricColors } = metrics;
- if (valuesColors && metricColors) {
- metricColors.forEach((metric, idx) => {
- const colorValue = valuesColors[idx];
- if (colorValue || colorValue === 0) {
- inner.push(`${metric.name}: ${formatMeasure(colorValue, metric.type)}`);
- }
- });
- }
- return (
- <div className="text-left">
- {inner.map((line, index) => (
- <React.Fragment key={index}>
- {line}
- {index < inner.length - 1 && <br />}
- </React.Fragment>
- ))}
- </div>
- );
- }
-
- handleRatingFilterClick = (selection: number) => {
- this.setState(({ ratingFilters }) => {
- return { ratingFilters: { ...ratingFilters, [selection]: !ratingFilters[selection] } };
- });
- };
-
- handleBubbleClick = (component: ComponentMeasureEnhanced) => this.props.updateSelected(component);
-
- getDescription(domain: string) {
- const description = `component_measures.overview.${domain}.description`;
- const translatedDescription = translate(description);
- if (description === translatedDescription) {
- return null;
- }
- return translatedDescription;
- }
-
- renderBubbleChart(metrics: { x: Metric; y: Metric; size: Metric; colors?: Metric[] }) {
- const { ratingFilters } = this.state;
-
- const items = this.props.components
- .map((component) => {
- 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?.map((metric) => this.getMeasureVal(component, metric));
- if ((!x && x !== 0) || (!y && y !== 0) || (!size && size !== 0)) {
- return undefined;
- }
-
- const colorRating = colors && Math.max(...colors.filter(isDefined));
-
- // Filter out items that match ratingFilters
- if (colorRating !== undefined && ratingFilters[colorRating]) {
- return undefined;
- }
-
- return {
- x,
- y,
- size,
- color: (colorRating as BubbleColorVal) ?? 0,
- data: component,
- tooltip: this.getTooltip(component, { x, y, size, colors }, metrics),
- };
- })
- .filter(isDefined);
-
- const formatXTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.x.type);
- const formatYTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.y.type);
-
- let xDomain: [number, number] | undefined;
- if (items.reduce((acc, item) => acc + item.x, 0) === 0) {
- // All items are on the 0 axis. This won't display the grid on the X axis,
- // which can make the graph a little hard to read. Force the display of
- // the X grid.
- xDomain = [0, 100];
- }
-
- return (
- <OriginalBubbleChart<ComponentMeasureEnhanced>
- data-testid="bubble-chart"
- formatXTick={formatXTick}
- formatYTick={formatYTick}
- height={HEIGHT}
- items={items}
- onBubbleClick={this.handleBubbleClick}
- padding={[0, 4, 50, 60]}
- yDomain={getBubbleYDomain(this.props.domain)}
- xDomain={xDomain}
- />
- );
- }
-
- renderChartHeader(domain: string, sizeMetric: Metric, colorsMetric?: Metric[]) {
- const { ratingFilters } = this.state;
- const { paging } = this.props;
-
- const title = isProjectOverview(domain)
- ? translate('component_measures.overview', domain, 'title')
- : translateWithParameters(
- 'component_measures.domain_x_overview',
- getLocalizedMetricDomain(domain)
- );
- return (
- <div className="measure-overview-bubble-chart-header">
- <span className="measure-overview-bubble-chart-title">
- <div className="display-flex-center">
- {title}
- <HelpTooltip className="spacer-left" overlay={this.getDescription(domain)} />
- </div>
-
- {paging?.total && paging?.total > BUBBLES_FETCH_LIMIT && (
- <div className="note spacer-top">
- ({translate('component_measures.legend.only_first_500_files')})
- </div>
- )}
- </span>
- <span className="measure-overview-bubble-chart-legend">
- <span className="note">
- {colorsMetric && (
- <span className="spacer-right">
- {translateWithParameters(
- 'component_measures.legend.color_x',
- colorsMetric.length > 1
- ? translateWithParameters(
- 'component_measures.legend.worse_of_x_y',
- ...colorsMetric.map((metric) => getLocalizedMetricName(metric))
- )
- : getLocalizedMetricName(colorsMetric[0])
- )}
- </span>
- )}
- {translateWithParameters(
- 'component_measures.legend.size_x',
- getLocalizedMetricName(sizeMetric)
- )}
- </span>
- {colorsMetric && (
- <ColorRatingsLegend
- className="spacer-top"
- filters={ratingFilters}
- onRatingClick={this.handleRatingFilterClick}
- />
- )}
- </span>
- </div>
- );
- }
-
- render() {
- if (this.props.components.length <= 0) {
- return <EmptyResult />;
- }
- const { domain, componentKey, branchLike } = this.props;
- const metrics = getBubbleMetrics(domain, this.props.metrics);
-
- return (
- <div className="measure-overview-bubble-chart">
- {this.renderChartHeader(domain, metrics.size, metrics.colors)}
- <div className="measure-overview-bubble-chart-content">
- <div className="text-center small spacer-top spacer-bottom">
- <Link
- to={getComponentDrilldownUrl({
- componentKey,
- branchLike,
- metric: isProjectOverview(domain) ? MetricKey.violations : metrics.size.key,
- listView: true,
- })}
- >
- {translate('component_measures.overview.see_data_as_list')}
- </Link>
- </div>
- {this.renderBubbleChart(metrics)}
- </div>
- <div className="measure-overview-bubble-chart-axis x">
- {getLocalizedMetricName(metrics.x)}
- </div>
- <div className="measure-overview-bubble-chart-axis y">
- {getLocalizedMetricName(metrics.y)}
- </div>
- </div>
- );
- }
-}
--- /dev/null
+/*
+ * 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 styled from '@emotion/styled';
+import {
+ BubbleColorVal,
+ HelperHintIcon,
+ Highlight,
+ Link,
+ BubbleChart as OriginalBubbleChart,
+ themeColor,
+} from 'design-system';
+import * as React from 'react';
+import HelpTooltip from '../../../components/controls/HelpTooltip';
+import {
+ getLocalizedMetricDomain,
+ getLocalizedMetricName,
+ translate,
+ translateWithParameters,
+} from '../../../helpers/l10n';
+import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
+import { isDefined } from '../../../helpers/types';
+import { getComponentDrilldownUrl } from '../../../helpers/urls';
+import { BranchLike } from '../../../types/branch-like';
+import { isProject, isView } from '../../../types/component';
+import { MetricKey } from '../../../types/metrics';
+import {
+ ComponentMeasureEnhanced,
+ ComponentMeasure as ComponentMeasureI,
+ ComponentMeasureIntern,
+ Dict,
+ Metric,
+ Paging,
+} from '../../../types/types';
+import {
+ BUBBLES_FETCH_LIMIT,
+ getBubbleMetrics,
+ getBubbleYDomain,
+ isProjectOverview,
+} from '../utils';
+import ColorRatingsLegend from './ColorRatingsLegend';
+import EmptyResult from './EmptyResult';
+
+const HEIGHT = 500;
+
+interface Props {
+ component: ComponentMeasureI;
+ components: ComponentMeasureEnhanced[];
+ branchLike?: BranchLike;
+ domain: string;
+ metrics: Dict<Metric>;
+ paging?: Paging;
+ updateSelected: (component: ComponentMeasureIntern) => void;
+}
+
+interface State {
+ ratingFilters: { [rating: number]: boolean };
+}
+
+export default class BubbleChartView extends React.PureComponent<Props, State> {
+ state: State = {
+ ratingFilters: {},
+ };
+
+ getMeasureVal = (component: ComponentMeasureEnhanced, metric: Metric) => {
+ const measure = component.measures.find((measure) => measure.metric.key === metric.key);
+ if (!measure) {
+ return undefined;
+ }
+ return Number(isDiffMetric(metric.key) ? measure.leak : measure.value);
+ };
+
+ getTooltip(
+ component: ComponentMeasureEnhanced,
+ values: { x: number; y: number; size: number; colors?: Array<number | undefined> },
+ metrics: { x: Metric; y: Metric; size: Metric; colors?: Metric[] }
+ ) {
+ const inner = [
+ [component.name, isProject(component.qualifier) ? component.branch : undefined]
+ .filter((s) => !!s)
+ .join(' / '),
+ `${metrics.x.name}: ${formatMeasure(values.x, metrics.x.type)}`,
+ `${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`,
+ `${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}`,
+ ].filter((s) => !!s);
+ const { colors: valuesColors } = values;
+ const { colors: metricColors } = metrics;
+ if (valuesColors && metricColors) {
+ metricColors.forEach((metric, idx) => {
+ const colorValue = valuesColors[idx];
+ if (colorValue || colorValue === 0) {
+ inner.push(`${metric.name}: ${formatMeasure(colorValue, metric.type)}`);
+ }
+ });
+ }
+ return (
+ <div className="sw-text-left">
+ {inner.map((line, index) => (
+ <React.Fragment key={index}>
+ {line}
+ {index < inner.length - 1 && <br />}
+ </React.Fragment>
+ ))}
+ </div>
+ );
+ }
+
+ handleRatingFilterClick = (selection: number) => {
+ this.setState(({ ratingFilters }) => {
+ return { ratingFilters: { ...ratingFilters, [selection]: !ratingFilters[selection] } };
+ });
+ };
+
+ handleBubbleClick = (component: ComponentMeasureEnhanced) => this.props.updateSelected(component);
+
+ getDescription(domain: string) {
+ const description = `component_measures.overview.${domain}.description`;
+ const translatedDescription = translate(description);
+ if (description === translatedDescription) {
+ return null;
+ }
+ return translatedDescription;
+ }
+
+ renderBubbleChart(metrics: { x: Metric; y: Metric; size: Metric; colors?: Metric[] }) {
+ const { ratingFilters } = this.state;
+
+ const items = this.props.components
+ .map((component) => {
+ 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?.map((metric) => this.getMeasureVal(component, metric));
+ if ((!x && x !== 0) || (!y && y !== 0) || (!size && size !== 0)) {
+ return undefined;
+ }
+
+ const colorRating = colors && Math.max(...colors.filter(isDefined));
+
+ // Filter out items that match ratingFilters
+ if (colorRating !== undefined && ratingFilters[colorRating]) {
+ return undefined;
+ }
+
+ return {
+ x,
+ y,
+ size,
+ color: (colorRating as BubbleColorVal) ?? 0,
+ data: component,
+ tooltip: this.getTooltip(component, { x, y, size, colors }, metrics),
+ };
+ })
+ .filter(isDefined);
+
+ const formatXTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.x.type);
+ const formatYTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.y.type);
+
+ let xDomain: [number, number] | undefined;
+ if (items.reduce((acc, item) => acc + item.x, 0) === 0) {
+ // All items are on the 0 axis. This won't display the grid on the X axis,
+ // which can make the graph a little hard to read. Force the display of
+ // the X grid.
+ xDomain = [0, 100];
+ }
+
+ return (
+ <OriginalBubbleChart<ComponentMeasureEnhanced>
+ data-testid="bubble-chart"
+ formatXTick={formatXTick}
+ formatYTick={formatYTick}
+ height={HEIGHT}
+ items={items}
+ onBubbleClick={this.handleBubbleClick}
+ padding={[0, 4, 50, 100]}
+ yDomain={getBubbleYDomain(this.props.domain)}
+ xDomain={xDomain}
+ />
+ );
+ }
+
+ renderChartHeader(domain: string, sizeMetric: Metric, colorsMetric?: Metric[]) {
+ const { ratingFilters } = this.state;
+ const { paging, component, branchLike, metrics: propsMetrics } = this.props;
+ const metrics = getBubbleMetrics(domain, propsMetrics);
+
+ const title = isProjectOverview(domain)
+ ? translate('component_measures.overview', domain, 'title')
+ : translateWithParameters(
+ 'component_measures.domain_x_overview',
+ getLocalizedMetricDomain(domain)
+ );
+
+ return (
+ <div className="sw-flex sw-justify-between sw-gap-3">
+ <div>
+ <div className="sw-flex sw-items-center sw-whitespace-nowrap">
+ <Highlight className="it__measure-overview-bubble-chart-title">{title}</Highlight>
+ <HelpTooltip className="spacer-left" overlay={this.getDescription(domain)}>
+ <HelperHintIcon />
+ </HelpTooltip>
+ </div>
+
+ {paging?.total && paging?.total > BUBBLES_FETCH_LIMIT && (
+ <div className="sw-mt-2">
+ ({translate('component_measures.legend.only_first_500_files')})
+ </div>
+ )}
+ {(isView(component?.qualifier) || isProject(component?.qualifier)) && (
+ <div className="sw-mt-2">
+ <Link
+ to={getComponentDrilldownUrl({
+ componentKey: component.key,
+ branchLike,
+ metric: isProjectOverview(domain) ? MetricKey.violations : metrics.size.key,
+ listView: true,
+ })}
+ >
+ {translate('component_measures.overview.see_data_as_list')}
+ </Link>
+ </div>
+ )}
+ </div>
+
+ <div className="sw-flex sw-flex-col sw-items-end">
+ <div className="sw-text-right">
+ {colorsMetric && (
+ <span className="sw-mr-3">
+ <strong className="sw-body-sm-highlight">
+ {translate('component_measures.legend.color')}
+ </strong>{' '}
+ {colorsMetric.length > 1
+ ? translateWithParameters(
+ 'component_measures.legend.worse_of_x_y',
+ ...colorsMetric.map((metric) => getLocalizedMetricName(metric))
+ )
+ : getLocalizedMetricName(colorsMetric[0])}
+ </span>
+ )}
+ <strong className="sw-body-sm-highlight">
+ {translate('component_measures.legend.size')}
+ </strong>{' '}
+ {getLocalizedMetricName(sizeMetric)}
+ </div>
+ {colorsMetric && (
+ <ColorRatingsLegend
+ className="spacer-top"
+ filters={ratingFilters}
+ onRatingClick={this.handleRatingFilterClick}
+ />
+ )}
+ </div>
+ </div>
+ );
+ }
+
+ render() {
+ if (this.props.components.length <= 0) {
+ return <EmptyResult />;
+ }
+ const { domain } = this.props;
+ const metrics = getBubbleMetrics(domain, this.props.metrics);
+
+ return (
+ <BubbleChartWrapper className="sw-relative sw-body-sm">
+ {this.renderChartHeader(domain, metrics.size, metrics.colors)}
+ {this.renderBubbleChart(metrics)}
+ <div className="sw-text-center">{getLocalizedMetricName(metrics.x)}</div>
+ <YAxis className="sw-absolute sw-top-1/2 sw-left-3">
+ {getLocalizedMetricName(metrics.y)}
+ </YAxis>
+ </BubbleChartWrapper>
+ );
+ }
+}
+
+const BubbleChartWrapper = styled.div`
+ color: ${themeColor('pageContentLight')};
+`;
+
+const YAxis = styled.div`
+ transform: rotate(-90deg) translateX(-50%);
+ transform-origin: left;
+`;
--- /dev/null
+/*
+ * 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.
+ */
+
+/*
+ * 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 { ColorFilterOption, ColorsLegend } from 'design-system';
+import * as React from 'react';
+import { translateWithParameters } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+import { MetricType } from '../../../types/metrics';
+
+export interface ColorRatingsLegendProps {
+ className?: string;
+ filters: { [rating: number]: boolean };
+ onRatingClick: (selection: number) => void;
+}
+
+const RATINGS = [1, 2, 3, 4, 5];
+
+export default function ColorRatingsLegend(props: ColorRatingsLegendProps) {
+ const { className, filters } = props;
+
+ const ratingsColors = RATINGS.map((rating) => {
+ const formattedMeasure = formatMeasure(rating, MetricType.Rating);
+ return {
+ overlay: translateWithParameters('component_measures.legend.help_x', formattedMeasure),
+ ariaLabel: translateWithParameters('component_measures.legend.help_x', formattedMeasure),
+ label: formattedMeasure,
+ value: rating,
+ selected: !filters[rating],
+ };
+ });
+
+ const handleColorClick = (color: ColorFilterOption) => {
+ props.onRatingClick(color.value as number);
+ };
+
+ return (
+ <ColorsLegend className={className} colors={ratingsColors} onColorClick={handleColorClick} />
+ );
+}
{sortedItems.map((item) =>
typeof item === 'string' ? (
showFullMeasures && (
- <SubnavigationSubheading>
+ <SubnavigationSubheading key={item}>
{translate('component_measures.subnavigation_category', item)}
</SubnavigationSubheading>
)
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-.domain-measures-value {
- margin-right: 4px;
-}
-
-.domain-measures-value span {
- line-height: 16px;
-}
-
-.domain-measures-value .rating {
- margin-left: -4px;
- margin-right: -4px;
-}
-
button.search-navigator-facet {
text-align: start;
}
.measure-favorite svg {
vertical-align: middle;
}
-
-.measure-overview-bubble-chart {
- position: relative;
- border: 1px solid var(--barBorderColor);
- background-color: #fff;
-}
-
-.measure-overview-bubble-chart-content {
- padding: 0;
- padding-left: 60px;
-}
-
-.measure-overview-bubble-chart-header {
- display: flex;
- align-items: center;
- padding: 16px;
- border-bottom: 1px solid var(--barBorderColor);
-}
-
-.measure-overview-bubble-chart-title {
- position: absolute;
-}
-
-.measure-overview-bubble-chart-legend {
- display: flex;
- flex-direction: column;
- text-align: center;
- flex-grow: 1;
-}
-
-.measure-overview-bubble-chart-footer {
- padding: 15px 60px;
- border-top: 1px solid var(--barBorderColor);
- text-align: center;
- font-size: var(--smallFontSize);
- line-height: 1.4;
-}
-
-.measure-overview-bubble-chart-axis {
- color: var(--secondFontColor);
- font-size: var(--smallFontSize);
-}
-
-.measure-overview-bubble-chart-axis.x {
- position: relative;
- top: -8px;
- padding-bottom: 8px;
- text-align: center;
-}
-
-.measure-overview-bubble-chart-axis.y {
- position: absolute;
- top: 50%;
- left: 30px;
- transform: rotate(-90deg) translateX(-50%);
- transform-origin: left;
-}
+++ /dev/null
-/*
- * 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 * as React from 'react';
-import Tooltip from '../../components/controls/Tooltip';
-import { RATING_COLORS } from '../../helpers/constants';
-import { translateWithParameters } from '../../helpers/l10n';
-import { formatMeasure } from '../../helpers/measures';
-import Checkbox from '../controls/Checkbox';
-import './ColorBoxLegend.css';
-
-export interface ColorRatingsLegendProps {
- className?: string;
- filters: { [rating: number]: boolean };
- onRatingClick: (selection: number) => void;
-}
-
-const RATINGS = [1, 2, 3, 4, 5];
-
-export default function ColorRatingsLegend(props: ColorRatingsLegendProps) {
- const { className, filters } = props;
- return (
- <ul className={classNames('color-box-legend', className)}>
- {RATINGS.map((rating) => (
- <li key={rating}>
- <Tooltip
- overlay={translateWithParameters(
- 'component_measures.legend.help_x',
- formatMeasure(rating, 'RATING')
- )}
- >
- <Checkbox
- className="display-flex-center"
- checked={!filters[rating]}
- onCheck={() => props.onRatingClick(rating)}
- >
- <span
- className="color-box-legend-rating little-spacer-left"
- style={{
- borderColor: RATING_COLORS[rating - 1].stroke,
- backgroundColor: RATING_COLORS[rating - 1].fillTransparent,
- }}
- >
- {formatMeasure(rating, 'RATING')}
- </span>
- </Checkbox>
- </Tooltip>
- </li>
- ))}
- </ul>
- );
-}
+++ /dev/null
-/*
- * 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 * as React from 'react';
-import { renderComponent } from '../../../helpers/testReactTestingUtils';
-import ColorRatingsLegend, { ColorRatingsLegendProps } from '../ColorRatingsLegend';
-
-it('should render correctly', () => {
- renderColorRatingsLegend();
- expect(screen.getByRole('checkbox', { name: 'A' })).toBeInTheDocument();
- expect(screen.getByRole('checkbox', { name: 'B' })).toBeInTheDocument();
- expect(screen.getByRole('checkbox', { name: 'C' })).toBeInTheDocument();
- expect(screen.getByRole('checkbox', { name: 'D' })).toBeInTheDocument();
- expect(screen.getByRole('checkbox', { name: 'E' })).toBeInTheDocument();
-});
-
-it('should react when a rating is clicked', () => {
- const onRatingClick = jest.fn();
- renderColorRatingsLegend({ onRatingClick });
-
- screen.getByRole('checkbox', { name: 'D' }).click();
- expect(onRatingClick).toHaveBeenCalledWith(4);
-});
-
-function renderColorRatingsLegend(props: Partial<ColorRatingsLegendProps> = {}) {
- return renderComponent(
- <ColorRatingsLegend filters={{ 2: true }} onRatingClick={jest.fn()} {...props} />
- );
-}
component_measures.tab.list=List
component_measures.tab.treemap=Treemap
component_measures.view_as=View as
-component_measures.legend.color_x=Color: {0}
-component_measures.legend.size_x=Size: {0}
+component_measures.legend.color=Color:
+component_measures.legend.size=Size:
component_measures.legend.worse_of_x_y=Worse of {0} and {1}
component_measures.legend.help_x=Click to toggle visibility for data with rating {0}.
component_measures.legend.only_first_500_files=Only showing data for the first 500 files