aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main
diff options
context:
space:
mode:
authorMathieu Suen <mathieu.suen@sonarsource.com>2021-08-11 15:51:04 +0200
committersonartech <sonartech@sonarsource.com>2021-08-13 20:03:54 +0000
commit1cb0f387e720cd1cb62e9b8d0954d90ff79d04e3 (patch)
tree6ad8fee67f4b6c5b908dfdc117d94c53007f05b2 /server/sonar-web/src/main
parent837be166ca9c04bebd7556bf3995cfa828e04f45 (diff)
downloadsonarqube-1cb0f387e720cd1cb62e9b8d0954d90ff79d04e3.tar.gz
sonarqube-1cb0f387e720cd1cb62e9b8d0954d90ff79d04e3.zip
SONAR-14139 Prevent users from using a Quality Gate with no conditions
Diffstat (limited to 'server/sonar-web/src/main')
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx49
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx53
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateAppRenderer-test.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateApp-test.tsx.snap75
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap233
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/components/AddLanguageModal.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/components/SetQualityProfileModal.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/AddLanguageModal-test.tsx.snap23
-rw-r--r--server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/SetQualityProfileModal-test.tsx.snap23
-rw-r--r--server/sonar-web/src/main/js/components/common/DisableableSelectOption.tsx13
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/DisableableSelectOption-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DisableableSelectOption-test.tsx.snap16
13 files changed, 429 insertions, 90 deletions
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx
index 5aa28ff3d43..095bde5a1e8 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateApp.tsx
@@ -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(() => []);
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx
index f1c0f20dcaf..2da84b07ec9 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx
@@ -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>
)}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx
index 81c27e29df6..d1643b8aba5 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateApp-test.tsx
@@ -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'
});
});
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateAppRenderer-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateAppRenderer-test.tsx
index cc1b3a36896..19691ab2088 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateAppRenderer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateAppRenderer-test.tsx
@@ -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()}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateApp-test.tsx.snap
index d7585406c6d..03331f67dba 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateApp-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateApp-test.tsx.snap
@@ -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}
/>
`;
diff --git a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap
index 26e95c11361..154841d6739 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap
@@ -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",
+ }
+ }
+/>
`;
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/AddLanguageModal.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/AddLanguageModal.tsx
index 871a5ce7885..e21e2b6cf20 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/AddLanguageModal.tsx
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/AddLanguageModal.tsx
@@ -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}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/SetQualityProfileModal.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/SetQualityProfileModal.tsx
index 20d5effd671..a42b488f035 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/SetQualityProfileModal.tsx
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/SetQualityProfileModal.tsx
@@ -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}
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/AddLanguageModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/AddLanguageModal-test.tsx.snap
index 8b22589ddcd..266194051dc 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/AddLanguageModal-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/AddLanguageModal-test.tsx.snap
@@ -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>
- }
/>
`;
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/SetQualityProfileModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/SetQualityProfileModal-test.tsx.snap
index c61e39d1cff..68a7cf2817c 100644
--- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/SetQualityProfileModal-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/SetQualityProfileModal-test.tsx.snap
@@ -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>
- }
/>
`;
diff --git a/server/sonar-web/src/main/js/components/common/DisableableSelectOption.tsx b/server/sonar-web/src/main/js/components/common/DisableableSelectOption.tsx
index 90c5612baf2..5ca6146e233 100644
--- a/server/sonar-web/src/main/js/components/common/DisableableSelectOption.tsx
+++ b/server/sonar-web/src/main/js/components/common/DisableableSelectOption.tsx
@@ -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>
);
}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/DisableableSelectOption-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/DisableableSelectOption-test.tsx
index c810aa4a687..ba8686c3bd1 100644
--- a/server/sonar-web/src/main/js/components/common/__tests__/DisableableSelectOption-test.tsx
+++ b/server/sonar-web/src/main/js/components/common/__tests__/DisableableSelectOption-test.tsx
@@ -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}
/>
);
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DisableableSelectOption-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DisableableSelectOption-test.tsx.snap
index 5ed0c2ae05a..ac1d9597703 100644
--- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DisableableSelectOption-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DisableableSelectOption-test.tsx.snap
@@ -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>
`;