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