]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23008 Add the new AI code assurance badge when the feature is in the feature...
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>
Mon, 23 Sep 2024 09:10:38 +0000 (11:10 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 25 Sep 2024 20:02:54 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/projectInformation/badges/ProjectBadges.tsx
server/sonar-web/src/main/js/apps/projectInformation/badges/__tests__/ProjectBadges-test.tsx
server/sonar-web/src/main/js/apps/projectInformation/badges/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/projectInformation/badges/utils.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 9a11599bfe902fce0c36269054c591d21d6fdc62..da0c3962addb0cd61f7afdf0e146d7f4a5d843b5 100644 (file)
@@ -36,6 +36,7 @@ import { useState } from 'react';
 import { Image } from '~sonar-aligned/components/common/Image';
 import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
 import { MetricKey } from '~sonar-aligned/types/metrics';
+import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { localizeMetric } from '../../../helpers/measures';
 import {
@@ -45,6 +46,8 @@ import {
   useRenewBagdeTokenMutation,
 } from '../../../queries/badges';
 import { BranchLike } from '../../../types/branch-like';
+import { isProject } from '../../../types/component';
+import { Feature } from '../../../types/features';
 import { Component } from '../../../types/types';
 import { BadgeFormats, BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from './utils';
 
@@ -68,6 +71,7 @@ export default function ProjectBadges(props: ProjectBadgesProps) {
   } = useBadgeTokenQuery(project);
   const { data: metricOptions, isLoading: isLoadingMetrics } = useBadgeMetricsQuery();
   const { mutate: renewToken, isPending: isRenewing } = useRenewBagdeTokenMutation();
+  const { hasFeature } = useAvailableFeatures();
   const isLoading = isLoadingMetrics || isLoadingToken || isRenewing;
 
   const handleSelectType = (selectedType: BadgeType) => {
@@ -107,7 +111,7 @@ export default function ProjectBadges(props: ProjectBadgesProps) {
             image={
               <Image
                 alt={translate('overview.badges', BadgeType.measure, 'alt')}
-                src={getBadgeUrl(BadgeType.measure, fullBadgeOptions, token)}
+                src={getBadgeUrl(BadgeType.measure, fullBadgeOptions, token, true)}
               />
             }
             description={translate('overview.badges', BadgeType.measure, 'description', qualifier)}
@@ -119,7 +123,7 @@ export default function ProjectBadges(props: ProjectBadgesProps) {
             image={
               <Image
                 alt={translate('overview.badges', BadgeType.qualityGate, 'alt')}
-                src={getBadgeUrl(BadgeType.qualityGate, fullBadgeOptions, token)}
+                src={getBadgeUrl(BadgeType.qualityGate, fullBadgeOptions, token, true)}
                 width="128px"
               />
             }
@@ -130,6 +134,25 @@ export default function ProjectBadges(props: ProjectBadgesProps) {
               qualifier,
             )}
           />
+          {hasFeature(Feature.AiCodeAssurance) && isProject(qualifier) && (
+            <IllustratedSelectionCard
+              className="sw-w-abs-300 it__badge-button"
+              onClick={() => handleSelectType(BadgeType.aiCodeAssurance)}
+              selected={BadgeType.aiCodeAssurance === selectedType}
+              image={
+                <Image
+                  alt={translate('overview.badges', BadgeType.aiCodeAssurance, 'alt')}
+                  src={getBadgeUrl(BadgeType.aiCodeAssurance, fullBadgeOptions, token, true)}
+                />
+              }
+              description={translate(
+                'overview.badges',
+                BadgeType.aiCodeAssurance,
+                'description',
+                qualifier,
+              )}
+            />
+          )}
         </div>
       </Spinner>
 
index 1ff688da6f80fd4363057ead3812c54c2740987c..2354d1f49500545f9951c36f437313b9b32ecc9d 100644 (file)
@@ -65,7 +65,9 @@ it('should renew token', async () => {
 
   expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute(
     'src',
-    `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.alert_status}&token=foo`,
+    expect.stringContaining(
+      `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.alert_status}&token=foo`,
+    ),
   );
 
   await user.click(screen.getByText('overview.badges.renew'));
@@ -74,12 +76,16 @@ it('should renew token', async () => {
     await screen.findByAltText(`overview.badges.${BadgeType.qualityGate}.alt`),
   ).toHaveAttribute(
     'src',
-    'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=bar',
+    expect.stringContaining(
+      'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=bar',
+    ),
   );
 
   expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute(
     'src',
-    `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.alert_status}&token=bar`,
+    expect.stringContaining(
+      `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.alert_status}&token=bar`,
+    ),
   );
 });
 
@@ -90,7 +96,8 @@ it('should update params', async () => {
 
   expect(
     screen.getByText(
-      `[![${MetricKey.alert_status}](host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.alert_status}&token=foo)](/dashboard)`,
+      `[![${MetricKey.alert_status}](host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.alert_status}&token=foo`,
+      { exact: false },
     ),
   ).toBeInTheDocument();
 
@@ -98,7 +105,8 @@ it('should update params', async () => {
 
   expect(
     screen.getByText(
-      `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.alert_status}&token=foo`,
+      'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=alert_status&token=foo',
+      { exact: false },
     ),
   ).toBeInTheDocument();
 
@@ -107,7 +115,8 @@ it('should update params', async () => {
 
   expect(
     screen.getByText(
-      `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.coverage}&token=foo`,
+      'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=coverage&token=foo',
+      { exact: false },
     ),
   ).toBeInTheDocument();
 
@@ -119,7 +128,8 @@ it('should update params', async () => {
 
   expect(
     screen.getByText(
-      `host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=foo`,
+      'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=foo',
+      { exact: false },
     ),
   ).toBeInTheDocument();
 
@@ -131,7 +141,8 @@ it('should update params', async () => {
 
   expect(
     screen.getByText(
-      `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.coverage}&token=foo`,
+      'host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=coverage&token=foo',
+      { exact: false },
     ),
   ).toBeInTheDocument();
 });
index 54e6db99d167e28377623dfa30b246f12774139a..06710bbd7e18eb062de16c246f7f078dee0de0b5 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { Location } from '~sonar-aligned/types/router';
+import { MetricKey } from '../../../../sonar-aligned/types/metrics';
 import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from '../utils';
 
 jest.mock('../../../../helpers/urls', () => ({
@@ -45,8 +46,20 @@ describe('#getBadgeUrl', () => {
     );
   });
 
+  it('should generate correct ai code assurance badge links', () => {
+    expect(getBadgeUrl(BadgeType.aiCodeAssurance, options, 'foo')).toBe(
+      'host/api/project_badges/ai_code_assurance?branch=master&project=foo&token=foo',
+    );
+  });
+
+  it('should generate correct ai code assurance badge links with timestamp', () => {
+    expect(getBadgeUrl(BadgeType.aiCodeAssurance, options, 'foo', true)).toContain(
+      'host/api/project_badges/ai_code_assurance?branch=master&project=foo&token=foo',
+    );
+  });
+
   it('should ignore undefined parameters', () => {
-    expect(getBadgeUrl(BadgeType.measure, { metric: 'alert_status' }, 'foo')).toBe(
+    expect(getBadgeUrl(BadgeType.measure, { metric: MetricKey.alert_status }, 'foo')).toBe(
       'host/api/project_badges/measure?metric=alert_status&token=foo',
     );
   });
@@ -59,9 +72,17 @@ describe('#getBadgeUrl', () => {
 });
 
 describe('#getBadgeSnippet', () => {
-  it('should generate a correct markdown image', () => {
-    expect(getBadgeSnippet(BadgeType.measure, { ...options, format: 'md' }, 'foo')).toBe(
+  it('should generate a correct markdown image for measure', () => {
+    const snippet = getBadgeSnippet(BadgeType.measure, options, 'foo');
+    expect(snippet).toBe(
       '[![alert_status](host/api/project_badges/measure?branch=master&project=foo&metric=alert_status&token=foo)](host/dashboard?id=foo&branch=master)',
     );
   });
+
+  it('should generate a correct markdown image for ai code assurance', () => {
+    const snippet = getBadgeSnippet(BadgeType.aiCodeAssurance, options, 'foo');
+    expect(snippet).toBe(
+      '[![overview.badges.ai_code_assurance](host/api/project_badges/ai_code_assurance?branch=master&project=foo&token=foo)](host/dashboard?id=foo&branch=master)',
+    );
+  });
 });
index b1c3435804bd0d880ff8db4fd418d719028288a9..904c2357c4bccb5d930f951266f10156a5c7b1da 100644 (file)
@@ -17,7 +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 { getLocalizedMetricName } from '../../../helpers/l10n';
+import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
 import { omitNil } from '../../../helpers/request';
 import { getHostUrl, getPathUrlAsString, getProjectUrl } from '../../../helpers/urls';
 
@@ -35,6 +35,7 @@ export interface BadgeOptions {
 export enum BadgeType {
   measure = 'measure',
   qualityGate = 'quality_gate',
+  aiCodeAssurance = 'ai_code_assurance',
 }
 
 export function getBadgeSnippet(type: BadgeType, options: BadgeOptions, token: string) {
@@ -52,6 +53,9 @@ export function getBadgeSnippet(type: BadgeType, options: BadgeOptions, token: s
     case BadgeType.measure:
       label = getLocalizedMetricName({ key: metric });
       break;
+    case BadgeType.aiCodeAssurance:
+      label = translate('overview.badges.ai_code_assurance');
+      break;
     case BadgeType.qualityGate:
     default:
       label = 'Quality gate';
@@ -70,16 +74,22 @@ export function getBadgeUrl(
   type: BadgeType,
   { branch, project, metric = 'alert_status', pullRequest }: BadgeOptions,
   token: string,
+  disableBrowserCache: boolean = false,
 ) {
   switch (type) {
     case BadgeType.qualityGate:
       return `${getHostUrl()}/api/project_badges/quality_gate?${new URLSearchParams(
         omitNil({ branch, project, pullRequest, token }),
-      ).toString()}`;
+      ).toString()}${disableBrowserCache ? `&${new Date().getTime()}` : ''}`;
+    case BadgeType.aiCodeAssurance:
+      return `${getHostUrl()}/api/project_badges/ai_code_assurance?${new URLSearchParams(
+        omitNil({ branch, project, pullRequest, token }),
+      ).toString()}${disableBrowserCache ? `&${new Date().getTime()}` : ''}`;
+
     case BadgeType.measure:
     default:
       return `${getHostUrl()}/api/project_badges/measure?${new URLSearchParams(
         omitNil({ branch, project, metric, pullRequest, token }),
-      ).toString()}`;
+      ).toString()}${disableBrowserCache ? `&${new Date().getTime()}` : ''}`;
   }
 }
index a72896a47f26fe051f84699b49d8d811395368c6..0a43a4c953cfc35a32462b134ce79b52ba9ce38a 100644 (file)
@@ -4360,11 +4360,16 @@ overview.badges.measure.alt=Standard badge
 overview.badges.measure.description.TRK=Displays the current status of one metric of your project.
 overview.badges.measure.description.VW=Displays the current status of one metric of your portfolio.
 overview.badges.measure.description.APP=Displays the current status of one metric of your application.
+overview.badges.quality_gate=Quality Gate
 overview.badges.quality_gate.alt=Quality Gate badge
 overview.badges.quality_gate.description=Displays the current quality gate status of your project.
 overview.badges.quality_gate.description.APP=Displays the current quality gate status of your application.
 overview.badges.quality_gate.description.TRK=Displays the current quality gate status of your project.
 overview.badges.quality_gate.description.VW=Displays the current quality gate status of your portfolio.
+overview.badges.ai_code_assurance=AI Code Assurance
+overview.badges.ai_code_assurance.alt=AI Code Assurance badge
+overview.badges.ai_code_assurance.description=Displays the current status of Sonar's AI Code Assurance.
+overview.badges.ai_code_assurance.description.TRK=Displays the current status of Sonar's AI Code Assurance of your project.
 overview.badges.leak_warning=Project badges can expose your security rating and other measures. Only use project badges in trusted environments.
 overview.badges.renew=Renew Token
 overview.badges.renew.description=If your project badge security token has leaked to an unsafe environment, you can renew it: