diff options
author | Revanshu Paliwal <revanshu.paliwal@sonarsource.com> | 2024-11-26 14:01:20 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-11-29 20:03:08 +0000 |
commit | 318799ac3c16659383b30ad47cc2ad716deb42eb (patch) | |
tree | 3c5dd7a1d7d7b0322b0242c80a52b8951384e156 /server | |
parent | 525a2ccd20a8893e1598e4fdf9dccf16100689dd (diff) | |
download | sonarqube-318799ac3c16659383b30ad47cc2ad716deb42eb.tar.gz sonarqube-318799ac3c16659383b30ad47cc2ad716deb42eb.zip |
SONAR-23619 Updating projects quality gate page with new code assurance feature
Diffstat (limited to 'server')
8 files changed, 448 insertions, 106 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts index 83003ac58d0..8bd2e8158b3 100644 --- a/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts @@ -125,6 +125,7 @@ export class QualityGatesServiceMock { isDefault: true, isBuiltIn: false, caycStatus: CaycStatus.Compliant, + isAiCodeSupported: false, }), mockQualityGate({ name: 'SonarSource way - CFamily', @@ -200,6 +201,7 @@ export class QualityGatesServiceMock { hasStandardConditions: false, hasMQRConditions: false, caycStatus: CaycStatus.Compliant, + isAiCodeSupported: false, }), mockQualityGate({ name: 'Sonar way for AI code', diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/AiAssuranceSuccessMessage.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/AiAssuranceSuccessMessage.tsx new file mode 100644 index 00000000000..1a07f54abfe --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/AiAssuranceSuccessMessage.tsx @@ -0,0 +1,37 @@ +/* + * 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 { IconCheck, Text } from '@sonarsource/echoes-react'; +import { translate } from '../../helpers/l10n'; + +interface Props { + className?: string; +} + +export default function AiAssuranceSuccessMessage({ className }: Readonly<Props>) { + return ( + <div className={className}> + <IconCheck color="echoes-color-icon-success" /> + <Text className="sw-ml-1" colorOverride="echoes-color-text-success"> + {translate('project_quality_gate.ai_assured_quality_gate')} + </Text> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/AiAssuranceWarningMessage.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/AiAssuranceWarningMessage.tsx new file mode 100644 index 00000000000..8f9f04bb651 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/AiAssuranceWarningMessage.tsx @@ -0,0 +1,37 @@ +/* + * 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 { IconWarning, Text } from '@sonarsource/echoes-react'; +import { translate } from '../../helpers/l10n'; + +interface Props { + className?: string; +} + +export default function AiAssuranceWarningMessage({ className }: Readonly<Props>) { + return ( + <div className={className}> + <IconWarning color="echoes-color-icon-warning" /> + <Text className="sw-ml-1" colorOverride="echoes-color-text-warning"> + {translate('project_quality_gate.not_ai_assured_quality_gate')} + </Text> + </div> + ); +} 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 a1d723e8684..26e239944bb 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx @@ -18,10 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { LinkStandalone } from '@sonarsource/echoes-react'; +import { Link, LinkHighlight } from '@sonarsource/echoes-react'; +import { useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { FormattedMessage } from 'react-intl'; -import { OptionProps, components } from 'react-select'; +import { OptionProps, SingleValueProps, components } from 'react-select'; import { ButtonPrimary, FlagMessage, @@ -29,33 +30,37 @@ import { InputSelect, LargeCenteredLayout, LightLabel, - Link, PageContentFontWrapper, - PageTitle, RadioButton, Spinner, Title, } from '~design-system'; import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget'; import HelpTooltip from '~sonar-aligned/components/controls/HelpTooltip'; +import { AiCodeAssuranceStatus } from '../../api/ai-code-assurance'; import withAvailableFeatures, { WithAvailableFeaturesProps, } from '../../app/components/available-features/withAvailableFeatures'; import DisableableSelectOption from '../../components/common/DisableableSelectOption'; import DocumentationLink from '../../components/common/DocumentationLink'; import Suggestions from '../../components/embed-docs-modal/Suggestions'; +import AIAssuredIcon, { + AiIconColor, + AiIconVariant, +} from '../../components/icon-mappers/AIAssuredIcon'; +import AiCodeAssuranceBanner from '../../components/ui/AiCodeAssuranceBanner'; import { DocLink } from '../../helpers/doc-links'; import { translate } from '../../helpers/l10n'; import { isDiffMetric } from '../../helpers/measures'; import { LabelValueSelectOption } from '../../helpers/search'; import { getQualityGateUrl } from '../../helpers/urls'; 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'; import { Feature } from '../../types/features'; import { Component, QualityGate } from '../../types/types'; import BuiltInQualityGateBadge from '../quality-gates/components/BuiltInQualityGateBadge'; +import AiAssuranceSuccessMessage from './AiAssuranceSuccessMessage'; +import AiAssuranceWarningMessage from './AiAssuranceWarningMessage'; import { USE_SYSTEM_DEFAULT } from './constants'; export interface ProjectQualityGateAppRendererProps extends WithAvailableFeaturesProps { @@ -64,7 +69,7 @@ export interface ProjectQualityGateAppRendererProps extends WithAvailableFeature currentQualityGate?: QualityGate; loading: boolean; onSelect: (id: string) => void; - onSubmit: () => void; + onSubmit: () => Promise<void>; selectedQualityGateName: string; submitting: boolean; } @@ -74,36 +79,51 @@ function hasConditionOnNewCode(qualityGate: QualityGate): boolean { } interface QualityGateOption extends LabelValueSelectOption { + isAiAssured: boolean; isDisabled: boolean; } -function renderQualitygateOption(props: OptionProps<QualityGateOption, false>) { +function renderOption(data: QualityGateOption) { return ( - <components.Option {...props}> - <div> - <DisableableSelectOption - className="sw-w-[100px]" - option={props.data} - disabledReason={translate('project_quality_gate.no_condition.reason')} - disableTooltipOverlay={() => ( - <FormattedMessage - id="project_quality_gate.no_condition" - defaultMessage={translate('project_quality_gate.no_condition')} - values={{ - link: ( - <Link to={getQualityGateUrl(props.data.label)}> - {translate('project_quality_gate.no_condition.link')} - </Link> - ), - }} - /> - )} + <div className="sw-flex sw-items-center sw-justify-between"> + <DisableableSelectOption + className="sw-mr-2" + option={data} + disabledReason={translate('project_quality_gate.no_condition.reason')} + disableTooltipOverlay={() => ( + <FormattedMessage + id="project_quality_gate.no_condition" + defaultMessage={translate('project_quality_gate.no_condition')} + values={{ + link: ( + <Link to={getQualityGateUrl(data.label)}> + {translate('project_quality_gate.no_condition.link')} + </Link> + ), + }} + /> + )} + /> + {data.isAiAssured && ( + <AIAssuredIcon + variant={AiIconVariant.Default} + color={AiIconColor.Subdued} + width={16} + height={16} /> - </div> - </components.Option> + )} + </div> ); } +function renderQualityGateOption(props: OptionProps<QualityGateOption, false>) { + return <components.Option {...props}>{renderOption(props.data)}</components.Option>; +} + +function singleValueRenderer(props: SingleValueProps<QualityGateOption, false>) { + return <components.SingleValue {...props}>{renderOption(props.data)}</components.SingleValue>; +} + function ProjectQualityGateAppRenderer(props: Readonly<ProjectQualityGateAppRendererProps>) { const { allQualityGates, @@ -114,17 +134,17 @@ function ProjectQualityGateAppRenderer(props: Readonly<ProjectQualityGateAppRend submitting, } = props; const defaultQualityGate = allQualityGates?.find((g) => g.isDefault); + const [isUserEditing, setIsUserEditing] = useState(false); - const location = useLocation(); - - const { data: aiAssuranceStatus } = useProjectAiCodeAssuranceStatusQuery( - { project: component.key }, - { - enabled: - component.qualifier === ComponentQualifier.Project && - props.hasFeature(Feature.AiCodeAssurance), - }, - ); + const { data: aiAssuranceStatus, refetch: refetchAiCodeAssuranceStatus } = + useProjectAiCodeAssuranceStatusQuery( + { project: component.key }, + { + enabled: + component.qualifier === ComponentQualifier.Project && + props.hasFeature(Feature.AiCodeAssurance), + }, + ); if (loading) { return <Spinner />; @@ -149,10 +169,15 @@ function ProjectQualityGateAppRenderer(props: Readonly<ProjectQualityGateAppRend const options: QualityGateOption[] = allQualityGates.map((g) => ({ isDisabled: g.conditions === undefined || g.conditions.length === 0, + isAiAssured: g.isAiCodeSupported ?? false, label: g.name, value: g.name, })); + const containsAiCode = + aiAssuranceStatus === AiCodeAssuranceStatus.AI_CODE_ASSURED || + aiAssuranceStatus === AiCodeAssuranceStatus.CONTAINS_AI_CODE; + return ( <LargeCenteredLayout id="project-quality-gate"> <PageContentFontWrapper className="sw-my-8 sw-typo-default"> @@ -172,14 +197,100 @@ function ProjectQualityGateAppRenderer(props: Readonly<ProjectQualityGateAppRend </header> <div className="sw-flex sw-flex-col sw-items-start"> - <div> - <PageTitle as="h2" text={translate('project_quality_gate.subtitle')} /> - </div> + {aiAssuranceStatus === AiCodeAssuranceStatus.AI_CODE_ASSURED && ( + <AiCodeAssuranceBanner + className="sw-mb-10 sw-w-abs-800" + icon={ + <AIAssuredIcon + variant={AiIconVariant.Check} + color={AiIconColor.Subdued} + width={84} + height={84} + /> + } + title={ + <FormattedMessage id="project_quality_gate.ai_generated_code_protected.title" /> + } + description={ + <FormattedMessage + id="project_quality_gate.ai_generated_code_protected.description" + values={{ + p: (text) => <p>{text}</p>, + link: (text) => ( + <DocumentationLink + highlight={LinkHighlight.Default} + className="sw-inline-block" + shouldOpenInNewTab + to={DocLink.AiCodeAssurance} + > + {text} + </DocumentationLink> + ), + }} + /> + } + /> + )} + + {aiAssuranceStatus === AiCodeAssuranceStatus.CONTAINS_AI_CODE && ( + <AiCodeAssuranceBanner + className="sw-mb-10 sw-w-abs-800" + icon={ + <AIAssuredIcon + variant={AiIconVariant.Default} + color={AiIconColor.Subdued} + width={84} + height={84} + /> + } + title={ + <FormattedMessage id="project_quality_gate.ai_generated_code_not_protected.title" /> + } + description={ + <FormattedMessage + id="project_quality_gate.ai_generated_code_not_protected.description" + values={{ + p: (text) => <p>{text}</p>, + link: (text) => ( + <DocumentationLink + highlight={LinkHighlight.Default} + shouldOpenInNewTab + to={DocLink.AiCodeAssurance} + > + {text} + </DocumentationLink> + ), + linkSonarWay: (text) => ( + <Link + highlight={LinkHighlight.Default} + to={{ + pathname: '/quality_gates/show/Sonar%20AI%20way', + }} + > + {text} + </Link> + ), + linkQualifyDoc: (text) => ( + <DocumentationLink + highlight={LinkHighlight.Default} + shouldOpenInNewTab + to={DocLink.AiCodeAssuranceQualifyQualityGate} + > + {text} + </DocumentationLink> + ), + }} + /> + } + /> + )} <form - onSubmit={(e) => { + onSubmit={async (e) => { e.preventDefault(); - props.onSubmit(); + await props.onSubmit(); + setIsUserEditing(false); + refetchAiCodeAssuranceStatus(); }} id="project_quality_gate" > @@ -190,7 +301,10 @@ function ProjectQualityGateAppRenderer(props: Readonly<ProjectQualityGateAppRend className="it__project-quality-default sw-items-start" checked={usesDefault} disabled={submitting} - onCheck={() => props.onSelect(USE_SYSTEM_DEFAULT)} + onCheck={() => { + setIsUserEditing(true); + props.onSelect(USE_SYSTEM_DEFAULT); + }} value={USE_SYSTEM_DEFAULT} > <div> @@ -199,10 +313,34 @@ function ProjectQualityGateAppRenderer(props: Readonly<ProjectQualityGateAppRend </div> <div> <LightLabel> - {translate('current_noun')}:{defaultQualityGate.name} - {defaultQualityGate.isBuiltIn && <BuiltInQualityGateBadge />} + {translate('current_noun')}: {defaultQualityGate.name} + {defaultQualityGate.isAiCodeSupported && ( + <AIAssuredIcon + className="sw-ml-1" + variant={AiIconVariant.Default} + color={AiIconColor.Subdued} + width={16} + height={16} + /> + )} + {defaultQualityGate.isBuiltIn && ( + <BuiltInQualityGateBadge className="sw-ml-1" /> + )} </LightLabel> </div> + {containsAiCode && + isUserEditing && + usesDefault && + defaultQualityGate.isAiCodeSupported === true && ( + <AiAssuranceSuccessMessage className="sw-mt-1" /> + )} + + {containsAiCode && + isUserEditing && + usesDefault && + defaultQualityGate.isAiCodeSupported === false && ( + <AiAssuranceWarningMessage className="sw-mt-1" /> + )} </div> </RadioButton> </div> @@ -213,6 +351,7 @@ function ProjectQualityGateAppRenderer(props: Readonly<ProjectQualityGateAppRend checked={!usesDefault} disabled={submitting} onCheck={(value: string) => { + setIsUserEditing(true); if (usesDefault) { props.onSelect(value); } @@ -230,11 +369,13 @@ function ProjectQualityGateAppRenderer(props: Readonly<ProjectQualityGateAppRend size="large" className="it__project-quality-gate-select" components={{ - Option: renderQualitygateOption, + Option: renderQualityGateOption, + SingleValue: singleValueRenderer, }} isClearable={usesDefault} isDisabled={submitting || usesDefault} onChange={({ value }: QualityGateOption) => { + setIsUserEditing(true); props.onSelect(value); }} aria-label={translate('project_quality_gate.select_specific_qg')} @@ -242,48 +383,20 @@ function ProjectQualityGateAppRenderer(props: Readonly<ProjectQualityGateAppRend value={options.find((o) => o.value === selectedQualityGateName)} /> </div> + {containsAiCode && + isUserEditing && + !usesDefault && + selectedQualityGate?.isAiCodeSupported === true && ( + <AiAssuranceSuccessMessage className="sw-mt-1 sw-ml-6" /> + )} - {aiAssuranceStatus && ( - <> - <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> - </> - )} + {containsAiCode && + isUserEditing && + !usesDefault && + selectedQualityGate && + selectedQualityGate.isAiCodeSupported === false && ( + <AiAssuranceWarningMessage className="sw-mt-1 sw-ml-6" /> + )} {selectedQualityGate && !hasConditionOnNewCode(selectedQualityGate) && ( <FlagMessage variant="warning"> 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 1298cec5f22..9b9ed79a9f2 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 @@ -22,12 +22,17 @@ import { waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { addGlobalErrorMessage, addGlobalSuccessMessage } from '~design-system'; import { byRole, byText } from '~sonar-aligned/helpers/testSelector'; +import { + AiCodeAssuredServiceMock, + PROJECT_WITH_AI_ASSURED_QG, + PROJECT_WITHOUT_AI_ASSURED_QG, +} from '../../../api/mocks/AiCodeAssuredServiceMock'; import { QualityGatesServiceMock } from '../../../api/mocks/QualityGatesServiceMock'; import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization'; import { mockComponent } from '../../../helpers/mocks/component'; import { - RenderContext, renderAppWithComponentContext, + RenderContext, } from '../../../helpers/testReactTestingUtils'; import { Feature } from '../../../types/features'; import { Component } from '../../../types/types'; @@ -43,11 +48,8 @@ jest.mock('~design-system', () => ({ addGlobalSuccessMessage: jest.fn(), })); -jest.mock('../../../api/ai-code-assurance', () => ({ - isProjectAiCodeAssured: jest.fn().mockResolvedValue(true), -})); - let handler: QualityGatesServiceMock; +const aiCodeAssurance = new AiCodeAssuredServiceMock(); const ui = { qualityGateHeading: byRole('heading', { name: 'project_quality_gate.page' }), @@ -62,15 +64,20 @@ const ui = { saveButton: byRole('button', { name: 'save' }), noConditionsNewCodeWarning: byText('project_quality_gate.no_condition_on_new_code'), - aiCodeAssuranceMessage1: byText('project_quality_gate.ai_assured.message1'), - aiCodeAssuranceMessage2: byText('project_quality_gate.ai_assured.message2'), + aiAssuredBanner: byText('project_quality_gate.ai_generated_code_protected.description'), + containsAiCodeBanner: byText('project_quality_gate.ai_generated_code_not_protected.description'), + qgAssuredSelctedSuccessMessage: byText('project_quality_gate.ai_assured_quality_gate'), + qgAssuredNotSelectedWarningMessage: byText('project_quality_gate.not_ai_assured_quality_gate'), }; beforeAll(() => { handler = new QualityGatesServiceMock(); }); -afterEach(() => handler.reset()); +afterEach(() => { + handler.reset(); + aiCodeAssurance.reset(); +}); it('should require authorization if no permissions set', async () => { renderProjectQualityGateApp({}, {}); @@ -124,17 +131,76 @@ it('shows warning for quality gate that doesnt have conditions on new code', asy expect(ui.noConditionsNewCodeWarning.get()).toBeInTheDocument(); }); -// 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] }); +it('should show AI assured banner if project is AI assured', async () => { + renderProjectQualityGateApp( + { featureList: [Feature.AiCodeAssurance] }, + { + configuration: { showQualityGates: true }, + key: PROJECT_WITH_AI_ASSURED_QG, + name: PROJECT_WITH_AI_ASSURED_QG, + }, + ); + + expect(await ui.aiAssuredBanner.find()).toBeInTheDocument(); +}); + +it('should show contains AI code banner if project contains AI code but quality gate is not correct', async () => { + renderProjectQualityGateApp( + { featureList: [Feature.AiCodeAssurance] }, + { + configuration: { showQualityGates: true }, + key: PROJECT_WITHOUT_AI_ASSURED_QG, + name: PROJECT_WITHOUT_AI_ASSURED_QG, + }, + ); + + expect(await ui.containsAiCodeBanner.find()).toBeInTheDocument(); +}); + +it('should not show any AI code banner if ai code feature is false', async () => { + renderProjectQualityGateApp( + { featureList: [Feature.AiCodeAssurance] }, + { + configuration: { showQualityGates: true }, + key: 'no-ai-code', + name: 'no-ai-code', + }, + ); - expect(await ui.aiCodeAssuranceMessage1.find()).toBeInTheDocument(); - expect(ui.aiCodeAssuranceMessage2.get()).toBeInTheDocument(); - expect(ui.specificRadioQualityGate.get()).toBeDisabled(); - expect(ui.defaultRadioQualityGate.get()).toBeDisabled(); + expect(await ui.qualityGateHeading.find()).toBeInTheDocument(); + expect(ui.aiAssuredBanner.query()).not.toBeInTheDocument(); + expect(ui.containsAiCodeBanner.query()).not.toBeInTheDocument(); +}); + +it('should show success/warning when selecting quality gate', async () => { + const user = userEvent.setup(); + + renderProjectQualityGateApp( + { featureList: [Feature.AiCodeAssurance] }, + { + configuration: { showQualityGates: true }, + key: PROJECT_WITH_AI_ASSURED_QG, + name: PROJECT_WITH_AI_ASSURED_QG, + }, + ); + + expect(await ui.aiAssuredBanner.find()).toBeInTheDocument(); + + await user.click(ui.specificRadioQualityGate.get()); + expect(ui.qualityGatesSelect.get()).toBeEnabled(); + + await user.click(ui.qualityGatesSelect.get()); + await user.click(byText('Sonar way for AI code').get()); + expect(ui.qgAssuredSelctedSuccessMessage.get()).toBeInTheDocument(); + + await user.click(ui.qualityGatesSelect.get()); + await user.click(byText('Sonar way').get()); + expect(ui.qgAssuredNotSelectedWarningMessage.get()).toBeInTheDocument(); + + await user.click(ui.defaultRadioQualityGate.get()); expect(ui.qualityGatesSelect.get()).toBeDisabled(); - expect(ui.saveButton.get()).toBeDisabled(); + expect(ui.defaultRadioQualityGate.get()).toBeChecked(); + expect(ui.qgAssuredNotSelectedWarningMessage.get()).toBeInTheDocument(); }); it('renders nothing and shows alert when any API fails', async () => { diff --git a/server/sonar-web/src/main/js/components/ui/AiCodeAssuranceBanner.tsx b/server/sonar-web/src/main/js/components/ui/AiCodeAssuranceBanner.tsx new file mode 100644 index 00000000000..5b5b8d388df --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/AiCodeAssuranceBanner.tsx @@ -0,0 +1,82 @@ +/* + * 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 { Heading } from '@sonarsource/echoes-react'; + +interface AiCodeAssuranceBannerProps { + className?: string; + description: React.ReactNode; + icon: React.ReactNode; + title: React.ReactNode; +} + +function AiCodeAssuranceBanner({ + className, + icon, + title, + description, +}: Readonly<AiCodeAssuranceBannerProps>) { + return ( + <StyledWrapper className={className}> + <MessageContainer> + <LeftContent> + {icon} + <TextWrapper> + <PromotedHeading as="h3">{title}</PromotedHeading> + {description} + </TextWrapper> + </LeftContent> + </MessageContainer> + </StyledWrapper> + ); +} + +export default AiCodeAssuranceBanner; + +const StyledWrapper = styled.div` + background-color: var(--echoes-color-background-accent-weak-default); + border: 1px solid var(--echoes-color-border-weak); + padding-left: var(--echoes-dimension-space-300); + border-radius: var(--echoes-border-radius-400); +`; + +const MessageContainer = styled.div` + padding-top: var(--echoes-dimension-space-100); + padding-bottom: var(--echoes-dimension-space-100); +`; + +const LeftContent = styled.div` + display: flex; + align-items: center; + gap: var(--echoes-border-radius-400); +`; + +const TextWrapper = styled.div` + display: flex; + flex-direction: column; + padding-top: var(--echoes-dimension-space-100); + padding-bottom: var(--echoes-dimension-space-100); + gap: var(--echoes-border-radius-400); +`; + +const PromotedHeading = styled(Heading)` + color: var(--echoes-color-text-accent-bold); +`; diff --git a/server/sonar-web/src/main/js/helpers/doc-links.ts b/server/sonar-web/src/main/js/helpers/doc-links.ts index a846d31cf84..393dcbea3e2 100644 --- a/server/sonar-web/src/main/js/helpers/doc-links.ts +++ b/server/sonar-web/src/main/js/helpers/doc-links.ts @@ -29,6 +29,7 @@ export enum DocLink { ActiveVersions = '/server-upgrade-and-maintenance/upgrade/upgrade-the-server/active-versions/', AiCodeAssurance = '/user-guide/ai-features/', AiCodeFixEnabling = '/instance-administration/system-functions/managing-ai-features/#enabling-ai-generated-fix-suggestions', + AiCodeAssuranceQualifyQualityGate = '/instance-administration/analysis-functions/ai-standards/#apply-qualified-quality-gate', AlmAzureIntegration = '/devops-platform-integration/azure-devops-integration/', AlmBitBucketCloudAuth = '/instance-administration/authentication/bitbucket-cloud/', AlmBitBucketCloudIntegration = '/devops-platform-integration/bitbucket-integration/bitbucket-cloud-integration/', diff --git a/server/sonar-web/src/main/js/queries/mode.ts b/server/sonar-web/src/main/js/queries/mode.ts index 2cc9e955ce5..51dcdfedfe8 100644 --- a/server/sonar-web/src/main/js/queries/mode.ts +++ b/server/sonar-web/src/main/js/queries/mode.ts @@ -47,7 +47,11 @@ export function useUpdateModeMutation() { return useMutation({ mutationFn: (mode: Mode) => updateMode(mode), onSuccess: (res) => { + // This can have a broader side effect on the backend + // Let's remove all frontend cache. + queryClient.invalidateQueries(); queryClient.setQueryData<ModeResponse>(['mode'], res); + addGlobalSuccessMessage( intl.formatMessage( { |