From: Wouter Admiraal Date: Fri, 18 Sep 2020 14:51:54 +0000 (+0200) Subject: SONAR-11063 Add 'Always use the Default' option at project level for QP X-Git-Tag: 8.6.0.39681~186 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=2a8f4fb40f513b9911ba3e5c21906464d38c909e;p=sonarqube.git SONAR-11063 Add 'Always use the Default' option at project level for QP --- diff --git a/server/sonar-web/src/main/js/api/quality-profiles.ts b/server/sonar-web/src/main/js/api/quality-profiles.ts index 5131add5f74..a19c067ea5b 100644 --- a/server/sonar-web/src/main/js/api/quality-profiles.ts +++ b/server/sonar-web/src/main/js/api/quality-profiles.ts @@ -69,7 +69,7 @@ export interface SearchQualityProfilesResponse { } export function searchQualityProfiles( - parameters: SearchQualityProfilesParameters + parameters?: SearchQualityProfilesParameters ): Promise { return getJSON('/api/qualityprofiles/search', parameters).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx deleted file mode 100644 index 8fd4a17e1f9..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx +++ /dev/null @@ -1,155 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { Helmet } from 'react-helmet-async'; -import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; -import { - associateProject, - dissociateProject, - Profile, - searchQualityProfiles -} from '../../api/quality-profiles'; -import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget'; -import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; -import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage'; -import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; -import Header from './Header'; -import Table from './Table'; - -interface Props { - component: T.Component; -} - -interface State { - allProfiles?: Profile[]; - loading: boolean; - profiles?: Profile[]; -} - -export default class QualityProfiles extends React.PureComponent { - mounted = false; - state: State = { loading: true }; - - componentDidMount() { - this.mounted = true; - if (this.checkPermissions()) { - this.fetchProfiles(); - } else { - handleRequiredAuthorization(); - } - } - - componentWillUnmount() { - this.mounted = false; - } - - checkPermissions() { - const { configuration } = this.props.component; - const hasPermission = configuration && configuration.showQualityProfiles; - return !!hasPermission; - } - - fetchProfiles() { - const { key, organization } = this.props.component; - Promise.all([ - searchQualityProfiles({ organization }).then(r => r.profiles), - searchQualityProfiles({ organization, project: key }).then(r => r.profiles) - ]).then( - ([allProfiles, profiles]) => { - if (this.mounted) { - this.setState({ loading: false, allProfiles, profiles }); - } - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); - } - - handleChangeProfile = (oldKey: string, newKey: string) => { - const { component } = this.props; - const { allProfiles, profiles } = this.state; - const oldProfile = allProfiles && allProfiles.find(profile => profile.key === oldKey); - const newProfile = allProfiles && allProfiles.find(profile => profile.key === newKey); - - let request; - - if (newProfile) { - if (newProfile.isDefault && oldProfile) { - request = dissociateProject(oldProfile, component.key); - } else { - request = associateProject(newProfile, component.key); - } - } - - if (request) { - return request.then(() => { - if (this.mounted && profiles && newProfile) { - // remove old profile, add new one - const nextProfiles = [...profiles.filter(profile => profile.key !== oldKey), newProfile]; - this.setState({ profiles: nextProfiles }); - - addGlobalSuccessMessage( - translateWithParameters( - 'project_quality_profile.successfully_updated', - newProfile.languageName - ) - ); - } - }); - } else { - return Promise.resolve(); - } - }; - - render() { - if (!this.checkPermissions()) { - return null; - } - - const { allProfiles, loading, profiles } = this.state; - - return ( -
- - - - - -
- - {loading ? ( - - ) : ( - allProfiles && - profiles && ( - - ) - )} - - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx deleted file mode 100644 index 10f8741af9b..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/Header.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip'; -import { translate } from 'sonar-ui-common/helpers/l10n'; - -export default function Header() { - return ( -
-
-

{translate('project_quality_profiles.page')}

- - {translate('quality_profiles.list.projects.help')} -
- } - /> - -
- {translate('project_quality_profiles.page.description')} -
-
- ); -} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProfileRow.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProfileRow.tsx deleted file mode 100644 index 5fc11fad1ea..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProfileRow.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import Select from 'sonar-ui-common/components/controls/Select'; -import { translate } from 'sonar-ui-common/helpers/l10n'; -import { Profile } from '../../api/quality-profiles'; - -interface Props { - onChangeProfile: (oldProfile: string, newProfile: string) => Promise; - possibleProfiles: Profile[]; - profile: Profile; -} - -interface State { - loading: boolean; -} - -export default class ProfileRow extends React.PureComponent { - mounted = false; - state: State = { loading: false }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - stopLoading = () => { - if (this.mounted) { - this.setState({ loading: false }); - } - }; - - handleChange = (option: { value: string }) => { - if (this.props.profile.key !== option.value) { - this.setState({ loading: true }); - this.props - .onChangeProfile(this.props.profile.key, option.value) - .then(this.stopLoading, this.stopLoading); - } - }; - - renderProfileName = (profileOption: { isDefault: boolean; label: string }) => { - if (profileOption.isDefault) { - return ( - - {translate('default')} - {': '} - {profileOption.label} - - ); - } - - return {profileOption.label}; - }; - - renderProfileSelect() { - const { profile, possibleProfiles } = this.props; - - const options = possibleProfiles.map(profile => ({ - value: profile.key, - label: profile.name, - isDefault: profile.isDefault - })); - - return ( - - - - - - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesApp.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesApp.tsx new file mode 100644 index 00000000000..0372d7d4c3b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesApp.tsx @@ -0,0 +1,292 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 { differenceBy } from 'lodash'; +import * as React from 'react'; +import { translateWithParameters } from 'sonar-ui-common/helpers/l10n'; +import { isDefined } from 'sonar-ui-common/helpers/types'; +import { + associateProject, + dissociateProject, + getProfileProjects, + Profile, + searchQualityProfiles +} from '../../api/quality-profiles'; +import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage'; +import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; +import ProjectQualityProfilesAppRenderer from './ProjectQualityProfilesAppRenderer'; +import { ProjectProfile } from './types'; + +interface Props { + component: T.Component; +} + +interface State { + allProfiles?: Profile[]; + loading: boolean; + projectProfiles?: ProjectProfile[]; + showAddLanguageModal?: boolean; + showProjectProfileInModal?: ProjectProfile; +} + +export default class ProjectQualityProfilesApp extends React.PureComponent { + mounted = false; + state: State = { loading: true }; + + componentDidMount() { + this.mounted = true; + if (this.checkPermissions()) { + this.fetchProfiles(); + } else { + handleRequiredAuthorization(); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + checkPermissions() { + const { configuration } = this.props.component; + const hasPermission = configuration && configuration.showQualityProfiles; + return !!hasPermission; + } + + fetchProfiles = async () => { + const { component } = this.props; + + const allProfiles = await searchQualityProfiles() + .then(({ profiles }) => profiles) + .catch(() => [] as Profile[]); + + // We need to know if a profile was explicitly assigned to a project, + // even if it's the system default. For this, we need to fetch the info + // for each existing profile. We only keep those that were effectively + // selected, and discard the rest. + const projectProfiles = await Promise.all( + allProfiles.map(profile => + getProfileProjects({ + key: profile.key, + q: component.name, + selected: 'selected' + }) + .then(({ results }) => ({ + selected: Boolean(results.find(p => p.key === component.key)?.selected), + profile + })) + .catch(() => ({ selected: false, profile })) + ) + ); + + const selectedProjectProfiles = projectProfiles + .filter(({ selected }) => selected) + .map(({ profile }) => ({ + profile, + selected: true + })); + + // Finally, the project uses some profiles implicitly, either inheriting + // from the system defaults, OR because the project wasn't re-analyzed + // yet (in which case the info is outdated). We also need this information. + const componentProfiles = differenceBy( + component.qualityProfiles, + selectedProjectProfiles.map(p => p.profile), + 'key' + ) + // Discard languages we already have up-to-date info for. + .filter(({ language }) => !selectedProjectProfiles.some(p => p.profile.language === language)) + .map(({ key }) => { + const profile = allProfiles.find(p => p.key === key); + if (profile) { + // If the profile is the default profile, all is good. + if (profile.isDefault) { + return { profile, selected: false }; + } else { + // If it is neither the default, nor explicitly selected, it + // means this is outdated information. This can only mean the + // user wants to use the default profile, but it will only + // be taken into account after a new analysis. Fetch the + // default profile. + const defaultProfile = allProfiles.find( + p => p.isDefault && p.language === profile.language + ); + return ( + defaultProfile && { + profile: defaultProfile, + selected: false + } + ); + } + } else { + return undefined; + } + }) + .filter(isDefined); + + if (this.mounted) { + this.setState({ + allProfiles, + projectProfiles: [...selectedProjectProfiles, ...componentProfiles], + loading: false + }); + } + }; + + handleOpenSetProfileModal = (showProjectProfileInModal: ProjectProfile) => { + this.setState({ showProjectProfileInModal }); + }; + + handleOpenAddLanguageModal = () => { + this.setState({ showAddLanguageModal: true }); + }; + + handleCloseModal = () => { + this.setState({ showAddLanguageModal: false, showProjectProfileInModal: undefined }); + }; + + handleAddLanguage = async (key: string) => { + const { component } = this.props; + const { allProfiles = [] } = this.state; + const newProfile = allProfiles.find(p => p.key === key); + + if (newProfile) { + try { + await associateProject(newProfile, component.key); + + if (this.mounted) { + this.setState(({ projectProfiles = [] }) => { + const newProjectProfiles = [ + ...projectProfiles, + { + profile: newProfile, + selected: true + } + ]; + + return { projectProfiles: newProjectProfiles, showAddLanguageModal: false }; + }); + + addGlobalSuccessMessage( + translateWithParameters( + 'project_quality_profile.successfully_updated', + newProfile.languageName + ) + ); + } + } catch (e) { + if (this.mounted) { + this.setState({ showAddLanguageModal: false }); + } + } + } + }; + + handleSetProfile = async (newKey: string | undefined, oldKey: string) => { + const { component } = this.props; + const { allProfiles = [], projectProfiles = [] } = this.state; + + const newProfile = newKey && allProfiles.find(p => p.key === newKey); + const oldProjectProfile = projectProfiles.find(p => p.profile.key === oldKey); + const defaultProfile = allProfiles.find( + p => p.isDefault && p.language === oldProjectProfile?.profile.language + ); + + if (defaultProfile === undefined || oldProjectProfile === undefined) { + // Isn't possible. We're in a messed up state. + return; + } + + let replaceProfile: Profile | undefined; + if (newProfile) { + replaceProfile = newProfile; + + // Associate with the new profile. + try { + await associateProject(newProfile, component.key); + } catch (e) { + // Something went wrong. Keep the old profile in the UI. + replaceProfile = oldProjectProfile.profile; + } + } else if (newKey === undefined) { + replaceProfile = defaultProfile; + + // We want to use the system default. Explicitly dissociate the project + // profile, if it was explicitly selected. + if (oldProjectProfile.selected) { + try { + await dissociateProject(oldProjectProfile.profile, component.key); + } catch (e) { + // Something went wrong. Keep the old profile in the UI. + replaceProfile = oldProjectProfile.profile; + } + } + } + + if (this.mounted) { + const newProjectProfiles = [ + // Remove the old profile. + ...projectProfiles.filter(p => p.profile.key !== oldKey), + // Replace with the "new" profile. + replaceProfile && { + profile: replaceProfile, + selected: newKey !== undefined + } + ].filter(isDefined); + + this.setState({ projectProfiles: newProjectProfiles, showProjectProfileInModal: undefined }); + + addGlobalSuccessMessage( + translateWithParameters( + 'project_quality_profile.successfully_updated', + defaultProfile.languageName + ) + ); + } + }; + + render() { + if (!this.checkPermissions()) { + return null; + } + + const { + allProfiles, + loading, + showProjectProfileInModal, + projectProfiles, + showAddLanguageModal + } = this.state; + + return ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesAppRenderer.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesAppRenderer.tsx new file mode 100644 index 00000000000..154aa4da45e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/ProjectQualityProfilesAppRenderer.tsx @@ -0,0 +1,187 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 { groupBy, orderBy } from 'lodash'; +import * as React from 'react'; +import { Helmet } from 'react-helmet-async'; +import { Link } from 'react-router'; +import { Button } from 'sonar-ui-common/components/controls/buttons'; +import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip'; +import EditIcon from 'sonar-ui-common/components/icons/EditIcon'; +import PlusCircleIcon from 'sonar-ui-common/components/icons/PlusCircleIcon'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import { Profile } from '../../api/quality-profiles'; +import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget'; +import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; +import { getRulesUrl } from '../../helpers/urls'; +import BuiltInQualityProfileBadge from '../quality-profiles/components/BuiltInQualityProfileBadge'; +import AddLanguageModal from './components/AddLanguageModal'; +import SetQualityProfileModal from './components/SetQualityProfileModal'; +import { ProjectProfile } from './types'; + +export interface ProjectQualityProfilesAppRendererProps { + allProfiles?: Profile[]; + component: T.Component; + loading: boolean; + onAddLanguage: (key: string) => Promise; + onCloseModal: () => void; + onOpenAddLanguageModal: () => void; + onOpenSetProfileModal: (projectProfile: ProjectProfile) => void; + onSetProfile: (newKey: string | undefined, oldKey: string) => Promise; + projectProfiles?: ProjectProfile[]; + showAddLanguageModal?: boolean; + showProjectProfileInModal?: ProjectProfile; +} + +export default function ProjectQualityProfilesAppRenderer( + props: ProjectQualityProfilesAppRendererProps +) { + const { + allProfiles, + component, + loading, + showProjectProfileInModal, + projectProfiles, + showAddLanguageModal + } = props; + + const profilesByLanguage = groupBy(allProfiles, 'language'); + const orderedProfiles = orderBy(projectProfiles, p => p.profile.languageName); + + return ( +
+ + + + +
+
+

{translate('project_quality_profiles.page')}

+ + {translate('quality_profiles.list.projects.help')} +
+ } + /> +
+ + +
+

{translate('project_quality_profile.subtitle')}

+ +
+

+ {translate('project_quality_profiles.page.description')} +

+ + {loading && } + + {!loading && orderedProfiles.length > 0 && ( +
{profile.languageName}{this.renderProfileSelect()}{this.state.loading && }
+ + + + + + + + + {orderedProfiles.map(projectProfile => { + const { profile, selected } = projectProfile; + return ( + + + + + + + ); + })} + +
{translate('language')}{translate('project_quality_profile.current')} + {translate('coding_rules.filters.activation.active_rules')} + +
{profile.languageName} + + {!selected && profile.isDefault ? ( + {translate('project_quality_profile.instance_default')} + ) : ( + <> + {profile.name} + {profile.isBuiltIn && ( + + )} + + )} + + + + {profile.activeRuleCount} + + + +
+ )} + +
+

{translate('project_quality_profile.add_language.title')}

+ +

+ {translate('project_quality_profile.add_language.description')} +

+ + +
+ + {showProjectProfileInModal && ( + + )} + + {showAddLanguageModal && projectProfiles && ( + p.profile.language)} + /> + )} +
+ + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx deleted file mode 100644 index f5617bfd996..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 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 { groupBy, orderBy } from 'lodash'; -import * as React from 'react'; -import { translate } from 'sonar-ui-common/helpers/l10n'; -import { Profile } from '../../api/quality-profiles'; -import ProfileRow from './ProfileRow'; - -interface Props { - allProfiles: Profile[]; - profiles: Profile[]; - onChangeProfile: (oldProfile: string, newProfile: string) => Promise; -} - -export default function Table(props: Props) { - const profilesByLanguage = groupBy(props.allProfiles, 'language'); - const orderedProfiles = orderBy(props.profiles, 'languageName'); - - // set key to language to avoid destroying of component - const profileRows = orderedProfiles.map(profile => ( - - )); - - return ( -
- - - - - - {/* keep one empty cell for the spinner */} - - - - {profileRows} -
{translate('language')}{translate('quality_profile')} 
-
- ); -} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx deleted file mode 100644 index b76184b910d..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { - associateProject, - dissociateProject, - searchQualityProfiles -} from '../../../api/quality-profiles'; -import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization'; -import { mockComponent, mockQualityProfile } from '../../../helpers/testMocks'; -import App from '../App'; -import Table from '../Table'; - -beforeEach(() => jest.clearAllMocks()); - -jest.mock('../../../api/quality-profiles', () => ({ - associateProject: jest.fn().mockResolvedValue({}), - dissociateProject: jest.fn().mockResolvedValue({}), - searchQualityProfiles: jest.fn().mockResolvedValue({}) -})); - -jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({ - default: jest.fn() -})); - -jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({ - default: jest.fn() -})); - -const component = mockComponent({ configuration: { showQualityProfiles: true } }); - -it('checks permissions', () => { - shallowRender({ component: { ...component, configuration: undefined } }); - expect(handleRequiredAuthorization).toBeCalled(); -}); - -it('fetches profiles', () => { - shallowRender(); - expect(searchQualityProfiles).toHaveBeenCalledTimes(2); - expect(searchQualityProfiles).toBeCalledWith({ organization: component.organization }); - expect(searchQualityProfiles).toBeCalledWith({ - organization: component.organization, - project: component.key - }); -}); - -it('changes profile', () => { - const wrapper = shallowRender(); - - const fooJava = mockQualityProfile({ key: 'foo-java', language: 'java' }); - const fooJs = mockQualityProfile({ key: 'foo-js', language: 'js' }); - const bar = mockQualityProfile({ key: 'bar-java', language: 'java' }); - const baz = mockQualityProfile({ key: 'baz-java', language: 'java', isDefault: true }); - const allProfiles = [fooJava, bar, baz, fooJs]; - const profiles = [fooJava, fooJs]; - wrapper.setState({ allProfiles, loading: false, profiles }); - - wrapper - .find(Table) - .props() - .onChangeProfile(fooJava.key, bar.key); - expect(associateProject).toBeCalledWith(bar, component.key); - - wrapper - .find(Table) - .props() - .onChangeProfile(fooJava.key, baz.key); - expect(dissociateProject).toBeCalledWith(fooJava, component.key); -}); - -function shallowRender(props: Partial = {}) { - return shallow(); -} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Header-test.tsx deleted file mode 100644 index 9730d14d916..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Header-test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import Header from '../Header'; - -it('renders', () => { - expect(shallow(
)).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProfileRow-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProfileRow-test.tsx deleted file mode 100644 index fe384a2731f..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProfileRow-test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import ProfileRow from '../ProfileRow'; - -it('renders', () => { - expect( - shallow( - - ) - ).toMatchSnapshot(); -}); - -it('changes profile', async () => { - const onChangeProfile = jest.fn(() => Promise.resolve()); - const wrapper = shallow( - - ); - (wrapper.instance() as ProfileRow).mounted = true; - wrapper.find('Select').prop('onChange')({ value: 'baz' }); - expect(onChangeProfile).toBeCalledWith('foo', 'baz'); - expect(wrapper.state().loading).toBe(true); - await new Promise(setImmediate); - expect(wrapper.state().loading).toBe(false); -}); - -function randomProfile(key: string) { - return { - activeRuleCount: 17, - activeDeprecatedRuleCount: 0, - key, - name: key, - language: 'xoo', - languageName: 'xoo', - organization: 'org' - }; -} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesApp-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesApp-test.tsx new file mode 100644 index 00000000000..bf5a00d09dc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesApp-test.tsx @@ -0,0 +1,239 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { + associateProject, + dissociateProject, + getProfileProjects, + ProfileProject, + searchQualityProfiles +} from '../../../api/quality-profiles'; +import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization'; +import { mockComponent } from '../../../helpers/testMocks'; +import ProjectQualityProfilesApp from '../ProjectQualityProfilesApp'; + +jest.mock('../../../api/quality-profiles', () => { + const { mockQualityProfile } = jest.requireActual('../../../helpers/testMocks'); + + return { + associateProject: jest.fn().mockResolvedValue({}), + dissociateProject: jest.fn().mockResolvedValue({}), + searchQualityProfiles: jest.fn().mockResolvedValue({ + profiles: [ + mockQualityProfile({ key: 'css', language: 'css' }), + mockQualityProfile({ key: 'css2', language: 'css' }), + mockQualityProfile({ key: 'css_default', language: 'css', isDefault: true }), + mockQualityProfile({ key: 'java', language: 'java' }), + mockQualityProfile({ key: 'java_default', language: 'java', isDefault: true }), + mockQualityProfile({ key: 'js', language: 'js' }), + mockQualityProfile({ key: 'js_default', language: 'js', isDefault: true }), + mockQualityProfile({ key: 'ts_default', language: 'ts', isDefault: true }), + mockQualityProfile({ key: 'html', language: 'html' }), + mockQualityProfile({ key: 'html_default', language: 'html', isDefault: true }) + ] + }), + getProfileProjects: jest.fn(({ key }) => { + const results: ProfileProject[] = []; + if (key === 'js' || key === 'css' || key === 'html_default') { + results.push({ + id: 1, + key: 'foo', + name: 'Foo', + selected: true + }); + } else if (key === 'html') { + results.push({ + id: 2, + key: 'foobar', + name: 'FooBar', + selected: true + }); + } + return Promise.resolve({ results }); + }) + }; +}); + +jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({ + default: jest.fn() +})); + +jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({ + default: jest.fn() +})); + +beforeEach(jest.clearAllMocks); + +it('renders correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('correctly checks permissions', () => { + const wrapper = shallowRender({ + component: mockComponent({ configuration: { showQualityProfiles: false } }) + }); + expect(wrapper.type()).toBeNull(); + expect(handleRequiredAuthorization).toBeCalled(); +}); + +it('correctly fetches and treats profile data', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + expect(searchQualityProfiles).toBeCalled(); + expect(getProfileProjects).toBeCalledTimes(10); + + expect(wrapper.state().projectProfiles).toEqual([ + expect.objectContaining({ + profile: expect.objectContaining({ key: 'css' }), + selected: true + }), + expect.objectContaining({ + profile: expect.objectContaining({ key: 'js' }), + selected: true + }), + expect.objectContaining({ + profile: expect.objectContaining({ key: 'html_default' }), + selected: true + }), + expect.objectContaining({ + profile: expect.objectContaining({ key: 'ts_default' }), + selected: false + }) + ]); +}); + +it('correctly sets a profile', async () => { + const wrapper = shallowRender(); + const instance = wrapper.instance(); + await waitAndUpdate(wrapper); + + // Dissociate a selected profile. + instance.handleSetProfile(undefined, 'css'); + expect(dissociateProject).toHaveBeenLastCalledWith( + expect.objectContaining({ key: 'css' }), + 'foo' + ); + await waitAndUpdate(wrapper); + expect(wrapper.state().projectProfiles).toEqual( + expect.arrayContaining([ + { + profile: expect.objectContaining({ key: 'css_default' }), + // It's not explicitly selected, as we're inheriting the default. + selected: false + } + ]) + ); + + // Associate a new profile. + instance.handleSetProfile('css2', 'css_default'); + expect(associateProject).toHaveBeenLastCalledWith( + expect.objectContaining({ key: 'css2' }), + 'foo' + ); + await waitAndUpdate(wrapper); + expect(wrapper.state().projectProfiles).toEqual( + expect.arrayContaining([ + { + profile: expect.objectContaining({ key: 'css2' }), + // It's explicitly selected. + selected: true + } + ]) + ); + + // Dissociate a default profile that was inherited. + (dissociateProject as jest.Mock).mockClear(); + instance.handleSetProfile(undefined, 'ts_default'); + // It won't call the WS. + expect(dissociateProject).not.toBeCalled(); + + // Associate a default profile that was already inherited. + instance.handleSetProfile('ts_default', 'ts_default'); + expect(associateProject).toHaveBeenLastCalledWith( + expect.objectContaining({ key: 'ts_default' }), + 'foo' + ); + await waitAndUpdate(wrapper); + expect(wrapper.state().projectProfiles).toEqual( + expect.arrayContaining([ + { + profile: expect.objectContaining({ key: 'ts_default' }), + // It's explicitly selected, even though it is the default profile. + selected: true + } + ]) + ); +}); + +it('correctly adds a new language', async () => { + const wrapper = shallowRender(); + const instance = wrapper.instance(); + await waitAndUpdate(wrapper); + + instance.handleAddLanguage('java'); + expect(associateProject).toHaveBeenLastCalledWith( + expect.objectContaining({ key: 'java' }), + 'foo' + ); + await waitAndUpdate(wrapper); + expect(wrapper.state().projectProfiles).toEqual( + expect.arrayContaining([ + { + profile: expect.objectContaining({ key: 'java' }), + // It must be explicitly selected. Adding an unanalyzed language can + // only happen by explicitly choosing a profile. + selected: true + } + ]) + ); +}); + +it('correctly handles WS errors', async () => { + (searchQualityProfiles as jest.Mock).mockRejectedValueOnce(null); + (getProfileProjects as jest.Mock).mockRejectedValueOnce(null); + + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + expect(wrapper.state().allProfiles).toHaveLength(0); + expect(wrapper.state().projectProfiles).toHaveLength(0); + expect(wrapper.state().loading).toBe(false); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesAppRenderer-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesAppRenderer-test.tsx new file mode 100644 index 00000000000..cca8f751070 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/ProjectQualityProfilesAppRenderer-test.tsx @@ -0,0 +1,91 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { mockComponent, mockQualityProfile } from '../../../helpers/testMocks'; +import ProjectQualityProfilesAppRenderer, { + ProjectQualityProfilesAppRendererProps +} from '../ProjectQualityProfilesAppRenderer'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ loading: true })).toMatchSnapshot('loading'); + expect( + shallowRender({ + showProjectProfileInModal: { + profile: mockQualityProfile({ key: 'foo', language: 'js' }), + selected: false + } + }) + ).toMatchSnapshot('open profile'); + expect(shallowRender({ showAddLanguageModal: true })).toMatchSnapshot('add language'); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Table-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Table-test.tsx deleted file mode 100644 index 0da956881bd..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/Table-test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import Table from '../Table'; - -it('renders', () => { - const fooJava = randomProfile('foo-java', 'java'); - const fooJs = randomProfile('foo-js', 'js'); - const allProfiles = [ - fooJava, - randomProfile('bar-java', 'java'), - randomProfile('baz-java', 'java'), - fooJs - ]; - const profiles = [fooJava, fooJs]; - expect( - shallow() - ).toMatchSnapshot(); -}); - -function randomProfile(key: string, language: string) { - return { - activeRuleCount: 17, - activeDeprecatedRuleCount: 0, - key, - name: key, - language, - languageName: language, - organization: 'org' - }; -} diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap deleted file mode 100644 index 6ab35969262..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Header-test.tsx.snap +++ /dev/null @@ -1,30 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` -
-
-

- project_quality_profiles.page -

- - quality_profiles.list.projects.help -
- } - /> - -
- project_quality_profiles.page.description -
-
-`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProfileRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProfileRow-test.tsx.snap deleted file mode 100644 index 5729b3d6716..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProfileRow-test.tsx.snap +++ /dev/null @@ -1,45 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` - - - - -`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProjectQualityProfilesApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProjectQualityProfilesApp-test.tsx.snap new file mode 100644 index 00000000000..ff4aaa0f4f9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProjectQualityProfilesApp-test.tsx.snap @@ -0,0 +1,52 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + +`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProjectQualityProfilesAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProjectQualityProfilesAppRenderer-test.tsx.snap new file mode 100644 index 00000000000..57513807f58 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/ProjectQualityProfilesAppRenderer-test.tsx.snap @@ -0,0 +1,958 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: add language 1`] = ` +
+ + + +
+
+

+ project_quality_profiles.page + +

+ + quality_profiles.list.projects.help +
+ } + /> +
+ +
+

+ project_quality_profile.subtitle +

+
+

+ project_quality_profiles.page.description +

+
- xoo - - -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ language + + project_quality_profile.current + + coding_rules.filters.activation.active_rules + +
+ CSS + + + + project_quality_profile.instance_default + + + + + 10 + + + +
+ HTML + + + Baz + + + + 10 + + + +
+ JS + + + + project_quality_profile.instance_default + + + + + 10 + + + +
+
+

+ project_quality_profile.add_language.title +

+

+ project_quality_profile.add_language.description +

+ +
+ + + + +`; + +exports[`should render correctly: default 1`] = ` +
+ + + +
+
+

+ project_quality_profiles.page + +

+ + quality_profiles.list.projects.help +
+ } + /> +
+
+
+

