Browse Source

SONAR-19391 Measures page sidebar adopts the new UI

tags/10.1.0.73491
stanislavh 1 year ago
parent
commit
0bc5896c6d

+ 12
- 2
server/sonar-web/design-system/src/components/icons/Icon.tsx View File

@@ -26,9 +26,11 @@ import { themeColor } from '../../helpers/theme';
import { CSSColor, ThemeColors } from '../../types/theme';

interface Props {
'aria-hidden'?: boolean | 'true' | 'false';
'aria-label'?: string;
children: React.ReactNode;
className?: string;
description?: React.ReactNode;
}

export interface IconProps extends Omit<Props, 'children'> {
@@ -38,10 +40,17 @@ export interface IconProps extends Omit<Props, 'children'> {
}

export function CustomIcon(props: Props) {
const { 'aria-label': ariaLabel, children, className, ...iconProps } = props;
const {
'aria-label': ariaLabel,
'aria-hidden': ariaHidden,
children,
className,
description,
...iconProps
} = props;
return (
<svg
aria-hidden={ariaLabel ? 'false' : 'true'}
aria-hidden={ariaHidden ?? ariaLabel ? 'false' : 'true'}
aria-label={ariaLabel}
className={className}
fill="none"
@@ -63,6 +72,7 @@ export function CustomIcon(props: Props) {
xmlnsXlink="http://www.w3.org/1999/xlink"
{...iconProps}
>
{description && <desc>{description}</desc>}
{children}
</svg>
);

+ 25
- 9
server/sonar-web/design-system/src/components/subnavigation/SubnavigationAccordion.tsx View File

@@ -18,27 +18,43 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import styled from '@emotion/styled';
import { ReactNode, useCallback } from 'react';
import { ReactNode, useCallback, useState } from 'react';
import tw from 'twin.macro';
import { themeColor, themeContrast } from '../../helpers/theme';
import { BareButton } from '../buttons';
import { OpenCloseIndicator } from '../icons/OpenCloseIndicator';
import { SubnavigationGroup } from './SubnavigationGroup';

interface Props {
interface CommonProps {
children: ReactNode;
className?: string;
expanded?: boolean;
header: ReactNode;
id: string;
onSetExpanded?: (expanded: boolean) => void;
}

interface ControlledProps extends CommonProps {
expanded: boolean | undefined;
initExpanded?: never;
}

interface UncontrolledProps extends CommonProps {
expanded?: never;
initExpanded?: boolean;
}

type Props = ControlledProps | UncontrolledProps;

export function SubnavigationAccordion(props: Props) {
const { children, className, header, id, expanded = true, onSetExpanded } = props;
const { children, className, expanded, header, id, initExpanded, onSetExpanded } = props;

const [innerExpanded, setInnerExpanded] = useState(initExpanded ?? false);
const finalExpanded = expanded ?? innerExpanded;

const toggleExpanded = useCallback(() => {
onSetExpanded?.(!expanded);
}, [expanded, onSetExpanded]);
setInnerExpanded(!finalExpanded);
onSetExpanded?.(!finalExpanded);
}, [finalExpanded, onSetExpanded]);

return (
<SubnavigationGroup
@@ -49,14 +65,14 @@ export function SubnavigationAccordion(props: Props) {
>
<SubnavigationAccordionItem
aria-controls={`${id}-subnavigation-accordion`}
aria-expanded={expanded}
aria-expanded={finalExpanded}
id={`${id}-subnavigation-accordion-button`}
onClick={toggleExpanded}
>
{header}
<OpenCloseIndicator open={Boolean(expanded)} />
<OpenCloseIndicator open={finalExpanded} />
</SubnavigationAccordionItem>
{expanded && children}
{finalExpanded && children}
</SubnavigationGroup>
);
}

+ 34
- 0
server/sonar-web/design-system/src/components/subnavigation/SubnavigationSubheading.tsx View File

@@ -0,0 +1,34 @@
/*
* 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 tw from 'twin.macro';
import { themeColor, themeContrast } from '../../helpers/theme';

export const SubnavigationSubheading = styled.div`
${tw`sw-flex`}
${tw`sw-box-border`}
${tw`sw-body-sm`}
${tw`sw-px-4 sw-pt-6 sw-pb-2`}
${tw`sw-w-full`}

color: ${themeContrast('subnavigationSubheading')};
background-color: ${themeColor('subnavigationSubheading')};
`;
SubnavigationSubheading.displayName = 'SubnavigationSubheading';

+ 3
- 3
server/sonar-web/design-system/src/components/subnavigation/__tests__/SubnavigationAccordion-test.tsx View File

@@ -31,17 +31,17 @@ it('should have correct style and html structure', () => {
});

it('should display expanded', () => {
setupWithProps({ expanded: true });
setupWithProps({ initExpanded: true });

expect(screen.getByRole('button', { expanded: true })).toBeVisible();
expect(screen.getByText('Foo')).toBeVisible();
});

it('should display expanded by default', () => {
it('should display collapsed by default', () => {
setupWithProps();

expect(screen.getByRole('button')).toBeVisible();
expect(screen.getByText('Foo')).toBeVisible();
expect(screen.queryByText('Foo')).not.toBeInTheDocument();
});

it('should toggle expand', async () => {

+ 1
- 0
server/sonar-web/design-system/src/components/subnavigation/index.ts View File

@@ -21,3 +21,4 @@ export * from './SubnavigationAccordion';
export * from './SubnavigationGroup';
export * from './SubnavigationHeading';
export * from './SubnavigationItem';
export * from './SubnavigationSubheading';

+ 2
- 1
server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx View File

@@ -42,6 +42,7 @@ import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../../he
import { translate } from '../../../helpers/l10n';
import { BranchLike } from '../../../types/branch-like';
import { ComponentQualifier } from '../../../types/component';
import { MetricKey } from '../../../types/metrics';
import {
ComponentMeasure,
Dict,
@@ -243,7 +244,7 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {

const hideDrilldown =
isPullRequest(branchLike) &&
(metric.key === 'coverage' || metric.key === 'duplicated_lines_density');
(metric.key === MetricKey.coverage || metric.key === MetricKey.duplicated_lines_density);

if (hideDrilldown) {
return (

+ 0
- 173
server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx View File

@@ -1,173 +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 * as React from 'react';
import FacetBox from '../../../components/facet/FacetBox';
import FacetHeader from '../../../components/facet/FacetHeader';
import FacetItem from '../../../components/facet/FacetItem';
import FacetItemsList from '../../../components/facet/FacetItemsList';
import BubblesIcon from '../../../components/icons/BubblesIcon';
import {
getLocalizedCategoryMetricName,
getLocalizedMetricDomain,
getLocalizedMetricName,
hasMessage,
translate,
} from '../../../helpers/l10n';
import { MeasureEnhanced } from '../../../types/types';
import {
addMeasureCategories,
filterMeasures,
hasBubbleChart,
hasFacetStat,
sortMeasures,
} from '../utils';
import FacetMeasureValue from './FacetMeasureValue';

interface Props {
domain: { name: string; measures: MeasureEnhanced[] };
onChange: (metric: string) => void;
onToggle: (property: string) => void;
open: boolean;
selected: string;
showFullMeasures: boolean;
}

export default class DomainFacet extends React.PureComponent<Props> {
getValues = () => {
const { domain, selected } = this.props;
const measureSelected = domain.measures.find((measure) => measure.metric.key === selected);
const overviewSelected = domain.name === selected && this.hasOverview(domain.name);
if (measureSelected) {
return [getLocalizedMetricName(measureSelected.metric)];
}
return overviewSelected ? [translate('component_measures.domain_overview')] : [];
};

handleHeaderClick = () => {
this.props.onToggle(this.props.domain.name);
};

hasFacetSelected = (domain: { name: string }, measures: MeasureEnhanced[], selected: string) => {
const measureSelected = measures.find((measure) => measure.metric.key === selected);
const overviewSelected = domain.name === selected && this.hasOverview(domain.name);
return measureSelected || overviewSelected;
};

hasOverview = (domain: string) => {
return this.props.showFullMeasures && hasBubbleChart(domain);
};

renderItemFacetStat = (item: MeasureEnhanced) => {
return hasFacetStat(item.metric.key) ? (
<FacetMeasureValue displayLeak={this.props.showFullMeasures} measure={item} />
) : null;
};

renderCategoryItem = (item: string) => {
return this.props.showFullMeasures || item === 'new_code_category' ? (
<span className="facet search-navigator-facet facet-category" key={item}>
<span className="facet-name">{translate('component_measures.facet_category', item)}</span>
</span>
) : null;
};

renderItemsFacet = () => {
const { domain, selected } = this.props;
const items = addMeasureCategories(domain.name, filterMeasures(domain.measures));
const hasCategories = items.some((item) => typeof item === 'string');
const translateMetric = hasCategories ? getLocalizedCategoryMetricName : getLocalizedMetricName;
let sortedItems = sortMeasures(domain.name, items);

sortedItems = sortedItems.filter((item, index) => {
return (
typeof item !== 'string' ||
(index + 1 !== sortedItems.length && typeof sortedItems[index + 1] !== 'string')
);
});

return sortedItems.map((item) =>
typeof item === 'string' ? (
this.renderCategoryItem(item)
) : (
<FacetItem
active={item.metric.key === selected}
key={item.metric.key}
name={
<span className="big-spacer-left" id={`measure-${item.metric.key}-name`}>
{translateMetric(item.metric)}
</span>
}
onClick={this.props.onChange}
stat={this.renderItemFacetStat(item)}
tooltip={translateMetric(item.metric)}
value={item.metric.key}
/>
)
);
};

renderOverviewFacet = () => {
const { domain, selected } = this.props;
if (!this.hasOverview(domain.name)) {
return null;
}
return (
<FacetItem
active={domain.name === selected}
key={domain.name}
name={
<span id={`measure-overview-${domain.name}-name`}>
{translate('component_measures.domain_overview')}
</span>
}
onClick={this.props.onChange}
stat={<BubblesIcon size={14} />}
tooltip={translate('component_measures.domain_overview')}
value={domain.name}
/>
);
};

render() {
const { domain, open } = this.props;
const helperMessageKey = `component_measures.domain_facets.${domain.name}.help`;
const helper = hasMessage(helperMessageKey) ? translate(helperMessageKey) : undefined;
const headerId = `facet_${domain.name}`;
return (
<FacetBox property={domain.name}>
<FacetHeader
helper={helper}
id={headerId}
name={getLocalizedMetricDomain(domain.name)}
onClick={this.handleHeaderClick}
open={open}
values={this.getValues()}
/>

{open && (
<FacetItemsList labelledby={headerId}>
{this.renderOverviewFacet()}
{this.renderItemsFacet()}
</FacetItemsList>
)}
</FacetBox>
);
}
}

+ 111
- 0
server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigation.tsx View File

@@ -0,0 +1,111 @@
/*
* 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 {
BareButton,
HelperHintIcon,
SubnavigationAccordion,
SubnavigationItem,
SubnavigationSubheading,
} from 'design-system';
import React from 'react';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import {
getLocalizedCategoryMetricName,
getLocalizedMetricDomain,
getLocalizedMetricName,
hasMessage,
translate,
} from '../../../helpers/l10n';
import { MeasureEnhanced } from '../../../types/types';
import { addMeasureCategories, hasBubbleChart, sortMeasures } from '../utils';
import DomainSubnavigationItem from './DomainSubnavigationItem';

interface Props {
domain: { measures: MeasureEnhanced[]; name: string };
onChange: (metric: string) => void;
open: boolean;
selected: string;
showFullMeasures: boolean;
}

export default function DomainSubnavigation(props: Props) {
const { domain, onChange, open, selected, showFullMeasures } = props;
const helperMessageKey = `component_measures.domain_subnavigation.${domain.name}.help`;
const helper = hasMessage(helperMessageKey) ? translate(helperMessageKey) : undefined;
const items = addMeasureCategories(domain.name, domain.measures);
const hasCategories = items.some((item) => typeof item === 'string');
const translateMetric = hasCategories ? getLocalizedCategoryMetricName : getLocalizedMetricName;
let sortedItems = sortMeasures(domain.name, items);

const hasOverview = (domain: string) => {
return showFullMeasures && hasBubbleChart(domain);
};

// sortedItems contains both measures (type object) and categories (type string)
// here we are filtering out categories that don't contain any measures (happen on the measures page for PRs)
sortedItems = sortedItems.filter((item, index) => {
return (
typeof item === 'object' ||
(index < sortedItems.length - 1 && typeof sortedItems[index + 1] === 'object')
);
});
return (
<SubnavigationAccordion
header={
<div className="sw-flex sw-items-center sw-gap-3">
<strong className="sw-body-sm-highlight">{getLocalizedMetricDomain(domain.name)}</strong>
{helper && (
<HelpTooltip overlay={helper}>
<HelperHintIcon aria-hidden="false" description={helper} />
</HelpTooltip>
)}
</div>
}
initExpanded={open}
id={`measure-${domain.name}`}
>
{hasOverview(domain.name) && (
<SubnavigationItem active={domain.name === selected} onClick={onChange} value={domain.name}>
<BareButton aria-current={domain.name === selected}>
{translate('component_measures.domain_overview')}
</BareButton>
</SubnavigationItem>
)}

{sortedItems.map((item) =>
typeof item === 'string' ? (
showFullMeasures && (
<SubnavigationSubheading>
{translate('component_measures.subnavigation_category', item)}
</SubnavigationSubheading>
)
) : (
<DomainSubnavigationItem
key={item.metric.key}
measure={item}
name={translateMetric(item.metric)}
onChange={onChange}
selected={selected}
/>
)
)}
</SubnavigationAccordion>
);
}

server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.tsx → server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigationItem.tsx View File

@@ -17,32 +17,31 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import FacetBox from '../../../components/facet/FacetBox';
import FacetItem from '../../../components/facet/FacetItem';
import FacetItemsList from '../../../components/facet/FacetItemsList';
import { translate } from '../../../helpers/l10n';
import { BareButton, SubnavigationItem } from 'design-system';
import React from 'react';
import { MeasureEnhanced } from '../../../types/types';
import SubnavigationMeasureValue from './SubnavigationMeasureValue';

interface Props {
measure: MeasureEnhanced;
name: string;
onChange: (metric: string) => void;
selected: string;
value: string;
}

export default function ProjectOverviewFacet({ value, selected, onChange }: Props) {
const facetName = translate('component_measures.overview', value, 'facet');
export default function DomainSubnavigationItem({ measure, name, onChange, selected }: Props) {
const { key } = measure.metric;
return (
<FacetBox property={value}>
<FacetItemsList label={facetName}>
<FacetItem
active={value === selected}
key={value}
name={<strong id={`measure-overview-${value}-name`}>{facetName}</strong>}
onClick={onChange}
tooltip={facetName}
value={value}
/>
</FacetItemsList>
</FacetBox>
<SubnavigationItem active={key === selected} key={key} onClick={onChange} value={key}>
<BareButton
aria-current={key === selected}
className="sw-ml-2 sw-w-full sw-flex sw-justify-between"
id={`measure-${key}-name`}
>
{name}
<SubnavigationMeasureValue measure={measure} />
</BareButton>
</SubnavigationItem>
);
}

+ 45
- 35
server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx View File

@@ -20,23 +20,25 @@
import { withTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
BareButton,
FlagMessage,
LAYOUT_FOOTER_HEIGHT,
LAYOUT_GLOBAL_NAV_HEIGHT,
LAYOUT_PROJECT_NAV_HEIGHT,
SubnavigationGroup,
SubnavigationItem,
themeBorder,
themeColor,
} from 'design-system/lib';
} from 'design-system';
import * as React from 'react';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import { translate } from '../../../helpers/l10n';
import useFollowScroll from '../../../hooks/useFollowScroll';
import { isPortfolioLike } from '../../../types/component';
import { Dict, MeasureEnhanced } from '../../../types/types';
import { KNOWN_DOMAINS, PROJECT_OVERVEW, Query, groupByDomains } from '../utils';
import DomainFacet from './DomainFacet';
import ProjectOverviewFacet from './ProjectOverviewFacet';
import { MeasureEnhanced } from '../../../types/types';
import { PROJECT_OVERVEW, Query, groupByDomains, isProjectOverview } from '../utils';
import DomainSubnavigation from './DomainSubnavigation';

interface Props {
canBrowseAllChildProjects: boolean;
@@ -56,15 +58,7 @@ export default function Sidebar(props: Props) {
selectedMetric,
measures,
} = props;
const [openFacets, setOpenFacets] = React.useState(getOpenFacets({}, props));
const { top: topScroll } = useFollowScroll();

const handleToggleFacet = React.useCallback(
(name: string) => {
setOpenFacets((openFacets) => ({ ...openFacets, [name]: !openFacets[name] }));
},
[setOpenFacets]
);
const { top: topScroll, scrolledOnce } = useFollowScroll();

const handleChangeMetric = React.useCallback(
(metric: string) => {
@@ -73,9 +67,17 @@ export default function Sidebar(props: Props) {
[updateQuery]
);

const distanceFromBottom = topScroll + window.innerHeight - document.body.clientHeight;
const handleProjectOverviewClick = () => {
handleChangeMetric(PROJECT_OVERVEW);
};

const distanceFromBottom = topScroll + window.innerHeight - document.body.scrollHeight;
const footerVisibleHeight =
distanceFromBottom > -LAYOUT_FOOTER_HEIGHT ? LAYOUT_FOOTER_HEIGHT + distanceFromBottom : 0;
(scrolledOnce &&
(distanceFromBottom > -LAYOUT_FOOTER_HEIGHT
? LAYOUT_FOOTER_HEIGHT + distanceFromBottom
: 0)) ||
0;

return (
<StyledSidebar
@@ -90,7 +92,7 @@ export default function Sidebar(props: Props) {
{!canBrowseAllChildProjects && isPortfolioLike(qualifier) && (
<FlagMessage
ariaLabel={translate('component_measures.not_all_measures_are_shown')}
className="it__portfolio_warning"
className="sw-mt-4 it__portfolio_warning"
variant="warning"
>
{translate('component_measures.not_all_measures_are_shown')}
@@ -109,18 +111,23 @@ export default function Sidebar(props: Props) {
label={translate('component_measures.skip_to_navigation')}
weight={10}
/>
<ProjectOverviewFacet
onChange={handleChangeMetric}
selected={selectedMetric}
value={PROJECT_OVERVEW}
/>
{groupByDomains(measures).map((domain) => (
<DomainFacet
<SubnavigationGroup>
<SubnavigationItem
active={isProjectOverview(selectedMetric)}
onClick={handleProjectOverviewClick}
>
<BareButton>
{translate('component_measures.overview', PROJECT_OVERVEW, 'subnavigation')}
</BareButton>
</SubnavigationItem>
</SubnavigationGroup>

{groupByDomains(measures).map((domain: Domain) => (
<DomainSubnavigation
domain={domain}
key={domain.name}
onChange={handleChangeMetric}
onToggle={handleToggleFacet}
open={openFacets[domain.name] === true}
open={isDomainSelected(selectedMetric, domain)}
selected={selectedMetric}
showFullMeasures={showFullMeasures}
/>
@@ -130,15 +137,16 @@ export default function Sidebar(props: Props) {
);
}

function getOpenFacets(openFacets: Dict<boolean>, { measures, selectedMetric }: Props) {
const newOpenFacets = { ...openFacets };
const measure = measures.find((measure) => measure.metric.key === selectedMetric);
if (measure && measure.metric && measure.metric.domain) {
newOpenFacets[measure.metric.domain] = true;
} else if (KNOWN_DOMAINS.includes(selectedMetric)) {
newOpenFacets[selectedMetric] = true;
}
return newOpenFacets;
interface Domain {
measures: MeasureEnhanced[];
name: string;
}

function isDomainSelected(selectedMetric: string, domain: Domain) {
return (
selectedMetric === domain.name ||
domain.measures.some((measure) => measure.metric.key === selectedMetric)
);
}

const StyledSidebar = withTheme(styled.div`
@@ -147,4 +155,6 @@ const StyledSidebar = withTheme(styled.div`

background-color: ${themeColor('filterbar')};
border-right: ${themeBorder('default', 'filterbarBorder')};
position: sticky;
overflow-x: hidden;
`);

server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.tsx → server/sonar-web/src/main/js/apps/component-measures/sidebar/SubnavigationMeasureValue.tsx View File

@@ -17,40 +17,30 @@
* 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 { Note } from 'design-system';
import React from 'react';
import Measure from '../../../components/measure/Measure';
import { isDiffMetric } from '../../../helpers/measures';
import { MeasureEnhanced } from '../../../types/types';

interface Props {
displayLeak?: boolean;
measure: MeasureEnhanced;
}

export default function FacetMeasureValue({ measure, displayLeak }: Props) {
if (isDiffMetric(measure.metric.key)) {
return (
<div
className={classNames('domain-measures-value', { 'leak-box': displayLeak })}
id={`measure-${measure.metric.key}-leak`}
>
<Measure
metricKey={measure.metric.key}
metricType={measure.metric.type}
value={measure.leak}
/>
</div>
);
}
export default function SubnavigationMeasureValue({ measure }: Props) {
const isDiff = isDiffMetric(measure.metric.key);

return (
<div className="domain-measures-value" id={`measure-${measure.metric.key}-value`}>
<Note
className="sw-flex sw-items-center sw-mr-1"
id={`measure-${measure.metric.key}-${isDiff ? 'leak' : 'value'}`}
>
<Measure
metricKey={measure.metric.key}
metricType={measure.metric.type}
value={measure.value}
value={isDiff ? measure.leak : measure.value}
/>
</div>
</Note>
);
}

+ 1
- 3
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx View File

@@ -74,9 +74,7 @@ export default function HotspotListItem(props: HotspotListItemProps) {
onClick={handleClick}
className="sw-flex-col sw-items-start"
>
<StyledHotspotTitle aria-current={selected} role="button">
{hotspot.message}
</StyledHotspotTitle>
<StyledHotspotTitle aria-current={selected}>{hotspot.message}</StyledHotspotTitle>
{locations.length > 0 && (
<StyledHotspotInfo className="sw-flex sw-justify-end sw-w-full">
<div className="sw-flex sw-mt-2 sw-items-center sw-justify-center sw-gap-1 sw-overflow-hidden">

+ 4
- 3
server/sonar-web/src/main/js/components/measure/Measure.tsx View File

@@ -22,6 +22,7 @@ import Tooltip from '../../components/controls/Tooltip';
import Level from '../../components/ui/Level';
import Rating from '../../components/ui/Rating';
import { formatMeasure } from '../../helpers/measures';
import { MetricType } from '../../types/metrics';
import RatingTooltipContent from './RatingTooltipContent';

interface Props {
@@ -47,14 +48,14 @@ export default function Measure({
return <span className={className}>–</span>;
}

if (metricType === 'LEVEL') {
if (metricType === MetricType.Level) {
return <Level className={className} level={value} small={small} />;
}

if (metricType !== 'RATING') {
if (metricType !== MetricType.Rating) {
const formattedValue = formatMeasure(value, metricType, {
decimals,
omitExtraDecimalZeros: metricType === 'PERCENT',
omitExtraDecimalZeros: metricType === MetricType.Percent,
});
return <span className={className}>{formattedValue != null ? formattedValue : '–'}</span>;
}

+ 3
- 1
server/sonar-web/src/main/js/hooks/useFollowScroll.ts View File

@@ -25,12 +25,14 @@ const THROTTLE_DELAY = 10;
export default function useFollowScroll() {
const [left, setLeft] = useState(0);
const [top, setTop] = useState(0);
const [scrolledOnce, setScrolledOnce] = useState(false);

useEffect(() => {
const followScroll = throttle(() => {
if (document.documentElement) {
setLeft(document.documentElement.scrollLeft);
setTop(document.documentElement.scrollTop);
setScrolledOnce(true);
}
}, THROTTLE_DELAY);

@@ -38,5 +40,5 @@ export default function useFollowScroll() {
return () => document.removeEventListener('scroll', followScroll);
}, []);

return { left, top };
return { left, top, scrolledOnce };
}

+ 10
- 9
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -3659,7 +3659,7 @@ component_measures.hidden_best_score_metrics=There are {0} hidden components wit
component_measures.navigation=Measures navigation
component_measures.skip_to_navigation=Skip to measure navigation

component_measures.overview.project_overview.facet=Project Overview
component_measures.overview.project_overview.subnavigation=Project Overview
component_measures.overview.project_overview.title=Risk
component_measures.overview.project_overview.description=Get quick insights into the operational risks. For users relying on their keyboard, elements are sorted by the number of lines of code for each file. Any color but green indicates immediate risks: Bugs or Vulnerabilities that should be examined. A position at the top or right of the graph means that the longer-term health may be at risk. Green bubbles at the bottom-left are best.
component_measures.overview.Reliability.description=See bugs' operational risks. For users relying on their keyboard, elements are sorted by volume of bugs per file. The closer a bubble's color is to red, the more severe the worst bugs are. Bubble size indicates bug volume, and each bubble's vertical position reflects the estimated time to address the bugs. Small green bubbles on the bottom edge are best.
@@ -3669,16 +3669,17 @@ component_measures.overview.Coverage.description=See missing test coverage's lon
component_measures.overview.Duplications.description=See duplications' long-term risks. For users relying on their keyboard, elements are sorted by the number of duplicated blocks per file. Bubble size indicates the volume of duplicated blocks, and each bubble's vertical position reflects the volume of lines in those blocks. Small bubbles on the bottom edge are best.
component_measures.overview.see_data_as_list=See the data presented on this chart as a list

component_measures.domain_facets.Reliability.help=Issues in this domain mark code where you will get behavior other than what was expected.
component_measures.domain_facets.Maintainability.help=Issues in this domain mark code that will be more difficult to update competently than it should.
component_measures.domain_facets.Security.help=Issues in this domain mark potential weaknesses to hackers.
component_measures.domain_facets.SecurityReview.help=This domain represents potential security risks in the form of hotspots and their review status.
component_measures.domain_facets.Complexity.help=How simple or complicated the control flow of the application is. Cyclomatic Complexity measures the minimum number of test cases required for full test coverage. Cognitive Complexity is a measure of how difficult the application is to understand
component_measures.domain_subnavigation.Reliability.help=Issues in this domain mark code where you will get behavior other than what was expected.
component_measures.domain_subnavigation.Maintainability.help=Issues in this domain mark code that will be more difficult to update competently than it should.
component_measures.domain_subnavigation.Security.help=Issues in this domain mark potential weaknesses to hackers.
component_measures.domain_subnavigation.SecurityReview.help=This domain represents potential security risks in the form of hotspots and their review status.
component_measures.domain_subnavigation.Complexity.help=How simple or complicated the control flow of the application is. Cyclomatic Complexity measures the minimum number of test cases required for full test coverage. Cognitive Complexity is a measure of how difficult the application is to understand

component_measures.subnavigation_category.new_code_category=New Code
component_measures.subnavigation_category.overall_category=Overall Code
component_measures.subnavigation_category.tests_category=Tests

component_measures.facet_category.new_code_category=On new code
component_measures.facet_category.overall_category=Overall
component_measures.facet_category.overall_category.estimated=Estimated after merge
component_measures.facet_category.tests_category=Tests
component_measures.bubble_chart.zoom_level=Current zoom level. Scroll on the chart to zoom or unzoom, click here to reset.
component_measures.not_all_measures_are_shown=Not all projects and applications are included
component_measures.not_all_measures_are_shown.help=You do not have access to all projects and/or applications. Measures are still computed based on all projects and applications.

Loading…
Cancel
Save