]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13150 Prevent using quality profiles with no active rules
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Thu, 12 Aug 2021 08:21:24 +0000 (10:21 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 13 Aug 2021 20:03:54 +0000 (20:03 +0000)
19 files changed:
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/AddLanguageModal.tsx
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/SetQualityProfileModal.tsx
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/AddLanguageModal-test.tsx
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/SetQualityProfileModal-test.tsx
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/AddLanguageModal-test.tsx.snap
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/SetQualityProfileModal-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileDetails-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileProjects-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileDetails-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap
server/sonar-web/src/main/js/components/common/DisableableSelectOption.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/DisableableSelectOption-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/DisableableSelectOption-test.tsx.snap [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 0aae23b00c796530079f6ec081d5b9625716e098..871a5ce78852d2f0be06423c2ebb7950a91c94ba 100644 (file)
 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>
index 5c0217f6b3e474cfd8ec46e3c52bd2faf83f8147..20d5effd67152d24a53a2de614fa51adfc0bc516 100644 (file)
@@ -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>
index 60d5960ca8987b7945b79984ba75585e1ca7b5cb..9438ce4f61fb2a2d0cdb8a78ac87921cb017bae6 100644 (file)
@@ -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' })],
index 6e3b4fe914559667ab163891d16415b1b713f7fd..9a7995ca6ee32f590b3539997e0f98d17c1c0bce 100644 (file)
@@ -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' })}
index 96957df74e44986727c7aad2b0bbec5391cfcfed..8b22589ddcdcbf21adcb5ad2d3f3b03e62d91dce 100644 (file)
@@ -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>
+  }
+/>
+`;
index d14dd4876b8581150be3febc937c23d97348ee09..c61e39d1cffef6aff5dadd78acc69fc50c90b151 100644 (file)
@@ -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>
+  }
+/>
 `;
index c8ac23413f6e75c5ce89ad972416441626fb1caf..8b80c62cb64c15bf7b44627ce49733fc94db3059 100644 (file)
@@ -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 />}
 
index a6ad0fffb57c9b7fc9e665aed5807563fc5e9a31..d5320fa50675176ffb94e918f8672bfdd92bbb89 100644 (file)
@@ -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>(
index 23a829c4d9acd6f4bcd0f14323bb3ebee1eebb02..ffb04e929d35b3cc677491c48cd83648f5a38786 100644 (file)
@@ -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>
+`;
index d6e55d237a07b83a488361c8507af91b6e2bd118..0e18bd84523995f41501969cdf67834d84ae0c01 100644 (file)
@@ -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}
index 2bbb79553974d691a9e630e5fdd4b926a9c17b12..33758d5616cbf3204130f3a37a190604d0946584 100644 (file)
@@ -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>
         )}
 
index 5fca0c51a781ec882984ab1fbcb68fad4188f75a..d564ce0d49cd4a63c76e9a80a2ae9e0fb31a5cac 100644 (file)
  */
 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}
+    />
+  );
+}
index 1303e4917c2e20f60cb471d2c1f78792119945b8..8599b62a419ef05d4168b6f9e7dec0562c2e541a 100644 (file)
@@ -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 () => {
index 0dcae2b693e83ae505f42ef66c1642e3b5dfb052..ff98cd9515dab7488ebf6107c07a9177a7a8c38f 100644 (file)
@@ -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>
index 32a907732308d71d711622d9c7cc3ff15e26df45..d69b1c67fa823990c396c0ed53b715b6a95bb24f 100644 (file)
@@ -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 (file)
index 0000000..90c5612
--- /dev/null
@@ -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 (file)
index 0000000..c810aa4
--- /dev/null
@@ -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 (file)
index 0000000..5ed0c2a
--- /dev/null
@@ -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>
+`;
index 0fe177ed8ad50a22427ba188c50efbaca5f6713d..ec14f5372d2a5798a139d16910c4df0017dcc3e7 100644 (file)
@@ -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