diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2021-08-12 10:21:24 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-08-13 20:03:54 +0000 |
commit | f8f1b4c9fdf6688e2336e57ebe1d23a82a1c58b7 (patch) | |
tree | 28d95d453b405cf7dedb327c4a634ead204ed0ae /server/sonar-web/src | |
parent | 0971ca99e937be30a54965bf616f78ec4779d108 (diff) | |
download | sonarqube-f8f1b4c9fdf6688e2336e57ebe1d23a82a1c58b7.tar.gz sonarqube-f8f1b4c9fdf6688e2336e57ebe1d23a82a1c58b7.zip |
SONAR-13150 Prevent using quality profiles with no active rules
Diffstat (limited to 'server/sonar-web/src')
18 files changed, 1071 insertions, 71 deletions
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 0aae23b00c7..871a5ce7885 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 @@ -20,11 +20,14 @@ import { difference } from 'lodash'; import * as React from 'react'; import { connect } from 'react-redux'; +import { Link } from 'react-router'; import { ButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons'; import Select from 'sonar-ui-common/components/controls/Select'; import SimpleModal from 'sonar-ui-common/components/controls/SimpleModal'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { Profile } from '../../../api/quality-profiles'; +import DisableableSelectOption from '../../../components/common/DisableableSelectOption'; +import { getQualityProfileUrl } from '../../../helpers/urls'; import { Store } from '../../../store/rootReducer'; export interface AddLanguageModalProps { @@ -52,7 +55,11 @@ export function AddLanguageModal(props: AddLanguageModalProps) { const profileOptions = language !== undefined - ? profilesByLanguage[language].map(p => ({ value: p.key, label: p.name })) + ? profilesByLanguage[language].map(p => ({ + value: p.key, + label: p.name, + disabled: p.activeRuleCount === 0 + })) : []; return ( @@ -102,6 +109,30 @@ export function AddLanguageModal(props: AddLanguageModalProps) { id="profiles" onChange={({ value }: { value: string }) => setSelected({ language, key: value })} options={profileOptions} + optionRenderer={option => ( + <DisableableSelectOption + option={option} + disabledReason={translate( + 'project_quality_profile.add_language_modal.no_active_rules' + )} + tooltipOverlay={ + <> + <p> + {translate( + 'project_quality_profile.add_language_modal.profile_unavailable_no_active_rules' + )} + </p> + {option.label && language && ( + <Link to={getQualityProfileUrl(option.label, language)}> + {translate( + 'project_quality_profile.add_language_modal.go_to_profile' + )} + </Link> + )} + </> + } + /> + )} value={key} /> </div> 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 5c0217f6b3e..20d5effd671 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 @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { Link } from 'react-router'; import { ButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons'; import Radio from 'sonar-ui-common/components/controls/Radio'; import Select from 'sonar-ui-common/components/controls/Select'; @@ -25,6 +26,8 @@ import SimpleModal from 'sonar-ui-common/components/controls/SimpleModal'; import { Alert } from 'sonar-ui-common/components/ui/Alert'; import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; import { Profile } from '../../../api/quality-profiles'; +import DisableableSelectOption from '../../../components/common/DisableableSelectOption'; +import { getQualityProfileUrl } from '../../../helpers/urls'; import BuiltInQualityProfileBadge from '../../quality-profiles/components/BuiltInQualityProfileBadge'; import { USE_SYSTEM_DEFAULT } from '../constants'; @@ -54,7 +57,11 @@ export default function SetQualityProfileModal(props: SetQualityProfileModalProp 'project_quality_profile.change_lang_X_profile', currentProfile.languageName ); - const profileOptions = availableProfiles.map(p => ({ value: p.key, label: p.name })); + const profileOptions = availableProfiles.map(p => ({ + value: p.key, + label: p.name, + disabled: p.activeRuleCount === 0 + })); const hasSelectedSysDefault = selected === USE_SYSTEM_DEFAULT; const hasChanged = usesDefault ? !hasSelectedSysDefault : selected !== currentProfile.key; const needsReanalysis = !component.qualityProfiles?.some(p => @@ -118,7 +125,34 @@ export default function SetQualityProfileModal(props: SetQualityProfileModalProp disabled={submitting || hasSelectedSysDefault} onChange={({ value }: { value: string }) => setSelected(value)} options={profileOptions} - optionRenderer={option => <span>{option.label}</span>} + optionRenderer={option => ( + <DisableableSelectOption + option={option} + disabledReason={translate( + 'project_quality_profile.add_language_modal.no_active_rules' + )} + tooltipOverlay={ + <> + <p> + {translate( + 'project_quality_profile.add_language_modal.profile_unavailable_no_active_rules' + )} + </p> + {option.label && ( + <Link + to={getQualityProfileUrl( + option.label, + currentProfile.language + )}> + {translate( + 'project_quality_profile.add_language_modal.go_to_profile' + )} + </Link> + )} + </> + } + /> + )} value={!hasSelectedSysDefault ? selected : currentProfile.key} /> </div> diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/AddLanguageModal-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/AddLanguageModal-test.tsx index 60d5960ca89..9438ce4f61f 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/AddLanguageModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/AddLanguageModal-test.tsx @@ -28,6 +28,27 @@ it('should render correctly', () => { expect(diveIntoSimpleModal(shallowRender())).toMatchSnapshot('default'); }); +it('should render select options correctly', () => { + return new Promise<void>((resolve, reject) => { + const wrapper = shallowRender(); + + const langOnChange = getLanguageSelect(wrapper).props().onChange; + if (!langOnChange) { + reject(); + return; + } + langOnChange({ value: 'js' }); + + const render = getProfileSelect(wrapper).props().optionRenderer; + if (!render) { + reject(); + return; + } + expect(render({ value: 'bar', label: 'Profile 1' })).toMatchSnapshot('default'); + resolve(); + }); +}); + it('should correctly handle changes', () => { const onSubmit = jest.fn(); const wrapper = shallowRender({ onSubmit }); @@ -50,6 +71,9 @@ it('should correctly handle changes', () => { // Should now show 2 available profiles. profileSelect = getProfileSelect(wrapper); expect(profileSelect.props().options).toHaveLength(2); + expect(profileSelect.props().options).toEqual( + expect.arrayContaining([expect.objectContaining({ disabled: true })]) + ); // Choose 1 profile. const profileChange = profileSelect.props().onChange; @@ -100,7 +124,7 @@ function shallowRender(props: Partial<AddLanguageModalProps> = {}) { onSubmit={jest.fn()} profilesByLanguage={{ css: [ - mockQualityProfile({ key: 'css', name: 'CSS' }), + mockQualityProfile({ key: 'css', name: 'CSS', activeRuleCount: 0 }), mockQualityProfile({ key: 'css2', name: 'CSS 2' }) ], ts: [mockQualityProfile({ key: 'ts', name: 'TS' })], diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/SetQualityProfileModal-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/SetQualityProfileModal-test.tsx index 6e3b4fe9145..9a7995ca6ee 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/SetQualityProfileModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/SetQualityProfileModal-test.tsx @@ -32,12 +32,16 @@ it('should render correctly', () => { }); it('should render select options correctly', () => { - const wrapper = shallowRender(); - const render = wrapper.find(Select).props().optionRenderer; - - expect(render).toBeDefined(); - - expect(render!({ value: 'bar', label: 'Profile 1' })).toMatchSnapshot('default'); + return new Promise<void>((resolve, reject) => { + const wrapper = shallowRender(); + const render = wrapper.find(Select).props().optionRenderer; + if (!render) { + reject(); + return; + } + expect(render({ value: 'bar', label: 'Profile 1' })).toMatchSnapshot('default'); + resolve(); + }); }); it('should correctly handle changes', () => { @@ -90,7 +94,7 @@ function shallowRender(props: Partial<SetQualityProfileModalProps> = {}, dive = <SetQualityProfileModal availableProfiles={[ mockQualityProfile({ key: 'foo', isDefault: true, language: 'js' }), - mockQualityProfile({ key: 'bar', language: 'js' }) + mockQualityProfile({ key: 'bar', language: 'js', activeRuleCount: 0 }) ]} component={mockComponent({ qualityProfiles: [{ key: 'foo', name: 'Foo', language: 'js' }] })} currentProfile={mockQualityProfile({ key: 'foo', language: 'js' })} 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 96957df74e4..8b22589ddcd 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 @@ -67,6 +67,7 @@ Array [ disabled={true} id="profiles" onChange={[Function]} + optionRenderer={[Function]} options={Array []} /> </div> @@ -89,3 +90,37 @@ Array [ </form>, ] `; + +exports[`should render select options correctly: default 1`] = ` +<DisableableSelectOption + disabledReason="project_quality_profile.add_language_modal.no_active_rules" + option={ + Object { + "label": "Profile 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 d14dd4876b8..c61e39d1cff 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 @@ -77,10 +77,12 @@ Array [ options={ Array [ Object { + "disabled": false, "label": "name", "value": "foo", }, Object { + "disabled": true, "label": "name", "value": "bar", }, @@ -189,10 +191,12 @@ Array [ options={ Array [ Object { + "disabled": false, "label": "name", "value": "foo", }, Object { + "disabled": true, "label": "name", "value": "bar", }, @@ -301,10 +305,12 @@ Array [ options={ Array [ Object { + "disabled": false, "label": "name", "value": "foo", }, Object { + "disabled": true, "label": "name", "value": "bar", }, @@ -342,7 +348,35 @@ Array [ `; exports[`should render select options correctly: default 1`] = ` -<span> - Profile 1 -</span> +<DisableableSelectOption + disabledReason="project_quality_profile.add_language_modal.no_active_rules" + option={ + Object { + "label": "Profile 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/quality-profiles/components/ProfileActions.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx index c8ac23413f6..8b80c62cb64 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx @@ -22,6 +22,7 @@ import ActionsDropdown, { ActionsDropdownDivider, ActionsDropdownItem } from 'sonar-ui-common/components/controls/ActionsDropdown'; +import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { changeProfileParent, @@ -145,7 +146,12 @@ export class ProfileActions extends React.PureComponent<Props, State> { }; handleSetDefaultClick = () => { - setDefaultProfile(this.props.profile).then(this.props.updateProfiles, () => {}); + const { profile } = this.props; + if (profile.activeRuleCount > 0) { + setDefaultProfile(profile).then(this.props.updateProfiles, () => { + /* noop */ + }); + } }; profileActionPerformed = (name: string) => { @@ -181,6 +187,8 @@ export class ProfileActions extends React.PureComponent<Props, State> { activation: 'false' }); + const hasNoActiveRules = profile.activeRuleCount === 0; + return ( <> <ActionsDropdown className={this.props.className}> @@ -231,13 +239,24 @@ export class ProfileActions extends React.PureComponent<Props, State> { </ActionsDropdownItem> )} - {actions.setAsDefault && ( - <ActionsDropdownItem - className="it__quality-profiles__set-as-default" - onClick={this.handleSetDefaultClick}> - {translate('set_as_default')} - </ActionsDropdownItem> - )} + {actions.setAsDefault && + (hasNoActiveRules ? ( + <li> + <Tooltip + placement="left" + overlay={translate('quality_profiles.cannot_set_default_no_rules')}> + <span className="it__quality-profiles__set-as-default text-muted-2"> + {translate('set_as_default')} + </span> + </Tooltip> + </li> + ) : ( + <ActionsDropdownItem + className="it__quality-profiles__set-as-default" + onClick={this.handleSetDefaultClick}> + {translate('set_as_default')} + </ActionsDropdownItem> + ))} {actions.delete && <ActionsDropdownDivider />} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx index a6ad0fffb57..d5320fa5067 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx @@ -323,6 +323,22 @@ it('should correctly set a profile as the default', async () => { expect(updateProfiles).toHaveBeenCalled(); }); +it('should not allow to set a profile as the default if the profile has no active rules', async () => { + const profile = mockQualityProfile({ + activeRuleCount: 0, + actions: { + setAsDefault: true + } + }); + + const wrapper = shallowRender({ profile }); + wrapper.instance().handleSetDefaultClick(); + await waitAndUpdate(wrapper); + + expect(setDefaultProfile).not.toHaveBeenCalled(); + expect(wrapper).toMatchSnapshot(); +}); + function shallowRender(props: Partial<ProfileActions['props']> = {}) { const router = mockRouter(); return shallow<ProfileActions>( diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap index 23a829c4d9a..ffb04e929d3 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap @@ -355,3 +355,43 @@ exports[`renders correctly: rename modal 1`] = ` /> </Fragment> `; + +exports[`should not allow to set a profile as the default if the profile has no active rules 1`] = ` +<Fragment> + <ActionsDropdown> + <ActionsDropdownItem + className="it__quality-profiles__backup" + download="key.xml" + to="/api/qualityprofiles/backup?language=js&qualityProfile=name" + > + backup_verb + </ActionsDropdownItem> + <ActionsDropdownItem + className="it__quality-profiles__compare" + to={ + Object { + "pathname": "/profiles/compare", + "query": Object { + "language": "js", + "name": "name", + }, + } + } + > + compare + </ActionsDropdownItem> + <li> + <Tooltip + overlay="quality_profiles.cannot_set_default_no_rules" + placement="left" + > + <span + className="it__quality-profiles__set-as-default text-muted-2" + > + set_as_default + </span> + </Tooltip> + </li> + </ActionsDropdown> +</Fragment> +`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx index d6e55d237a0..0e18bd84523 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx @@ -18,6 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { Alert } from 'sonar-ui-common/components/ui/Alert'; +import { translate } from 'sonar-ui-common/helpers/l10n'; import { Exporter, Profile } from '../types'; import ProfileExporters from './ProfileExporters'; import ProfileInheritance from './ProfileInheritance'; @@ -25,14 +27,14 @@ import ProfilePermissions from './ProfilePermissions'; import ProfileProjects from './ProfileProjects'; import ProfileRules from './ProfileRules'; -interface Props { +export interface ProfileDetailsProps { exporters: Exporter[]; profile: Profile; profiles: Profile[]; updateProfiles: () => Promise<void>; } -export default function ProfileDetails(props: Props) { +export default function ProfileDetails(props: ProfileDetailsProps) { const { profile } = props; return ( <div> @@ -45,6 +47,17 @@ export default function ProfileDetails(props: Props) { )} </div> <div className="quality-profile-grid-right"> + {profile.activeRuleCount === 0 && (profile.projectCount || profile.isDefault) && ( + <Alert className="big-spacer-bottom" variant="warning"> + {profile.projectCount !== undefined && + profile.projectCount > 0 && + translate('quality_profiles.warning.used_by_projects_no_rules')} + {!profile.projectCount && + profile.isDefault && + translate('quality_profiles.warning.is_default_no_rules')} + </Alert> + )} + <ProfileInheritance profile={profile} profiles={props.profiles} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx index 2bbb7955397..33758d5616c 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import { Link } from 'react-router'; import { Button } from 'sonar-ui-common/components/controls/buttons'; import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; +import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { getProfileProjects } from '../../../api/quality-profiles'; @@ -131,6 +132,11 @@ export default class ProfileProjects extends React.PureComponent<Props, State> { } const { projects } = this.state; + const { profile } = this.props; + + if (profile.activeRuleCount === 0 && projects.length === 0) { + return <div>{translate('quality_profiles.cannot_associate_projects_no_rules')}</div>; + } if (projects.length === 0) { return <div>{translate('quality_profiles.no_projects_associated_to_profile')}</div>; @@ -159,13 +165,24 @@ export default class ProfileProjects extends React.PureComponent<Props, State> { render() { const { profile } = this.props; + const hasNoActiveRules = profile.activeRuleCount === 0; return ( <div className="boxed-group quality-profile-projects"> {profile.actions && profile.actions.associateProjects && ( <div className="boxed-group-actions"> - <Button className="js-change-projects" onClick={this.handleChangeClick}> - {translate('quality_profiles.change_projects')} - </Button> + <Tooltip + overlay={ + hasNoActiveRules + ? translate('quality_profiles.cannot_associate_projects_no_rules') + : null + }> + <Button + className="js-change-projects" + onClick={this.handleChangeClick} + disabled={hasNoActiveRules}> + {translate('quality_profiles.change_projects')} + </Button> + </Tooltip> </div> )} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx index 5fca0c51a78..d564ce0d49c 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx @@ -19,31 +19,37 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { Profile } from '../../types'; -import ProfileDetails from '../ProfileDetails'; +import { mockQualityProfile } from '../../../../helpers/testMocks'; +import ProfileDetails, { ProfileDetailsProps } from '../ProfileDetails'; -it('renders without permissions', () => { +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); expect( - shallow( - <ProfileDetails - exporters={[]} - profile={{} as Profile} - profiles={[]} - updateProfiles={jest.fn()} - /> - ) - ).toMatchSnapshot(); -}); - -it('renders with edit permission', () => { + shallowRender({ profile: mockQualityProfile({ actions: { edit: true } }) }) + ).toMatchSnapshot('edit permissions'); + expect( + shallowRender({ + profile: mockQualityProfile({ activeRuleCount: 0, projectCount: 0 }) + }) + ).toMatchSnapshot('no active rules (same as default)'); expect( - shallow( - <ProfileDetails - exporters={[]} - profile={{ actions: { edit: true } } as Profile} - profiles={[]} - updateProfiles={jest.fn()} - /> - ) - ).toMatchSnapshot(); + shallowRender({ + profile: mockQualityProfile({ projectCount: 0, isDefault: true, activeRuleCount: 0 }) + }) + ).toMatchSnapshot('is default profile, no active rules'); + expect( + shallowRender({ profile: mockQualityProfile({ projectCount: 10, activeRuleCount: 0 }) }) + ).toMatchSnapshot('projects associated, no active rules'); }); + +function shallowRender(props: Partial<ProfileDetailsProps> = {}) { + return shallow<ProfileDetailsProps>( + <ProfileDetails + exporters={[]} + profile={mockQualityProfile()} + profiles={[]} + updateProfiles={jest.fn()} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileProjects-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileProjects-test.tsx index 1303e4917c2..8599b62a419 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileProjects-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileProjects-test.tsx @@ -41,9 +41,26 @@ jest.mock('../../../../api/quality-profiles', () => ({ it('should render correctly', async () => { const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot('loading'); await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot('default'); + wrapper.setProps({ + profile: mockQualityProfile({ actions: { associateProjects: false } }) + }); + expect(wrapper).toMatchSnapshot('no rights'); + wrapper.setProps({ + profile: mockQualityProfile({ + projectCount: 0, + activeRuleCount: 0, + actions: { associateProjects: true } + }) + }); + expect(wrapper).toMatchSnapshot('no active rules, but associated projects'); + wrapper.setProps({ + profile: mockQualityProfile({ activeRuleCount: 0, actions: { associateProjects: true } }) + }); + wrapper.setState({ projects: [] }); + expect(wrapper).toMatchSnapshot('no active rules, no associated projects'); }); it('should open and close the form', async () => { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileDetails-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileDetails-test.tsx.snap index 0dcae2b693e..ff98cd9515d 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileDetails-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileDetails-test.tsx.snap @@ -1,6 +1,98 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders with edit permission 1`] = ` +exports[`should render correctly: default 1`] = ` +<div> + <div + className="quality-profile-grid" + > + <div + className="quality-profile-grid-left" + > + <ProfileRules + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 10, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 3, + } + } + /> + <ProfileExporters + exporters={Array []} + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 10, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 3, + } + } + /> + </div> + <div + className="quality-profile-grid-right" + > + <ProfileInheritance + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 10, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 3, + } + } + profiles={Array []} + updateProfiles={[MockFunction]} + /> + <ProfileProjects + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 10, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 3, + } + } + /> + </div> + </div> +</div> +`; + +exports[`should render correctly: edit permissions 1`] = ` <div> <div className="quality-profile-grid" @@ -14,6 +106,18 @@ exports[`renders with edit permission 1`] = ` "actions": Object { "edit": true, }, + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 10, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 3, } } /> @@ -24,6 +128,18 @@ exports[`renders with edit permission 1`] = ` "actions": Object { "edit": true, }, + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 10, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 3, } } /> @@ -33,6 +149,18 @@ exports[`renders with edit permission 1`] = ` "actions": Object { "edit": true, }, + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 10, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 3, } } /> @@ -46,6 +174,18 @@ exports[`renders with edit permission 1`] = ` "actions": Object { "edit": true, }, + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 10, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 3, } } profiles={Array []} @@ -57,6 +197,18 @@ exports[`renders with edit permission 1`] = ` "actions": Object { "edit": true, }, + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 10, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 3, } } /> @@ -65,7 +217,7 @@ exports[`renders with edit permission 1`] = ` </div> `; -exports[`renders without permissions 1`] = ` +exports[`should render correctly: is default profile, no active rules 1`] = ` <div> <div className="quality-profile-grid" @@ -74,23 +226,279 @@ exports[`renders without permissions 1`] = ` className="quality-profile-grid-left" > <ProfileRules - profile={Object {}} + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 0, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": true, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 0, + } + } /> <ProfileExporters exporters={Array []} - profile={Object {}} + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 0, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": true, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 0, + } + } /> </div> <div className="quality-profile-grid-right" > + <Alert + className="big-spacer-bottom" + variant="warning" + > + quality_profiles.warning.is_default_no_rules + </Alert> <ProfileInheritance - profile={Object {}} + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 0, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": true, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 0, + } + } profiles={Array []} updateProfiles={[MockFunction]} /> <ProfileProjects - profile={Object {}} + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 0, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": true, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 0, + } + } + /> + </div> + </div> +</div> +`; + +exports[`should render correctly: no active rules (same as default) 1`] = ` +<div> + <div + className="quality-profile-grid" + > + <div + className="quality-profile-grid-left" + > + <ProfileRules + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 0, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 0, + } + } + /> + <ProfileExporters + exporters={Array []} + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 0, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 0, + } + } + /> + </div> + <div + className="quality-profile-grid-right" + > + <ProfileInheritance + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 0, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 0, + } + } + profiles={Array []} + updateProfiles={[MockFunction]} + /> + <ProfileProjects + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 0, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 0, + } + } + /> + </div> + </div> +</div> +`; + +exports[`should render correctly: projects associated, no active rules 1`] = ` +<div> + <div + className="quality-profile-grid" + > + <div + className="quality-profile-grid-left" + > + <ProfileRules + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 0, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 10, + } + } + /> + <ProfileExporters + exporters={Array []} + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 0, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 10, + } + } + /> + </div> + <div + className="quality-profile-grid-right" + > + <Alert + className="big-spacer-bottom" + variant="warning" + > + quality_profiles.warning.used_by_projects_no_rules + </Alert> + <ProfileInheritance + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 0, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 10, + } + } + profiles={Array []} + updateProfiles={[MockFunction]} + /> + <ProfileProjects + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 0, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "projectCount": 10, + } + } /> </div> </div> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap index 32a90773230..d69b1c67fa8 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap @@ -1,18 +1,92 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly 1`] = ` +exports[`should render correctly: default 1`] = ` <div className="boxed-group quality-profile-projects" > <div className="boxed-group-actions" > - <Button - className="js-change-projects" - onClick={[Function]} + <Tooltip + overlay={null} > - quality_profiles.change_projects - </Button> + <Button + className="js-change-projects" + disabled={false} + onClick={[Function]} + > + quality_profiles.change_projects + </Button> + </Tooltip> + </div> + <header + className="boxed-group-header" + > + <h2> + projects + </h2> + </header> + <div + className="boxed-group-inner" + > + <ul> + <li + className="spacer-top js-profile-project" + data-key="org.sonarsource.xml:xml" + key="org.sonarsource.xml:xml" + > + <Link + className="link-with-icon" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "org.sonarsource.xml:xml", + }, + } + } + > + <QualifierIcon + qualifier="TRK" + /> + + <span> + SonarXML + </span> + </Link> + </li> + </ul> + <ListFooter + count={1} + loadMore={[Function]} + ready={true} + total={10} + /> + </div> +</div> +`; + +exports[`should render correctly: loading 1`] = ` +<div + className="boxed-group quality-profile-projects" +> + <div + className="boxed-group-actions" + > + <Tooltip + overlay={null} + > + <Button + className="js-change-projects" + disabled={false} + onClick={[Function]} + > + quality_profiles.change_projects + </Button> + </Tooltip> </div> <header className="boxed-group-header" @@ -31,19 +105,24 @@ exports[`should render correctly 1`] = ` </div> `; -exports[`should render correctly 2`] = ` +exports[`should render correctly: no active rules, but associated projects 1`] = ` <div className="boxed-group quality-profile-projects" > <div className="boxed-group-actions" > - <Button - className="js-change-projects" - onClick={[Function]} + <Tooltip + overlay="quality_profiles.cannot_associate_projects_no_rules" > - quality_profiles.change_projects - </Button> + <Button + className="js-change-projects" + disabled={true} + onClick={[Function]} + > + quality_profiles.change_projects + </Button> + </Tooltip> </div> <header className="boxed-group-header" @@ -94,3 +173,93 @@ exports[`should render correctly 2`] = ` </div> </div> `; + +exports[`should render correctly: no active rules, no associated projects 1`] = ` +<div + className="boxed-group quality-profile-projects" +> + <div + className="boxed-group-actions" + > + <Tooltip + overlay="quality_profiles.cannot_associate_projects_no_rules" + > + <Button + className="js-change-projects" + disabled={true} + onClick={[Function]} + > + quality_profiles.change_projects + </Button> + </Tooltip> + </div> + <header + className="boxed-group-header" + > + <h2> + projects + </h2> + </header> + <div + className="boxed-group-inner" + > + <div> + quality_profiles.cannot_associate_projects_no_rules + </div> + </div> +</div> +`; + +exports[`should render correctly: no rights 1`] = ` +<div + className="boxed-group quality-profile-projects" +> + <header + className="boxed-group-header" + > + <h2> + projects + </h2> + </header> + <div + className="boxed-group-inner" + > + <ul> + <li + className="spacer-top js-profile-project" + data-key="org.sonarsource.xml:xml" + key="org.sonarsource.xml:xml" + > + <Link + className="link-with-icon" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "org.sonarsource.xml:xml", + }, + } + } + > + <QualifierIcon + qualifier="TRK" + /> + + <span> + SonarXML + </span> + </Link> + </li> + </ul> + <ListFooter + count={1} + loadMore={[Function]} + ready={true} + total={10} + /> + </div> +</div> +`; diff --git a/server/sonar-web/src/main/js/components/common/DisableableSelectOption.tsx b/server/sonar-web/src/main/js/components/common/DisableableSelectOption.tsx new file mode 100644 index 00000000000..90c5612baf2 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/DisableableSelectOption.tsx @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +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; + disabledReason?: string; +} + +export default function DisableableSelectOption(props: DisableableSelectOptionProps) { + const { option, tooltipOverlay, disabledReason } = props; + const label = option.label || option.value; + return option.disabled ? ( + <Tooltip overlay={tooltipOverlay} placement="left"> + <span> + {label} + {disabledReason !== undefined && ( + <em className="small little-spacer-left">({disabledReason})</em> + )} + </span> + </Tooltip> + ) : ( + <span>{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 new file mode 100644 index 00000000000..c810aa4a687 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/DisableableSelectOption-test.tsx @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { shallow } from 'enzyme'; +import * as React from 'react'; +import DisableableSelectOption, { DisableableSelectOptionProps } from '../DisableableSelectOption'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ option: { value: 'baz' } })).toMatchSnapshot('no label'); + expect(shallowRender({ option: { label: 'Bar', value: 'bar', disabled: true } })).toMatchSnapshot( + 'disabled' + ); + expect( + shallowRender({ + option: { label: 'Bar', value: 'bar', disabled: true }, + disabledReason: 'bar baz' + }) + ).toMatchSnapshot('disabled, with explanation'); +}); + +function shallowRender(props: Partial<DisableableSelectOptionProps> = {}) { + return shallow<DisableableSelectOptionProps>( + <DisableableSelectOption + option={{ label: 'Foo', value: 'foo' }} + tooltipOverlay="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 new file mode 100644 index 00000000000..5ed0c2ae05a --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DisableableSelectOption-test.tsx.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` +<span> + Foo +</span> +`; + +exports[`should render correctly: disabled 1`] = ` +<Tooltip + overlay="foo bar" + placement="left" +> + <span> + Bar + </span> +</Tooltip> +`; + +exports[`should render correctly: disabled, with explanation 1`] = ` +<Tooltip + overlay="foo bar" + placement="left" +> + <span> + Bar + <em + className="small little-spacer-left" + > + ( + bar baz + ) + </em> + </span> +</Tooltip> +`; + +exports[`should render correctly: no label 1`] = ` +<span> + baz +</span> +`; |