]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20356 Add message in QP copy, extend and create modal to warn user when a QP...
author7PH <benjamin.raymond@sonarsource.com>
Thu, 14 Sep 2023 04:32:08 +0000 (06:32 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 14 Sep 2023 20:02:39 +0000 (20:02 +0000)
server/sonar-web/src/main/js/api/quality-profiles.ts
server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx
server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileModalForm.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx
server/sonar-web/src/main/js/queries/quality-profiles.ts [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 2860afa142cc51ef61a7b70c9fa73ea99164b922..d0c90c71881b8a9984971e6ce9690e30267b7818 100644 (file)
@@ -103,7 +103,10 @@ export function getProfileProjects(
   return getJSON('/api/qualityprofiles/projects', data).catch(throwGlobalError);
 }
 
-export function getProfileInheritance({ language, name: qualityProfile }: Profile): Promise<{
+export function getProfileInheritance({
+  language,
+  name: qualityProfile,
+}: Pick<Profile, 'language' | 'name'>): Promise<{
   ancestors: ProfileInheritanceDetails[];
   children: ProfileInheritanceDetails[];
   profile: ProfileInheritanceDetails;
index 33b5c34404354c0a35d82107fa34638eefb5aebe..f8febad6618879fe9d06266577eaaae799e86aaa 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import { act, screen } from '@testing-library/react';
+import { act, screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import selectEvent from 'react-select-event';
 import QualityProfilesServiceMock from '../../../api/mocks/QualityProfilesServiceMock';
@@ -35,6 +35,7 @@ beforeEach(() => {
 
 const serviceMock = new QualityProfilesServiceMock();
 const ui = {
+  loading: byRole('status', { name: 'loading' }),
   permissionSection: byRole('region', { name: 'permissions.page' }),
   projectSection: byRole('region', { name: 'projects' }),
   rulesSection: byRole('region', { name: 'rules' }),
@@ -80,6 +81,12 @@ const ui = {
   rulesMissingSonarWayLink: byRole('link', { name: '2' }),
   rulesDeprecatedWarning: byText('quality_profiles.deprecated_rules_description'),
   rulesDeprecatedLink: byRole('link', { name: '8' }),
+
+  waitForDataLoaded: async () => {
+    await waitFor(() => {
+      expect(ui.loading.query()).not.toBeInTheDocument();
+    });
+  },
 };
 
 describe('Admin or user with permission', () => {
@@ -91,6 +98,7 @@ describe('Admin or user with permission', () => {
     it('should be able to grant permission to a user and remove it', async () => {
       const user = userEvent.setup();
       renderQualityProfile();
+      await ui.waitForDataLoaded();
 
       expect(await ui.permissionSection.find()).toBeInTheDocument();
 
@@ -117,6 +125,7 @@ describe('Admin or user with permission', () => {
     it('should be able to grant permission to a group and remove it', async () => {
       const user = userEvent.setup();
       renderQualityProfile();
+      await ui.waitForDataLoaded();
 
       expect(await ui.permissionSection.find()).toBeInTheDocument();
 
@@ -142,6 +151,7 @@ describe('Admin or user with permission', () => {
 
     it('should not be able to grant permission if the profile is built-in', async () => {
       renderQualityProfile('sonar');
+      await ui.waitForDataLoaded();
       expect(await screen.findByText('Sonar way')).toBeInTheDocument();
       expect(ui.permissionSection.query()).not.toBeInTheDocument();
     });
@@ -151,14 +161,20 @@ describe('Admin or user with permission', () => {
     it('should be able to add a project to Quality Profile with active rules', async () => {
       const user = userEvent.setup();
       renderQualityProfile();
+      await ui.waitForDataLoaded();
 
       expect(await ui.projectSection.find()).toBeInTheDocument();
+
       expect(ui.projectSection.byText('Twitter').query()).not.toBeInTheDocument();
-      await user.click(ui.changeProjectsButton.get());
+      await act(async () => {
+        await user.click(ui.changeProjectsButton.get());
+      });
       expect(ui.dialog.get()).toBeInTheDocument();
 
-      await user.click(ui.withoutFilterButton.get());
-      await user.click(ui.twitterCheckbox.get());
+      await act(async () => {
+        await user.click(ui.withoutFilterButton.get());
+        await user.click(ui.twitterCheckbox.get());
+      });
       await user.click(ui.closeButton.get());
       expect(ui.projectSection.byText('Twitter').get()).toBeInTheDocument();
     });
@@ -166,27 +182,38 @@ describe('Admin or user with permission', () => {
     it('should be able to remove a project from a Quality Profile with active rules', async () => {
       const user = userEvent.setup();
       renderQualityProfile();
+      await ui.waitForDataLoaded();
 
       expect(await ui.projectSection.find()).toBeInTheDocument();
+
       expect(ui.projectSection.byText('Benflix').get()).toBeInTheDocument();
-      await user.click(ui.changeProjectsButton.get());
+      await act(async () => {
+        await user.click(ui.changeProjectsButton.get());
+      });
       expect(ui.dialog.get()).toBeInTheDocument();
 
-      await user.click(ui.benflixCheckbox.get());
+      await act(async () => {
+        await user.click(ui.benflixCheckbox.get());
+      });
       await user.click(ui.closeButton.get());
       expect(ui.projectSection.byText('Benflix').query()).not.toBeInTheDocument();
     });
 
     it('should not be able to change project for Quality Profile with no active rules', async () => {
       renderQualityProfile('no-rule-qp');
+      await ui.waitForDataLoaded();
 
       expect(await ui.projectSection.find()).toBeInTheDocument();
+
       expect(ui.changeProjectsButton.get()).toHaveAttribute('disabled');
     });
 
     it('should not be able to change projects for default profiles', async () => {
       renderQualityProfile('sonar');
+      await ui.waitForDataLoaded();
+
       expect(await ui.projectSection.find()).toBeInTheDocument();
+
       expect(
         await ui.projectSection.byText('quality_profiles.projects_for_default').get(),
       ).toBeInTheDocument();
@@ -196,6 +223,7 @@ describe('Admin or user with permission', () => {
   describe('Rules', () => {
     it('should be able to activate more rules', async () => {
       renderQualityProfile();
+      await ui.waitForDataLoaded();
 
       expect(await ui.rulesSection.find()).toBeInTheDocument();
 
@@ -208,6 +236,7 @@ describe('Admin or user with permission', () => {
 
     it("shouldn't be able to activate more rules for built in Quality Profile", async () => {
       renderQualityProfile('sonar');
+      await ui.waitForDataLoaded();
       expect(await ui.rulesSection.find()).toBeInTheDocument();
       expect(ui.activateMoreRulesButton.get()).toBeInTheDocument();
       expect(ui.activateMoreRulesButton.get()).toBeDisabled();
@@ -218,21 +247,29 @@ describe('Admin or user with permission', () => {
     it("should be able to change a quality profile's parents", async () => {
       const user = userEvent.setup();
       renderQualityProfile();
+      await ui.waitForDataLoaded();
 
       expect(await ui.inheritanceSection.find()).toBeInTheDocument();
+
       // Parents
       expect(ui.inheritanceSection.byText('PHP Sonar way 1').get()).toBeInTheDocument();
       expect(ui.inheritanceSection.byText('PHP Sonar way 2').query()).not.toBeInTheDocument();
       // Children
       expect(ui.inheritanceSection.byText('PHP way').get()).toBeInTheDocument();
 
-      await user.click(ui.changeParentButton.get());
+      await act(async () => {
+        await user.click(ui.changeParentButton.get());
+      });
       expect(await ui.dialog.find()).toBeInTheDocument();
       expect(ui.changeButton.get()).toBeDisabled();
       await selectEvent.select(ui.selectField.get(), 'PHP Sonar way 2');
-      await user.click(ui.changeButton.get());
+      await act(async () => {
+        await user.click(ui.changeButton.get());
+      });
       expect(ui.dialog.query()).not.toBeInTheDocument();
 
+      await ui.waitForDataLoaded();
+
       // Parents
       expect(ui.inheritanceSection.byText('PHP Sonar way 2').get()).toBeInTheDocument();
       expect(ui.inheritanceSection.byText('PHP Sonar way 1').query()).not.toBeInTheDocument();
@@ -242,6 +279,7 @@ describe('Admin or user with permission', () => {
 
     it("should not be able to change a Built-in quality profile's parents", async () => {
       renderQualityProfile('php-sonar-way-1');
+      await ui.waitForDataLoaded();
 
       expect(await ui.inheritanceSection.find()).toBeInTheDocument();
       expect(ui.changeParentButton.query()).not.toBeInTheDocument();
@@ -252,6 +290,7 @@ describe('Admin or user with permission', () => {
     it('should be able to activate more rules', async () => {
       const user = userEvent.setup();
       renderQualityProfile();
+      await ui.waitForDataLoaded();
 
       await user.click(await ui.qualityProfileActions.find());
       expect(ui.activateMoreRulesLink.get()).toBeInTheDocument();
@@ -264,6 +303,7 @@ describe('Admin or user with permission', () => {
     it('should be able to extend a quality profile', async () => {
       const user = userEvent.setup();
       renderQualityProfile();
+      await ui.waitForDataLoaded();
 
       expect(await screen.findByText('Good old PHP quality profile')).toBeInTheDocument();
 
@@ -281,6 +321,8 @@ describe('Admin or user with permission', () => {
 
       expect(ui.dialog.query()).not.toBeInTheDocument();
 
+      await ui.waitForDataLoaded();
+
       expect(screen.getAllByText('Bad new PHP quality profile')).toHaveLength(2);
       expect(screen.getByText('Good old PHP quality profile')).toBeInTheDocument();
     });
@@ -288,6 +330,7 @@ describe('Admin or user with permission', () => {
     it('should be able to copy a quality profile', async () => {
       const user = userEvent.setup();
       renderQualityProfile();
+      await ui.waitForDataLoaded();
 
       await user.click(await ui.qualityProfileActions.find());
       await user.click(ui.copyButton.get());
@@ -303,12 +346,14 @@ describe('Admin or user with permission', () => {
 
       expect(ui.dialog.query()).not.toBeInTheDocument();
 
-      expect(screen.getAllByText('Good old PHP quality profile copy')).toHaveLength(2);
+      await ui.waitForDataLoaded();
+      expect(await screen.findAllByText('Good old PHP quality profile copy')).toHaveLength(2);
     });
 
     it('should be able to rename a quality profile', async () => {
       const user = userEvent.setup();
       renderQualityProfile();
+      await ui.waitForDataLoaded();
 
       await user.click(await ui.qualityProfileActions.find());
       await user.click(ui.renameButton.get());
@@ -324,12 +369,14 @@ describe('Admin or user with permission', () => {
 
       expect(ui.dialog.query()).not.toBeInTheDocument();
 
+      await ui.waitForDataLoaded();
       expect(screen.getAllByText('Fossil PHP quality profile')).toHaveLength(2);
     });
 
     it('should be able to set a quality profile as default', async () => {
       const user = userEvent.setup();
       renderQualityProfile();
+      await ui.waitForDataLoaded();
 
       await user.click(await ui.qualityProfileActions.find());
       await user.click(ui.setAsDefaultButton.get());
@@ -340,6 +387,7 @@ describe('Admin or user with permission', () => {
     it('should NOT be able to set a quality profile as default if it has no active rules', async () => {
       const user = userEvent.setup();
       renderQualityProfile('no-rule-qp');
+      await ui.waitForDataLoaded();
 
       await user.click(await ui.qualityProfileActions.find());
       expect(ui.setAsDefaultButton.query()).not.toBeInTheDocument();
@@ -348,6 +396,7 @@ describe('Admin or user with permission', () => {
     it("should be able to delete a Quality Profile and it's children", async () => {
       const user = userEvent.setup();
       renderQualityProfile();
+      await ui.waitForDataLoaded();
 
       await user.click(await ui.qualityProfileActions.find());
       await user.click(ui.deleteQualityProfileButton.get());
@@ -373,6 +422,7 @@ describe('Admin or user with permission', () => {
 describe('Users with no permission', () => {
   it('should not be able to activate more rules', async () => {
     renderQualityProfile();
+    await ui.waitForDataLoaded();
 
     expect(await ui.rulesSection.find()).toBeInTheDocument();
     expect(ui.activateMoreLink.query()).not.toBeInTheDocument();
@@ -386,6 +436,7 @@ describe('Users with no permission', () => {
 
   it("should not be able to change a quality profile's parents", async () => {
     renderQualityProfile();
+    await ui.waitForDataLoaded();
 
     expect(await ui.inheritanceSection.find()).toBeInTheDocument();
     expect(ui.inheritanceSection.byText('PHP Sonar way 1').get()).toBeInTheDocument();
@@ -396,6 +447,7 @@ describe('Users with no permission', () => {
 
   it('should not be able to change projects for Quality Profile', async () => {
     renderQualityProfile();
+    await ui.waitForDataLoaded();
 
     expect(await ui.projectSection.find()).toBeInTheDocument();
     expect(ui.changeProjectsButton.query()).not.toBeInTheDocument();
@@ -405,6 +457,7 @@ describe('Users with no permission', () => {
 describe('Every Users', () => {
   it('should be able to see active/inactive rules for a Quality Profile', async () => {
     renderQualityProfile();
+    await ui.waitForDataLoaded();
 
     expect(await ui.rulesSection.find()).toBeInTheDocument();
 
@@ -425,6 +478,7 @@ describe('Every Users', () => {
 
   it('should be able to see a warning when some rules are missing compare to Sonar way', async () => {
     renderQualityProfile();
+    await ui.waitForDataLoaded();
 
     expect(await ui.rulesMissingSonarWayWarning.findAll()).toHaveLength(2);
     expect(ui.rulesMissingSonarWayLink.get()).toBeInTheDocument();
@@ -436,6 +490,7 @@ describe('Every Users', () => {
 
   it('should be able to see a warning when some rules are deprecated', async () => {
     renderQualityProfile();
+    await ui.waitForDataLoaded();
 
     expect(await ui.rulesDeprecatedWarning.findAll()).toHaveLength(1);
     expect(ui.rulesDeprecatedLink.get()).toBeInTheDocument();
@@ -447,6 +502,7 @@ describe('Every Users', () => {
 
   it('should be able to see exporters links when there are exporters for the language', async () => {
     renderQualityProfile();
+    await ui.waitForDataLoaded();
 
     expect(await ui.exportersSection.find()).toBeInTheDocument();
     expect(ui.exportersSection.byText('SonarLint for Visual Studio').get()).toBeInTheDocument();
@@ -455,6 +511,7 @@ describe('Every Users', () => {
 
   it('should be informed when the quality profile has not been found', async () => {
     renderQualityProfile('i-dont-exist');
+    await ui.waitForDataLoaded();
 
     expect(await screen.findByText('quality_profiles.not_found')).toBeInTheDocument();
     expect(ui.qualityProfilePageLink.get()).toBeInTheDocument();
@@ -463,6 +520,7 @@ describe('Every Users', () => {
   it('should be able to backup quality profile', async () => {
     const user = userEvent.setup();
     renderQualityProfile();
+    await ui.waitForDataLoaded();
 
     await user.click(await ui.qualityProfileActions.find());
     expect(ui.backUpLink.get()).toHaveAttribute(
@@ -475,6 +533,7 @@ describe('Every Users', () => {
   it('should not be able to backup a built-in quality profile', async () => {
     const user = userEvent.setup();
     renderQualityProfile('sonar');
+    await ui.waitForDataLoaded();
 
     await user.click(await ui.qualityProfileActions.find());
     expect(ui.backUpLink.query()).not.toBeInTheDocument();
@@ -483,6 +542,7 @@ describe('Every Users', () => {
   it('should be able to compare quality profile', async () => {
     const user = userEvent.setup();
     renderQualityProfile();
+    await ui.waitForDataLoaded();
 
     await user.click(await ui.qualityProfileActions.find());
 
index ec48942dc25f9b92ec9a137eac7af19c705cceba..2be89439d938833f5e09abb2513d12b4eebf19f0 100644 (file)
@@ -73,6 +73,9 @@ const ui = {
   copyRadio: byRole('radio', {
     name: 'quality_profiles.creation_from_copy quality_profiles.creation_from_copy_description_1 quality_profiles.creation_from_copy_description_2',
   }),
+  extendRadio: byRole('radio', {
+    name: 'quality_profiles.creation_from_extend quality_profiles.creation_from_extend_description_1 quality_profiles.creation_from_extend_description_2',
+  }),
   blankRadio: byRole('radio', {
     name: 'quality_profiles.creation_from_blank quality_profiles.creation_from_blank_description',
   }),
@@ -125,13 +128,11 @@ it('should list Quality Profiles and filter by language', async () => {
   expect(ui.listLinkJavaQualityProfile.query()).not.toBeInTheDocument();
 
   // Creation form should have language pre-selected
-  await user.click(await ui.createButton.find());
+  await act(async () => {
+    await user.click(await ui.createButton.find());
+  });
   // eslint-disable-next-line testing-library/prefer-screen-queries
   expect(getByText(ui.popup.get(), 'C')).toBeInTheDocument();
-  await selectEvent.select(ui.profileExtendSelect.get(), ui.cQualityProfileName);
-  await user.type(ui.nameCreatePopupInput.get(), ui.newCQualityProfileName);
-
-  expect(ui.createButton.get(ui.popup.get())).toBeEnabled();
 });
 
 describe('Evolution', () => {
@@ -191,7 +192,10 @@ describe('Create', () => {
     expect(await ui.headingNewCQualityProfile.find()).toBeInTheDocument();
 
     await user.click(ui.returnToList.get());
-    await user.click(ui.createButton.get());
+    await act(async () => {
+      await user.click(ui.createButton.get());
+      await user.click(ui.extendRadio.get());
+    });
     await selectEvent.select(ui.languageSelect.get(), 'C');
     await selectEvent.select(ui.profileExtendSelect.get(), ui.newCQualityProfileName);
     await user.type(ui.nameCreatePopupInput.get(), ui.newCQualityProfileNameFromCreateButton);
@@ -218,7 +222,9 @@ describe('Create', () => {
     expect(await ui.headingNewCQualityProfile.find()).toBeInTheDocument();
 
     await user.click(ui.returnToList.get());
-    await user.click(ui.createButton.get());
+    await act(async () => {
+      await user.click(ui.createButton.get());
+    });
     await user.click(ui.copyRadio.get());
     await selectEvent.select(ui.languageSelect.get(), 'C');
     await selectEvent.select(ui.profileCopySelect.get(), ui.newCQualityProfileName);
@@ -234,7 +240,10 @@ describe('Create', () => {
     const user = userEvent.setup();
     serviceMock.setAdmin();
     renderQualityProfiles();
-    await user.click(await ui.createButton.find());
+
+    await act(async () => {
+      await user.click(await ui.createButton.find());
+    });
     await user.click(ui.blankRadio.get());
     await selectEvent.select(ui.languageSelect.get(), 'C');
     await user.type(ui.nameCreatePopupInput.get(), ui.newCQualityProfileName);
index 22e01fec550123b8cc2d73ca52affc1b30a7d056..a615de68270117c235b2bb6f04a8aebe07225a76 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { ButtonPrimary, FormField, InputField, Modal } from 'design-system';
+import { ButtonPrimary, FlagMessage, FormField, InputField, Modal } from 'design-system';
 import * as React from 'react';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { useProfileInheritanceQuery } from '../../../queries/quality-profiles';
 import { Dict } from '../../../types/types';
 import { Profile, ProfileActionModals } from '../types';
 
@@ -45,6 +46,13 @@ export default function ProfileModalForm(props: ProfileModalFormProps) {
   const submitDisabled = loading || !name || name === profile.name;
   const labels = LABELS_FOR_ACTION[action];
 
+  const { data: { ancestors } = {} } = useProfileInheritanceQuery(props.profile);
+
+  const extendsBuiltIn = ancestors?.some((profile) => profile.isBuiltIn);
+  const showBuiltInWarning =
+    (action === ProfileActionModals.Copy && !extendsBuiltIn) ||
+    (action === ProfileActionModals.Extend && !profile.isBuiltIn && !extendsBuiltIn);
+
   return (
     <Modal
       headerTitle={translateWithParameters(labels.header, profile.name, profile.languageName)}
@@ -52,6 +60,17 @@ export default function ProfileModalForm(props: ProfileModalFormProps) {
       loading={loading}
       body={
         <>
+          {showBuiltInWarning && (
+            <FlagMessage variant="info" className="sw-mb-4">
+              <div className="sw-flex sw-flex-col">
+                {translate('quality_profiles.no_built_in_updates_warning.new_profile')}
+                <span className="sw-mt-2">
+                  {translate('quality_profiles.no_built_in_updates_warning.new_profile.2')}
+                </span>
+              </div>
+            </FlagMessage>
+          )}
+
           {action === ProfileActionModals.Copy && (
             <p className="sw-mb-8">
               {translateWithParameters('quality_profiles.copy_help', profile.name)}
index 421fc552beb36bd4cc30e2eb4e031fdd22e4e3e2..f877e5b1ad697209f96262827b2eee37fa5e78f3 100644 (file)
@@ -24,13 +24,16 @@ import { useLocation } from '../../../components/hoc/withRouter';
 import DateFromNow from '../../../components/intl/DateFromNow';
 import { AdminPageHeader } from '../../../components/ui/AdminPageHeader';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { getQualityProfileUrl } from '../../../helpers/urls';
 import BuiltInQualityProfileBadge from '../components/BuiltInQualityProfileBadge';
 import ProfileActions from '../components/ProfileActions';
 import { PROFILE_PATH } from '../constants';
 import { QualityProfilePath } from '../routes';
 import { Profile } from '../types';
-import { getProfileChangelogPath, isProfileComparePath } from '../utils';
+import {
+  getProfileChangelogPath,
+  getProfilesForLanguagePath,
+  isProfileComparePath,
+} from '../utils';
 
 interface Props {
   profile: Profile;
@@ -60,7 +63,7 @@ export default function ProfileHeader(props: Props) {
 
       <Breadcrumbs className="sw-mb-6">
         <HoverLink to={PROFILE_PATH}>{translate('quality_profiles.page')}</HoverLink>
-        <HoverLink to={getQualityProfileUrl(profile.name, profile.language)}>
+        <HoverLink to={getProfilesForLanguagePath(profile.language)}>
           {profile.languageName}
         </HoverLink>
       </Breadcrumbs>
index a0791f3071705e45fb479ab2ede3c477465a5e7e..3410e29ac11d3ed2716daf1fae867bd7dfbb0922 100644 (file)
@@ -20,9 +20,8 @@
 import classNames from 'classnames';
 import { ButtonSecondary, FlagMessage, Spinner, SubTitle, Table } from 'design-system';
 import * as React from 'react';
-import { getProfileInheritance } from '../../../api/quality-profiles';
 import { translate } from '../../../helpers/l10n';
-import { ProfileInheritanceDetails } from '../../../types/types';
+import { useProfileInheritanceQuery } from '../../../queries/quality-profiles';
 import { Profile } from '../types';
 import ChangeParentForm from './ChangeParentForm';
 import ProfileInheritanceBox from './ProfileInheritanceBox';
@@ -33,167 +32,115 @@ interface Props {
   updateProfiles: () => Promise<void>;
 }
 
-interface State {
-  ancestors?: ProfileInheritanceDetails[];
-  children?: ProfileInheritanceDetails[];
-  formOpen: boolean;
-  loading: boolean;
-  profile?: ProfileInheritanceDetails;
-}
+export default function ProfileInheritance(props: Props) {
+  const { profile, profiles, updateProfiles } = props;
+  const [formOpen, setFormOpen] = React.useState(false);
 
-export default class ProfileInheritance extends React.PureComponent<Props, State> {
-  mounted = false;
+  const { data: { ancestors, children, profile: profileInheritanceDetail } = {}, isLoading } =
+    useProfileInheritanceQuery(profile);
 
-  state: State = {
-    formOpen: false,
-    loading: true,
-  };
+  const handleChangeParentClick = React.useCallback(() => {
+    setFormOpen(true);
+  }, [setFormOpen]);
 
-  componentDidMount() {
-    this.mounted = true;
-    this.loadData();
-  }
+  const closeForm = React.useCallback(() => {
+    setFormOpen(false);
+  }, [setFormOpen]);
 
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.profile.key !== this.props.profile.key) {
-      this.loadData();
+  const handleParentChange = React.useCallback(async () => {
+    try {
+      await updateProfiles();
+    } catch (error) {
+      // ignore
+    } finally {
+      closeForm();
     }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  loadData() {
-    getProfileInheritance(this.props.profile).then(
-      (r) => {
-        if (this.mounted) {
-          const { ancestors, children } = r;
-          ancestors.reverse();
-
-          this.setState({
-            children,
-            ancestors,
-            profile: r.profile,
-            loading: false,
-          });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      },
-    );
-  }
-
-  handleChangeParentClick = () => {
-    this.setState({ formOpen: true });
-  };
-
-  closeForm = () => {
-    this.setState({ formOpen: false });
-  };
-
-  handleParentChange = () => {
-    this.props.updateProfiles().then(
-      () => {
-        this.loadData();
-      },
-      () => {},
-    );
-    this.closeForm();
-  };
-
-  render() {
-    const { profile, profiles } = this.props;
-    const { ancestors, loading, formOpen, children } = this.state;
-
-    const highlightCurrent =
-      !this.state.loading &&
-      ancestors != null &&
-      this.state.children != null &&
-      (ancestors.length > 0 || this.state.children.length > 0);
-
-    const extendsBuiltIn = ancestors?.some((profile) => profile.isBuiltIn);
-
-    return (
-      <section
-        aria-label={translate('quality_profiles.profile_inheritance')}
-        className="it__quality-profiles__inheritance"
-      >
-        <div className="sw-flex sw-items-center sw-gap-3 sw-mb-6">
-          <SubTitle className="sw-mb-0">
-            {translate('quality_profiles.profile_inheritance')}
-          </SubTitle>
-          {profile.actions?.edit && !profile.isBuiltIn && (
-            <ButtonSecondary
-              className="it__quality-profiles__change-parent"
-              onClick={this.handleChangeParentClick}
-            >
-              {translate('quality_profiles.change_parent')}
-            </ButtonSecondary>
-          )}
-        </div>
-
-        {!extendsBuiltIn && (
-          <FlagMessage variant="info" className="sw-mb-4">
-            <div className="sw-flex sw-flex-col">
-              {translate('quality_profiles.no_built_in_updates_warning')}
-              {profile.actions?.edit && (
-                <span className="sw-mt-1">
-                  {translate('quality_profiles.no_built_in_updates_warning_admin')}
-                </span>
-              )}
-            </div>
-          </FlagMessage>
+  }, [closeForm, updateProfiles]);
+
+  const highlightCurrent =
+    !isLoading &&
+    ancestors != null &&
+    children != null &&
+    (ancestors.length > 0 || children.length > 0);
+
+  const extendsBuiltIn = ancestors?.some((p) => p.isBuiltIn);
+
+  return (
+    <section
+      aria-label={translate('quality_profiles.profile_inheritance')}
+      className="it__quality-profiles__inheritance"
+    >
+      <div className="sw-flex sw-items-center sw-gap-3 sw-mb-6">
+        <SubTitle className="sw-mb-0">{translate('quality_profiles.profile_inheritance')}</SubTitle>
+        {profile.actions?.edit && !profile.isBuiltIn && (
+          <ButtonSecondary
+            className="it__quality-profiles__change-parent"
+            onClick={handleChangeParentClick}
+          >
+            {translate('quality_profiles.change_parent')}
+          </ButtonSecondary>
         )}
-
-        <Spinner loading={loading}>
-          <Table columnCount={3} noSidePadding>
-            {ancestors?.map((ancestor, index) => (
-              <ProfileInheritanceBox
-                depth={index}
-                key={ancestor.key}
-                language={profile.language}
-                profile={ancestor}
-                type="ancestor"
-              />
-            ))}
-
-            {this.state.profile && (
-              <ProfileInheritanceBox
-                className={classNames({
-                  selected: highlightCurrent,
-                })}
-                depth={ancestors ? ancestors.length : 0}
-                displayLink={false}
-                language={profile.language}
-                profile={this.state.profile}
-              />
+      </div>
+
+      {!extendsBuiltIn && !profile.isBuiltIn && (
+        <FlagMessage variant="info" className="sw-mb-4">
+          <div className="sw-flex sw-flex-col">
+            {translate('quality_profiles.no_built_in_updates_warning')}
+            {profile.actions?.edit && (
+              <span className="sw-mt-1">
+                {translate('quality_profiles.no_built_in_updates_warning_admin')}
+              </span>
             )}
+          </div>
+        </FlagMessage>
+      )}
+
+      <Spinner loading={isLoading}>
+        <Table columnCount={3} noSidePadding>
+          {ancestors?.map((ancestor, index) => (
+            <ProfileInheritanceBox
+              depth={index}
+              key={ancestor.key}
+              language={profile.language}
+              profile={ancestor}
+              type="ancestor"
+            />
+          ))}
+
+          {profileInheritanceDetail && (
+            <ProfileInheritanceBox
+              className={classNames({
+                selected: highlightCurrent,
+              })}
+              depth={ancestors ? ancestors.length : 0}
+              displayLink={false}
+              language={profile.language}
+              profile={profileInheritanceDetail}
+            />
+          )}
 
-            {children?.map((child) => (
-              <ProfileInheritanceBox
-                depth={ancestors ? ancestors.length + 1 : 0}
-                key={child.key}
-                language={profile.language}
-                profile={child}
-                type="child"
-              />
-            ))}
-          </Table>
-        </Spinner>
-
-        {formOpen && (
-          <ChangeParentForm
-            onChange={this.handleParentChange}
-            onClose={this.closeForm}
-            profile={profile}
-            profiles={profiles.filter((p) => p !== profile && p.language === profile.language)}
-          />
-        )}
-      </section>
-    );
-  }
+          {children?.map((child) => (
+            <ProfileInheritanceBox
+              depth={ancestors ? ancestors.length + 1 : 0}
+              key={child.key}
+              language={profile.language}
+              profile={child}
+              type="child"
+            />
+          ))}
+        </Table>
+      </Spinner>
+
+      {formOpen && (
+        <ChangeParentForm
+          onChange={handleParentChange}
+          onClose={closeForm}
+          profile={profile}
+          profiles={profiles.filter(
+            (p) => p !== profileInheritanceDetail && p.language === profile.language,
+          )}
+        />
+      )}
+    </section>
+  );
 }
index 4370e6a382b107e713f0ac995db1210547be9c11..d85073feb1776ebcae94e829b30abaee31a977ba 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { FlagMessage } from 'design-system';
 import { sortBy } from 'lodash';
 import * as React from 'react';
 import {
@@ -35,8 +36,10 @@ import CopyQualityProfileIcon from '../../../components/icons/CopyQualityProfile
 import ExtendQualityProfileIcon from '../../../components/icons/ExtendQualityProfileIcon';
 import NewQualityProfileIcon from '../../../components/icons/NewQualityProfileIcon';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
+import Spinner from '../../../components/ui/Spinner';
 import { translate } from '../../../helpers/l10n';
 import { parseAsOptionalString } from '../../../helpers/query';
+import { useProfileInheritanceQuery } from '../../../queries/quality-profiles';
 import { Profile, ProfileActionModals } from '../types';
 
 interface Props {
@@ -47,313 +50,325 @@ interface Props {
   profiles: Profile[];
 }
 
-interface State {
-  importers: Array<{ key: string; languages: Array<string>; name: string }>;
-  action?: ProfileActionModals.Copy | ProfileActionModals.Extend;
-  language?: string;
-  loading: boolean;
-  name: string;
-  profile?: string;
-  preloading: boolean;
-  isValidName?: boolean;
-  isValidProflie?: boolean;
-  isValidLanguage?: boolean;
-}
-
-export default class CreateProfileForm extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = {
-    importers: [],
-    loading: false,
-    name: '',
-    preloading: true,
-    action: ProfileActionModals.Extend,
-  };
+export default function CreateProfileForm(props: Props) {
+  const { languages, profiles, onCreate } = props;
+  const [importers, setImporters] = React.useState<
+    Array<{ key: string; languages: string[]; name: string }>
+  >([]);
+  const [action, setAction] = React.useState<
+    ProfileActionModals.Copy | ProfileActionModals.Extend | undefined
+  >();
+  const [submitting, setSubmitting] = React.useState(false);
+  const [name, setName] = React.useState('');
+  const [loading, setLoading] = React.useState(true);
+  const [language, setLanguage] = React.useState<string>();
+  const [isValidLanguage, setIsValidLanguage] = React.useState<boolean>();
+  const [isValidName, setIsValidName] = React.useState<boolean>();
+  const [isValidProfile, setIsValidProfile] = React.useState<boolean>();
+  const [profile, setProfile] = React.useState<Profile>();
 
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchImporters();
-    const languageQueryFilter = parseAsOptionalString(this.props.location.query.language);
-    if (languageQueryFilter !== undefined) {
-      this.setState({ language: languageQueryFilter, isValidLanguage: true });
+  const fetchImporters = React.useCallback(async () => {
+    setLoading(true);
+    try {
+      const importers = await getImporters();
+      setImporters(importers);
+    } finally {
+      setLoading(false);
     }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
+  }, [setImporters, setLoading]);
 
-  fetchImporters() {
-    getImporters().then(
-      (importers) => {
-        if (this.mounted) {
-          this.setState({ importers, preloading: false });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ preloading: false });
-        }
-      },
-    );
+  function handleSelectExtend() {
+    setAction(ProfileActionModals.Extend);
   }
 
-  handleSelectExtend = () => {
-    this.setState({ action: ProfileActionModals.Extend });
-  };
+  const handleSelectCopy = React.useCallback(() => {
+    setAction(ProfileActionModals.Copy);
+  }, [setAction]);
 
-  handleSelectCopy = () => {
-    this.setState({ action: ProfileActionModals.Copy });
-  };
+  const handleSelectBlank = React.useCallback(() => {
+    setAction(undefined);
+  }, [setAction]);
 
-  handleSelectBlank = () => {
-    this.setState({ action: undefined });
-  };
+  const handleNameChange = React.useCallback(
+    (event: React.SyntheticEvent<HTMLInputElement>) => {
+      setName(event.currentTarget.value);
+      setIsValidName(event.currentTarget.value.length > 0);
+    },
+    [setName, setIsValidName],
+  );
 
-  handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
-    this.setState({
-      name: event.currentTarget.value,
-      isValidName: event.currentTarget.value.length > 0,
-    });
-  };
+  const handleLanguageChange = React.useCallback(
+    (option: { value: string }) => {
+      setLanguage(option.value);
+      setIsValidLanguage(true);
+      setProfile(undefined);
+      setIsValidProfile(false);
+    },
+    [setLanguage, setIsValidLanguage],
+  );
 
-  handleLanguageChange = (option: { value: string }) => {
-    this.setState({ language: option.value, isValidLanguage: true });
-  };
+  const handleQualityProfileChange = React.useCallback(
+    (option: { value: Profile } | null) => {
+      setProfile(option?.value);
+      setIsValidProfile(option !== null);
+    },
+    [setProfile, setIsValidProfile],
+  );
 
-  handleQualityProfileChange = (option: { value: string } | null) => {
-    this.setState({ profile: option ? option.value : undefined, isValidProflie: option !== null });
-  };
+  const handleFormSubmit = React.useCallback(
+    async (event: React.SyntheticEvent<HTMLFormElement>) => {
+      event.preventDefault();
 
-  handleFormSubmit = async (event: React.SyntheticEvent<HTMLFormElement>) => {
-    event.preventDefault();
+      setSubmitting(true);
+      const profileKey = profile?.key;
+      try {
+        if (action === ProfileActionModals.Copy && profileKey && name) {
+          const profile = await copyProfile(profileKey, name);
+          onCreate(profile);
+        } else if (action === ProfileActionModals.Extend) {
+          const { profile } = await createQualityProfile({ language, name });
 
-    this.setState({ loading: true });
-    const { action, language, name, profile: parent } = this.state;
+          const parentProfile = profiles.find((p) => p.key === profileKey);
+          if (parentProfile) {
+            await changeProfileParent(profile, parentProfile);
+          }
 
-    try {
-      if (action === ProfileActionModals.Copy && parent && name) {
-        const profile = await copyProfile(parent, name);
-        this.props.onCreate(profile);
-      } else if (action === ProfileActionModals.Extend) {
-        const { profile } = await createQualityProfile({ language, name });
-
-        const parentProfile = this.props.profiles.find((p) => p.key === parent);
-        if (parentProfile) {
-          await changeProfileParent(profile, parentProfile);
+          onCreate(profile);
+        } else {
+          const data = new FormData(event.currentTarget);
+          const { profile } = await createQualityProfile(data);
+          onCreate(profile);
         }
-
-        this.props.onCreate(profile);
-      } else {
-        const data = new FormData(event.currentTarget);
-        const { profile } = await createQualityProfile(data);
-        this.props.onCreate(profile);
-      }
-    } finally {
-      if (this.mounted) {
-        this.setState({ loading: false });
+      } finally {
+        setSubmitting(false);
       }
-    }
-  };
+    },
+    [setSubmitting, onCreate, profiles, action, language, name, profile],
+  );
 
-  canSubmit() {
-    const { action, isValidName, isValidProflie, isValidLanguage } = this.state;
+  React.useEffect(() => {
+    fetchImporters();
+    const languageQueryFilter = parseAsOptionalString(props.location.query.language);
+    if (languageQueryFilter !== undefined) {
+      setLanguage(languageQueryFilter);
+      setIsValidLanguage(true);
+    }
+  }, [fetchImporters, props.location.query.language]);
 
-    return (
-      (action === undefined && isValidName && isValidLanguage) ||
-      (action !== undefined && isValidLanguage && isValidName && isValidProflie)
-    );
-  }
+  const { data: { ancestors } = {}, isLoading } = useProfileInheritanceQuery(
+    action === undefined || language === undefined || profile === undefined
+      ? undefined
+      : {
+          language,
+          name: profile.name,
+        },
+  );
 
-  render() {
-    const header = translate('quality_profiles.new_profile');
-    const { action, isValidName, isValidProflie, isValidLanguage } = this.state;
-    const languageQueryFilter = parseAsOptionalString(this.props.location.query.language);
-    const languages = sortBy(this.props.languages, 'name');
+  const extendsBuiltIn = ancestors?.some((p) => p.isBuiltIn);
+  const showBuiltInWarning =
+    action === undefined ||
+    (action === ProfileActionModals.Copy && !extendsBuiltIn && profile !== undefined) ||
+    (action === ProfileActionModals.Extend &&
+      !extendsBuiltIn &&
+      profile !== undefined &&
+      !profile.isBuiltIn);
+  const canSubmit =
+    (action === undefined && isValidName && isValidLanguage) ||
+    (action !== undefined && isValidLanguage && isValidName && isValidProfile);
+  const header = translate('quality_profiles.new_profile');
 
-    const selectedLanguage = this.state.language || languageQueryFilter;
-    const importers = selectedLanguage
-      ? this.state.importers.filter((importer) => importer.languages.includes(selectedLanguage))
-      : [];
+  const languageQueryFilter = parseAsOptionalString(props.location.query.language);
+  const selectedLanguage = language ?? languageQueryFilter;
+  const filteredImporters = selectedLanguage
+    ? importers.filter((importer) => importer.languages.includes(selectedLanguage))
+    : [];
 
-    const languageProfiles = this.props.profiles.filter((p) => p.language === selectedLanguage);
-    const profiles = sortBy(languageProfiles, 'name').map((profile) => ({
-      label: profile.isBuiltIn
-        ? `${profile.name} (${translate('quality_profiles.built_in')})`
-        : profile.name,
-      value: profile.key,
-    }));
+  const profilesForSelectedLanguage = profiles.filter((p) => p.language === selectedLanguage);
+  const profileOptions = sortBy(profilesForSelectedLanguage, 'name').map((profile) => ({
+    label: profile.isBuiltIn
+      ? `${profile.name} (${translate('quality_profiles.built_in')})`
+      : profile.name,
+    value: profile,
+  }));
 
-    const languagesOptions = languages.map((l) => ({
-      label: l.name,
-      value: l.key,
-    }));
+  const languagesOptions = sortBy(languages, 'name').map((l) => ({
+    label: l.name,
+    value: l.key,
+  }));
 
-    const canSubmit = this.canSubmit();
+  return (
+    <Modal contentLabel={header} onRequestClose={props.onClose} size="medium">
+      <form id="create-profile-form" onSubmit={handleFormSubmit}>
+        <div className="modal-head">
+          <h2>{header}</h2>
+        </div>
 
-    return (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose} size="medium">
-        <form id="create-profile-form" onSubmit={this.handleFormSubmit}>
-          <div className="modal-head">
-            <h2>{header}</h2>
+        {loading ? (
+          <div className="modal-body">
+            <Spinner />
           </div>
+        ) : (
+          <div className="modal-body modal-container">
+            <fieldset className="modal-field big-spacer-bottom">
+              <label className="spacer-top">
+                {translate('quality_profiles.chose_creation_type')}
+              </label>
+              <div className="display-flex-row spacer-top">
+                <RadioCard
+                  noRadio
+                  selected={action === ProfileActionModals.Extend}
+                  onClick={handleSelectExtend}
+                  title={<ExtendQualityProfileIcon size={64} />}
+                >
+                  <h3 className="spacer-bottom h4">
+                    {translate('quality_profiles.creation_from_extend')}
+                  </h3>
+                  <p className="spacer-bottom">
+                    {translate('quality_profiles.creation_from_extend_description_1')}
+                  </p>
+                  <p>{translate('quality_profiles.creation_from_extend_description_2')}</p>
+                </RadioCard>
+                <RadioCard
+                  noRadio
+                  selected={action === ProfileActionModals.Copy}
+                  onClick={handleSelectCopy}
+                  title={<CopyQualityProfileIcon size={64} />}
+                >
+                  <h3 className="spacer-bottom h4">
+                    {translate('quality_profiles.creation_from_copy')}
+                  </h3>
+                  <p className="spacer-bottom">
+                    {translate('quality_profiles.creation_from_copy_description_1')}
+                  </p>
+                  <p>{translate('quality_profiles.creation_from_copy_description_2')}</p>
+                </RadioCard>
+                <RadioCard
+                  noRadio
+                  onClick={handleSelectBlank}
+                  selected={action === undefined}
+                  title={<NewQualityProfileIcon size={64} />}
+                >
+                  <h3 className="spacer-bottom h4">
+                    {translate('quality_profiles.creation_from_blank')}
+                  </h3>
+                  <p>{translate('quality_profiles.creation_from_blank_description')}</p>
+                </RadioCard>
+              </div>
+            </fieldset>
 
-          {this.state.preloading ? (
-            <div className="modal-body">
-              <i className="spinner" />
-            </div>
-          ) : (
-            <div className="modal-body modal-container">
-              <fieldset className="modal-field big-spacer-bottom">
-                <label className="spacer-top">
-                  {translate('quality_profiles.chose_creation_type')}
-                </label>
-                <div className="display-flex-row spacer-top">
-                  <RadioCard
-                    noRadio
-                    selected={action === ProfileActionModals.Extend}
-                    onClick={this.handleSelectExtend}
-                    title={<ExtendQualityProfileIcon size={64} />}
-                  >
-                    <h3 className="spacer-bottom h4">
-                      {translate('quality_profiles.creation_from_extend')}
-                    </h3>
-                    <p className="spacer-bottom">
-                      {translate('quality_profiles.creation_from_extend_description_1')}
-                    </p>
-                    <p>{translate('quality_profiles.creation_from_extend_description_2')}</p>
-                  </RadioCard>
-                  <RadioCard
-                    noRadio
-                    selected={action === ProfileActionModals.Copy}
-                    onClick={this.handleSelectCopy}
-                    title={<CopyQualityProfileIcon size={64} />}
-                  >
-                    <h3 className="spacer-bottom h4">
-                      {translate('quality_profiles.creation_from_copy')}
-                    </h3>
-                    <p className="spacer-bottom">
-                      {translate('quality_profiles.creation_from_copy_description_1')}
-                    </p>
-                    <p>{translate('quality_profiles.creation_from_copy_description_2')}</p>
-                  </RadioCard>
-                  <RadioCard
-                    noRadio
-                    onClick={this.handleSelectBlank}
-                    selected={action === undefined}
-                    title={<NewQualityProfileIcon size={64} />}
-                  >
-                    <h3 className="spacer-bottom h4">
-                      {translate('quality_profiles.creation_from_blank')}
-                    </h3>
-                    <p>{translate('quality_profiles.creation_from_blank_description')}</p>
-                  </RadioCard>
+            {!isLoading && showBuiltInWarning && (
+              <FlagMessage variant="info" className="sw-mb-4">
+                <div className="sw-flex sw-flex-col">
+                  {translate('quality_profiles.no_built_in_updates_warning.new_profile')}
+                  <span className="sw-mt-1">
+                    {translate('quality_profiles.no_built_in_updates_warning.new_profile.2')}
+                  </span>
                 </div>
-              </fieldset>
+              </FlagMessage>
+            )}
 
-              <MandatoryFieldsExplanation className="modal-field" />
+            <MandatoryFieldsExplanation className="modal-field" />
 
+            <ValidationInput
+              className="form-field"
+              labelHtmlFor="create-profile-language-input"
+              label={translate('language')}
+              required
+              isInvalid={isValidLanguage !== undefined && !isValidLanguage}
+              isValid={!!isValidLanguage}
+            >
+              <Select
+                autoFocus
+                inputId="create-profile-language-input"
+                name="language"
+                isClearable={false}
+                onChange={handleLanguageChange}
+                options={languagesOptions}
+                isSearchable
+                value={languagesOptions.filter((o) => o.value === selectedLanguage)}
+              />
+            </ValidationInput>
+            {action !== undefined && (
               <ValidationInput
                 className="form-field"
-                labelHtmlFor="create-profile-language-input"
-                label={translate('language')}
+                labelHtmlFor="create-profile-parent-input"
+                label={translate(
+                  action === ProfileActionModals.Copy
+                    ? 'quality_profiles.creation.choose_copy_quality_profile'
+                    : 'quality_profiles.creation.choose_parent_quality_profile',
+                )}
                 required
-                isInvalid={isValidLanguage !== undefined && !isValidLanguage}
-                isValid={!!isValidLanguage}
+                isInvalid={isValidProfile !== undefined && !isValidProfile}
+                isValid={!!isValidProfile}
               >
                 <Select
                   autoFocus
-                  inputId="create-profile-language-input"
-                  name="language"
+                  inputId="create-profile-parent-input"
+                  name="parentKey"
                   isClearable={false}
-                  onChange={this.handleLanguageChange}
-                  options={languagesOptions}
+                  onChange={handleQualityProfileChange}
+                  options={profileOptions}
                   isSearchable
-                  value={languagesOptions.filter((o) => o.value === selectedLanguage)}
+                  value={profileOptions.filter((o) => o.value === profile)}
                 />
               </ValidationInput>
-              {action !== undefined && (
-                <ValidationInput
-                  className="form-field"
-                  labelHtmlFor="create-profile-parent-input"
-                  label={translate(
-                    action === ProfileActionModals.Copy
-                      ? 'quality_profiles.creation.choose_copy_quality_profile'
-                      : 'quality_profiles.creation.choose_parent_quality_profile',
-                  )}
-                  required
-                  isInvalid={isValidProflie !== undefined && !isValidProflie}
-                  isValid={!!isValidProflie}
+            )}
+            <ValidationInput
+              className="form-field"
+              labelHtmlFor="create-profile-name"
+              label={translate('name')}
+              error={translate('quality_profiles.name_invalid')}
+              required
+              isInvalid={isValidName !== undefined && !isValidName}
+              isValid={!!isValidName}
+            >
+              <input
+                autoFocus
+                id="create-profile-name"
+                maxLength={100}
+                name="name"
+                onChange={handleNameChange}
+                size={50}
+                type="text"
+                value={name}
+              />
+            </ValidationInput>
+
+            {action === undefined &&
+              filteredImporters.map((importer) => (
+                <div
+                  className="modal-field spacer-bottom js-importer"
+                  data-key={importer.key}
+                  key={importer.key}
                 >
-                  <Select
-                    autoFocus
-                    inputId="create-profile-parent-input"
-                    name="parentKey"
-                    isClearable={false}
-                    onChange={this.handleQualityProfileChange}
-                    options={profiles}
-                    isSearchable
-                    value={profiles.filter((o) => o.value === this.state.profile)}
+                  <label htmlFor={'create-profile-form-backup-' + importer.key}>
+                    {importer.name}
+                  </label>
+                  <input
+                    id={'create-profile-form-backup-' + importer.key}
+                    name={'backup_' + importer.key}
+                    type="file"
                   />
-                </ValidationInput>
-              )}
-              <ValidationInput
-                className="form-field"
-                labelHtmlFor="create-profile-name"
-                label={translate('name')}
-                error={translate('quality_profiles.name_invalid')}
-                required
-                isInvalid={isValidName !== undefined && !isValidName}
-                isValid={!!isValidName}
-              >
-                <input
-                  autoFocus
-                  id="create-profile-name"
-                  maxLength={100}
-                  name="name"
-                  onChange={this.handleNameChange}
-                  size={50}
-                  type="text"
-                  value={this.state.name}
-                />
-              </ValidationInput>
+                  <p className="note">
+                    {translate('quality_profiles.optional_configuration_file')}
+                  </p>
+                </div>
+              ))}
+          </div>
+        )}
 
-              {action === undefined &&
-                importers.map((importer) => (
-                  <div
-                    className="modal-field spacer-bottom js-importer"
-                    data-key={importer.key}
-                    key={importer.key}
-                  >
-                    <label htmlFor={'create-profile-form-backup-' + importer.key}>
-                      {importer.name}
-                    </label>
-                    <input
-                      id={'create-profile-form-backup-' + importer.key}
-                      name={'backup_' + importer.key}
-                      type="file"
-                    />
-                    <p className="note">
-                      {translate('quality_profiles.optional_configuration_file')}
-                    </p>
-                  </div>
-                ))}
-            </div>
+        <div className="modal-foot">
+          {(submitting || isLoading) && <i className="spinner spacer-right" />}
+          {!loading && (
+            <SubmitButton disabled={submitting || !canSubmit} id="create-profile-submit">
+              {translate('create')}
+            </SubmitButton>
           )}
-
-          <div className="modal-foot">
-            {this.state.loading && <i className="spinner spacer-right" />}
-            {!this.state.preloading && (
-              <SubmitButton disabled={this.state.loading || !canSubmit} id="create-profile-submit">
-                {translate('create')}
-              </SubmitButton>
-            )}
-            <ResetButtonLink id="create-profile-cancel" onClick={this.props.onClose}>
-              {translate('cancel')}
-            </ResetButtonLink>
-          </div>
-        </form>
-      </Modal>
-    );
-  }
+          <ResetButtonLink id="create-profile-cancel" onClick={props.onClose}>
+            {translate('cancel')}
+          </ResetButtonLink>
+        </div>
+      </form>
+    </Modal>
+  );
 }
diff --git a/server/sonar-web/src/main/js/queries/quality-profiles.ts b/server/sonar-web/src/main/js/queries/quality-profiles.ts
new file mode 100644 (file)
index 0000000..3783e07
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { UseQueryResult, useQuery } from '@tanstack/react-query';
+import { Profile, getProfileInheritance } from '../api/quality-profiles';
+import { ProfileInheritanceDetails } from '../types/types';
+
+export function useProfileInheritanceQuery(
+  profile?: Pick<Profile, 'language' | 'name' | 'parentKey'>,
+): UseQueryResult<{
+  ancestors: ProfileInheritanceDetails[];
+  children: ProfileInheritanceDetails[];
+  profile: ProfileInheritanceDetails | null;
+}> {
+  const { language, name, parentKey } = profile ?? {};
+  return useQuery({
+    queryKey: ['quality-profiles', 'inheritance', language, name, parentKey],
+    queryFn: async ({ queryKey: [, , language, name] }) => {
+      if (!language || !name) {
+        return { ancestors: [], children: [], profile: null };
+      }
+      const response = await getProfileInheritance({ language, name });
+      response.ancestors.reverse();
+      return response;
+    },
+  });
+}
index c86473c75781527438b9e9fe69b37ba3d5e83bd5..c68205281a7f6e13b36f72a11681137d600c00bf 100644 (file)
@@ -2038,6 +2038,8 @@ quality_profiles.built_in.description=This is a built-in quality profile that mi
 quality_profiles.extends_built_in=Because this quality profile inherits from a built-in quality profile, it might be updated automatically.
 quality_profiles.no_built_in_updates_warning=This quality profile does not inherit from a built-in profile. It will not benefit from automatic updates when new rules are introduced.
 quality_profiles.no_built_in_updates_warning_admin=To benefit from automatic updates, set the corresponding built-in profile as the parent of your quality profile.
+quality_profiles.no_built_in_updates_warning.new_profile=This new quality profile won't inherit from a built-in profile. It will not benefit from automatic updates when new rules are introduced.
+quality_profiles.no_built_in_updates_warning.new_profile.2=If you want to benefit from automatic updates, consider extending a built-in quality profile instead.
 quality_profiles.default_permissions=Users with the global "Administer Quality Profiles" permission and those listed below can manage this quality profile.
 quality_profiles.grant_permissions_to_more_users=Grant permissions to more users
 quality_profiles.grant_permissions_to_user_or_group=Grant permissions to a user or a group