Browse Source

SONAR-13154 Prevent assigning projects to Quality Gate with no conditions, or setting it as the default

tags/9.1.0.47736
Wouter Admiraal 2 years ago
parent
commit
0971ca99e9

+ 7
- 0
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx View 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}

+ 17
- 6
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx View 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

+ 8
- 0
server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx View 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)}

+ 5
- 1
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/DetailsContent-test.tsx View File

@@ -20,11 +20,15 @@
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}
/>
);

+ 13
- 4
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/DetailsHeader-test.tsx View 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}

+ 14
- 7
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/Projects-test.tsx View 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}
/>
);
}

+ 93
- 1
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsContent-test.tsx.snap View 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",
}

+ 57
- 6
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/DetailsHeader-test.tsx.snap View 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>
`;

+ 9
- 3
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/Projects-test.tsx.snap View 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"
>

+ 3
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View 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

Loading…
Cancel
Save