From: stanislavh Date: Tue, 30 May 2023 10:58:20 +0000 (+0200) Subject: SONAR-19391 Measures page sidebar adopts the new UI X-Git-Tag: 10.1.0.73491~151 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=0bc5896c6d93610455401dc4caaad3bc6a99a0cc;p=sonarqube.git SONAR-19391 Measures page sidebar adopts the new UI --- diff --git a/server/sonar-web/design-system/src/components/icons/Icon.tsx b/server/sonar-web/design-system/src/components/icons/Icon.tsx index 3a90211b525..80e0e64be88 100644 --- a/server/sonar-web/design-system/src/components/icons/Icon.tsx +++ b/server/sonar-web/design-system/src/components/icons/Icon.tsx @@ -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 { @@ -38,10 +40,17 @@ export interface IconProps extends Omit { } 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 ( + {description && {description}} {children} ); diff --git a/server/sonar-web/design-system/src/components/subnavigation/SubnavigationAccordion.tsx b/server/sonar-web/design-system/src/components/subnavigation/SubnavigationAccordion.tsx index e38644eea62..01e017f100f 100644 --- a/server/sonar-web/design-system/src/components/subnavigation/SubnavigationAccordion.tsx +++ b/server/sonar-web/design-system/src/components/subnavigation/SubnavigationAccordion.tsx @@ -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 ( {header} - + - {expanded && children} + {finalExpanded && children} ); } diff --git a/server/sonar-web/design-system/src/components/subnavigation/SubnavigationSubheading.tsx b/server/sonar-web/design-system/src/components/subnavigation/SubnavigationSubheading.tsx new file mode 100644 index 00000000000..574247578b8 --- /dev/null +++ b/server/sonar-web/design-system/src/components/subnavigation/SubnavigationSubheading.tsx @@ -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'; diff --git a/server/sonar-web/design-system/src/components/subnavigation/__tests__/SubnavigationAccordion-test.tsx b/server/sonar-web/design-system/src/components/subnavigation/__tests__/SubnavigationAccordion-test.tsx index ebf76171247..884fd13db3b 100644 --- a/server/sonar-web/design-system/src/components/subnavigation/__tests__/SubnavigationAccordion-test.tsx +++ b/server/sonar-web/design-system/src/components/subnavigation/__tests__/SubnavigationAccordion-test.tsx @@ -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 () => { diff --git a/server/sonar-web/design-system/src/components/subnavigation/index.ts b/server/sonar-web/design-system/src/components/subnavigation/index.ts index 89410cd2e49..0d4265cbfe1 100644 --- a/server/sonar-web/design-system/src/components/subnavigation/index.ts +++ b/server/sonar-web/design-system/src/components/subnavigation/index.ts @@ -21,3 +21,4 @@ export * from './SubnavigationAccordion'; export * from './SubnavigationGroup'; export * from './SubnavigationHeading'; export * from './SubnavigationItem'; +export * from './SubnavigationSubheading'; 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 2aba7aba066..426abce1033 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 @@ -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 { 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 ( diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx deleted file mode 100644 index 638040d7232..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx +++ /dev/null @@ -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 { - 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) ? ( - - ) : null; - }; - - renderCategoryItem = (item: string) => { - return this.props.showFullMeasures || item === 'new_code_category' ? ( - - {translate('component_measures.facet_category', item)} - - ) : 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) - ) : ( - - {translateMetric(item.metric)} - - } - 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 ( - - {translate('component_measures.domain_overview')} - - } - onClick={this.props.onChange} - stat={} - 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 ( - - - - {open && ( - - {this.renderOverviewFacet()} - {this.renderItemsFacet()} - - )} - - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigation.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigation.tsx new file mode 100644 index 00000000000..f308d9ae487 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigation.tsx @@ -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 ( + + {getLocalizedMetricDomain(domain.name)} + {helper && ( + + + + )} + + } + initExpanded={open} + id={`measure-${domain.name}`} + > + {hasOverview(domain.name) && ( + + + {translate('component_measures.domain_overview')} + + + )} + + {sortedItems.map((item) => + typeof item === 'string' ? ( + showFullMeasures && ( + + {translate('component_measures.subnavigation_category', item)} + + ) + ) : ( + + ) + )} + + ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigationItem.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigationItem.tsx new file mode 100644 index 00000000000..eab341fcca3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainSubnavigationItem.tsx @@ -0,0 +1,47 @@ +/* + * 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, 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; +} + +export default function DomainSubnavigationItem({ measure, name, onChange, selected }: Props) { + const { key } = measure.metric; + return ( + + + {name} + + + + ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.tsx deleted file mode 100644 index 097aa52e66f..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/FacetMeasureValue.tsx +++ /dev/null @@ -1,56 +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 * as 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 ( -
- -
- ); - } - - return ( -
- -
- ); -} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.tsx deleted file mode 100644 index bb33f4419ea..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.tsx +++ /dev/null @@ -1,48 +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 FacetItem from '../../../components/facet/FacetItem'; -import FacetItemsList from '../../../components/facet/FacetItemsList'; -import { translate } from '../../../helpers/l10n'; - -interface Props { - onChange: (metric: string) => void; - selected: string; - value: string; -} - -export default function ProjectOverviewFacet({ value, selected, onChange }: Props) { - const facetName = translate('component_measures.overview', value, 'facet'); - return ( - - - {facetName}} - onClick={onChange} - tooltip={facetName} - value={value} - /> - - - ); -} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx index 0b786954998..caa4aa7f631 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx @@ -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 ( {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} /> - - {groupByDomains(measures).map((domain) => ( - + + + {translate('component_measures.overview', PROJECT_OVERVEW, 'subnavigation')} + + + + + {groupByDomains(measures).map((domain: Domain) => ( + @@ -130,15 +137,16 @@ export default function Sidebar(props: Props) { ); } -function getOpenFacets(openFacets: Dict, { 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; `); diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/SubnavigationMeasureValue.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/SubnavigationMeasureValue.tsx new file mode 100644 index 00000000000..4dce88ea909 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/SubnavigationMeasureValue.tsx @@ -0,0 +1,46 @@ +/* + * 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 { 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 { + measure: MeasureEnhanced; +} + +export default function SubnavigationMeasureValue({ measure }: Props) { + const isDiff = isDiffMetric(measure.metric.key); + + return ( + + + + ); +} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx index d123cf260d7..e7d8d9fd11b 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx @@ -74,9 +74,7 @@ export default function HotspotListItem(props: HotspotListItemProps) { onClick={handleClick} className="sw-flex-col sw-items-start" > - - {hotspot.message} - + {hotspot.message} {locations.length > 0 && (
diff --git a/server/sonar-web/src/main/js/components/measure/Measure.tsx b/server/sonar-web/src/main/js/components/measure/Measure.tsx index a980441cf9e..bf83d336a6a 100644 --- a/server/sonar-web/src/main/js/components/measure/Measure.tsx +++ b/server/sonar-web/src/main/js/components/measure/Measure.tsx @@ -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 –; } - if (metricType === 'LEVEL') { + if (metricType === MetricType.Level) { return ; } - if (metricType !== 'RATING') { + if (metricType !== MetricType.Rating) { const formattedValue = formatMeasure(value, metricType, { decimals, - omitExtraDecimalZeros: metricType === 'PERCENT', + omitExtraDecimalZeros: metricType === MetricType.Percent, }); return {formattedValue != null ? formattedValue : '–'}; } diff --git a/server/sonar-web/src/main/js/hooks/useFollowScroll.ts b/server/sonar-web/src/main/js/hooks/useFollowScroll.ts index 0692d5e2ab7..ee30b41aae7 100644 --- a/server/sonar-web/src/main/js/hooks/useFollowScroll.ts +++ b/server/sonar-web/src/main/js/hooks/useFollowScroll.ts @@ -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 }; } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 6ea6b7b2ae9..2433b4b33fe 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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.