@@ -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; |
@@ -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()); | |||
@@ -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); |
@@ -17,10 +17,11 @@ | |||
* 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)} |
@@ -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> |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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; | |||
}, | |||
}); | |||
} |
@@ -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 |