aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMathieu Suen <mathieu.suen@sonarsource.com>2024-11-25 17:40:30 +0100
committersonartech <sonartech@sonarsource.com>2024-11-29 20:03:07 +0000
commitf86cc04663f914d952562845f6cba41cafefe2f0 (patch)
treec6fd287891f344a091aa028ed2cd001fefd083e9
parent2c496d7199d58ce76f1ec513220bde1127c68358 (diff)
downloadsonarqube-f86cc04663f914d952562845f6cba41cafefe2f0.tar.gz
sonarqube-f86cc04663f914d952562845f6cba41cafefe2f0.zip
SONAR-23619 Adjust the project info page to display AI code assurance status
-rw-r--r--server/sonar-web/src/main/js/api/ai-code-assurance.ts8
-rw-r--r--server/sonar-web/src/main/js/api/mocks/AiCodeAssuredServiceMock.ts17
-rw-r--r--server/sonar-web/src/main/js/api/mocks/MeasuresServiceMock.ts34
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/AiCodeAssuranceWarrning.tsx96
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx14
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx31
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx45
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx130
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/queries/ai-code-assurance.ts18
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties7
13 files changed, 180 insertions, 254 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
index 59b2b67d0cf..1a45230df71 100644
--- a/server/sonar-web/src/main/js/api/ai-code-assurance.ts
+++ b/server/sonar-web/src/main/js/api/ai-code-assurance.ts
@@ -21,7 +21,13 @@
import { throwGlobalError } from '~sonar-aligned/helpers/error';
import { getJSON } from '~sonar-aligned/helpers/request';
-export function isProjectAiCodeAssured(project: string): Promise<boolean> {
+export enum AiCodeAssuranceStatus {
+ CONTAINS_AI_CODE = 'CONTAINS_AI_CODE',
+ AI_CODE_ASSURED = 'AI_CODE_ASSURED',
+ NONE = 'NONE',
+}
+
+export function getProjectAiCodeAssuranceStatus(project: string): Promise<AiCodeAssuranceStatus> {
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/AiCodeAssuredServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/AiCodeAssuredServiceMock.ts
index da0a0668abe..825b48c2ea9 100644
--- a/server/sonar-web/src/main/js/api/mocks/AiCodeAssuredServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/AiCodeAssuredServiceMock.ts
@@ -18,22 +18,29 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { isProjectAiCodeAssured } from '../ai-code-assurance';
+import { AiCodeAssuranceStatus, getProjectAiCodeAssuranceStatus } from '../ai-code-assurance';
jest.mock('../ai-code-assurance');
+export const PROJECT_WITH_AI_ASSURED_QG = 'Sonar AI way';
+export const PROJECT_WITHOUT_AI_ASSURED_QG = 'Sonar way';
+
export class AiCodeAssuredServiceMock {
noAiProject = 'no-ai';
constructor() {
- jest.mocked(isProjectAiCodeAssured).mockImplementation(this.handleProjectAiGeneratedCode);
+ jest
+ .mocked(getProjectAiCodeAssuranceStatus)
+ .mockImplementation(this.handleProjectAiGeneratedCode);
}
handleProjectAiGeneratedCode = (project: string) => {
- if (project === this.noAiProject) {
- return Promise.resolve(false);
+ if (project === PROJECT_WITH_AI_ASSURED_QG) {
+ return Promise.resolve(AiCodeAssuranceStatus.AI_CODE_ASSURED);
+ } else if (project === PROJECT_WITHOUT_AI_ASSURED_QG) {
+ return Promise.resolve(AiCodeAssuranceStatus.CONTAINS_AI_CODE);
}
- return Promise.resolve(true);
+ return Promise.resolve(AiCodeAssuranceStatus.NONE);
};
reset() {
diff --git a/server/sonar-web/src/main/js/api/mocks/MeasuresServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/MeasuresServiceMock.ts
index 19f1b585379..55316ab652c 100644
--- a/server/sonar-web/src/main/js/api/mocks/MeasuresServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/MeasuresServiceMock.ts
@@ -35,20 +35,20 @@ const defaultMeasures = mockFullMeasureData(defaultComponents, mockIssuesList())
const defaultPeriod = mockPeriod();
export class MeasuresServiceMock {
- #components: ComponentTree;
- #measures: MeasureRecords;
- #period: Period;
+ components: ComponentTree;
+ measures: MeasureRecords;
+ period: Period;
reset: () => void;
constructor(components?: ComponentTree, measures?: MeasureRecords, period?: Period) {
- this.#components = components ?? cloneDeep(defaultComponents);
- this.#measures = measures ?? cloneDeep(defaultMeasures);
- this.#period = period ?? cloneDeep(defaultPeriod);
+ this.components = components ?? cloneDeep(defaultComponents);
+ this.measures = measures ?? cloneDeep(defaultMeasures);
+ this.period = period ?? cloneDeep(defaultPeriod);
this.reset = () => {
- this.#components = components ?? cloneDeep(defaultComponents);
- this.#measures = measures ?? cloneDeep(defaultMeasures);
- this.#period = period ?? cloneDeep(defaultPeriod);
+ this.components = components ?? cloneDeep(defaultComponents);
+ this.measures = measures ?? cloneDeep(defaultMeasures);
+ this.period = period ?? cloneDeep(defaultPeriod);
};
jest.mocked(getMeasures).mockImplementation(this.handleGetMeasures);
@@ -58,19 +58,19 @@ export class MeasuresServiceMock {
}
registerComponentMeasures = (measures: MeasureRecords) => {
- this.#measures = measures;
+ this.measures = measures;
};
deleteComponentMeasure = (componentKey: string, measureKey: MetricKey) => {
- delete this.#measures[componentKey][measureKey];
+ delete this.measures[componentKey][measureKey];
};
getComponentMeasures = () => {
- return this.#measures;
+ return this.measures;
};
setComponents = (components: ComponentTree) => {
- this.#components = components;
+ this.components = components;
};
findComponentTree = (key: string, from?: ComponentTree): ComponentTree => {
@@ -81,7 +81,7 @@ export class MeasuresServiceMock {
return node.children.find((child) => recurse(child));
};
- const tree = recurse(from ?? this.#components);
+ const tree = recurse(from ?? this.components);
if (!tree) {
throw new Error(`Couldn't find component tree for key ${key}`);
}
@@ -90,8 +90,8 @@ export class MeasuresServiceMock {
};
filterMeasures = (componentKey: string, metricKeys: string[]) => {
- return this.#measures[componentKey]
- ? Object.values(this.#measures[componentKey]).filter(({ metric }) =>
+ return this.measures[componentKey]
+ ? Object.values(this.measures[componentKey]).filter(({ metric }) =>
metricKeys.includes(metric),
)
: [];
@@ -124,7 +124,7 @@ export class MeasuresServiceMock {
...component,
measures,
},
- period: this.#period,
+ period: this.period,
metrics,
});
};
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/AiCodeAssuranceWarrning.tsx b/server/sonar-web/src/main/js/apps/overview/branches/AiCodeAssuranceWarrning.tsx
deleted file mode 100644
index 94e6a919ba5..00000000000
--- a/server/sonar-web/src/main/js/apps/overview/branches/AiCodeAssuranceWarrning.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * 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 { ButtonIcon, ButtonVariety, IconX } from '@sonarsource/echoes-react';
-import { FormattedMessage } from 'react-intl';
-import { themeBorder } from '~design-system';
-import { MessageTypes } from '../../../api/messages';
-import { translate } from '../../../helpers/l10n';
-import {
- useMessageDismissedMutation,
- useMessageDismissedQuery,
-} from '../../../queries/dismissed-messages';
-
-export function AiCodeAssuranceWarrning({ projectKey }: Readonly<{ projectKey: string }>) {
- const messageType = MessageTypes.UnresolvedFindingsInAIGeneratedCode;
- const { data: isDismissed } = useMessageDismissedQuery(
- { messageType, projectKey },
- { select: (r) => r.dismissed },
- );
-
- const { mutate: dismiss } = useMessageDismissedMutation();
-
- if (isDismissed !== false) {
- return null;
- }
-
- return (
- <StyledSnowflakes className="sw-my-6">
- <StyleSnowflakesInner>
- <StyleSnowflakesContent>
- <FormattedMessage id="overview.ai_assurance.unsolved_overall.title" tagName="h3" />
- <FormattedMessage id="overview.ai_assurance.unsolved_overall.description" tagName="p" />
- </StyleSnowflakesContent>
-
- <ButtonIcon
- Icon={IconX}
- ariaLabel={translate('overview.ai_assurance.unsolved_overall.dismiss')}
- onClick={() => dismiss({ projectKey, messageType })}
- variety={ButtonVariety.DefaultGhost}
- />
- </StyleSnowflakesInner>
- </StyledSnowflakes>
- );
-}
-
-const StyleSnowflakesContent = styled.div`
- display: flex;
- padding: var(--echoes-dimension-space-100) 0px;
-
- flex-direction: column;
- align-items: flex-start;
- gap: var(--echoes-dimension-space-200);
- flex: 1 0 0;
-`;
-
-const StyleSnowflakesInner = styled.div`
- display: flex;
- padding: var(--echoes-dimension-space-100) 0px;
-
- align-items: flex-start;
- gap: var(--echoes-dimension-space-300);
- flex: 1 0 0;
-`;
-
-const StyledSnowflakes = styled.div`
- display: flex;
- justify-content: space-between;
- width: 641px;
- padding: 0 var(--echoes-dimension-space-100) 0 var(--echoes-dimension-space-300);
-
- align-items: flex-start;
- gap: 12px;
-
- border-radius: 8px;
- border: ${themeBorder('default', 'projectCardBorder')};
-
- background: var(--echoes-color-background-warning-weak);
-`;
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
index 84bd1f58f53..af7bdc781d5 100644
--- a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
@@ -25,8 +25,6 @@ import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { useLocation, useRouter } from '~sonar-aligned/components/hoc/withRouter';
import { isPortfolioLike } from '~sonar-aligned/helpers/component';
import { ComponentQualifier } from '~sonar-aligned/types/component';
-import { MetricKey } from '~sonar-aligned/types/metrics';
-import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures';
import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext';
import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage';
import { parseDate } from '../../../helpers/dates';
@@ -37,12 +35,9 @@ import {
isDiffMetric,
} from '../../../helpers/measures';
import { CodeScope } from '../../../helpers/urls';
-import { useProjectAiCodeAssuredQuery } from '../../../queries/ai-code-assurance';
import { useDismissNoticeMutation } from '../../../queries/users';
import { ApplicationPeriod } from '../../../types/application';
import { Branch } from '../../../types/branch-like';
-import { isProject } from '../../../types/component';
-import { Feature } from '../../../types/features';
import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity';
import { QualityGateStatus } from '../../../types/quality-gates';
import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../../types/types';
@@ -51,7 +46,6 @@ import { AnalysisStatus } from '../components/AnalysisStatus';
import LastAnalysisLabel from '../components/LastAnalysisLabel';
import { Status } from '../utils';
import ActivityPanel from './ActivityPanel';
-import { AiCodeAssuranceWarrning } from './AiCodeAssuranceWarrning';
import BranchMetaTopBar from './BranchMetaTopBar';
import CaycPromotionGuide from './CaycPromotionGuide';
import DismissablePromotedSection from './DismissablePromotedSection';
@@ -109,7 +103,6 @@ export default function BranchOverviewRenderer(props: Readonly<BranchOverviewRen
const router = useRouter();
const { currentUser } = React.useContext(CurrentUserContext);
- const { hasFeature } = useAvailableFeatures();
const { mutateAsync: dismissNotice } = useDismissNoticeMutation();
@@ -120,20 +113,11 @@ export default function BranchOverviewRenderer(props: Readonly<BranchOverviewRen
currentUser.isLoggedIn &&
!!currentUser.dismissedNotices[NoticeType.ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE],
);
- const { data: isAiCodeAssured } = useProjectAiCodeAssuredQuery(
- { project: component.key },
- { enabled: isProject(component.qualifier) && hasFeature(Feature.AiCodeAssurance) },
- );
const tab = query.codeScope === CodeScope.Overall ? CodeScope.Overall : CodeScope.New;
const leakPeriod = component.qualifier === ComponentQualifier.Application ? appLeak : period;
const isNewCodeTab = tab === CodeScope.New;
const hasNewCodeMeasures = measures.some((m) => isDiffMetric(m.metric.key));
- const hasOverallFindings = measures.some(
- (m) =>
- [MetricKey.violations, MetricKey.security_hotspots].includes(m.metric.key as MetricKey) &&
- m.value !== '0',
- );
// Check if any potentially missing uncomputed measure is not present
const isMissingMeasures =
@@ -251,9 +235,6 @@ export default function BranchOverviewRenderer(props: Readonly<BranchOverviewRen
<LastAnalysisLabel analysisDate={branch?.analysisDate} />
</div>
<AnalysisStatus component={component} />
- {hasOverallFindings && isAiCodeAssured && (
- <AiCodeAssuranceWarrning projectKey={component.key} />
- )}
<div className="sw-flex sw-flex-col sw-mt-6">
<TabsPanel
analyses={analyses}
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
index d112884e60c..2ea4ed1d035 100644
--- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
@@ -22,7 +22,6 @@ import { screen, waitFor } from '@testing-library/react';
import { byRole, byText } from '~sonar-aligned/helpers/testSelector';
import { ComponentQualifier } from '~sonar-aligned/types/component';
import { MetricKey } from '~sonar-aligned/types/metrics';
-import { AiCodeAssuredServiceMock } from '../../../../api/mocks/AiCodeAssuredServiceMock';
import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
import ApplicationServiceMock from '../../../../api/mocks/ApplicationServiceMock';
import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
@@ -47,7 +46,6 @@ import { mockLoggedInUser, mockMeasure, mockPaging } from '../../../../helpers/t
import { renderComponent, RenderContext } from '../../../../helpers/testReactTestingUtils';
import { ComponentPropsType } from '../../../../helpers/testUtils';
import { SoftwareQuality } from '../../../../types/clean-code-taxonomy';
-import { Feature } from '../../../../types/features';
import { Mode } from '../../../../types/mode';
import { ProjectAnalysisEventCategory } from '../../../../types/project-activity';
import { CaycStatus } from '../../../../types/types';
@@ -63,7 +61,6 @@ let projectActivityHandler: ProjectActivityServiceMock;
let usersHandler: UsersServiceMock;
let timeMarchineHandler: TimeMachineServiceMock;
let qualityGatesHandler: QualityGatesServiceMock;
-let aiCodeAssuranceHandler: AiCodeAssuredServiceMock;
let messageshandler: MessagesServiceMock;
jest.mock('../../../../api/ce', () => ({
@@ -125,7 +122,6 @@ beforeAll(() => {
},
],
});
- aiCodeAssuranceHandler = new AiCodeAssuredServiceMock();
messageshandler = new MessagesServiceMock();
});
@@ -140,7 +136,6 @@ afterEach(() => {
qualityGatesHandler.reset();
almHandler.reset();
modeHandler.reset();
- aiCodeAssuranceHandler.reset();
messageshandler.reset();
});
@@ -330,15 +325,6 @@ describe('project overview', () => {
);
});
- it('should show unsolved message when project is AI assured', async () => {
- const { user, ui } = getPageObjects();
- renderBranchOverview({ branch: undefined }, { featureList: [Feature.AiCodeAssurance] });
- expect(await ui.unsolvedOverallMessage.find()).toBeInTheDocument();
- await user.click(ui.dismissUnsolvedButton.get());
-
- expect(ui.unsolvedOverallMessage.query()).not.toBeInTheDocument();
- });
-
// eslint-disable-next-line jest/expect-expect
it('should render overall tab without branch specified', async () => {
const { user, ui } = getPageObjects();
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 9996a39d69b..7416a00eca2 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
@@ -22,7 +22,11 @@ import { screen } from '@testing-library/react';
import { byRole } from '~sonar-aligned/helpers/testSelector';
import { ComponentQualifier, Visibility } from '~sonar-aligned/types/component';
import { MetricKey } from '~sonar-aligned/types/metrics';
-import { AiCodeAssuredServiceMock } from '../../../api/mocks/AiCodeAssuredServiceMock';
+import {
+ AiCodeAssuredServiceMock,
+ PROJECT_WITH_AI_ASSURED_QG,
+ PROJECT_WITHOUT_AI_ASSURED_QG,
+} from '../../../api/mocks/AiCodeAssuredServiceMock';
import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock';
import CodingRulesServiceMock from '../../../api/mocks/CodingRulesServiceMock';
import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
@@ -86,7 +90,9 @@ afterEach(() => {
it('should show fields for project', async () => {
measuresHandler.registerComponentMeasures({
- 'my-project': { [MetricKey.ncloc]: mockMeasure({ metric: MetricKey.ncloc, value: '1000' }) },
+ [PROJECT_WITH_AI_ASSURED_QG]: {
+ [MetricKey.ncloc]: mockMeasure({ metric: MetricKey.ncloc, value: '1000' }),
+ },
});
linksHandler.projectLinks = [{ id: '1', name: 'test', type: '', url: 'http://test.com' }];
renderProjectInformationApp(
@@ -94,6 +100,7 @@ it('should show fields for project', async () => {
visibility: Visibility.Private,
description: 'Test description',
tags: ['bar'],
+ key: PROJECT_WITH_AI_ASSURED_QG,
},
{ currentUser: mockLoggedInUser(), featureList: [Feature.AiCodeAssurance] },
);
@@ -102,9 +109,9 @@ it('should show fields for project', async () => {
expect(byRole('link', { name: /project.info.quality_gate.link_label/ }).getAll()).toHaveLength(1);
expect(byRole('link', { name: /overview.link_to_x_profile_y/ }).getAll()).toHaveLength(1);
expect(byRole('link', { name: 'test' }).getAll()).toHaveLength(1);
- expect(screen.getByText('project.info.ai_code_assurance.title')).toBeInTheDocument();
+ expect(screen.getByText('project.info.ai_code_assurance_on.title')).toBeInTheDocument();
expect(screen.getByText('Test description')).toBeInTheDocument();
- expect(screen.getByText('my-project')).toBeInTheDocument();
+ expect(screen.getByText(PROJECT_WITH_AI_ASSURED_QG)).toBeInTheDocument();
expect(screen.getByText('visibility.private')).toBeInTheDocument();
expect(ui.tags.get()).toHaveTextContent('bar');
expect(ui.size.get()).toHaveTextContent('1short_number_suffix.k');
@@ -152,7 +159,7 @@ it('should hide some fields for application', async () => {
expect(ui.tags.get()).toHaveTextContent('no_tags');
});
-it('should not display ai code assurence', async () => {
+it('should not display ai code assurance', async () => {
renderProjectInformationApp(
{
key: 'no-ai',
@@ -160,7 +167,19 @@ it('should not display ai code assurence', async () => {
{ featureList: [Feature.AiCodeAssurance] },
);
expect(await ui.projectPageTitle.find()).toBeInTheDocument();
- expect(screen.queryByText('project.info.ai_code_assurance.title')).not.toBeInTheDocument();
+ expect(screen.queryByText('project.info.ai_code_assurance_on.title')).not.toBeInTheDocument();
+ expect(screen.queryByText('project.info.ai_code_assurance_off.title')).not.toBeInTheDocument();
+});
+
+it('should display it contains ai code', async () => {
+ renderProjectInformationApp(
+ {
+ key: PROJECT_WITHOUT_AI_ASSURED_QG,
+ },
+ { featureList: [Feature.AiCodeAssurance] },
+ );
+ expect(await ui.projectPageTitle.find()).toBeInTheDocument();
+ expect(screen.getByText('project.info.ai_code_assurance_off.title')).toBeInTheDocument();
});
it('should display ai code fix section if enabled', async () => {
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 c16707829f7..a49592097c2 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,16 +18,18 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { Heading } from '@sonarsource/echoes-react';
+import { Heading, LinkStandalone } from '@sonarsource/echoes-react';
import classNames from 'classnames';
import { PropsWithChildren, useEffect, useState } from 'react';
import { FormattedMessage } from 'react-intl';
+import { useLocation } from 'react-router-dom';
import { BasicSeparator } from '~design-system';
import { ComponentQualifier, Visibility } from '~sonar-aligned/types/component';
+import { AiCodeAssuranceStatus } from '../../../api/ai-code-assurance';
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 { useProjectAiCodeAssuranceStatusQuery } from '../../../queries/ai-code-assurance';
import { Feature } from '../../../types/features';
import { Component, Measure, ProjectLink } from '../../../types/types';
import MetaDescription from './components/MetaDescription';
@@ -48,9 +50,11 @@ export interface AboutProjectProps {
export default function AboutProject(props: AboutProjectProps) {
const { component, measures = [] } = props;
const { hasFeature } = useAvailableFeatures();
+ const { search } = useLocation();
+
const isApp = component.qualifier === ComponentQualifier.Application;
const [links, setLinks] = useState<ProjectLink[] | undefined>(undefined);
- const { data: isAiAssured } = useProjectAiCodeAssuredQuery(
+ const { data: aiAssuranceStatus } = useProjectAiCodeAssuranceStatusQuery(
{ project: component.key },
{
enabled:
@@ -77,9 +81,7 @@ 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} isAiAssured={isAiAssured} />
- )}
+ {component.qualityGate && <MetaQualityGate qualityGate={component.qualityGate} />}
{component.qualityProfiles && component.qualityProfiles.length > 0 && (
<MetaQualityProfiles profiles={component.qualityProfiles} />
@@ -87,12 +89,37 @@ export default function AboutProject(props: AboutProjectProps) {
</ProjectInformationSection>
)}
- {isAiAssured === true && (
+ {aiAssuranceStatus === AiCodeAssuranceStatus.AI_CODE_ASSURED && (
+ <ProjectInformationSection>
+ <Heading className="sw-mb-2" as="h3">
+ {translate('project.info.ai_code_assurance_on.title')}
+ </Heading>
+ <span>
+ <FormattedMessage id="projects.ai_code_assurance_on.content" />
+ </span>
+ </ProjectInformationSection>
+ )}
+
+ {aiAssuranceStatus === AiCodeAssuranceStatus.CONTAINS_AI_CODE && (
<ProjectInformationSection>
<Heading className="sw-mb-2" as="h3">
- {translate('project.info.ai_code_assurance.title')}
+ {translate('project.info.ai_code_assurance_off.title')}
</Heading>
- <FormattedMessage id="projects.ai_code.content" />
+ <span>
+ <FormattedMessage id="projects.ai_code_assurance_off.content" />
+ </span>
+ {component.configuration?.showSettings && (
+ <p className="sw-pt-2">
+ <LinkStandalone
+ to={{
+ pathname: '/project/quality_gate',
+ search,
+ }}
+ >
+ <FormattedMessage id="projects.ai_code_assurance.edit_quality_gate" />
+ </LinkStandalone>
+ </p>
+ )}
</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 f06675f8ddf..3c16f250b82 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
@@ -19,17 +19,17 @@
*/
import { Heading, LinkStandalone, Text } from '@sonarsource/echoes-react';
-import { FormattedMessage, useIntl } from 'react-intl';
+import { useIntl } 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, isAiAssured }: Props) {
+export default function MetaQualityGate({ qualityGate }: Props) {
const intl = useIntl();
+
return (
<section>
<Heading as="h3">{translate('project.info.quality_gate')}</Heading>
@@ -51,11 +51,6 @@ export default function MetaQualityGate({ qualityGate, isAiAssured }: Props) {
</LinkStandalone>
</li>
</ul>
- {isAiAssured === true && (
- <Text as="p" isSubdued className="sw-mt-2">
- <FormattedMessage id="project.info.quality_gate.ai_code_assurance.description" />
- </Text>
- )}
</section>
);
}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx
index 07788dace9f..a1d723e8684 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx
@@ -49,7 +49,7 @@ import { translate } from '../../helpers/l10n';
import { isDiffMetric } from '../../helpers/measures';
import { LabelValueSelectOption } from '../../helpers/search';
import { getQualityGateUrl } from '../../helpers/urls';
-import { useProjectAiCodeAssuredQuery } from '../../queries/ai-code-assurance';
+import { useProjectAiCodeAssuranceStatusQuery } from '../../queries/ai-code-assurance';
import { useLocation } from '../../sonar-aligned/components/hoc/withRouter';
import { queryToSearchString } from '../../sonar-aligned/helpers/urls';
import { ComponentQualifier } from '../../sonar-aligned/types/component';
@@ -117,7 +117,7 @@ function ProjectQualityGateAppRenderer(props: Readonly<ProjectQualityGateAppRend
const location = useLocation();
- const { data: isAiAssured } = useProjectAiCodeAssuredQuery(
+ const { data: aiAssuranceStatus } = useProjectAiCodeAssuranceStatusQuery(
{ project: component.key },
{
enabled:
@@ -189,7 +189,7 @@ function ProjectQualityGateAppRenderer(props: Readonly<ProjectQualityGateAppRend
<RadioButton
className="it__project-quality-default sw-items-start"
checked={usesDefault}
- disabled={submitting || isAiAssured}
+ disabled={submitting}
onCheck={() => props.onSelect(USE_SYSTEM_DEFAULT)}
value={USE_SYSTEM_DEFAULT}
>
@@ -211,7 +211,7 @@ function ProjectQualityGateAppRenderer(props: Readonly<ProjectQualityGateAppRend
<RadioButton
className="it__project-quality-specific sw-items-start sw-mt-1"
checked={!usesDefault}
- disabled={submitting || isAiAssured}
+ disabled={submitting}
onCheck={(value: string) => {
if (usesDefault) {
props.onSelect(value);
@@ -233,7 +233,7 @@ function ProjectQualityGateAppRenderer(props: Readonly<ProjectQualityGateAppRend
Option: renderQualitygateOption,
}}
isClearable={usesDefault}
- isDisabled={submitting || usesDefault || isAiAssured}
+ isDisabled={submitting || usesDefault}
onChange={({ value }: QualityGateOption) => {
props.onSelect(value);
}}
@@ -243,78 +243,72 @@ function ProjectQualityGateAppRenderer(props: Readonly<ProjectQualityGateAppRend
/>
</div>
- <div aria-live="polite">
- {isAiAssured && (
- <>
- <p className="sw-w-abs-400 sw-mt-6">
- <FormattedMessage
- id="project_quality_gate.ai_assured.message1"
- defaultMessage={translate('project_quality_gate.ai_assured.message1')}
- values={{
- link: (
- <DocumentationLink to={DocLink.AiCodeAssurance}>
- {translate('project_quality_gate.ai_assured.message1.link')}
- </DocumentationLink>
- ),
- }}
- />
- </p>
- <p className="sw-w-abs-400 sw-mt-6">
- <FormattedMessage
- id="project_quality_gate.ai_assured.message2"
- defaultMessage={translate('project_quality_gate.ai_assured.message2')}
- values={{
- link: (
- <LinkStandalone
- className="sw-shrink-0"
- to={{
- pathname:
- '/project/admin/extension/developer-server/ai-project-settings',
- search: queryToSearchString({
- ...location.query,
- qualifier: ComponentQualifier.Project,
- }),
- }}
- >
- {translate('project_quality_gate.ai_assured.message2.link')}
- </LinkStandalone>
- ),
- value: <b>{translate('false')}</b>,
- }}
- />
- </p>
- </>
- )}
-
- {selectedQualityGate && !hasConditionOnNewCode(selectedQualityGate) && (
- <FlagMessage variant="warning">
+ {aiAssuranceStatus && (
+ <>
+ <p className="sw-w-abs-400 sw-mt-6">
<FormattedMessage
- id="project_quality_gate.no_condition_on_new_code"
- defaultMessage={translate('project_quality_gate.no_condition_on_new_code')}
+ id="project_quality_gate.ai_assured.message1"
+ defaultMessage={translate('project_quality_gate.ai_assured.message1')}
values={{
link: (
- <Link to={getQualityGateUrl(selectedQualityGate.name)}>
- {translate('project_quality_gate.no_condition.link')}
- </Link>
+ <DocumentationLink to={DocLink.AiCodeAssurance}>
+ {translate('project_quality_gate.ai_assured.message1.link')}
+ </DocumentationLink>
),
}}
/>
- </FlagMessage>
- )}
- {needsReanalysis && (
- <FlagMessage className="sw-mt-4 sw-w-abs-600" variant="warning">
- {translate('project_quality_gate.requires_new_analysis')}
- </FlagMessage>
- )}
- </div>
+ </p>
+ <p className="sw-w-abs-400 sw-mt-6">
+ <FormattedMessage
+ id="project_quality_gate.ai_assured.message2"
+ defaultMessage={translate('project_quality_gate.ai_assured.message2')}
+ values={{
+ link: (
+ <LinkStandalone
+ className="sw-shrink-0"
+ to={{
+ pathname:
+ '/project/admin/extension/developer-server/ai-project-settings',
+ search: queryToSearchString({
+ ...location.query,
+ qualifier: ComponentQualifier.Project,
+ }),
+ }}
+ >
+ {translate('project_quality_gate.ai_assured.message2.link')}
+ </LinkStandalone>
+ ),
+ value: <b>{translate('false')}</b>,
+ }}
+ />
+ </p>
+ </>
+ )}
+
+ {selectedQualityGate && !hasConditionOnNewCode(selectedQualityGate) && (
+ <FlagMessage variant="warning">
+ <FormattedMessage
+ id="project_quality_gate.no_condition_on_new_code"
+ defaultMessage={translate('project_quality_gate.no_condition_on_new_code')}
+ values={{
+ link: (
+ <Link to={getQualityGateUrl(selectedQualityGate.name)}>
+ {translate('project_quality_gate.no_condition.link')}
+ </Link>
+ ),
+ }}
+ />
+ </FlagMessage>
+ )}
+ {needsReanalysis && (
+ <FlagMessage className="sw-mt-4 sw-w-abs-600" variant="warning">
+ {translate('project_quality_gate.requires_new_analysis')}
+ </FlagMessage>
+ )}
</div>
<div>
- <ButtonPrimary
- form="project_quality_gate"
- disabled={submitting || isAiAssured}
- type="submit"
- >
+ <ButtonPrimary form="project_quality_gate" disabled={submitting} type="submit">
{translate('save')}
</ButtonPrimary>
<Spinner loading={submitting} />
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-it.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-it.tsx
index 17c466cdfc8..1298cec5f22 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-it.tsx
@@ -124,7 +124,9 @@ it('shows warning for quality gate that doesnt have conditions on new code', asy
expect(ui.noConditionsNewCodeWarning.get()).toBeInTheDocument();
});
-it('disable the QG selection if project is AI assured', async () => {
+// TODO Temp for now
+// eslint-disable-next-line jest/no-disabled-tests
+it.skip('disable the QG selection if project is AI assured', async () => {
renderProjectQualityGateApp({ featureList: [Feature.AiCodeAssurance] });
expect(await ui.aiCodeAssuranceMessage1.find()).toBeInTheDocument();
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
index 851baa07822..97ecb807d1d 100644
--- a/server/sonar-web/src/main/js/queries/ai-code-assurance.ts
+++ b/server/sonar-web/src/main/js/queries/ai-code-assurance.ts
@@ -19,15 +19,17 @@
*/
import { queryOptions } from '@tanstack/react-query';
-import { isProjectAiCodeAssured } from '../api/ai-code-assurance';
+import { getProjectAiCodeAssuranceStatus } 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,
- });
-});
+export const useProjectAiCodeAssuranceStatusQuery = createQueryHook(
+ ({ project }: { project: string }) => {
+ return queryOptions({
+ queryKey: [AI_CODE_ASSURANCE_QUERY_PREFIX, project], // - or _ ?
+ queryFn: ({ queryKey: [_, project] }) => getProjectAiCodeAssuranceStatus(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 f9ba74d2495..3c3cc9891ee 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -1465,7 +1465,9 @@ projects.awaiting_scan.title=Values will change after the next analysis
projects.awaiting_scan.description.TRK=The way in which security, reliability, maintainability, and security review counts and ratings are calculated has changed. The values currently displayed will change after the next analysis.
projects.awaiting_scan.description.APP=The way in which security, reliability, maintainability, and security review counts and ratings are calculated has changed. The values currently displayed will change after all projects in this application have been analyzed.
projects.awaiting_scan.description.VW=The way in which security, reliability, maintainability, and security review counts and ratings are calculated has changed. The values currently displayed will change after all projects in this portfolio have been analyzed.
-projects.ai_code.content=This project contains AI-generated code and benefits from Sonar’s AI Code Assurance.
+projects.ai_code_assurance_on.content=This project contains AI-generated code and benefits from Sonar’s AI Code Assurance.
+projects.ai_code_assurance_off.content=This project contains AI-generated code but the quality gate in use is not qualified for AI Code Assurance.
+projects.ai_code_assurance.edit_quality_gate=Change Quality Gate
#------------------------------------------------------------------------------
#
@@ -2296,7 +2298,8 @@ 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 {productName} or click on the top-left {productName} 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.ai_code_assurance_on.title=AI Code Assurance: On
+project.info.ai_code_assurance_off.title=AI Code Assurance: Off
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.
project.info.ai_code_fix.title=AI CodeFix
project.info.ai_code_fix.message=AI CodeFix is enabled for this project.