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'> {
}
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"
xmlnsXlink="http://www.w3.org/1999/xlink"
{...iconProps}
>
+ {description && <desc>{description}</desc>}
{children}
</svg>
);
* 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
>
<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>
);
}
--- /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 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';
});
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 () => {
export * from './SubnavigationGroup';
export * from './SubnavigationHeading';
export * from './SubnavigationItem';
+export * from './SubnavigationSubheading';
import { translate } from '../../../helpers/l10n';
import { BranchLike } from '../../../types/branch-like';
import { ComponentQualifier } from '../../../types/component';
+import { MetricKey } from '../../../types/metrics';
import {
ComponentMeasure,
Dict,
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 (
+++ /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 * 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>
- );
- }
-}
--- /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 {
+ 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>
+ );
+}
--- /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 { 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 (
+ <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>
+ );
+}
+++ /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 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>
- );
- }
-
- return (
- <div className="domain-measures-value" id={`measure-${measure.metric.key}-value`}>
- <Measure
- metricKey={measure.metric.key}
- metricType={measure.metric.type}
- value={measure.value}
- />
- </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 * 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 (
- <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>
- );
-}
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;
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) => {
[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
{!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')}
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}
/>
);
}
-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`
background-color: ${themeColor('filterbar')};
border-right: ${themeBorder('default', 'filterbarBorder')};
+ position: sticky;
+ overflow-x: hidden;
`);
--- /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 { 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 (
+ <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={isDiff ? measure.leak : measure.value}
+ />
+ </Note>
+ );
+}
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">
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 {
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>;
}
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);
return () => document.removeEventListener('scroll', followScroll);
}, []);
- return { left, top };
+ return { left, top, scrolledOnce };
}
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.
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.