@@ -22,6 +22,7 @@ import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { | |||
associateGateWithProject, | |||
dissociateGateWithProject, | |||
fetchQualityGate, | |||
fetchQualityGates, | |||
getGateForProject, | |||
searchProjects | |||
@@ -93,12 +94,22 @@ export default class ProjectQualityGateApp extends React.PureComponent<Props, St | |||
return !selected; | |||
}; | |||
fetchDetailedQualityGates = async () => { | |||
const { qualitygates } = await fetchQualityGates(); | |||
return Promise.all( | |||
qualitygates.map(async qg => { | |||
const detailedQp = await fetchQualityGate({ id: qg.id }).catch(() => qg); | |||
return { ...detailedQp, ...qg }; | |||
}) | |||
); | |||
}; | |||
fetchQualityGates = async () => { | |||
const { component } = this.props; | |||
this.setState({ loading: true }); | |||
const [allQualityGates, currentQualityGate] = await Promise.all([ | |||
fetchQualityGates().then(({ qualitygates }) => qualitygates), | |||
this.fetchDetailedQualityGates(), | |||
getGateForProject({ project: component.key }) | |||
]).catch(() => []); | |||
@@ -19,14 +19,18 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { Helmet } from 'react-helmet-async'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { Link } from 'react-router'; | |||
import { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip'; | |||
import Radio from 'sonar-ui-common/components/controls/Radio'; | |||
import Select from 'sonar-ui-common/components/controls/Select'; | |||
import { Alert } from 'sonar-ui-common/components/ui/Alert'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { isDiffMetric } from 'sonar-ui-common/helpers/measures'; | |||
import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget'; | |||
import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; | |||
import DisableableSelectOption from '../../components/common/DisableableSelectOption'; | |||
import BuiltInQualityGateBadge from '../quality-gates/components/BuiltInQualityGateBadge'; | |||
import { USE_SYSTEM_DEFAULT } from './constants'; | |||
@@ -40,6 +44,10 @@ export interface ProjectQualityGateAppRendererProps { | |||
submitting: boolean; | |||
} | |||
function hasConditionOnNewCode(qualityGate: T.QualityGate): boolean { | |||
return !!qualityGate.conditions?.some(condition => isDiffMetric(condition.metric)); | |||
} | |||
export default function ProjectQualityGateAppRenderer(props: ProjectQualityGateAppRendererProps) { | |||
const { allQualityGates, currentQualityGate, loading, selectedQualityGateId, submitting } = props; | |||
const defaultQualityGate = allQualityGates?.find(g => g.isDefault); | |||
@@ -63,7 +71,10 @@ export default function ProjectQualityGateAppRenderer(props: ProjectQualityGateA | |||
defaultQualityGate.id !== currentQualityGate.id | |||
: selectedQualityGateId !== currentQualityGate.id; | |||
const selectedQualityGate = allQualityGates.find(qg => qg.id === selectedQualityGateId); | |||
const options = allQualityGates.map(g => ({ | |||
disabled: g.conditions === undefined || g.conditions.length === 0, | |||
label: g.name, | |||
value: g.id | |||
})); | |||
@@ -141,15 +152,49 @@ export default function ProjectQualityGateAppRenderer(props: ProjectQualityGateA | |||
disabled={submitting || usesDefault} | |||
onChange={({ value }: { value: string }) => props.onSelect(value)} | |||
options={options} | |||
optionRenderer={option => <span>{option.label}</span>} | |||
optionRenderer={option => ( | |||
<DisableableSelectOption | |||
className="abs-width-100" | |||
option={option} | |||
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={{ pathname: `/quality_gates/show/${option.value}` }}> | |||
{translate('project_quality_gate.no_condition.link')} | |||
</Link> | |||
) | |||
}} | |||
/> | |||
)} | |||
/> | |||
)} | |||
value={selectedQualityGateId} | |||
/> | |||
</div> | |||
</div> | |||
</Radio> | |||
{selectedQualityGate && !hasConditionOnNewCode(selectedQualityGate) && ( | |||
<Alert className="abs-width-600 spacer-top" variant="warning"> | |||
<FormattedMessage | |||
id="project_quality_gate.no_condition_on_new_code" | |||
defaultMessage={translate('project_quality_gate.no_condition_on_new_code')} | |||
values={{ | |||
link: ( | |||
<Link to={{ pathname: `/quality_gates/show/${selectedQualityGate.id}` }}> | |||
{translate('project_quality_gate.no_condition.link')} | |||
</Link> | |||
) | |||
}} | |||
/> | |||
</Alert> | |||
)} | |||
{needsReanalysis && ( | |||
<Alert className="big-spacer-top" variant="warning"> | |||
<Alert className="big-spacer-top abs-width-600" variant="warning"> | |||
{translate('project_quality_gate.requires_new_analysis')} | |||
</Alert> | |||
)} |
@@ -35,18 +35,29 @@ import ProjectQualityGateApp from '../ProjectQualityGateApp'; | |||
jest.mock('../../../api/quality-gates', () => { | |||
const { mockQualityGate } = jest.requireActual('../../../helpers/mocks/quality-gates'); | |||
const gate1 = mockQualityGate(); | |||
const gate2 = mockQualityGate({ id: '2', isBuiltIn: true }); | |||
const gate3 = mockQualityGate({ id: '3', isDefault: true }); | |||
const { mockCondition } = jest.requireActual('../../../helpers/testMocks'); | |||
const conditions = [mockCondition(), mockCondition({ metric: 'new_bugs' })]; | |||
const gates = { | |||
gate1: mockQualityGate({ id: 'gate1' }), | |||
gate2: mockQualityGate({ id: 'gate2', isBuiltIn: true }), | |||
gate3: mockQualityGate({ id: 'gate3', isDefault: true }), | |||
gate4: mockQualityGate({ id: 'gate4' }) | |||
}; | |||
return { | |||
associateGateWithProject: jest.fn().mockResolvedValue(null), | |||
dissociateGateWithProject: jest.fn().mockResolvedValue(null), | |||
fetchQualityGates: jest.fn().mockResolvedValue({ | |||
qualitygates: [gate1, gate2, gate3] | |||
qualitygates: Object.values(gates) | |||
}), | |||
fetchQualityGate: jest.fn().mockImplementation((qg: { id: keyof typeof gates }) => { | |||
if (qg.id === 'gate4') { | |||
return Promise.reject(); | |||
} | |||
return Promise.resolve({ conditions, ...gates[qg.id] }); | |||
}), | |||
getGateForProject: jest.fn().mockResolvedValue(gate2), | |||
getGateForProject: jest.fn().mockResolvedValue(gates.gate2), | |||
searchProjects: jest.fn().mockResolvedValue({ results: [] }) | |||
}; | |||
}); | |||
@@ -61,8 +72,10 @@ jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({ | |||
beforeEach(jest.clearAllMocks); | |||
it('renders correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
it('renders correctly', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('correctly checks user permissions', () => { | |||
@@ -77,27 +90,27 @@ it('correctly loads Quality Gate data', async () => { | |||
expect(fetchQualityGates).toBeCalled(); | |||
expect(getGateForProject).toBeCalledWith({ project: 'foo' }); | |||
expect(wrapper.state().allQualityGates).toHaveLength(3); | |||
expect(wrapper.state().currentQualityGate?.id).toBe('2'); | |||
expect(wrapper.state().selectedQualityGateId).toBe('2'); | |||
expect(wrapper.state().allQualityGates).toHaveLength(4); | |||
expect(wrapper.state().currentQualityGate?.id).toBe('gate2'); | |||
expect(wrapper.state().selectedQualityGateId).toBe('gate2'); | |||
}); | |||
it('correctly fallbacks to the default Quality Gate', async () => { | |||
(getGateForProject as jest.Mock).mockResolvedValueOnce( | |||
mockQualityGate({ id: '3', isDefault: true }) | |||
mockQualityGate({ id: 'gate3', isDefault: true }) | |||
); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(searchProjects).toBeCalled(); | |||
expect(wrapper.state().currentQualityGate?.id).toBe('3'); | |||
expect(wrapper.state().currentQualityGate?.id).toBe('gate3'); | |||
expect(wrapper.state().selectedQualityGateId).toBe(USE_SYSTEM_DEFAULT); | |||
}); | |||
it('correctly detects if the default Quality Gate was explicitly selected', async () => { | |||
(getGateForProject as jest.Mock).mockResolvedValueOnce( | |||
mockQualityGate({ id: '3', isDefault: true }) | |||
mockQualityGate({ id: 'gate3', isDefault: true }) | |||
); | |||
(searchProjects as jest.Mock).mockResolvedValueOnce({ | |||
results: [{ key: 'foo', selected: true }] | |||
@@ -107,19 +120,19 @@ it('correctly detects if the default Quality Gate was explicitly selected', asyn | |||
expect(searchProjects).toBeCalled(); | |||
expect(wrapper.state().currentQualityGate?.id).toBe('3'); | |||
expect(wrapper.state().selectedQualityGateId).toBe('3'); | |||
expect(wrapper.state().currentQualityGate?.id).toBe('gate3'); | |||
expect(wrapper.state().selectedQualityGateId).toBe('gate3'); | |||
}); | |||
it('correctly associates a selected Quality Gate', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.instance().handleSelect('3'); | |||
wrapper.instance().handleSelect('gate3'); | |||
wrapper.instance().handleSubmit(); | |||
expect(associateGateWithProject).toHaveBeenCalledWith({ | |||
gateId: '3', | |||
gateId: 'gate3', | |||
projectKey: 'foo' | |||
}); | |||
}); | |||
@@ -129,13 +142,13 @@ it('correctly associates a project with the system default Quality Gate', async | |||
await waitAndUpdate(wrapper); | |||
wrapper.setState({ | |||
currentQualityGate: mockQualityGate({ id: '1' }), | |||
currentQualityGate: mockQualityGate({ id: 'gate1' }), | |||
selectedQualityGateId: USE_SYSTEM_DEFAULT | |||
}); | |||
wrapper.instance().handleSubmit(); | |||
expect(dissociateGateWithProject).toHaveBeenCalledWith({ | |||
gateId: '1', | |||
gateId: 'gate1', | |||
projectKey: 'foo' | |||
}); | |||
}); |
@@ -23,6 +23,8 @@ import Radio from 'sonar-ui-common/components/controls/Radio'; | |||
import Select from 'sonar-ui-common/components/controls/Select'; | |||
import { submit } from 'sonar-ui-common/helpers/testUtils'; | |||
import { mockQualityGate } from '../../../helpers/mocks/quality-gates'; | |||
import { mockCondition } from '../../../helpers/testMocks'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import { USE_SYSTEM_DEFAULT } from '../constants'; | |||
import ProjectQualityGateAppRenderer, { | |||
ProjectQualityGateAppRendererProps | |||
@@ -38,6 +40,7 @@ it('should render correctly', () => { | |||
selectedQualityGateId: USE_SYSTEM_DEFAULT | |||
}) | |||
).toMatchSnapshot('always use system default'); | |||
expect(shallowRender({ selectedQualityGateId: '3' })).toMatchSnapshot('show new code warning'); | |||
expect( | |||
shallowRender({ | |||
selectedQualityGateId: '5' | |||
@@ -96,9 +99,15 @@ it('should correctly handle form submission', () => { | |||
}); | |||
function shallowRender(props: Partial<ProjectQualityGateAppRendererProps> = {}) { | |||
const conditions = [mockCondition(), mockCondition({ metric: MetricKey.new_bugs })]; | |||
const conditionsEmptyOnNew = [mockCondition({ metric: MetricKey.bugs })]; | |||
return shallow<ProjectQualityGateAppRendererProps>( | |||
<ProjectQualityGateAppRenderer | |||
allQualityGates={[mockQualityGate(), mockQualityGate({ id: '2', isDefault: true })]} | |||
allQualityGates={[ | |||
mockQualityGate({ conditions }), | |||
mockQualityGate({ id: '2', isDefault: true, conditions }), | |||
mockQualityGate({ id: '3', isDefault: true, conditions: conditionsEmptyOnNew }) | |||
]} | |||
currentQualityGate={mockQualityGate({ id: '1' })} | |||
loading={false} | |||
onSelect={jest.fn()} |
@@ -2,10 +2,81 @@ | |||
exports[`renders correctly 1`] = ` | |||
<ProjectQualityGateAppRenderer | |||
loading={true} | |||
allQualityGates={ | |||
Array [ | |||
Object { | |||
"conditions": Array [ | |||
Object { | |||
"error": "10", | |||
"id": 1, | |||
"metric": "coverage", | |||
"op": "LT", | |||
}, | |||
Object { | |||
"error": "10", | |||
"id": 1, | |||
"metric": "new_bugs", | |||
"op": "LT", | |||
}, | |||
], | |||
"id": "gate1", | |||
"name": "qualitygate", | |||
}, | |||
Object { | |||
"conditions": Array [ | |||
Object { | |||
"error": "10", | |||
"id": 1, | |||
"metric": "coverage", | |||
"op": "LT", | |||
}, | |||
Object { | |||
"error": "10", | |||
"id": 1, | |||
"metric": "new_bugs", | |||
"op": "LT", | |||
}, | |||
], | |||
"id": "gate2", | |||
"isBuiltIn": true, | |||
"name": "qualitygate", | |||
}, | |||
Object { | |||
"conditions": Array [ | |||
Object { | |||
"error": "10", | |||
"id": 1, | |||
"metric": "coverage", | |||
"op": "LT", | |||
}, | |||
Object { | |||
"error": "10", | |||
"id": 1, | |||
"metric": "new_bugs", | |||
"op": "LT", | |||
}, | |||
], | |||
"id": "gate3", | |||
"isDefault": true, | |||
"name": "qualitygate", | |||
}, | |||
Object { | |||
"id": "gate4", | |||
"name": "qualitygate", | |||
}, | |||
] | |||
} | |||
currentQualityGate={ | |||
Object { | |||
"id": "gate2", | |||
"isBuiltIn": true, | |||
"name": "qualitygate", | |||
} | |||
} | |||
loading={false} | |||
onSelect={[Function]} | |||
onSubmit={[Function]} | |||
selectedQualityGateId="-1" | |||
selectedQualityGateId="gate2" | |||
submitting={false} | |||
/> | |||
`; |
@@ -116,13 +116,20 @@ exports[`should render correctly: always use system default 1`] = ` | |||
options={ | |||
Array [ | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "1", | |||
}, | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "2", | |||
}, | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "3", | |||
}, | |||
] | |||
} | |||
value="-1" | |||
@@ -259,13 +266,20 @@ exports[`should render correctly: default 1`] = ` | |||
options={ | |||
Array [ | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "1", | |||
}, | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "2", | |||
}, | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "3", | |||
}, | |||
] | |||
} | |||
value="1" | |||
@@ -292,6 +306,186 @@ exports[`should render correctly: loading 1`] = ` | |||
/> | |||
`; | |||
exports[`should render correctly: show new code warning 1`] = ` | |||
<div | |||
className="page page-limited" | |||
id="project-quality-gate" | |||
> | |||
<Suggestions | |||
suggestions="project_quality_gate" | |||
/> | |||
<Helmet | |||
defer={false} | |||
encodeSpecialCharacters={true} | |||
title="project_quality_gate.page" | |||
/> | |||
<A11ySkipTarget | |||
anchor="qg_main" | |||
/> | |||
<header | |||
className="page-header" | |||
> | |||
<div | |||
className="page-title display-flex-center" | |||
> | |||
<h1> | |||
project_quality_gate.page | |||
</h1> | |||
<HelpTooltip | |||
className="spacer-left" | |||
overlay={ | |||
<div | |||
className="big-padded-top big-padded-bottom" | |||
> | |||
quality_gates.projects.help | |||
</div> | |||
} | |||
/> | |||
</div> | |||
</header> | |||
<div | |||
className="boxed-group" | |||
> | |||
<h2 | |||
className="boxed-group-header" | |||
> | |||
project_quality_gate.subtitle | |||
</h2> | |||
<form | |||
className="boxed-group-inner" | |||
onSubmit={[Function]} | |||
> | |||
<p | |||
className="big-spacer-bottom" | |||
> | |||
project_quality_gate.page.description | |||
</p> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<Radio | |||
checked={false} | |||
className="display-flex-start" | |||
disabled={false} | |||
onCheck={[Function]} | |||
value="-1" | |||
> | |||
<div | |||
className="spacer-left" | |||
> | |||
<div | |||
className="little-spacer-bottom" | |||
> | |||
project_quality_gate.always_use_default | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span | |||
className="text-muted little-spacer-right" | |||
> | |||
current_noun | |||
: | |||
</span> | |||
qualitygate | |||
</div> | |||
</div> | |||
</Radio> | |||
</div> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<Radio | |||
checked={true} | |||
className="display-flex-start" | |||
disabled={false} | |||
onCheck={[Function]} | |||
value="3" | |||
> | |||
<div | |||
className="spacer-left" | |||
> | |||
<div | |||
className="little-spacer-bottom" | |||
> | |||
project_quality_gate.always_use_specific | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Select | |||
className="abs-width-300" | |||
clearable={false} | |||
disabled={false} | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "1", | |||
}, | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "2", | |||
}, | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "3", | |||
}, | |||
] | |||
} | |||
value="3" | |||
/> | |||
</div> | |||
</div> | |||
</Radio> | |||
<Alert | |||
className="abs-width-600 spacer-top" | |||
variant="warning" | |||
> | |||
<FormattedMessage | |||
defaultMessage="project_quality_gate.no_condition_on_new_code" | |||
id="project_quality_gate.no_condition_on_new_code" | |||
values={ | |||
Object { | |||
"link": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/quality_gates/show/3", | |||
} | |||
} | |||
> | |||
project_quality_gate.no_condition.link | |||
</Link>, | |||
} | |||
} | |||
/> | |||
</Alert> | |||
<Alert | |||
className="big-spacer-top abs-width-600" | |||
variant="warning" | |||
> | |||
project_quality_gate.requires_new_analysis | |||
</Alert> | |||
</div> | |||
<div> | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
save | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: show warning 1`] = ` | |||
<div | |||
className="page page-limited" | |||
@@ -408,13 +602,20 @@ exports[`should render correctly: show warning 1`] = ` | |||
options={ | |||
Array [ | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "1", | |||
}, | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "2", | |||
}, | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "3", | |||
}, | |||
] | |||
} | |||
value="5" | |||
@@ -423,7 +624,7 @@ exports[`should render correctly: show warning 1`] = ` | |||
</div> | |||
</Radio> | |||
<Alert | |||
className="big-spacer-top" | |||
className="big-spacer-top abs-width-600" | |||
variant="warning" | |||
> | |||
project_quality_gate.requires_new_analysis | |||
@@ -557,13 +758,20 @@ exports[`should render correctly: show warning if not using default 1`] = ` | |||
options={ | |||
Array [ | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "1", | |||
}, | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "2", | |||
}, | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "3", | |||
}, | |||
] | |||
} | |||
value="-1" | |||
@@ -572,7 +780,7 @@ exports[`should render correctly: show warning if not using default 1`] = ` | |||
</div> | |||
</Radio> | |||
<Alert | |||
className="big-spacer-top" | |||
className="big-spacer-top abs-width-600" | |||
variant="warning" | |||
> | |||
project_quality_gate.requires_new_analysis | |||
@@ -706,13 +914,20 @@ exports[`should render correctly: submitting 1`] = ` | |||
options={ | |||
Array [ | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "1", | |||
}, | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "2", | |||
}, | |||
Object { | |||
"disabled": false, | |||
"label": "qualitygate", | |||
"value": "3", | |||
}, | |||
] | |||
} | |||
value="1" | |||
@@ -737,7 +952,15 @@ exports[`should render correctly: submitting 1`] = ` | |||
`; | |||
exports[`should render select options correctly: default 1`] = ` | |||
<span> | |||
Gate 1 | |||
</span> | |||
<DisableableSelectOption | |||
className="abs-width-100" | |||
disableTooltipOverlay={[Function]} | |||
disabledReason="project_quality_gate.no_condition.reason" | |||
option={ | |||
Object { | |||
"label": "Gate 1", | |||
"value": "1", | |||
} | |||
} | |||
/> | |||
`; |
@@ -115,7 +115,7 @@ export function AddLanguageModal(props: AddLanguageModalProps) { | |||
disabledReason={translate( | |||
'project_quality_profile.add_language_modal.no_active_rules' | |||
)} | |||
tooltipOverlay={ | |||
disableTooltipOverlay={() => ( | |||
<> | |||
<p> | |||
{translate( | |||
@@ -130,7 +130,7 @@ export function AddLanguageModal(props: AddLanguageModalProps) { | |||
</Link> | |||
)} | |||
</> | |||
} | |||
)} | |||
/> | |||
)} | |||
value={key} |
@@ -131,7 +131,7 @@ export default function SetQualityProfileModal(props: SetQualityProfileModalProp | |||
disabledReason={translate( | |||
'project_quality_profile.add_language_modal.no_active_rules' | |||
)} | |||
tooltipOverlay={ | |||
disableTooltipOverlay={() => ( | |||
<> | |||
<p> | |||
{translate( | |||
@@ -150,7 +150,7 @@ export default function SetQualityProfileModal(props: SetQualityProfileModalProp | |||
</Link> | |||
)} | |||
</> | |||
} | |||
)} | |||
/> | |||
)} | |||
value={!hasSelectedSysDefault ? selected : currentProfile.key} |
@@ -93,6 +93,7 @@ Array [ | |||
exports[`should render select options correctly: default 1`] = ` | |||
<DisableableSelectOption | |||
disableTooltipOverlay={[Function]} | |||
disabledReason="project_quality_profile.add_language_modal.no_active_rules" | |||
option={ | |||
Object { | |||
@@ -100,27 +101,5 @@ exports[`should render select options correctly: default 1`] = ` | |||
"value": "bar", | |||
} | |||
} | |||
tooltipOverlay={ | |||
<React.Fragment> | |||
<p> | |||
project_quality_profile.add_language_modal.profile_unavailable_no_active_rules | |||
</p> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/profiles/show", | |||
"query": Object { | |||
"language": "js", | |||
"name": "Profile 1", | |||
}, | |||
} | |||
} | |||
> | |||
project_quality_profile.add_language_modal.go_to_profile | |||
</Link> | |||
</React.Fragment> | |||
} | |||
/> | |||
`; |
@@ -349,6 +349,7 @@ Array [ | |||
exports[`should render select options correctly: default 1`] = ` | |||
<DisableableSelectOption | |||
disableTooltipOverlay={[Function]} | |||
disabledReason="project_quality_profile.add_language_modal.no_active_rules" | |||
option={ | |||
Object { | |||
@@ -356,27 +357,5 @@ exports[`should render select options correctly: default 1`] = ` | |||
"value": "bar", | |||
} | |||
} | |||
tooltipOverlay={ | |||
<React.Fragment> | |||
<p> | |||
project_quality_profile.add_language_modal.profile_unavailable_no_active_rules | |||
</p> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/profiles/show", | |||
"query": Object { | |||
"language": "js", | |||
"name": "Profile 1", | |||
}, | |||
} | |||
} | |||
> | |||
project_quality_profile.add_language_modal.go_to_profile | |||
</Link> | |||
</React.Fragment> | |||
} | |||
/> | |||
`; |
@@ -21,17 +21,18 @@ import * as React from 'react'; | |||
import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; | |||
export interface DisableableSelectOptionProps { | |||
option: { label?: string; value?: string | number | boolean; disabled?: boolean }; | |||
tooltipOverlay: React.ReactNode; | |||
className?: string; | |||
disabledReason?: string; | |||
option: { label?: string; value?: string | number | boolean; disabled?: boolean }; | |||
disableTooltipOverlay: () => React.ReactNode; | |||
} | |||
export default function DisableableSelectOption(props: DisableableSelectOptionProps) { | |||
const { option, tooltipOverlay, disabledReason } = props; | |||
const { option, disableTooltipOverlay, disabledReason, className = '' } = props; | |||
const label = option.label || option.value; | |||
return option.disabled ? ( | |||
<Tooltip overlay={tooltipOverlay} placement="left"> | |||
<span> | |||
<Tooltip overlay={disableTooltipOverlay()} placement="left"> | |||
<span className={className}> | |||
{label} | |||
{disabledReason !== undefined && ( | |||
<em className="small little-spacer-left">({disabledReason})</em> | |||
@@ -39,6 +40,6 @@ export default function DisableableSelectOption(props: DisableableSelectOptionPr | |||
</span> | |||
</Tooltip> | |||
) : ( | |||
<span>{label}</span> | |||
<span className={className}>{label}</span> | |||
); | |||
} |
@@ -40,7 +40,7 @@ function shallowRender(props: Partial<DisableableSelectOptionProps> = {}) { | |||
return shallow<DisableableSelectOptionProps>( | |||
<DisableableSelectOption | |||
option={{ label: 'Foo', value: 'foo' }} | |||
tooltipOverlay="foo bar" | |||
disableTooltipOverlay={() => 'foo bar'} | |||
{...props} | |||
/> | |||
); |
@@ -1,7 +1,9 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: default 1`] = ` | |||
<span> | |||
<span | |||
className="" | |||
> | |||
Foo | |||
</span> | |||
`; | |||
@@ -11,7 +13,9 @@ exports[`should render correctly: disabled 1`] = ` | |||
overlay="foo bar" | |||
placement="left" | |||
> | |||
<span> | |||
<span | |||
className="" | |||
> | |||
Bar | |||
</span> | |||
</Tooltip> | |||
@@ -22,7 +26,9 @@ exports[`should render correctly: disabled, with explanation 1`] = ` | |||
overlay="foo bar" | |||
placement="left" | |||
> | |||
<span> | |||
<span | |||
className="" | |||
> | |||
Bar | |||
<em | |||
className="small little-spacer-left" | |||
@@ -36,7 +42,9 @@ exports[`should render correctly: disabled, with explanation 1`] = ` | |||
`; | |||
exports[`should render correctly: no label 1`] = ` | |||
<span> | |||
<span | |||
className="" | |||
> | |||
baz | |||
</span> | |||
`; |
@@ -1520,6 +1520,10 @@ project_quality_gate.subtitle=Manage project Quality Gate | |||
project_quality_gate.always_use_default=Always use the instance default Quality Gate | |||
project_quality_gate.always_use_specific=Always use a specific Quality Gate | |||
project_quality_gate.requires_new_analysis=Changes will be applied after the next analysis. | |||
project_quality_gate.no_condition=This Quality Gate is empty. To make it usable, add conditions to the {link}. | |||
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 | |||
#------------------------------------------------------------------------------ | |||
# |