+ project_quality_profile.subtitle +

+
+

+ project_quality_profiles.page.description +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ language + + project_quality_profile.current + + coding_rules.filters.activation.active_rules + +
+ CSS + + + + project_quality_profile.instance_default + + + + + 10 + + + +
+ HTML + + + Baz + + + + 10 + + + +
+ JS + + + + project_quality_profile.instance_default + + + + + 10 + + + +
+
+

+ project_quality_profile.add_language.title +

+

+ project_quality_profile.add_language.description +

+ +
+
+
+ +`; + +exports[`should render correctly: loading 1`] = ` +
+ + + +
+
+

+ project_quality_profiles.page + +

+ + quality_profiles.list.projects.help +
+ } + /> +
+ +
+

+ project_quality_profile.subtitle +

+
+

+ project_quality_profiles.page.description +

+ +
+

+ project_quality_profile.add_language.title +

+

+ project_quality_profile.add_language.description +

+ +
+
+
+ +`; + +exports[`should render correctly: open profile 1`] = ` +
+ + + +
+
+

+ project_quality_profiles.page + +

+ + quality_profiles.list.projects.help +
+ } + /> +
+ +
+

+ project_quality_profile.subtitle +

+
+

+ project_quality_profiles.page.description +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ language + + project_quality_profile.current + + coding_rules.filters.activation.active_rules + +
+ CSS + + + + project_quality_profile.instance_default + + + + + 10 + + + +
+ HTML + + + Baz + + + + 10 + + + +
+ JS + + + + project_quality_profile.instance_default + + + + + 10 + + + +
+
+

