diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2021-08-11 15:51:04 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-08-13 20:03:54 +0000 |
commit | 1cb0f387e720cd1cb62e9b8d0954d90ff79d04e3 (patch) | |
tree | 6ad8fee67f4b6c5b908dfdc117d94c53007f05b2 /server/sonar-web/src/main | |
parent | 837be166ca9c04bebd7556bf3995cfa828e04f45 (diff) | |
download | sonarqube-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')
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> `; |