Browse Source

SONAR-21455 Update overall tab secondary measures cards

tags/10.4.0.87286
7PH 4 months ago
parent
commit
ff18a5bce1
24 changed files with 353 additions and 835 deletions
  1. 1
    1
      server/sonar-web/design-system/src/components/CoverageIndicator.tsx
  2. 1
    1
      server/sonar-web/design-system/src/components/DuplicationsIndicator.tsx
  3. 18
    2
      server/sonar-web/design-system/src/components/icons/NoDataIcon.tsx
  4. 0
    145
      server/sonar-web/src/main/js/apps/overview/branches/AcceptedIssuesPanel.tsx
  5. 0
    56
      server/sonar-web/src/main/js/apps/overview/branches/BranchOverallCodePanel.tsx
  6. 13
    7
      server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
  7. 16
    28
      server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
  8. 0
    102
      server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx
  9. 0
    85
      server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasure.tsx
  10. 91
    0
      server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentCards.tsx
  11. 0
    140
      server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasure.tsx
  12. 0
    74
      server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasureLabel.tsx
  13. 14
    51
      server/sonar-web/src/main/js/apps/overview/branches/NewCodeMeasuresPanel.tsx
  14. 146
    0
      server/sonar-web/src/main/js/apps/overview/branches/OverallCodeMeasuresPanel.tsx
  15. 2
    2
      server/sonar-web/src/main/js/apps/overview/components/IssueMeasuresCardInner.tsx
  16. 3
    3
      server/sonar-web/src/main/js/apps/overview/components/MeasuresCard.tsx
  17. 3
    4
      server/sonar-web/src/main/js/apps/overview/components/MeasuresCardNumber.tsx
  18. 38
    39
      server/sonar-web/src/main/js/apps/overview/components/MeasuresCardPercent.tsx
  19. 0
    52
      server/sonar-web/src/main/js/apps/overview/pullRequests/AfterMergeEstimate.tsx
  20. 1
    0
      server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCardPanel.tsx
  21. 0
    41
      server/sonar-web/src/main/js/apps/overview/utils.tsx
  22. 3
    1
      server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts
  23. 1
    0
      server/sonar-web/src/main/js/types/quality-gates.ts
  24. 2
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 1
server/sonar-web/design-system/src/components/CoverageIndicator.tsx View File

