Browse Source

SONAR-14139 Prevent users from using a Quality Gate with no conditions

tags/9.1.0.47736
Mathieu Suen 2 years ago
parent
commit
1cb0f387e7
14 changed files with 433 additions and 90 deletions
  1. 12
    1
      server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx
  2. 47
    2
      server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx
  3. 33
    20
      server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx
  4. 10
    1
      server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateAppRenderer-test.tsx
  5. 73
    2
      server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateApp-test.tsx.snap
  6. 228
    5
      server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap
  7. 2
    2
      server/sonar-web/src/main/js/apps/projectQualityProfiles/components/AddLanguageModal.tsx
  8. 2
    2
      server/sonar-web/src/main/js/apps/projectQualityProfiles/components/SetQualityProfileModal.tsx
  9. 1
    22
      server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/AddLanguageModal-test.tsx.snap
  10. 1
    22
      server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/SetQualityProfileModal-test.tsx.snap
  11. 7
    6
      server/sonar-web/src/main/js/components/common/DisableableSelectOption.tsx
  12. 1
    1
      server/sonar-web/src/main/js/components/common/__tests__/DisableableSelectOption-test.tsx
  13. 12
    4
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DisableableSelectOption-test.tsx.snap
  14. 4
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 12
- 1
server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx View File

@@ -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(() => []);


+ 47
- 2
server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx View File

@@ -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>
)}

+ 33
- 20
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx View File

@@ -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'
});
});

+ 10
- 1
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateAppRenderer-test.tsx View File

@@ -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()}

+ 73
- 2
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateApp-test.tsx.snap View File

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

+ 228
- 5
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap View File

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

+ 2
- 2
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/AddLanguageModal.tsx View File

@@ -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}

+ 2
- 2
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/SetQualityProfileModal.tsx View File

@@ -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}

+ 1
- 22
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/AddLanguageModal-test.tsx.snap View File

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

+ 1
- 22
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/SetQualityProfileModal-test.tsx.snap View File

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

+ 7
- 6
server/sonar-web/src/main/js/components/common/DisableableSelectOption.tsx View File

@@ -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>
);
}

+ 1
- 1
server/sonar-web/src/main/js/components/common/__tests__/DisableableSelectOption-test.tsx View File

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

+ 12
- 4
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DisableableSelectOption-test.tsx.snap View File

@@ -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>
`;

+ 4
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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

#------------------------------------------------------------------------------
#

Loading…
Cancel
Save