diff options
9 files changed, 158 insertions, 180 deletions
diff --git a/server/sonar-web/design-system/src/components/icons/QualifierIcon.tsx b/server/sonar-web/design-system/src/components/icons/QualifierIcon.tsx index 4f5bbc4d99b..92685c586f9 100644 --- a/server/sonar-web/design-system/src/components/icons/QualifierIcon.tsx +++ b/server/sonar-web/design-system/src/components/icons/QualifierIcon.tsx @@ -21,7 +21,7 @@ import { useTheme } from '@emotion/react'; import { themeColor } from '../../helpers/theme'; import { DirectoryIcon } from './DirectoryIcon'; import { FileIcon } from './FileIcon'; -import { IconProps } from './Icon'; +import { CustomIcon, IconProps } from './Icon'; import { ProjectIcon } from './ProjectIcon'; import { TestFileIcon } from './TestFileIcon'; @@ -41,7 +41,49 @@ export function QualifierIcon({ qualifier, fill, ...iconProps }: Props) { fil: <FileIcon fill={fill ?? themeColor('iconFile')({ theme })} {...iconProps} />, trk: <ProjectIcon fill={fill ?? themeColor('iconProject')({ theme })} {...iconProps} />, uts: <TestFileIcon fill={fill ?? themeColor('iconProject')({ theme })} {...iconProps} />, + app: ApplicationIcon({ fill: fill ?? themeColor('iconProject')({ theme }), ...iconProps }), + vw: PortfolioIcon({ fill: fill ?? themeColor('iconProject')({ theme }), ...iconProps }), + svw: SubPortfolioIcon({ fill: fill ?? themeColor('iconProject')({ theme }), ...iconProps }), }[qualifier.toLowerCase()]; return icon ?? null; } + +function PortfolioIcon({ fill = 'currentColor', ...iconProps }: IconProps) { + const theme = useTheme(); + + return ( + <CustomIcon {...iconProps}> + <path + d="M14.97 14.97H1.016V1.015H14.97V14.97zm-1-12.955H2.015V13.97H13.97V2.015zm-.973 10.982H9V9h3.997v3.997zM7 12.996H3.004V9H7v3.996zM11.997 10H10v1.997h1.997V10zM6 10H4.004v1.996H6V10zm1-3H3.006V3.006H7V7zm5.985 0H9V3.015h3.985V7zM6 4.006H4.006V6H6V4.006zm5.985.009H10V6h1.985V4.015z" + fill={themeColor(fill)({ theme })} + /> + </CustomIcon> + ); +} + +function ApplicationIcon({ fill = 'currentColor', ...iconProps }: IconProps) { + const theme = useTheme(); + + return ( + <CustomIcon {...iconProps}> + <path + d="M3.014 10.986a2 2 0 1 1-.001 4.001 2 2 0 0 1 .001-4.001zm9.984 0a2 2 0 1 1-.001 4.001 2 2 0 0 1 .001-4.001zm-5.004-.021c1.103 0 2 .896 2 2s-.897 2-2 2a2 2 0 0 1 0-4zm-4.98 1.021a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm9.984 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm-5.004-.021a1 1 0 1 1 0 2 1 1 0 0 1 0-2zM2.984 6a2 2 0 1 1-.001 4.001A2 2 0 0 1 2.984 6zm9.984 0a2 2 0 1 1-.001 4.001A2 2 0 0 1 12.968 6zm-5.004-.021c1.103 0 2 .897 2 2a2 2 0 1 1-2-2zM2.984 7a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm9.984 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm-5.004-.021a1.001 1.001 0 0 1 0 2 1 1 0 0 1 0-2zM3 1.025a2 2 0 1 1-.001 4.001A2 2 0 0 1 3 1.025zm9.984 0a2 2 0 1 1-.001 4.001 2 2 0 0 1 .001-4.001zM7.98 1.004c1.103 0 2 .896 2 2s-.897 2-2 2a2 2 0 0 1 0-4zM3 2.025a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm9.984 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2zM7.98 2.004a1.001 1.001 0 0 1 0 2 1 1 0 0 1 0-2z" + fill={themeColor(fill)({ theme })} + /> + </CustomIcon> + ); +} + +function SubPortfolioIcon({ fill = 'currentColor', ...iconProps }: IconProps) { + const theme = useTheme(); + + return ( + <CustomIcon {...iconProps}> + <path + d="M14 7h2v9H7v-2H0V0h14v7zM8 8v7h7V8H8zm3 6H9v-2h2v2zm3 0h-2v-2h2v2zm-1-7V1H1v12h6V7h6zm-7 5H2V8h4v4zm5-1H9V9h2v2zm3 0h-2V9h2v2zM5 9H3v2h2V9zm1-3H2V2h4v4zm6 0H8V2h4v4zM5 3H3v2h2V3zm6 0H9v2h2V3z" + fill={themeColor(fill)({ theme })} + /> + </CustomIcon> + ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx b/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx index be3ab4737bb..6d8679e4a7b 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx @@ -255,7 +255,7 @@ describe('navigation', () => { within(ui.measuresRow('test1.js').get()).getByRole('cell', { name: '2' }) ).toBeInTheDocument(); - await user.click(ui.fileLink('foo:folderA').get()); + await user.click(ui.fileLink('folderA').get()); expect( within(ui.measuresRow('out.tsx').get()).getByRole('cell', { name: '1' }) ).toBeInTheDocument(); @@ -263,7 +263,7 @@ describe('navigation', () => { within(ui.measuresRow('in.tsx').get()).getByRole('cell', { name: '2' }) ).toBeInTheDocument(); - await user.click(ui.fileLink('foo:folderA/out.tsx').get()); + await user.click(ui.fileLink('out.tsx').get()); expect((await ui.sourceCode.findAll()).length).toBeGreaterThan(0); // Go back using the breadcrumbs. @@ -288,7 +288,7 @@ describe('navigation', () => { within(ui.measuresRow('test1.js').get()).getByRole('cell', { name: '2' }) ).toBeInTheDocument(); - await user.click(ui.fileLink('foo:folderA/out.tsx').get()); + await user.click(ui.fileLink('out.tsx').get()); expect((await ui.sourceCode.findAll()).length).toBeGreaterThan(0); }); @@ -479,7 +479,9 @@ function getPageObject() { detailsUnavailableText: byText('component_measures.details_are_not_available'), noAccessWarning: byRole('alert'), showingOutOfTxt: (x: string, y: string) => byText(`x_of_y_shown.${x}.${y}`), - showAllBtn: byRole('button', { name: 'show_them' }), + showAllBtn: byRole('button', { + name: 'component_measures.hidden_best_score_metrics_show_label', + }), goToActivityLink: byRole('link', { name: 'component_measures.show_metric_history' }), }; diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx index 3f43f541f46..4a2c158e82c 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx @@ -17,23 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ContentCell, HoverLink, Note, QualifierIcon } from 'design-system'; import * as React from 'react'; import { To } from 'react-router-dom'; -import Link from '../../../components/common/Link'; -import BranchIcon from '../../../components/icons/BranchIcon'; -import LinkIcon from '../../../components/icons/LinkIcon'; -import QualifierIcon from '../../../components/icons/QualifierIcon'; import { fillBranchLike } from '../../../helpers/branch-like'; -import { translate } from '../../../helpers/l10n'; import { splitPath } from '../../../helpers/path'; import { getComponentDrilldownUrlWithSelection, getProjectUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; -import { - ComponentQualifier, - isApplication, - isPortfolioLike, - isProject, -} from '../../../types/component'; +import { ComponentQualifier, isApplication, isProject } from '../../../types/component'; import { MeasurePageView } from '../../../types/measures'; import { MetricKey } from '../../../types/metrics'; import { ComponentMeasure, ComponentMeasureEnhanced, Metric } from '../../../types/types'; @@ -93,34 +84,18 @@ export default function ComponentCell(props: ComponentCellProps) { } return ( - <td className="measure-details-component-cell"> - <div className="text-ellipsis"> - <Link - className="link-no-underline" - to={path} - id={'component-measures-component-link-' + component.key} - > - {component.refKey && ( - <span className="big-spacer-right"> - <LinkIcon /> - </span> - )} - <span title={component.key}> - <QualifierIcon className="little-spacer-right" qualifier={component.qualifier} /> - {head.length > 0 && <span className="note">{head}/</span>} - <span>{tail}</span> - {(isApplication(rootComponent.qualifier) || isPortfolioLike(rootComponent.qualifier)) && - (component.branch ? ( - <> - <BranchIcon className="spacer-left little-spacer-right" /> - <span className="note">{component.branch}</span> - </> - ) : ( - <span className="spacer-left badge">{translate('branches.main_branch')}</span> - ))} - </span> - </Link> - </div> - </td> + <ContentCell className="sw-py-3 sw-truncate sw-flex"> + <HoverLink + aria-hidden={true} + tabIndex={-1} + icon={<QualifierIcon qualifier={component.qualifier} />} + to={path} + title={component.path} + /> + <HoverLink to={path} title={component.path}> + {head.length > 0 && <Note>{head}/</Note>} + <strong>{tail}</strong> + </HoverLink> + </ContentCell> ); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx index dd7f9ae2593..d67a32ddf3d 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.tsx @@ -17,15 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ContentCell, NumericalCell, Table, TableRow, TableRowInteractive } from 'design-system'; import * as React from 'react'; -import { getComponentMeasureUniqueKey } from '../../../helpers/component'; import { getLocalizedMetricName } from '../../../helpers/l10n'; import { BranchLike } from '../../../types/branch-like'; import { MeasurePageView } from '../../../types/measures'; import { ComponentMeasure, ComponentMeasureEnhanced, Dict, Metric } from '../../../types/types'; import { complementary } from '../config/complementary'; -import ComponentsListRow from './ComponentsListRow'; +import ComponentCell from './ComponentCell'; import EmptyResult from './EmptyResult'; +import MeasureCell from './MeasureCell'; interface Props { branchLike?: BranchLike; @@ -38,44 +39,51 @@ interface Props { } export default function ComponentsList({ components, metric, metrics, ...props }: Props) { + const { branchLike, rootComponent, selectedComponent } = props; + if (!components.length) { return <EmptyResult />; } const otherMetrics = (complementary[metric.key] || []).map((key) => metrics[key]); return ( - <table className="data zebra zebra-hover"> - {otherMetrics.length > 0 && ( - <thead> - <tr> - <th> </th> - <th className="text-right"> - <span className="small">{getLocalizedMetricName(metric)}</span> - </th> + <Table + gridTemplate={`1fr repeat(${otherMetrics.length + 1}, min-content)`} + header={ + otherMetrics.length > 0 && ( + <TableRow> + <ContentCell /> + <NumericalCell className="sw-body-sm">{getLocalizedMetricName(metric)}</NumericalCell> {otherMetrics.map((metric) => ( - <th className="text-right" key={metric.key}> - <span className="small">{getLocalizedMetricName(metric)}</span> - </th> + <NumericalCell className="sw-body-sm" key={metric.key}> + {getLocalizedMetricName(metric)} + </NumericalCell> ))} - </tr> - </thead> - )} - - <tbody> - {components.map((component) => ( - <ComponentsListRow + </TableRow> + ) + } + > + {components.map((component) => ( + <TableRowInteractive + key={component.key} + className="it__measures-component-row" + selected={component.key === selectedComponent?.key} + > + <ComponentCell + branchLike={branchLike} component={component} - isSelected={ - getComponentMeasureUniqueKey(component) === - getComponentMeasureUniqueKey(props.selectedComponent) - } - key={getComponentMeasureUniqueKey(component)} metric={metric} - otherMetrics={otherMetrics} - {...props} + rootComponent={rootComponent} + view={props.view} /> - ))} - </tbody> - </table> + + <MeasureCell component={component} metric={metric} /> + + {otherMetrics.map((metric) => ( + <MeasureCell key={metric.key} component={component} metric={metric} /> + ))} + </TableRowInteractive> + ))} + </Table> ); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.tsx deleted file mode 100644 index fb761dafb36..00000000000 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.tsx +++ /dev/null @@ -1,69 +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 { BranchLike } from '../../../types/branch-like'; -import { MeasurePageView } from '../../../types/measures'; -import { ComponentMeasure, ComponentMeasureEnhanced, Metric } from '../../../types/types'; -import ComponentCell from './ComponentCell'; -import MeasureCell from './MeasureCell'; - -interface Props { - branchLike?: BranchLike; - component: ComponentMeasureEnhanced; - isSelected: boolean; - otherMetrics: Metric[]; - metric: Metric; - rootComponent: ComponentMeasure; - view: MeasurePageView; -} - -export default function ComponentsListRow(props: Props) { - const { branchLike, component, rootComponent } = props; - const otherMeasures = props.otherMetrics.map((metric) => { - const measure = component.measures.find((measure) => measure.metric.key === metric.key); - return { ...measure, metric }; - }); - const rowClass = classNames('measure-details-component-row', { - selected: props.isSelected, - }); - return ( - <tr className={rowClass}> - <ComponentCell - branchLike={branchLike} - component={component} - metric={props.metric} - rootComponent={rootComponent} - view={props.view} - /> - - <MeasureCell component={component} metric={props.metric} /> - - {otherMeasures.map((measure) => ( - <MeasureCell - component={component} - key={measure.metric.key} - measure={measure} - metric={measure.metric} - /> - ))} - </tr> - ); -} diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx index a909faf1575..5c98cb047f4 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx @@ -17,17 +17,17 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ButtonSecondary, FlagMessage } from 'design-system'; import { throttle } from 'lodash'; import * as React from 'react'; import ListFooter from '../../../components/controls/ListFooter'; -import { Button } from '../../../components/controls/buttons'; -import { Alert } from '../../../components/ui/Alert'; import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; import { KeyboardKeys } from '../../../helpers/keycodes'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure, isDiffMetric, isPeriodBestValue } from '../../../helpers/measures'; import { BranchLike } from '../../../types/branch-like'; import { MeasurePageView } from '../../../types/measures'; +import { MetricType } from '../../../types/metrics'; import { ComponentMeasure, ComponentMeasureEnhanced, @@ -181,18 +181,34 @@ export default class FilesView extends React.PureComponent<Props, State> { view={this.props.view} /> {hidingBestMeasures && this.props.paging && ( - <Alert className="spacer-top" variant="info"> - <div className="display-flex-center"> - {translateWithParameters( - 'component_measures.hidden_best_score_metrics', - formatMeasure(this.props.paging.total - filteredComponents.length, 'INT'), - formatMeasure(this.props.metric.bestValue, this.props.metric.type) - )} - <Button className="button-small spacer-left" onClick={this.handleShowBestMeasures}> - {translate('show_them')} - </Button> - </div> - </Alert> + <FlagMessage + ariaLabel={translateWithParameters( + 'component_measures.hidden_best_score_metrics', + formatMeasure( + this.props.paging.total - filteredComponents.length, + MetricType.Integer + ), + formatMeasure(this.props.metric.bestValue, this.props.metric.type) + )} + variant="info" + className="sw-mt-4" + > + {translateWithParameters( + 'component_measures.hidden_best_score_metrics', + formatMeasure( + this.props.paging.total - filteredComponents.length, + MetricType.Integer + ), + formatMeasure(this.props.metric.bestValue, this.props.metric.type) + )} + <ButtonSecondary + onClick={this.handleShowBestMeasures} + className="sw-ml-4" + aria-label={translate('component_measures.hidden_best_score_metrics_show_label')} + > + {translate('show_them')} + </ButtonSecondary> + </FlagMessage> )} {!hidingBestMeasures && this.props.paging && this.props.components.length > 0 && ( <ListFooter diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx index 28699421570..fe2ad2a372b 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/MeasureCell.tsx @@ -17,9 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { MetricsLabel, MetricsRatingBadge, NumericalCell } from 'design-system'; import * as React from 'react'; import Measure from '../../../components/measure/Measure'; -import { isDiffMetric } from '../../../helpers/measures'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; +import { MetricType } from '../../../types/metrics'; import { ComponentMeasureEnhanced, MeasureEnhanced, Metric } from '../../../types/types'; interface Props { @@ -35,10 +38,26 @@ export default function MeasureCell({ component, measure, metric }: Props) { const value = getValue(measure || component); return ( - <td className="thin nowrap text-right"> - <span id={`component-measures-component-measure-${component.key}-${metric.key}`}> - <Measure metricKey={metric.key} metricType={metric.type} value={value} small={true} /> - </span> - </td> + <NumericalCell className="sw-py-3"> + <Measure + metricKey={metric.key} + metricType={metric.type} + value={value} + small={true} + ratingComponent={ + <MetricsRatingBadge + label={ + value + ? translateWithParameters( + 'metric.has_rating_X', + formatMeasure(value, MetricType.Rating) + ) + : translate('metric.no_rating') + } + rating={formatMeasure(value, MetricType.Rating) as MetricsLabel} + /> + } + /> + </NumericalCell> ); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/style.css b/server/sonar-web/src/main/js/apps/component-measures/style.css index e4d8107cab4..218abc31966 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/style.css +++ b/server/sonar-web/src/main/js/apps/component-measures/style.css @@ -78,18 +78,6 @@ button.search-navigator-facet { margin-top: 4px; } -.measure-details-component-row.selected { - background-color: var(--lightBlue) !important; -} - -.measure-details-component-cell { - max-width: 0; -} - -.measure-details-component-cell > div { - max-width: 100%; -} - .domain-measures-value .rating, .measure-details-value .rating { width: 18px; @@ -122,7 +110,3 @@ button.search-navigator-facet { width: calc(60vw - 80px); } } - -.measure-favorite svg { - vertical-align: middle; -} 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 2af9659f827..f1c615589cd 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3656,6 +3656,7 @@ component_measures.to_select_files=to select files component_measures.to_navigate=to navigate component_measures.to_navigate_files=to next/previous file component_measures.hidden_best_score_metrics=There are {0} hidden components with a score of {1}. +component_measures.hidden_best_score_metrics_show_label=Show hidden components component_measures.navigation=Measures navigation component_measures.skip_to_navigation=Skip to measure navigation |