diff options
author | Viktor Vorona <viktor.vorona@sonarsource.com> | 2023-07-14 13:53:58 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-07-18 20:03:22 +0000 |
commit | 683b7ad836fda87fb6e35388389dc9df07b8aeba (patch) | |
tree | d8002bb7d983ee27d696105ebace7b787af72638 /server/sonar-web/src/main/js/apps/projectsManagement | |
parent | f8db20b2213cfcde4c7e8256fdbe5c718a3dac2d (diff) | |
download | sonarqube-683b7ad836fda87fb6e35388389dc9df07b8aeba.tar.gz sonarqube-683b7ad836fda87fb6e35388389dc9df07b8aeba.zip |
SONAR-19790 Disallow applying permission template on managed project
Diffstat (limited to 'server/sonar-web/src/main/js/apps/projectsManagement')
4 files changed, 169 insertions, 162 deletions
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx index 780a6a16775..73b876c80d7 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx @@ -188,7 +188,9 @@ class ProjectManagementApp extends React.PureComponent<Props, State> { }; onAllSelected = () => { - this.setState(({ projects }) => ({ selection: projects.map((project) => project.key) })); + this.setState(({ projects }) => ({ + selection: projects.filter((p) => !p.managed).map((project) => project.key), + })); }; onAllDeselected = () => { diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx index 026f6cac3cb..9a4c207e465 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx @@ -25,8 +25,9 @@ import Checkbox from '../../components/controls/Checkbox'; import Tooltip from '../../components/controls/Tooltip'; import QualifierIcon from '../../components/icons/QualifierIcon'; import DateFormatter from '../../components/intl/DateFormatter'; -import { translateWithParameters } from '../../helpers/l10n'; +import { translate, translateWithParameters } from '../../helpers/l10n'; import { getComponentOverviewUrl } from '../../helpers/urls'; +import { ComponentQualifier } from '../../types/component'; import { LoggedInUser } from '../../types/users'; import './ProjectRow.css'; import ProjectRowActions from './ProjectRowActions'; @@ -38,59 +39,61 @@ interface Props { selected: boolean; } -export default class ProjectRow extends React.PureComponent<Props> { - handleProjectCheck = (checked: boolean) => { - this.props.onProjectCheck(this.props.project, checked); - }; +export default function ProjectRow(props: Props) { + const { currentUser, project, selected } = props; - render() { - const { project, selected } = this.props; + const handleProjectCheck = (checked: boolean) => { + props.onProjectCheck(project, checked); + }; - return ( - <tr data-project-key={project.key}> - <td className="thin"> - <Checkbox - label={translateWithParameters('projects_management.select_project', project.name)} - checked={selected} - onCheck={this.handleProjectCheck} - /> - </td> + return ( + <tr data-project-key={project.key}> + <td className="thin"> + <Checkbox + label={translateWithParameters('projects_management.select_project', project.name)} + checked={selected} + disabled={project.managed} + onCheck={handleProjectCheck} + /> + </td> - <td className="nowrap hide-overflow project-row-text-cell"> - <Link - className="link-no-underline" - to={getComponentOverviewUrl(project.key, project.qualifier)} - > - <QualifierIcon className="little-spacer-right" qualifier={project.qualifier} /> + <td className="nowrap hide-overflow project-row-text-cell"> + <Link + className="link-no-underline" + to={getComponentOverviewUrl(project.key, project.qualifier)} + > + <QualifierIcon className="little-spacer-right" qualifier={project.qualifier} /> - <Tooltip overlay={project.name} placement="left"> - <span>{project.name}</span> - </Tooltip> - </Link> - </td> + <Tooltip overlay={project.name} placement="left"> + <span>{project.name}</span> + </Tooltip> + </Link> + {project.qualifier === ComponentQualifier.Project && !project.managed && ( + <span className="badge sw-ml-1">{translate('local')}</span> + )} + </td> - <td className="thin nowrap"> - <PrivacyBadgeContainer qualifier={project.qualifier} visibility={project.visibility} /> - </td> + <td className="thin nowrap"> + <PrivacyBadgeContainer qualifier={project.qualifier} visibility={project.visibility} /> + </td> - <td className="nowrap hide-overflow project-row-text-cell"> - <Tooltip overlay={project.key} placement="left"> - <span className="note">{project.key}</span> - </Tooltip> - </td> + <td className="nowrap hide-overflow project-row-text-cell"> + <Tooltip overlay={project.key} placement="left"> + <span className="note">{project.key}</span> + </Tooltip> + </td> - <td className="thin nowrap text-right"> - {project.lastAnalysisDate ? ( - <DateFormatter date={project.lastAnalysisDate} /> - ) : ( - <span className="note">—</span> - )} - </td> + <td className="thin nowrap text-right"> + {project.lastAnalysisDate ? ( + <DateFormatter date={project.lastAnalysisDate} /> + ) : ( + <span className="note">—</span> + )} + </td> - <td className="thin nowrap"> - <ProjectRowActions currentUser={this.props.currentUser} project={project} /> - </td> - </tr> - ); - } + <td className="thin nowrap"> + <ProjectRowActions currentUser={currentUser} project={project} /> + </td> + </tr> + ); } diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx index b41ac29b0b3..fa9064adf54 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx @@ -17,7 +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 * as React from 'react'; +import React, { useState } from 'react'; import { getComponentNavigation } from '../../api/navigation'; import { Project } from '../../api/project-management'; import ActionsDropdown, { ActionsDropdownItem } from '../../components/controls/ActionsDropdown'; @@ -33,129 +33,93 @@ export interface Props { project: Project; } -interface State { - applyTemplateModal: boolean; - hasAccess?: boolean; - loading: boolean; - restoreAccessModal: boolean; -} - -export default class ProjectRowActions extends React.PureComponent<Props, State> { - mounted = false; - state: State = { applyTemplateModal: false, loading: false, restoreAccessModal: false }; - - componentDidMount() { - this.mounted = true; - } +export default function ProjectRowActions({ currentUser, project }: Props) { + const [applyTemplateModal, setApplyTemplateModal] = useState(false); + const [hasAccess, setHasAccess] = useState<boolean | undefined>(undefined); + const [loading, setLoading] = useState(false); + const [restoreAccessModal, setRestoreAccessModal] = useState(false); - componentWillUnmount() { - this.mounted = false; - } - - fetchPermissions = () => { - this.setState({ loading: true }); - getComponentNavigation({ component: this.props.project.key }).then( + const fetchPermissions = () => { + setLoading(true); + getComponentNavigation({ component: project.key }).then( ({ configuration }) => { - if (this.mounted) { - const hasAccess = Boolean( - configuration && configuration.showPermissions && configuration.canBrowseProject - ); - this.setState({ hasAccess, loading: false }); - } + const hasAccess = Boolean( + configuration && configuration.showPermissions && configuration.canBrowseProject + ); + setHasAccess(hasAccess); + setLoading(false); }, () => { - if (this.mounted) { - this.setState({ loading: false }); - } + setLoading(false); } ); }; - handleDropdownOpen = () => { - if (this.state.hasAccess === undefined && !this.state.loading) { - this.fetchPermissions(); - } - }; - - handleApplyTemplateClick = () => { - this.setState({ applyTemplateModal: true }); - }; - - handleApplyTemplateClose = () => { - if (this.mounted) { - this.setState({ applyTemplateModal: false }); + const handleDropdownOpen = () => { + if (hasAccess === undefined && !loading) { + fetchPermissions(); } }; - handleRestoreAccessClick = () => { - this.setState({ restoreAccessModal: true }); - }; - - handleRestoreAccessClose = () => this.setState({ restoreAccessModal: false }); - - handleRestoreAccessDone = () => { - this.setState({ hasAccess: true, restoreAccessModal: false }); + const handleRestoreAccessDone = () => { + setRestoreAccessModal(false); + setHasAccess(true); }; - render() { - const { hasAccess, loading } = this.state; - - return ( - <> - <ActionsDropdown - label={translateWithParameters( - 'projects_management.show_actions_for_x', - this.props.project.name - )} - onOpen={this.handleDropdownOpen} - > - {loading ? ( - <ActionsDropdownItem> - <DeferredSpinner /> - </ActionsDropdownItem> - ) : ( - <> - {hasAccess === true && ( - <ActionsDropdownItem - className="js-edit-permissions" - to={getComponentPermissionsUrl(this.props.project.key)} - > - {translate('edit_permissions')} - </ActionsDropdownItem> - )} - - {hasAccess === false && ( - <ActionsDropdownItem - className="js-restore-access" - onClick={this.handleRestoreAccessClick} - > - {translate('global_permissions.restore_access')} - </ActionsDropdownItem> - )} - </> - )} + return ( + <> + <ActionsDropdown + label={translateWithParameters('projects_management.show_actions_for_x', project.name)} + onOpen={handleDropdownOpen} + > + {loading ? ( + <ActionsDropdownItem> + <DeferredSpinner /> + </ActionsDropdownItem> + ) : ( + <> + {hasAccess === true && ( + <ActionsDropdownItem + className="js-edit-permissions" + to={getComponentPermissionsUrl(project.key)} + > + {translate(project.managed ? 'show_permissions' : 'edit_permissions')} + </ActionsDropdownItem> + )} + + {hasAccess === false && !project.managed && ( + <ActionsDropdownItem + className="js-restore-access" + onClick={() => setRestoreAccessModal(true)} + > + {translate('global_permissions.restore_access')} + </ActionsDropdownItem> + )} + </> + )} + {!project.managed && ( <ActionsDropdownItem className="js-apply-template" - onClick={this.handleApplyTemplateClick} + onClick={() => setApplyTemplateModal(true)} > {translate('projects_role.apply_template')} </ActionsDropdownItem> - </ActionsDropdown> - - {this.state.restoreAccessModal && ( - <RestoreAccessModal - currentUser={this.props.currentUser} - onClose={this.handleRestoreAccessClose} - onRestoreAccess={this.handleRestoreAccessDone} - project={this.props.project} - /> )} - - {this.state.applyTemplateModal && ( - <ApplyTemplate onClose={this.handleApplyTemplateClose} project={this.props.project} /> - )} - </> - ); - } + </ActionsDropdown> + + {restoreAccessModal && ( + <RestoreAccessModal + currentUser={currentUser} + onClose={() => setRestoreAccessModal(false)} + onRestoreAccess={handleRestoreAccessDone} + project={project} + /> + )} + + {applyTemplateModal && ( + <ApplyTemplate onClose={() => setApplyTemplateModal(false)} project={project} /> + )} + </> + ); } diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx index 195c37e350d..f4631ddc7db 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx @@ -17,7 +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 { screen, waitFor, within } from '@testing-library/react'; +import { act, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import selectEvent from 'react-select-event'; import AuthenticationServiceMock from '../../../api/mocks/AuthenticationServiceMock'; @@ -73,7 +73,9 @@ const ui = { firstProjectActions: byRole('button', { name: 'projects_management.show_actions_for_x.Project 1', }), + projectActions: byRole('button', { name: /projects_management.show_actions_for_x/ }), editPermissions: byRole('link', { name: 'edit_permissions' }), + showPermissions: byRole('link', { name: 'show_permissions' }), applyPermissionTemplate: byRole('button', { name: 'projects_role.apply_template' }), restoreAccess: byRole('button', { name: 'global_permissions.restore_access' }), editPermissionsPage: byText('/project_roles?id=project1'), @@ -365,13 +367,13 @@ it('should create project', async () => { expect(ui.successMsg.get(dialog)).toBeInTheDocument(); await user.click(ui.close.get(dialog)); expect(ui.row.getAll()).toHaveLength(6); - expect(ui.row.getAll()[1]).toHaveTextContent('qualifier.TRKa Testvisibility.privatetest—'); + expect(ui.row.getAll()[1]).toHaveTextContent('qualifier.TRKa Testlocalvisibility.privatetest—'); }); it('should edit permissions of single project', async () => { const user = userEvent.setup(); renderProjectManagementApp(); - await user.click(await ui.firstProjectActions.find()); + await act(async () => user.click(await ui.firstProjectActions.find())); expect(ui.restoreAccess.query()).not.toBeInTheDocument(); expect(ui.editPermissions.get()).toBeInTheDocument(); await user.click(ui.editPermissions.get()); @@ -382,7 +384,7 @@ it('should edit permissions of single project', async () => { it('should apply template for single object', async () => { const user = userEvent.setup(); renderProjectManagementApp(); - await user.click(await ui.firstProjectActions.find()); + await act(async () => user.click(await ui.firstProjectActions.find())); await user.click(ui.applyPermissionTemplate.get()); expect(ui.applyTemplateDialog.get()).toBeInTheDocument(); @@ -400,14 +402,14 @@ it('should apply template for single object', async () => { it('should restore access to admin', async () => { const user = userEvent.setup(); renderProjectManagementApp({}, { login: 'gooduser2' }); - await user.click(await ui.firstProjectActions.find()); + await act(async () => user.click(await ui.firstProjectActions.find())); expect(await ui.restoreAccess.find()).toBeInTheDocument(); expect(ui.editPermissions.query()).not.toBeInTheDocument(); await user.click(ui.restoreAccess.get()); expect(ui.restoreAccessDialog.get()).toBeInTheDocument(); - await user.click(ui.restore.get(ui.restoreAccessDialog.get())); + await act(() => user.click(ui.restore.get(ui.restoreAccessDialog.get()))); expect(ui.restoreAccessDialog.query()).not.toBeInTheDocument(); - await user.click(await ui.firstProjectActions.find()); + await act(async () => user.click(await ui.firstProjectActions.find())); expect(ui.restoreAccess.query()).not.toBeInTheDocument(); expect(ui.editPermissions.get()).toBeInTheDocument(); }); @@ -421,6 +423,42 @@ it('should show github warning on changing default visibility to admin', async ( expect(ui.defaultVisibilityWarning.get()).toHaveTextContent('.github'); }); +it('should not allow apply permissions for managed projects', async () => { + const user = userEvent.setup(); + renderProjectManagementApp(); + await waitFor(() => expect(ui.row.getAll()).toHaveLength(5)); + const rows = ui.row.getAll(); + expect(rows[1]).toHaveTextContent('local'); + expect(rows[2]).toHaveTextContent('local'); + expect(rows[3]).toHaveTextContent('local'); + expect(rows[4]).not.toHaveTextContent('local'); + expect(ui.checkbox.get(rows[4])).toHaveAttribute('aria-disabled', 'true'); + expect(ui.checkbox.get(rows[1])).not.toHaveAttribute('aria-disabled'); + await user.click(ui.checkAll.get()); + expect(ui.checkbox.get(rows[4])).not.toBeChecked(); + expect(ui.checkbox.get(rows[1])).toBeChecked(); + await act(() => user.click(ui.projectActions.get(rows[4]))); + expect(ui.applyPermissionTemplate.query()).not.toBeInTheDocument(); + expect(ui.restoreAccess.query()).not.toBeInTheDocument(); + expect(ui.editPermissions.query()).not.toBeInTheDocument(); + expect(ui.showPermissions.get()).toBeInTheDocument(); + await act(() => user.click(ui.projectActions.get(rows[1]))); + expect(ui.applyPermissionTemplate.get()).toBeInTheDocument(); + expect(ui.editPermissions.get()).toBeInTheDocument(); + expect(ui.showPermissions.query()).not.toBeInTheDocument(); +}); + +it('should not show local badge for applications and portfolios', async () => { + renderProjectManagementApp(); + await waitFor(() => expect(screen.getAllByText('local')).toHaveLength(3)); + + await selectEvent.select(ui.qualifierFilter.get(), 'qualifiers.VW'); + expect(screen.queryByText('local')).not.toBeInTheDocument(); + + await selectEvent.select(ui.qualifierFilter.get(), 'qualifiers.APP'); + expect(screen.queryByText('local')).not.toBeInTheDocument(); +}); + function renderProjectManagementApp( overrides: Partial<AppState> = {}, user: Partial<LoggedInUser> = {}, |