]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20993 Add option to optimize quality gates CaYC compliant without 0 issues...
authorAndrey Luiz <andrey.luiz@sonarsource.com>
Mon, 13 Nov 2023 09:34:28 +0000 (10:34 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 15 Nov 2023 09:11:03 +0000 (09:11 +0000)
server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts
server/sonar-web/src/main/js/apps/quality-gates/components/CaycCompliantBanner.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditionsListItem.tsx [deleted file]
server/sonar-web/src/main/js/apps/quality-gates/components/CaycFixOptimizeBanner.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ConditionValueDescription.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
server/sonar-web/src/main/js/apps/quality-gates/utils.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 6aedf5dcd258f509473dbbaec0e3943ac3a9ac07..5b4ce44f7e21539e0fbd416b2b0ddfba65bc2ec5 100644 (file)
@@ -125,6 +125,13 @@ export class QualityGatesServiceMock {
       mockQualityGate({
         name: 'SonarSource way - CFamily',
         conditions: [
+          {
+            id: 'AXJMbIUHPAOIsUIE3eOi',
+            metric: 'new_security_hotspots_reviewed',
+            op: 'LT',
+            error: '85',
+            isCaycCondition: true,
+          },
           {
             id: 'AXJMbIUHPAOIsUIE3eOu',
             metric: 'new_coverage',
@@ -187,6 +194,17 @@ export class QualityGatesServiceMock {
         isBuiltIn: false,
         caycStatus: CaycStatus.NonCompliant,
       }),
+      mockQualityGate({
+        name: 'Non Cayc Compliant QG',
+        conditions: [
+          { id: 'AXJMbIUHPAOIsUIE3eNs', metric: 'new_security_rating', op: 'GT', error: '1' },
+          { id: 'AXJMbIUHPAOIsUIE3eOD', metric: 'new_reliability_rating', op: 'GT', error: '1' },
+          { id: 'AXJMbIUHPAOIsUIE3eOF', metric: 'new_coverage', op: 'LT', error: '80' },
+        ],
+        isDefault: false,
+        isBuiltIn: false,
+        caycStatus: CaycStatus.Compliant,
+      }),
       mockQualityGate({
         name: 'Over Compliant CAYC QG',
         conditions: [
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycCompliantBanner.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycCompliantBanner.tsx
new file mode 100644 (file)
index 0000000..d9b9d6d
--- /dev/null
@@ -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 {
+  CardWithPrimaryBackground,
+  CheckIcon,
+  LightLabel,
+  SubHeadingHighlight,
+} from 'design-system';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import DocumentationLink from '../../../components/common/DocumentationLink';
+import { translate } from '../../../helpers/l10n';
+import { OPTIMIZED_CAYC_CONDITIONS } from '../utils';
+
+export default function CaycCompliantBanner() {
+  return (
+    <CardWithPrimaryBackground className="sw-mb-9 sw-p-8">
+      <SubHeadingHighlight className="sw-mb-2">
+        {translate('quality_gates.cayc.banner.title')}
+      </SubHeadingHighlight>
+
+      <div>
+        <FormattedMessage
+          id="quality_gates.cayc.banner.description1"
+          values={{
+            cayc_link: (
+              <DocumentationLink to="/user-guide/clean-as-you-code/">
+                {translate('quality_gates.cayc')}
+              </DocumentationLink>
+            ),
+          }}
+        />
+      </div>
+      <div className="sw-my-2">{translate('quality_gates.cayc.banner.description2')}</div>
+      <ul className="sw-body-sm sw-flex sw-flex-col sw-gap-2">
+        {Object.values(OPTIMIZED_CAYC_CONDITIONS).map((condition) => (
+          <li key={condition.metric}>
+            <CheckIcon className="sw-mr-1 sw-pt-1/2" />
+            <LightLabel>{translate(`metric.${condition.metric}.description.positive`)}</LightLabel>
+          </li>
+        ))}
+      </ul>
+    </CardWithPrimaryBackground>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditionsListItem.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycConditionsListItem.tsx
deleted file mode 100644 (file)
index 0b61159..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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 { CheckIcon, LightLabel } from 'design-system';
-import * as React from 'react';
-import { translate } from '../../../helpers/l10n';
-
-export default function CaycConditionsListItem({ metricKey }: Readonly<{ metricKey: string }>) {
-  return (
-    <li>
-      <CheckIcon className="sw-mr-1 sw-pt-1/2" />
-      <LightLabel>{translate(`metric.${metricKey}.description.positive`)}</LightLabel>
-    </li>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CaycFixOptimizeBanner.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CaycFixOptimizeBanner.tsx
new file mode 100644 (file)
index 0000000..b388cc2
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * 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 { ButtonPrimary, CardWithPrimaryBackground, SubHeadingHighlight } from 'design-system';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import DocumentationLink from '../../../components/common/DocumentationLink';
+import ModalButton from '../../../components/controls/ModalButton';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  renderCaycModal: ({ onClose }: { onClose: () => void }) => React.ReactNode;
+  isOptimizing?: boolean;
+}
+
+export default function CaycNonCompliantBanner({ renderCaycModal, isOptimizing }: Readonly<Props>) {
+  return (
+    <CardWithPrimaryBackground className="sw-mb-9 sw-p-8">
+      <SubHeadingHighlight className="sw-mb-2">
+        {translate(
+          isOptimizing
+            ? 'quality_gates.cayc_optimize.banner.title'
+            : 'quality_gates.cayc_missing.banner.title',
+        )}
+      </SubHeadingHighlight>
+      <div>
+        <FormattedMessage
+          id={
+            isOptimizing
+              ? 'quality_gates.cayc_optimize.banner.description'
+              : 'quality_gates.cayc_missing.banner.description'
+          }
+          values={{
+            cayc_link: (
+              <DocumentationLink to="/user-guide/clean-as-you-code/">
+                {translate('quality_gates.cayc')}
+              </DocumentationLink>
+            ),
+          }}
+        />
+      </div>
+      <ModalButton modal={renderCaycModal}>
+        {({ onClick }) => (
+          <ButtonPrimary className="sw-mt-4" onClick={onClick}>
+            {translate(
+              isOptimizing
+                ? 'quality_gates.cayc_condition.review_optimize'
+                : 'quality_gates.cayc_condition.review_update',
+            )}
+          </ButtonPrimary>
+        )}
+      </ModalButton>
+    </CardWithPrimaryBackground>
+  );
+}
index 384f664ca90d0a6123bc818eee4606d3761c49c8..63b9d78ca1487964051837c2fc116ce84cecb8bb 100644 (file)
@@ -40,6 +40,7 @@ interface Props {
   onSaveCondition: (newCondition: Condition, oldCondition: Condition) => void;
   lockEditing: () => void;
   qualityGate: QualityGate;
+  isOptimizing?: boolean;
 }
 
 export default function CaycReviewUpdateConditionsModal(props: Readonly<Props>) {
@@ -51,6 +52,7 @@ export default function CaycReviewUpdateConditionsModal(props: Readonly<Props>)
     onAddCondition,
     lockEditing,
     onClose,
+    isOptimizing,
   } = props;
 
   const { weakConditions, missingConditions } = getWeakMissingAndNonCaycConditions(conditions);
@@ -108,8 +110,11 @@ export default function CaycReviewUpdateConditionsModal(props: Readonly<Props>)
     <div className="sw-mb-10">
       <SubHeading as="p" className="sw-body-sm">
         <FormattedMessage
-          id="quality_gates.cayc.review_update_modal.description1"
-          defaultMessage={translate('quality_gates.cayc.review_update_modal.description1')}
+          id={
+            isOptimizing
+              ? 'quality_gates.cayc.review_optimize_modal.description1'
+              : 'quality_gates.cayc.review_update_modal.description1'
+          }
           values={{
             cayc_link: (
               <Link to={getDocUrl('/user-guide/clean-as-you-code/')}>
@@ -164,7 +169,9 @@ export default function CaycReviewUpdateConditionsModal(props: Readonly<Props>)
     <Modal
       isLarge
       headerTitle={translateWithParameters(
-        'quality_gates.cayc.review_update_modal.header',
+        isOptimizing
+          ? 'quality_gates.cayc.review_optimize_modal.header'
+          : 'quality_gates.cayc.review_update_modal.header',
         qualityGate.name,
       )}
       onClose={onClose}
@@ -176,7 +183,11 @@ export default function CaycReviewUpdateConditionsModal(props: Readonly<Props>)
           type="submit"
           onClick={updateCaycQualityGate}
         >
-          {translate('quality_gates.cayc.review_update_modal.confirm_text')}
+          {translate(
+            isOptimizing
+              ? 'quality_gates.cayc.review_optimize_modal.confirm_text'
+              : 'quality_gates.cayc.review_update_modal.confirm_text',
+          )}
         </ButtonPrimary>
       }
       secondaryButtonLabel={translate('close')}
index 64d7d220d11a200deb494a76a9be8426d2bea1de..61673b91d88c196a0d5fd86b24166563c76d3a1a 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { translate } from '../../../helpers/l10n';
 import { formatMeasure } from '../../../helpers/measures';
-import {
-  GRID_INDEX_OFFSET,
-  PERCENT_MULTIPLIER,
-  getMaintainabilityGrid,
-} from '../../../helpers/ratings';
-import { AppState } from '../../../types/appstate';
-import { MetricKey, MetricType } from '../../../types/metrics';
-import { GlobalSettingKeys } from '../../../types/settings';
+import { MetricKey } from '../../../types/metrics';
 import { Condition, Metric } from '../../../types/types';
 import { GreenColorText } from './ConditionValue';
 
@@ -37,50 +29,22 @@ const NO_DESCRIPTION_CONDITION = [
   MetricKey.new_security_hotspots_reviewed,
   MetricKey.new_coverage,
   MetricKey.new_duplicated_lines_density,
+  MetricKey.new_reliability_rating,
+  MetricKey.new_security_rating,
+  MetricKey.new_maintainability_rating,
 ];
 
 interface Props {
-  appState: AppState;
   condition: Condition;
   metric: Metric;
   isToBeModified?: boolean;
 }
 
-function ConditionValueDescription({
+export default function ConditionValueDescription({
   condition,
-  appState: { settings },
   metric,
   isToBeModified = false,
 }: Readonly<Props>) {
-  if (condition.metric === MetricKey.new_maintainability_rating) {
-    const maintainabilityGrid = getMaintainabilityGrid(
-      settings[GlobalSettingKeys.RatingGrid] ?? '',
-    );
-    const maintainabilityRatingThreshold =
-      maintainabilityGrid[Math.floor(Number(condition.error)) - GRID_INDEX_OFFSET];
-    const ratingLetter = formatMeasure(condition.error, MetricType.Rating);
-
-    return (
-      <GreenColorText isToBeModified={isToBeModified}>
-        (
-        {condition.error === '1'
-          ? translateWithParameters(
-              'quality_gates.cayc.new_maintainability_rating.A',
-              formatMeasure(maintainabilityGrid[0] * PERCENT_MULTIPLIER, MetricType.Percent),
-            )
-          : translateWithParameters(
-              'quality_gates.cayc.new_maintainability_rating',
-              ratingLetter,
-              formatMeasure(
-                maintainabilityRatingThreshold * PERCENT_MULTIPLIER,
-                MetricType.Percent,
-              ),
-            )}
-        )
-      </GreenColorText>
-    );
-  }
-
   return (
     <GreenColorText isToBeModified={isToBeModified}>
       {condition.isCaycCondition &&
@@ -99,5 +63,3 @@ function ConditionValueDescription({
     </GreenColorText>
   );
 }
-
-export default withAppStateContext(ConditionValueDescription);
index cf714dbaaf5857307c8095302491b224b01df355..c3b9ec5c0521d937f16d185064839110b29f3e44 100644 (file)
@@ -19,9 +19,7 @@
  */
 
 import {
-  ButtonPrimary,
   ButtonSecondary,
-  CardWithPrimaryBackground,
   FlagMessage,
   HeadingDark,
   HelperHintIcon,
@@ -29,7 +27,6 @@ import {
   Link,
   Note,
   SubHeading,
-  SubHeadingHighlight,
 } from 'design-system';
 import { differenceWith, map, uniqBy } from 'lodash';
 import * as React from 'react';
@@ -51,10 +48,11 @@ import {
   Metric,
   QualityGate,
 } from '../../../types/types';
-import { CAYC_CONDITIONS, groupAndSortByPriorityConditions } from '../utils';
+import { groupAndSortByPriorityConditions, isQualityGateOptimized } from '../utils';
 import CaYCConditionsSimplificationGuide from './CaYCConditionsSimplificationGuide';
-import CaycConditionsListItem from './CaycConditionsListItem';
+import CaycCompliantBanner from './CaycCompliantBanner';
 import CaycConditionsTable from './CaycConditionsTable';
+import CaycFixOptimizeBanner from './CaycFixOptimizeBanner';
 import ConditionModal from './ConditionModal';
 import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal';
 import ConditionsTable from './ConditionsTable';
@@ -76,7 +74,7 @@ const FORBIDDEN_METRICS: string[] = [
   MetricKey.new_security_hotspots,
 ];
 
-export function Conditions({
+function Conditions({
   qualityGate,
   metrics,
   onRemoveCondition,
@@ -109,8 +107,6 @@ export function Conditions({
     metric: metrics[condition.metric],
   }));
 
-  const getDocUrl = useDocUrl();
-
   // set edit only when the name is change
   // i.e when user changes the quality gate
   React.useEffect(() => {
@@ -143,6 +139,11 @@ export function Conditions({
     [metrics, qualityGate, onAddCondition],
   );
 
+  const getDocUrl = useDocUrl();
+  const isCompliantCustomQualityGate =
+    qualityGate.caycStatus !== CaycStatus.NonCompliant && !qualityGate.isBuiltIn;
+  const isOptimizing = isCompliantCustomQualityGate && !isQualityGateOptimized(qualityGate);
+
   const renderCaycModal = React.useCallback(
     ({ onClose }: ModalProps) => {
       const { conditions = [] } = qualityGate;
@@ -160,71 +161,31 @@ export function Conditions({
           conditions={conditions}
           scope="new-cayc"
           onClose={onClose}
+          isOptimizing={isOptimizing}
         />
       );
     },
-    [qualityGate, metrics, updatedConditionId, onAddCondition, onRemoveCondition, onSaveCondition],
+    [
+      qualityGate,
+      metrics,
+      updatedConditionId,
+      onAddCondition,
+      onRemoveCondition,
+      onSaveCondition,
+      isOptimizing,
+    ],
   );
 
   return (
     <div>
       <CaYCConditionsSimplificationGuide />
 
-      {qualityGate.caycStatus !== CaycStatus.NonCompliant && !qualityGate.isBuiltIn && (
-        <CardWithPrimaryBackground className="sw-mb-9 sw-p-8">
-          <SubHeadingHighlight className="sw-mb-2">
-            {translate('quality_gates.cayc.banner.title')}
-          </SubHeadingHighlight>
-
-          <div>
-            <FormattedMessage
-              id="quality_gates.cayc.banner.description1"
-              defaultMessage={translate('quality_gates.cayc.banner.description1')}
-              values={{
-                cayc_link: (
-                  <Link to={getDocUrl('/user-guide/clean-as-you-code/')}>
-                    {translate('quality_gates.cayc')}
-                  </Link>
-                ),
-              }}
-            />
-          </div>
-          <div className="sw-my-2">{translate('quality_gates.cayc.banner.description2')}</div>
-          <ul className="sw-body-sm sw-flex sw-flex-col sw-gap-2">
-            {Object.values(CAYC_CONDITIONS).map((condition) => (
-              <CaycConditionsListItem key={condition.metric} metricKey={condition.metric} />
-            ))}
-          </ul>
-        </CardWithPrimaryBackground>
+      {isCompliantCustomQualityGate && !isOptimizing && <CaycCompliantBanner />}
+      {isCompliantCustomQualityGate && isOptimizing && canEdit && (
+        <CaycFixOptimizeBanner renderCaycModal={renderCaycModal} isOptimizing />
       )}
       {qualityGate.caycStatus === CaycStatus.NonCompliant && canEdit && (
-        <CardWithPrimaryBackground className="sw-mb-9 sw-p-8">
-          <SubHeadingHighlight className="sw-mb-2">
-            {translate('quality_gates.cayc_missing.banner.title')}
-          </SubHeadingHighlight>
-          <div>
-            <FormattedMessage
-              id="quality_gates.cayc_missing.banner.description"
-              defaultMessage={translate('quality_gates.cayc_missing.banner.description')}
-              values={{
-                cayc_link: (
-                  <Link to={getDocUrl('/user-guide/clean-as-you-code/')}>
-                    {translate('quality_gates.cayc')}
-                  </Link>
-                ),
-              }}
-            />
-          </div>
-          {canEdit && (
-            <ModalButton modal={renderCaycModal}>
-              {({ onClick }) => (
-                <ButtonPrimary className="sw-mt-4" onClick={onClick}>
-                  {translate('quality_gates.cayc_condition.review_update')}
-                </ButtonPrimary>
-              )}
-            </ModalButton>
-          )}
-        </CardWithPrimaryBackground>
+        <CaycFixOptimizeBanner renderCaycModal={renderCaycModal} />
       )}
 
       <header className="sw-flex sw-items-center sw-mb-4 sw-justify-between">
index ce39c5ec511622f9fe6245cfd3474cc59d5ce448..f6e563cc4225b6523be4ffc371d0b14b2ebfc9b9 100644 (file)
@@ -379,6 +379,44 @@ it('should show warning banner when CAYC condition is not properly set and shoul
   expect(overallConditionsWrapper.getByText('Complexity / Function')).toBeInTheDocument();
 });
 
+it('should show optimize banner when CAYC condition is not properly set and QG is compliant and should be able to update them', async () => {
+  const user = userEvent.setup();
+  qualityGateHandler.setIsAdmin(true);
+  renderQualityGateApp();
+
+  const qualityGate = await screen.findByText('Non Cayc Compliant QG');
+
+  await user.click(qualityGate);
+
+  expect(screen.getByText('quality_gates.cayc_optimize.banner.title')).toBeInTheDocument();
+  expect(screen.getByText('quality_gates.cayc_optimize.banner.description')).toBeInTheDocument();
+  expect(
+    screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_optimize' }),
+  ).toBeInTheDocument();
+
+  await user.click(
+    screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_optimize' }),
+  );
+  expect(
+    screen.getByRole('dialog', {
+      name: 'quality_gates.cayc.review_optimize_modal.header.Non Cayc Compliant QG',
+    }),
+  ).toBeInTheDocument();
+  expect(
+    screen.getByText('quality_gates.cayc.review_optimize_modal.description1'),
+  ).toBeInTheDocument();
+  expect(
+    screen.getByText('quality_gates.cayc.review_update_modal.description2'),
+  ).toBeInTheDocument();
+  expect(
+    screen.getByRole('button', { name: 'quality_gates.cayc.review_optimize_modal.confirm_text' }),
+  ).toBeInTheDocument();
+
+  await user.click(
+    screen.getByRole('button', { name: 'quality_gates.cayc.review_optimize_modal.confirm_text' }),
+  );
+});
+
 it('should not warn user when quality gate is not CAYC compliant and user has no permission to edit it', async () => {
   const user = userEvent.setup();
   renderQualityGateApp();
@@ -391,6 +429,20 @@ it('should not warn user when quality gate is not CAYC compliant and user has no
   expect(screen.queryByText('quality_gates.cayc.tooltip.message')).not.toBeInTheDocument();
 });
 
+it('should not show optimize banner when quality gate is compliant but non-CaYC and user has no permission to edit it', async () => {
+  const user = userEvent.setup();
+  renderQualityGateApp();
+
+  const nonCompliantQualityGate = await screen.findByRole('button', {
+    name: 'Non Cayc Compliant QG',
+  });
+
+  await user.click(nonCompliantQualityGate);
+
+  expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+  expect(screen.queryByText('quality_gates.cayc.tooltip.message')).not.toBeInTheDocument();
+});
+
 it('should warn user when quality gate is not CAYC compliant and user has permission to edit it', async () => {
   const user = userEvent.setup();
   qualityGateHandler.setIsAdmin(true);
@@ -399,11 +451,28 @@ it('should warn user when quality gate is not CAYC compliant and user has permis
   const nonCompliantQualityGate = await screen.findByRole('button', { name: /Non Cayc QG/ });
 
   await user.click(nonCompliantQualityGate);
+  // expect(screen.getByTestId('conditions')).toMatchSnapshot();
 
   expect(await screen.findByText(/quality_gates.cayc_missing.banner.title/)).toBeInTheDocument();
   expect(screen.getAllByText('quality_gates.cayc.tooltip.message').length).toBeGreaterThan(0);
 });
 
+it('should show optimize banner when quality gate is compliant but non-CaYC and user has permission to edit it', async () => {
+  const user = userEvent.setup();
+  qualityGateHandler.setIsAdmin(true);
+  renderQualityGateApp();
+
+  const nonCompliantQualityGate = await screen.findByRole('button', {
+    name: /Non Cayc Compliant QG/,
+  });
+
+  await user.click(nonCompliantQualityGate);
+  // expect(screen.getByTestId('conditions')).toMatchSnapshot();
+
+  expect(await screen.findByText(/quality_gates.cayc_optimize.banner.title/)).toBeInTheDocument();
+  expect(screen.getAllByText('quality_gates.cayc.tooltip.message').length).toBeGreaterThan(0);
+});
+
 it('should render CaYC conditions on a separate table if Sonar way', async () => {
   const user = userEvent.setup();
   qualityGateHandler.setIsAdmin(true);
index 31ad1ffdae7f21a72038d3b6b314a651cb02a439..3376cbb2267408a38fb479147fa0be6a5b9fb71d 100644 (file)
@@ -28,23 +28,25 @@ interface GroupedByMetricConditions {
   caycConditions: Condition[];
 }
 
-type CaycMetricKeys =
-  | MetricKey.new_violations
+type CommonCaycMetricKeys =
   | MetricKey.new_security_hotspots_reviewed
   | MetricKey.new_coverage
   | MetricKey.new_duplicated_lines_density;
 
-export const CAYC_CONDITIONS: Record<
-  CaycMetricKeys,
+type OptimizedCaycMetricKeys = MetricKey.new_violations | CommonCaycMetricKeys;
+
+type UnoptimizedCaycMetricKeys =
+  | MetricKey.new_reliability_rating
+  | MetricKey.new_security_rating
+  | MetricKey.new_maintainability_rating
+  | CommonCaycMetricKeys;
+
+type AllCaycMetricKeys = OptimizedCaycMetricKeys | UnoptimizedCaycMetricKeys;
+
+const COMMON_CONDITIONS: Record<
+  CommonCaycMetricKeys,
   Condition & { shouldRenderOperator?: boolean }
 > = {
-  [MetricKey.new_violations]: {
-    id: MetricKey.new_violations,
-    metric: MetricKey.new_violations,
-    op: 'GT',
-    error: '0',
-    isCaycCondition: true,
-  },
   [MetricKey.new_security_hotspots_reviewed]: {
     id: MetricKey.new_security_hotspots_reviewed,
     metric: MetricKey.new_security_hotspots_reviewed,
@@ -70,6 +72,56 @@ export const CAYC_CONDITIONS: Record<
   },
 };
 
+export const OPTIMIZED_CAYC_CONDITIONS: Record<
+  OptimizedCaycMetricKeys,
+  Condition & { shouldRenderOperator?: boolean }
+> = {
+  [MetricKey.new_violations]: {
+    id: MetricKey.new_violations,
+    metric: MetricKey.new_violations,
+    op: 'GT',
+    error: '0',
+    isCaycCondition: true,
+  },
+  ...COMMON_CONDITIONS,
+};
+
+const UNOPTIMIZED_CAYC_CONDITIONS: Record<
+  UnoptimizedCaycMetricKeys,
+  Condition & { shouldRenderOperator?: boolean }
+> = {
+  [MetricKey.new_reliability_rating]: {
+    id: MetricKey.new_reliability_rating,
+    metric: MetricKey.new_reliability_rating,
+    op: 'GT',
+    error: '1',
+    isCaycCondition: true,
+  },
+  [MetricKey.new_security_rating]: {
+    id: MetricKey.new_security_rating,
+    metric: MetricKey.new_security_rating,
+    op: 'GT',
+    error: '1',
+    isCaycCondition: true,
+  },
+  [MetricKey.new_maintainability_rating]: {
+    id: MetricKey.new_maintainability_rating,
+    metric: MetricKey.new_maintainability_rating,
+    op: 'GT',
+    error: '1',
+    isCaycCondition: true,
+  },
+  ...COMMON_CONDITIONS,
+};
+
+const ALL_CAYC_CONDITIONS: Record<
+  AllCaycMetricKeys,
+  Condition & { shouldRenderOperator?: boolean }
+> = {
+  ...OPTIMIZED_CAYC_CONDITIONS,
+  ...UNOPTIMIZED_CAYC_CONDITIONS,
+};
+
 const CAYC_CONDITION_ORDER_PRIORITIES: Dict<number> = [
   MetricKey.new_violations,
   MetricKey.new_security_hotspots_reviewed,
@@ -79,26 +131,50 @@ const CAYC_CONDITION_ORDER_PRIORITIES: Dict<number> = [
   .reverse()
   .reduce((acc, key, i) => ({ ...acc, [key.toString()]: i + 1 }), {} as Dict<number>);
 
-const CAYC_CONDITIONS_WITHOUT_FIXED_VALUE: CaycMetricKeys[] = [
+const CAYC_CONDITIONS_WITHOUT_FIXED_VALUE: AllCaycMetricKeys[] = [
   MetricKey.new_duplicated_lines_density,
   MetricKey.new_coverage,
 ];
-const CAYC_CONDITIONS_WITH_FIXED_VALUE: CaycMetricKeys[] = [
+const CAYC_CONDITIONS_WITH_FIXED_VALUE: AllCaycMetricKeys[] = [
   MetricKey.new_security_hotspots_reviewed,
   MetricKey.new_violations,
+  MetricKey.new_reliability_rating,
+  MetricKey.new_security_rating,
+  MetricKey.new_maintainability_rating,
 ];
 
 export function isConditionWithFixedValue(condition: Condition) {
-  return CAYC_CONDITIONS_WITH_FIXED_VALUE.includes(condition.metric as CaycMetricKeys);
+  return CAYC_CONDITIONS_WITH_FIXED_VALUE.includes(condition.metric as OptimizedCaycMetricKeys);
 }
 
 export function getCaycConditionMetadata(condition: Condition) {
-  const foundCondition = CAYC_CONDITIONS[condition.metric as CaycMetricKeys];
+  const foundCondition = OPTIMIZED_CAYC_CONDITIONS[condition.metric as OptimizedCaycMetricKeys];
   return {
     shouldRenderOperator: foundCondition?.shouldRenderOperator,
   };
 }
 
+export function isQualityGateOptimized(qualityGate: QualityGate) {
+  return (
+    !qualityGate.isBuiltIn &&
+    qualityGate.caycStatus !== CaycStatus.NonCompliant &&
+    Object.values(OPTIMIZED_CAYC_CONDITIONS).every((condition) => {
+      const foundCondition = qualityGate.conditions?.find((c) => c.metric === condition.metric);
+      return (
+        foundCondition &&
+        !isWeakCondition(condition.metric as OptimizedCaycMetricKeys, foundCondition)
+      );
+    })
+  );
+}
+
+function isWeakCondition(key: AllCaycMetricKeys, selectedCondition: Condition) {
+  return (
+    !CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(key) &&
+    ALL_CAYC_CONDITIONS[key]?.error !== selectedCondition.error
+  );
+}
+
 export function getWeakMissingAndNonCaycConditions(conditions: Condition[]) {
   const result: {
     weakConditions: Condition[];
@@ -107,31 +183,18 @@ export function getWeakMissingAndNonCaycConditions(conditions: Condition[]) {
     weakConditions: [],
     missingConditions: [],
   };
-  Object.keys(CAYC_CONDITIONS).forEach((key: CaycMetricKeys) => {
+  Object.keys(OPTIMIZED_CAYC_CONDITIONS).forEach((key: OptimizedCaycMetricKeys) => {
     const selectedCondition = conditions.find((condition) => condition.metric === key);
     if (!selectedCondition) {
-      result.missingConditions.push(CAYC_CONDITIONS[key]);
-    } else if (
-      !CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(key) &&
-      CAYC_CONDITIONS[key]?.error !== selectedCondition.error
-    ) {
+      result.missingConditions.push(OPTIMIZED_CAYC_CONDITIONS[key]);
+    } else if (isWeakCondition(key, selectedCondition)) {
       result.weakConditions.push(selectedCondition);
     }
   });
   return result;
 }
 
-export function getCaycConditionsWithCorrectValue(conditions: Condition[]) {
-  return Object.keys(CAYC_CONDITIONS).map((key: CaycMetricKeys) => {
-    const selectedCondition = conditions.find((condition) => condition.metric === key);
-    if (CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(key) && selectedCondition) {
-      return selectedCondition;
-    }
-    return CAYC_CONDITIONS[key];
-  });
-}
-
-export function groupConditionsByMetric(
+function groupConditionsByMetric(
   conditions: Condition[],
   isBuiltInQG = false,
 ): GroupedByMetricConditions {
@@ -183,11 +246,11 @@ export function groupAndSortByPriorityConditions(
 }
 
 export function getCorrectCaycCondition(condition: Condition) {
-  const conditionMetric = condition.metric as CaycMetricKeys;
+  const conditionMetric = condition.metric as OptimizedCaycMetricKeys;
   if (CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(conditionMetric)) {
     return condition;
   }
-  return CAYC_CONDITIONS[conditionMetric];
+  return OPTIMIZED_CAYC_CONDITIONS[conditionMetric];
 }
 
 export function addCondition(qualityGate: QualityGate, condition: Condition): QualityGate {
@@ -225,32 +288,27 @@ export function replaceCondition(
   return { ...qualityGate, conditions };
 }
 
-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 caycMetric = key as CaycMetricKeys;
-    const selectedCondition = conditions.find((condition) => condition.metric === key);
-    if (!selectedCondition) {
-      return CaycStatus.NonCompliant;
-    }
-
-    if (
-      !CAYC_CONDITIONS_WITHOUT_FIXED_VALUE.includes(caycMetric) &&
-      selectedCondition &&
-      selectedCondition.error !== CAYC_CONDITIONS[caycMetric].error
-    ) {
-      return CaycStatus.NonCompliant;
-    }
-  }
+function updateCaycCompliantStatus(conditions: Condition[]) {
+  const isCompliantOptimized = Object.values(OPTIMIZED_CAYC_CONDITIONS).every((condition) => {
+    const foundCondition = conditions.find((c) => c.metric === condition.metric);
+    return (
+      foundCondition &&
+      !isWeakCondition(condition.metric as OptimizedCaycMetricKeys, foundCondition)
+    );
+  });
+  const isCompliantUnoptimized = Object.values(UNOPTIMIZED_CAYC_CONDITIONS).every((condition) => {
+    const foundCondition = conditions.find((c) => c.metric === condition.metric);
+    return (
+      foundCondition &&
+      !isWeakCondition(condition.metric as UnoptimizedCaycMetricKeys, foundCondition)
+    );
+  });
 
-  if (conditions.length > Object.keys(CAYC_CONDITIONS).length) {
-    return CaycStatus.OverCompliant;
+  if (isCompliantOptimized || isCompliantUnoptimized) {
+    return CaycStatus.Compliant;
   }
 
-  return CaycStatus.Compliant;
+  return CaycStatus.NonCompliant;
 }
 
 export function getPossibleOperators(metric: Metric) {
index f24fae9f333c14522cc6931b30e084570e85adc7..26c7c19db9e8cd5d964d396215cf30145b37191a 100644 (file)
@@ -2221,6 +2221,12 @@ quality_gates.cayc.review_update_modal.header=Fix "{0}" to comply with Clean as
 quality_gates.cayc.review_update_modal.confirm_text=Fix Quality Gate
 quality_gates.cayc.review_update_modal.description1=This quality gate will be updated to comply with {cayc_link}. Please review the changes below.
 quality_gates.cayc.review_update_modal.description2=All other conditions will be preserved
+quality_gates.cayc_optimize.banner.title=This quality gate can be further optimized for Clean as You Code
+quality_gates.cayc_optimize.banner.description=This quality gate complies with the {cayc_link} methodology, but it can be further optimized to ensure that new code has 0 issues.
+quality_gates.cayc_condition.review_optimize=Review and Optimize Quality Gate
+quality_gates.cayc.review_optimize_modal.header=Optimize "{0}" for Clean as You Code
+quality_gates.cayc.review_optimize_modal.confirm_text=Optimize Quality Gate
+quality_gates.cayc.review_optimize_modal.description1=This quality gate will be optimized for {cayc_link}. Please review the changes below.
 quality_gates.cayc.condition_simplification_tour.page_1.title='Clean as You Code' ready!
 quality_gates.cayc.condition_simplification_tour.page_1.content1=The conditions in this quality gate have been updated to ensure that any code added or changed is clean.
 quality_gates.cayc.condition_simplification_tour.page_2.title=One condition, zero issues