diff options
Diffstat (limited to 'server/sonar-web/src/main')
12 files changed, 175 insertions, 252 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, + }); + }, +); |