aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/projectsManagement
diff options
context:
space:
mode:
authorViktor Vorona <viktor.vorona@sonarsource.com>2023-07-14 13:53:58 +0200
committersonartech <sonartech@sonarsource.com>2023-07-18 20:03:22 +0000
commit683b7ad836fda87fb6e35388389dc9df07b8aeba (patch)
treed8002bb7d983ee27d696105ebace7b787af72638 /server/sonar-web/src/main/js/apps/projectsManagement
parentf8db20b2213cfcde4c7e8256fdbe5c718a3dac2d (diff)
downloadsonarqube-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')
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx99
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx176
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx52
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> = {},