aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMathieu Suen <mathieu.suen@sonarsource.com>2024-09-20 10:05:18 +0200
committersonartech <sonartech@sonarsource.com>2024-09-25 20:02:53 +0000
commit517518e18caff16605672bdc028aab2d108a0db7 (patch)
treeb6cbb515ea9dd732f3d7fae8d7593ec37e840ae8
parent7a57a3ad0f517f598a5bc4d38620ec3a517425e8 (diff)
downloadsonarqube-517518e18caff16605672bdc028aab2d108a0db7.tar.gz
sonarqube-517518e18caff16605672bdc028aab2d108a0db7.zip
SONAR-23064 Add information on ai code assurance for project
-rw-r--r--server/sonar-web/src/main/js/api/ai-code-assurance.ts28
-rw-r--r--server/sonar-web/src/main/js/api/mocks/ProjectBadgesServiceMock.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx37
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx11
-rw-r--r--server/sonar-web/src/main/js/queries/ai-code-assurance.ts32
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties4
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.
+
#------------------------------------------------------------------------------
#