+ project_quality_profile.add_language.title +

+

+ project_quality_profile.add_language.description +

+ +
+ +
+
+ +`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Table-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Table-test.tsx.snap deleted file mode 100644 index a64ebb3f105..00000000000 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Table-test.tsx.snap +++ /dev/null @@ -1,105 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` -
- - - - - - - - - - - - -
- language - - quality_profile - -   -
-
-`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/AddLanguageModal.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/AddLanguageModal.tsx new file mode 100644 index 00000000000..c8334f5f47a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/AddLanguageModal.tsx @@ -0,0 +1,131 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 { difference } from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; +import { ButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons'; +import Select from 'sonar-ui-common/components/controls/Select'; +import SimpleModal from 'sonar-ui-common/components/controls/SimpleModal'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import { Profile } from '../../../api/quality-profiles'; +import { Store } from '../../../store/rootReducer'; + +export interface AddLanguageModalProps { + languages: T.Languages; + onClose: () => void; + onSubmit: (key: string) => Promise; + profilesByLanguage: T.Dict; + unavailableLanguages: string[]; +} + +export function AddLanguageModal(props: AddLanguageModalProps) { + const { languages, profilesByLanguage, unavailableLanguages } = props; + + const [{ language, key }, setSelected] = React.useState<{ language?: string; key?: string }>({ + language: undefined, + key: undefined + }); + + const header = translate('project_quality_profile.add_language_modal.title'); + + const languageOptions = difference( + Object.keys(profilesByLanguage), + unavailableLanguages + ).map(l => ({ value: l, label: languages[l].name })); + + const profileOptions = + language !== undefined + ? profilesByLanguage[language].map(p => ({ value: p.key, label: p.name })) + : []; + + return ( + { + if (language && key) { + props.onSubmit(key); + } + }}> + {({ onCloseClick, onFormSubmit, submitting }) => ( + <> +
+

{header}

+
+ +
+
+
+
+ +
+ setSelected({ language, key: value })} + options={profileOptions} + value={key} + /> +
+
+ +
+ {submitting && } + + {translate('save')} + + + {translate('cancel')} + +
+ + + )} +
+ ); +} + +function mapStateToProps({ languages }: Store) { + return { languages }; +} + +export default connect(mapStateToProps)(AddLanguageModal); diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/SetQualityProfileModal.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/SetQualityProfileModal.tsx new file mode 100644 index 00000000000..ebd8def8477 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/SetQualityProfileModal.tsx @@ -0,0 +1,148 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { ButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons'; +import Radio from 'sonar-ui-common/components/controls/Radio'; +import Select from 'sonar-ui-common/components/controls/Select'; +import SimpleModal from 'sonar-ui-common/components/controls/SimpleModal'; +import { Alert } from 'sonar-ui-common/components/ui/Alert'; +import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; +import { Profile } from '../../../api/quality-profiles'; +import BuiltInQualityProfileBadge from '../../quality-profiles/components/BuiltInQualityProfileBadge'; +import { USE_SYSTEM_DEFAULT } from '../constants'; + +export interface SetQualityProfileModalProps { + availableProfiles: Profile[]; + component: T.Component; + currentProfile: Profile; + onClose: () => void; + onSubmit: (newKey: string | undefined, oldKey: string) => Promise; + usesDefault: boolean; +} + +export default function SetQualityProfileModal(props: SetQualityProfileModalProps) { + const { availableProfiles, component, currentProfile, usesDefault } = props; + const [selected, setSelected] = React.useState( + usesDefault ? USE_SYSTEM_DEFAULT : currentProfile.key + ); + + const defaultProfile = availableProfiles.find(p => p.isDefault); + + if (defaultProfile === undefined) { + // Cannot be undefined + return null; + } + + const header = translateWithParameters( + 'project_quality_profile.change_lang_X_profile', + currentProfile.languageName + ); + const profileOptions = availableProfiles.map(p => ({ value: p.key, label: p.name })); + const hasSelectedSysDefault = selected === USE_SYSTEM_DEFAULT; + const hasChanged = usesDefault ? !hasSelectedSysDefault : selected !== currentProfile.key; + const needsReanalysis = !component.qualityProfiles?.some(p => + hasSelectedSysDefault ? p.key === defaultProfile.key : p.key === selected + ); + + return ( + + props.onSubmit(hasSelectedSysDefault ? undefined : selected, currentProfile.key) + }> + {({ onCloseClick, onFormSubmit, submitting }) => ( + <> +
+

{header}

+
+ +
+
+
+ setSelected(USE_SYSTEM_DEFAULT)} + value={USE_SYSTEM_DEFAULT}> +
+
+ {translate('project_quality_profile.always_use_default')} +
+
+ {translate('current_noun')}: + {defaultProfile.name} + {defaultProfile.isBuiltIn && ( + + )} +
+
+
+
+ +
+ + setSelected(!hasSelectedSysDefault ? selected : currentProfile.key) + } + value={currentProfile.key}> +
+
+ {translate('project_quality_profile.always_use_specific')} +
+
+ +
+
+
+ +
+ +
+
+
+
+
+
+ + save + + + cancel + +
+
, +] +`; + +exports[`should render correctly: inherits system default 1`] = ` +Array [ +
+

+ project_quality_profile.change_lang_X_profile.JavaScript +

+
, +
+
+
+ +
+
+ project_quality_profile.always_use_default +
+
+ + current_noun + : + + name +
+
+
+
+
+ +
+
+ project_quality_profile.always_use_specific +
+
+ +
+
+
+
+ + project_quality_profile.requires_new_analysis + +
+
+ + save + + + cancel + +
+
, +] +`; + +exports[`should render select options correctly: default 1`] = ` + + Profile 1 + +`; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/constants.ts b/server/sonar-web/src/main/js/apps/projectQualityProfiles/constants.ts new file mode 100644 index 00000000000..64d7d66a5b6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/constants.ts @@ -0,0 +1,20 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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. + */ +export const USE_SYSTEM_DEFAULT = '-1'; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts b/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts index d82abd3b146..8ede0cfc4ad 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/routes.ts @@ -21,7 +21,9 @@ import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent' const routes = [ { - indexRoute: { component: lazyLoadComponent(() => import('./App')) } + indexRoute: { + component: lazyLoadComponent(() => import('./ProjectQualityProfilesApp')) + } } ]; diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/types.ts b/server/sonar-web/src/main/js/apps/projectQualityProfiles/types.ts new file mode 100644 index 00000000000..0024277a39d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/types.ts @@ -0,0 +1,25 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 { Profile } from '../../api/quality-profiles'; + +export interface ProjectProfile { + profile: Profile; + selected: boolean; +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index bd49bac6d31..663fc305c5c 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -540,7 +540,7 @@ settings.page=General Settings settings.page.description=Edit global settings for this {instance} instance. system_info.page=System Info project_quality_profiles.page=Quality Profiles -project_quality_profiles.page.description=Choose which profile is associated with this project on a language-by-language basis. (Note that you will only need to select profiles for multiple languages for multi-language projects.) +project_quality_profiles.page.description=Choose which profile is associated with this project on a language-by-language basis. project_quality_gate.page=Quality Gate project_quality_gate.page.description=Choose which quality gate is associated with this project. update_key.page=Update Key @@ -1350,8 +1350,21 @@ update_key.are_you_sure_to_change_key=Are you sure you want to change key of "{0 # PROJECT QUALITY PROFILE PAGE # #------------------------------------------------------------------------------ -project_quality_profile.default_profile=Default +project_quality_profile.instance_default=Instance default project_quality_profile.successfully_updated={0} Quality Profile has been successfully updated. +project_quality_profile.subtitle=Manage project Quality Profiles +project_quality_profile.always_use_default=Always use the instance default Quality Profile +project_quality_profile.current=Current Quality Profile +project_quality_profile.always_use_specific=Always use a specific Quality Profile +project_quality_profile.change_lang_X_profile=Change {0} Quality Profile +project_quality_profile.requires_new_analysis=Changes will be applied after the next analysis. +project_quality_profile.add_language.title=Add a new language +project_quality_profile.add_language.description=Manually configure a specific profile for a new language before the next analysis. +project_quality_profile.add_language.action=Add language +project_quality_profile.add_language_modal.title=Add a language +project_quality_profile.add_language_modal.choose_language=Choose a language +project_quality_profile.add_language_modal.choose_profile=Choose a profile +project_quality_profile.change_profile=Change profile #------------------------------------------------------------------------------ # @@ -1444,7 +1457,7 @@ quality_profiles.activate_more_rules=Activate More Rules quality_profiles.intro1=Quality Profiles are collections of rules to apply during an analysis. quality_profiles.intro2=For each language there is a default profile. All projects not explicitly assigned to some other profile will be analyzed with the default. Ideally, all projects will use the same profile for a language. quality_profiles.list.projects=Projects -quality_profiles.list.projects.help=Projects assigned to a profile will always be analyzed with it for that language, regardless of which profile is the default. Quality Profile administrators may assign projects to a profile. Project administrators may also choose a non-default profile for each language. +quality_profiles.list.projects.help=Projects assigned to a profile will always be analyzed with it for that language, regardless of which profile is the default. Quality Profile administrators may assign projects to a non-default profile, or always make it follow the system default. Project administrators may choose any profile for each language. quality_profiles.list.rules=Rules quality_profiles.list.updated=Updated quality_profiles.list.used=Used @@ -1631,6 +1644,7 @@ coding_rules.filter_similar_rules=Filter Similar Rules coding_rules.filters.activation=Activation coding_rules.filters.activation.active=Active +coding_rules.filters.activation.active_rules=Active Rules coding_rules.filters.activation.inactive=Inactive coding_rules.filters.activation.help=Activation criterion is available when a Quality Profile is selected coding_rules.filters.active_severity=Active Severity