]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23064 Advertise the 'Sonar way' Quality Gate as AI-ready
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>
Tue, 17 Sep 2024 09:56:12 +0000 (11:56 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 25 Sep 2024 20:02:52 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/quality-gates/components/AIGeneratedIcon.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/quality-gates/components/CaycCompliantBanner.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/CaycFixOptimizeBanner.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
server/sonar-web/src/main/js/helpers/doc-links.ts
server/sonar-web/src/main/js/types/features.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/AIGeneratedIcon.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/AIGeneratedIcon.tsx
new file mode 100644 (file)
index 0000000..e643adf
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 styled from '@emotion/styled';
+import { IconSparkle } from '@sonarsource/echoes-react';
+import { themeColor } from 'design-system';
+
+const AIGeneratedIcon = styled(IconSparkle)`
+  color: ${themeColor('primary')};
+`;
+
+export default AIGeneratedIcon;
index b57025d0bc9ab2dfa6fc640be0409f521c452ca3..9e81a10c4b7226f1b574436a178c4e0df1396661 100644 (file)
@@ -34,7 +34,7 @@ import QGRecommendedIcon from './QGRecommendedIcon';
 
 export default function CaycCompliantBanner() {
   return (
-    <CardWithPrimaryBackground className="sw-mb-9 sw-p-8">
+    <CardWithPrimaryBackground className="sw-mt-9 sw-p-8">
       <div className="sw-flex sw-items-center sw-mb-2">
         <QGRecommendedIcon className="sw-mr-2" />
         <SubHeadingHighlight className="sw-m-0">
index 8e895e6c682f652c958f9e348f9d4d1ad03e8c5a..ba5ca4d8856852781380668ca7cefce8be6eda8a 100644 (file)
@@ -34,7 +34,7 @@ interface Props {
 
 export default function CaycNonCompliantBanner({ renderCaycModal, isOptimizing }: Readonly<Props>) {
   return (
-    <CardWithPrimaryBackground className="sw-mb-9 sw-p-8">
+    <CardWithPrimaryBackground className="sw-mt-9 sw-p-8">
       <SubHeadingHighlight className="sw-mb-2">
         {translate(
           isOptimizing
index 208823ed7bf766d80b28f6cc17292632185ef9cd..c2fd8cf8712b81d2bf83f00a8be0fe55787ce7c2 100644 (file)
@@ -46,6 +46,7 @@ import { Feature } from '../../../types/features';
 import { CaycStatus, Condition as ConditionType, QualityGate } from '../../../types/types';
 import { groupAndSortByPriorityConditions, isQualityGateOptimized } from '../utils';
 import AddConditionModal from './AddConditionModal';
+import AIGeneratedIcon from './AIGeneratedIcon';
 import CaycCompliantBanner from './CaycCompliantBanner';
 import CaycCondition from './CaycCondition';
 import CaYCConditionsSimplificationGuide from './CaYCConditionsSimplificationGuide';
@@ -60,16 +61,18 @@ interface Props {
 }
 
 export default function Conditions({ qualityGate, isFetching }: Readonly<Props>) {
-  const [editing, setEditing] = React.useState<boolean>(
-    qualityGate.caycStatus === CaycStatus.NonCompliant,
-  );
-  const { name } = qualityGate;
+  const { name, isBuiltIn, actions, conditions = [], caycStatus } = qualityGate;
+
+  const [editing, setEditing] = React.useState<boolean>(caycStatus === CaycStatus.NonCompliant);
   const metrics = useMetrics();
-  const canEdit = Boolean(qualityGate.actions?.manageConditions);
-  const { conditions = [] } = qualityGate;
+  const { hasFeature } = useAvailableFeatures();
+
+  const canEdit = Boolean(actions?.manageConditions);
   const existingConditions = conditions.filter((condition) => metrics[condition.metric]);
   const { overallCodeConditions, newCodeConditions, caycConditions } =
-    groupAndSortByPriorityConditions(existingConditions, metrics, qualityGate.isBuiltIn);
+    groupAndSortByPriorityConditions(existingConditions, metrics, isBuiltIn);
+  const isAICodeAssuranceQualityGate =
+    hasFeature(Feature.AiCodeAssurance) && isBuiltIn && name === 'Sonar way';
 
   const duplicates: ConditionType[] = [];
   const savedConditions = existingConditions.filter((condition) => condition.id != null);
@@ -79,7 +82,6 @@ export default function Conditions({ qualityGate, isFetching }: Readonly<Props>)
       duplicates.push(condition);
     }
   });
-  const { hasFeature } = useAvailableFeatures();
 
   const uniqDuplicates = uniqBy(duplicates, (d) => d.metric).map((condition) => ({
     ...condition,
@@ -89,18 +91,15 @@ export default function Conditions({ qualityGate, isFetching }: Readonly<Props>)
   // set edit only when the name is change
   // i.e when user changes the quality gate
   React.useEffect(() => {
-    setEditing(qualityGate.caycStatus === CaycStatus.NonCompliant);
+    setEditing(caycStatus === CaycStatus.NonCompliant);
   }, [name]); // eslint-disable-line react-hooks/exhaustive-deps
 
   const docUrl = useDocUrl(DocLink.CaYC);
-  const isCompliantCustomQualityGate =
-    qualityGate.caycStatus !== CaycStatus.NonCompliant && !qualityGate.isBuiltIn;
+  const isCompliantCustomQualityGate = caycStatus !== CaycStatus.NonCompliant && !isBuiltIn;
   const isOptimizing = isCompliantCustomQualityGate && !isQualityGateOptimized(qualityGate);
 
   const renderCaycModal = React.useCallback(
     ({ onClose }: ModalProps) => {
-      const { conditions = [] } = qualityGate;
-      const canEdit = Boolean(qualityGate.actions?.manageConditions);
       return (
         <CaycReviewUpdateConditionsModal
           qualityGate={qualityGate}
@@ -114,15 +113,15 @@ export default function Conditions({ qualityGate, isFetching }: Readonly<Props>)
         />
       );
     },
-    [qualityGate, metrics, isOptimizing],
+    [qualityGate, conditions, metrics, isOptimizing, canEdit],
   );
 
   return (
     <div>
       <CaYCConditionsSimplificationGuide qualityGate={qualityGate} />
 
-      {qualityGate.isBuiltIn && (
-        <div className="sw-flex sw-items-center sw-mt-2 sw-mb-9">
+      {isBuiltIn && (
+        <div className="sw-flex sw-items-center">
           <QGRecommendedIcon className="sw-mr-1" />
           <LightLabel>
             <FormattedMessage
@@ -140,20 +139,39 @@ export default function Conditions({ qualityGate, isFetching }: Readonly<Props>)
         </div>
       )}
 
+      {isAICodeAssuranceQualityGate && (
+        <div className="sw-flex sw-items-center sw-mt-2">
+          <AIGeneratedIcon className="sw-mr-1" />
+          <LightLabel>
+            <FormattedMessage
+              defaultMessage="quality_gates.ai_generated.description"
+              id="quality_gates.ai_generated.description"
+              values={{
+                link: (
+                  <DocumentationLink to={DocLink.AiCodeAssurance}>
+                    {translate('quality_gates.ai_generated.description.clean_ai_generated_code')}
+                  </DocumentationLink>
+                ),
+              }}
+            />
+          </LightLabel>
+        </div>
+      )}
+
       {isCompliantCustomQualityGate && !isOptimizing && <CaycCompliantBanner />}
       {isCompliantCustomQualityGate && isOptimizing && canEdit && (
         <CaycFixOptimizeBanner renderCaycModal={renderCaycModal} isOptimizing />
       )}
-      {qualityGate.caycStatus === CaycStatus.NonCompliant && canEdit && (
+      {caycStatus === CaycStatus.NonCompliant && canEdit && (
         <CaycFixOptimizeBanner renderCaycModal={renderCaycModal} />
       )}
 
-      <header className="sw-flex sw-items-center sw-mb-4 sw-justify-between">
+      <header className="sw-flex sw-items-center sw-mt-9 sw-mb-4 sw-justify-between">
         <div className="sw-flex">
           <HeadingDark className="sw-typo-lg-semibold sw-m-0">
             {translate('quality_gates.conditions')}
           </HeadingDark>
-          {!qualityGate.isBuiltIn && (
+          {!isBuiltIn && (
             <DocHelpTooltip
               className="sw-ml-2"
               content={translate('quality_gates.conditions.help')}
@@ -170,7 +188,7 @@ export default function Conditions({ qualityGate, isFetching }: Readonly<Props>)
           <Spinner loading={isFetching} className="sw-ml-4 sw-mt-1" />
         </div>
         <div>
-          {(qualityGate.caycStatus === CaycStatus.NonCompliant || editing) && canEdit && (
+          {(caycStatus === CaycStatus.NonCompliant || editing) && canEdit && (
             <AddConditionModal qualityGate={qualityGate} />
           )}
         </div>
@@ -271,7 +289,7 @@ export default function Conditions({ qualityGate, isFetching }: Readonly<Props>)
         )}
       </div>
 
-      {qualityGate.caycStatus !== CaycStatus.NonCompliant && !editing && canEdit && (
+      {caycStatus !== CaycStatus.NonCompliant && !editing && canEdit && (
         <div className="sw-mt-4 it__qg-unfollow-cayc">
           <SubHeading as="p" className="sw-mb-2 sw-typo-default">
             <FormattedMessage
index 2948d4663dbca5e61fa7e6f4d1f0f32502a41976..6ce82c9c880c7778a9f02f2d83a9ac41691015aa 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Tooltip } from '@sonarsource/echoes-react';
 import { Badge, BareButton, SubnavigationGroup, SubnavigationItem } from 'design-system';
 import * as React from 'react';
 import { useNavigate } from 'react-router-dom';
-import Tooltip from '../../../components/controls/Tooltip';
-
+import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures';
 import { translate } from '../../../helpers/l10n';
 import { getQualityGateUrl } from '../../../helpers/urls';
+import { Feature } from '../../../types/features';
 import { CaycStatus, QualityGate } from '../../../types/types';
+import AIGeneratedIcon from './AIGeneratedIcon';
 import BuiltInQualityGateBadge from './BuiltInQualityGateBadge';
 import QGRecommendedIcon from './QGRecommendedIcon';
 
@@ -35,53 +37,57 @@ interface Props {
 
 export default function List({ qualityGates, currentQualityGate }: Props) {
   const navigateTo = useNavigate();
-
-  function redirectQualityGate(qualityGateName: string) {
-    navigateTo(getQualityGateUrl(qualityGateName));
-  }
+  const { hasFeature } = useAvailableFeatures();
 
   return (
     <SubnavigationGroup>
-      {qualityGates.map((qualityGate) => {
-        const isDefaultTitle = qualityGate.isDefault ? ` ${translate('default')}` : '';
-        const isBuiltInTitle = qualityGate.isBuiltIn
-          ? ` ${translate('quality_gates.built_in')}`
-          : '';
+      {qualityGates.map(({ isDefault, isBuiltIn, name, caycStatus }) => {
+        const isDefaultTitle = isDefault ? ` ${translate('default')}` : '';
+        const isBuiltInTitle = isBuiltIn ? ` ${translate('quality_gates.built_in')}` : '';
+        const isAICodeAssuranceQualityGate =
+          hasFeature(Feature.AiCodeAssurance) && isBuiltIn && name === 'Sonar way';
 
         return (
           <SubnavigationItem
             className="it__list-group-item"
-            active={currentQualityGate === qualityGate.name}
-            key={qualityGate.name}
+            active={currentQualityGate === name}
+            key={name}
             onClick={() => {
-              redirectQualityGate(qualityGate.name);
+              navigateTo(getQualityGateUrl(name));
             }}
           >
             <div className="sw-flex sw-flex-col sw-min-w-0">
               <BareButton
-                aria-current={currentQualityGate === qualityGate.name && 'page'}
-                title={`${qualityGate.name}${isDefaultTitle}${isBuiltInTitle}`}
+                aria-current={currentQualityGate === name && 'page'}
+                title={`${name}${isDefaultTitle}${isBuiltInTitle}`}
                 className="sw-flex-1 sw-text-ellipsis sw-overflow-hidden sw-max-w-abs-250 sw-whitespace-nowrap"
               >
-                {qualityGate.name}
+                {name}
               </BareButton>
 
-              {(qualityGate.isDefault || qualityGate.isBuiltIn) && (
+              {(isDefault || isBuiltIn) && (
                 <div className="sw-mt-2">
-                  {qualityGate.isDefault && (
-                    <Badge className="sw-mr-2">{translate('default')}</Badge>
-                  )}
-                  {qualityGate.isBuiltIn && <BuiltInQualityGateBadge />}
+                  {isDefault && <Badge className="sw-mr-2">{translate('default')}</Badge>}
+                  {isBuiltIn && <BuiltInQualityGateBadge />}
                 </div>
               )}
             </div>
-            {qualityGate.caycStatus !== CaycStatus.NonCompliant && (
-              <Tooltip content={translate('quality_gates.cayc.tooltip.message')}>
-                <span>
-                  <QGRecommendedIcon />
-                </span>
-              </Tooltip>
-            )}
+            <div>
+              {isAICodeAssuranceQualityGate && (
+                <Tooltip content={translate('quality_gates.ai_generated.tootltip.message')}>
+                  <span className="sw-mr-1">
+                    <AIGeneratedIcon />
+                  </span>
+                </Tooltip>
+              )}
+              {caycStatus !== CaycStatus.NonCompliant && (
+                <Tooltip content={translate('quality_gates.cayc.tooltip.message')}>
+                  <span>
+                    <QGRecommendedIcon />
+                  </span>
+                </Tooltip>
+              )}
+            </div>
           </SubnavigationItem>
         );
       })}
index 27d92f4acc4b71df872e8d9735c0345c680977a7..132ec6528cfd2b027708c2bc88742a334e730885 100644 (file)
@@ -485,7 +485,6 @@ it('should warn user when quality gate is not CaYC compliant and user has permis
   await user.click(nonCompliantQualityGate);
 
   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 () => {
@@ -498,10 +497,8 @@ it('should show optimize banner when quality gate is compliant but non-CaYC and
   });
 
   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 () => {
@@ -571,6 +568,24 @@ it('should not display CaYC condition simplification tour for users who dismisse
   expect(byRole('alertdialog').query()).not.toBeInTheDocument();
 });
 
+it('should advertise the Sonar way Quality Gate as AI-ready', async () => {
+  const user = userEvent.setup();
+  qualityGateHandler.setIsAdmin(true);
+  renderQualityGateApp({
+    currentUser: mockLoggedInUser({
+      dismissedNotices: { [NoticeType.QG_CAYC_CONDITIONS_SIMPLIFICATION]: true },
+    }),
+    featureList: [Feature.AiCodeAssurance],
+  });
+
+  await user.click(await screen.findByRole('link', { name: /Sonar way/ }));
+  expect(
+    await screen.findByRole('link', {
+      name: 'quality_gates.ai_generated.description.clean_ai_generated_code open_in_new_tab',
+    }),
+  ).toBeInTheDocument();
+});
+
 it('should not allow to change value of prioritized_rule_issues', async () => {
   const user = userEvent.setup();
   qualityGateHandler.setIsAdmin(true);
index 9d067e3a512edabcbec2facb9ac9b0ea141a9e0f..61735ee8392eb7104487e0f8de2e2e9d7d1e2f4c 100644 (file)
@@ -25,6 +25,7 @@ export const DOC_URL = 'https://docs.sonarsource.com/sonarqube/latest';
 export enum DocLink {
   AccountTokens = '/user-guide/managing-tokens/',
   ActiveVersions = '/server-upgrade-and-maintenance/upgrade/upgrade-the-server/active-versions/',
+  AiCodeAssurance = '/ai-features/ai-code-assurance/',
   AlmAzureIntegration = '/devops-platform-integration/azure-devops-integration/',
   AlmBitBucketCloudAuth = '/instance-administration/authentication/bitbucket-cloud/',
   AlmBitBucketCloudIntegration = '/devops-platform-integration/bitbucket-integration/bitbucket-cloud-integration/',
index abcfe8fbb64d4275db1150536671b6205aaacca8..bb5ded65b35ca6dd1be396d43b687b09024bd109 100644 (file)
@@ -30,4 +30,5 @@ export enum Feature {
   GitlabProvisioning = 'gitlab-provisioning',
   PrioritizedRules = 'prioritized-rules',
   FixSuggestions = 'fix-suggestions',
+  AiCodeAssurance = 'ai-code-assurance',
 }
index c7b83c2f31cb943c3da7273d6032d229b04c0685..2f1f69ef271fccbb2e59f9b78269144b2f0e8084 100644 (file)
@@ -2458,6 +2458,9 @@ quality_gates.cayc.banner.description2=It ensures that:
 quality_gates.cayc_unfollow.description=You may click unlock to edit this quality gate. Adding extra conditions to a compliant quality gate can result in drawbacks. Are you reconsidering {cayc_link}? We strongly recommend this methodology to achieve a Clean Code status.
 quality_gates.cayc.review_update_modal.add_condition.header= {0} condition(s) on new code will be added
 quality_gates.cayc.review_update_modal.modify_condition.header= {0} condition(s) on new code will be modified
+quality_gates.ai_generated.tootltip.message=Sonar way ensures clean AI-generated code
+quality_gates.ai_generated.description=Sonar way ensures {link}
+quality_gates.ai_generated.description.clean_ai_generated_code=clean AI-generated code
 
 #------------------------------------------------------------------------------
 #