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;
* 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';
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' }),
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', () => {
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();
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();
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();
});
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();
});
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();
describe('Rules', () => {
it('should be able to activate more rules', async () => {
renderQualityProfile();
+ await ui.waitForDataLoaded();
expect(await ui.rulesSection.find()).toBeInTheDocument();
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();
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();
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();
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();
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();
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();
});
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());
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());
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());
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();
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());
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();
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();
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();
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();
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();
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();
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();
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();
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(
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();
it('should be able to compare quality profile', async () => {
const user = userEvent.setup();
renderQualityProfile();
+ await ui.waitForDataLoaded();
await user.click(await ui.qualityProfileActions.find());
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',
}),
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', () => {
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);
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);
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);
* 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';
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)}
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)}
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;
<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>
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';
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>
+ );
}
* 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 {
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 {
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>
+ );
}
--- /dev/null
+/*
+ * 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;
+ },
+ });
+}
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