@@ -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} |
@@ -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 |
@@ -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)} |
@@ -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} | |||
/> | |||
); |
@@ -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} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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", | |||
} |
@@ -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> | |||
`; |
@@ -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" | |||
> |
@@ -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 |