diff options
author | Matteo Mara <matteo.mara@sonarsource.com> | 2023-01-27 16:16:07 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-02-03 14:41:21 +0000 |
commit | ca5790f9c26dcd9fdb291a91193dcf3ffe7bf7de (patch) | |
tree | fd13a52ea8fea3ec35b777c0f83e9e4f8a9f909b /server/sonar-web/src/main/js/apps | |
parent | f507535da5cc4f1f2d25d6cd3c7c742cda1e8465 (diff) | |
download | sonarqube-ca5790f9c26dcd9fdb291a91193dcf3ffe7bf7de.tar.gz sonarqube-ca5790f9c26dcd9fdb291a91193dcf3ffe7bf7de.zip |
SONAR-17815 implement updated logic for CaYC quality gates
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
21 files changed, 511 insertions, 172 deletions
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/ApplicationNonCaycProjectWarning.tsx b/server/sonar-web/src/main/js/apps/overview/branches/ApplicationNonCaycProjectWarning.tsx new file mode 100644 index 00000000000..e1fc30b255b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/branches/ApplicationNonCaycProjectWarning.tsx @@ -0,0 +1,86 @@ +/* + * 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 DocLink from '../../../components/common/DocLink'; +import Link from '../../../components/common/Link'; +import QualifierIcon from '../../../components/icons/QualifierIcon'; +import { Alert } from '../../../components/ui/Alert'; +import { getBranchLikeQuery } from '../../../helpers/branch-like'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { getProjectQueryUrl } from '../../../helpers/urls'; +import { ComponentQualifier } from '../../../types/component'; +import { QualityGateStatus } from '../../../types/quality-gates'; +import { CaycStatus } from '../../../types/types'; + +interface Props { + projects: QualityGateStatus[]; + caycStatus: CaycStatus; +} + +export default function ApplicationNonCaycProjectWarning({ projects, caycStatus }: Props) { + return ( + <div className="overview-quality-gate-conditions-list padded big-spacer-top"> + {caycStatus === CaycStatus.NonCompliant ? ( + <Alert variant="warning"> + {translateWithParameters( + 'overview.quality_gate.application.non_cayc.projects_x', + projects.length + )} + </Alert> + ) : ( + <p className="padded"> + {translateWithParameters( + 'overview.quality_gate.application.cayc_over_compliant.projects_x', + projects.length + )} + </p> + )} + + <ul className="spacer-left spacer-bottom big-spacer-top"> + {projects.map(({ key, name, branchLike }) => ( + <li key={key} className="text-ellipsis spacer-bottom" title={name}> + <Link + className="link-no-underline" + to={getProjectQueryUrl(key, getBranchLikeQuery(branchLike))} + > + <QualifierIcon + className="little-spacer-right" + qualifier={ComponentQualifier.Project} + /> + {name} + </Link> + </li> + ))} + </ul> + <hr className="big-spacer-top big-spacer-bottom" /> + <div className="spacer spacer-bottom big-spacer-top"> + {caycStatus === CaycStatus.NonCompliant ? ( + <DocLink to="/user-guide/clean-as-you-code/"> + {translate('overview.quality_gate.conditions.cayc.link')} + </DocLink> + ) : ( + <DocLink to="/user-guide/clean-as-you-code/#potential-drawbacks"> + {translate('overview.quality_gate.conditions.cayc_over_compliant.link')} + </DocLink> + )} + </div> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx index e7953550f34..6c021598d32 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx @@ -187,13 +187,13 @@ export default class BranchOverview extends React.PureComponent<Props, State> { if (this.mounted) { const qgStatuses = results .map(({ measures = [], project, projectBranchLike }) => { - const { key, name, status, isCaycCompliant } = project; + const { key, name, status, caycStatus } = project; const conditions = extractStatusConditionsFromApplicationStatusChildProject(project); const failedConditions = this.getFailedConditions(conditions, measures); return { failedConditions, - isCaycCompliant, + caycStatus, key, name, status, @@ -241,13 +241,13 @@ export default class BranchOverview extends React.PureComponent<Props, State> { this.loadMeasuresAndMeta(key, branch, metricKeys).then( ({ measures, metrics, period }) => { if (this.mounted && measures) { - const { ignoredConditions, isCaycCompliant, status } = projectStatus; + const { ignoredConditions, caycStatus, status } = projectStatus; const conditions = extractStatusConditionsFromProjectStatus(projectStatus); const failedConditions = this.getFailedConditions(conditions, measures); const qgStatus = { ignoredConditions, - isCaycCompliant, + caycStatus, failedConditions, key, name, diff --git a/server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarning.tsx b/server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarning.tsx index 9dbb28caa5c..5d5c64345c1 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarning.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarning.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; import DocLink from '../../../components/common/DocLink'; import Link from '../../../components/common/Link'; import { Alert } from '../../../components/ui/Alert'; @@ -33,17 +34,24 @@ export default function CleanAsYouCodeWarning({ component }: Props) { return ( <> <Alert variant="warning">{translate('overview.quality_gate.conditions.cayc.warning')}</Alert> - - <p className="big-spacer-top big-spacer-bottom"> - {translate('overview.quality_gate.conditions.cayc.details')} - </p> - - {component.qualityGate && ( - <div className="big-spacer-bottom"> - <Link className="button" to={getQualityGateUrl(component.qualityGate.key)}> - {translate('overview.quality_gate.conditions.cayc.review')} - </Link> - </div> + {component.qualityGate ? ( + <p className="big-spacer-top big-spacer-bottom"> + <FormattedMessage + id="overview.quality_gate.conditions.cayc.details_with_link" + defaultMessage={translate('overview.quality_gate.conditions.cayc.details_with_link')} + values={{ + link: ( + <Link to={getQualityGateUrl(component.qualityGate.key)}> + {translate('overview.quality_gate.conditions.non_cayc.warning.link')} + </Link> + ), + }} + /> + </p> + ) : ( + <p className="big-spacer-top big-spacer-bottom"> + {translate('overview.quality_gate.conditions.cayc.details')} + </p> )} <DocLink to="/user-guide/clean-as-you-code/"> diff --git a/server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarningOverCompliant.tsx b/server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarningOverCompliant.tsx new file mode 100644 index 00000000000..ae1a241257e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarningOverCompliant.tsx @@ -0,0 +1,62 @@ +/* + * 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 { FormattedMessage } from 'react-intl'; +import DocLink from '../../../components/common/DocLink'; +import Link from '../../../components/common/Link'; +import { translate } from '../../../helpers/l10n'; +import { getQualityGateUrl } from '../../../helpers/urls'; +import { Component } from '../../../types/types'; + +interface Props { + component: Pick<Component, 'key' | 'qualifier' | 'qualityGate'>; +} + +export default function CleanAsYouCodeWarningOverCompliant({ component }: Props) { + return ( + <> + {component.qualityGate ? ( + <p className="big-spacer-bottom"> + <FormattedMessage + id="overview.quality_gate.conditions.cayc_over_compliant.details_with_link" + defaultMessage={translate( + 'overview.quality_gate.conditions.cayc_over_compliant.details_with_link' + )} + values={{ + link: ( + <Link to={getQualityGateUrl(component.qualityGate.key)}> + {translate('overview.quality_gate.conditions.cayc_over_compliant.warning.link')} + </Link> + ), + }} + /> + </p> + ) : ( + <p className="big-spacer-bottom"> + {translate('overview.quality_gate.conditions.cayc_over_compliant.details')} + </p> + )} + + <DocLink to="/user-guide/clean-as-you-code/#potential-drawbacks"> + {translate('overview.quality_gate.conditions.cayc_over_compliant.link')} + </DocLink> + </> + ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx index 25610006a7b..dd41fa73376 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx @@ -20,18 +20,15 @@ import classNames from 'classnames'; import { flatMap } from 'lodash'; import * as React from 'react'; -import Link from '../../../components/common/Link'; import HelpTooltip from '../../../components/controls/HelpTooltip'; -import QualifierIcon from '../../../components/icons/QualifierIcon'; import { Alert } from '../../../components/ui/Alert'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; -import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { getProjectQueryUrl } from '../../../helpers/urls'; import { ComponentQualifier, isApplication } from '../../../types/component'; import { QualityGateStatus } from '../../../types/quality-gates'; -import { Component } from '../../../types/types'; +import { CaycStatus, Component } from '../../../types/types'; import SonarLintPromotion from '../components/SonarLintPromotion'; +import ApplicationNonCaycProjectWarning from './ApplicationNonCaycProjectWarning'; import QualityGatePanelSection from './QualityGatePanelSection'; export interface QualityGatePanelProps { @@ -57,7 +54,13 @@ export function QualityGatePanel(props: QualityGatePanelProps) { const nonCaycProjectsInApp = isApplication(component.qualifier) ? qgStatuses - .filter(({ isCaycCompliant }) => !isCaycCompliant) + .filter(({ caycStatus }) => caycStatus === CaycStatus.NonCompliant) + .sort(({ name: a }, { name: b }) => a.localeCompare(b, undefined, { sensitivity: 'base' })) + : []; + + const overCompliantCaycProjectsInApp = isApplication(component.qualifier) + ? qgStatuses + .filter(({ caycStatus }) => caycStatus === CaycStatus.OverCompliant) .sort(({ name: a }, { name: b }) => a.localeCompare(b, undefined, { sensitivity: 'base' })) : []; @@ -118,7 +121,7 @@ export function QualityGatePanel(props: QualityGatePanelProps) { </div> {(overallFailedConditionsCount > 0 || - qgStatuses.some(({ isCaycCompliant }) => !isCaycCompliant)) && ( + qgStatuses.some(({ caycStatus }) => caycStatus !== CaycStatus.Compliant)) && ( <div data-test="overview__quality-gate-conditions"> {qgStatuses.map((qgStatus) => ( <QualityGatePanelSection @@ -131,39 +134,17 @@ export function QualityGatePanel(props: QualityGatePanelProps) { )} {nonCaycProjectsInApp.length > 0 && ( - <div className="overview-quality-gate-conditions-list padded big-spacer-top"> - <Alert variant="warning"> - {translateWithParameters( - 'overview.quality_gate.application.non_cayc.projects_x', - nonCaycProjectsInApp.length - )} - </Alert> - <div className="spacer big-spacer-bottom big-spacer-top"> - <Link - target="_blank" - to="https://docs.sonarqube.org/latest/user-guide/clean-as-you-code/#quality-gate" - > - {translate('overview.quality_gate.conditions.cayc.link')} - </Link> - </div> - <hr className="big-spacer-top big-spacer-bottom" /> - <ul className="spacer-left spacer-bottom"> - {nonCaycProjectsInApp.map(({ key, name, branchLike }) => ( - <li key={key} className="text-ellipsis spacer-bottom" title={name}> - <Link - className="link-no-underline" - to={getProjectQueryUrl(key, getBranchLikeQuery(branchLike))} - > - <QualifierIcon - className="little-spacer-right" - qualifier={ComponentQualifier.Project} - /> - {name} - </Link> - </li> - ))} - </ul> - </div> + <ApplicationNonCaycProjectWarning + projects={nonCaycProjectsInApp} + caycStatus={CaycStatus.NonCompliant} + /> + )} + + {overCompliantCaycProjectsInApp.length > 0 && ( + <ApplicationNonCaycProjectWarning + projects={overCompliantCaycProjectsInApp} + caycStatus={CaycStatus.OverCompliant} + /> )} </> )} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx b/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx index 62eba2a1c20..fe6547c7175 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx @@ -29,9 +29,10 @@ import { QualityGateStatus, QualityGateStatusConditionEnhanced, } from '../../../types/quality-gates'; -import { Component } from '../../../types/types'; +import { CaycStatus, Component } from '../../../types/types'; import QualityGateConditions from '../components/QualityGateConditions'; import CleanAsYouCodeWarning from './CleanAsYouCodeWarning'; +import CleanAsYouCodeWarningOverCompliant from './CleanAsYouCodeWarningOverCompliant'; export interface QualityGatePanelSectionProps { branchLike?: BranchLike; @@ -85,7 +86,7 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) { if ( !( qgStatus.failedConditions.length > 0 || - (!qgStatus.isCaycCompliant && !isApplication(component.qualifier)) + (qgStatus.caycStatus !== CaycStatus.Compliant && !isApplication(component.qualifier)) ) ) { return null; @@ -99,7 +100,7 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) { const showSectionTitles = isApplication(component.qualifier) || - !qgStatus.isCaycCompliant || + qgStatus.caycStatus !== CaycStatus.Compliant || (overallFailedConditions.length > 0 && newCodeFailedConditions.length > 0); const toggleLabel = collapsed @@ -130,11 +131,19 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) { {!collapsed && ( <> - {!qgStatus.isCaycCompliant && !isApplication(component.qualifier) && ( - <div className="big-padded bordered-bottom overview-quality-gate-conditions-list"> - <CleanAsYouCodeWarning component={component} /> - </div> - )} + {qgStatus.caycStatus === CaycStatus.NonCompliant && + !isApplication(component.qualifier) && ( + <div className="big-padded bordered-bottom overview-quality-gate-conditions-list"> + <CleanAsYouCodeWarning component={component} /> + </div> + )} + + {qgStatus.caycStatus === CaycStatus.OverCompliant && + !isApplication(component.qualifier) && ( + <div className="big-padded bordered-bottom overview-quality-gate-conditions-list"> + <CleanAsYouCodeWarningOverCompliant component={component} /> + </div> + )} {newCodeFailedConditions.length > 0 && ( <> diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx index 24c1b6dbf33..428ef513c29 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx @@ -42,7 +42,7 @@ import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import { ComponentQualifier } from '../../../../types/component'; import { MetricKey } from '../../../../types/metrics'; import { GraphType } from '../../../../types/project-activity'; -import { Measure, Metric } from '../../../../types/types'; +import { CaycStatus, Measure, Metric } from '../../../../types/types'; import BranchOverview, { BRANCH_OVERVIEW_ACTIVITY_GRAPH, NO_CI_DETECTED } from '../BranchOverview'; jest.mock('../../../../api/measures', () => { @@ -223,7 +223,7 @@ describe('project overview', () => { jest .mocked(getQualityGateProjectStatus) .mockResolvedValueOnce( - mockQualityGateProjectStatus({ status: 'OK', isCaycCompliant: false }) + mockQualityGateProjectStatus({ status: 'OK', caycStatus: CaycStatus.NonCompliant }) ); renderBranchOverview(); @@ -267,9 +267,27 @@ describe('application overview', () => { it("should show projects that don't have a compliant quality gate", async () => { const appStatus = mockQualityGateApplicationStatus({ projects: [ - { key: '1', name: 'first project', conditions: [], isCaycCompliant: false, status: 'OK' }, - { key: '2', name: 'second', conditions: [], isCaycCompliant: true, status: 'OK' }, - { key: '3', name: 'number 3', conditions: [], isCaycCompliant: false, status: 'OK' }, + { + key: '1', + name: 'first project', + conditions: [], + caycStatus: CaycStatus.NonCompliant, + status: 'OK', + }, + { + key: '2', + name: 'second', + conditions: [], + caycStatus: CaycStatus.Compliant, + status: 'OK', + }, + { + key: '3', + name: 'number 3', + conditions: [], + caycStatus: CaycStatus.NonCompliant, + status: 'OK', + }, { key: '4', name: 'four', @@ -282,7 +300,7 @@ describe('application overview', () => { errorThreshold: '0', }, ], - isCaycCompliant: false, + caycStatus: CaycStatus.NonCompliant, status: 'ERROR', }, ], diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/QualityGatePanelSection-test.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/QualityGatePanelSection-test.tsx index 6d151b5bff1..1345e86bd6a 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/QualityGatePanelSection-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/QualityGatePanelSection-test.tsx @@ -27,6 +27,7 @@ import { } from '../../../../helpers/mocks/quality-gates'; import { ComponentQualifier } from '../../../../types/component'; import { MetricKey } from '../../../../types/metrics'; +import { CaycStatus } from '../../../../types/types'; import { QualityGatePanelSection, QualityGatePanelSectionProps } from '../QualityGatePanelSection'; it('should render correctly', () => { @@ -36,6 +37,7 @@ it('should render correctly', () => { qgStatus: mockQualityGateStatus({ failedConditions: [], status: 'OK', + caycStatus: CaycStatus.Compliant, }), }).type() ).toBeNull(); @@ -55,7 +57,7 @@ function shallowRender(props: Partial<QualityGatePanelSectionProps> = {}) { mockQualityGateStatusConditionEnhanced({ metric: MetricKey.new_bugs }), ], status: 'ERROR', - isCaycCompliant: false, + caycStatus: CaycStatus.NonCompliant, })} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanel-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanel-test.tsx.snap index 3ae184caf52..f0dd91c82f5 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanel-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanel-test.tsx.snap @@ -69,6 +69,7 @@ exports[`should render correctly for applications 1`] = ` key="foo" qgStatus={ { + "caycStatus": "compliant", "failedConditions": [ { "actual": "10", @@ -95,7 +96,6 @@ exports[`should render correctly for applications 1`] = ` }, ], "ignoredConditions": false, - "isCaycCompliant": true, "key": "foo", "name": "Foo", "status": "ERROR", @@ -128,6 +128,7 @@ exports[`should render correctly for applications 1`] = ` key="foo" qgStatus={ { + "caycStatus": "compliant", "failedConditions": [ { "actual": "10", @@ -177,7 +178,6 @@ exports[`should render correctly for applications 1`] = ` }, ], "ignoredConditions": false, - "isCaycCompliant": true, "key": "foo", "name": "Foo", "status": "ERROR", @@ -333,6 +333,7 @@ exports[`should render correctly for applications 2`] = ` key="foo" qgStatus={ { + "caycStatus": "compliant", "failedConditions": [ { "actual": "10", @@ -359,7 +360,6 @@ exports[`should render correctly for applications 2`] = ` }, ], "ignoredConditions": false, - "isCaycCompliant": true, "key": "foo", "name": "Foo", "status": "ERROR", @@ -392,9 +392,9 @@ exports[`should render correctly for applications 2`] = ` key="foo" qgStatus={ { + "caycStatus": "compliant", "failedConditions": [], "ignoredConditions": false, - "isCaycCompliant": true, "key": "foo", "name": "Foo", "status": "OK", @@ -504,6 +504,7 @@ exports[`should render correctly for projects 1`] = ` key="foo" qgStatus={ { + "caycStatus": "compliant", "failedConditions": [ { "actual": "10", @@ -530,7 +531,6 @@ exports[`should render correctly for projects 1`] = ` }, ], "ignoredConditions": false, - "isCaycCompliant": true, "key": "foo", "name": "Foo", "status": "ERROR", @@ -702,6 +702,7 @@ exports[`should render correctly for projects 3`] = ` key="foo" qgStatus={ { + "caycStatus": "compliant", "failedConditions": [ { "actual": "10", @@ -728,7 +729,6 @@ exports[`should render correctly for projects 3`] = ` }, ], "ignoredConditions": true, - "isCaycCompliant": true, "key": "foo", "name": "Foo", "status": "ERROR", diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanelSection-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanelSection-test.tsx.snap index aff23a9b3bd..15613e9ec23 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanelSection-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanelSection-test.tsx.snap @@ -40,6 +40,7 @@ exports[`should render correctly 1`] = ` <Memo(QualityGateConditions) component={ { + "caycStatus": "non-compliant", "failedConditions": [ { "actual": "10", @@ -89,7 +90,6 @@ exports[`should render correctly 1`] = ` }, ], "ignoredConditions": false, - "isCaycCompliant": false, "key": "foo", "name": "Foo", "status": "ERROR", @@ -131,6 +131,7 @@ exports[`should render correctly 1`] = ` <Memo(QualityGateConditions) component={ { + "caycStatus": "non-compliant", "failedConditions": [ { "actual": "10", @@ -180,7 +181,6 @@ exports[`should render correctly 1`] = ` }, ], "ignoredConditions": false, - "isCaycCompliant": false, "key": "foo", "name": "Foo", "status": "ERROR", @@ -251,6 +251,7 @@ exports[`should render correctly 2`] = ` <Memo(QualityGateConditions) component={ { + "caycStatus": "non-compliant", "failedConditions": [ { "actual": "10", @@ -300,7 +301,6 @@ exports[`should render correctly 2`] = ` }, ], "ignoredConditions": false, - "isCaycCompliant": false, "key": "foo", "name": "Foo", "status": "ERROR", @@ -342,6 +342,7 @@ exports[`should render correctly 2`] = ` <Memo(QualityGateConditions) component={ { + "caycStatus": "non-compliant", "failedConditions": [ { "actual": "10", @@ -391,7 +392,6 @@ exports[`should render correctly 2`] = ` }, ], "ignoredConditions": false, - "isCaycCompliant": false, "key": "foo", "name": "Foo", "status": "ERROR", diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycOverCompliantBadgeTooltip.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycOverCompliantBadgeTooltip.tsx new file mode 100644 index 00000000000..1ca53aa2ea9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycOverCompliantBadgeTooltip.tsx @@ -0,0 +1,35 @@ +/* + * 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 DocLink from '../../../components/common/DocLink'; +import { translate } from '../../../helpers/l10n'; + +export default function CaycOverCompliantBadgeTooltip() { + return ( + <div> + <p className="spacer-bottom padded-bottom bordered-bottom-cayc"> + {translate('quality_gates.cayc_over_compliant.tooltip.message')} + </p> + <DocLink to="/user-guide/clean-as-you-code/#potential-drawbacks"> + {translate('quality_gates.cayc_over_compliant.badge.tooltip.learn_more')} + </DocLink> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx index 7bba57a6236..7dda45d0ee9 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx @@ -21,11 +21,25 @@ import classNames from 'classnames'; import * as React from 'react'; import { deleteCondition } from '../../../api/quality-gates'; import withMetricsContext from '../../../app/components/metrics/withMetricsContext'; +import { colors } from '../../../app/theme'; import { DeleteButton, EditButton } from '../../../components/controls/buttons'; import ConfirmModal from '../../../components/controls/ConfirmModal'; +import Tooltip from '../../../components/controls/Tooltip'; +import InfoIcon from '../../../components/icons/InfoIcon'; import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n'; -import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types'; -import { CAYC_CONDITIONS_WITHOUT_FIXED_VALUE, getLocalizedMetricNameNoDiffMetric } from '../utils'; +import { + CaycStatus, + Condition as ConditionType, + Dict, + Metric, + QualityGate, +} from '../../../types/types'; +import { + CAYC_CONDITIONS_WITH_FIXED_VALUE, + getLocalizedMetricNameNoDiffMetric, + isCaycCondition, +} from '../utils'; +import CaycOverCompliantBadgeTooltip from './CaycOverCompliantBadgeTooltip'; import ConditionModal from './ConditionModal'; import ConditionValue from './ConditionValue'; @@ -47,6 +61,7 @@ interface State { modal: boolean; } +const TOOLTIP_MOUSE_LEAVE_DELAY = 0.3; export class ConditionComponent extends React.PureComponent<Props, State> { constructor(props: Props) { super(props); @@ -103,6 +118,8 @@ export class ConditionComponent extends React.PureComponent<Props, State> { isCaycModal = false, } = this.props; + const isCaycCompliantAndOverCompliant = qualityGate.caycStatus !== CaycStatus.NonCompliant; + return ( <tr className={classNames({ highlighted: updated })}> <td className="text-middle"> @@ -119,15 +136,25 @@ export class ConditionComponent extends React.PureComponent<Props, State> { metric={metric} isCaycModal={isCaycModal} condition={condition} - isCaycCompliant={qualityGate.isCaycCompliant} + isCaycCompliantAndOverCompliant={isCaycCompliantAndOverCompliant} /> </td> <td className="text-middle nowrap display-flex-justify-end"> + {!isCaycCondition(condition) && isCaycCompliantAndOverCompliant && ( + <span className="display-flex-center spacer-right"> + <Tooltip + overlay={<CaycOverCompliantBadgeTooltip />} + mouseLeaveDelay={TOOLTIP_MOUSE_LEAVE_DELAY} + > + <InfoIcon fill={colors.alertIconInfo} /> + </Tooltip> + </span> + )} {!isCaycModal && canEdit && ( <> - {(!qualityGate.isCaycCompliant || - CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(condition.metric) || - (qualityGate.isCaycCompliant && showEdit)) && ( + {(!isCaycCompliantAndOverCompliant || + !CAYC_CONDITIONS_WITH_FIXED_VALUE.includes(condition.metric) || + (isCaycCompliantAndOverCompliant && showEdit)) && ( <> <EditButton aria-label={translateWithParameters( @@ -150,7 +177,9 @@ export class ConditionComponent extends React.PureComponent<Props, State> { )} </> )} - {(!qualityGate.isCaycCompliant || (qualityGate.isCaycCompliant && showEdit)) && ( + {(!isCaycCompliantAndOverCompliant || + !isCaycCondition(condition) || + (isCaycCompliantAndOverCompliant && showEdit)) && ( <> <DeleteButton aria-label={translateWithParameters( diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx index 328ad0dffc2..f6aba654ea1 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx @@ -20,16 +20,12 @@ import { sortBy } from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { createCondition, deleteCondition, updateCondition } from '../../../api/quality-gates'; +import { createCondition, updateCondition } from '../../../api/quality-gates'; import DocLink from '../../../components/common/DocLink'; import ConfirmModal from '../../../components/controls/ConfirmModal'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { Condition, Dict, Metric, QualityGate } from '../../../types/types'; -import { - getCaycConditionsWithCorrectValue, - getCorrectCaycCondition, - getWeakMissingAndNonCaycConditions, -} from '../utils'; +import { getCorrectCaycCondition, getWeakMissingAndNonCaycConditions } from '../utils'; import ConditionsTable from './ConditionsTable'; interface Props { @@ -50,8 +46,7 @@ export default class CaycReviewUpdateConditionsModal extends React.PureComponent updateCaycQualityGate = () => { const { conditions, qualityGate } = this.props; const promiseArr: Promise<Condition | undefined | void>[] = []; - const { weakConditions, missingConditions, nonCaycConditions } = - getWeakMissingAndNonCaycConditions(conditions); + const { weakConditions, missingConditions } = getWeakMissingAndNonCaycConditions(conditions); weakConditions.forEach((condition) => { promiseArr.push( @@ -80,14 +75,6 @@ export default class CaycReviewUpdateConditionsModal extends React.PureComponent ); }); - nonCaycConditions.forEach((condition) => { - promiseArr.push( - deleteCondition({ id: condition.id }) - .then(() => this.props.onRemoveCondition(condition)) - .catch(() => undefined) - ); - }); - return Promise.all(promiseArr).then(() => { this.props.lockEditing(); }); @@ -95,11 +82,18 @@ export default class CaycReviewUpdateConditionsModal extends React.PureComponent render() { const { conditions, qualityGate, metrics } = this.props; - const caycConditionsWithCorrectValue = getCaycConditionsWithCorrectValue(conditions); - const sortedConditions = sortBy( - caycConditionsWithCorrectValue, + + const { weakConditions, missingConditions } = getWeakMissingAndNonCaycConditions(conditions); + const sortedWeakConditions = sortBy( + weakConditions, + (condition) => metrics[condition.metric] && metrics[condition.metric].name + ); + + const sortedMissingConditions = sortBy( + missingConditions, (condition) => metrics[condition.metric] && metrics[condition.metric].name ); + return ( <ConfirmModal header={translateWithParameters( @@ -125,18 +119,44 @@ export default class CaycReviewUpdateConditionsModal extends React.PureComponent }} /> </p> - <p className="big-spacer-top big-spacer-bottom"> + + {sortedMissingConditions.length > 0 && ( + <> + <h4 className="big-spacer-top spacer-bottom"> + {translateWithParameters( + 'quality_gates.cayc.review_update_modal.add_condition.header', + sortedMissingConditions.length + )} + </h4> + <ConditionsTable + {...this.props} + conditions={sortedMissingConditions} + showEdit={false} + isCaycModal={true} + /> + </> + )} + + {sortedWeakConditions.length > 0 && ( + <> + <h4 className="big-spacer-top spacer-bottom"> + {translateWithParameters( + 'quality_gates.cayc.review_update_modal.modify_condition.header', + sortedWeakConditions.length + )} + </h4> + <ConditionsTable + {...this.props} + conditions={sortedWeakConditions} + showEdit={false} + isCaycModal={true} + /> + </> + )} + + <h4 className="big-spacer-top spacer-bottom"> {translate('quality_gates.cayc.review_update_modal.description2')} - </p> - <h3 className="medium text-normal spacer-top spacer-bottom"> - {translate('quality_gates.conditions.new_code', 'long')} - </h3> - <ConditionsTable - {...this.props} - conditions={sortedConditions} - showEdit={false} - isCaycModal={true} - /> + </h4> </div> </ConfirmModal> ); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValue.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValue.tsx index 4c3a4961c51..92191f4d819 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValue.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValue.tsx @@ -17,6 +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 classNames from 'classnames'; import * as React from 'react'; import { formatMeasure } from '../../../helpers/measures'; import { Condition, Metric } from '../../../types/types'; @@ -27,17 +28,33 @@ interface Props { condition: Condition; isCaycModal?: boolean; metric: Metric; - isCaycCompliant?: boolean; + isCaycCompliantAndOverCompliant?: boolean; } -function ConditionValue({ condition, isCaycModal, metric, isCaycCompliant }: Props) { +function ConditionValue({ + condition, + isCaycModal, + metric, + isCaycCompliantAndOverCompliant, +}: Props) { if (isCaycModal) { + const isToBeModified = condition.error !== getCorrectCaycCondition(condition).error; + return ( <> - <span className="spacer-right"> + {isToBeModified && ( + <span className="red-text strike-through spacer-right"> + {formatMeasure(condition.error, metric.type)} + </span> + )} + <span className={classNames('spacer-right', { 'green-text': isToBeModified })}> {formatMeasure(getCorrectCaycCondition(condition).error, metric.type)} </span> - <ConditionValueDescription condition={getCorrectCaycCondition(condition)} metric={metric} /> + <ConditionValueDescription + className={classNames({ 'green-text': isToBeModified })} + condition={getCorrectCaycCondition(condition)} + metric={metric} + /> </> ); } @@ -45,7 +62,7 @@ function ConditionValue({ condition, isCaycModal, metric, isCaycCompliant }: Pro return ( <> <span className="spacer-right">{formatMeasure(condition.error, metric.type)}</span> - {isCaycCompliant && isCaycCondition(condition) && ( + {isCaycCompliantAndOverCompliant && isCaycCondition(condition) && ( <ConditionValueDescription condition={condition} metric={metric} /> )} </> diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx index 707387f074d..8ad2cf00593 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx @@ -33,7 +33,13 @@ import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; import { isDiffMetric } from '../../../helpers/measures'; import { Feature } from '../../../types/features'; import { MetricKey } from '../../../types/metrics'; -import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types'; +import { + CaycStatus, + Condition as ConditionType, + Dict, + Metric, + QualityGate, +} from '../../../types/types'; import ConditionModal from './ConditionModal'; import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal'; import ConditionsTable from './ConditionsTable'; @@ -63,14 +69,14 @@ export class Conditions extends React.PureComponent<Props, State> { constructor(props: Props) { super(props); this.state = { - unlockEditing: !props.qualityGate.isCaycCompliant, + unlockEditing: props.qualityGate.caycStatus === CaycStatus.NonCompliant, }; } componentDidUpdate(prevProps: Readonly<Props>): void { const { qualityGate } = this.props; if (prevProps.qualityGate.name !== qualityGate.name) { - this.setState({ unlockEditing: !qualityGate.isCaycCompliant }); + this.setState({ unlockEditing: qualityGate.caycStatus === CaycStatus.NonCompliant }); } } @@ -164,7 +170,7 @@ export class Conditions extends React.PureComponent<Props, State> { return ( <div className="quality-gate-section"> - {qualityGate.isCaycCompliant && ( + {qualityGate.caycStatus !== CaycStatus.NonCompliant && ( <Alert className="big-spacer-top big-spacer-bottom cayc-success-banner" variant="success"> <h4 className="spacer-bottom cayc-success-header"> {translate('quality_gates.cayc.banner.title')} @@ -180,7 +186,7 @@ export class Conditions extends React.PureComponent<Props, State> { </DocLink> ), new_code_link: ( - <DocLink to="/project-administration/defining-new-code//"> + <DocLink to="/project-administration/defining-new-code/"> {translate('quality_gates.cayc.new_code')} </DocLink> ), @@ -197,7 +203,29 @@ export class Conditions extends React.PureComponent<Props, State> { </Alert> )} - {!qualityGate.isCaycCompliant && ( + {qualityGate.caycStatus === CaycStatus.OverCompliant && ( + <Alert className="big-spacer-top big-spacer-bottom cayc-success-banner" variant="info"> + <h4 className="spacer-bottom cayc-over-compliant-header"> + {translate('quality_gates.cayc_over_compliant.banner.title')} + </h4> + <p>{translate('quality_gates.cayc_over_compliant.banner.description1')}</p> + <div className="cayc-warning-description spacer-top"> + <FormattedMessage + id="quality_gates.cayc_over_compliant.banner.description2" + defaultMessage={translate('quality_gates.cayc_over_compliant.banner.description2')} + values={{ + link: ( + <DocLink to="/user-guide/clean-as-you-code/#potential-drawbacks"> + {translate('quality_gates.cayc_over_compliant.banner.link')} + </DocLink> + ), + }} + /> + </div> + </Alert> + )} + + {qualityGate.caycStatus === CaycStatus.NonCompliant && ( <Alert className="big-spacer-top big-spacer-bottom" variant="warning"> <h4 className="spacer-bottom cayc-warning-header"> {translate('quality_gates.cayc_missing.banner.title')} @@ -227,18 +255,17 @@ export class Conditions extends React.PureComponent<Props, State> { </Alert> )} - {(!qualityGate.isCaycCompliant || (qualityGate.isCaycCompliant && unlockEditing)) && - canEdit && ( - <div className="pull-right"> - <ModalButton modal={this.renderConditionModal}> - {({ onClick }) => ( - <Button data-test="quality-gates__add-condition" onClick={onClick}> - {translate('quality_gates.add_condition')} - </Button> - )} - </ModalButton> - </div> - )} + {(qualityGate.caycStatus === CaycStatus.NonCompliant || unlockEditing) && canEdit && ( + <div className="pull-right"> + <ModalButton modal={this.renderConditionModal}> + {({ onClick }) => ( + <Button data-test="quality-gates__add-condition" onClick={onClick}> + {translate('quality_gates.add_condition')} + </Button> + )} + </ModalButton> + </div> + )} <header className="display-flex-center"> <h2 className="big">{translate('quality_gates.conditions')}</h2> @@ -314,7 +341,7 @@ export class Conditions extends React.PureComponent<Props, State> { </div> )} - {qualityGate.isCaycCompliant && !unlockEditing && canEdit && ( + {qualityGate.caycStatus !== CaycStatus.NonCompliant && !unlockEditing && canEdit && ( <div className="big-spacer-top big-spacer-bottom cayc-warning-description it__qg-unfollow-cayc"> <p> <FormattedMessage diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx index 48e22abfe16..b9d8f407ade 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx @@ -86,7 +86,7 @@ export default class Details extends React.PureComponent<Props, State> { addGlobalSuccessMessage(translate('quality_gates.condition_added')); const updatedQualityGate = addCondition(clone(qualityGate), condition); - if (qualityGate.isCaycCompliant !== updatedQualityGate.isCaycCompliant) { + if (qualityGate.caycStatus !== updatedQualityGate.caycStatus) { this.props.refreshQualityGates(); } @@ -104,7 +104,7 @@ export default class Details extends React.PureComponent<Props, State> { } addGlobalSuccessMessage(translate('quality_gates.condition_updated')); const updatedQualityGate = replaceCondition(clone(qualityGate), newCondition, oldCondition); - if (qualityGate.isCaycCompliant !== updatedQualityGate.isCaycCompliant) { + if (qualityGate.caycStatus !== updatedQualityGate.caycStatus) { this.props.refreshQualityGates(); } return { @@ -121,7 +121,7 @@ export default class Details extends React.PureComponent<Props, State> { } addGlobalSuccessMessage(translate('quality_gates.condition_deleted')); const updatedQualityGate = deleteCondition(clone(qualityGate), condition); - if (qualityGate.isCaycCompliant !== updatedQualityGate.isCaycCompliant) { + if (qualityGate.caycStatus !== updatedQualityGate.caycStatus) { this.props.refreshQualityGates(); } return { diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx index 53e5490d777..a9581f9d8fa 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx @@ -24,7 +24,7 @@ import ModalButton from '../../../components/controls/ModalButton'; import Tooltip from '../../../components/controls/Tooltip'; import AlertWarnIcon from '../../../components/icons/AlertWarnIcon'; import { translate } from '../../../helpers/l10n'; -import { QualityGate } from '../../../types/types'; +import { CaycStatus, QualityGate } from '../../../types/types'; import BuiltInQualityGateBadge from './BuiltInQualityGateBadge'; import CaycBadgeTooltip from './CaycBadgeTooltip'; import CopyQualityGateForm from './CopyQualityGateForm'; @@ -64,7 +64,6 @@ export default class DetailsHeader extends React.PureComponent<Props> { render() { const { qualityGate } = this.props; const actions = qualityGate.actions || ({} as any); - const { isCaycCompliant } = qualityGate; return ( <div className="layout-page-header-panel layout-page-main-header issues-main-header"> @@ -73,7 +72,7 @@ export default class DetailsHeader extends React.PureComponent<Props> { <div className="pull-left display-flex-center"> <h2>{qualityGate.name}</h2> {qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="spacer-left" />} - {!qualityGate.isCaycCompliant && ( + {qualityGate.caycStatus === CaycStatus.NonCompliant && ( <Tooltip overlay={<CaycBadgeTooltip />} mouseLeaveDelay={TOOLTIP_MOUSE_LEAVE_DELAY}> <AlertWarnIcon className="spacer-left" /> </Tooltip> @@ -111,7 +110,9 @@ export default class DetailsHeader extends React.PureComponent<Props> { {({ onClick }) => ( <Tooltip overlay={ - !isCaycCompliant ? translate('quality_gates.cannot_copy_no_cayc') : null + qualityGate.caycStatus === CaycStatus.NonCompliant + ? translate('quality_gates.cannot_copy_no_cayc') + : null } accessible={false} > @@ -119,7 +120,7 @@ export default class DetailsHeader extends React.PureComponent<Props> { className="little-spacer-left" id="quality-gate-copy" onClick={onClick} - disabled={!isCaycCompliant} + disabled={qualityGate.caycStatus === CaycStatus.NonCompliant} > {translate('copy')} </Button> @@ -130,13 +131,15 @@ export default class DetailsHeader extends React.PureComponent<Props> { {actions.setAsDefault && ( <Tooltip overlay={ - !isCaycCompliant ? translate('quality_gates.cannot_set_default_no_cayc') : null + qualityGate.caycStatus === CaycStatus.NonCompliant + ? translate('quality_gates.cannot_set_default_no_cayc') + : null } accessible={false} > <Button className="little-spacer-left" - disabled={!isCaycCompliant} + disabled={qualityGate.caycStatus === CaycStatus.NonCompliant} id="quality-gate-toggle-default" onClick={this.handleSetAsDefaultClick} > diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx index 12982af51ad..dcbb7d5ff80 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx @@ -23,7 +23,7 @@ import Tooltip from '../../../components/controls/Tooltip'; import AlertWarnIcon from '../../../components/icons/AlertWarnIcon'; import { translate } from '../../../helpers/l10n'; import { getQualityGateUrl } from '../../../helpers/urls'; -import { QualityGate } from '../../../types/types'; +import { CaycStatus, QualityGate } from '../../../types/types'; import BuiltInQualityGateBadge from './BuiltInQualityGateBadge'; interface Props { @@ -49,7 +49,7 @@ export default function List({ qualityGates }: Props) { )} {qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="little-spacer-left" />} - {!qualityGate.isCaycCompliant && ( + {qualityGate.caycStatus === CaycStatus.NonCompliant && ( <> {/* Adding a11y-hidden span for accessibility */} <span className="a11y-hidden">{translate('quality_gates.cayc.tooltip.message')}</span> diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx index 58d83b0da6d..2a4046afaf2 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx @@ -337,7 +337,10 @@ it('should show warning banner when CAYC condition is not properly set and shoul expect(conditionsWrapper.getAllByText('Coverage')).toHaveLength(2); // This quality gate has duplicate condition expect(conditionsWrapper.getByText('Duplicated Lines (%)')).toBeInTheDocument(); - expect(screen.queryByTestId('quality-gates__conditions-overall')).not.toBeInTheDocument(); + const overallConditionsWrapper = within( + await screen.findByTestId('quality-gates__conditions-overall') + ); + expect(overallConditionsWrapper.getByText('Complexity / Function')).toBeInTheDocument(); }); it('should show success banner when quality gate is CAYC compliant', async () => { @@ -368,6 +371,27 @@ it('should show success banner when quality gate is CAYC compliant', async () => expect(await conditionsWrapper.findByText('Duplicated Lines (%)')).toBeInTheDocument(); }); +it('should show info banner when quality gate is CAYC over-compliant', async () => { + const user = userEvent.setup(); + handler.setIsAdmin(true); + renderQualityGateApp(); + + const qualityGate = await screen.findByText('Over Compliant CAYC QG'); + + await user.click(qualityGate); + + expect(screen.getByText('quality_gates.cayc.banner.title')).toBeInTheDocument(); + expect(screen.getByText('quality_gates.cayc.banner.description')).toBeInTheDocument(); + expect(screen.getByText('quality_gates.cayc_over_compliant.banner.title')).toBeInTheDocument(); + expect( + screen.queryByText('quality_gates.cayc_condition.missing_warning.title') + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: 'quality_gates.cayc_condition.review_update' }) + ).not.toBeInTheDocument(); + expect(screen.getByText('quality_gates.cayc.unlock_edit')).toBeInTheDocument(); +}); + it('should unlock editing option for CAYC conditions', async () => { const user = userEvent.setup(); handler.setIsAdmin(true); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/styles.css b/server/sonar-web/src/main/js/apps/quality-gates/styles.css index 649182ea7dc..20597091444 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/styles.css +++ b/server/sonar-web/src/main/js/apps/quality-gates/styles.css @@ -73,6 +73,22 @@ color: var(--alertTextSuccess); } +.cayc-over-compliant-header { + color: var(--veryDarkBlue); +} + .cayc-warning-description { line-height: 18px; } + +.red-text { + color: var(--red); +} + +.green-text { + color: var(--success500); +} + +.strike-through { + text-decoration: line-through; +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts index be68153f64b..aa4b2e6d260 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts +++ b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts @@ -19,7 +19,7 @@ */ import { getLocalizedMetricName } from '../../helpers/l10n'; import { isDiffMetric } from '../../helpers/measures'; -import { Condition, Dict, Metric, QualityGate } from '../../types/types'; +import { CaycStatus, Condition, Dict, Metric, QualityGate } from '../../types/types'; const CAYC_CONDITIONS: { [key: string]: Condition } = { new_reliability_rating: { @@ -61,6 +61,12 @@ const CAYC_CONDITIONS: { [key: string]: Condition } = { }; export const CAYC_CONDITIONS_WITHOUT_FIXED_VALUE = ['new_duplicated_lines_density', 'new_coverage']; +export const CAYC_CONDITIONS_WITH_FIXED_VALUE = [ + 'new_security_hotspots_reviewed', + 'new_maintainability_rating', + 'new_security_rating', + 'new_reliability_rating', +]; export function isCaycCondition(condition: Condition) { return condition.metric in CAYC_CONDITIONS; @@ -70,11 +76,9 @@ export function getWeakMissingAndNonCaycConditions(conditions: Condition[]) { const result: { weakConditions: Condition[]; missingConditions: Condition[]; - nonCaycConditions: Condition[]; } = { weakConditions: [], missingConditions: [], - nonCaycConditions: [], }; Object.keys(CAYC_CONDITIONS).forEach((key) => { const selectedCondition = conditions.find((condition) => condition.metric === key); @@ -87,8 +91,6 @@ export function getWeakMissingAndNonCaycConditions(conditions: Condition[]) { result.weakConditions.push(selectedCondition); } }); - - result.nonCaycConditions = getNonCaycConditions(conditions); return result; } @@ -102,10 +104,6 @@ export function getCaycConditionsWithCorrectValue(conditions: Condition[]) { }); } -export function getNonCaycConditions(conditions: Condition[]) { - return conditions.filter((condition) => !isCaycCondition(condition)); -} - export function getCorrectCaycCondition(condition: Condition) { if (CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(condition.metric)) { return condition; @@ -122,7 +120,7 @@ export function addCondition(qualityGate: QualityGate, condition: Condition): Qu const oldConditions = qualityGate.conditions || []; const conditions = [...oldConditions, condition]; if (conditions) { - qualityGate.isCaycCompliant = updateCaycComplaintStatus(conditions); + qualityGate.caycStatus = updateCaycCompliantStatus(conditions); } return { ...qualityGate, conditions }; } @@ -131,7 +129,7 @@ export function deleteCondition(qualityGate: QualityGate, condition: Condition): const conditions = qualityGate.conditions && qualityGate.conditions.filter((candidate) => candidate !== condition); if (conditions) { - qualityGate.isCaycCompliant = updateCaycComplaintStatus(conditions); + qualityGate.caycStatus = updateCaycCompliantStatus(conditions); } return { ...qualityGate, conditions }; } @@ -147,21 +145,21 @@ export function replaceCondition( return candidate === oldCondition ? newCondition : candidate; }); if (conditions) { - qualityGate.isCaycCompliant = updateCaycComplaintStatus(conditions); + qualityGate.caycStatus = updateCaycCompliantStatus(conditions); } return { ...qualityGate, conditions }; } -export function updateCaycComplaintStatus(conditions: Condition[]) { - if (conditions.length !== Object.keys(CAYC_CONDITIONS).length) { - return false; +export function updateCaycCompliantStatus(conditions: Condition[]) { + if (conditions.length < Object.keys(CAYC_CONDITIONS).length) { + return CaycStatus.NonCompliant; } for (const key of Object.keys(CAYC_CONDITIONS)) { const selectedCondition = conditions.find((condition) => condition.metric === key); if (!selectedCondition) { - return false; + return CaycStatus.NonCompliant; } if ( @@ -169,11 +167,15 @@ export function updateCaycComplaintStatus(conditions: Condition[]) { selectedCondition && selectedCondition.error !== CAYC_CONDITIONS[key].error ) { - return false; + return CaycStatus.NonCompliant; } } - return true; + if (conditions.length > Object.keys(CAYC_CONDITIONS).length) { + return CaycStatus.OverCompliant; + } + + return CaycStatus.Compliant; } export function getPossibleOperators(metric: Metric) { |