diff options
44 files changed, 726 insertions, 248 deletions
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryData.java b/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryData.java index c7bc4a0b1d9..1682ef8ab8f 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryData.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryData.java @@ -280,7 +280,7 @@ public class TelemetryData { record Project(String projectUuid, Long lastAnalysis, String language, Long loc) { } - record QualityGate(String uuid, boolean isCaycCompliant) { + record QualityGate(String uuid, String caycStatus) { } public static class ProjectStatistics { diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryDataJsonWriter.java b/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryDataJsonWriter.java index 57171896fdb..62ae8e49d2c 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryDataJsonWriter.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryDataJsonWriter.java @@ -174,7 +174,7 @@ public class TelemetryDataJsonWriter { statistics.getQualityGates().forEach(qualityGate -> { json.beginObject(); json.prop("uuid", qualityGate.uuid()); - json.prop("isCaycCompliant", qualityGate.isCaycCompliant()); + json.prop("caycStatus", qualityGate.caycStatus()); json.endObject(); }); json.endArray(); diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/telemetry/TelemetryDataJsonWriterTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/telemetry/TelemetryDataJsonWriterTest.java index cfd2521e969..59c2f805cd4 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/telemetry/TelemetryDataJsonWriterTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/telemetry/TelemetryDataJsonWriterTest.java @@ -453,15 +453,15 @@ public class TelemetryDataJsonWriterTest { "quality-gates": [ { "uuid": "uuid-0", - "isCaycCompliant": true + "caycStatus": "non-compliant" }, { "uuid": "uuid-1", - "isCaycCompliant": false + "caycStatus": "compliant" }, { "uuid": "uuid-2", - "isCaycCompliant": true + "caycStatus": "over-compliant" } ] } @@ -518,8 +518,9 @@ public class TelemetryDataJsonWriterTest { } private List<TelemetryData.QualityGate> attachQualityGates() { - return IntStream.range(0, 3).mapToObj(i -> new TelemetryData.QualityGate("uuid-" + i, i % 2 == 0)) - .collect(Collectors.toList()); + return List.of(new TelemetryData.QualityGate("uuid-0", "non-compliant"), + new TelemetryData.QualityGate("uuid-1", "compliant"), + new TelemetryData.QualityGate("uuid-2", "over-compliant")); } @DataProvider diff --git a/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts index bec51cabac4..7c918b749cd 100644 --- a/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts @@ -23,7 +23,7 @@ import { mockQualityGate } from '../../helpers/mocks/quality-gates'; import { mockUserBase } from '../../helpers/mocks/users'; import { mockCondition, mockGroup } from '../../helpers/testMocks'; import { MetricKey } from '../../types/metrics'; -import { Condition, QualityGate } from '../../types/types'; +import { CaycStatus, Condition, QualityGate } from '../../types/types'; import { addGroup, addUser, @@ -82,7 +82,7 @@ export class QualityGatesServiceMock { ], isDefault: true, isBuiltIn: false, - isCaycCompliant: true, + caycStatus: CaycStatus.Compliant, }), mockQualityGate({ id: 'AXGYZrDqC-YjVCvvbRDY', @@ -94,6 +94,7 @@ export class QualityGatesServiceMock { ], isDefault: false, isBuiltIn: false, + caycStatus: CaycStatus.NonCompliant, }), mockQualityGate({ id: 'AWBWEMe4qGAMGEYPjJlr', @@ -123,7 +124,7 @@ export class QualityGatesServiceMock { ], isDefault: false, isBuiltIn: true, - isCaycCompliant: true, + caycStatus: CaycStatus.Compliant, }), mockQualityGate({ id: 'AWBWEMe4qGAMGEYPjJlruit', @@ -135,7 +136,39 @@ export class QualityGatesServiceMock { ], isDefault: false, isBuiltIn: false, - isCaycCompliant: false, + caycStatus: CaycStatus.NonCompliant, + }), + mockQualityGate({ + id: 'AWBWEMe4qGAMGEYPjJlruitoc', + name: 'Over Compliant CAYC QG', + conditions: [ + { id: 'deprecatedoc', metric: 'function_complexity', op: 'LT', error: '1' }, + { id: 'AXJMbIUHPAOIsUIE3eOFoc', metric: 'new_coverage', op: 'LT', error: '80' }, + { id: 'AXJMbIUHPAOIsUIE3eNsoc', metric: 'new_security_rating', op: 'GT', error: '1' }, + { id: 'AXJMbIUHPAOIsUIE3eODoc', metric: 'new_reliability_rating', op: 'GT', error: '1' }, + { + id: 'AXJMbIUHPAOIsUIE3eOEoc', + metric: 'new_maintainability_rating', + op: 'GT', + error: '1', + }, + { id: 'AXJMbIUHPAOIsUIE3eOFocdl', metric: 'new_coverage', op: 'LT', error: '80' }, + { + id: 'AXJMbIUHPAOIsUIE3eOGoc', + metric: 'new_duplicated_lines_density', + op: 'GT', + error: '3', + }, + { + id: 'AXJMbIUHPAOIsUIE3eOkoc', + metric: 'new_security_hotspots_reviewed', + op: 'LT', + error: '100', + }, + ], + isDefault: false, + isBuiltIn: false, + caycStatus: CaycStatus.OverCompliant, }), ]; @@ -253,7 +286,7 @@ export class QualityGatesServiceMock { ], isDefault: false, isBuiltIn: false, - isCaycCompliant: true, + caycStatus: CaycStatus.Compliant, }) ); return this.reply({ 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) { diff --git a/server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts b/server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts index ce49bd097c2..23131abbbd0 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/quality-gates.ts @@ -24,7 +24,7 @@ import { QualityGateStatusCondition, QualityGateStatusConditionEnhanced, } from '../../types/quality-gates'; -import { QualityGate } from '../../types/types'; +import { CaycStatus, QualityGate } from '../../types/types'; import { mockMeasureEnhanced, mockMetric } from '../testMocks'; export function mockQualityGate(overrides: Partial<QualityGate> = {}): QualityGate { @@ -40,7 +40,7 @@ export function mockQualityGateStatus( ): QualityGateStatus { return { ignoredConditions: false, - isCaycCompliant: true, + caycStatus: CaycStatus.Compliant, failedConditions: [mockQualityGateStatusConditionEnhanced()], key: 'foo', name: 'Foo', @@ -91,7 +91,7 @@ export function mockQualityGateProjectStatus( }, ], ignoredConditions: false, - isCaycCompliant: true, + caycStatus: CaycStatus.Compliant, status: 'OK', ...overrides, }; @@ -123,7 +123,7 @@ export function mockQualityGateApplicationStatus( value: '5', }, ], - isCaycCompliant: true, + caycStatus: CaycStatus.Compliant, status: 'ERROR', }, { @@ -139,7 +139,7 @@ export function mockQualityGateApplicationStatus( value: '15', }, ], - isCaycCompliant: true, + caycStatus: CaycStatus.Compliant, status: 'ERROR', }, ], diff --git a/server/sonar-web/src/main/js/types/quality-gates.ts b/server/sonar-web/src/main/js/types/quality-gates.ts index ed27b6713d5..9faa052c536 100644 --- a/server/sonar-web/src/main/js/types/quality-gates.ts +++ b/server/sonar-web/src/main/js/types/quality-gates.ts @@ -18,14 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { BranchLike } from './branch-like'; -import { MeasureEnhanced, Metric, Status } from './types'; +import { CaycStatus, MeasureEnhanced, Metric, Status } from './types'; import { UserBase } from './users'; export interface QualityGateProjectStatus { conditions?: QualityGateProjectStatusCondition[]; ignoredConditions: boolean; status: Status; - isCaycCompliant: boolean; + caycStatus: CaycStatus; } export interface QualityGateProjectStatusCondition { @@ -59,13 +59,13 @@ export interface QualityGateApplicationStatusChildProject { key: string; name: string; status: Status; - isCaycCompliant: boolean; + caycStatus: CaycStatus; } export interface QualityGateStatus { failedConditions: QualityGateStatusConditionEnhanced[]; ignoredConditions?: boolean; - isCaycCompliant: boolean; + caycStatus: CaycStatus; key: string; name: string; status: Status; diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts index 6a9e9ee8ff2..c7c9f5fa45e 100644 --- a/server/sonar-web/src/main/js/types/types.ts +++ b/server/sonar-web/src/main/js/types/types.ts @@ -507,6 +507,12 @@ export interface ProjectLink { url: string; } +export enum CaycStatus { + Compliant = 'compliant', + NonCompliant = 'non-compliant', + OverCompliant = 'over-compliant', +} + export interface QualityGate { actions?: { associateProjects?: boolean; @@ -520,7 +526,7 @@ export interface QualityGate { conditions?: Condition[]; id: string; isBuiltIn?: boolean; - isCaycCompliant?: boolean; + caycStatus?: CaycStatus; isDefault?: boolean; name: string; } diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java index b82f39156b3..a24ec92656f 100644 --- a/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java @@ -241,7 +241,7 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader { for (QualityGateDto qualityGateDto : qualityGateDtos) { qualityGates.add( new TelemetryData.QualityGate(qualityGateDto.getUuid(), qualityGateCaycChecker.checkCaycCompliant(dbSession, - qualityGateDto.getUuid())) + qualityGateDto.getUuid()).toString()) ); } diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDataLoaderImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDataLoaderImplTest.java index ffcbc9f0411..e996981a909 100644 --- a/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDataLoaderImplTest.java +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDataLoaderImplTest.java @@ -63,6 +63,7 @@ import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.groups.Tuple.tuple; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -83,6 +84,7 @@ import static org.sonar.core.platform.EditionProvider.Edition.ENTERPRISE; import static org.sonar.db.component.BranchType.BRANCH; import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_CPP_KEY; import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_C_KEY; +import static org.sonar.server.qualitygate.QualityGateCaycStatus.NON_COMPLIANT; import static org.sonar.server.telemetry.TelemetryDataLoaderImpl.SCIM_PROPERTY_ENABLED; @RunWith(DataProviderRunner.class) @@ -118,6 +120,7 @@ public class TelemetryDataLoaderImplTest { public void setUpBuiltInQualityGate() { String builtInQgName = "Sonar Way"; builtInDefaultQualityGate = db.qualityGates().insertQualityGate(qg -> qg.setName(builtInQgName).setBuiltIn(true)); + when(qualityGateCaycChecker.checkCaycCompliant(any(), any())).thenReturn(NON_COMPLIANT); db.qualityGates().setDefaultQualityGate(builtInDefaultQualityGate); bugsDto = db.measures().insertMetric(m -> m.setKey(BUGS_KEY)); @@ -229,11 +232,11 @@ public class TelemetryDataLoaderImplTest { tuple(1L, 0L, qualityGate1.getUuid(), "scm-1", "ci-1", "azure_devops_cloud", Optional.of(1L), Optional.of(1L), Optional.of(1L), Optional.of(50L), Optional.of(5L)), tuple(1L, 0L, builtInDefaultQualityGate.getUuid(), "scm-2", "ci-2", "github_cloud", Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty())); assertThat(data.getQualityGates()) - .extracting(TelemetryData.QualityGate::uuid, TelemetryData.QualityGate::isCaycCompliant) + .extracting(TelemetryData.QualityGate::uuid, TelemetryData.QualityGate::caycStatus) .containsExactlyInAnyOrder( - tuple(builtInDefaultQualityGate.getUuid(), false), - tuple(qualityGate1.getUuid(), false), - tuple(qualityGate2.getUuid(), false) + tuple(builtInDefaultQualityGate.getUuid(), "non-compliant"), + tuple(qualityGate1.getUuid(), "non-compliant"), + tuple(qualityGate2.getUuid(), "non-compliant") ); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycChecker.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycChecker.java index c1ae05f9364..6dcc8a71ee2 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycChecker.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycChecker.java @@ -40,6 +40,9 @@ import static org.sonar.api.measures.CoreMetrics.NEW_RELIABILITY_RATING; import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED; import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_RATING; import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; +import static org.sonar.server.qualitygate.QualityGateCaycStatus.COMPLIANT; +import static org.sonar.server.qualitygate.QualityGateCaycStatus.NON_COMPLIANT; +import static org.sonar.server.qualitygate.QualityGateCaycStatus.OVER_COMPLIANT; public class QualityGateCaycChecker { @@ -67,13 +70,13 @@ public class QualityGateCaycChecker { this.dbClient = dbClient; } - public boolean checkCaycCompliant(DbSession dbSession, String qualityGateUuid) { + public QualityGateCaycStatus checkCaycCompliant(DbSession dbSession, String qualityGateUuid) { var conditionsByMetricId = dbClient.gateConditionDao().selectForQualityGate(dbSession, qualityGateUuid) .stream() .collect(uniqueIndex(QualityGateConditionDto::getMetricUuid)); - if (conditionsByMetricId.size() != CAYC_METRICS.size()) { - return false; + if (conditionsByMetricId.size() < CAYC_METRICS.size()) { + return NON_COMPLIANT; } var metrics = dbClient.metricDao().selectByUuids(dbSession, conditionsByMetricId.keySet()) @@ -85,14 +88,20 @@ public class QualityGateCaycChecker { .filter(metric -> checkMetricCaycCompliant(conditionsByMetricId.get(metric.getUuid()), metric)) .count(); - return count == CAYC_METRICS.size(); + if (metrics.size() == count && count == CAYC_METRICS.size()) { + return COMPLIANT; + } else if (metrics.size() > count && count == CAYC_METRICS.size()) { + return OVER_COMPLIANT; + } + + return NON_COMPLIANT; } - public boolean checkCaycCompliantFromProject(DbSession dbSession, String projectUuid) { + public QualityGateCaycStatus checkCaycCompliantFromProject(DbSession dbSession, String projectUuid) { return Optional.ofNullable(dbClient.qualityGateDao().selectByProjectUuid(dbSession, projectUuid)) .or(() -> Optional.ofNullable(dbClient.qualityGateDao().selectDefault(dbSession))) .map(qualityGate -> checkCaycCompliant(dbSession, qualityGate.getUuid())) - .orElse(false); + .orElse(NON_COMPLIANT); } private static boolean checkMetricCaycCompliant(QualityGateConditionDto condition, MetricDto metric) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycStatus.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycStatus.java new file mode 100644 index 00000000000..74fd54ee8f5 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycStatus.java @@ -0,0 +1,38 @@ +/* + * 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. + */ +package org.sonar.server.qualitygate; + +public enum QualityGateCaycStatus { + + NON_COMPLIANT("non-compliant"), + COMPLIANT("compliant"), + OVER_COMPLIANT("over-compliant"); + + final String status; + + QualityGateCaycStatus(String status) { + this.status = status; + } + + @Override + public String toString() { + return status; + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ListAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ListAction.java index 4a722672f7a..8a6362d1686 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ListAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ListAction.java @@ -59,7 +59,7 @@ public class ListAction implements QualityGatesWsAction { .setSince("4.3") .setResponseExample(Resources.getResource(this.getClass(), "list-example.json")) .setChangelog( - new Change("9.9", "'isCaycCompliant' field is added on quality gate"), + new Change("9.9", "'caycStatus' field is added on quality gate"), new Change("8.4", "Field 'id' in the response is deprecated. Format changes from integer to string."), new Change("7.0", "'isDefault' field is added on quality gate"), new Change("7.0", "'default' field on root level is deprecated"), @@ -88,7 +88,7 @@ public class ListAction implements QualityGatesWsAction { .setName(qualityGate.getName()) .setIsDefault(qualityGate.getUuid().equals(defaultUuid)) .setIsBuiltIn(qualityGate.isBuiltIn()) - .setIsCaycCompliant(qualityGateCaycChecker.checkCaycCompliant(dbSession, qualityGate.getUuid())) + .setCaycStatus(qualityGateCaycChecker.checkCaycCompliant(dbSession, qualityGate.getUuid()).toString()) .setActions(wsSupport.getActions(dbSession, qualityGate, defaultQualityGate)) .build()) .collect(toList())); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ProjectStatusAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ProjectStatusAction.java index 828d9eb67cb..54fc1d9db32 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ProjectStatusAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ProjectStatusAction.java @@ -42,6 +42,7 @@ import org.sonar.db.project.ProjectDto; import org.sonar.server.component.ComponentFinder; import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.qualitygate.QualityGateCaycChecker; +import org.sonar.server.qualitygate.QualityGateCaycStatus; import org.sonar.server.user.UserSession; import org.sonar.server.ws.KeyExamples; import org.sonarqube.ws.Qualitygates.ProjectStatusResponse; @@ -95,7 +96,7 @@ public class ProjectStatusAction implements QualityGatesWsAction { .setSince("5.3") .setHandler(this) .setChangelog( - new Change("9.9", "'isCaycCompliant' field is added to the response"), + new Change("9.9", "'caycStatus' field is added to the response"), new Change("9.5", "The 'Execute Analysis' permission also allows to access the endpoint"), new Change("8.5", "The field 'periods' in the response is deprecated. Use 'period' instead"), new Change("7.7", "The parameters 'branch' and 'pullRequest' were added"), @@ -152,10 +153,10 @@ public class ProjectStatusAction implements QualityGatesWsAction { ProjectAndSnapshot projectAndSnapshot = getProjectAndSnapshot(dbSession, analysisId, projectUuid, projectKey, branchKey, pullRequestId); checkPermission(projectAndSnapshot.project); Optional<String> measureData = loadQualityGateDetails(dbSession, projectAndSnapshot, analysisId != null); - boolean isCaycCompliant = qualityGateCaycChecker.checkCaycCompliantFromProject(dbSession, projectAndSnapshot.project.getUuid()); + QualityGateCaycStatus caycStatus = qualityGateCaycChecker.checkCaycCompliantFromProject(dbSession, projectAndSnapshot.project.getUuid()); return ProjectStatusResponse.newBuilder() - .setProjectStatus(new QualityGateDetailsFormatter(measureData.orElse(null), projectAndSnapshot.snapshotDto.orElse(null), isCaycCompliant).format()) + .setProjectStatus(new QualityGateDetailsFormatter(measureData.orElse(null), projectAndSnapshot.snapshotDto.orElse(null), caycStatus).format()) .build(); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java index 5351687d2c0..e32b433b482 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java @@ -28,6 +28,7 @@ import java.util.function.Predicate; import java.util.stream.StreamSupport; import javax.annotation.Nullable; import org.sonar.db.component.SnapshotDto; +import org.sonar.server.qualitygate.QualityGateCaycStatus; import org.sonarqube.ws.Qualitygates.ProjectStatusResponse; import org.sonarqube.ws.Qualitygates.ProjectStatusResponse.NewCodePeriod; import org.sonarqube.ws.Qualitygates.ProjectStatusResponse.Period; @@ -38,13 +39,13 @@ import static org.sonar.api.utils.DateUtils.formatDateTime; public class QualityGateDetailsFormatter { private final Optional<String> optionalMeasureData; private final Optional<SnapshotDto> optionalSnapshot; - private final boolean isCaycCompliant; + private final QualityGateCaycStatus caycStatus; private final ProjectStatusResponse.ProjectStatus.Builder projectStatusBuilder; - public QualityGateDetailsFormatter(@Nullable String measureData, @Nullable SnapshotDto snapshot, boolean isCaycCompliant) { + public QualityGateDetailsFormatter(@Nullable String measureData, @Nullable SnapshotDto snapshot, QualityGateCaycStatus caycStatus) { this.optionalMeasureData = Optional.ofNullable(measureData); this.optionalSnapshot = Optional.ofNullable(snapshot); - this.isCaycCompliant = isCaycCompliant; + this.caycStatus = caycStatus; this.projectStatusBuilder = ProjectStatusResponse.ProjectStatus.newBuilder(); } @@ -57,7 +58,7 @@ public class QualityGateDetailsFormatter { ProjectStatusResponse.Status qualityGateStatus = measureLevelToQualityGateStatus(json.get("level").getAsString()); projectStatusBuilder.setStatus(qualityGateStatus); - projectStatusBuilder.setIsCaycCompliant(isCaycCompliant); + projectStatusBuilder.setCaycStatus(caycStatus.toString()); formatIgnoredConditions(json); formatConditions(json.getAsJsonArray("conditions")); @@ -204,7 +205,7 @@ public class QualityGateDetailsFormatter { } private ProjectStatusResponse.ProjectStatus newResponseWithoutQualityGateDetails() { - return ProjectStatusResponse.ProjectStatus.newBuilder().setStatus(ProjectStatusResponse.Status.NONE).setIsCaycCompliant(isCaycCompliant).build(); + return ProjectStatusResponse.ProjectStatus.newBuilder().setStatus(ProjectStatusResponse.Status.NONE).setCaycStatus(caycStatus.toString()).build(); } private static Predicate<JsonObject> isConditionOnValidPeriod() { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ShowAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ShowAction.java index 265ba4c20de..07bc8fa3321 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ShowAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ShowAction.java @@ -35,6 +35,7 @@ import org.sonar.db.metric.MetricDto; import org.sonar.db.qualitygate.QualityGateConditionDto; import org.sonar.db.qualitygate.QualityGateDto; import org.sonar.server.qualitygate.QualityGateCaycChecker; +import org.sonar.server.qualitygate.QualityGateCaycStatus; import org.sonar.server.qualitygate.QualityGateFinder; import org.sonarqube.ws.Qualitygates.ShowWsResponse; @@ -71,7 +72,7 @@ public class ShowAction implements QualityGatesWsAction { .setSince("4.3") .setResponseExample(Resources.getResource(this.getClass(), "show-example.json")) .setChangelog( - new Change("9.9", "'isCaycCompliant' field is added to the response"), + new Change("9.9", "'caycStatus' field is added to the response"), new Change("8.4", "Parameter 'id' is deprecated. Format changes from integer to string. Use 'name' instead."), new Change("8.4", "Field 'id' in the response is deprecated."), new Change("7.6", "'period' and 'warning' fields of conditions are removed from the response"), @@ -99,8 +100,8 @@ public class ShowAction implements QualityGatesWsAction { Collection<QualityGateConditionDto> conditions = getConditions(dbSession, qualityGate); Map<String, MetricDto> metricsByUuid = getMetricsByUuid(dbSession, conditions); QualityGateDto defaultQualityGate = qualityGateFinder.getDefault(dbSession); - boolean isCaycCompliant = qualityGateCaycChecker.checkCaycCompliant(dbSession, qualityGate.getUuid()); - writeProtobuf(buildResponse(dbSession, qualityGate, defaultQualityGate, conditions, metricsByUuid, isCaycCompliant), request, response); + QualityGateCaycStatus caycStatus = qualityGateCaycChecker.checkCaycCompliant(dbSession, qualityGate.getUuid()); + writeProtobuf(buildResponse(dbSession, qualityGate, defaultQualityGate, conditions, metricsByUuid, caycStatus), request, response); } } @@ -124,12 +125,12 @@ public class ShowAction implements QualityGatesWsAction { } private ShowWsResponse buildResponse(DbSession dbSession, QualityGateDto qualityGate, QualityGateDto defaultQualityGate, Collection<QualityGateConditionDto> conditions, - Map<String, MetricDto> metricsByUuid, boolean isCaycCompliant) { + Map<String, MetricDto> metricsByUuid, QualityGateCaycStatus caycStatus) { return ShowWsResponse.newBuilder() .setId(qualityGate.getUuid()) .setName(qualityGate.getName()) .setIsBuiltIn(qualityGate.isBuiltIn()) - .setIsCaycCompliant(isCaycCompliant) + .setCaycStatus(caycStatus.toString()) .addAllConditions(conditions.stream() .map(toWsCondition(metricsByUuid)) .collect(toList())) diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/project_status-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/project_status-example.json index 6119eada050..b17e84c2c76 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/project_status-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/project_status-example.json @@ -2,7 +2,7 @@ "projectStatus": { "status": "ERROR", "ignoredConditions": false, - "isCaycCompliant": false, + "caycStatus": "non-compliant", "conditions": [ { "status": "ERROR", diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateCaycCheckerTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateCaycCheckerTest.java index 51471717a45..e62702569de 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateCaycCheckerTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateCaycCheckerTest.java @@ -30,7 +30,7 @@ import org.sonar.db.DbTester; import org.sonar.db.metric.MetricDto; import org.sonar.db.qualitygate.QualityGateConditionDto; -import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES; import static org.sonar.api.measures.CoreMetrics.LINE_COVERAGE; import static org.sonar.api.measures.CoreMetrics.NEW_COVERAGE; @@ -40,6 +40,9 @@ import static org.sonar.api.measures.CoreMetrics.NEW_RELIABILITY_RATING; import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED; import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_RATING; import static org.sonar.server.qualitygate.QualityGateCaycChecker.CAYC_METRICS; +import static org.sonar.server.qualitygate.QualityGateCaycStatus.COMPLIANT; +import static org.sonar.server.qualitygate.QualityGateCaycStatus.NON_COMPLIANT; +import static org.sonar.server.qualitygate.QualityGateCaycStatus.OVER_COMPLIANT; public class QualityGateCaycCheckerTest { @@ -51,7 +54,7 @@ public class QualityGateCaycCheckerTest { public void checkCaycCompliant() { String qualityGateUuid = "abcd"; CAYC_METRICS.forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue())); - assertThat(underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)).isTrue(); + assertEquals(COMPLIANT, underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)); } @Test @@ -62,7 +65,7 @@ public class QualityGateCaycCheckerTest { // extra conditions outside of CAYC requirements List.of(LINE_COVERAGE, DUPLICATED_LINES).forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue())); - assertThat(underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)).isFalse(); + assertEquals(OVER_COMPLIANT, underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)); } @Test @@ -75,7 +78,7 @@ public class QualityGateCaycCheckerTest { var metric = metrics.get(i); insertCondition(metric, qualityGateUuid, idx == i ? metric.getWorstValue() : metric.getBestValue()); } - assertThat(underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)).isFalse(); + assertEquals(NON_COMPLIANT, underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)); }); } @@ -84,7 +87,7 @@ public class QualityGateCaycCheckerTest { String qualityGateUuid = "abcd"; List.of(NEW_MAINTAINABILITY_RATING, NEW_RELIABILITY_RATING, NEW_SECURITY_HOTSPOTS_REVIEWED, NEW_DUPLICATED_LINES_DENSITY) .forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue())); - assertThat(underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)).isFalse(); + assertEquals(NON_COMPLIANT, underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)); } @Test @@ -94,7 +97,7 @@ public class QualityGateCaycCheckerTest { .forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue())); List.of(NEW_COVERAGE, NEW_DUPLICATED_LINES_DENSITY) .forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getWorstValue())); - assertThat(underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)).isTrue(); + assertEquals(COMPLIANT, underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)); } private void insertCondition(MetricDto metricDto, String qualityGateUuid, Double threshold) { diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ListActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ListActionTest.java index b314b63f220..50e488df895 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ListActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ListActionTest.java @@ -19,6 +19,7 @@ */ package org.sonar.server.qualitygate.ws; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.sonar.api.server.ws.WebService; @@ -43,6 +44,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_GATES; import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES; +import static org.sonar.server.qualitygate.QualityGateCaycStatus.COMPLIANT; +import static org.sonar.server.qualitygate.QualityGateCaycStatus.NON_COMPLIANT; +import static org.sonar.server.qualitygate.QualityGateCaycStatus.OVER_COMPLIANT; import static org.sonar.test.JsonAssert.assertJson; import static org.sonarqube.ws.Qualitygates.ListWsResponse; @@ -61,6 +65,11 @@ public class ListActionTest { private final WsActionTester ws = new WsActionTester(new ListAction(db.getDbClient(), new QualityGatesWsSupport(dbClient, userSession, TestComponentFinder.from(db)), qualityGateFinder, qualityGateCaycChecker)); + @Before + public void setUp() { + when(qualityGateCaycChecker.checkCaycCompliant(any(), any())).thenReturn(COMPLIANT); + } + @Test public void list_quality_gates() { QualityGateDto defaultQualityGate = db.qualityGates().insertQualityGate(); @@ -94,11 +103,13 @@ public class ListActionTest { } @Test - public void test_isCaycCompliant_flag() { + public void test_caycStatus_flag() { QualityGateDto qualityGate1 = db.qualityGates().insertQualityGate(); QualityGateDto qualityGate2 = db.qualityGates().insertQualityGate(); - when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate1.getUuid()))).thenReturn(true); - when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate2.getUuid()))).thenReturn(false); + QualityGateDto qualityGate3 = db.qualityGates().insertQualityGate(); + when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate1.getUuid()))).thenReturn(COMPLIANT); + when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate2.getUuid()))).thenReturn(NON_COMPLIANT); + when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate3.getUuid()))).thenReturn(OVER_COMPLIANT); db.qualityGates().setDefaultQualityGate(qualityGate1); @@ -106,10 +117,11 @@ public class ListActionTest { .executeProtobuf(ListWsResponse.class); assertThat(response.getQualitygatesList()) - .extracting(QualityGate::getId, QualityGate::getIsCaycCompliant) + .extracting(QualityGate::getId, QualityGate::getCaycStatus) .containsExactlyInAnyOrder( - tuple(qualityGate1.getUuid(), true), - tuple(qualityGate2.getUuid(), false)); + tuple(qualityGate1.getUuid(), COMPLIANT.toString()), + tuple(qualityGate2.getUuid(), NON_COMPLIANT.toString()), + tuple(qualityGate3.getUuid(), OVER_COMPLIANT.toString())); } @Test diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ProjectStatusActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ProjectStatusActionTest.java index 53ace6c0539..1c87e3e3962 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ProjectStatusActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ProjectStatusActionTest.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.RandomStringUtils; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.sonar.api.measures.CoreMetrics; @@ -37,7 +38,6 @@ import org.sonar.db.component.ComponentDto; import org.sonar.db.component.SnapshotDto; import org.sonar.db.metric.MetricDto; import org.sonar.db.permission.GlobalPermission; -import org.sonar.db.qualitygate.QualityGateDto; import org.sonar.server.component.TestComponentFinder; import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.exceptions.ForbiddenException; @@ -53,6 +53,7 @@ import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.tuple; +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -61,6 +62,8 @@ import static org.sonar.db.component.SnapshotTesting.newAnalysis; import static org.sonar.db.measure.MeasureTesting.newLiveMeasure; import static org.sonar.db.measure.MeasureTesting.newMeasureDto; import static org.sonar.db.metric.MetricTesting.newMetricDto; +import static org.sonar.server.qualitygate.QualityGateCaycStatus.COMPLIANT; +import static org.sonar.server.qualitygate.QualityGateCaycStatus.NON_COMPLIANT; import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_ANALYSIS_ID; import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_BRANCH; import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_ID; @@ -82,6 +85,11 @@ public class ProjectStatusActionTest { private final WsActionTester ws = new WsActionTester(new ProjectStatusAction(dbClient, TestComponentFinder.from(db), userSession, qualityGateCaycChecker)); + @Before + public void setUp() { + when(qualityGateCaycChecker.checkCaycCompliantFromProject(any(), any())).thenReturn(NON_COMPLIANT); + } + @Test public void test_definition() { WebService.Action action = ws.getDef(); @@ -328,7 +336,7 @@ public class ProjectStatusActionTest { ComponentDto project = db.components().insertPrivateProject(); var qg = db.qualityGates().insertBuiltInQualityGate(); db.qualityGates().setDefaultQualityGate(qg); - when(qualityGateCaycChecker.checkCaycCompliantFromProject(any(DbSession.class), eq(project.uuid()))).thenReturn(true); + when(qualityGateCaycChecker.checkCaycCompliantFromProject(any(DbSession.class), eq(project.uuid()))).thenReturn(COMPLIANT); SnapshotDto snapshot = dbClient.snapshotDao().insert(dbSession, newAnalysis(project)); dbSession.commit(); userSession.addProjectPermission(UserRole.USER, project); @@ -337,7 +345,7 @@ public class ProjectStatusActionTest { .setParam(PARAM_ANALYSIS_ID, snapshot.getUuid()) .executeProtobuf(ProjectStatusResponse.class); - assertThat(result.getProjectStatus().getIsCaycCompliant()).isTrue(); + assertEquals(COMPLIANT.toString(), result.getProjectStatus().getCaycStatus()); } @Test diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest.java index c6aaf141573..ebf394c06d3 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest.java @@ -21,7 +21,6 @@ package org.sonar.server.qualitygate.ws; import java.io.IOException; import java.util.List; -import java.util.Optional; import javax.annotation.Nullable; import org.apache.commons.io.IOUtils; import org.junit.Test; @@ -31,7 +30,9 @@ import org.sonarqube.ws.Qualitygates.ProjectStatusResponse.ProjectStatus; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertEquals; import static org.sonar.api.utils.DateUtils.formatDateTime; +import static org.sonar.server.qualitygate.QualityGateCaycStatus.NON_COMPLIANT; public class QualityGateDetailsFormatterTest { @@ -49,7 +50,7 @@ public class QualityGateDetailsFormatterTest { ProjectStatus result = underTest.format(); assertThat(result.getStatus()).isEqualTo(ProjectStatusResponse.Status.ERROR); - assertThat(result.getIsCaycCompliant()).isFalse(); + assertEquals(NON_COMPLIANT.toString(), result.getCaycStatus()); // check conditions assertThat(result.getConditionsCount()).isEqualTo(3); List<ProjectStatusResponse.Condition> conditions = result.getConditionsList(); @@ -145,6 +146,6 @@ public class QualityGateDetailsFormatterTest { } private static QualityGateDetailsFormatter newQualityGateDetailsFormatter(@Nullable String measureData, @Nullable SnapshotDto snapshotDto) { - return new QualityGateDetailsFormatter(measureData, snapshotDto, false); + return new QualityGateDetailsFormatter(measureData, snapshotDto, NON_COMPLIANT); } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ShowActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ShowActionTest.java index 4bf7df81744..1e19d5ca378 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ShowActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ShowActionTest.java @@ -19,6 +19,7 @@ */ package org.sonar.server.qualitygate.ws; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.sonar.api.server.ws.WebService; @@ -43,12 +44,14 @@ import static java.lang.String.format; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.AssertionsForClassTypes.tuple; +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_GATES; import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES; +import static org.sonar.server.qualitygate.QualityGateCaycStatus.COMPLIANT; import static org.sonar.test.JsonAssert.assertJson; import static org.sonarqube.ws.Qualitygates.Actions; @@ -64,6 +67,11 @@ public class ShowActionTest { new ShowAction(db.getDbClient(), new QualityGateFinder(db.getDbClient()), new QualityGatesWsSupport(db.getDbClient(), userSession, TestComponentFinder.from(db)), qualityGateCaycChecker)); + @Before + public void setUp() { + when(qualityGateCaycChecker.checkCaycCompliant(any(), any())).thenReturn(COMPLIANT); + } + @Test public void show() { QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); @@ -103,14 +111,14 @@ public class ShowActionTest { @Test public void show_isCaycCompliant() { QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); - when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate.getUuid()))).thenReturn(true); + when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate.getUuid()))).thenReturn(COMPLIANT); db.qualityGates().setDefaultQualityGate(qualityGate); ShowWsResponse response = ws.newRequest() .setParam("name", qualityGate.getName()) .executeProtobuf(ShowWsResponse.class); - assertThat(response.getIsCaycCompliant()).isTrue(); + assertEquals(COMPLIANT.toString(), response.getCaycStatus()); } @Test 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 24801719a71..f7420980fb2 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1844,8 +1844,8 @@ quality_gates.cayc_missing.banner.description={cayc_link} is the most efficient quality_gates.cayc_condition.review_update=Review and Fix Quality Gate quality_gates.cayc.review_update_modal.header=Fix "{0}" to comply with Clean as You Code quality_gates.cayc.review_update_modal.confirm_text=Fix Quality Gate -quality_gates.cayc.review_update_modal.description1=This quality gate will be updated to comply with {cayc_link}. Existing conditions on duplication and coverage on new code will be preserved. All non-compliant conditions will be deleted, inlcuding conditions on overall code. -quality_gates.cayc.review_update_modal.description2=Please review the new quality gate conditions below. +quality_gates.cayc.review_update_modal.description1=This quality gate will be updated to comply with {cayc_link}. Please review the changes below. +quality_gates.cayc.review_update_modal.description2=All other conditions will be preserved quality_gates.cayc.new_maintainability_rating.A=Technical debt ratio is less than {0} quality_gates.cayc.new_maintainability_rating=Technical debt ratio is greater than {1} quality_gates.cayc.new_reliability_rating.A=No bugs @@ -1853,6 +1853,8 @@ quality_gates.cayc.new_security_rating.A=No vulnerabilities quality_gates.cayc.unlock_edit=Unlock editing quality_gates.cayc.tooltip.message=This quality gate does not comply with Clean as You Code. quality_gates.cayc.badge.tooltip.learn_more=Learn more: Clean as You Code +quality_gates.cayc_over_compliant.tooltip.message=Additional conditions may have potential drawbacks. +quality_gates.cayc_over_compliant.badge.tooltip.learn_more=Learn more: Potential drawbacks quality_gates.cayc.banner.title=This quality gate complies with Clean as You Code quality_gates.cayc.banner.description=This quality gate complies with the {cayc_link} methodology, so that you benefit from the most efficient approach to delivering Clean Code. The quality gate ensures that the {new_code_link} you’re working on has: quality_gates.cayc.banner.list_item1=No bugs @@ -1860,7 +1862,13 @@ quality_gates.cayc.banner.list_item2=No vulnerabilities quality_gates.cayc.banner.list_item3=No security hotspots to review quality_gates.cayc.banner.list_item4=A limited amount of code smells (i.e. low technical debt ratio) quality_gates.cayc.banner.list_item5=A controlled level of duplication and coverage -quality_gates.cayc_unfollow.description=Are you reconsidering {cayc_link}? We strongly recommend this methodology to achieve a Clean Code state. However, if you still wish to move out of this approach you may edit this quality gate. +quality_gates.cayc_over_compliant.banner.title=This quality gate may have drawbacks +quality_gates.cayc_over_compliant.banner.description1=This quality gate includes additional conditions that may have drawbacks. +quality_gates.cayc_over_compliant.banner.description2=Learn more: {link} +quality_gates.cayc_over_compliant.banner.link=Potential drawbacks +quality_gates.cayc_unfollow.description=You may click unlock to edit this quality gate. Adding extra conditions to a compliant quality gate can result in drawbacks. Are you reconsidering {cayc_link}? We strongly recommend this methodology to achieve a Clean Code status. +quality_gates.cayc.review_update_modal.add_condition.header= {0} condition(s) on New Code will be added +quality_gates.cayc.review_update_modal.modify_condition.header= {0} condition(s) on New Code will be modified #------------------------------------------------------------------------------ # @@ -3262,9 +3270,15 @@ overview.quality_gate.ignored_conditions.tooltip=At the start of a new code peri overview.quality_gate.conditions_on_new_code=Only conditions on new code that are defined in the Quality Gate are checked. See the {link} associated to the project for details. overview.quality_gate.conditions.cayc.warning=The quality gate used by this project does not comply with Clean as You Code. overview.quality_gate.conditions.cayc.details=Fixing this quality gate will help you achieve a Clean Code state. -overview.quality_gate.conditions.cayc.review=Review Quality Gate +overview.quality_gate.conditions.cayc.details_with_link=Fixing {link} will help you achieve a Clean Code state. +overview.quality_gate.conditions.non_cayc.warning.link=this quality gate +overview.quality_gate.conditions.cayc_over_compliant.warning.link=This quality gate +overview.quality_gate.conditions.cayc_over_compliant.details=This quality gate is Clean as You Code compliant, but it includes additional conditions that may have drawbacks. +overview.quality_gate.conditions.cayc_over_compliant.details_with_link={link} is Clean as You Code compliant, but it includes additional conditions that may have drawbacks. overview.quality_gate.conditions.cayc.link=Learn more: Clean as You Code +overview.quality_gate.conditions.cayc_over_compliant.link=Learn more: Potential drawbacks overview.quality_gate.application.non_cayc.projects_x={0} project(s) in this application use a Quality Gate that does not comply with Clean as You Code +overview.quality_gate.application.cayc_over_compliant.projects_x={0} project(s) in this application use a Clean as You Code compliant quality gate with extra conditions. This may result in drawbacks. overview.quality_gate.show_project_conditions_x=Show failed conditions for project {0} overview.quality_gate.hide_project_conditions_x=Hide failed conditions for project {0} overview.quality_profiles=Quality Profiles used diff --git a/sonar-ws/src/main/protobuf/ws-qualitygates.proto b/sonar-ws/src/main/protobuf/ws-qualitygates.proto index 168a6b226ae..b1e9f05226f 100644 --- a/sonar-ws/src/main/protobuf/ws-qualitygates.proto +++ b/sonar-ws/src/main/protobuf/ws-qualitygates.proto @@ -36,7 +36,7 @@ message ProjectStatusResponse { repeated Period periods = 3; optional bool ignoredConditions = 4; optional NewCodePeriod period = 5; - optional bool isCaycCompliant = 6; + optional string caycStatus = 6; } message Condition { @@ -131,7 +131,7 @@ message ShowWsResponse { repeated Condition conditions = 3; optional bool isBuiltIn = 4; optional Actions actions = 5; - optional bool isCaycCompliant = 6; + optional string caycStatus = 6; message Condition { optional string id = 1; @@ -168,7 +168,7 @@ message ListWsResponse { optional bool isDefault = 3; optional bool isBuiltIn = 4; optional Actions actions = 5; - optional bool isCaycCompliant = 6; + optional string caycStatus = 6; } message RootActions { |