diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2024-09-20 10:05:18 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-09-25 20:02:53 +0000 |
commit | 517518e18caff16605672bdc028aab2d108a0db7 (patch) | |
tree | b6cbb515ea9dd732f3d7fae8d7593ec37e840ae8 | |
parent | 7a57a3ad0f517f598a5bc4d38620ec3a517425e8 (diff) | |
download | sonarqube-517518e18caff16605672bdc028aab2d108a0db7.tar.gz sonarqube-517518e18caff16605672bdc028aab2d108a0db7.zip |
SONAR-23064 Add information on ai code assurance for project
7 files changed, 136 insertions, 13 deletions
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 index 00000000000..59b2b67d0cf --- /dev/null +++ b/server/sonar-web/src/main/js/api/ai-code-assurance.ts @@ -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); +} diff --git a/server/sonar-web/src/main/js/api/mocks/ProjectBadgesServiceMock.tsx b/server/sonar-web/src/main/js/api/mocks/ProjectBadgesServiceMock.tsx index 0e1839aa8b9..0c2de2bffa3 100644 --- a/server/sonar-web/src/main/js/api/mocks/ProjectBadgesServiceMock.tsx +++ b/server/sonar-web/src/main/js/api/mocks/ProjectBadgesServiceMock.tsx @@ -17,10 +17,12 @@ * 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 = diff --git a/server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx b/server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx index 14c59da187d..07884aa5838 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx @@ -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 }, ); } diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx index af612975d1d..1bd26ac2efd 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx @@ -18,11 +18,15 @@ * 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> diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx index ada3ea85a58..7325d31cb2e 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx @@ -17,16 +17,18 @@ * 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 index 00000000000..2f8096cf859 --- /dev/null +++ b/server/sonar-web/src/main/js/queries/ai-code-assurance.ts @@ -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, + }); +}); diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 9ad8ea5cfda..a72896a47f2 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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. + #------------------------------------------------------------------------------ # |