aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>2024-11-26 14:01:20 +0100
committersonartech <sonartech@sonarsource.com>2024-11-29 20:03:08 +0000
commit318799ac3c16659383b30ad47cc2ad716deb42eb (patch)
tree3c5dd7a1d7d7b0322b0242c80a52b8951384e156 /server
parent525a2ccd20a8893e1598e4fdf9dccf16100689dd (diff)
downloadsonarqube-318799ac3c16659383b30ad47cc2ad716deb42eb.tar.gz
sonarqube-318799ac3c16659383b30ad47cc2ad716deb42eb.zip
SONAR-23619 Updating projects quality gate page with new code assurance feature
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/AiAssuranceSuccessMessage.tsx37
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/AiAssuranceWarningMessage.tsx37
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx291
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-it.tsx100
-rw-r--r--server/sonar-web/src/main/js/components/ui/AiCodeAssuranceBanner.tsx82
-rw-r--r--server/sonar-web/src/main/js/helpers/doc-links.ts1
-rw-r--r--server/sonar-web/src/main/js/queries/mode.ts4
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(
{