]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23064 Add information on ai code assurance for project
authorMathieu Suen <mathieu.suen@sonarsource.com>
Fri, 20 Sep 2024 08:05:18 +0000 (10:05 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 25 Sep 2024 20:02:53 +0000 (20:02 +0000)
server/sonar-web/src/main/js/api/ai-code-assurance.ts [new file with mode: 0644]
server/sonar-web/src/main/js/api/mocks/ProjectBadgesServiceMock.tsx
server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx
server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx
server/sonar-web/src/main/js/queries/ai-code-assurance.ts [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/api/ai-code-assurance.ts b/server/sonar-web/src/main/js/api/ai-code-assurance.ts
new file mode 100644 (file)
index 0000000..59b2b67
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * 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 { throwGlobalError } from '~sonar-aligned/helpers/error';
+import { getJSON } from '~sonar-aligned/helpers/request';
+
+export function isProjectAiCodeAssured(project: string): Promise<boolean> {
+  return getJSON('/api/projects/get_ai_code_assurance', { project })
+    .then((response) => response.aiCodeAssurance)
+    .catch(throwGlobalError);
+}
index 0e1839aa8b976636354e474be958174a8aab84ea..0c2de2bffa3ae87ab1254bb5b4306c582f293463 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 { isProjectAiCodeAssured } from '../ai-code-assurance';
 import { getProjectBadgesToken, renewProjectBadgesToken } from '../project-badges';
 
 jest.mock('../project-badges');
 jest.mock('../project-badges');
+jest.mock('../ai-code-assurance');
 
 const defaultToken = 'sqb_2b5052cef8eac91a921ac71be9227a27f6b6b38b';
 
@@ -32,12 +34,20 @@ export class ProjectBadgesServiceMock {
 
     jest.mocked(getProjectBadgesToken).mockImplementation(this.handleGetProjectBadgesToken);
     jest.mocked(renewProjectBadgesToken).mockImplementation(this.handleRenewProjectBadgesToken);
+    jest.mocked(isProjectAiCodeAssured).mockImplementation(this.handleProjectAiGeneratedCode);
   }
 
   handleGetProjectBadgesToken = () => {
     return Promise.resolve(this.token);
   };
 
+  handleProjectAiGeneratedCode = (project: string) => {
+    if (project === 'no-ai') {
+      return Promise.resolve(false);
+    }
+    return Promise.resolve(true);
+  };
+
   handleRenewProjectBadgesToken = () => {
     const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
     this.token =
index 14c59da187d78371db189864b9f4013e9979d8d5..07884aa5838a690d51b56d4c5386e8881b063ce8 100644 (file)
@@ -30,9 +30,12 @@ import { ProjectBadgesServiceMock } from '../../../api/mocks/ProjectBadgesServic
 import ProjectLinksServiceMock from '../../../api/mocks/ProjectLinksServiceMock';
 import { mockComponent } from '../../../helpers/mocks/component';
 import { mockCurrentUser, mockLoggedInUser, mockMeasure } from '../../../helpers/testMocks';
-import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils';
+import {
+  renderAppWithComponentContext,
+  RenderContext,
+} from '../../../helpers/testReactTestingUtils';
+import { Feature } from '../../../types/features';
 import { Component } from '../../../types/types';
-import { CurrentUser } from '../../../types/users';
 import routes from '../routes';
 
 jest.mock('../../../api/rules');
@@ -86,13 +89,14 @@ it('should show fields for project', async () => {
       description: 'Test description',
       tags: ['bar'],
     },
-    mockLoggedInUser(),
+    { currentUser: mockLoggedInUser(), featureList: [Feature.AiCodeAssurance] },
   );
   expect(await ui.projectPageTitle.find()).toBeInTheDocument();
   expect(ui.qualityGateList.get()).toBeInTheDocument();
   expect(ui.link.getAll(ui.qualityGateList.get())).toHaveLength(1);
   expect(ui.link.getAll(ui.qualityProfilesList.get())).toHaveLength(1);
   expect(ui.link.getAll(ui.externalLinksList.get())).toHaveLength(1);
+  expect(screen.getByText('project.info.ai_code_assurance.title')).toBeInTheDocument();
   expect(screen.getByText('Test description')).toBeInTheDocument();
   expect(screen.getByText('my-project')).toBeInTheDocument();
   expect(screen.getByText('visibility.private')).toBeInTheDocument();
@@ -114,7 +118,7 @@ it('should show application fields', async () => {
       description: 'Test description',
       tags: ['bar'],
     },
-    mockLoggedInUser(),
+    { currentUser: mockLoggedInUser() },
   );
   expect(await ui.applicationPageTitle.find()).toBeInTheDocument();
   expect(ui.qualityGateList.query()).not.toBeInTheDocument();
@@ -129,15 +133,30 @@ it('should show application fields', async () => {
 });
 
 it('should hide some fields for application', async () => {
-  renderProjectInformationApp({
-    qualifier: ComponentQualifier.Application,
-  });
+  renderProjectInformationApp(
+    {
+      qualifier: ComponentQualifier.Application,
+    },
+    { featureList: [Feature.AiCodeAssurance] },
+  );
   expect(await ui.applicationPageTitle.find()).toBeInTheDocument();
   expect(screen.getByText('application.info.empty_description')).toBeInTheDocument();
+  expect(screen.queryByText('project.info.ai_code_assurance.title')).not.toBeInTheDocument();
   expect(screen.getByText('visibility.public')).toBeInTheDocument();
   expect(ui.tags.get()).toHaveTextContent('no_tags');
 });
 
+it('should not display ai code assurence', async () => {
+  renderProjectInformationApp(
+    {
+      key: 'no-ai',
+    },
+    { featureList: [Feature.AiCodeAssurance] },
+  );
+  expect(await ui.projectPageTitle.find()).toBeInTheDocument();
+  expect(screen.queryByText('project.info.ai_code_assurance.title')).not.toBeInTheDocument();
+});
+
 it('should not show field that is not configured', async () => {
   renderProjectInformationApp({
     qualityGate: undefined,
@@ -167,7 +186,7 @@ it('should hide visibility if public', async () => {
 
 function renderProjectInformationApp(
   overrides: Partial<Component> = {},
-  currentUser: CurrentUser = mockCurrentUser(),
+  context: RenderContext = { currentUser: mockCurrentUser() },
 ) {
   const component = mockComponent(overrides);
   componentsMock.registerComponent(component, [componentsMock.components[0].component]);
@@ -175,7 +194,7 @@ function renderProjectInformationApp(
   return renderAppWithComponentContext(
     'project/information',
     routes,
-    { currentUser },
+    { ...context },
     { component },
   );
 }
index af612975d1d58ed4573b8f0f5512908aba1f7f93..1bd26ac2efd514ec42cea789314ba52de575a0a8 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import classNames from 'classnames';
-import { BasicSeparator, SubTitle } from 'design-system';
+import { BasicSeparator, SubHeading, SubTitle } from 'design-system';
 import React, { PropsWithChildren, useEffect, useState } from 'react';
+import { FormattedMessage } from 'react-intl';
 import { ComponentQualifier, Visibility } from '~sonar-aligned/types/component';
 import { getProjectLinks } from '../../../api/projectLinks';
+import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures';
 import { translate } from '../../../helpers/l10n';
+import { useProjectAiCodeAssuredQuery } from '../../../queries/ai-code-assurance';
+import { Feature } from '../../../types/features';
 import { Component, Measure, ProjectLink } from '../../../types/types';
 import MetaDescription from './components/MetaDescription';
 import MetaKey from './components/MetaKey';
@@ -41,8 +45,16 @@ export interface AboutProjectProps {
 
 export default function AboutProject(props: AboutProjectProps) {
   const { component, measures = [] } = props;
+  const { hasFeature } = useAvailableFeatures();
   const isApp = component.qualifier === ComponentQualifier.Application;
   const [links, setLinks] = useState<ProjectLink[] | undefined>(undefined);
+  const { data: isAiAssured } = useProjectAiCodeAssuredQuery(
+    { project: component.key },
+    {
+      enabled:
+        component.qualifier === ComponentQualifier.Project && hasFeature(Feature.AiCodeAssurance),
+    },
+  );
 
   useEffect(() => {
     if (!isApp) {
@@ -63,7 +75,9 @@ export default function AboutProject(props: AboutProjectProps) {
         (component.qualityGate ||
           (component.qualityProfiles && component.qualityProfiles.length > 0)) && (
           <ProjectInformationSection className="sw-pt-0 sw-flex sw-flex-col sw-gap-4">
-            {component.qualityGate && <MetaQualityGate qualityGate={component.qualityGate} />}
+            {component.qualityGate && (
+              <MetaQualityGate qualityGate={component.qualityGate} isAiAssured={isAiAssured} />
+            )}
 
             {component.qualityProfiles && component.qualityProfiles.length > 0 && (
               <MetaQualityProfiles profiles={component.qualityProfiles} />
@@ -71,6 +85,15 @@ export default function AboutProject(props: AboutProjectProps) {
           </ProjectInformationSection>
         )}
 
+      {isAiAssured === true && (
+        <ProjectInformationSection>
+          <SubHeading>{translate('project.info.ai_code_assurance.title')}</SubHeading>
+          <span>
+            <FormattedMessage id="projects.ai_code.content" />
+          </span>
+        </ProjectInformationSection>
+      )}
+
       <ProjectInformationSection>
         <MetaKey componentKey={component.key} qualifier={component.qualifier} />
       </ProjectInformationSection>
index ada3ea85a581c2a0758c6090f2778ea5187d0239..7325d31cb2e1cf9b3c09e2a948967e459bdfd79c 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 { Link, Note, SubHeading } from 'design-system';
+import { Link, Note, StyledMutedText, SubHeading } from 'design-system';
 import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
 import { translate } from '../../../../helpers/l10n';
 import { getQualityGateUrl } from '../../../../helpers/urls';
 
 interface Props {
+  isAiAssured?: boolean;
   qualityGate: { isDefault?: boolean; name: string };
 }
 
-export default function MetaQualityGate({ qualityGate }: Props) {
+export default function MetaQualityGate({ qualityGate, isAiAssured }: Props) {
   return (
     <div>
       <SubHeading id="quality-gate-header">{translate('project.info.quality_gate')}</SubHeading>
@@ -37,6 +39,11 @@ export default function MetaQualityGate({ qualityGate }: Props) {
           <Link to={getQualityGateUrl(qualityGate.name)}>{qualityGate.name}</Link>
         </li>
       </ul>
+      {isAiAssured === true && (
+        <StyledMutedText className="sw-text-wrap sw-mt-2">
+          <FormattedMessage id="project.info.quality_gate.ai_code_assurance.description" />
+        </StyledMutedText>
+      )}
     </div>
   );
 }
diff --git a/server/sonar-web/src/main/js/queries/ai-code-assurance.ts b/server/sonar-web/src/main/js/queries/ai-code-assurance.ts
new file mode 100644 (file)
index 0000000..2f8096c
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * 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 { queryOptions } from '@tanstack/react-query';
+import { isProjectAiCodeAssured } from '../api/ai-code-assurance';
+import { createQueryHook } from './common';
+
+export const AI_CODE_ASSURANCE_QUERY_PREFIX = 'ai-code-assurance';
+
+export const useProjectAiCodeAssuredQuery = createQueryHook(({ project }: { project: string }) => {
+  return queryOptions({
+    queryKey: [AI_CODE_ASSURANCE_QUERY_PREFIX, project], // - or _ ?
+    queryFn: ({ queryKey: [_, project] }) => isProjectAiCodeAssured(project),
+    enabled: project !== undefined,
+  });
+});
index 9ad8ea5cfdab8e36423c6d5833a228b6623f613e..a72896a47f26fe051f84699b49d8d811395368c6 100644 (file)
@@ -13,6 +13,7 @@ add_verb=Add
 admin=Admin
 after=After
 ai_code=AI Code
+ai_code_assurance=AI CODE ASSURANCE
 apply=Apply
 all=All
 and=And
@@ -2204,6 +2205,9 @@ project.info.make_home.tooltip=This means you'll be redirected to this project w
 application.info.make_home.tooltip=This means you'll be redirected to this application whenever you log in to SonarQube or click on the top-left SonarQube logo.
 overview.project_key.tooltip.TRK=Your project key is a unique identifier for your project. If you are using Maven, make sure the key matches the "groupId:artifactId" format.
 overview.project_key.tooltip.APP=Your application key is a unique identifier for your application.
+project.info.ai_code_assurance.title=AI Code Assurance
+project.info.quality_gate.ai_code_assurance.description=This project contains AI-generated code. It must use Sonar way Quality Gate to benefit from Sonar’s AI Code Assurance.
+
 
 #------------------------------------------------------------------------------
 #