]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23064 Lock projects quality gate settings if project has ai generated code
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Tue, 24 Sep 2024 08:15:18 +0000 (10:15 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 25 Sep 2024 20:02:54 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx
server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-it.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 1f010656856fb1f01b47fdaa68c6dbed53c06684..4493989259689e260894e8bebdf2a75253ceb675 100644 (file)
@@ -184,12 +184,15 @@ class ProjectQualityGateApp extends React.PureComponent<Props, State> {
       return null;
     }
 
+    const { component } = this.props;
+
     const { allQualityGates, currentQualityGate, loading, selectedQualityGateName, submitting } =
       this.state;
 
     return (
       <ProjectQualityGateAppRenderer
         allQualityGates={allQualityGates}
+        component={component}
         currentQualityGate={currentQualityGate}
         loading={loading}
         onSubmit={this.handleSubmit}
index fb03e5d49aefbe88b92dfbc18b0dcd83161ff4e0..8378b1c68bce44e59e4a80cc9d5045a05af6e12e 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
+import { LinkStandalone } from '@sonarsource/echoes-react';
 import {
   ButtonPrimary,
   FlagMessage,
@@ -38,19 +39,29 @@ import { FormattedMessage } from 'react-intl';
 import { OptionProps, components } from 'react-select';
 import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
 import HelpTooltip from '~sonar-aligned/components/controls/HelpTooltip';
+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 { 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 { QualityGate } from '../../types/types';
+import { useProjectAiCodeAssuredQuery } 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 { USE_SYSTEM_DEFAULT } from './constants';
 
-export interface ProjectQualityGateAppRendererProps {
+export interface ProjectQualityGateAppRendererProps extends WithAvailableFeaturesProps {
   allQualityGates?: QualityGate[];
+  component: Component;
   currentQualityGate?: QualityGate;
   loading: boolean;
   onSelect: (id: string) => void;
@@ -94,11 +105,28 @@ function renderQualitygateOption(props: OptionProps<QualityGateOption, false>) {
   );
 }
 
-export default function ProjectQualityGateAppRenderer(props: ProjectQualityGateAppRendererProps) {
-  const { allQualityGates, currentQualityGate, loading, selectedQualityGateName, submitting } =
-    props;
+function ProjectQualityGateAppRenderer(props: Readonly<ProjectQualityGateAppRendererProps>) {
+  const {
+    allQualityGates,
+    component,
+    currentQualityGate,
+    loading,
+    selectedQualityGateName,
+    submitting,
+  } = props;
   const defaultQualityGate = allQualityGates?.find((g) => g.isDefault);
 
+  const location = useLocation();
+
+  const { data: isAiAssured } = useProjectAiCodeAssuredQuery(
+    { project: component.key },
+    {
+      enabled:
+        component.qualifier === ComponentQualifier.Project &&
+        props.hasFeature(Feature.AiCodeAssurance),
+    },
+  );
+
   if (loading) {
     return <Spinner />;
   }
@@ -162,7 +190,7 @@ export default function ProjectQualityGateAppRenderer(props: ProjectQualityGateA
               <RadioButton
                 className="it__project-quality-default sw-items-start"
                 checked={usesDefault}
-                disabled={submitting}
+                disabled={submitting || isAiAssured}
                 onCheck={() => props.onSelect(USE_SYSTEM_DEFAULT)}
                 value={USE_SYSTEM_DEFAULT}
               >
@@ -184,7 +212,7 @@ export default function ProjectQualityGateAppRenderer(props: ProjectQualityGateA
               <RadioButton
                 className="it__project-quality-specific sw-items-start sw-mt-1"
                 checked={!usesDefault}
-                disabled={submitting}
+                disabled={submitting || isAiAssured}
                 onCheck={(value: string) => {
                   if (usesDefault) {
                     props.onSelect(value);
@@ -206,7 +234,7 @@ export default function ProjectQualityGateAppRenderer(props: ProjectQualityGateA
                     Option: renderQualitygateOption,
                   }}
                   isClearable={usesDefault}
-                  isDisabled={submitting || usesDefault}
+                  isDisabled={submitting || usesDefault || isAiAssured}
                   onChange={({ value }: QualityGateOption) => {
                     props.onSelect(value);
                   }}
@@ -216,6 +244,48 @@ export default function ProjectQualityGateAppRenderer(props: ProjectQualityGateA
                 />
               </div>
 
+              {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">
                   <FormattedMessage
@@ -239,7 +309,11 @@ export default function ProjectQualityGateAppRenderer(props: ProjectQualityGateA
             </div>
 
             <div>
-              <ButtonPrimary form="project_quality_gate" disabled={submitting} type="submit">
+              <ButtonPrimary
+                form="project_quality_gate"
+                disabled={submitting || isAiAssured}
+                type="submit"
+              >
                 {translate('save')}
               </ButtonPrimary>
               <Spinner loading={submitting} />
@@ -250,3 +324,5 @@ export default function ProjectQualityGateAppRenderer(props: ProjectQualityGateA
     </LargeCenteredLayout>
   );
 }
+
+export default withAvailableFeatures(ProjectQualityGateAppRenderer);
index 6be831f883b945065b049a92828dfb03b5d23c22..e8d6e0c35c3c60d4a6f087f55cbaca7be64aa7f4 100644 (file)
@@ -29,6 +29,7 @@ import {
   RenderContext,
   renderAppWithComponentContext,
 } from '../../../helpers/testReactTestingUtils';
+import { Feature } from '../../../types/features';
 import { Component } from '../../../types/types';
 import routes from '../routes';
 
@@ -42,6 +43,10 @@ jest.mock('design-system', () => ({
   addGlobalSuccessMessage: jest.fn(),
 }));
 
+jest.mock('../../../api/ai-code-assurance', () => ({
+  isProjectAiCodeAssured: jest.fn().mockResolvedValue(true),
+}));
+
 let handler: QualityGatesServiceMock;
 
 const ui = {
@@ -57,6 +62,8 @@ 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'),
 };
 
 beforeAll(() => {
@@ -114,6 +121,17 @@ 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 () => {
+  renderProjectQualityGateApp({ featureList: [Feature.AiCodeAssurance] });
+
+  expect(await ui.aiCodeAssuranceMessage1.find()).toBeInTheDocument();
+  expect(ui.aiCodeAssuranceMessage2.get()).toBeInTheDocument();
+  expect(ui.specificRadioQualityGate.get()).toBeDisabled();
+  expect(ui.defaultRadioQualityGate.get()).toBeDisabled();
+  expect(ui.qualityGatesSelect.get()).toBeDisabled();
+  expect(ui.saveButton.get()).toBeDisabled();
+});
+
 it('renders nothing and shows alert when any API fails', async () => {
   handler.setThrowOnGetGateForProject(true);
   renderProjectQualityGateApp();
index 0a43a4c953cfc35a32462b134ce79b52ba9ce38a..314dc966426205dc5571e49fdd003ce6b466a55e 100644 (file)
@@ -2162,6 +2162,10 @@ project_quality_gate.no_condition=This Quality Gate is empty. To make it usable,
 project_quality_gate.no_condition_on_new_code=This Quality Gate sets conditions on overall code but not on new code. It will not appear on pull requests. To enable it for pull requests, add conditions to the {link}.
 project_quality_gate.no_condition.link=Quality Gate definition
 project_quality_gate.no_condition.reason=No conditions
+project_quality_gate.ai_assured.message1=This project contains AI-generated code. It must use Sonar way Quality Gate to benefit from Sonar’s {link}.
+project_quality_gate.ai_assured.message1.link=AI Code Assurance
+project_quality_gate.ai_assured.message2=To change this project’s Quality Gate, first go to the project’s {link} and set “Contains AI-generated code” value to {value}.
+project_quality_gate.ai_assured.message2.link=AI-generated code settings
 
 #------------------------------------------------------------------------------
 #