Browse Source

SONAR-21215 Update PR Overview UI

tags/10.4.0.87286
Jay 6 months ago
parent
commit
99f771c795

+ 15
- 6
server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx View File

@@ -18,7 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { ChevronRightIcon, DangerButtonSecondary } from 'design-system';
import styled from '@emotion/styled';
import { Badge, ButtonSecondary, themeColor } from 'design-system';
import React from 'react';
import { useIntl } from 'react-intl';
import {
@@ -27,7 +28,7 @@ import {
propsToIssueParams,
} from '../../../components/shared/utils';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { getLocalizedMetricName } from '../../../helpers/l10n';
import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
import { formatMeasure, getShortType, isDiffMetric } from '../../../helpers/measures';
import {
getComponentDrilldownUrl,
@@ -78,10 +79,14 @@ function FailedQGCondition(
const url = getQGConditionUrl(component.key, condition, branchLike);

return (
<DangerButtonSecondary className="sw-px-2 sw-py-1 sw-rounded-1/2 sw-body-sm" to={url}>
<FailedMetric condition={condition} />
<ChevronRightIcon className="sw-ml-1" />
</DangerButtonSecondary>
<ButtonSecondary className="sw-px-2 sw-py-1 sw-rounded-1/2 sw-body-sm" to={url}>
<Badge className="sw-mr-2" variant="deleted">
{translate('overview.measures.failed_badge')}
</Badge>
<SpanDanger>
<FailedMetric condition={condition} />
</SpanDanger>
</ButtonSecondary>
);
}

@@ -209,3 +214,7 @@ function getQGConditionUrl(
listView: true,
});
}

const SpanDanger = styled.span`
color: ${themeColor('danger')};
`;

+ 4
- 4
server/sonar-web/src/main/js/apps/overview/components/__tests__/BranchQualityGate-it.tsx View File

