From: Wouter Admiraal Date: Tue, 14 Feb 2023 09:59:50 +0000 (+0100) Subject: SONAR-17810 Use Visibility enum instead of hard-coded strings X-Git-Tag: 10.0.0.68432~219 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=edc84dc653f5cc208bf071f325b08334ad1bddc8;p=sonarqube.git SONAR-17810 Use Visibility enum instead of hard-coded strings --- diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index 85aa9e48933..ddecc845d70 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -20,7 +20,12 @@ import { throwGlobalError } from '../helpers/error'; import { getJSON, post, postJSON, RequestData } from '../helpers/request'; import { BranchParameters } from '../types/branch-like'; -import { ComponentQualifier, TreeComponent, TreeComponentWithPath } from '../types/component'; +import { + ComponentQualifier, + TreeComponent, + TreeComponentWithPath, + Visibility, +} from '../types/component'; import { ComponentMeasure, Dict, @@ -31,7 +36,6 @@ import { Paging, SourceLine, SourceViewerFile, - Visibility, } from '../types/types'; export interface BaseSearchProjectsParameters { diff --git a/server/sonar-web/src/main/js/api/mocks/PermissionTemplateServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/PermissionTemplateServiceMock.ts deleted file mode 100644 index d93999a7434..00000000000 --- a/server/sonar-web/src/main/js/api/mocks/PermissionTemplateServiceMock.ts +++ /dev/null @@ -1,191 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { chunk, cloneDeep } from 'lodash'; -import { - mockPermissionTemplate, - mockTemplateGroup, - mockTemplateUser, -} from '../../helpers/testMocks'; -import { PermissionTemplate } from '../../types/types'; -import { BaseSearchProjectsParameters } from '../components'; -import { - addProjectCreatorToTemplate, - bulkApplyTemplate, - getPermissionTemplateGroups, - getPermissionTemplates, - getPermissionTemplateUsers, - grantTemplatePermissionToGroup, - grantTemplatePermissionToUser, - removeProjectCreatorFromTemplate, - revokeTemplatePermissionFromGroup, - revokeTemplatePermissionFromUser, -} from '../permissions'; - -const MAX_PROJECTS_TO_APPLY_PERMISSION_TEMPLATE = 10; - -const defaultPermissionTemplates: PermissionTemplate[] = [ - mockPermissionTemplate(), - mockPermissionTemplate({ - id: 'template2', - name: 'Permission Template 2', - }), -]; - -const templateUsers = [ - mockTemplateUser(), - mockTemplateUser({ - login: 'gooduser1', - name: 'John', - permissions: ['issueadmin', 'securityhotspotadmin', 'user'], - }), - mockTemplateUser({ - login: 'gooduser2', - name: 'Alexa', - permissions: ['issueadmin', 'user'], - }), - mockTemplateUser({ - name: 'Siri', - login: 'gooduser3', - }), - mockTemplateUser({ - login: 'gooduser4', - name: 'Cool', - permissions: ['user'], - }), - mockTemplateUser({ - name: 'White', - login: 'baduser1', - }), - mockTemplateUser({ - name: 'Green', - login: 'baduser2', - }), -]; - -const templateGroups = [ - mockTemplateGroup(), - mockTemplateGroup({ id: 'admins', name: 'admins', permissions: [] }), -]; - -const PAGE_SIZE = 5; -const MIN_QUERY_LENGTH = 3; -const DEFAULT_PAGE = 1; - -jest.mock('../permissions'); - -export default class PermissionTemplateServiceMock { - permissionTemplates: PermissionTemplate[] = []; - isAllowedPermissionChange = true; - - constructor() { - this.permissionTemplates = cloneDeep(defaultPermissionTemplates); - (getPermissionTemplates as jest.Mock).mockImplementation(this.handleGetPermissionTemplates); - (bulkApplyTemplate as jest.Mock).mockImplementation(this.handleBulkApplyTemplate); - (getPermissionTemplateUsers as jest.Mock).mockImplementation( - this.handleGetPermissionTemplateUsers - ); - (getPermissionTemplateGroups as jest.Mock).mockImplementation( - this.handleGetPermissionTemplateGroups - ); - (addProjectCreatorToTemplate as jest.Mock).mockImplementation(this.handlePermissionChange); - (removeProjectCreatorFromTemplate as jest.Mock).mockImplementation(this.handlePermissionChange); - (grantTemplatePermissionToGroup as jest.Mock).mockImplementation(this.handlePermissionChange); - (revokeTemplatePermissionFromGroup as jest.Mock).mockImplementation( - this.handlePermissionChange - ); - (grantTemplatePermissionToUser as jest.Mock).mockImplementation(this.handlePermissionChange); - (revokeTemplatePermissionFromUser as jest.Mock).mockImplementation(this.handlePermissionChange); - } - - handleGetPermissionTemplates = () => { - return this.reply({ permissionTemplates: this.permissionTemplates }); - }; - - handleBulkApplyTemplate = (params: BaseSearchProjectsParameters) => { - if ( - params.projects && - params.projects.split(',').length > MAX_PROJECTS_TO_APPLY_PERMISSION_TEMPLATE - ) { - const response = new Response( - JSON.stringify({ errors: [{ msg: 'bulk apply permission template error message' }] }) - ); - return Promise.reject(response); - } - - return Promise.resolve(); - }; - - handleGetPermissionTemplateUsers = (data: { q?: string | null; p?: number; ps?: number }) => { - const { ps = PAGE_SIZE, p = DEFAULT_PAGE, q } = data; - - const users = - q && q.length >= MIN_QUERY_LENGTH - ? templateUsers.filter((user) => - [user.login, user.name].some((key) => key.toLowerCase().includes(q.toLowerCase())) - ) - : templateUsers; - - const usersChunks = chunk(users, ps); - - return this.reply({ - paging: { pageSize: ps, total: users.length, pageIndex: p }, - users: usersChunks[p - 1] ?? [], - }); - }; - - handleGetPermissionTemplateGroups = (data: { - templateId: string; - q?: string | null; - permission?: string; - p?: number; - ps?: number; - }) => { - const { ps = PAGE_SIZE, p = DEFAULT_PAGE, q } = data; - - const groups = - q && q.length >= MIN_QUERY_LENGTH - ? templateGroups.filter((group) => group.name.toLowerCase().includes(q.toLowerCase())) - : templateGroups; - - const groupsChunks = chunk(groups, ps); - - return this.reply({ - paging: { pageSize: ps, total: groups.length, pageIndex: p }, - groups: groupsChunks[p - 1] ?? [], - }); - }; - - handlePermissionChange = () => { - return this.isAllowedPermissionChange ? Promise.resolve() : Promise.reject(); - }; - - updatePermissionChangeAllowance = (val: boolean) => { - this.isAllowedPermissionChange = val; - }; - - reset = () => { - this.permissionTemplates = cloneDeep(defaultPermissionTemplates); - this.updatePermissionChangeAllowance(true); - }; - - reply(response: T): Promise { - return Promise.resolve(cloneDeep(response)); - } -} diff --git a/server/sonar-web/src/main/js/api/mocks/PermissionsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/PermissionsServiceMock.ts new file mode 100644 index 00000000000..96531f55e5c --- /dev/null +++ b/server/sonar-web/src/main/js/api/mocks/PermissionsServiceMock.ts @@ -0,0 +1,389 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { chunk, cloneDeep, remove, uniq } from 'lodash'; +import { + mockPermission, + mockPermissionGroup, + mockPermissionTemplate, + mockPermissionUser, + mockTemplateGroup, + mockTemplateUser, +} from '../../helpers/mocks/permissions'; +import { ComponentQualifier, Visibility } from '../../types/component'; +import { Permissions } from '../../types/permissions'; +import { Permission, PermissionGroup, PermissionTemplate, PermissionUser } from '../../types/types'; +import { BaseSearchProjectsParameters } from '../components'; +import { + addProjectCreatorToTemplate, + applyTemplateToProject, + bulkApplyTemplate, + changeProjectVisibility, + getPermissionsGroupsForComponent, + getPermissionsUsersForComponent, + getPermissionTemplateGroups, + getPermissionTemplates, + getPermissionTemplateUsers, + grantPermissionToGroup, + grantPermissionToUser, + grantTemplatePermissionToGroup, + grantTemplatePermissionToUser, + removeProjectCreatorFromTemplate, + revokePermissionFromGroup, + revokePermissionFromUser, + revokeTemplatePermissionFromGroup, + revokeTemplatePermissionFromUser, +} from '../permissions'; + +const MAX_PROJECTS_TO_APPLY_PERMISSION_TEMPLATE = 10; + +const defaultPermissionTemplates: PermissionTemplate[] = [ + mockPermissionTemplate(), + mockPermissionTemplate({ + id: 'template2', + name: 'Permission Template 2', + }), +]; + +const templateUsers = [ + mockTemplateUser(), + mockTemplateUser({ + login: 'gooduser1', + name: 'John', + permissions: ['issueadmin', 'securityhotspotadmin', 'user'], + }), + mockTemplateUser({ + login: 'gooduser2', + name: 'Alexa', + permissions: ['issueadmin', 'user'], + }), + mockTemplateUser({ + name: 'Siri', + login: 'gooduser3', + }), + mockTemplateUser({ + login: 'gooduser4', + name: 'Cool', + permissions: ['user'], + }), + mockTemplateUser({ + name: 'White', + login: 'baduser1', + }), + mockTemplateUser({ + name: 'Green', + login: 'baduser2', + }), +]; + +const templateGroups = [ + mockTemplateGroup(), + mockTemplateGroup({ id: 'admins', name: 'admins', permissions: [] }), +]; + +const defaultUsers = [mockPermissionUser()]; + +const defaultGroups = [ + mockPermissionGroup({ name: 'sonar-users', permissions: [Permissions.Browse] }), + mockPermissionGroup({ + name: 'sonar-admins', + permissions: [Permissions.Admin, Permissions.Browse], + }), + mockPermissionGroup({ name: 'sonar-losers', permissions: [] }), +]; + +const PAGE_SIZE = 5; +const MIN_QUERY_LENGTH = 3; +const DEFAULT_PAGE = 1; + +jest.mock('../permissions'); + +export default class PermissionsServiceMock { + permissionTemplates: PermissionTemplate[] = []; + permissions: Permission[]; + defaultTemplates: Array<{ templateId: string; qualifier: string }>; + groups: PermissionGroup[]; + users: PermissionUser[]; + isAllowedPermissionChange = true; + + constructor() { + this.permissionTemplates = cloneDeep(defaultPermissionTemplates); + this.defaultTemplates = [ + ComponentQualifier.Project, + ComponentQualifier.Application, + ComponentQualifier.Portfolio, + ].map((qualifier) => ({ templateId: this.permissionTemplates[0].id, qualifier })); + this.permissions = [ + Permissions.Admin, + Permissions.CodeViewer, + Permissions.IssueAdmin, + Permissions.SecurityHotspotAdmin, + Permissions.Scan, + Permissions.Browse, + ].map((key) => mockPermission({ key, name: key })); + this.groups = cloneDeep(defaultGroups); + this.users = cloneDeep(defaultUsers); + + jest.mocked(getPermissionTemplates).mockImplementation(this.handleGetPermissionTemplates); + jest.mocked(bulkApplyTemplate).mockImplementation(this.handleBulkApplyTemplate); + jest.mocked(applyTemplateToProject).mockImplementation(this.handleApplyTemplateToProject); + jest + .mocked(getPermissionTemplateUsers) + .mockImplementation(this.handleGetPermissionTemplateUsers); + jest + .mocked(getPermissionTemplateGroups) + .mockImplementation(this.handleGetPermissionTemplateGroups); + jest.mocked(addProjectCreatorToTemplate).mockImplementation(this.handlePermissionChange); + jest.mocked(removeProjectCreatorFromTemplate).mockImplementation(this.handlePermissionChange); + jest.mocked(grantTemplatePermissionToGroup).mockImplementation(this.handlePermissionChange); + jest.mocked(revokeTemplatePermissionFromGroup).mockImplementation(this.handlePermissionChange); + jest.mocked(grantTemplatePermissionToUser).mockImplementation(this.handlePermissionChange); + jest.mocked(revokeTemplatePermissionFromUser).mockImplementation(this.handlePermissionChange); + jest.mocked(changeProjectVisibility).mockImplementation(this.handleChangeProjectVisibility); + jest + .mocked(getPermissionsGroupsForComponent) + .mockImplementation(this.handleGetPermissionGroupsForComponent); + jest + .mocked(getPermissionsUsersForComponent) + .mockImplementation(this.handleGetPermissionUsersForComponent); + jest.mocked(grantPermissionToGroup).mockImplementation(this.handleGrantPermissionToGroup); + jest.mocked(revokePermissionFromGroup).mockImplementation(this.handleRevokePermissionFromGroup); + jest.mocked(grantPermissionToUser).mockImplementation(this.handleGrantPermissionToUser); + jest.mocked(revokePermissionFromUser).mockImplementation(this.handleRevokePermissionFromUser); + } + + handleGetPermissionTemplates = () => { + return this.reply({ + permissionTemplates: this.permissionTemplates, + defaultTemplates: this.defaultTemplates, + permissions: this.permissions, + }); + }; + + handleApplyTemplateToProject = (_data: { projectKey: string; templateId: string }) => { + return this.reply(undefined); + }; + + handleBulkApplyTemplate = (params: BaseSearchProjectsParameters) => { + if ( + params.projects && + params.projects.split(',').length > MAX_PROJECTS_TO_APPLY_PERMISSION_TEMPLATE + ) { + const response = new Response( + JSON.stringify({ errors: [{ msg: 'bulk apply permission template error message' }] }) + ); + return Promise.reject(response); + } + + return Promise.resolve(); + }; + + handleGetPermissionTemplateUsers = (data: { q?: string | null; p?: number; ps?: number }) => { + const { ps = PAGE_SIZE, p = DEFAULT_PAGE, q } = data; + + const users = + q && q.length >= MIN_QUERY_LENGTH + ? templateUsers.filter((user) => + [user.login, user.name].some((key) => key.toLowerCase().includes(q.toLowerCase())) + ) + : templateUsers; + + const usersChunks = chunk(users, ps); + + return this.reply({ + paging: { pageSize: ps, total: users.length, pageIndex: p }, + users: usersChunks[p - 1] ?? [], + }); + }; + + handleGetPermissionTemplateGroups = (data: { + templateId: string; + q?: string | null; + permission?: string; + p?: number; + ps?: number; + }) => { + const { ps = PAGE_SIZE, p = DEFAULT_PAGE, q } = data; + + const groups = + q && q.length >= MIN_QUERY_LENGTH + ? templateGroups.filter((group) => group.name.toLowerCase().includes(q.toLowerCase())) + : templateGroups; + + const groupsChunks = chunk(groups, ps); + + return this.reply({ + paging: { pageSize: ps, total: groups.length, pageIndex: p }, + groups: groupsChunks[p - 1] ?? [], + }); + }; + + handleChangeProjectVisibility = (_project: string, _visibility: Visibility) => { + return this.reply(undefined); + }; + + handleGetPermissionGroupsForComponent = (data: { + projectKey: string; + q?: string; + permission?: string; + p?: number; + ps?: number; + }) => { + const { ps = PAGE_SIZE, p = DEFAULT_PAGE, q, permission } = data; + + const groups = + q && q.length >= MIN_QUERY_LENGTH + ? this.groups.filter((group) => group.name.toLowerCase().includes(q.toLowerCase())) + : this.groups; + + const groupsChunked = chunk( + permission ? groups.filter((g) => g.permissions.includes(permission)) : groups, + ps + ); + + return this.reply({ + paging: { pageSize: ps, total: groups.length, pageIndex: p }, + groups: groupsChunked[p - 1] ?? [], + }); + }; + + handleGetPermissionUsersForComponent = (data: { + projectKey: string; + q?: string; + permission?: string; + p?: number; + ps?: number; + }) => { + const { ps = PAGE_SIZE, p = DEFAULT_PAGE, q, permission } = data; + + const users = + q && q.length >= MIN_QUERY_LENGTH + ? this.users.filter((user) => user.name.toLowerCase().includes(q.toLowerCase())) + : this.users; + + const usersChunked = chunk( + permission ? users.filter((u) => u.permissions.includes(permission)) : users, + ps + ); + + return this.reply({ + paging: { pageSize: ps, total: users.length, pageIndex: p }, + users: usersChunked[p - 1] ?? [], + }); + }; + + handleGrantPermissionToGroup = (data: { + projectKey?: string; + groupName: string; + permission: string; + }) => { + if (!this.isAllowedPermissionChange) { + return Promise.reject(); + } + + const { groupName, permission } = data; + const group = this.groups.find((g) => g.name === groupName); + if (group === undefined) { + throw new Error(`Could not find group with name ${groupName}`); + } + group.permissions = uniq([...group.permissions, permission]); + return this.reply(undefined); + }; + + handleRevokePermissionFromGroup = (data: { + projectKey?: string; + groupName: string; + permission: string; + }) => { + if (!this.isAllowedPermissionChange) { + return Promise.reject(); + } + + const { groupName, permission } = data; + const group = this.groups.find((g) => g.name === groupName); + if (group === undefined) { + throw new Error(`Could not find group with name ${groupName}`); + } + group.permissions = remove(group.permissions, permission); + return this.reply(undefined); + }; + + handleGrantPermissionToUser = (data: { + projectKey?: string; + login: string; + permission: string; + }) => { + if (!this.isAllowedPermissionChange) { + return Promise.reject(); + } + + const { login, permission } = data; + const user = this.users.find((u) => u.login === login); + if (user === undefined) { + throw new Error(`Could not find user with login ${login}`); + } + user.permissions = uniq([...user.permissions, permission]); + return this.reply(undefined); + }; + + handleRevokePermissionFromUser = (data: { + projectKey?: string; + login: string; + permission: string; + }) => { + if (!this.isAllowedPermissionChange) { + return Promise.reject(); + } + + const { login, permission } = data; + const user = this.users.find((u) => u.login === login); + if (user === undefined) { + throw new Error(`Could not find user with name ${login}`); + } + user.permissions = remove(user.permissions, permission); + return this.reply(undefined); + }; + + handlePermissionChange = () => { + return this.isAllowedPermissionChange ? Promise.resolve() : Promise.reject(); + }; + + updatePermissionChangeAllowance = (val: boolean) => { + this.isAllowedPermissionChange = val; + }; + + setGroups = (groups: PermissionGroup[]) => { + this.groups = groups; + }; + + setUsers = (users: PermissionUser[]) => { + this.users = users; + }; + + reset = () => { + this.permissionTemplates = cloneDeep(defaultPermissionTemplates); + this.groups = cloneDeep(defaultGroups); + this.users = cloneDeep(defaultUsers); + this.updatePermissionChangeAllowance(true); + }; + + reply(response: T): Promise { + return Promise.resolve(cloneDeep(response)); + } +} diff --git a/server/sonar-web/src/main/js/api/permissions.ts b/server/sonar-web/src/main/js/api/permissions.ts index 4247153957f..8e65bd6ead7 100644 --- a/server/sonar-web/src/main/js/api/permissions.ts +++ b/server/sonar-web/src/main/js/api/permissions.ts @@ -19,13 +19,13 @@ */ import { throwGlobalError } from '../helpers/error'; import { getJSON, post, postJSON, RequestData } from '../helpers/request'; +import { Visibility } from '../types/component'; import { Paging, Permission, PermissionGroup, PermissionTemplate, PermissionUser, - Visibility, } from '../types/types'; import { BaseSearchProjectsParameters } from './components'; @@ -93,7 +93,7 @@ export function setDefaultPermissionTemplate(templateId: string, qualifier: stri return post('/api/permissions/set_default_template', { templateId, qualifier }); } -export function applyTemplateToProject(data: RequestData) { +export function applyTemplateToProject(data: { projectKey: string; templateId: string }) { return post('/api/permissions/apply_template', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx index b8eefc3a041..27c68dc4db1 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx @@ -32,7 +32,7 @@ import { HttpStatus } from '../../../helpers/request'; import { mockLocation, mockRouter } from '../../../helpers/testMocks'; import { waitAndUpdate } from '../../../helpers/testUtils'; import { AlmKeys } from '../../../types/alm-settings'; -import { ComponentQualifier } from '../../../types/component'; +import { ComponentQualifier, Visibility } from '../../../types/component'; import { TaskStatuses, TaskTypes } from '../../../types/tasks'; import { Component } from '../../../types/types'; import handleRequiredAuthorization from '../../utils/handleRequiredAuthorization'; @@ -98,12 +98,18 @@ it('changes component', () => { const wrapper = shallowRender(); wrapper.setState({ branchLikes: [mockMainBranch()], - component: { qualifier: 'TRK', visibility: 'public' } as Component, + component: { + qualifier: ComponentQualifier.Project, + visibility: Visibility.Public, + } as Component, loading: false, }); - wrapper.instance().handleComponentChange({ visibility: 'private' }); - expect(wrapper.state().component).toEqual({ qualifier: 'TRK', visibility: 'private' }); + wrapper.instance().handleComponentChange({ visibility: Visibility.Private }); + expect(wrapper.state().component).toEqual({ + qualifier: ComponentQualifier.Project, + visibility: Visibility.Private, + }); }); it('loads the project binding, if any', async () => { @@ -151,7 +157,7 @@ it('updates branches on change', async () => { wrapper.setState({ branchLikes: [mockMainBranch()], component: mockComponent({ - breadcrumbs: [{ key: 'projectKey', name: 'project', qualifier: 'TRK' }], + breadcrumbs: [{ key: 'projectKey', name: 'project', qualifier: ComponentQualifier.Project }], }), loading: false, }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformation-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformation-test.tsx index 54ee4417b5d..c6150027ce4 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformation-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformation-test.tsx @@ -22,7 +22,7 @@ import * as React from 'react'; import { mockComponent } from '../../../../../../helpers/mocks/component'; import { mockCurrentUser, mockLoggedInUser, mockMetric } from '../../../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../../../helpers/testUtils'; -import { ComponentQualifier } from '../../../../../../types/component'; +import { ComponentQualifier, Visibility } from '../../../../../../types/component'; import ProjectBadges from '../badges/ProjectBadges'; import { ProjectInformation } from '../ProjectInformation'; import { ProjectInformationPages } from '../ProjectInformationPages'; @@ -37,9 +37,9 @@ jest.mock('../../../../../../api/measures', () => { it('should render correctly', async () => { expect(shallowRender()).toMatchSnapshot('default'); expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot('logged in user'); - expect(shallowRender({ component: mockComponent({ visibility: 'private' }) })).toMatchSnapshot( - 'private' - ); + expect( + shallowRender({ component: mockComponent({ visibility: Visibility.Private }) }) + ).toMatchSnapshot('private'); const wrapper = shallowRender(); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot('measures loaded'); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformationRenderer-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformationRenderer-test.tsx index 8e8a84e3eef..796f3e6274d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformationRenderer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformationRenderer-test.tsx @@ -20,6 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { mockComponent } from '../../../../../../helpers/mocks/component'; +import { ComponentQualifier, Visibility } from '../../../../../../types/component'; import { ProjectInformationRenderer, ProjectInformationRendererProps, @@ -43,7 +44,9 @@ it('should render correctly', () => { }); it('should render a private project correctly', () => { - expect(shallowRender({ component: mockComponent({ visibility: 'private' }) })).toMatchSnapshot(); + expect( + shallowRender({ component: mockComponent({ visibility: Visibility.Private }) }) + ).toMatchSnapshot(); }); it('should render an app correctly', () => { @@ -87,7 +90,10 @@ function shallowRender(props: Partial = {}) { hasFeature={jest.fn().mockReturnValue(true)} canConfigureNotifications={true} canUseBadges={true} - component={mockComponent({ qualifier: 'TRK', visibility: 'public' })} + component={mockComponent({ + qualifier: ComponentQualifier.Project, + visibility: Visibility.Public, + })} onComponentChange={jest.fn()} onPageChange={jest.fn()} {...props} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/PermissionTemplatesApp-it.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/PermissionTemplatesApp-it.tsx index ba80b6a6f78..52e7da56855 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/PermissionTemplatesApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/PermissionTemplatesApp-it.tsx @@ -20,12 +20,14 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { byRole } from 'testing-library-selector'; -import PermissionTemplateServiceMock from '../../../../api/mocks/PermissionTemplateServiceMock'; +import PermissionsServiceMock from '../../../../api/mocks/PermissionsServiceMock'; import { mockAppState } from '../../../../helpers/testMocks'; import { renderApp } from '../../../../helpers/testReactTestingUtils'; +import { ComponentQualifier } from '../../../../types/component'; +import { Permissions } from '../../../../types/permissions'; import PermissionTemplatesApp from '../PermissionTemplatesApp'; -const serviceMock = new PermissionTemplateServiceMock(); +const serviceMock = new PermissionsServiceMock(); beforeEach(() => { serviceMock.reset(); @@ -33,37 +35,11 @@ beforeEach(() => { const ui = { templateLink1: byRole('link', { name: 'Permission Template 1' }), - adminUserBrowseCheckboxChecked: byRole('checkbox', { - name: `checked permission 'projects_role.user' for user 'Admin Admin'`, - }), - adminUserBrowseCheckboxUnchecked: byRole('checkbox', { - name: `unchecked permission 'projects_role.user' for user 'Admin Admin'`, - }), - adminUserAdministerCheckboxChecked: byRole('checkbox', { - name: `checked permission 'projects_role.admin' for user 'Admin Admin'`, - }), - adminUserAdministerCheckboxUnchecked: byRole('checkbox', { - name: `unchecked permission 'projects_role.admin' for user 'Admin Admin'`, - }), - - anyoneGroupBrowseCheckboxChecked: byRole('checkbox', { - name: `checked permission 'projects_role.user' for group 'Anyone'`, - }), - anyoneGroupBrowseCheckboxUnchecked: byRole('checkbox', { - name: `unchecked permission 'projects_role.user' for group 'Anyone'`, - }), - - anyoneGroupCodeviewCheckboxChecked: byRole('checkbox', { - name: `checked permission 'projects_role.codeviewer' for group 'Anyone'`, - }), - anyoneGroupCodeviewCheckboxUnchecked: byRole('checkbox', { - name: `unchecked permission 'projects_role.codeviewer' for group 'Anyone'`, - }), - + permissionCheckbox: (target: string, permission: Permissions) => + byRole('checkbox', { + name: `permission.assign_x_to_y.projects_role.${permission}.${target}`, + }), showMoreButton: byRole('button', { name: 'show_more' }), - whiteUserBrowseCheckbox: byRole('checkbox', { - name: `unchecked permission 'projects_role.user' for user 'White'`, - }), }; it('grants/revokes permission from users or groups', async () => { @@ -73,36 +49,33 @@ it('grants/revokes permission from users or groups', async () => { await user.click(await ui.templateLink1.find()); // User - expect(ui.adminUserBrowseCheckboxUnchecked.get()).not.toBeChecked(); - await user.click(ui.adminUserBrowseCheckboxUnchecked.get()); - expect(ui.adminUserBrowseCheckboxChecked.get()).toBeChecked(); + expect(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get()).not.toBeChecked(); + await user.click(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get()); + expect(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get()).toBeChecked(); - expect(ui.adminUserAdministerCheckboxChecked.get()).toBeChecked(); - await user.click(ui.adminUserAdministerCheckboxChecked.get()); - expect(ui.adminUserAdministerCheckboxUnchecked.get()).not.toBeChecked(); + expect(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get()).toBeChecked(); + await user.click(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get()); + expect(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get()).not.toBeChecked(); // Group - expect(ui.anyoneGroupBrowseCheckboxUnchecked.get()).not.toBeChecked(); - await user.click(ui.anyoneGroupBrowseCheckboxUnchecked.get()); - expect(ui.anyoneGroupBrowseCheckboxChecked.get()).toBeChecked(); + expect(ui.permissionCheckbox('Anyone', Permissions.Browse).get()).not.toBeChecked(); + await user.click(ui.permissionCheckbox('Anyone', Permissions.Browse).get()); + expect(ui.permissionCheckbox('Anyone', Permissions.Browse).get()).toBeChecked(); - expect(ui.anyoneGroupCodeviewCheckboxChecked.get()).toBeChecked(); - await user.click(ui.anyoneGroupCodeviewCheckboxChecked.get()); - expect(ui.anyoneGroupCodeviewCheckboxUnchecked.get()).not.toBeChecked(); + expect(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get()).toBeChecked(); + await user.click(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get()); + expect(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get()).not.toBeChecked(); // Handles error on permission change serviceMock.updatePermissionChangeAllowance(false); - await user.click(ui.adminUserBrowseCheckboxChecked.get()); - expect(ui.adminUserBrowseCheckboxChecked.get()).toBeChecked(); - - await user.click(ui.anyoneGroupCodeviewCheckboxUnchecked.get()); - expect(ui.anyoneGroupCodeviewCheckboxUnchecked.get()).not.toBeChecked(); + await user.click(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get()); + expect(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get()).toBeChecked(); - await user.click(ui.adminUserBrowseCheckboxChecked.get()); - expect(ui.adminUserBrowseCheckboxChecked.get()).toBeChecked(); + await user.click(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get()); + expect(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get()).not.toBeChecked(); - await user.click(ui.adminUserAdministerCheckboxUnchecked.get()); - expect(ui.adminUserAdministerCheckboxUnchecked.get()).not.toBeChecked(); + await user.click(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get()); + expect(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get()).not.toBeChecked(); }); it('loads more items on Show More', async () => { @@ -111,13 +84,13 @@ it('loads more items on Show More', async () => { await user.click(await ui.templateLink1.find()); - expect(ui.whiteUserBrowseCheckbox.query()).not.toBeInTheDocument(); + expect(ui.permissionCheckbox('White', Permissions.Browse).query()).not.toBeInTheDocument(); await user.click(ui.showMoreButton.get()); - expect(ui.whiteUserBrowseCheckbox.get()).toBeInTheDocument(); + expect(ui.permissionCheckbox('White', Permissions.Browse).get()).toBeInTheDocument(); }); function renderPermissionTemplatesApp() { renderApp('admin/permission_templates', , { - appState: mockAppState({ qualifiers: ['TRK'] }), + appState: mockAppState({ qualifiers: [ComponentQualifier.Project] }), }); } diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx deleted file mode 100644 index 11e31ff7461..00000000000 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx +++ /dev/null @@ -1,408 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { without } from 'lodash'; -import * as React from 'react'; -import { Helmet } from 'react-helmet-async'; -import * as api from '../../../../api/permissions'; -import withComponentContext from '../../../../app/components/componentContext/withComponentContext'; -import VisibilitySelector from '../../../../components/common/VisibilitySelector'; -import { translate } from '../../../../helpers/l10n'; -import { Component, Paging, PermissionGroup, PermissionUser } from '../../../../types/types'; -import AllHoldersList from '../../shared/components/AllHoldersList'; -import { FilterOption } from '../../shared/components/SearchForm'; -import '../../styles.css'; -import { convertToPermissionDefinitions, PERMISSIONS_ORDER_BY_QUALIFIER } from '../../utils'; -import PageHeader from './PageHeader'; -import PublicProjectDisclaimer from './PublicProjectDisclaimer'; - -interface Props { - component: Component; - onComponentChange: (changes: Partial) => void; -} - -interface State { - disclaimer: boolean; - filter: FilterOption; - groups: PermissionGroup[]; - groupsPaging?: Paging; - loading: boolean; - query: string; - selectedPermission?: string; - users: PermissionUser[]; - usersPaging?: Paging; -} - -export class App extends React.PureComponent { - mounted = false; - - constructor(props: Props) { - super(props); - this.state = { - disclaimer: false, - filter: 'all', - groups: [], - loading: true, - query: '', - users: [], - }; - } - - componentDidMount() { - this.mounted = true; - this.loadHolders(); - } - - componentWillUnmount() { - this.mounted = false; - } - - stopLoading = () => { - if (this.mounted) { - this.setState({ loading: false }); - } - }; - - loadUsersAndGroups = (userPage?: number, groupsPage?: number) => { - const { component } = this.props; - const { filter, query, selectedPermission } = this.state; - - const getUsers: Promise<{ paging?: Paging; users: PermissionUser[] }> = - filter !== 'groups' - ? api.getPermissionsUsersForComponent({ - projectKey: component.key, - q: query || undefined, - permission: selectedPermission, - p: userPage, - }) - : Promise.resolve({ paging: undefined, users: [] }); - - const getGroups: Promise<{ paging?: Paging; groups: PermissionGroup[] }> = - filter !== 'users' - ? api.getPermissionsGroupsForComponent({ - projectKey: component.key, - q: query || undefined, - permission: selectedPermission, - p: groupsPage, - }) - : Promise.resolve({ paging: undefined, groups: [] }); - - return Promise.all([getUsers, getGroups]); - }; - - loadHolders = () => { - this.setState({ loading: true }); - return this.loadUsersAndGroups().then(([usersResponse, groupsResponse]) => { - if (this.mounted) { - this.setState({ - groups: groupsResponse.groups, - groupsPaging: groupsResponse.paging, - loading: false, - users: usersResponse.users, - usersPaging: usersResponse.paging, - }); - } - }, this.stopLoading); - }; - - onLoadMore = () => { - const { usersPaging, groupsPaging } = this.state; - this.setState({ loading: true }); - return this.loadUsersAndGroups( - usersPaging ? usersPaging.pageIndex + 1 : 1, - groupsPaging ? groupsPaging.pageIndex + 1 : 1 - ).then(([usersResponse, groupsResponse]) => { - if (this.mounted) { - this.setState(({ groups, users }) => ({ - groups: [...groups, ...groupsResponse.groups], - groupsPaging: groupsResponse.paging, - loading: false, - users: [...users, ...usersResponse.users], - usersPaging: usersResponse.paging, - })); - } - }, this.stopLoading); - }; - - handleFilterChange = (filter: FilterOption) => { - if (this.mounted) { - this.setState({ filter }, this.loadHolders); - } - }; - - handleQueryChange = (query: string) => { - if (this.mounted) { - this.setState({ query }, this.loadHolders); - } - }; - - handlePermissionSelect = (selectedPermission?: string) => { - if (this.mounted) { - this.setState( - (state: State) => ({ - selectedPermission: - state.selectedPermission === selectedPermission ? undefined : selectedPermission, - }), - this.loadHolders - ); - } - }; - - addPermissionToGroup = (group: string, permission: string) => { - return this.state.groups.map((candidate) => - candidate.name === group - ? { ...candidate, permissions: [...candidate.permissions, permission] } - : candidate - ); - }; - - addPermissionToUser = (user: string, permission: string) => { - return this.state.users.map((candidate) => - candidate.login === user - ? { ...candidate, permissions: [...candidate.permissions, permission] } - : candidate - ); - }; - - removePermissionFromGroup = (group: string, permission: string) => { - return this.state.groups.map((candidate) => - candidate.name === group - ? { ...candidate, permissions: without(candidate.permissions, permission) } - : candidate - ); - }; - - removePermissionFromUser = (user: string, permission: string) => { - return this.state.users.map((candidate) => - candidate.login === user - ? { ...candidate, permissions: without(candidate.permissions, permission) } - : candidate - ); - }; - - grantPermissionToGroup = (group: string, permission: string) => { - if (this.mounted) { - this.setState({ - loading: true, - groups: this.addPermissionToGroup(group, permission), - }); - return api - .grantPermissionToGroup({ - projectKey: this.props.component.key, - groupName: group, - permission, - }) - .then(this.stopLoading, () => { - if (this.mounted) { - this.setState({ - loading: false, - groups: this.removePermissionFromGroup(group, permission), - }); - } - }); - } - return Promise.resolve(); - }; - - grantPermissionToUser = (user: string, permission: string) => { - if (this.mounted) { - this.setState({ - loading: true, - users: this.addPermissionToUser(user, permission), - }); - return api - .grantPermissionToUser({ - projectKey: this.props.component.key, - login: user, - permission, - }) - .then(this.stopLoading, () => { - if (this.mounted) { - this.setState({ - loading: false, - users: this.removePermissionFromUser(user, permission), - }); - } - }); - } - return Promise.resolve(); - }; - - revokePermissionFromGroup = (group: string, permission: string) => { - if (this.mounted) { - this.setState({ - loading: true, - groups: this.removePermissionFromGroup(group, permission), - }); - return api - .revokePermissionFromGroup({ - projectKey: this.props.component.key, - groupName: group, - permission, - }) - .then(this.stopLoading, () => { - if (this.mounted) { - this.setState({ - loading: false, - groups: this.addPermissionToGroup(group, permission), - }); - } - }); - } - return Promise.resolve(); - }; - - revokePermissionFromUser = (user: string, permission: string) => { - if (this.mounted) { - this.setState({ - loading: true, - users: this.removePermissionFromUser(user, permission), - }); - return api - .revokePermissionFromUser({ - projectKey: this.props.component.key, - login: user, - permission, - }) - .then(this.stopLoading, () => { - if (this.mounted) { - this.setState({ - loading: false, - users: this.addPermissionToUser(user, permission), - }); - } - }); - } - return Promise.resolve(); - }; - - handleVisibilityChange = (visibility: string) => { - if (visibility === 'public') { - this.openDisclaimer(); - } else { - this.turnProjectToPrivate(); - } - }; - - turnProjectToPublic = () => { - this.props.onComponentChange({ visibility: 'public' }); - api.changeProjectVisibility(this.props.component.key, 'public').then( - () => { - this.loadHolders(); - }, - () => { - this.props.onComponentChange({ - visibility: 'private', - }); - } - ); - }; - - turnProjectToPrivate = () => { - this.props.onComponentChange({ visibility: 'private' }); - api.changeProjectVisibility(this.props.component.key, 'private').then( - () => { - this.loadHolders(); - }, - () => { - this.props.onComponentChange({ - visibility: 'public', - }); - } - ); - }; - - openDisclaimer = () => { - if (this.mounted) { - this.setState({ disclaimer: true }); - } - }; - - closeDisclaimer = () => { - if (this.mounted) { - this.setState({ disclaimer: false }); - } - }; - - render() { - const { component } = this.props; - const { - filter, - groups, - disclaimer, - loading, - selectedPermission, - query, - users, - usersPaging, - groupsPaging, - } = this.state; - const canTurnToPrivate = - component.configuration && component.configuration.canUpdateProjectVisibilityToPrivate; - - let order = PERMISSIONS_ORDER_BY_QUALIFIER[component.qualifier]; - if (component.visibility === 'public') { - order = without(order, 'user', 'codeviewer'); - } - const permissions = convertToPermissionDefinitions(order, 'projects_role'); - - return ( -
- - - -
- - {disclaimer && ( - - )} -
- -
- ); - } -} - -export default withComponentContext(App); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx index 32f435b5bcb..bc1d118871c 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx @@ -83,8 +83,6 @@ export default class ApplyTemplate extends React.PureComponent { this.setState({ done: true }); } }); - } else { - return Promise.reject(undefined); } }; @@ -94,7 +92,7 @@ export default class ApplyTemplate extends React.PureComponent { render() { const header = translateWithParameters( - 'projects_role.apply_template_to_xxx', + 'projects_role.apply_template_to_x', this.props.project.name ); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx index 0816f07df61..a1d7c39f50c 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx @@ -19,8 +19,9 @@ */ import * as React from 'react'; import { Button } from '../../../../components/controls/buttons'; +import DeferredSpinner from '../../../../components/ui/DeferredSpinner'; import { translate } from '../../../../helpers/l10n'; -import { isApplication, isPortfolioLike } from '../../../../types/component'; +import { ComponentQualifier, isApplication, isPortfolioLike } from '../../../../types/component'; import { Component } from '../../../../types/types'; import ApplyTemplate from './ApplyTemplate'; @@ -70,7 +71,7 @@ export default class PageHeader extends React.PureComponent { } const visibilityDescription = - component.qualifier === 'TRK' && component.visibility + component.qualifier === ComponentQualifier.Project && component.visibility ? translate('visibility', component.visibility, 'description', component.qualifier) : undefined; @@ -78,7 +79,7 @@ export default class PageHeader extends React.PureComponent {

{translate('permissions.page')}

- {this.props.loading && } + {canApplyPermissionTemplate && (
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx new file mode 100644 index 00000000000..acefdf94fb6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx @@ -0,0 +1,387 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { without } from 'lodash'; +import * as React from 'react'; +import { Helmet } from 'react-helmet-async'; +import * as api from '../../../../api/permissions'; +import withComponentContext from '../../../../app/components/componentContext/withComponentContext'; +import VisibilitySelector from '../../../../components/common/VisibilitySelector'; +import { translate } from '../../../../helpers/l10n'; +import { Visibility } from '../../../../types/component'; +import { Permissions } from '../../../../types/permissions'; +import { Component, Paging, PermissionGroup, PermissionUser } from '../../../../types/types'; +import AllHoldersList from '../../shared/components/AllHoldersList'; +import { FilterOption } from '../../shared/components/SearchForm'; +import '../../styles.css'; +import { convertToPermissionDefinitions, PERMISSIONS_ORDER_BY_QUALIFIER } from '../../utils'; +import PageHeader from './PageHeader'; +import PublicProjectDisclaimer from './PublicProjectDisclaimer'; + +interface Props { + component: Component; + onComponentChange: (changes: Partial) => void; +} + +interface State { + disclaimer: boolean; + filter: FilterOption; + groups: PermissionGroup[]; + groupsPaging?: Paging; + loading: boolean; + query: string; + selectedPermission?: string; + users: PermissionUser[]; + usersPaging?: Paging; +} + +export class PermissionsProjectApp extends React.PureComponent { + mounted = false; + + constructor(props: Props) { + super(props); + this.state = { + disclaimer: false, + filter: 'all', + groups: [], + loading: true, + query: '', + users: [], + }; + } + + componentDidMount() { + this.mounted = true; + this.loadHolders(); + } + + componentWillUnmount() { + this.mounted = false; + } + + loadUsersAndGroups = (userPage?: number, groupsPage?: number) => { + const { component } = this.props; + const { filter, query, selectedPermission } = this.state; + + const getUsers: Promise<{ paging?: Paging; users: PermissionUser[] }> = + filter !== 'groups' + ? api.getPermissionsUsersForComponent({ + projectKey: component.key, + q: query || undefined, + permission: selectedPermission, + p: userPage, + }) + : Promise.resolve({ paging: undefined, users: [] }); + + const getGroups: Promise<{ paging?: Paging; groups: PermissionGroup[] }> = + filter !== 'users' + ? api.getPermissionsGroupsForComponent({ + projectKey: component.key, + q: query || undefined, + permission: selectedPermission, + p: groupsPage, + }) + : Promise.resolve({ paging: undefined, groups: [] }); + + return Promise.all([getUsers, getGroups]); + }; + + loadHolders = () => { + this.setState({ loading: true }); + return this.loadUsersAndGroups().then(([usersResponse, groupsResponse]) => { + if (this.mounted) { + this.setState({ + groups: groupsResponse.groups, + groupsPaging: groupsResponse.paging, + loading: false, + users: usersResponse.users, + usersPaging: usersResponse.paging, + }); + } + }, this.stopLoading); + }; + + onLoadMore = () => { + const { usersPaging, groupsPaging } = this.state; + this.setState({ loading: true }); + return this.loadUsersAndGroups( + usersPaging ? usersPaging.pageIndex + 1 : 1, + groupsPaging ? groupsPaging.pageIndex + 1 : 1 + ).then(([usersResponse, groupsResponse]) => { + if (this.mounted) { + this.setState(({ groups, users }) => ({ + groups: [...groups, ...groupsResponse.groups], + groupsPaging: groupsResponse.paging, + loading: false, + users: [...users, ...usersResponse.users], + usersPaging: usersResponse.paging, + })); + } + }, this.stopLoading); + }; + + handleFilterChange = (filter: FilterOption) => { + if (this.mounted) { + this.setState({ filter }, this.loadHolders); + } + }; + + handleQueryChange = (query: string) => { + if (this.mounted) { + this.setState({ query }, this.loadHolders); + } + }; + + handlePermissionSelect = (selectedPermission?: string) => { + if (this.mounted) { + this.setState( + (state: State) => ({ + selectedPermission: + state.selectedPermission === selectedPermission ? undefined : selectedPermission, + }), + this.loadHolders + ); + } + }; + + addPermissionToGroup = (group: string, permission: string) => { + return this.state.groups.map((candidate) => + candidate.name === group + ? { ...candidate, permissions: [...candidate.permissions, permission] } + : candidate + ); + }; + + addPermissionToUser = (user: string, permission: string) => { + return this.state.users.map((candidate) => + candidate.login === user + ? { ...candidate, permissions: [...candidate.permissions, permission] } + : candidate + ); + }; + + removePermissionFromGroup = (group: string, permission: string) => { + return this.state.groups.map((candidate) => + candidate.name === group + ? { ...candidate, permissions: without(candidate.permissions, permission) } + : candidate + ); + }; + + removePermissionFromUser = (user: string, permission: string) => { + return this.state.users.map((candidate) => + candidate.login === user + ? { ...candidate, permissions: without(candidate.permissions, permission) } + : candidate + ); + }; + + grantPermissionToGroup = (group: string, permission: string) => { + if (this.mounted) { + this.setState({ loading: true }); + return api + .grantPermissionToGroup({ + projectKey: this.props.component.key, + groupName: group, + permission, + }) + .then(() => { + if (this.mounted) { + this.setState({ + loading: false, + groups: this.addPermissionToGroup(group, permission), + }); + } + }, this.stopLoading); + } + return Promise.resolve(); + }; + + grantPermissionToUser = (user: string, permission: string) => { + if (this.mounted) { + this.setState({ loading: true }); + return api + .grantPermissionToUser({ + projectKey: this.props.component.key, + login: user, + permission, + }) + .then(() => { + if (this.mounted) { + this.setState({ + loading: false, + users: this.addPermissionToUser(user, permission), + }); + } + }, this.stopLoading); + } + return Promise.resolve(); + }; + + revokePermissionFromGroup = (group: string, permission: string) => { + if (this.mounted) { + this.setState({ loading: true }); + return api + .revokePermissionFromGroup({ + projectKey: this.props.component.key, + groupName: group, + permission, + }) + .then(() => { + if (this.mounted) { + this.setState({ + loading: false, + groups: this.removePermissionFromGroup(group, permission), + }); + } + }, this.stopLoading); + } + return Promise.resolve(); + }; + + revokePermissionFromUser = (user: string, permission: string) => { + if (this.mounted) { + this.setState({ loading: true }); + return api + .revokePermissionFromUser({ + projectKey: this.props.component.key, + login: user, + permission, + }) + .then(() => { + if (this.mounted) { + this.setState({ + loading: false, + users: this.removePermissionFromUser(user, permission), + }); + } + }, this.stopLoading); + } + return Promise.resolve(); + }; + + handleVisibilityChange = (visibility: string) => { + if (visibility === Visibility.Public) { + this.openDisclaimer(); + } else { + this.turnProjectToPrivate(); + } + }; + + turnProjectToPublic = () => { + this.setState({ loading: true }); + return api.changeProjectVisibility(this.props.component.key, Visibility.Public).then(() => { + this.props.onComponentChange({ visibility: Visibility.Public }); + this.loadHolders(); + }); + }; + + turnProjectToPrivate = () => { + this.setState({ loading: true }); + return api.changeProjectVisibility(this.props.component.key, Visibility.Private).then(() => { + this.props.onComponentChange({ visibility: Visibility.Private }); + this.loadHolders(); + }); + }; + + openDisclaimer = () => { + if (this.mounted) { + this.setState({ disclaimer: true }); + } + }; + + closeDisclaimer = () => { + if (this.mounted) { + this.setState({ disclaimer: false }); + } + }; + + stopLoading = () => { + if (this.mounted) { + this.setState({ loading: false }); + } + }; + + render() { + const { component } = this.props; + const { + filter, + groups, + disclaimer, + loading, + selectedPermission, + query, + users, + usersPaging, + groupsPaging, + } = this.state; + const canTurnToPrivate = + component.configuration && component.configuration.canUpdateProjectVisibilityToPrivate; + + let order = PERMISSIONS_ORDER_BY_QUALIFIER[component.qualifier]; + if (component.visibility === Visibility.Public) { + order = without(order, Permissions.Browse, Permissions.CodeViewer); + } + const permissions = convertToPermissionDefinitions(order, 'projects_role'); + + return ( +
+ + + +
+ + {disclaimer && ( + + )} +
+ +
+ ); + } +} + +export default withComponentContext(PermissionsProjectApp); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx deleted file mode 100644 index dda3c36ef49..00000000000 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx +++ /dev/null @@ -1,157 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { - grantPermissionToGroup, - grantPermissionToUser, - revokePermissionFromGroup, - revokePermissionFromUser, -} from '../../../../../api/permissions'; -import { mockComponent } from '../../../../../helpers/mocks/component'; -import { waitAndUpdate } from '../../../../../helpers/testUtils'; -import { App } from '../App'; - -jest.mock('../../../../../api/permissions', () => ({ - getPermissionsGroupsForComponent: jest.fn().mockResolvedValue({ - paging: { pageIndex: 1, pageSize: 100, total: 2 }, - groups: [ - { - id: '1', - name: 'SonarSource', - description: 'SonarSource team', - permissions: ['admin', 'codeviewer', 'issueadmin'], - }, - { name: 'Anyone', permissions: [] }, - ], - }), - getPermissionsUsersForComponent: jest.fn().mockResolvedValue({ - paging: { pageIndex: 1, pageSize: 100, total: 3 }, - users: [ - { - avatar: 'admin-avatar', - email: 'admin@gmail.com', - login: 'admin', - name: 'Admin Admin', - permissions: ['admin'], - }, - { - avatar: 'user-avatar-1', - email: 'user1@gmail.com', - login: 'user1', - name: 'User Number 1', - permissions: [], - }, - { - avatar: 'user-avatar-2', - email: 'user2@gmail.com', - login: 'user2', - name: 'User Number 2', - permissions: [], - }, - ], - }), - grantPermissionToGroup: jest.fn().mockResolvedValue({}), - grantPermissionToUser: jest.fn().mockResolvedValue({}), - revokePermissionFromGroup: jest.fn().mockResolvedValue({}), - revokePermissionFromUser: jest.fn().mockResolvedValue({}), -})); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -it('should render correctly', async () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); -}); - -describe('should manage state correctly', () => { - it('should handle permission select', async () => { - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - const instance = wrapper.instance(); - instance.handlePermissionSelect('foo'); - expect(wrapper.state('selectedPermission')).toBe('foo'); - instance.handlePermissionSelect('foo'); - expect(wrapper.state('selectedPermission')).toBeUndefined(); - }); - - it('should add and remove permission to a group', async () => { - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - const instance = wrapper.instance(); - const apiPayload = { - projectKey: 'my-project', - groupName: 'SonarSource', - permission: 'foo', - }; - - instance.grantPermissionToGroup('SonarSource', 'foo'); - const groupState = wrapper.state('groups'); - expect(groupState[0].permissions).toHaveLength(4); - expect(groupState[0].permissions).toContain('foo'); - await waitAndUpdate(wrapper); - expect(grantPermissionToGroup).toHaveBeenCalledWith(apiPayload); - expect(wrapper.state('groups')).toBe(groupState); - - (grantPermissionToGroup as jest.Mock).mockRejectedValueOnce({}); - instance.grantPermissionToGroup('SonarSource', 'bar'); - expect(wrapper.state('groups')[0].permissions).toHaveLength(5); - expect(wrapper.state('groups')[0].permissions).toContain('bar'); - await waitAndUpdate(wrapper); - expect(wrapper.state('groups')[0].permissions).toHaveLength(4); - expect(wrapper.state('groups')[0].permissions).not.toContain('bar'); - - instance.revokePermissionFromGroup('SonarSource', 'foo'); - expect(wrapper.state('groups')[0].permissions).toHaveLength(3); - expect(wrapper.state('groups')[0].permissions).not.toContain('foo'); - await waitAndUpdate(wrapper); - expect(revokePermissionFromGroup).toHaveBeenCalledWith(apiPayload); - }); - - it('should add and remove permission to a user', async () => { - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - const instance = wrapper.instance(); - const apiPayload = { - projectKey: 'my-project', - login: 'user1', - permission: 'foo', - }; - - instance.grantPermissionToUser('user1', 'foo'); - expect(wrapper.state('users')[1].permissions).toHaveLength(1); - expect(wrapper.state('users')[1].permissions).toContain('foo'); - await waitAndUpdate(wrapper); - expect(grantPermissionToUser).toHaveBeenCalledWith(apiPayload); - - instance.revokePermissionFromUser('user1', 'foo'); - expect(wrapper.state('users')[1].permissions).toHaveLength(0); - await waitAndUpdate(wrapper); - expect(revokePermissionFromUser).toHaveBeenCalledWith(apiPayload); - }); -}); - -function shallowRender(props: Partial = {}) { - return shallow(); -} diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/ApplyTemplate-test.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/ApplyTemplate-test.tsx deleted file mode 100644 index a6b6c3ab333..00000000000 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/ApplyTemplate-test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { waitAndUpdate } from '../../../../../helpers/testUtils'; -import ApplyTemplate from '../ApplyTemplate'; - -jest.mock('../../../../../api/permissions', () => ({ - getPermissionTemplates: jest.fn().mockResolvedValue({ - permissionTemplates: [ - { - id: 'tmp1', - name: 'SonarSource projects', - createdAt: '2015-11-27T15:20:32+0100', - permissions: [ - { key: 'admin', usersCount: 0, groupsCount: 3 }, - { key: 'codeviewer', usersCount: 0, groupsCount: 6 }, - ], - }, - ], - defaultTemplates: [{ templateId: 'tmp1', qualifier: 'TRK' }], - permissions: [ - { key: 'admin', name: 'Administer', description: 'Administer access' }, - { key: 'codeviewer', name: 'See Source Code', description: 'View code' }, - ], - }), -})); - -it('render correctly', async () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - await waitAndUpdate(wrapper); - expect(wrapper.dive()).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx new file mode 100644 index 00000000000..0e8d81a0d0f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx @@ -0,0 +1,287 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { act, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; +import * as React from 'react'; +import selectEvent from 'react-select-event'; +import { byLabelText, byRole, byText } from 'testing-library-selector'; +import PermissionsServiceMock from '../../../../../api/mocks/PermissionsServiceMock'; +import { mockComponent } from '../../../../../helpers/mocks/component'; +import { renderApp } from '../../../../../helpers/testReactTestingUtils'; +import { ComponentQualifier, Visibility } from '../../../../../types/component'; +import { Permissions } from '../../../../../types/permissions'; +import { Component } from '../../../../../types/types'; +import { PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE, PERMISSIONS_ORDER_FOR_VIEW } from '../../../utils'; +import { PermissionsProjectApp } from '../PermissionsProjectApp'; + +let serviceMock: PermissionsServiceMock; +beforeAll(() => { + serviceMock = new PermissionsServiceMock(); +}); + +afterEach(() => { + serviceMock.reset(); +}); + +describe('rendering', () => { + it.each([ + [ComponentQualifier.Project, 'roles.page.description2', PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE], + [ComponentQualifier.Portfolio, 'roles.page.description_portfolio', PERMISSIONS_ORDER_FOR_VIEW], + [ + ComponentQualifier.Application, + 'roles.page.description_application', + PERMISSIONS_ORDER_FOR_VIEW, + ], + ])('should render correctly for %s', async (qualifier, description, permissions) => { + const user = userEvent.setup(); + const ui = getPageObject(user); + renderPermissionsProjectApp({ qualifier, visibility: Visibility.Private }); + await ui.appLoaded(); + + expect(screen.getByText(description)).toBeInTheDocument(); + permissions.forEach((permission) => { + expect(ui.permissionCheckbox('johndoe', permission).get()).toBeInTheDocument(); + }); + }); +}); + +describe('filtering', () => { + it('should allow to filter permission holders', async () => { + const user = userEvent.setup(); + const ui = getPageObject(user); + renderPermissionsProjectApp(); + await ui.appLoaded(); + + expect(screen.getByText('sonar-users')).toBeInTheDocument(); + expect(screen.getByText('johndoe')).toBeInTheDocument(); + + await ui.showOnlyUsers(); + expect(screen.queryByText('sonar-users')).not.toBeInTheDocument(); + expect(screen.getByText('johndoe')).toBeInTheDocument(); + + await ui.showOnlyGroups(); + expect(screen.getByText('sonar-users')).toBeInTheDocument(); + expect(screen.queryByText('johndoe')).not.toBeInTheDocument(); + + await ui.showAll(); + expect(screen.getByText('sonar-users')).toBeInTheDocument(); + expect(screen.getByText('johndoe')).toBeInTheDocument(); + + await ui.searchFor('sonar-adm'); + expect(screen.getByText('sonar-admins')).toBeInTheDocument(); + expect(screen.queryByText('sonar-users')).not.toBeInTheDocument(); + expect(screen.queryByText('johndoe')).not.toBeInTheDocument(); + + await ui.clearSearch(); + expect(screen.getByText('sonar-users')).toBeInTheDocument(); + expect(screen.getByText('johndoe')).toBeInTheDocument(); + }); + + it('should allow to show only permission holders with a specific permission', async () => { + const user = userEvent.setup(); + const ui = getPageObject(user); + renderPermissionsProjectApp(); + await ui.appLoaded(); + + expect(screen.getAllByRole('row').length).toBe(7); + await ui.toggleFilterByPermission(Permissions.Admin); + expect(screen.getAllByRole('row').length).toBe(2); + }); +}); + +describe('assigning/revoking permissions', () => { + it('should allow to apply a permission template', async () => { + const user = userEvent.setup(); + const ui = getPageObject(user); + renderPermissionsProjectApp(); + await ui.appLoaded(); + + await ui.openTemplateModal(); + expect(ui.confirmApplyTemplateBtn.get()).toBeDisabled(); + await ui.chooseTemplate('Permission Template 2'); + expect(ui.templateSuccessfullyApplied.get()).toBeInTheDocument(); + await ui.closeTemplateModal(); + expect(ui.templateSuccessfullyApplied.query()).not.toBeInTheDocument(); + }); + + it('should allow to turn a public project private (and vice-versa)', async () => { + const user = userEvent.setup(); + const ui = getPageObject(user); + renderPermissionsProjectApp(); + await ui.appLoaded(); + + expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked(); + expect( + ui.permissionCheckbox('sonar-users', Permissions.Browse).query() + ).not.toBeInTheDocument(); + await act(async () => { + await ui.turnProjectPrivate(); + }); + expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked(); + expect(ui.permissionCheckbox('sonar-users', Permissions.Browse).get()).toBeInTheDocument(); + + await ui.turnProjectPublic(); + expect(ui.makePublicDisclaimer.get()).toBeInTheDocument(); + await act(async () => { + await ui.confirmTurnProjectPublic(); + }); + expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked(); + }); + + it('should add and remove permissions to/from a group', async () => { + const user = userEvent.setup(); + const ui = getPageObject(user); + renderPermissionsProjectApp(); + await ui.appLoaded(); + + expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked(); + + await ui.togglePermission('sonar-users', Permissions.Admin); + await ui.appLoaded(); + expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).toBeChecked(); + + await ui.togglePermission('sonar-users', Permissions.Admin); + await ui.appLoaded(); + expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked(); + }); + + it('should add and remove permissions to/from a user', async () => { + const user = userEvent.setup(); + const ui = getPageObject(user); + renderPermissionsProjectApp(); + await ui.appLoaded(); + + expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked(); + + await ui.togglePermission('johndoe', Permissions.Scan); + await ui.appLoaded(); + expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).toBeChecked(); + + await ui.togglePermission('johndoe', Permissions.Scan); + await ui.appLoaded(); + expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked(); + }); +}); + +function getPageObject(user: UserEvent) { + const ui = { + loading: byLabelText('loading'), + permissionCheckbox: (target: string, permission: Permissions) => + byRole('checkbox', { + name: `permission.assign_x_to_y.projects_role.${permission}.${target}`, + }), + visibilityRadio: (visibility: Visibility) => + byRole('radio', { name: `visibility.${visibility}` }), + makePublicDisclaimer: byText( + 'projects_role.are_you_sure_to_turn_project_to_public.warning.TRK' + ), + confirmPublicBtn: byRole('button', { name: 'projects_role.turn_project_to_public.TRK' }), + openModalBtn: byRole('button', { name: 'projects_role.apply_template' }), + closeModalBtn: byRole('button', { name: 'close' }), + templateSelect: byRole('combobox', { name: /template/ }), + templateSuccessfullyApplied: byText('projects_role.apply_template.success'), + confirmApplyTemplateBtn: byRole('button', { name: 'apply' }), + tableHeaderFilter: (permission: Permissions) => + byRole('link', { name: `projects_role.${permission}` }), + onlyUsersBtn: byRole('button', { name: 'users.page' }), + onlyGroupsBtn: byRole('button', { name: 'user_groups.page' }), + showAllBtn: byRole('button', { name: 'all' }), + searchInput: byRole('searchbox', { name: 'search.search_for_users_or_groups' }), + }; + + return { + ...ui, + async appLoaded() { + await waitFor(() => { + expect(ui.loading.query()).not.toBeInTheDocument(); + }); + }, + async togglePermission(target: string, permission: Permissions) { + await user.click(ui.permissionCheckbox(target, permission).get()); + }, + async turnProjectPrivate() { + await user.click(ui.visibilityRadio(Visibility.Private).get()); + }, + async turnProjectPublic() { + await user.click(ui.visibilityRadio(Visibility.Public).get()); + }, + async confirmTurnProjectPublic() { + await user.click(ui.confirmPublicBtn.get()); + }, + async openTemplateModal() { + await user.click(ui.openModalBtn.get()); + }, + async closeTemplateModal() { + await user.click(ui.closeModalBtn.get()); + }, + async chooseTemplate(name: string) { + await selectEvent.select(ui.templateSelect.get(), [name]); + await user.click(ui.confirmApplyTemplateBtn.get()); + }, + async toggleFilterByPermission(permission: Permissions) { + await user.click(ui.tableHeaderFilter(permission).get()); + }, + async showOnlyUsers() { + await user.click(ui.onlyUsersBtn.get()); + }, + async showOnlyGroups() { + await user.click(ui.onlyGroupsBtn.get()); + }, + async showAll() { + await user.click(ui.showAllBtn.get()); + }, + async searchFor(name: string) { + await user.type(ui.searchInput.get(), name); + }, + async clearSearch() { + await user.clear(ui.searchInput.get()); + }, + }; +} + +function renderPermissionsProjectApp(override?: Partial) { + function App({ component }: { component: Component }) { + const [realComponent, setRealComponent] = React.useState(component); + return ( + ) => { + setRealComponent({ ...realComponent, ...changes }); + }} + /> + ); + } + + return renderApp( + '/', + + ); +} diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/App-test.tsx.snap deleted file mode 100644 index de21fda0f30..00000000000 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/App-test.tsx.snap +++ /dev/null @@ -1,246 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -
- - -
- -
- -
-`; - -exports[`should render correctly 2`] = ` -
- - -
- -
- -
-`; diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/ApplyTemplate-test.tsx.snap b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/ApplyTemplate-test.tsx.snap deleted file mode 100644 index 78bf796249c..00000000000 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/ApplyTemplate-test.tsx.snap +++ /dev/null @@ -1,83 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render correctly 1`] = ` - - - -`; - -exports[`render correctly 2`] = ` - -
-
-

- projects_role.apply_template_to_xxx.Foo -

-
-
- -
- -