]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13154 Prevent assigning projects to Quality Gate with no conditions, or setting...
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Thu, 12 Aug 2021 11:32:26 +0000 (13:32 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 13 Aug 2021 20:03:54 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/DetailsContent-test.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/DetailsHeader-test.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Projects-test.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsContent-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsHeader-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/Projects-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index ca5f4f0c64ff012d270160816b9b13419113475f..9f0476dfc33614005ad96d4bcc1db80d2c96a350 100644 (file)
@@ -19,6 +19,7 @@
  */
 import * as React from 'react';
 import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
+import { Alert } from 'sonar-ui-common/components/ui/Alert';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import Conditions from './Conditions';
 import Projects from './Projects';
@@ -40,6 +41,12 @@ export function DetailsContent(props: DetailsContentProps) {
 
   return (
     <div className="layout-page-main-inner">
+      {isDefault && (qualityGate.conditions === undefined || qualityGate.conditions.length === 0) && (
+        <Alert className="big-spacer-bottom" variant="warning">
+          {translate('quality_gates.is_default_no_conditions')}
+        </Alert>
+      )}
+
       <Conditions
         canEdit={actions.manageConditions}
         conditions={conditions}
index d937fb8616f36c5648f2fb95c1493ea8fc959bbe..924fc33e6182cd9506d146b523369e43b7023534 100644 (file)
@@ -20,6 +20,7 @@
 import * as React from 'react';
 import { Button } from 'sonar-ui-common/components/controls/buttons';
 import ModalButton from 'sonar-ui-common/components/controls/ModalButton';
+import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import { setQualityGateAsDefault } from '../../../api/quality-gates';
 import BuiltInQualityGateBadge from './BuiltInQualityGateBadge';
@@ -58,6 +59,8 @@ export default class DetailsHeader extends React.PureComponent<Props> {
   render() {
     const { qualityGate } = this.props;
     const actions = qualityGate.actions || ({} as any);
+    const hasNoConditions =
+      qualityGate.conditions === undefined || qualityGate.conditions.length === 0;
     return (
       <div className="layout-page-header-panel layout-page-main-header issues-main-header">
         <div className="layout-page-header-panel-inner layout-page-main-header-inner">
@@ -101,12 +104,20 @@ export default class DetailsHeader extends React.PureComponent<Props> {
                 </ModalButton>
               )}
               {actions.setAsDefault && (
-                <Button
-                  className="little-spacer-left"
-                  id="quality-gate-toggle-default"
-                  onClick={this.handleSetAsDefaultClick}>
-                  {translate('set_as_default')}
-                </Button>
+                <Tooltip
+                  overlay={
+                    hasNoConditions
+                      ? translate('quality_gates.cannot_set_default_no_conditions')
+                      : null
+                  }>
+                  <Button
+                    className="little-spacer-left"
+                    disabled={hasNoConditions}
+                    id="quality-gate-toggle-default"
+                    onClick={this.handleSetAsDefaultClick}>
+                    {translate('set_as_default')}
+                  </Button>
+                </Tooltip>
               )}
               {actions.delete && (
                 <DeleteQualityGateForm
index e1f2f3701237419c72fecae8e96bbb0849b65fab..13bcfc42e5eb3dcae6af0f069bf3c283ab04158d 100644 (file)
@@ -139,6 +139,14 @@ export default class Projects extends React.PureComponent<Props, State> {
   };
 
   render() {
+    const { qualityGate } = this.props;
+
+    if (qualityGate.conditions === undefined || qualityGate.conditions.length === 0) {
+      return (
+        <div>{translate('quality_gates.projects.cannot_associate_projects_no_conditions')}</div>
+      );
+    }
+
     return (
       <SelectList
         elements={this.state.projects.map(project => project.key)}
index 10d18f4ddd9467a9df6e7ea5bc23a31649db8665..8f8c3c7af2f8a8b62dd55531dc35222c6b12db8d 100644 (file)
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import { mockQualityGate } from '../../../../helpers/mocks/quality-gates';
+import { mockCondition } from '../../../../helpers/testMocks';
 import { DetailsContent, DetailsContentProps } from '../DetailsContent';
 
 it('should render correctly', () => {
   expect(shallowRender()).toMatchSnapshot('is not default');
   expect(shallowRender({ isDefault: true })).toMatchSnapshot('is default');
+  expect(
+    shallowRender({ isDefault: true, qualityGate: mockQualityGate({ conditions: [] }) })
+  ).toMatchSnapshot('is default, no conditions');
 });
 
 function shallowRender(props: Partial<DetailsContentProps> = {}) {
@@ -34,7 +38,7 @@ function shallowRender(props: Partial<DetailsContentProps> = {}) {
       onAddCondition={jest.fn()}
       onRemoveCondition={jest.fn()}
       onSaveCondition={jest.fn()}
-      qualityGate={mockQualityGate()}
+      qualityGate={mockQualityGate({ conditions: [mockCondition()] })}
       {...props}
     />
   );
index 0d7ba10bb1a9d305e4c19788c7e62a05e7da59d3..0fd8c9bf2f865ac075f8b44a649f0065399f583e 100644 (file)
@@ -22,6 +22,7 @@ import * as React from 'react';
 import { click, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
 import { setQualityGateAsDefault } from '../../../../api/quality-gates';
 import { mockQualityGate } from '../../../../helpers/mocks/quality-gates';
+import { mockCondition } from '../../../../helpers/testMocks';
 import DetailsHeader from '../DetailsHeader';
 
 jest.mock('../../../../api/quality-gates', () => ({
@@ -32,12 +33,15 @@ beforeEach(jest.clearAllMocks);
 
 it('should render correctly', () => {
   expect(shallowRender()).toMatchSnapshot('default');
-  expect(shallowRender({ qualityGate: mockQualityGate({ isBuiltIn: true }) })).toMatchSnapshot(
-    'built-in'
-  );
+  expect(
+    shallowRender({
+      qualityGate: mockQualityGate({ isBuiltIn: true, conditions: [mockCondition()] })
+    })
+  ).toMatchSnapshot('built-in');
   expect(
     shallowRender({
       qualityGate: mockQualityGate({
+        conditions: [mockCondition()],
         actions: {
           copy: true,
           delete: true,
@@ -47,6 +51,11 @@ it('should render correctly', () => {
       })
     })
   ).toMatchSnapshot('admin actions');
+  expect(
+    shallowRender({
+      qualityGate: mockQualityGate({ actions: { setAsDefault: true }, conditions: [] })
+    })
+  ).toMatchSnapshot('no conditions, cannot set as default');
 });
 
 it('should allow the QG to be set as the default', async () => {
@@ -79,7 +88,7 @@ function shallowRender(props: Partial<DetailsHeader['props']> = {}) {
   return shallow<DetailsHeader>(
     <DetailsHeader
       onSetDefault={jest.fn()}
-      qualityGate={mockQualityGate()}
+      qualityGate={mockQualityGate({ conditions: [mockCondition()] })}
       refreshItem={jest.fn().mockResolvedValue(null)}
       refreshList={jest.fn().mockResolvedValue(null)}
       {...props}
index 8ff3d78a7a2e62a7f499daa2582391d2713a3351..3629a40d84bcdb7fe8cb2648507d489182da470c 100644 (file)
@@ -27,10 +27,9 @@ import {
   searchProjects
 } from '../../../../api/quality-gates';
 import { mockQualityGate } from '../../../../helpers/mocks/quality-gates';
+import { mockCondition } from '../../../../helpers/testMocks';
 import Projects from '../Projects';
 
-const qualityGate = mockQualityGate();
-
 jest.mock('../../../../api/quality-gates', () => ({
   searchProjects: jest.fn().mockResolvedValue({
     paging: { pageIndex: 1, pageSize: 3, total: 55 },
@@ -62,13 +61,16 @@ it('should render correctly', async () => {
   await waitAndUpdate(wrapper);
 
   expect(wrapper.instance().mounted).toBe(true);
-  expect(wrapper).toMatchSnapshot();
-  expect(wrapper.instance().renderElement('test1')).toMatchSnapshot();
-  expect(wrapper.instance().renderElement('test_foo')).toMatchSnapshot();
+  expect(wrapper).toMatchSnapshot('default');
+  expect(wrapper.instance().renderElement('test1')).toMatchSnapshot('known project');
+  expect(wrapper.instance().renderElement('test_foo')).toMatchSnapshot('unknown project');
+  expect(shallowRender({ qualityGate: mockQualityGate({ conditions: [] }) })).toMatchSnapshot(
+    'quality gate without conditions'
+  );
 
   expect(searchProjects).toHaveBeenCalledWith(
     expect.objectContaining({
-      gateName: qualityGate.name,
+      gateName: 'Foo',
       page: 1,
       pageSize: 100,
       query: undefined,
@@ -108,5 +110,10 @@ it('should handle deselection properly', async () => {
 });
 
 function shallowRender(props: Partial<Projects['props']> = {}) {
-  return shallow<Projects>(<Projects qualityGate={qualityGate} {...props} />);
+  return shallow<Projects>(
+    <Projects
+      qualityGate={mockQualityGate({ name: 'Foo', conditions: [mockCondition()] })}
+      {...props}
+    />
+  );
 }
index 59ba0ebfd6524e94fa11fcd5b4180836b0cd3800..a8261ab3b2ee752d56eec992cb5733543270bc3f 100644 (file)
@@ -4,6 +4,72 @@ exports[`should render correctly: is default 1`] = `
 <div
   className="layout-page-main-inner"
 >
+  <Connect(withAppState(Conditions))
+    conditions={
+      Array [
+        Object {
+          "error": "10",
+          "id": 1,
+          "metric": "coverage",
+          "op": "LT",
+        },
+      ]
+    }
+    metrics={Object {}}
+    onAddCondition={[MockFunction]}
+    onRemoveCondition={[MockFunction]}
+    onSaveCondition={[MockFunction]}
+    qualityGate={
+      Object {
+        "conditions": Array [
+          Object {
+            "error": "10",
+            "id": 1,
+            "metric": "coverage",
+            "op": "LT",
+          },
+        ],
+        "id": "1",
+        "name": "qualitygate",
+      }
+    }
+  />
+  <div
+    className="quality-gate-section"
+    id="quality-gate-projects"
+  >
+    <header
+      className="display-flex-center spacer-bottom"
+    >
+      <h3>
+        quality_gates.projects
+      </h3>
+      <HelpTooltip
+        className="spacer-left"
+        overlay={
+          <div
+            className="big-padded-top big-padded-bottom"
+          >
+            quality_gates.projects.help
+          </div>
+        }
+      />
+    </header>
+    quality_gates.projects_for_default
+  </div>
+</div>
+`;
+
+exports[`should render correctly: is default, no conditions 1`] = `
+<div
+  className="layout-page-main-inner"
+>
+  <Alert
+    className="big-spacer-bottom"
+    variant="warning"
+  >
+    quality_gates.is_default_no_conditions
+  </Alert>
   <Connect(withAppState(Conditions))
     conditions={Array []}
     metrics={Object {}}
@@ -12,6 +78,7 @@ exports[`should render correctly: is default 1`] = `
     onSaveCondition={[MockFunction]}
     qualityGate={
       Object {
+        "conditions": Array [],
         "id": "1",
         "name": "qualitygate",
       }
@@ -48,13 +115,30 @@ exports[`should render correctly: is not default 1`] = `
   className="layout-page-main-inner"
 >
   <Connect(withAppState(Conditions))
-    conditions={Array []}
+    conditions={
+      Array [
+        Object {
+          "error": "10",
+          "id": 1,
+          "metric": "coverage",
+          "op": "LT",
+        },
+      ]
+    }
     metrics={Object {}}
     onAddCondition={[MockFunction]}
     onRemoveCondition={[MockFunction]}
     onSaveCondition={[MockFunction]}
     qualityGate={
       Object {
+        "conditions": Array [
+          Object {
+            "error": "10",
+            "id": 1,
+            "metric": "coverage",
+            "op": "LT",
+          },
+        ],
         "id": "1",
         "name": "qualitygate",
       }
@@ -85,6 +169,14 @@ exports[`should render correctly: is not default 1`] = `
       key="1"
       qualityGate={
         Object {
+          "conditions": Array [
+            Object {
+              "error": "10",
+              "id": 1,
+              "metric": "coverage",
+              "op": "LT",
+            },
+          ],
           "id": "1",
           "name": "qualitygate",
         }
index 9326e5cf1d1f38fac362d1a88fd8f1c749eab830..614efb96ed87856b85ee8839ff207d8205128f46 100644 (file)
@@ -30,13 +30,18 @@ exports[`should render correctly: admin actions 1`] = `
         >
           <Component />
         </ModalButton>
-        <Button
-          className="little-spacer-left"
-          id="quality-gate-toggle-default"
-          onClick={[Function]}
+        <Tooltip
+          overlay={null}
         >
-          set_as_default
-        </Button>
+          <Button
+            className="little-spacer-left"
+            disabled={false}
+            id="quality-gate-toggle-default"
+            onClick={[Function]}
+          >
+            set_as_default
+          </Button>
+        </Tooltip>
         <withRouter(DeleteQualityGateForm)
           onDelete={[MockFunction]}
           qualityGate={
@@ -47,6 +52,14 @@ exports[`should render correctly: admin actions 1`] = `
                 "rename": true,
                 "setAsDefault": true,
               },
+              "conditions": Array [
+                Object {
+                  "error": "10",
+                  "id": 1,
+                  "metric": "coverage",
+                  "op": "LT",
+                },
+              ],
               "id": "1",
               "name": "qualitygate",
             }
@@ -110,3 +123,41 @@ exports[`should render correctly: default 1`] = `
   </div>
 </div>
 `;
+
+exports[`should render correctly: no conditions, cannot set as default 1`] = `
+<div
+  className="layout-page-header-panel layout-page-main-header issues-main-header"
+>
+  <div
+    className="layout-page-header-panel-inner layout-page-main-header-inner"
+  >
+    <div
+      className="layout-page-main-inner"
+    >
+      <div
+        className="pull-left display-flex-center"
+      >
+        <h2>
+          qualitygate
+        </h2>
+      </div>
+      <div
+        className="pull-right"
+      >
+        <Tooltip
+          overlay="quality_gates.cannot_set_default_no_conditions"
+        >
+          <Button
+            className="little-spacer-left"
+            disabled={true}
+            id="quality-gate-toggle-default"
+            onClick={[Function]}
+          >
+            set_as_default
+          </Button>
+        </Tooltip>
+      </div>
+    </div>
+  </div>
+</div>
+`;
index 58db09fcecd1c24512cb3a995cac243707a363c3..417137c72cc05fd286f7d0fd2e6656cb9b8d577b 100644 (file)
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`should render correctly 1`] = `
+exports[`should render correctly: default 1`] = `
 <SelectList
   elements={
     Array [
@@ -28,7 +28,7 @@ exports[`should render correctly 1`] = `
 />
 `;
 
-exports[`should render correctly 2`] = `
+exports[`should render correctly: known project 1`] = `
 <div
   className="select-list-list-item"
 >
@@ -44,7 +44,13 @@ exports[`should render correctly 2`] = `
 </div>
 `;
 
-exports[`should render correctly 3`] = `
+exports[`should render correctly: quality gate without conditions 1`] = `
+<div>
+  quality_gates.projects.cannot_associate_projects_no_conditions
+</div>
+`;
+
+exports[`should render correctly: unknown project 1`] = `
 <div
   className="select-list-list-item"
 >
index 674543e833222ce7626578a243f32519e3c62812..0fe177ed8ad50a22427ba188c50efbaca5f6713d 100644 (file)
@@ -1638,6 +1638,8 @@ quality_gates.create=Create Quality Gate
 quality_gates.rename=Rename Quality Gate
 quality_gates.delete=Delete Quality Gate
 quality_gates.copy=Copy Quality Gate
+quality_gates.cannot_set_default_no_conditions=You must configure at least 1 condition before you can make this the default quality gate.
+quality_gates.is_default_no_conditions=This is the default quality gate, but it has no configured conditions. Please configure at least 1 condition for this quality gate.
 quality_gates.conditions=Conditions
 quality_gates.conditions.help=Your project will fail the Quality Gate if it crosses any metric thresholds set for New Code or Overall Code.
 quality_gates.conditions.help.link=See also: Clean as You Code
@@ -1656,6 +1658,7 @@ quality_gates.projects.all=All
 quality_gates.projects.noResults=No Projects
 quality_gates.projects.select_hint=Click to associate this project with the quality gate
 quality_gates.projects.deselect_hint=Click to remove association between this project and the quality gate
+quality_gates.projects.cannot_associate_projects_no_conditions=You must configure at least 1 condition before you can assign projects to this quality gate.
 quality_gates.operator.LT=is less than
 quality_gates.operator.GT=is greater than
 quality_gates.operator.EQ=equals