From 0e8a9dfad579f1a7a2c7ac92c2d4f1d63856b40d Mon Sep 17 00:00:00 2001 From: stanislavh Date: Wed, 24 Jan 2024 11:05:02 +0100 Subject: [PATCH] SONAR-21467 Adopts new code measures tab to Clean Code taxonomy --- .../design-system/src/components/Card.tsx | 14 +- .../src/components/__tests__/Card-test.tsx | 36 ++-- .../src/components/icons/SnoozeCircleIcon.tsx | 14 +- .../design-system/src/theme/light.ts | 2 + .../overview/branches/AcceptedIssuesPanel.tsx | 9 +- .../branches/BranchOverviewRenderer.tsx | 33 ++- .../branches/NewCodeMeasuresPanel.tsx | 204 ++++++++++++++++++ .../js/apps/overview/branches/TabsPanel.tsx | 4 +- .../branches/__tests__/BranchOverview-it.tsx | 29 ++- .../MeasuresCard.tsx | 0 .../MeasuresCardNumber.tsx | 6 +- .../MeasuresCardPercent.tsx | 38 ++-- .../components/QualityGateStatusTitle.tsx | 4 +- .../pullRequests/IssueMeasuresCard.tsx | 22 +- .../pullRequests/MeasuresCardPanel.tsx | 13 +- .../src/main/js/apps/overview/utils.tsx | 1 + .../resources/org/sonar/l10n/core.properties | 6 +- 17 files changed, 358 insertions(+), 77 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/overview/branches/NewCodeMeasuresPanel.tsx rename server/sonar-web/src/main/js/apps/overview/{pullRequests => components}/MeasuresCard.tsx (100%) rename server/sonar-web/src/main/js/apps/overview/{pullRequests => components}/MeasuresCardNumber.tsx (93%) rename server/sonar-web/src/main/js/apps/overview/{pullRequests => components}/MeasuresCardPercent.tsx (83%) diff --git a/server/sonar-web/design-system/src/components/Card.tsx b/server/sonar-web/design-system/src/components/Card.tsx index e587d49d2a5..43ef238a2e3 100644 --- a/server/sonar-web/design-system/src/components/Card.tsx +++ b/server/sonar-web/design-system/src/components/Card.tsx @@ -26,18 +26,24 @@ interface CardProps extends React.HTMLAttributes { children: React.ReactNode; } -export function Card(props: CardProps) { +export function Card(props: Readonly) { const { children, ...rest } = props; return {children}; } -export function GreyCard(props: CardProps) { +export function GreyCard(props: Readonly) { const { children, ...rest } = props; return {children}; } +export function LightGreyCard(props: Readonly) { + const { children, ...rest } = props; + + return {children}; +} + export const CardWithPrimaryBackground = styled(Card)` background-color: ${themeColor('backgroundPrimary')}; `; @@ -53,3 +59,7 @@ const CardStyled = styled.div` const GreyCardStyled = styled(CardStyled)` border: ${themeBorder('default', 'almCardBorder')}; `; + +const LightGreyCardStyled = styled(CardStyled)` + border: ${themeBorder('default')}; +`; diff --git a/server/sonar-web/design-system/src/components/__tests__/Card-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Card-test.tsx index bd28481bf72..f6d02f77f8d 100644 --- a/server/sonar-web/design-system/src/components/__tests__/Card-test.tsx +++ b/server/sonar-web/design-system/src/components/__tests__/Card-test.tsx @@ -19,7 +19,7 @@ */ import { screen } from '@testing-library/react'; import { render } from '../../helpers/testUtils'; -import { Card, GreyCard } from '../Card'; +import { Card, GreyCard, LightGreyCard } from '../Card'; it('renders card correctly', () => { render(Hello); @@ -30,24 +30,16 @@ it('renders card correctly', () => { }); }); -it('renders card correctly with classNames', () => { - render( - - Hello - , - ); - const cardContent = screen.getByText('Hello'); - expect(cardContent).toHaveClass('sw-bg-black sw-border-8'); - expect(cardContent).toHaveAttribute('role', 'tabpanel'); -}); - -it('renders grey card correctly with classNames', () => { - render( - - Hello - , - ); - const cardContent = screen.getByText('Hello'); - expect(cardContent).toHaveClass('sw-bg-black sw-border-8'); - expect(cardContent).toHaveAttribute('role', 'tabpanel'); -}); +it.each([Card, GreyCard, LightGreyCard])( + 'renders %p correctly with classNames', + (CardComponent) => { + render( + + Hello + , + ); + const cardContent = screen.getByText('Hello'); + expect(cardContent).toHaveClass('sw-bg-black sw-border-8'); + expect(cardContent).toHaveAttribute('role', 'tabpanel'); + }, +); diff --git a/server/sonar-web/design-system/src/components/icons/SnoozeCircleIcon.tsx b/server/sonar-web/design-system/src/components/icons/SnoozeCircleIcon.tsx index 4324edc9848..95f034c7307 100644 --- a/server/sonar-web/design-system/src/components/icons/SnoozeCircleIcon.tsx +++ b/server/sonar-web/design-system/src/components/icons/SnoozeCircleIcon.tsx @@ -19,16 +19,22 @@ */ import { useTheme } from '@emotion/react'; import { themeColor, themeContrast } from '../../helpers'; +import { ThemeColors } from '../../types'; import { CustomIcon, IconProps } from './Icon'; -export function SnoozeCircleIcon(props: Readonly) { +interface SnoozeCircleIconProps extends IconProps { + color?: ThemeColors; +} + +export function SnoozeCircleIcon(props: Readonly) { + const { color = 'overviewCardWarningIcon', ...rest } = props; const theme = useTheme(); - const bgColor = themeColor('overviewCardWarningIcon')({ theme }); - const iconColor = themeContrast('overviewCardWarningIcon')({ theme }); + const bgColor = themeColor(color)({ theme }); + const iconColor = themeContrast(color)({ theme }); return ( - + ) { id: isNewCode ? 'overview.accepted_issues' : 'overview.accepted_issues.total', })} url={acceptedIssuesUrl} - icon={} + icon={ + + } /> {!isNewCode && ( <> diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx index 9c6e4415766..289d9131bf0 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx @@ -17,7 +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 { BasicSeparator, Card, LargeCenteredLayout, PageContentFontWrapper } from 'design-system'; +import { + BasicSeparator, + LargeCenteredLayout, + LightGreyCard, + PageContentFontWrapper, +} from 'design-system'; import * as React from 'react'; import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; import { useLocation } from '../../../components/hoc/withRouter'; @@ -37,8 +42,9 @@ import AcceptedIssuesPanel from './AcceptedIssuesPanel'; import ActivityPanel from './ActivityPanel'; import BranchMetaTopBar from './BranchMetaTopBar'; import FirstAnalysisNextStepsNotif from './FirstAnalysisNextStepsNotif'; -import MeasuresPanel from './MeasuresPanel'; +import { MeasuresPanel } from './MeasuresPanel'; import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode'; +import NewCodeMeasuresPanel from './NewCodeMeasuresPanel'; import NoCodeWarning from './NoCodeWarning'; import QualityGatePanel from './QualityGatePanel'; import { TabsPanel } from './TabsPanel'; @@ -128,20 +134,20 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
- + - + qg.failedConditions)} />
- +
- {!hasNewCodeMeasures && isNewCodeTab ? ( + {!hasNewCodeMeasures && isNewCodeTab && ( - ) : ( + )} + + {hasNewCodeMeasures && isNewCodeTab && ( + + )} + + {!isNewCodeTab && ( <>
-
+
)} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/NewCodeMeasuresPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/NewCodeMeasuresPanel.tsx new file mode 100644 index 00000000000..a579166992b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/branches/NewCodeMeasuresPanel.tsx @@ -0,0 +1,204 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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 { + LightGreyCard, + LightLabel, + SnoozeCircleIcon, + TextError, + TextSubdued, + TrendUpCircleIcon, + themeColor, +} from 'design-system'; +import React from 'react'; +import { useIntl } from 'react-intl'; +import { getLeakValue } from '../../../components/measure/utils'; +import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils'; +import { getBranchLikeQuery } from '../../../helpers/branch-like'; +import { findMeasure, formatMeasure } from '../../../helpers/measures'; +import { + getComponentDrilldownUrl, + getComponentIssuesUrl, + getComponentSecurityHotspotsUrl, +} from '../../../helpers/urls'; +import { Branch } from '../../../types/branch-like'; +import { isApplication } from '../../../types/component'; +import { IssueStatus } from '../../../types/issues'; +import { MetricKey, MetricType } from '../../../types/metrics'; +import { QualityGateStatus } from '../../../types/quality-gates'; +import { Component, MeasureEnhanced } from '../../../types/types'; +import { IssueMeasuresCardInner } from '../components/IssueMeasuresCardInner'; +import MeasuresCardNumber from '../components/MeasuresCardNumber'; +import MeasuresCardPercent from '../components/MeasuresCardPercent'; +import { + MeasurementType, + Status, + getConditionRequiredLabel, + getMeasurementMetricKey, +} from '../utils'; + +interface Props { + branch?: Branch; + component: Component; + measures: MeasureEnhanced[]; + qgStatuses?: QualityGateStatus[]; +} + +export default function NewCodeMeasuresPanel(props: Readonly) { + const { branch, component, measures, qgStatuses } = props; + const intl = useIntl(); + const isApp = isApplication(component.qualifier); + + const failedConditions = qgStatuses?.flatMap((qg) => qg.failedConditions) ?? []; + + const newIssues = getLeakValue(findMeasure(measures, MetricKey.new_violations)); + const newIssuesCondition = failedConditions.find((c) => c.metric === MetricKey.new_violations); + const issuesConditionFailed = newIssuesCondition?.level === Status.ERROR; + const newAcceptedIssues = getLeakValue(findMeasure(measures, MetricKey.new_accepted_issues)); + const newSecurityHotspots = getLeakValue( + findMeasure(measures, MetricKey.new_security_hotspots), + ) as string; + + let issuesFooter; + if (newIssuesCondition && !isApp) { + issuesFooter = issuesConditionFailed ? ( + + ) : ( + + {getConditionRequiredLabel(newIssuesCondition, intl)} + + ); + } + + return ( +
+ + } + footer={issuesFooter} + /> + + + {intl.formatMessage({ id: 'overview.accepted_issues.help' })} + + } + icon={ + + } + /> + +
+ + + + + +
+
+ ); +} + +const StyledCardSeparator = styled.div` + width: 1px; + background-color: ${themeColor('projectCardBorder')}; +`; diff --git a/server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx index 87a370b243b..8b000523a8d 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx @@ -19,8 +19,8 @@ */ import { isBefore, sub } from 'date-fns'; import { + BasicSeparator, ButtonLink, - CardSeparator, FlagMessage, LightLabel, PageTitle, @@ -133,7 +133,7 @@ export function TabsPanel(props: React.PropsWithChildren) { - + {loading ? (
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx index 75e12de4e5c..22b3c562514 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx @@ -222,10 +222,10 @@ describe('project overview', () => { ).not.toBeInTheDocument(); //Measures panel - expect(screen.getByText('metric.new_vulnerabilities.name')).toBeInTheDocument(); + expect(screen.getByText('overview.new_issues')).toBeInTheDocument(); expect( byRole('link', { - name: 'overview.see_more_details_on_x_of_y.1.metric.accepted_issues.name', + name: 'overview.see_more_details_on_x_of_y.1.metric.new_accepted_issues.name', }).get(), ).toBeInTheDocument(); @@ -293,6 +293,30 @@ describe('project overview', () => { periodIndex: 0, status: 'ERROR', }, + { + actualValue: '10', + comparator: 'PT', + errorThreshold: '85', + metricKey: MetricKey.new_coverage, + periodIndex: 0, + status: 'ERROR', + }, + { + actualValue: '5', + comparator: 'GT', + errorThreshold: '2.0', + metricKey: MetricKey.new_security_hotspots_reviewed, + periodIndex: 0, + status: 'ERROR', + }, + { + actualValue: '5', + comparator: 'GT', + errorThreshold: '2.0', + metricKey: MetricKey.new_violations, + periodIndex: 0, + status: 'ERROR', + }, { actualValue: '2', comparator: 'GT', @@ -309,6 +333,7 @@ describe('project overview', () => { expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument(); expect(screen.getAllByText(/overview.X_conditions_failed/)).toHaveLength(2); + expect(screen.getAllByText(/overview.quality_gate.required_x/)).toHaveLength(3); }); it('should correctly show a project as empty', async () => { diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCard.tsx b/server/sonar-web/src/main/js/apps/overview/components/MeasuresCard.tsx similarity index 100% rename from server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCard.tsx rename to server/sonar-web/src/main/js/apps/overview/components/MeasuresCard.tsx diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCardNumber.tsx b/server/sonar-web/src/main/js/apps/overview/components/MeasuresCardNumber.tsx similarity index 93% rename from server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCardNumber.tsx rename to server/sonar-web/src/main/js/apps/overview/components/MeasuresCardNumber.tsx index d451fc0711e..1e3214186ea 100644 --- a/server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCardNumber.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/MeasuresCardNumber.tsx @@ -33,12 +33,13 @@ interface Props { url: To; value: string; conditionMetric: MetricKey; + showRequired?: boolean; } export default function MeasuresCardNumber( props: React.PropsWithChildren>, ) { - const { label, value, conditions, url, conditionMetric, ...rest } = props; + const { label, value, conditions, url, conditionMetric, showRequired = false, ...rest } = props; const intl = useIntl(); @@ -56,7 +57,8 @@ export default function MeasuresCardNumber( {...rest} > - {condition && + {showRequired && + condition && (conditionFailed ? ( <> - {condition && + {showRequired && + condition && (conditionFailed ? ( - {formatMeasure(newLinesValue ?? '0', MetricType.ShortInteger)} + {formatMeasure(linesValue ?? '0', MetricType.ShortInteger)} ), }} diff --git a/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx b/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx index 4e2e543581c..14e985cb86b 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/QualityGateStatusTitle.tsx @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { CardSeparator, HelperHintIcon, PageTitle } from 'design-system'; +import { BasicSeparator, HelperHintIcon, PageTitle } from 'design-system'; import React from 'react'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import { translate } from '../../../helpers/l10n'; @@ -36,7 +36,7 @@ export function QualityGateStatusTitle() {
- + ); } diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/IssueMeasuresCard.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/IssueMeasuresCard.tsx index 0b27c1b6eff..e4017a68a95 100644 --- a/server/sonar-web/src/main/js/apps/overview/pullRequests/IssueMeasuresCard.tsx +++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/IssueMeasuresCard.tsx @@ -19,17 +19,17 @@ */ import styled from '@emotion/styled'; import { - Card, HelperHintIcon, + LightGreyCard, LightLabel, PopupPlacement, SnoozeCircleIcon, TextError, TextSubdued, - themeColor, Tooltip, TrendDownCircleIcon, TrendUpCircleIcon, + themeColor, } from 'design-system'; import * as React from 'react'; import { useIntl } from 'react-intl'; @@ -43,7 +43,7 @@ import { MetricKey, MetricType } from '../../../types/metrics'; import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; import { Component, MeasureEnhanced } from '../../../types/types'; import { IssueMeasuresCardInner } from '../components/IssueMeasuresCardInner'; -import { getConditionRequiredLabel, Status } from '../utils'; +import { Status, getConditionRequiredLabel } from '../utils'; interface Props { conditions: QualityGateStatusConditionEnhanced[]; @@ -79,10 +79,10 @@ export default function IssueMeasuresCard( }); return ( - + } + icon={ + + } footer={ - {intl.formatMessage({ id: 'overview.pull_request.accepted_issues.help' })} + {intl.formatMessage({ id: 'overview.accepted_issues.help' })} } /> @@ -155,7 +159,7 @@ export default function IssueMeasuresCard( } /> - + ); } diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCardPanel.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCardPanel.tsx index 2df727a1abe..37a36ecb1e2 100644 --- a/server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCardPanel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCardPanel.tsx @@ -27,10 +27,10 @@ import { PullRequest } from '../../../types/branch-like'; import { MetricKey } from '../../../types/metrics'; import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; import { Component, MeasureEnhanced } from '../../../types/types'; +import MeasuresCardNumber from '../components/MeasuresCardNumber'; +import MeasuresCardPercent from '../components/MeasuresCardPercent'; import { MeasurementType, getMeasurementMetricKey } from '../utils'; import IssueMeasuresCard from './IssueMeasuresCard'; -import MeasuresCardNumber from './MeasuresCardNumber'; -import MeasuresCardPercent from './MeasuresCardPercent'; interface Props { className?: string; @@ -71,8 +71,10 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren) })} conditions={conditions} conditionMetric={MetricKey.new_coverage} - newLinesMetric={MetricKey.new_lines_to_cover} + linesMetric={MetricKey.new_lines_to_cover} measures={measures} + showRequired + useDiffMetric /> ) value={newSecurityHotspots} conditions={conditions} conditionMetric={MetricKey.new_security_hotspots_reviewed} + showRequired /> @@ -104,8 +107,10 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren) })} conditions={conditions} conditionMetric={MetricKey.new_duplicated_lines_density} - newLinesMetric={MetricKey.new_lines} + linesMetric={MetricKey.new_lines} measures={measures} + useDiffMetric + showRequired /> diff --git a/server/sonar-web/src/main/js/apps/overview/utils.tsx b/server/sonar-web/src/main/js/apps/overview/utils.tsx index 47021a519c5..f0e14d2caeb 100644 --- a/server/sonar-web/src/main/js/apps/overview/utils.tsx +++ b/server/sonar-web/src/main/js/apps/overview/utils.tsx @@ -38,6 +38,7 @@ export const METRICS: string[] = [ MetricKey.quality_gate_details, // TODO: still relevant? // issues + MetricKey.new_violations, MetricKey.accepted_issues, MetricKey.new_accepted_issues, MetricKey.high_impact_accepted_issues, 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 c48bc626eda..4a9156f4d3a 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3852,13 +3852,13 @@ overview.X_conditions_failed={conditions} {conditions, plural, one {failed condi overview.failed_condition.x_rating_required={rating} is {value}. Required {threshold} overview.failed_condition.x_required={metric}. Required {threshold} overview.fix_failed_conditions_with_sonarlint=Fix issues before they fail your Quality Gate with {link} in your IDE. Power up with connected mode! -overview.pull_request.new_issues=New issues +overview.new_issues=New issues overview.pull_request.fixed_issues=Fixed issues overview.pull_request.fixed_issues.help=Estimation of issues fixed by this PR overview.pull_request.fixed_issues.disclaimer=Only issues fixed on the files modified by the pull request are taken into account. Issues incidentally fixed on unmodified files are not counted. overview.pull_request.fixed_issues.disclaimer.2=When the pull request and the target branch are not synchronized, issues introduced on the target branch may be incorrectly considered fixed by the pull request. Rebasing the pull request would give an updated value. -overview.pull_request.accepted_issues=Accepted issues -overview.pull_request.accepted_issues.help=Valid issues that were not fixed +overview.accepted_issues=Accepted issues +overview.accepted_issues.help=Valid issues that were not fixed overview.quality_gate.status=Quality Gate Status overview.quality_gate=Quality Gate overview.quality_gate_x=Quality Gate: {0} -- 2.39.5