From 2d60913db1dc3a499ab5d681b7b2ad324b3ac19d Mon Sep 17 00:00:00 2001 From: Matteo Mara Date: Fri, 27 Jan 2023 16:16:07 +0100 Subject: [PATCH] SONAR-17815 implement updated logic for CaYC quality gates --- .../sonar/server/telemetry/TelemetryData.java | 2 +- .../telemetry/TelemetryDataJsonWriter.java | 2 +- .../TelemetryDataJsonWriterTest.java | 11 +-- .../js/api/mocks/QualityGatesServiceMock.ts | 46 ++++++++-- .../ApplicationNonCaycProjectWarning.tsx | 86 +++++++++++++++++++ .../apps/overview/branches/BranchOverview.tsx | 8 +- .../branches/CleanAsYouCodeWarning.tsx | 30 ++++--- .../CleanAsYouCodeWarningOverCompliant.tsx | 62 +++++++++++++ .../overview/branches/QualityGatePanel.tsx | 61 +++++-------- .../branches/QualityGatePanelSection.tsx | 25 ++++-- .../__tests__/BranchOverview-test.tsx | 30 +++++-- .../QualityGatePanelSection-test.tsx | 4 +- .../QualityGatePanel-test.tsx.snap | 12 +-- .../QualityGatePanelSection-test.tsx.snap | 8 +- .../CaycOverCompliantBadgeTooltip.tsx | 35 ++++++++ .../quality-gates/components/Condition.tsx | 43 ++++++++-- .../ConditionReviewAndUpdateModal.tsx | 80 ++++++++++------- .../components/ConditionValue.tsx | 27 ++++-- .../quality-gates/components/Conditions.tsx | 65 ++++++++++---- .../apps/quality-gates/components/Details.tsx | 6 +- .../components/DetailsHeader.tsx | 17 ++-- .../js/apps/quality-gates/components/List.tsx | 4 +- .../components/__tests__/QualityGate-it.tsx | 26 +++++- .../src/main/js/apps/quality-gates/styles.css | 16 ++++ .../src/main/js/apps/quality-gates/utils.ts | 38 ++++---- .../main/js/helpers/mocks/quality-gates.ts | 10 +-- .../src/main/js/types/quality-gates.ts | 8 +- server/sonar-web/src/main/js/types/types.ts | 8 +- .../telemetry/TelemetryDataLoaderImpl.java | 2 +- .../TelemetryDataLoaderImplTest.java | 11 ++- .../qualitygate/QualityGateCaycChecker.java | 21 +++-- .../qualitygate/QualityGateCaycStatus.java | 38 ++++++++ .../server/qualitygate/ws/ListAction.java | 4 +- .../qualitygate/ws/ProjectStatusAction.java | 7 +- .../ws/QualityGateDetailsFormatter.java | 11 +-- .../server/qualitygate/ws/ShowAction.java | 11 +-- .../ws/project_status-example.json | 2 +- .../QualityGateCaycCheckerTest.java | 15 ++-- .../server/qualitygate/ws/ListActionTest.java | 24 ++++-- .../ws/ProjectStatusActionTest.java | 14 ++- .../ws/QualityGateDetailsFormatterTest.java | 7 +- .../server/qualitygate/ws/ShowActionTest.java | 12 ++- .../resources/org/sonar/l10n/core.properties | 22 ++++- .../src/main/protobuf/ws-qualitygates.proto | 6 +- 44 files changed, 727 insertions(+), 250 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/overview/branches/ApplicationNonCaycProjectWarning.tsx create mode 100644 server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarningOverCompliant.tsx create mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/CaycOverCompliantBadgeTooltip.tsx create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycStatus.java 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 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 269875da4b6..3259ef0f239 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, @@ -84,7 +84,7 @@ export class QualityGatesServiceMock { ], isDefault: true, isBuiltIn: false, - isCaycCompliant: true, + caycStatus: CaycStatus.Compliant, }), mockQualityGate({ name: 'SonarSource way - CFamily', @@ -95,6 +95,7 @@ export class QualityGatesServiceMock { ], isDefault: false, isBuiltIn: false, + caycStatus: CaycStatus.NonCompliant, }), mockQualityGate({ name: 'Sonar way', @@ -123,7 +124,7 @@ export class QualityGatesServiceMock { ], isDefault: false, isBuiltIn: true, - isCaycCompliant: true, + caycStatus: CaycStatus.Compliant, }), mockQualityGate({ name: 'Non Cayc QG', @@ -134,14 +135,45 @@ export class QualityGatesServiceMock { ], isDefault: false, isBuiltIn: false, - isCaycCompliant: false, + caycStatus: CaycStatus.NonCompliant, + }), + mockQualityGate({ + 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, }), mockQualityGate({ name: 'QG without conditions', conditions: [], isDefault: false, isBuiltIn: false, - isCaycCompliant: false, + caycStatus: CaycStatus.NonCompliant, }), mockQualityGate({ name: 'QG without new code conditions', @@ -150,7 +182,7 @@ export class QualityGatesServiceMock { ], isDefault: false, isBuiltIn: false, - isCaycCompliant: false, + caycStatus: CaycStatus.NonCompliant, }), ]; @@ -279,7 +311,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 ( +
+ {caycStatus === CaycStatus.NonCompliant ? ( + + {translateWithParameters( + 'overview.quality_gate.application.non_cayc.projects_x', + projects.length + )} + + ) : ( +

+ {translateWithParameters( + 'overview.quality_gate.application.cayc_over_compliant.projects_x', + projects.length + )} +

+ )} + +
    + {projects.map(({ key, name, branchLike }) => ( +
  • + + + {name} + +
  • + ))} +
+
+
+ {caycStatus === CaycStatus.NonCompliant ? ( + + {translate('overview.quality_gate.conditions.cayc.link')} + + ) : ( + + {translate('overview.quality_gate.conditions.cayc_over_compliant.link')} + + )} +
+
+ ); +} 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 { 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 { 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 d72d68a4066..19a876a7889 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 ( <> {translate('overview.quality_gate.conditions.cayc.warning')} - -

- {translate('overview.quality_gate.conditions.cayc.details')} -

- - {component.qualityGate && ( -
- - {translate('overview.quality_gate.conditions.cayc.review')} - -
+ {component.qualityGate ? ( +

+ + {translate('overview.quality_gate.conditions.non_cayc.warning.link')} + + ), + }} + /> +

+ ) : ( +

+ {translate('overview.quality_gate.conditions.cayc.details')} +

)} 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..f055badf67c --- /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; +} + +export default function CleanAsYouCodeWarningOverCompliant({ component }: Props) { + return ( + <> + {component.qualityGate ? ( +

+ + {translate('overview.quality_gate.conditions.cayc_over_compliant.warning.link')} + + ), + }} + /> +

+ ) : ( +

+ {translate('overview.quality_gate.conditions.cayc_over_compliant.details')} +

+ )} + + + {translate('overview.quality_gate.conditions.cayc_over_compliant.link')} + + + ); +} 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) { {(overallFailedConditionsCount > 0 || - qgStatuses.some(({ isCaycCompliant }) => !isCaycCompliant)) && ( + qgStatuses.some(({ caycStatus }) => caycStatus !== CaycStatus.Compliant)) && (
{qgStatuses.map((qgStatus) => ( 0 && ( -
- - {translateWithParameters( - 'overview.quality_gate.application.non_cayc.projects_x', - nonCaycProjectsInApp.length - )} - -
- - {translate('overview.quality_gate.conditions.cayc.link')} - -
-
-
    - {nonCaycProjectsInApp.map(({ key, name, branchLike }) => ( -
  • - - - {name} - -
  • - ))} -
-
+ + )} + + {overCompliantCaycProjectsInApp.length > 0 && ( + )} )} 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) && ( -
- -
- )} + {qgStatus.caycStatus === CaycStatus.NonCompliant && + !isApplication(component.qualifier) && ( +
+ +
+ )} + + {qgStatus.caycStatus === CaycStatus.OverCompliant && + !isApplication(component.qualifier) && ( +
+ +
+ )} {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 = {}) { 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`] = ` +

+ {translate('quality_gates.cayc_over_compliant.tooltip.message')} +

+ + {translate('quality_gates.cayc_over_compliant.badge.tooltip.learn_more')} + +
+ ); +} 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 { constructor(props: Props) { super(props); @@ -103,6 +118,8 @@ export class ConditionComponent extends React.PureComponent { isCaycModal = false, } = this.props; + const isCaycCompliantAndOverCompliant = qualityGate.caycStatus !== CaycStatus.NonCompliant; + return ( @@ -119,15 +136,25 @@ export class ConditionComponent extends React.PureComponent { metric={metric} isCaycModal={isCaycModal} condition={condition} - isCaycCompliant={qualityGate.isCaycCompliant} + isCaycCompliantAndOverCompliant={isCaycCompliantAndOverCompliant} /> + {!isCaycCondition(condition) && isCaycCompliantAndOverCompliant && ( + + } + mouseLeaveDelay={TOOLTIP_MOUSE_LEAVE_DELAY} + > + + + + )} {!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)) && ( <> { )} )} - {(!qualityGate.isCaycCompliant || (qualityGate.isCaycCompliant && showEdit)) && ( + {(!isCaycCompliantAndOverCompliant || + !isCaycCondition(condition) || + (isCaycCompliantAndOverCompliant && showEdit)) && ( <> { const { conditions, qualityGate } = this.props; const promiseArr: Promise[] = []; - 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 (

-

+ + {sortedMissingConditions.length > 0 && ( + <> +

+ {translateWithParameters( + 'quality_gates.cayc.review_update_modal.add_condition.header', + sortedMissingConditions.length + )} +

+ + + )} + + {sortedWeakConditions.length > 0 && ( + <> +

+ {translateWithParameters( + 'quality_gates.cayc.review_update_modal.modify_condition.header', + sortedWeakConditions.length + )} +

+ + + )} + +

{translate('quality_gates.cayc.review_update_modal.description2')} -

-

- {translate('quality_gates.conditions.new_code', 'long')} -

- +
); 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 ( <> - + {isToBeModified && ( + + {formatMeasure(condition.error, metric.type)} + + )} + {formatMeasure(getCorrectCaycCondition(condition).error, metric.type)} - + ); } @@ -45,7 +62,7 @@ function ConditionValue({ condition, isCaycModal, metric, isCaycCompliant }: Pro return ( <> {formatMeasure(condition.error, metric.type)} - {isCaycCompliant && isCaycCondition(condition) && ( + {isCaycCompliantAndOverCompliant && isCaycCondition(condition) && ( )} 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 { constructor(props: Props) { super(props); this.state = { - unlockEditing: !props.qualityGate.isCaycCompliant, + unlockEditing: props.qualityGate.caycStatus === CaycStatus.NonCompliant, }; } componentDidUpdate(prevProps: Readonly): 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 { return (
- {qualityGate.isCaycCompliant && ( + {qualityGate.caycStatus !== CaycStatus.NonCompliant && (

{translate('quality_gates.cayc.banner.title')} @@ -180,7 +186,7 @@ export class Conditions extends React.PureComponent { ), new_code_link: ( - + {translate('quality_gates.cayc.new_code')} ), @@ -197,7 +203,29 @@ export class Conditions extends React.PureComponent { )} - {!qualityGate.isCaycCompliant && ( + {qualityGate.caycStatus === CaycStatus.OverCompliant && ( + +

+ {translate('quality_gates.cayc_over_compliant.banner.title')} +

+

{translate('quality_gates.cayc_over_compliant.banner.description1')}

+
+ + {translate('quality_gates.cayc_over_compliant.banner.link')} + + ), + }} + /> +
+
+ )} + + {qualityGate.caycStatus === CaycStatus.NonCompliant && (

{translate('quality_gates.cayc_missing.banner.title')} @@ -227,18 +255,17 @@ export class Conditions extends React.PureComponent { )} - {(!qualityGate.isCaycCompliant || (qualityGate.isCaycCompliant && unlockEditing)) && - canEdit && ( -
- - {({ onClick }) => ( - - )} - -
- )} + {(qualityGate.caycStatus === CaycStatus.NonCompliant || unlockEditing) && canEdit && ( +
+ + {({ onClick }) => ( + + )} + +
+ )}

{translate('quality_gates.conditions')}

@@ -314,7 +341,7 @@ export class Conditions extends React.PureComponent {

)} - {qualityGate.isCaycCompliant && !unlockEditing && canEdit && ( + {qualityGate.caycStatus !== CaycStatus.NonCompliant && !unlockEditing && canEdit && (

{ 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 { } 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 { } 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 0276c9f7df2..0ed90728d12 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 { render() { const { qualityGate } = this.props; const actions = qualityGate.actions || ({} as any); - const { isCaycCompliant } = qualityGate; return (

@@ -73,7 +72,7 @@ export default class DetailsHeader extends React.PureComponent {

{qualityGate.name}

{qualityGate.isBuiltIn && } - {!qualityGate.isCaycCompliant && ( + {qualityGate.caycStatus === CaycStatus.NonCompliant && ( } mouseLeaveDelay={TOOLTIP_MOUSE_LEAVE_DELAY}> @@ -111,7 +110,9 @@ export default class DetailsHeader extends React.PureComponent { {({ onClick }) => ( @@ -119,7 +120,7 @@ export default class DetailsHeader extends React.PureComponent { className="little-spacer-left" id="quality-gate-copy" onClick={onClick} - disabled={!isCaycCompliant} + disabled={qualityGate.caycStatus === CaycStatus.NonCompliant} > {translate('copy')} @@ -130,13 +131,15 @@ export default class DetailsHeader extends React.PureComponent { {actions.setAsDefault && (