diff options
12 files changed, 539 insertions, 116 deletions
diff --git a/server/sonar-web/__mocks__/react-intl.tsx b/server/sonar-web/__mocks__/react-intl.tsx index aa20e55fbda..03edab2f5fd 100644 --- a/server/sonar-web/__mocks__/react-intl.tsx +++ b/server/sonar-web/__mocks__/react-intl.tsx @@ -17,12 +17,25 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { isObject, some } from 'lodash'; import * as React from 'react'; module.exports = { ...jest.requireActual('react-intl'), useIntl: () => ({ - formatMessage: ({ id }, values = {}) => [id, ...Object.values(values)].join('.'), + formatMessage: ({ id }, values = {}) => { + if (some(values, isObject)) { + return ( + <> + {id} + {Object.entries(values).map(([key, value]) => ( + <React.Fragment key={key}>{value}</React.Fragment> + ))} + </> + ); + } + return [id, ...Object.values(values)].join('.'); + }, }), FormattedMessage: ({ id, values }: { id: string; values?: { [x: string]: React.ReactNode } }) => { return ( diff --git a/server/sonar-web/design-system/src/components/__tests__/layouts-test.tsx b/server/sonar-web/design-system/src/components/__tests__/layouts-test.tsx index 02e32e26edf..41ed6a7eecc 100644 --- a/server/sonar-web/design-system/src/components/__tests__/layouts-test.tsx +++ b/server/sonar-web/design-system/src/components/__tests__/layouts-test.tsx @@ -26,7 +26,7 @@ describe('CenteredLayout', () => { expect(screen.getByText('content')).toHaveStyle({ 'min-width': '1280px', - 'max-width': '1400px', + 'max-width': '1280px', }); }); }); diff --git a/server/sonar-web/design-system/src/helpers/constants.ts b/server/sonar-web/design-system/src/helpers/constants.ts index 368b81e9986..ce1c4080f0e 100644 --- a/server/sonar-web/design-system/src/helpers/constants.ts +++ b/server/sonar-web/design-system/src/helpers/constants.ts @@ -50,7 +50,7 @@ export const INPUT_SIZES = { }; export const LAYOUT_VIEWPORT_MIN_WIDTH = 1280; -export const LAYOUT_VIEWPORT_MAX_WIDTH = 1400; +export const LAYOUT_VIEWPORT_MAX_WIDTH = 1280; export const LAYOUT_VIEWPORT_MAX_WIDTH_LARGE = 1680; export const LAYOUT_MAIN_CONTENT_GUTTER = 60; export const LAYOUT_SIDEBAR_WIDTH = 240; diff --git a/server/sonar-web/src/main/js/apps/overview/components/BranchQualityGate.tsx b/server/sonar-web/src/main/js/apps/overview/components/BranchQualityGate.tsx new file mode 100644 index 00000000000..1b39965ed65 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/BranchQualityGate.tsx @@ -0,0 +1,87 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { HelperHintIcon, LightPrimary, QualityGateIndicator, TextMuted } from 'design-system'; +import React from 'react'; +import { useIntl } from 'react-intl'; +import HelpTooltip from '../../../components/controls/HelpTooltip'; +import { BranchLike } from '../../../types/branch-like'; +import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; +import { Component, Status } from '../../../types/types'; +import BranchQualityGateConditions from './BranchQualityGateConditions'; + +interface Props { + status: Status; + branchLike?: BranchLike; + component: Pick<Component, 'key'>; + failedConditions: QualityGateStatusConditionEnhanced[]; +} + +export default function BranchQualityGate(props: Readonly<Props>) { + const { status, branchLike, component, failedConditions } = props; + + return ( + <> + <BranchQGStatus status={status} /> + <BranchQualityGateConditions + branchLike={branchLike} + component={component} + failedConditions={failedConditions} + /> + </> + ); +} + +function BranchQGStatus({ status }: Readonly<Pick<Props, 'status'>>) { + const intl = useIntl(); + + return ( + <div className="sw-flex sw-items-center sw-mb-5"> + <QualityGateIndicator + status={status} + className="sw-mr-2" + size="xl" + ariaLabel={intl.formatMessage( + { id: 'overview.quality_gate_x' }, + { '0': intl.formatMessage({ id: `overview.gate.${status}` }) }, + )} + /> + <div className="sw-flex sw-flex-col sw-justify-around"> + <div className="sw-flex sw-items-center"> + <TextMuted + className="sw-body-sm" + text={intl.formatMessage({ id: 'overview.quality_gate' })} + /> + <HelpTooltip + className="sw-ml-2" + overlay={intl.formatMessage({ id: 'overview.quality_gate.help' })} + > + <HelperHintIcon aria-label="help-tooltip" /> + </HelpTooltip> + </div> + <div> + <LightPrimary as="h1" className="sw-heading-xl"> + {intl.formatMessage({ id: `metric.level.${status}` })} + </LightPrimary> + </div> + </div> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx b/server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx new file mode 100644 index 00000000000..0f5a23cdf86 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx @@ -0,0 +1,197 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { ChevronRightIcon, DangerButtonSecondary } from 'design-system'; +import React from 'react'; +import { useIntl } from 'react-intl'; +import { getBranchLikeQuery } from '../../../helpers/branch-like'; +import { getLocalizedMetricName } from '../../../helpers/l10n'; +import { formatMeasure, getShortType, isDiffMetric } from '../../../helpers/measures'; +import { + getComponentDrilldownUrl, + getComponentIssuesUrl, + getComponentSecurityHotspotsUrl, +} from '../../../helpers/urls'; +import { BranchLike } from '../../../types/branch-like'; +import { IssueType } from '../../../types/issues'; +import { MetricKey, MetricType } from '../../../types/metrics'; +import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; +import { Component } from '../../../types/types'; +import { + METRICS_REPORTED_IN_OVERVIEW_CARDS, + RATING_METRICS_MAPPING, + RATING_TO_SEVERITIES_MAPPING, +} from '../utils'; + +interface Props { + branchLike?: BranchLike; + component: Pick<Component, 'key'>; + failedConditions: QualityGateStatusConditionEnhanced[]; +} + +export default function BranchQualityGateConditions(props: Readonly<Props>) { + const { branchLike, component, failedConditions } = props; + + const filteredFailedConditions = failedConditions.filter( + (condition) => !METRICS_REPORTED_IN_OVERVIEW_CARDS.includes(condition.metric as MetricKey), + ); + + return ( + <ul className="sw-flex sw-items-center sw-gap-2 sw-flex-wrap"> + {filteredFailedConditions.map((condition) => ( + <li key={condition.metric}> + <FailedQGCondition branchLike={branchLike} component={component} condition={condition} /> + </li> + ))} + </ul> + ); +} + +function FailedQGCondition( + props: Readonly< + Pick<Props, 'branchLike' | 'component'> & { condition: QualityGateStatusConditionEnhanced } + >, +) { + const { branchLike, component, condition } = props; + 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> + ); +} + +interface FailedMetricProps { + condition: QualityGateStatusConditionEnhanced; +} + +export function FailedMetric(props: Readonly<FailedMetricProps>) { + const { + condition: { + measure: { metric }, + }, + } = props; + + if (metric.type === MetricType.Rating) { + return <FailedRatingMetric {...props} />; + } + + return <FailedGeneralMetric {...props} />; +} + +function FailedRatingMetric({ condition }: Readonly<FailedMetricProps>) { + const { + error, + measure: { + metric: { type, domain }, + }, + } = condition; + const intl = useIntl(); + + return ( + <> + {intl.formatMessage( + { id: 'overview.failed_condition.x_required' }, + { + metric: `${intl.formatMessage({ + id: `metric_domain.${domain}`, + })} ${intl.formatMessage({ id: 'metric.type.RATING' }).toLowerCase()}`, + threshold: ( + <strong className="sw-body-sm-highlight sw-ml-1">{formatMeasure(error, type)}</strong> + ), + }, + )} + </> + ); +} + +function FailedGeneralMetric({ condition }: Readonly<FailedMetricProps>) { + const { + error, + measure: { metric }, + } = condition; + const intl = useIntl(); + const measureFormattingOptions = { decimals: 2, omitExtraDecimalZeros: true }; + + return ( + <> + {intl.formatMessage( + { id: 'overview.failed_condition.x_required' }, + { + metric: ( + <> + <strong className="sw-body-sm-highlight sw-mr-1"> + {formatMeasure( + condition.actual, + getShortType(metric.type), + measureFormattingOptions, + )} + </strong> + {getLocalizedMetricName(metric, true)} + </> + ), + threshold: ( + <strong className="sw-body-sm-highlight sw-ml-1"> + {condition.op === 'GT' ? <>≤</> : <>≥</>}{' '} + {formatMeasure(error, getShortType(metric.type), measureFormattingOptions)} + </strong> + ), + }, + )} + </> + ); +} + +function getQGConditionUrl( + componentKey: string, + condition: QualityGateStatusConditionEnhanced, + branchLike?: BranchLike, +) { + const { metric } = condition; + const sinceLeakPeriod = isDiffMetric(metric); + const issueType = RATING_METRICS_MAPPING[metric]; + + if (issueType) { + if (issueType === IssueType.SecurityHotspot) { + return getComponentSecurityHotspotsUrl(componentKey, { + ...getBranchLikeQuery(branchLike), + ...(sinceLeakPeriod ? { sinceLeakPeriod: 'true' } : {}), + }); + } + return getComponentIssuesUrl(componentKey, { + resolved: 'false', + types: issueType, + ...getBranchLikeQuery(branchLike), + ...(sinceLeakPeriod ? { sinceLeakPeriod: 'true' } : {}), + ...(issueType !== IssueType.CodeSmell + ? { severities: RATING_TO_SEVERITIES_MAPPING[Number(condition.error) - 1] } + : {}), + }); + } + + return getComponentDrilldownUrl({ + componentKey, + metric, + branchLike, + listView: true, + }); +} diff --git a/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx b/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx index 1bb11ca1114..2104460da8a 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx @@ -36,6 +36,7 @@ import { IssueType } from '../../../types/issues'; import { MetricKey, MetricType } from '../../../types/metrics'; import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; import { Component, Dict } from '../../../types/types'; +import { RATING_TO_SEVERITIES_MAPPING } from '../utils'; interface Props { branchLike?: BranchLike; @@ -71,13 +72,6 @@ export default class QualityGateCondition extends React.PureComponent<Props> { } getUrlForBugsOrVulnerabilities(type: string, inNewCodePeriod: boolean) { - const RATING_TO_SEVERITIES_MAPPING = [ - 'BLOCKER,CRITICAL,MAJOR,MINOR', - 'BLOCKER,CRITICAL,MAJOR', - 'BLOCKER,CRITICAL', - 'BLOCKER', - ]; - const { condition } = this.props; const threshold = condition.level === 'ERROR' ? condition.error : condition.warning; diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/BranchQualityGate-it.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/BranchQualityGate-it.tsx new file mode 100644 index 00000000000..714a6d7d76e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/BranchQualityGate-it.tsx @@ -0,0 +1,136 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { mockPullRequest } from '../../../../helpers/mocks/branch-like'; +import { mockComponent } from '../../../../helpers/mocks/component'; +import { mockQualityGateStatusConditionEnhanced } from '../../../../helpers/mocks/quality-gates'; +import { mockMeasureEnhanced, mockMetric } from '../../../../helpers/testMocks'; +import { renderComponent } from '../../../../helpers/testReactTestingUtils'; +import { byLabelText, byRole } from '../../../../helpers/testSelector'; +import { MetricKey, MetricType } from '../../../../types/metrics'; +import { FCProps } from '../../../../types/misc'; +import { Status } from '../../utils'; +import BranchQualityGate from '../BranchQualityGate'; + +it('renders failed QG', () => { + renderBranchQualityGate(); + + // Maintainability rating condition + expect( + byRole('link', { + name: 'overview.failed_condition.x_requiredmetric_domain.Maintainability metric.type.rating A', + }).get(), + ).toBeInTheDocument(); + + // Security Hotspots rating condition + expect( + byRole('link', { + name: 'overview.failed_condition.x_requiredmetric_domain.Security Review metric.type.rating A', + }).get(), + ).toBeInTheDocument(); + + // New code smells + expect( + byRole('link', { + name: 'overview.failed_condition.x_required 5 Code Smells ≤ 1', + }).get(), + ).toBeInTheDocument(); + + // Conditions to cover + expect( + byRole('link', { + name: 'overview.failed_condition.x_required 5 Conditions to cover ≥ 10', + }).get(), + ).toBeInTheDocument(); + + expect(byLabelText('overview.quality_gate_x.overview.gate.ERROR').get()).toBeInTheDocument(); +}); + +it('renders passed QG', () => { + renderBranchQualityGate({ failedConditions: [], status: Status.OK }); + + expect(byLabelText('overview.quality_gate_x.overview.gate.OK').get()).toBeInTheDocument(); + expect(byRole('link').query()).not.toBeInTheDocument(); +}); + +function renderBranchQualityGate(props: Partial<FCProps<typeof BranchQualityGate>> = {}) { + return renderComponent( + <BranchQualityGate + status={Status.ERROR} + branchLike={mockPullRequest()} + component={mockComponent()} + failedConditions={[ + mockQualityGateStatusConditionEnhanced({ + actual: '5.0', + error: '1.0', + metric: MetricKey.new_maintainability_rating, + measure: mockMeasureEnhanced({ + metric: mockMetric({ + domain: 'Maintainability', + key: MetricKey.new_maintainability_rating, + name: 'Maintainability rating', + type: MetricType.Rating, + }), + }), + }), + mockQualityGateStatusConditionEnhanced({ + actual: '5.0', + error: '1.0', + metric: MetricKey.new_security_review_rating, + measure: mockMeasureEnhanced({ + metric: mockMetric({ + domain: 'Security Review', + key: MetricKey.new_security_review_rating, + name: 'Security Review Rating', + type: MetricType.Rating, + }), + }), + }), + mockQualityGateStatusConditionEnhanced({ + actual: '5', + error: '1', + metric: MetricKey.new_code_smells, + measure: mockMeasureEnhanced({ + metric: mockMetric({ + domain: 'Maintainability', + key: MetricKey.new_code_smells, + name: 'Code Smells', + type: MetricType.ShortInteger, + }), + }), + }), + mockQualityGateStatusConditionEnhanced({ + actual: '5', + error: '10', + op: 'up', + metric: MetricKey.conditions_to_cover, + measure: mockMeasureEnhanced({ + metric: mockMetric({ + key: MetricKey.conditions_to_cover, + name: 'Conditions to cover', + type: MetricType.ShortInteger, + }), + }), + }), + ]} + {...props} + />, + ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx index ae2114dac0f..d070c52d006 100644 --- a/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx +++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx @@ -23,34 +23,25 @@ import { CenteredLayout, CoverageIndicator, DuplicationsIndicator, - HelperHintIcon, - Link, Spinner, - TextMuted, } from 'design-system'; import { uniq } from 'lodash'; import * as React from 'react'; import { useEffect, useState } from 'react'; -import { FormattedMessage } from 'react-intl'; import { getMeasuresWithMetrics } from '../../../api/measures'; -import HelpTooltip from '../../../components/controls/HelpTooltip'; import { duplicationRatingConverter } from '../../../components/measure/utils'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; -import { translate } from '../../../helpers/l10n'; import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures'; import { isDefined } from '../../../helpers/types'; -import { getQualityGateUrl, getQualityGatesUrl } from '../../../helpers/urls'; import { useBranchStatusQuery } from '../../../queries/branch'; import { PullRequest } from '../../../types/branch-like'; import { IssueType } from '../../../types/issues'; import { Component, MeasureEnhanced } from '../../../types/types'; import MeasuresPanelIssueMeasure from '../branches/MeasuresPanelIssueMeasure'; import MeasuresPanelPercentMeasure from '../branches/MeasuresPanelPercentMeasure'; +import BranchQualityGate from '../components/BranchQualityGate'; import IgnoredConditionWarning from '../components/IgnoredConditionWarning'; import MetaTopBar from '../components/MetaTopBar'; -import QualityGateConditions from '../components/QualityGateConditions'; -import QualityGateStatusHeader from '../components/QualityGateStatusHeader'; -import QualityGateStatusPassedView from '../components/QualityGateStatusPassedView'; import SonarLintPromotion from '../components/SonarLintPromotion'; import '../styles.css'; import { MeasurementType, PR_METRICS, Status } from '../utils'; @@ -107,11 +98,6 @@ export default function PullRequestOverview(props: Props) { return null; } - const path = - component.qualityGate === undefined - ? getQualityGatesUrl() - : getQualityGateUrl(component.qualityGate.name); - const failedConditions = conditions .filter((condition) => condition.level === 'ERROR') .map((c) => enhanceConditionWithMeasure(c, measures)) @@ -119,93 +105,58 @@ export default function PullRequestOverview(props: Props) { return ( <CenteredLayout> - <div className="it__pr-overview sw-mt-12"> - <MetaTopBar branchLike={branchLike} measures={measures} /> - <BasicSeparator className="sw-my-4" /> - - {ignoredConditions && <IgnoredConditionWarning />} - - <div className="sw-flex sw-flex-col sw-mr-12 width-30"> - <Card> - {status && ( - <QualityGateStatusHeader - status={status} - failedConditionCount={failedConditions.length} - /> - )} - - <div className="sw-flex sw-items-center sw-mb-4"> - <TextMuted text={translate('overview.on_new_code_long')} /> - <HelpTooltip - className="sw-ml-2" - overlay={ - <FormattedMessage - defaultMessage={translate('overview.quality_gate.conditions_on_new_code')} - id="overview.quality_gate.conditions_on_new_code" - values={{ - link: <Link to={path}>{translate('overview.quality_gate')}</Link>, - }} - /> - } - > - <HelperHintIcon aria-label="help-tooltip" /> - </HelpTooltip> - </div> - - {status === Status.OK && failedConditions.length === 0 && ( - <QualityGateStatusPassedView /> - )} - - {status !== Status.OK && <BasicSeparator />} - - {failedConditions.length > 0 && ( - <div> - <QualityGateConditions - branchLike={branchLike} - collapsible - component={component} - failedConditions={failedConditions} - /> - </div> - )} - </Card> - <SonarLintPromotion qgConditions={conditions} /> - </div> - - <div className="sw-flex-1"> - <div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4"> - {[ - IssueType.Bug, - IssueType.CodeSmell, - IssueType.Vulnerability, - IssueType.SecurityHotspot, - ].map((type: IssueType) => ( - <Card key={type} className="sw-p-8"> - <MeasuresPanelIssueMeasure - branchLike={branchLike} - component={component} - isNewCodeTab - measures={measures} - type={type} - /> - </Card> - ))} - - {[MeasurementType.Coverage, MeasurementType.Duplication].map( - (type: MeasurementType) => ( + <div className="it__pr-overview sw-mt-12 sw-grid sw-grid-cols-12"> + <div className="sw-col-start-2 sw-col-span-10"> + <MetaTopBar branchLike={branchLike} measures={measures} /> + <BasicSeparator className="sw-my-4" /> + + {ignoredConditions && <IgnoredConditionWarning />} + + {status && ( + <BranchQualityGate + branchLike={branchLike} + component={component} + status={status} + failedConditions={failedConditions} + /> + )} + + <div className="sw-flex-1"> + <div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4"> + {[ + IssueType.Bug, + IssueType.CodeSmell, + IssueType.Vulnerability, + IssueType.SecurityHotspot, + ].map((type: IssueType) => ( <Card key={type} className="sw-p-8"> - <MeasuresPanelPercentMeasure + <MeasuresPanelIssueMeasure branchLike={branchLike} component={component} + isNewCodeTab measures={measures} - ratingIcon={renderMeasureIcon(type)} type={type} - useDiffMetric /> </Card> - ), - )} + ))} + + {[MeasurementType.Coverage, MeasurementType.Duplication].map( + (type: MeasurementType) => ( + <Card key={type} className="sw-p-8"> + <MeasuresPanelPercentMeasure + branchLike={branchLike} + component={component} + measures={measures} + ratingIcon={renderMeasureIcon(type)} + type={type} + useDiffMetric + /> + </Card> + ), + )} + </div> </div> + <SonarLintPromotion qgConditions={conditions} /> </div> </div> </CenteredLayout> diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx index 26af400499a..7fc29a23c58 100644 --- a/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx +++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx @@ -26,6 +26,7 @@ import { mockComponent } from '../../../../helpers/mocks/component'; import { mockQualityGateProjectCondition } from '../../../../helpers/mocks/quality-gates'; import { mockLoggedInUser, mockMeasure, mockMetric } from '../../../../helpers/testMocks'; import { renderComponent } from '../../../../helpers/testReactTestingUtils'; +import { byLabelText, byRole } from '../../../../helpers/testSelector'; import { ComponentPropsType } from '../../../../helpers/testUtils'; import { ComponentQualifier } from '../../../../types/component'; import { MetricKey, MetricType } from '../../../../types/metrics'; @@ -121,6 +122,8 @@ it('should render correctly for a passed QG', async () => { renderPullRequestOverview(); await waitFor(async () => expect(await screen.findByText('metric.level.OK')).toBeInTheDocument()); + expect(screen.getByLabelText('overview.quality_gate_x.overview.gate.OK')).toBeInTheDocument(); + expect(screen.getByText('metric.new_lines.name')).toBeInTheDocument(); expect(screen.getByText(/overview.last_analysis_x/)).toBeInTheDocument(); }); @@ -165,18 +168,21 @@ it('should render correctly for a failed QG', async () => { renderPullRequestOverview(); await waitFor(async () => - expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument(), + expect( + await byLabelText('overview.quality_gate_x.overview.gate.ERROR').find(), + ).toBeInTheDocument(), ); - expect(await screen.findByText('1.0% metric.new_coverage.name')).toBeInTheDocument(); - expect(await screen.findByText('quality_gates.operator.GT 2.0%')).toBeInTheDocument(); - expect( - await screen.findByText('1.0% metric.duplicated_lines.name quality_gates.conditions.new_code'), + byRole('link', { + name: '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', + }).get(), ).toBeInTheDocument(); - expect(await screen.findByText('quality_gates.operator.GT 1.0%')).toBeInTheDocument(); - - expect(screen.getByText('quality_gates.operator.GT 3')).toBeInTheDocument(); }); function renderPullRequestOverview( diff --git a/server/sonar-web/src/main/js/apps/overview/utils.ts b/server/sonar-web/src/main/js/apps/overview/utils.ts index abca784fab8..aeb0251f16b 100644 --- a/server/sonar-web/src/main/js/apps/overview/utils.ts +++ b/server/sonar-web/src/main/js/apps/overview/utils.ts @@ -26,7 +26,7 @@ import { parseAsString } from '../../helpers/query'; import { IssueType } from '../../types/issues'; import { MetricKey } from '../../types/metrics'; import { AnalysisMeasuresVariations, MeasureHistory } from '../../types/project-activity'; -import { RawQuery } from '../../types/types'; +import { Dict, RawQuery } from '../../types/types'; export const METRICS: string[] = [ // quality gate @@ -152,6 +152,35 @@ const MEASUREMENTS_MAP = { }, }; +export const RATING_TO_SEVERITIES_MAPPING = [ + 'BLOCKER,CRITICAL,MAJOR,MINOR', + 'BLOCKER,CRITICAL,MAJOR', + 'BLOCKER,CRITICAL', + 'BLOCKER', +]; + +export const RATING_METRICS_MAPPING: Dict<IssueType> = { + [MetricKey.reliability_rating]: IssueType.Bug, + [MetricKey.new_reliability_rating]: IssueType.Bug, + [MetricKey.security_rating]: IssueType.Vulnerability, + [MetricKey.new_security_rating]: IssueType.Vulnerability, + [MetricKey.sqale_rating]: IssueType.CodeSmell, + [MetricKey.new_maintainability_rating]: IssueType.CodeSmell, + [MetricKey.security_review_rating]: IssueType.SecurityHotspot, + [MetricKey.new_security_review_rating]: IssueType.SecurityHotspot, +}; + +export const METRICS_REPORTED_IN_OVERVIEW_CARDS = [ + MetricKey.new_violations, + MetricKey.violations, + MetricKey.new_coverage, + MetricKey.coverage, + MetricKey.new_security_hotspots_reviewed, + MetricKey.security_hotspots_reviewed, + MetricKey.new_duplicated_lines_density, + MetricKey.duplicated_lines_density, +]; + export function getIssueRatingName(type: IssueType) { return translate('metric_domain', ISSUETYPE_METRIC_KEYS_MAP[type].ratingName); } diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index b353b7baea1..b0fece2070a 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -34,6 +34,7 @@ import { RuleRepository } from '../types/coding-rules'; import { EditionKey } from '../types/editions'; import { IssueScope, IssueSeverity, IssueStatus, IssueType, RawIssue } from '../types/issues'; import { Language } from '../types/languages'; +import { MetricKey, MetricType } from '../types/metrics'; import { Notification } from '../types/notifications'; import { DumpStatus, DumpTask } from '../types/project-dump'; import { TaskStatuses } from '../types/tasks'; @@ -395,11 +396,14 @@ export function mockLocation(overrides: Partial<Location> = {}): Location { }; } -export function mockMetric(overrides: Partial<Pick<Metric, 'key' | 'name' | 'type'>> = {}): Metric { - const key = overrides.key || 'coverage'; +export function mockMetric( + overrides: Partial<Pick<Metric, 'key' | 'name' | 'type' | 'domain'>> = {}, +): Metric { + const key = overrides.key || MetricKey.coverage; const name = overrides.name || key; - const type = overrides.type || 'PERCENT'; + const type = overrides.type || MetricType.Percent; return { + ...overrides, id: key, key, name, 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 fc35a833d05..869b49dc597 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2897,6 +2897,7 @@ metric.ncloc_language_distribution.description=Non Commenting Lines of Code Dist metric.ncloc_language_distribution.name=Lines of Code Per Language metric.new_blocker_violations.description=New Blocker issues metric.new_blocker_violations.name=New Blocker Issues +metric.new_blocker_violations.short_name=Blocker Issues metric.new_branch_coverage.description=Condition coverage of new/changed code metric.new_branch_coverage.name=Condition Coverage on New Code metric.new_branch_coverage.extra_short_name=Condition Coverage @@ -2915,6 +2916,7 @@ metric.new_coverage.name=Coverage on New Code metric.new_coverage.short_name=Coverage metric.new_critical_violations.description=New Critical issues metric.new_critical_violations.name=New Critical Issues +metric.new_critical_violations.short_name=Critical Issues metric.new_development_cost.description=Development cost on new code metric.new_development_cost.name=Development Cost on New Code metric.new_duplicated_blocks.name=Duplicated Blocks on New Code @@ -2930,6 +2932,7 @@ metric.new_duplicated_lines_density.short_name=Duplications metric.new_duplicated_lines_density.extra_short_name=Density metric.new_info_violations.description=New Info issues metric.new_info_violations.name=New Info Issues +metric.new_info_violations.short_name=Info Issues metric.new_it_branch_coverage.description=Integration tests condition coverage of new/changed code metric.new_it_branch_coverage.name=Condition Coverage by IT on New Code metric.new_it_conditions_to_cover.description=New conditions to cover by integration tests @@ -2955,8 +2958,10 @@ metric.new_maintainability_rating.name=Maintainability Rating on New Code metric.new_maintainability_rating.extra_short_name=Rating metric.new_major_violations.description=New Major issues metric.new_major_violations.name=New Major Issues +metric.new_major_violations.short_name=Major Issues metric.new_minor_violations.description=New Minor issues metric.new_minor_violations.name=New Minor Issues +metric.new_minor_violations.short_name=Minor Issues metric.new_lines.name=New Lines metric.new_lines.description=New lines metric.new_lines.short_name=Lines @@ -3740,6 +3745,7 @@ 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_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 |