Explorar el Código

SONAR-13150 Prevent using quality profiles with no active rules

tags/9.1.0.47736
Wouter Admiraal hace 2 años
padre
commit
f8f1b4c9fd
Se han modificado 19 ficheros con 1078 adiciones y 71 borrados
  1. 32
    1
      server/sonar-web/src/main/js/apps/projectQualityProfiles/components/AddLanguageModal.tsx
  2. 36
    2
      server/sonar-web/src/main/js/apps/projectQualityProfiles/components/SetQualityProfileModal.tsx
  3. 25
    1
      server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/AddLanguageModal-test.tsx
  4. 11
    7
      server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/SetQualityProfileModal-test.tsx
  5. 35
    0
      server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/AddLanguageModal-test.tsx.snap
  6. 37
    3
      server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/SetQualityProfileModal-test.tsx.snap
  7. 27
    8
      server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
  8. 16
    0
      server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx
  9. 40
    0
      server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap
  10. 15
    2
      server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx
  11. 20
    3
      server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx
  12. 30
    24
      server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx
  13. 19
    2
      server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileProjects-test.tsx
  14. 414
    6
      server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileDetails-test.tsx.snap
  15. 181
    12
      server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap
  16. 44
    0
      server/sonar-web/src/main/js/components/common/DisableableSelectOption.tsx
  17. 47
    0
      server/sonar-web/src/main/js/components/common/__tests__/DisableableSelectOption-test.tsx
  18. 42
    0
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DisableableSelectOption-test.tsx.snap
  19. 7
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 32
- 1
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/AddLanguageModal.tsx Ver fichero

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

+ 36
- 2
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/SetQualityProfileModal.tsx Ver fichero

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

+ 25
- 1
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/AddLanguageModal-test.tsx Ver fichero

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

+ 11
- 7
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/SetQualityProfileModal-test.tsx Ver fichero

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

+ 35
- 0
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/AddLanguageModal-test.tsx.snap Ver fichero

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

+ 37
- 3
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/SetQualityProfileModal-test.tsx.snap Ver fichero

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

+ 27
- 8
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx Ver fichero

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


+ 16
- 0
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx Ver fichero

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

+ 40
- 0
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap Ver fichero

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

+ 15
- 2
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx Ver fichero

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

+ 20
- 3
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx Ver fichero

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


+ 30
- 24
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx Ver fichero

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

+ 19
- 2
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileProjects-test.tsx Ver fichero

@@ -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 () => {

+ 414
- 6
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileDetails-test.tsx.snap Ver fichero

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

+ 181
- 12
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap Ver fichero

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

+ 44
- 0
server/sonar-web/src/main/js/components/common/DisableableSelectOption.tsx Ver fichero

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

+ 47
- 0
server/sonar-web/src/main/js/components/common/__tests__/DisableableSelectOption-test.tsx Ver fichero

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

+ 42
- 0
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DisableableSelectOption-test.tsx.snap Ver fichero

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

+ 7
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Ver fichero

@@ -1503,6 +1503,9 @@ project_quality_profile.add_language.action=Add language
project_quality_profile.add_language_modal.title=Add a language
project_quality_profile.add_language_modal.choose_language=Choose a language
project_quality_profile.add_language_modal.choose_profile=Choose a profile
project_quality_profile.add_language_modal.no_active_rules=this profile has no active rules
project_quality_profile.add_language_modal.profile_unavailable_no_active_rules=This profile has no active rules, and cannot be used. Please enable at least 1 rule before using this profile.
project_quality_profile.add_language_modal.go_to_profile=Go to Quality Profile
project_quality_profile.change_profile=Change profile

#------------------------------------------------------------------------------
@@ -1564,6 +1567,10 @@ quality_profiles.are_you_sure_want_delete_profile_x_and_descendants=Are you sure
quality_profiles.this_profile_has_descendants=This profile has descendants.
quality_profiles.profile_inheritance=Inheritance
quality_profiles.no_projects_associated_to_profile=No projects are explicitly associated to the profile.
quality_profiles.cannot_associate_projects_no_rules=You must activate at least 1 rule before you can assign projects to this profile.
quality_profiles.cannot_set_default_no_rules=You must activate at least 1 rule before you can make this profile the default profile.
quality_profiles.warning.used_by_projects_no_rules=The current profile is used on several projects, but it has no active rules. Please activate at least 1 rule for this profile.
quality_profiles.warning.is_default_no_rules=The current profile is the default profile, but it has no active rules. Please activate at least 1 rule for this profile.
quality_profiles.parent=Parent:
quality_profiles.parameter_set_to=Parameter {0} set to {1}
quality_profiles.x_rules_only_in={0} rules only in

Cargando…
Cancelar
Guardar