@@ -34,7 +34,7 @@ it('renders failed QG', () => {

// Maintainability rating condition
const maintainabilityRatingLink = byRole('link', {
name: 'overview.failed_condition.x_rating_requiredmetric_domain.Maintainability metric.type.ratingE A',
name: 'overview.measures.failed_badge overview.failed_condition.x_rating_requiredmetric_domain.Maintainability metric.type.ratingE A',
}).get();
expect(maintainabilityRatingLink).toBeInTheDocument();
expect(maintainabilityRatingLink).toHaveAttribute(
@@ -44,7 +44,7 @@ it('renders failed QG', () => {

// Security Hotspots rating condition
const securityHotspotsRatingLink = byRole('link', {
name: 'overview.failed_condition.x_rating_requiredmetric_domain.Security Review metric.type.ratingE A',
name: 'overview.measures.failed_badge overview.failed_condition.x_rating_requiredmetric_domain.Security Review metric.type.ratingE A',
}).get();
expect(securityHotspotsRatingLink).toBeInTheDocument();
expect(securityHotspotsRatingLink).toHaveAttribute(
@@ -54,7 +54,7 @@ it('renders failed QG', () => {

// New code smells
const codeSmellsLink = byRole('link', {
name: 'overview.failed_condition.x_required 5 Code Smells ≤ 1',
name: 'overview.measures.failed_badge overview.failed_condition.x_required 5 Code Smells ≤ 1',
}).get();
expect(codeSmellsLink).toBeInTheDocument();
expect(codeSmellsLink).toHaveAttribute(
@@ -64,7 +64,7 @@ it('renders failed QG', () => {

// Conditions to cover
const conditionToCoverLink = byRole('link', {
name: 'overview.failed_condition.x_required 5 Conditions to cover ≥ 10',
name: 'overview.measures.failed_badge overview.failed_condition.x_required 5 Conditions to cover ≥ 10',
}).get();
expect(conditionToCoverLink).toBeInTheDocument();
expect(conditionToCoverLink).toHaveAttribute(

server/sonar-web/src/main/js/apps/overview/branches/MeasuresCard.tsx → server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCard.tsx View File

@@ -18,8 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import styled from '@emotion/styled';
import classNames from 'classnames';
import { Card, ContentLink, PageContentFontWrapper, themeColor } from 'design-system';
import { Badge, Card, ContentLink, themeColor } from 'design-system';
import * as React from 'react';
import { To } from 'react-router-dom';
import { translate, translateWithParameters } from '../../../helpers/l10n';
@@ -41,16 +40,11 @@ export default function MeasuresCard(
const { failed, children, metric, icon, value, url, label, ...rest } = props;

return (
<StyledCard
className={classNames(
'sw-h-fit sw-p-8 sw-rounded-2 sw-flex sw-justify-between sw-items-center sw-text-base',
{
failed,
},
)}
<Card
className="sw-h-fit sw-p-8 sw-rounded-2 sw-flex sw-justify-between sw-items-center sw-text-base"
{...rest}
>
<PageContentFontWrapper className="sw-flex sw-flex-col sw-gap-1 sw-justify-between">
<div className="sw-flex sw-flex-col sw-gap-1 sw-justify-between">
<div className="sw-flex sw-items-center sw-gap-2 sw-font-semibold">
{value ? (
<ContentLink
@@ -68,21 +62,20 @@ export default function MeasuresCard(
<StyledNoValue> — </StyledNoValue>
)}
{translate(label)}
{failed && (
<Badge className="sw-mt-1/2" variant="deleted">
{translate('overview.measures.failed_badge')}
</Badge>
)}
</div>
{children && <div className="sw-flex sw-flex-col">{children}</div>}
</PageContentFontWrapper>
</div>

{icon && <div>{icon}</div>}
</StyledCard>
</Card>
);
}

const StyledNoValue = styled.span`
color: ${themeColor('pageTitle')};
`;

export const StyledCard = styled(Card)`
&.failed {
border-color: ${themeColor('qgCardFailed')};
}
`;

server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardNumber.tsx → server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCardNumber.tsx View File

@@ -17,53 +17,65 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { TextError } from 'design-system';
import { LightLabel, TextError } from 'design-system';
import * as React from 'react';
import { useIntl } from 'react-intl';
import { To } from 'react-router-dom';
import { formatMeasure } from '../../../helpers/measures';
import { MetricKey, MetricType } from '../../../types/metrics';
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
import { Status } from '../utils';
import MeasuresCard from './MeasuresCard';

interface Props {
failedConditions: QualityGateStatusConditionEnhanced[];
conditions: QualityGateStatusConditionEnhanced[];
label: string;
url: To;
value: string;
failingConditionMetric: MetricKey;
requireLabel: string;
conditionMetric: MetricKey;
guidingKeyOnError?: string;
}

export default function MeasuresCardNumber(
props: React.PropsWithChildren<Props & React.HTMLAttributes<HTMLDivElement>>,
) {
const {
label,
value,
failedConditions,
url,
failingConditionMetric,
requireLabel,
guidingKeyOnError,
...rest
} = props;
const { label, value, conditions, url, conditionMetric, guidingKeyOnError, ...rest } = props;

const failed = Boolean(
failedConditions.find((condition) => condition.metric === failingConditionMetric),
);
const intl = useIntl();

const condition = conditions.find((condition) => condition.metric === conditionMetric);

const conditionFailed = condition?.level === Status.ERROR;

const requireLabel =
condition &&
intl.formatMessage(
{ id: 'overview.quality_gate.required_x' },
{
operator: condition.op === 'GT' ? '≤' : '≥',
value: formatMeasure(condition.error, MetricType.Percent, {
decimals: 2,
omitExtraDecimalZeros: true,
}),
},
);

return (
<MeasuresCard
url={url}
value={formatMeasure(value, MetricType.ShortInteger)}
metric={failingConditionMetric}
metric={conditionMetric}
label={label}
failed={failed}
data-guiding-id={failed ? guidingKeyOnError : undefined}
failed={conditionFailed}
data-guiding-id={conditionFailed ? guidingKeyOnError : undefined}
{...rest}
>
{failed && <TextError className="sw-font-regular sw-mt-2" text={requireLabel} />}
{requireLabel &&
(conditionFailed ? (
<TextError className="sw-mt-2 sw-font-regular" text={requireLabel} />
) : (
<LightLabel className="sw-mt-2">{requireLabel}</LightLabel>
))}
</MeasuresCard>
);
}

server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPanel.tsx → server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCardPanel.tsx View File

@@ -19,7 +19,6 @@
*/
import classNames from 'classnames';
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';
@@ -42,13 +41,11 @@ interface Props {
branchLike?: BranchLike;
component: Component;
measures: MeasureEnhanced[];
failedConditions: QualityGateStatusConditionEnhanced[];
conditions: QualityGateStatusConditionEnhanced[];
}

export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>) {
const { branchLike, component, measures, failedConditions, className } = props;

const intl = useIntl();
const { branchLike, component, measures, conditions, className } = props;

const newViolations = getLeakValue(findMeasure(measures, MetricKey.new_violations)) as string;
const newSecurityHotspots = getLeakValue(
@@ -66,14 +63,8 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>)
...DEFAULT_ISSUES_QUERY,
})}
value={newViolations}
failedConditions={failedConditions}
failingConditionMetric={MetricKey.new_violations}
requireLabel={intl.formatMessage(
{ id: 'overview.quality_gate.require_fixing' },
{
count: newViolations,
},
)}
conditions={conditions}
conditionMetric={MetricKey.new_violations}
guidingKeyOnError="overviewZeroNewIssuesSimplification"
/>

@@ -88,8 +79,8 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>)
branchLike,
listView: true,
})}
failedConditions={failedConditions}
failingConditionMetric={MetricKey.new_coverage}
conditions={conditions}
conditionMetric={MetricKey.new_coverage}
newLinesMetric={MetricKey.new_lines_to_cover}
afterMergeMetric={MetricKey.coverage}
measures={measures}
@@ -107,14 +98,8 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>)
...getBranchLikeQuery(branchLike),
})}
value={newSecurityHotspots}
failedConditions={failedConditions}
failingConditionMetric={MetricKey.new_security_hotspots_reviewed}
requireLabel={intl.formatMessage(
{ id: 'overview.quality_gate.require_reviewing' },
{
count: newSecurityHotspots,
},
)}
conditions={conditions}
conditionMetric={MetricKey.new_security_hotspots_reviewed}
/>

<MeasuresCardPercent
@@ -128,8 +113,8 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>)
branchLike,
listView: true,
})}
failedConditions={failedConditions}
failingConditionMetric={MetricKey.new_duplicated_lines_density}
conditions={conditions}
conditionMetric={MetricKey.new_duplicated_lines_density}
newLinesMetric={MetricKey.new_lines}
afterMergeMetric={MetricKey.duplicated_lines_density}
measures={measures}