@@ -38,7 +38,7 @@ export function CoverageIndicator({ size = 'sm', value }: CoverageIndicatorProps
const thickness = SIZE_TO_THICKNESS_MAPPING[size];

if (value === undefined) {
return <NoDataIcon height={width} width={width} />;
return <NoDataIcon size={size} />;
}

const themeRed = themeColor('coverageRed')({ theme });

+ 1
- 1
server/sonar-web/design-system/src/components/DuplicationsIndicator.tsx View File

@@ -35,7 +35,7 @@ export function DuplicationsIndicator({ size = 'sm', rating }: Props) {
const sizePX = SIZE_TO_PX_MAPPING[size];

if (rating === undefined) {
return <NoDataIcon height={sizePX} width={sizePX} />;
return <NoDataIcon size={size} />;
}

const primaryColor = themeColor(`duplicationsIndicator.${rating}`)({ theme });

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

@@ -19,9 +19,25 @@
*/
import { CustomIcon, IconProps } from './Icon';

export function NoDataIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
export interface NoDataIconProps extends IconProps {
size?: 'xs' | 'sm' | 'md';
}

const SIZES: Record<NonNullable<NoDataIconProps['size']>, number> = {
xs: 16,
sm: 24,
md: 36,
};

export function NoDataIcon({
fill = 'currentColor',
size = 'md',
...iconProps
}: Readonly<NoDataIconProps>) {
const iconSize = SIZES[size];

return (
<CustomIcon {...iconProps}>
<CustomIcon height={iconSize} width={iconSize} {...iconProps}>
<path
clipRule="evenodd"
d="M16 8C16 12.4183 12.4183 16 8 16C5.5106 16 3.28676 14.863 1.81951 13.0799L15.4913 5.1865C15.8201 6.06172 16 7.00986 16 8ZM14.5574 3.41624L0.750565 11.3876C0.269025 10.3589 0 9.21089 0 8C0 3.58172 3.58172 0 8 0C10.7132 0 13.1109 1.35064 14.5574 3.41624Z"

+ 0
- 145
server/sonar-web/src/main/js/apps/overview/branches/AcceptedIssuesPanel.tsx View File

@@ -1,145 +0,0 @@
/*
* 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 classNames from 'classnames';
import {
Card,
HighImpactCircleIcon,
LightLabel,
NoDataIcon,
PageTitle,
SnoozeCircleIcon,
Spinner,
themeColor,
} from 'design-system';
import * as 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 { getComponentIssuesUrl } from '../../../helpers/urls';
import { Branch } from '../../../types/branch-like';
import { SoftwareImpactSeverity } from '../../../types/clean-code-taxonomy';
import { IssueStatus } from '../../../types/issues';
import { MetricKey, MetricType } from '../../../types/metrics';
import { Component, MeasureEnhanced } from '../../../types/types';
import { IssueMeasuresCardInner } from '../components/IssueMeasuresCardInner';

export interface AcceptedIssuesPanelProps {
branch?: Branch;
component: Component;
measures?: MeasureEnhanced[];
isNewCode: boolean;
loading?: boolean;
}

function AcceptedIssuesPanel(props: Readonly<AcceptedIssuesPanelProps>) {
const { branch, component, measures = [], isNewCode, loading } = props;
const intl = useIntl();

const acceptedIssuesUrl = getComponentIssuesUrl(component.key, {
...getBranchLikeQuery(branch),
issueStatuses: IssueStatus.Accepted,
...(isNewCode ? { inNewCodePeriod: 'true' } : {}),
});

const acceptedIssuesWithHighImpactUrl = getComponentIssuesUrl(component.key, {
...getBranchLikeQuery(branch),
...DEFAULT_ISSUES_QUERY,
issueStatuses: IssueStatus.Accepted,
impactSeverities: SoftwareImpactSeverity.High,
});

const acceptedCount = isNewCode
? getLeakValue(findMeasure(measures, MetricKey.new_accepted_issues))
: findMeasure(measures, MetricKey.accepted_issues)?.value;

const acceptedWithHighImpactCount = isNewCode
? undefined
: findMeasure(measures, MetricKey.high_impact_accepted_issues)?.value;

return (
<div className="sw-mt-8">
<PageTitle as="h2" text={intl.formatMessage({ id: 'overview.accepted_issues' })} />
<LightLabel as="div" className="sw-mt-1 sw-mb-4">
{intl.formatMessage({ id: 'overview.accepted_issues.description' })}
</LightLabel>
<Spinner loading={loading}>
<div
className={classNames('sw-grid sw-gap-4', {
'sw-grid-cols-2': isNewCode,
'sw-grid-cols-1': !isNewCode,
})}
>
<Card className="sw-flex sw-gap-4">
<IssueMeasuresCardInner
disabled={component.needIssueSync}
className={classNames({ 'sw-w-1/2': !isNewCode, 'sw-w-full': isNewCode })}
metric={MetricKey.accepted_issues}
value={formatMeasure(acceptedCount, MetricType.ShortInteger)}
header={intl.formatMessage({
id: isNewCode ? 'overview.accepted_issues' : 'overview.accepted_issues.total',
})}
url={acceptedIssuesUrl}
icon={
<SnoozeCircleIcon
color={
acceptedCount === '0' ? 'overviewCardDefaultIcon' : 'overviewCardWarningIcon'
}
className="sw--translate-y-3"
/>
}
/>
{!isNewCode && (
<>
<StyledCardSeparator />
<IssueMeasuresCardInner
disabled={Boolean(component.needIssueSync) || !acceptedWithHighImpactCount}
className="sw-w-1/2"
metric={MetricKey.high_impact_accepted_issues}
value={formatMeasure(acceptedWithHighImpactCount, MetricType.ShortInteger)}
header={intl.formatMessage({
id: 'overview.high_impact_accepted_issues',
})}
url={acceptedIssuesWithHighImpactUrl}
icon={
acceptedWithHighImpactCount ? (
<HighImpactCircleIcon className="sw--translate-y-3" />
) : (
<NoDataIcon className="sw--translate-y-3" width={36} height={36} />
)
}
/>
</>
)}
</Card>
</div>
</Spinner>
</div>
);
}

const StyledCardSeparator = styled.div`
width: 1px;
background-color: ${themeColor('projectCardBorder')};
`;

export default React.memo(AcceptedIssuesPanel);

+ 0
- 56
server/sonar-web/src/main/js/apps/overview/branches/BranchOverallCodePanel.tsx View File

@@ -1,56 +0,0 @@
/*
* 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 * as React from 'react';
import { SoftwareQuality } from '../../../types/clean-code-taxonomy';
import { MetricKey } from '../../../types/metrics';
import { Component, MeasureEnhanced } from '../../../types/types';
import SoftwareImpactMeasureCard from './SoftwareImpactMeasureCard';

export interface BranchOverallCodePanelProps {
component: Component;
measures: MeasureEnhanced[];
}

export default function BranchOverallCodePanel(props: Readonly<BranchOverallCodePanelProps>) {
const { component, measures } = props;

return (
<div className="sw-flex sw-gap-4 sw-mt-4">
<SoftwareImpactMeasureCard
component={component}
softwareQuality={SoftwareQuality.Security}
ratingMetricKey={MetricKey.security_rating}
measures={measures}
/>
<SoftwareImpactMeasureCard
component={component}
softwareQuality={SoftwareQuality.Reliability}
ratingMetricKey={MetricKey.reliability_rating}
measures={measures}
/>
<SoftwareImpactMeasureCard
component={component}
softwareQuality={SoftwareQuality.Maintainability}
ratingMetricKey={MetricKey.sqale_rating}
measures={measures}
/>
</div>
);
}

+ 13
- 7
server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx View File

@@ -54,7 +54,7 @@ import { Analysis, GraphType, MeasureHistory } from '../../../types/project-acti
import { QualityGateStatus, QualityGateStatusCondition } from '../../../types/quality-gates';
import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../../types/types';
import '../styles.css';
import { BRANCH_OVERVIEW_METRICS, HISTORY_METRICS_LIST } from '../utils';
import { BRANCH_OVERVIEW_METRICS, HISTORY_METRICS_LIST, Status } from '../utils';
import BranchOverviewRenderer from './BranchOverviewRenderer';

interface Props {
@@ -187,12 +187,14 @@ export default class BranchOverview extends React.PureComponent<Props, State> {
(results) => {
if (this.mounted) {
const qgStatuses = results
.map(({ measures = [], project, projectBranchLike }) => {
.map(({ measures = [], project, projectBranchLike }): QualityGateStatus => {
const { key, name, status, caycStatus } = project;
const conditions = extractStatusConditionsFromApplicationStatusChildProject(project);
const failedConditions = this.getFailedConditions(conditions, measures);
const enhancedConditions = this.getEnhancedConditions(conditions, measures);
const failedConditions = enhancedConditions.filter((c) => c.level !== Status.OK);

return {
conditions: enhancedConditions,
failedConditions,
caycStatus,
key,
@@ -244,11 +246,13 @@ export default class BranchOverview extends React.PureComponent<Props, State> {
if (this.mounted && measures) {
const { ignoredConditions, caycStatus, status } = projectStatus;
const conditions = extractStatusConditionsFromProjectStatus(projectStatus);
const failedConditions = this.getFailedConditions(conditions, measures);
const enhancedConditions = this.getEnhancedConditions(conditions, measures);
const failedConditions = enhancedConditions.filter((c) => c.level !== Status.OK);

const qgStatus = {
const qgStatus: QualityGateStatus = {
ignoredConditions,
caycStatus,
conditions: enhancedConditions,
failedConditions,
key,
name,
@@ -362,10 +366,12 @@ export default class BranchOverview extends React.PureComponent<Props, State> {
);
};

getFailedConditions = (conditions: QualityGateStatusCondition[], measures: MeasureEnhanced[]) => {
getEnhancedConditions = (
conditions: QualityGateStatusCondition[],
measures: MeasureEnhanced[],
) => {
return (
conditions
.filter((c) => c.level !== 'OK')
// Enhance them with Metric information, which will be needed
// to render the conditions properly.
.map((c) => enhanceConditionWithMeasure(c, measures))

+ 16
- 28
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx View File

@@ -41,15 +41,13 @@ import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../.
import { AnalysisStatus } from '../components/AnalysisStatus';
import SonarLintPromotion from '../components/SonarLintPromotion';
import { MeasuresTabs } from '../utils';
import AcceptedIssuesPanel from './AcceptedIssuesPanel';
import ActivityPanel from './ActivityPanel';
import BranchMetaTopBar from './BranchMetaTopBar';
import BranchOverallCodePanel from './BranchOverallCodePanel';
import FirstAnalysisNextStepsNotif from './FirstAnalysisNextStepsNotif';
import { MeasuresPanel } from './MeasuresPanel';
import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode';
import NewCodeMeasuresPanel from './NewCodeMeasuresPanel';
import NoCodeWarning from './NoCodeWarning';
import OverallCodeMeasuresPanel from './OverallCodeMeasuresPanel';
import QualityGatePanel from './QualityGatePanel';
import { TabsPanel } from './TabsPanel';

@@ -173,14 +171,6 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
isNewCode={isNewCodeTab}
onTabSelect={selectTab}
>
{!hasNewCodeMeasures && isNewCodeTab && (
<MeasuresPanelNoNewCode
branch={branch}
component={component}
period={period}
/>
)}

{hasNewCodeMeasures &&
isMissingMeasures &&
isApplication(component.qualifier) && (
@@ -191,11 +181,15 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
</FlagMessage>
)}

{!isNewCodeTab && (
<BranchOverallCodePanel component={component} measures={measures} />
{isNewCodeTab && !hasNewCodeMeasures && (
<MeasuresPanelNoNewCode
branch={branch}
component={component}
period={period}
/>
)}

{hasNewCodeMeasures && isNewCodeTab && (
{isNewCodeTab && hasNewCodeMeasures && (
<NewCodeMeasuresPanel
qgStatuses={qgStatuses}
branch={branch}
@@ -204,20 +198,14 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
/>
)}

<MeasuresPanel
branch={branch}
component={component}
measures={measures}
isNewCode={isNewCodeTab}
/>

<AcceptedIssuesPanel
branch={branch}
component={component}
measures={measures}
isNewCode={isNewCodeTab}
loading={loadingStatus}
/>
{!isNewCodeTab && (
<OverallCodeMeasuresPanel
branch={branch}
qgStatuses={qgStatuses}
component={component}
measures={measures}
/>
)}
</TabsPanel>

<ActivityPanel

+ 0
- 102
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx View File

@@ -1,102 +0,0 @@
/*
* 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 { Card, CoverageIndicator, DuplicationsIndicator } from 'design-system';
import * as React from 'react';
import { getTabPanelId } from '../../../components/controls/BoxedTabs';
import { duplicationRatingConverter } from '../../../components/measure/utils';
import { findMeasure } from '../../../helpers/measures';
import { Branch } from '../../../types/branch-like';
import { IssueType } from '../../../types/issues';
import { MetricKey } from '../../../types/metrics';
import { Component, MeasureEnhanced } from '../../../types/types';
import { MeasurementType, MeasuresTabs } from '../utils';
import MeasuresPanelIssueMeasure from './MeasuresPanelIssueMeasure';
import MeasuresPanelPercentMeasure from './MeasuresPanelPercentMeasure';

export interface MeasuresPanelProps {
branch?: Branch;
component: Component;
measures: MeasureEnhanced[];
isNewCode: boolean;
}

export function MeasuresPanel(props: MeasuresPanelProps) {
const { branch, component, measures, isNewCode } = props;

return (
<div
className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-6"
id={getTabPanelId(MeasuresTabs.Overall)}
>
{[IssueType.Bug, IssueType.CodeSmell, IssueType.Vulnerability, IssueType.SecurityHotspot].map(
(type: IssueType) => (
<Card key={type} className="sw-p-8">
<MeasuresPanelIssueMeasure
branchLike={branch}
component={component}
isNewCodeTab={isNewCode}
measures={measures}
type={type}
/>
</Card>
),
)}

{(findMeasure(measures, MetricKey.coverage) ||
findMeasure(measures, MetricKey.new_coverage)) && (
<Card className="sw-p-8" data-test="overview__measures-coverage">
<MeasuresPanelPercentMeasure
branchLike={branch}
component={component}
measures={measures}
ratingIcon={renderCoverageIcon}
secondaryMetricKey={MetricKey.tests}
type={MeasurementType.Coverage}
useDiffMetric={isNewCode}
/>
</Card>
)}

<Card className="sw-p-8">
<MeasuresPanelPercentMeasure
branchLike={branch}
component={component}
measures={measures}
ratingIcon={renderDuplicationIcon}
secondaryMetricKey={MetricKey.duplicated_blocks}
type={MeasurementType.Duplication}
useDiffMetric={isNewCode}
/>
</Card>
</div>
);
}

export default React.memo(MeasuresPanel);

function renderCoverageIcon(value?: string) {
return <CoverageIndicator value={value} size="md" />;
}

function renderDuplicationIcon(value?: string) {
const rating = value !== undefined ? duplicationRatingConverter(Number(value)) : undefined;

return <DuplicationsIndicator rating={rating} size="md" />;
}

+ 0
- 85
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelIssueMeasure.tsx View File

@@ -1,85 +0,0 @@
/*
* 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 { LightPrimary, ThemeColors } from 'design-system';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
import { BranchLike } from '../../../types/branch-like';
import { ComponentQualifier } from '../../../types/component';
import { IssueType } from '../../../types/issues';
import { Component, MeasureEnhanced } from '../../../types/types';
import IssueLabel from '../components/IssueLabel';
import IssueRating from '../components/IssueRating';
import { getIssueIconClass, getIssueRatingName } from '../utils';
import MeasuresPanelCard from './MeasuresPanelCard';

interface Props {
branchLike?: BranchLike;
component: Component;
isNewCodeTab: boolean;
measures: MeasureEnhanced[];
type: IssueType;
}

export default function MeasuresPanelIssueMeasure(props: Props) {
const { branchLike, component, isNewCodeTab, measures, type } = props;

const isApp = component.qualifier === ComponentQualifier.Application;

const IconClass = getIssueIconClass(type) as (args: {
className?: string;
fill?: ThemeColors;
}) => JSX.Element;

return (
<MeasuresPanelCard
data-test={`overview__measures-${type.toString().toLowerCase()}`}
category={
<div className="sw-flex sw-items-center">
<IconClass className="sw-mr-1" fill="discreetInteractiveIcon" />
<LightPrimary>{getIssueRatingName(type)}</LightPrimary>
</div>
}
rating={
!isApp || !isNewCodeTab ? (
<IssueRating
branchLike={branchLike}
component={component}
measures={measures}
type={type}
useDiffMetric={isNewCodeTab}
/>
) : null
}
>
<IssueLabel
branchLike={branchLike}
component={component}
helpTooltip={
type === IssueType.SecurityHotspot
? translate('metric.security_hotspots.full_description')
: undefined
}
measures={measures}
type={type}
useDiffMetric={isNewCodeTab}
/>
</MeasuresPanelCard>
);
}

+ 91
- 0
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentCards.tsx View File

@@ -0,0 +1,91 @@
/*
* 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 * as React from 'react';
import { getComponentDrilldownUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like';
import { isApplication } from '../../../types/component';
import { MetricKey } from '../../../types/metrics';
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
import { Component, MeasureEnhanced } from '../../../types/types';
import MeasuresCardPercent from '../components/MeasuresCardPercent';
import { MeasurementType, getMeasurementMetricKey } from '../utils';

interface Props {
useDiffMetric?: boolean;
branch?: BranchLike;
component: Component;
measures: MeasureEnhanced[];
failedConditions: QualityGateStatusConditionEnhanced[];
}

/**
* Renders Coverage and Duplication cards for the Overview page.
*/
export default function MeasuresPanelPercentCards(props: Readonly<Props>) {
const { useDiffMetric, branch, component, measures, failedConditions } = props;

const isApp = isApplication(component.qualifier);

return (
<div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
<MeasuresCardPercent
branchLike={branch}
componentKey={component.key}
conditions={failedConditions}
measures={measures}
measurementType={MeasurementType.Coverage}
label="overview.quality_gate.coverage"
url={getComponentDrilldownUrl({
componentKey: component.key,
metric: getMeasurementMetricKey(MeasurementType.Coverage, Boolean(useDiffMetric)),
branchLike: branch,
listView: true,
})}
conditionMetric={useDiffMetric ? MetricKey.new_coverage : MetricKey.coverage}
linesMetric={useDiffMetric ? MetricKey.new_lines_to_cover : MetricKey.lines_to_cover}
useDiffMetric={useDiffMetric}
showRequired={!isApp}
/>

<MeasuresCardPercent
branchLike={branch}
componentKey={component.key}
conditions={failedConditions}
measures={measures}
measurementType={MeasurementType.Duplication}
label="overview.quality_gate.duplications"
url={getComponentDrilldownUrl({
componentKey: component.key,
metric: getMeasurementMetricKey(MeasurementType.Duplication, Boolean(useDiffMetric)),
branchLike: branch,
listView: true,
})}
conditionMetric={
useDiffMetric
? MetricKey.new_duplicated_lines_density
: MetricKey.duplicated_lines_density
}
linesMetric={useDiffMetric ? MetricKey.new_lines : MetricKey.lines}
useDiffMetric={useDiffMetric}
showRequired={!isApp}
/>
</div>
);
}

+ 0
- 140
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasure.tsx View File

@@ -1,140 +0,0 @@
/*
* 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 { DrilldownLink, GreySeparator, LightLabel, LightPrimary } from 'design-system';
import * as React from 'react';
import { getLeakValue } from '../../../components/measure/utils';
import { isPullRequest } from '../../../helpers/branch-like';
import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
import { findMeasure, formatMeasure, localizeMetric } from '../../../helpers/measures';
import { getComponentDrilldownUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like';
import { MetricKey, MetricType } from '../../../types/metrics';
import { Component, MeasureEnhanced } from '../../../types/types';
import AfterMergeEstimate from '../pullRequests/AfterMergeEstimate';
import { MeasurementType, getMeasurementMetricKey } from '../utils';
import DrilldownMeasureValue from './DrilldownMeasureValue';
import MeasuresPanelCard from './MeasuresPanelCard';
import MeasuresPanelPercentMeasureLabel from './MeasuresPanelPercentMeasureLabel';

interface Props {
branchLike?: BranchLike;
component: Component;
useDiffMetric: boolean;
measures: MeasureEnhanced[];
ratingIcon: (value: string | undefined) => React.ReactElement;
secondaryMetricKey?: MetricKey;
type: MeasurementType;
}

export default function MeasuresPanelPercentMeasure(props: Props) {
const {
branchLike,
component,
measures,
ratingIcon,
secondaryMetricKey,
type,
useDiffMetric = false,
} = props;
const metricKey = getMeasurementMetricKey(type, useDiffMetric);
const measure = findMeasure(measures, metricKey);

let value;
if (measure) {
value = useDiffMetric ? getLeakValue(measure) : measure.value;
}

const url = getComponentDrilldownUrl({
componentKey: component.key,
metric: metricKey,
branchLike,
listView: true,
});

const formattedValue = formatMeasure(value, MetricType.Percent, {
decimals: 2,
omitExtraDecimalZeros: true,
});

return (
<MeasuresPanelCard
data-test={`overview__measures-${type.toString().toLowerCase()}`}
category={<LightPrimary>{translate('overview.measurement_type', type)}</LightPrimary>}
rating={ratingIcon(value)}
>
<>
<div className="sw-body-md sw-flex sw-items-center sw-mb-3">
{value === undefined ? (
<LightLabel aria-label={translate('no_data')}> — </LightLabel>
) : (
<DrilldownLink
aria-label={translateWithParameters(
'overview.see_more_details_on_x_of_y',
value,
localizeMetric(metricKey),
)}
to={url}
>
{formattedValue}
</DrilldownLink>
)}

<LightLabel className="sw-ml-2">
{translate('overview.measurement_type', type)}
</LightLabel>
</div>
<MeasuresPanelPercentMeasureLabel
component={component}
measures={measures}
type={type}
useDiffMetric={useDiffMetric}
branchLike={branchLike}
/>

{!useDiffMetric && secondaryMetricKey && (
<>
<GreySeparator className="sw-mt-4" />
<div className="sw-body-md sw-flex sw-items-center sw-mt-4">
<DrilldownMeasureValue
branchLike={branchLike}
component={component}
measures={measures}
metric={secondaryMetricKey}
/>
<LightLabel className="sw-ml-2">
{getLocalizedMetricName({ key: secondaryMetricKey })}
</LightLabel>
</div>
</>
)}

{isPullRequest(branchLike) && (
<div className="sw-body-sm sw-flex sw-items-center sw-mt-4">
<AfterMergeEstimate measures={measures} type={type} />

<LightLabel className="sw-ml-2">
{translate('component_measures.facet_category.overall_category.estimated')}
</LightLabel>
</div>
)}
</>
</MeasuresPanelCard>
);
}

+ 0
- 74
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelPercentMeasureLabel.tsx View File

@@ -1,74 +0,0 @@
/*
* 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 { DrilldownLink, LightLabel } from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { getLeakValue } from '../../../components/measure/utils';
import { translate } from '../../../helpers/l10n';
import { findMeasure, formatMeasure } from '../../../helpers/measures';
import { getComponentDrilldownUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like';
import { MetricType } from '../../../types/metrics';
import { Component, MeasureEnhanced } from '../../../types/types';
import { MeasurementType, getMeasurementLabelKeys, getMeasurementLinesMetricKey } from '../utils';

interface Props {
branchLike?: BranchLike;
component: Component;
useDiffMetric: boolean;
measures: MeasureEnhanced[];
type: MeasurementType;
}

export default function MeasuresPanelPercentMeasureLabel(props: Props) {
const { branchLike, component, measures, type, useDiffMetric = false } = props;
const { expandedLabelKey, labelKey } = getMeasurementLabelKeys(type, useDiffMetric);
const linesMetric = getMeasurementLinesMetricKey(type, useDiffMetric);
const measure = findMeasure(measures, linesMetric);

if (!measure) {
return <LightLabel>{translate(labelKey)}</LightLabel>;
}

const value = useDiffMetric ? getLeakValue(measure) : measure.value;

const url = getComponentDrilldownUrl({
componentKey: component.key,
metric: linesMetric,
branchLike,
listView: true,
});

return (
<LightLabel>
<FormattedMessage
defaultMessage={translate(expandedLabelKey)}
id={expandedLabelKey}
values={{
count: (
<DrilldownLink className="sw-body-md-highlight" to={url}>
{formatMeasure(value, MetricType.ShortInteger)}
</DrilldownLink>
),
}}
/>
</LightLabel>
);
}

+ 14
- 51
server/sonar-web/src/main/js/apps/overview/branches/NewCodeMeasuresPanel.tsx View File

@@ -34,11 +34,7 @@ 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 { getComponentIssuesUrl, getComponentSecurityHotspotsUrl } from '../../../helpers/urls';
import { Branch } from '../../../types/branch-like';
import { isApplication } from '../../../types/component';
import { IssueStatus } from '../../../types/issues';
@@ -47,14 +43,8 @@ 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,
MeasuresTabs,
Status,
getConditionRequiredLabel,
getMeasurementMetricKey,
} from '../utils';
import { MeasuresTabs, Status, getConditionRequiredLabel } from '../utils';
import MeasuresPanelPercentCards from './MeasuresPanelPercentCards';

interface Props {
branch?: Branch;
@@ -68,6 +58,7 @@ export default function NewCodeMeasuresPanel(props: Readonly<Props>) {
const intl = useIntl();
const isApp = isApplication(component.qualifier);

const conditions = qgStatuses?.flatMap((qg) => qg.conditions) ?? [];
const failedConditions = qgStatuses?.flatMap((qg) => qg.failedConditions) ?? [];

const newIssues = getLeakValue(findMeasure(measures, MetricKey.new_violations));
@@ -158,45 +149,16 @@ export default function NewCodeMeasuresPanel(props: Readonly<Props>) {
}
/>
</LightGreyCard>
<div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
<MeasuresCardPercent
branchLike={branch}
componentKey={component.key}
conditions={failedConditions}
measures={measures}
measurementType={MeasurementType.Coverage}
label="overview.quality_gate.coverage"
url={getComponentDrilldownUrl({
componentKey: component.key,
metric: getMeasurementMetricKey(MeasurementType.Coverage, true),
branchLike: branch,
listView: true,
})}
conditionMetric={MetricKey.new_coverage}
linesMetric={MetricKey.new_lines_to_cover}
useDiffMetric
showRequired={!isApp}
/>

<MeasuresCardPercent
branchLike={branch}
componentKey={component.key}
conditions={failedConditions}
measures={measures}
measurementType={MeasurementType.Duplication}
label="overview.quality_gate.duplications"
url={getComponentDrilldownUrl({
componentKey: component.key,
metric: getMeasurementMetricKey(MeasurementType.Duplication, true),
branchLike: branch,
listView: true,
})}
conditionMetric={MetricKey.new_duplicated_lines_density}
linesMetric={MetricKey.new_lines}
useDiffMetric
showRequired={!isApp}
/>
<MeasuresPanelPercentCards
useDiffMetric
branch={branch}
component={component}
measures={measures}
failedConditions={failedConditions}
/>

<div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
<MeasuresCardNumber
label={
newSecurityHotspots === '1'
@@ -207,7 +169,8 @@ export default function NewCodeMeasuresPanel(props: Readonly<Props>) {
...getBranchLikeQuery(branch),
})}
value={newSecurityHotspots}
conditions={failedConditions}
metric={MetricKey.new_security_hotspots}
conditions={conditions}
conditionMetric={MetricKey.new_security_hotspots_reviewed}
showRequired={!isApp}
/>

+ 146
- 0
server/sonar-web/src/main/js/apps/overview/branches/OverallCodeMeasuresPanel.tsx View File

@@ -0,0 +1,146 @@
/*
* 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 {
MetricsRatingBadge,
NoDataIcon,
SnoozeCircleIcon,
TextSubdued,
getTabPanelId,
} from 'design-system';
import * as React from 'react';
import { useIntl } from 'react-intl';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { findMeasure, formatMeasure, formatRating } from '../../../helpers/measures';
import { getComponentIssuesUrl, getComponentSecurityHotspotsUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like';
import { SoftwareQuality } from '../../../types/clean-code-taxonomy';
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 MeasuresCard from '../components/MeasuresCard';
import MeasuresCardNumber from '../components/MeasuresCardNumber';
import { MeasuresTabs } from '../utils';
import MeasuresPanelPercentCards from './MeasuresPanelPercentCards';
import SoftwareImpactMeasureCard from './SoftwareImpactMeasureCard';

export interface OverallCodeMeasuresPanelProps {
branch?: BranchLike;
component: Component;
measures: MeasureEnhanced[];
qgStatuses?: QualityGateStatus[];
}

export default function OverallCodeMeasuresPanel(props: Readonly<OverallCodeMeasuresPanelProps>) {
const { branch, qgStatuses, component, measures } = props;

const intl = useIntl();

const isApp = isApplication(component.qualifier);
const conditions = qgStatuses?.flatMap((qg) => qg.conditions) ?? [];
const failedConditions = qgStatuses?.flatMap((qg) => qg.failedConditions) ?? [];
const acceptedIssues = findMeasure(measures, MetricKey.accepted_issues)?.value;
const securityHotspots = findMeasure(measures, MetricKey.security_hotspots)?.value;
const securityRating = findMeasure(measures, MetricKey.security_rating)?.value;

return (
<div id={getTabPanelId(MeasuresTabs.Overall)} className="sw-mt-6">
<div className="sw-flex sw-gap-4">
<SoftwareImpactMeasureCard
component={component}
softwareQuality={SoftwareQuality.Security}
ratingMetricKey={MetricKey.security_rating}
measures={measures}
/>
<SoftwareImpactMeasureCard
component={component}
softwareQuality={SoftwareQuality.Reliability}
ratingMetricKey={MetricKey.reliability_rating}
measures={measures}
/>
<SoftwareImpactMeasureCard
component={component}
softwareQuality={SoftwareQuality.Maintainability}
ratingMetricKey={MetricKey.sqale_rating}
measures={measures}
/>
</div>

<div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
<MeasuresCard
url={getComponentIssuesUrl(component.key, {
...getBranchLikeQuery(branch),
issueStatuses: IssueStatus.Accepted,
})}
value={formatMeasure(acceptedIssues, MetricType.ShortInteger)}
metric={MetricKey.accepted_issues}
label="overview.accepted_issues"
failed={false}
icon={
<SnoozeCircleIcon
color={acceptedIssues === '0' ? 'overviewCardDefaultIcon' : 'overviewCardWarningIcon'}
/>
}
>
<TextSubdued className="sw-body-xs sw-mt-3">
{intl.formatMessage({
id: 'overview.accepted_issues.help_verbose',
})}
</TextSubdued>
</MeasuresCard>

<MeasuresCardNumber
label={
securityHotspots === '1'
? 'issue.type.SECURITY_HOTSPOT'
: 'issue.type.SECURITY_HOTSPOT.plural'
}
url={getComponentSecurityHotspotsUrl(component.key, {
...getBranchLikeQuery(branch),
})}
value={securityHotspots}
metric={MetricKey.security_hotspots}
conditions={conditions}
conditionMetric={MetricKey.security_hotspots_reviewed}
showRequired={!isApp}
icon={
securityRating ? (
<MetricsRatingBadge
label={securityRating}
rating={formatRating(securityRating)}
size="md"
/>
) : (
<NoDataIcon size="md" />
)
}
/>
</div>

<MeasuresPanelPercentCards
branch={branch}
component={component}
measures={measures}
failedConditions={failedConditions}
/>
</div>
);
}

+ 2
- 2
server/sonar-web/src/main/js/apps/overview/components/IssueMeasuresCardInner.tsx View File

@@ -19,7 +19,7 @@
*/
import styled from '@emotion/styled';
import classNames from 'classnames';
import { Badge, ContentLink, themeColor } from 'design-system';
import { Badge, ContentLink, NoDataIcon, themeColor } from 'design-system';
import * as React from 'react';
import { Path } from 'react-router-dom';
import Tooltip from '../../../components/controls/Tooltip';
@@ -82,7 +82,7 @@ export function IssueMeasuresCardInner(props: Readonly<IssueMeasuresCardInnerPro
</ContentLink>
</Tooltip>
</div>
{icon}
{value ? icon : <NoDataIcon size="md" />}
</div>
</div>
{footer}

+ 3
- 3
server/sonar-web/src/main/js/apps/overview/components/MeasuresCard.tsx View File

@@ -25,9 +25,9 @@ import { translate, translateWithParameters } from '../../../helpers/l10n';
import { localizeMetric } from '../../../helpers/measures';
import { MetricKey } from '../../../types/metrics';

interface Props {
export interface MeasuresCardProps {
url: To;
value: string;
value?: string;
metric: MetricKey;
label: string;
failed?: boolean;
@@ -35,7 +35,7 @@ interface Props {
}

export default function MeasuresCard(
props: React.PropsWithChildren<Props & React.HTMLAttributes<HTMLDivElement>>,
props: React.PropsWithChildren<MeasuresCardProps & React.HTMLAttributes<HTMLDivElement>>,
) {
const { failed, children, metric, icon, value, url, label, ...rest } = props;


+ 3
- 4
server/sonar-web/src/main/js/apps/overview/components/MeasuresCardNumber.tsx View File

@@ -25,13 +25,13 @@ import { formatMeasure } from '../../../helpers/measures';
import { MetricKey, MetricType } from '../../../types/metrics';
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
import { Status, getConditionRequiredLabel } from '../utils';
import MeasuresCard from './MeasuresCard';
import MeasuresCard, { MeasuresCardProps } from './MeasuresCard';

interface Props {
interface Props extends MeasuresCardProps {
conditions: QualityGateStatusConditionEnhanced[];
label: string;
url: To;
value: string;
value?: string;
conditionMetric: MetricKey;
showRequired?: boolean;
}
@@ -51,7 +51,6 @@ export default function MeasuresCardNumber(
<MeasuresCard
url={url}
value={formatMeasure(value, MetricType.ShortInteger)}
metric={conditionMetric}
label={label}
failed={conditionFailed}
{...rest}

+ 38
- 39
server/sonar-web/src/main/js/apps/overview/components/MeasuresCardPercent.tsx View File

@@ -80,8 +80,9 @@ export default function MeasuresCardPercent(
const value = useDiffMetric
? getLeakValue(findMeasure(measures, metricKey))
: findMeasure(measures, metricKey)?.value;

const linesValue = getLeakValue(findMeasure(measures, linesMetric));
const linesValue = useDiffMetric
? getLeakValue(findMeasure(measures, linesMetric))
: findMeasure(measures, linesMetric)?.value;
const linesLabel =
measurementType === MeasurementType.Coverage
? 'overview.quality_gate.on_x_new_lines_to_cover'
@@ -105,44 +106,42 @@ export default function MeasuresCardPercent(
failed={conditionFailed}
icon={renderIcon(measurementType, value)}
>
<>
<span className="sw-body-xs sw-mt-3">
{showRequired &&
condition &&
(conditionFailed ? (
<TextError
className="sw-font-regular sw-inline"
text={getConditionRequiredLabel(condition, intl, true)}
/>
) : (
<LightLabel>{getConditionRequiredLabel(condition, intl)}</LightLabel>
))}
</span>

<div className="sw-flex sw-body-sm sw-justify-between sw-items-center sw-mt-1">
<LightLabel className="sw-flex sw-items-center sw-gap-1 ">
<FormattedMessage
defaultMessage={translate(linesLabel)}
id={linesLabel}
values={{
link: (
<ContentLink
aria-label={translateWithParameters(
'overview.see_more_details_on_x_y',
linesValue ?? '0',
localizeMetric(linesMetric),
)}
className="sw-body-md-highlight sw-text-lg"
to={linesUrl}
>
{formatMeasure(linesValue ?? '0', MetricType.ShortInteger)}
</ContentLink>
),
}}
<span className="sw-body-xs sw-mt-3">
{showRequired &&
condition &&
(conditionFailed ? (
<TextError
className="sw-font-regular sw-inline"
text={getConditionRequiredLabel(condition, intl, true)}
/>
</LightLabel>
</div>
</>
) : (
<LightLabel>{getConditionRequiredLabel(condition, intl)}</LightLabel>
))}
</span>

<div className="sw-flex sw-body-sm sw-justify-between sw-items-center sw-mt-1">
<LightLabel className="sw-flex sw-items-center sw-gap-1 ">
<FormattedMessage
defaultMessage={translate(linesLabel)}
id={linesLabel}
values={{
link: (
<ContentLink
aria-label={translateWithParameters(
'overview.see_more_details_on_x_y',
linesValue ?? '0',
localizeMetric(linesMetric),
)}
className="sw-body-md-highlight sw-text-lg"
to={linesUrl}
>
{formatMeasure(linesValue ?? '0', MetricType.ShortInteger)}
</ContentLink>
),
}}
/>
</LightLabel>
</div>
</MeasuresCard>
);
}

+ 0
- 52
server/sonar-web/src/main/js/apps/overview/pullRequests/AfterMergeEstimate.tsx View File

@@ -1,52 +0,0 @@
/*
* 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 classNames from 'classnames';
import { LightPrimary } from 'design-system';
import * as React from 'react';
import { findMeasure, formatMeasure } from '../../../helpers/measures';
import { MetricType } from '../../../types/metrics';
import { MeasureEnhanced } from '../../../types/types';
import { MeasurementType, getMeasurementAfterMergeMetricKey } from '../utils';

export interface AfterMergeEstimateProps {
className?: string;
measures: MeasureEnhanced[];
type: MeasurementType;
}

export function AfterMergeEstimate({ className, measures, type }: AfterMergeEstimateProps) {
const afterMergeMetric = getMeasurementAfterMergeMetricKey(type);

const measure = findMeasure(measures, afterMergeMetric);

if (!measure || measure.value === undefined) {
return null;
}

return (
<div className={classNames(className, 'sw-flex sw-items-center')}>
<LightPrimary className="sw-body-sm-highlight">
{formatMeasure(measure.value, MetricType.Percent)}
</LightPrimary>
</div>
);
}

export default React.memo(AfterMergeEstimate);

+ 1
- 0
server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCardPanel.tsx View File

@@ -87,6 +87,7 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>)
...getBranchLikeQuery(pullRequest),
})}
value={newSecurityHotspots}
metric={MetricKey.new_security_hotspots}
conditions={conditions}
conditionMetric={MetricKey.new_security_hotspots_reviewed}
showRequired

+ 0
- 41
server/sonar-web/src/main/js/apps/overview/utils.tsx View File

@@ -20,8 +20,6 @@
import { memoize } from 'lodash';
import React from 'react';
import { IntlShape } from 'react-intl';
import CoverageRating from '../../components/ui/CoverageRating';
import DuplicationsRating from '../../components/ui/DuplicationsRating';
import { ISSUETYPE_METRIC_KEYS_MAP } from '../../helpers/issues';
import { translate } from '../../helpers/l10n';
import { formatMeasure } from '../../helpers/measures';
@@ -156,24 +154,10 @@ const MEASUREMENTS_MAP = {
[MeasurementType.Coverage]: {
metric: MetricKey.coverage,
newMetric: MetricKey.new_coverage,
linesMetric: MetricKey.lines_to_cover,
newLinesMetric: MetricKey.new_lines_to_cover,
afterMergeMetric: MetricKey.coverage,
labelKey: 'metric.coverage.name',
expandedLabelKey: 'overview.coverage_on_X_lines',
newLinesExpandedLabelKey: 'overview.coverage_on_X_new_lines',
iconClass: CoverageRating,
},
[MeasurementType.Duplication]: {
metric: MetricKey.duplicated_lines_density,
newMetric: MetricKey.new_duplicated_lines_density,
linesMetric: MetricKey.ncloc,
newLinesMetric: MetricKey.new_lines,
afterMergeMetric: MetricKey.duplicated_lines_density,
labelKey: 'metric.duplicated_lines_density.short_name',
expandedLabelKey: 'overview.duplications_on_X_lines',
newLinesExpandedLabelKey: 'overview.duplications_on_X_new_lines',
iconClass: DuplicationsRating,
},
};

@@ -223,10 +207,6 @@ export function getIssueRatingName(type: IssueType) {
return translate('metric_domain', ISSUETYPE_METRIC_KEYS_MAP[type].ratingName);
}

export function getIssueIconClass(type: IssueType) {
return ISSUETYPE_METRIC_KEYS_MAP[type].iconClass;
}

export function getIssueMetricKey(type: IssueType, useDiffMetric: boolean) {
return useDiffMetric
? ISSUETYPE_METRIC_KEYS_MAP[type].newMetric
@@ -239,31 +219,10 @@ export function getIssueRatingMetricKey(type: IssueType, useDiffMetric: boolean)
: ISSUETYPE_METRIC_KEYS_MAP[type].rating;
}

export function getMeasurementIconClass(type: MeasurementType) {
return MEASUREMENTS_MAP[type].iconClass;
}

export function getMeasurementMetricKey(type: MeasurementType, useDiffMetric: boolean) {
return useDiffMetric ? MEASUREMENTS_MAP[type].newMetric : MEASUREMENTS_MAP[type].metric;
}

export function getMeasurementAfterMergeMetricKey(type: MeasurementType) {
return MEASUREMENTS_MAP[type].afterMergeMetric;
}

export function getMeasurementLinesMetricKey(type: MeasurementType, useDiffMetric: boolean) {
return useDiffMetric ? MEASUREMENTS_MAP[type].newLinesMetric : MEASUREMENTS_MAP[type].linesMetric;
}

export function getMeasurementLabelKeys(type: MeasurementType, useDiffMetric: boolean) {
return {
expandedLabelKey: useDiffMetric
? MEASUREMENTS_MAP[type].newLinesExpandedLabelKey
: MEASUREMENTS_MAP[type].expandedLabelKey,
labelKey: MEASUREMENTS_MAP[type].labelKey,
};
}

export const parseQuery = memoize((urlQuery: RawQuery): { codeScope: string } => {
return {
codeScope: parseAsString(urlQuery['code_scope']),

+ 3
- 1
server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts View File

@@ -39,10 +39,12 @@ export function mockQualityGate(overrides: Partial<QualityGate> = {}): QualityGa
export function mockQualityGateStatus(
overrides: Partial<QualityGateStatus> = {},
): QualityGateStatus {
const condition = mockQualityGateStatusConditionEnhanced();
return {
ignoredConditions: false,
caycStatus: CaycStatus.Compliant,
failedConditions: [mockQualityGateStatusConditionEnhanced()],
conditions: [condition],
failedConditions: [condition],
key: 'foo',
name: 'Foo',
status: 'ERROR',

+ 1
- 0
server/sonar-web/src/main/js/types/quality-gates.ts View File

@@ -64,6 +64,7 @@ export interface QualityGateApplicationStatusChildProject {
}

export interface QualityGateStatus {
conditions: QualityGateStatusConditionEnhanced[];
failedConditions: QualityGateStatusConditionEnhanced[];
ignoredConditions?: boolean;
caycStatus: CaycStatus;

+ 2
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -977,10 +977,10 @@ issue.type.CODE_SMELL=Code Smell
issue.type.BUG=Bug
issue.type.VULNERABILITY=Vulnerability
issue.type.SECURITY_HOTSPOT=Security Hotspot
issue.type.SECURITY_HOTSPOT.plural=Security Hotspots
issue.type.CODE_SMELL.plural=Code Smells
issue.type.BUG.plural=Bugs
issue.type.VULNERABILITY.plural=Vulnerabilities
issue.type.SECURITY_HOTSPOT.plural=Security Hotspots

issue.type.deprecation.title=Issue types are deprecated and can no longer be modified.
issue.type.deprecation.filter_by=You can now filter issues by:
@@ -3861,6 +3861,7 @@ overview.pull_request.fixed_issues.disclaimer=Only issues fixed on the files mod
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.accepted_issues=Accepted issues
overview.accepted_issues.help=Valid issues that were not fixed
overview.accepted_issues.help_verbose=Valid issues, but not fixed. They represent accepted technical debt.
overview.quality_gate.status=Quality Gate Status
overview.quality_gate=Quality Gate
overview.quality_gate_x=Quality Gate: {0}

Loading…
Cancel
Save