server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPercent.tsx → server/sonar-web/src/main/js/apps/overview/pullRequests/MeasuresCardPercent.tsx View File

@@ -35,7 +35,7 @@ import { BranchLike } from '../../../types/branch-like';
import { MetricKey, MetricType } from '../../../types/metrics';
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
import { MeasureEnhanced } from '../../../types/types';
import { MeasurementType, getMeasurementMetricKey } from '../utils';
import { MeasurementType, Status, getMeasurementMetricKey } from '../utils';
import MeasuresCard from './MeasuresCard';

interface Props {
@@ -45,8 +45,8 @@ interface Props {
label: string;
url: To;
measures: MeasureEnhanced[];
failedConditions: QualityGateStatusConditionEnhanced[];
failingConditionMetric: MetricKey;
conditions: QualityGateStatusConditionEnhanced[];
conditionMetric: MetricKey;
newLinesMetric: MetricKey;
afterMergeMetric: MetricKey;
}
@@ -61,8 +61,8 @@ export default function MeasuresCardPercent(
label,
url,
measures,
failedConditions,
failingConditionMetric,
conditions,
conditionMetric,
newLinesMetric,
afterMergeMetric,
} = props;
@@ -87,27 +87,21 @@ export default function MeasuresCardPercent(

const afterMergeValue = findMeasure(measures, afterMergeMetric)?.value;

const failedCondition = failedConditions.find(
(condition) => condition.metric === failingConditionMetric,
);
const condition = conditions.find((c) => c.metric === conditionMetric);
const conditionFailed = condition?.level === Status.ERROR;

let errorRequireLabel = '';
if (failedCondition) {
errorRequireLabel = intl.formatMessage(
const requireLabel =
condition &&
intl.formatMessage(
{ id: 'overview.quality_gate.required_x' },
{
operator: failedCondition.op === 'GT' ? '≤' : '≥',
value: formatMeasure(
failedCondition.level === 'ERROR' ? failedCondition.error : failedCondition.warning,
MetricType.Percent,
{
decimals: 2,
omitExtraDecimalZeros: true,
},
),
operator: condition.op === 'GT' ? '≤' : '≥',
value: formatMeasure(condition.error, MetricType.Percent, {
decimals: 2,
omitExtraDecimalZeros: true,
}),
},
);
}

return (
<MeasuresCard
@@ -115,11 +109,18 @@ export default function MeasuresCardPercent(
metric={metricKey}
url={url}
label={label}
failed={Boolean(failedCondition)}
failed={conditionFailed}
icon={renderIcon(measurementType, value)}
>
<div className="sw-flex sw-flex-col">
<LightLabel className="sw-flex sw-items-center sw-gap-1">
<>
{requireLabel &&
(conditionFailed ? (
<TextError className="sw-mt-2 sw-font-regular" text={requireLabel} />
) : (
<LightLabel className="sw-mt-2">{requireLabel}</LightLabel>
))}

<LightLabel className="sw-flex sw-items-center sw-gap-1 sw-mt-4">
<FormattedMessage
defaultMessage={translate(newLinesLabel)}
id={newLinesLabel}
@@ -152,11 +153,7 @@ export default function MeasuresCardPercent(
/>
</LightLabel>
)}

{failedCondition && (
<TextError className="sw-mt-2 sw-font-regular" text={errorRequireLabel} />
)}
</div>
</>
</MeasuresCard>
);
}

+ 12
- 12
server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx View File

@@ -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 { BasicSeparator, CenteredLayout, Spinner } from 'design-system';
import { BasicSeparator, CenteredLayout, PageContentFontWrapper, Spinner } from 'design-system';
import { uniq } from 'lodash';
import * as React from 'react';
import { useEffect, useState } from 'react';
@@ -29,13 +29,13 @@ import { isDefined } from '../../../helpers/types';
import { useBranchStatusQuery } from '../../../queries/branch';
import { PullRequest } from '../../../types/branch-like';
import { Component, MeasureEnhanced, QualityGate } from '../../../types/types';
import MeasuresCardPanel from '../branches/MeasuresCardPanel';
import BranchQualityGate from '../components/BranchQualityGate';
import IgnoredConditionWarning from '../components/IgnoredConditionWarning';
import MetaTopBar from '../components/MetaTopBar';
import ZeroNewIssuesSimplificationGuide from '../components/ZeroNewIssuesSimplificationGuide';
import '../styles.css';
import { PR_METRICS, Status } from '../utils';
import MeasuresCardPanel from './MeasuresCardPanel';
import SonarLintAd from './SonarLintAd';

interface Props {
@@ -60,11 +60,8 @@ export default function PullRequestOverview(props: Props) {

const metricKeys =
conditions !== undefined
? // Also load metrics that apply to failing QG conditions.
uniq([
...PR_METRICS,
...conditions.filter((c) => c.level !== Status.OK).map((c) => c.metric),
])
? // Also load metrics that apply to QG conditions.
uniq([...PR_METRICS, ...conditions.map((c) => c.metric)])
: PR_METRICS;

getMeasuresWithMetrics(component.key, metricKeys, getBranchLikeQuery(branchLike)).then(
@@ -108,14 +105,17 @@ export default function PullRequestOverview(props: Props) {
return null;
}

const failedConditions = conditions
.filter((condition) => condition.level === Status.ERROR)
const enhancedConditions = conditions
.map((c) => enhanceConditionWithMeasure(c, measures))
.filter(isDefined);

const failedConditions = enhancedConditions.filter(
(condition) => condition.level === Status.ERROR,
);

return (
<CenteredLayout>
<div className="it__pr-overview sw-mt-12 sw-mb-8 sw-grid sw-grid-cols-12">
<PageContentFontWrapper className="it__pr-overview sw-mt-12 sw-mb-8 sw-grid sw-grid-cols-12 sw-body-sm">
<div className="sw-col-start-2 sw-col-span-10">
<MetaTopBar branchLike={branchLike} measures={measures} />
<BasicSeparator className="sw-my-4" />
@@ -135,7 +135,7 @@ export default function PullRequestOverview(props: Props) {
className="sw-flex-1"
branchLike={branchLike}
component={component}
failedConditions={failedConditions}
conditions={enhancedConditions}
measures={measures}
/>

@@ -143,7 +143,7 @@ export default function PullRequestOverview(props: Props) {

<SonarLintAd status={status} />
</div>
</div>
</PageContentFontWrapper>
</CenteredLayout>
);
}

+ 2
- 2
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx View File

@@ -180,12 +180,12 @@ it('should render correctly for a failed QG', async () => {

expect(
byRole('link', {
name: 'overview.failed_condition.x_required 10.0% duplicated_lines ≤ 1.0%',
name: 'overview.measures.failed_badge overview.failed_condition.x_required 10.0% duplicated_lines ≤ 1.0%',
}).get(),
).toBeInTheDocument();
expect(
byRole('link', {
name: 'overview.failed_condition.x_required 10 new_bugs ≤ 3',
name: 'overview.measures.failed_badge overview.failed_condition.x_required 10 new_bugs ≤ 3',
}).get(),
).toBeInTheDocument();
});

+ 5
- 3
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -3802,8 +3802,8 @@ system.version_is_availble={version} is available
#------------------------------------------------------------------------------
overview.1_condition_failed=1 failed condition
overview.X_conditions_failed={0} failed conditions
overview.failed_condition.x_rating_required={rating} is {value} required {threshold}
overview.failed_condition.x_required={metric} required {threshold}
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.quality_gate.status=Quality Gate Status
overview.quality_gate=Quality Gate
@@ -3835,7 +3835,7 @@ overview.quality_gate.on_x_new_lines=On {link} New Lines
overview.quality_gate.x_estimated_after_merge={value} Estimated after merge
overview.quality_gate.require_fixing={count, plural, one {requires} other {require}} fixing
overview.quality_gate.require_reviewing={count, plural, one {requires} other {require}} reviewing
overview.quality_gate.required_x=required {operator} {value}
overview.quality_gate.required_x=Required {operator} {value}
overview.quality_profiles=Quality Profiles used
overview.new_code_period_x=New Code: {0}
overview.max_new_code_period_from_x=Max New Code from: {0}
@@ -3920,6 +3920,8 @@ overview.gate.view.errors=The view failed the quality gate on the following cond
overview.measurement_type.DUPLICATION=Duplications
overview.measurement_type.COVERAGE=Coverage

overview.measures.failed_badge=Failed

overview.complexity_tooltip.function={0} functions have complexity around {1}
overview.complexity_tooltip.file={0} files have complexity around {1}


Loading…
Cancel
Save