diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2023-02-14 10:59:50 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-02-20 20:03:01 +0000 |
commit | edc84dc653f5cc208bf071f325b08334ad1bddc8 (patch) | |
tree | 6d85b21e2ac8ff6745fdc7a8b8d8b3fcdf975c3e /server | |
parent | e16a504c67e6ee38757e8fc4899e336b4f3c0cda (diff) | |
download | sonarqube-edc84dc653f5cc208bf071f325b08334ad1bddc8.tar.gz sonarqube-edc84dc653f5cc208bf071f325b08334ad1bddc8.zip |
SONAR-17810 Use Visibility enum instead of hard-coded strings
Diffstat (limited to 'server')
45 files changed, 1017 insertions, 1118 deletions
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<T>(response: T): Promise<T> { - 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<T>(response: T): Promise<T> { + 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<ProjectInformationRendererProps> = {}) { 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', <PermissionTemplatesApp />, { - appState: mockAppState({ qualifiers: ['TRK'] }), + appState: mockAppState({ qualifiers: [ComponentQualifier.Project] }), }); } 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<Props, State> { this.setState({ done: true }); } }); - } else { - return Promise.reject(undefined); } }; @@ -94,7 +92,7 @@ export default class ApplyTemplate extends React.PureComponent<Props, State> { 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<Props, State> { } 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<Props, State> { <header className="page-header"> <h1 className="page-title">{translate('permissions.page')}</h1> - {this.props.loading && <i className="spinner" />} + <DeferredSpinner loading={this.props.loading} /> {canApplyPermissionTemplate && ( <div className="page-actions"> 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/PermissionsProjectApp.tsx index 11e31ff7461..acefdf94fb6 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx @@ -24,6 +24,8 @@ 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'; @@ -49,7 +51,7 @@ interface State { usersPaging?: Paging; } -export class App extends React.PureComponent<Props, State> { +export class PermissionsProjectApp extends React.PureComponent<Props, State> { mounted = false; constructor(props: Props) { @@ -73,12 +75,6 @@ export class App extends React.PureComponent<Props, State> { 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; @@ -198,102 +194,90 @@ export class App extends React.PureComponent<Props, State> { grantPermissionToGroup = (group: string, permission: string) => { if (this.mounted) { - this.setState({ - loading: true, - groups: this.addPermissionToGroup(group, permission), - }); + this.setState({ loading: true }); return api .grantPermissionToGroup({ projectKey: this.props.component.key, groupName: group, permission, }) - .then(this.stopLoading, () => { + .then(() => { if (this.mounted) { this.setState({ loading: false, - groups: this.removePermissionFromGroup(group, permission), + groups: this.addPermissionToGroup(group, permission), }); } - }); + }, this.stopLoading); } return Promise.resolve(); }; grantPermissionToUser = (user: string, permission: string) => { if (this.mounted) { - this.setState({ - loading: true, - users: this.addPermissionToUser(user, permission), - }); + this.setState({ loading: true }); return api .grantPermissionToUser({ projectKey: this.props.component.key, login: user, permission, }) - .then(this.stopLoading, () => { + .then(() => { if (this.mounted) { this.setState({ loading: false, - users: this.removePermissionFromUser(user, permission), + users: this.addPermissionToUser(user, permission), }); } - }); + }, this.stopLoading); } return Promise.resolve(); }; revokePermissionFromGroup = (group: string, permission: string) => { if (this.mounted) { - this.setState({ - loading: true, - groups: this.removePermissionFromGroup(group, permission), - }); + this.setState({ loading: true }); return api .revokePermissionFromGroup({ projectKey: this.props.component.key, groupName: group, permission, }) - .then(this.stopLoading, () => { + .then(() => { if (this.mounted) { this.setState({ loading: false, - groups: this.addPermissionToGroup(group, permission), + groups: this.removePermissionFromGroup(group, permission), }); } - }); + }, this.stopLoading); } return Promise.resolve(); }; revokePermissionFromUser = (user: string, permission: string) => { if (this.mounted) { - this.setState({ - loading: true, - users: this.removePermissionFromUser(user, permission), - }); + this.setState({ loading: true }); return api .revokePermissionFromUser({ projectKey: this.props.component.key, login: user, permission, }) - .then(this.stopLoading, () => { + .then(() => { if (this.mounted) { this.setState({ loading: false, - users: this.addPermissionToUser(user, permission), + users: this.removePermissionFromUser(user, permission), }); } - }); + }, this.stopLoading); } return Promise.resolve(); }; handleVisibilityChange = (visibility: string) => { - if (visibility === 'public') { + if (visibility === Visibility.Public) { this.openDisclaimer(); } else { this.turnProjectToPrivate(); @@ -301,31 +285,19 @@ export class App extends React.PureComponent<Props, State> { }; turnProjectToPublic = () => { - this.props.onComponentChange({ visibility: 'public' }); - api.changeProjectVisibility(this.props.component.key, 'public').then( - () => { - this.loadHolders(); - }, - () => { - this.props.onComponentChange({ - visibility: 'private', - }); - } - ); + this.setState({ loading: true }); + return api.changeProjectVisibility(this.props.component.key, Visibility.Public).then(() => { + this.props.onComponentChange({ visibility: Visibility.Public }); + this.loadHolders(); + }); }; turnProjectToPrivate = () => { - this.props.onComponentChange({ visibility: 'private' }); - api.changeProjectVisibility(this.props.component.key, 'private').then( - () => { - this.loadHolders(); - }, - () => { - this.props.onComponentChange({ - visibility: 'public', - }); - } - ); + this.setState({ loading: true }); + return api.changeProjectVisibility(this.props.component.key, Visibility.Private).then(() => { + this.props.onComponentChange({ visibility: Visibility.Private }); + this.loadHolders(); + }); }; openDisclaimer = () => { @@ -340,6 +312,12 @@ export class App extends React.PureComponent<Props, State> { } }; + stopLoading = () => { + if (this.mounted) { + this.setState({ loading: false }); + } + }; + render() { const { component } = this.props; const { @@ -357,8 +335,8 @@ export class App extends React.PureComponent<Props, State> { component.configuration && component.configuration.canUpdateProjectVisibilityToPrivate; let order = PERMISSIONS_ORDER_BY_QUALIFIER[component.qualifier]; - if (component.visibility === 'public') { - order = without(order, 'user', 'codeviewer'); + if (component.visibility === Visibility.Public) { + order = without(order, Permissions.Browse, Permissions.CodeViewer); } const permissions = convertToPermissionDefinitions(order, 'projects_role'); @@ -372,6 +350,7 @@ export class App extends React.PureComponent<Props, State> { canTurnToPrivate={canTurnToPrivate} className="big-spacer-top big-spacer-bottom" onChange={this.handleVisibilityChange} + loading={loading} visibility={component.visibility} /> {disclaimer && ( @@ -405,4 +384,4 @@ export class App extends React.PureComponent<Props, State> { } } -export default withComponentContext(App); +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<App['props']> = {}) { - return shallow<App>(<App component={mockComponent()} onComponentChange={jest.fn()} {...props} />); -} 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( - <ApplyTemplate onClose={jest.fn()} project={{ key: 'foo', name: 'Foo' }} /> - ); - 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<Component>) { + function App({ component }: { component: Component }) { + const [realComponent, setRealComponent] = React.useState(component); + return ( + <PermissionsProjectApp + component={realComponent} + onComponentChange={(changes: Partial<Component>) => { + setRealComponent({ ...realComponent, ...changes }); + }} + /> + ); + } + + return renderApp( + '/', + <App + component={mockComponent({ + visibility: Visibility.Public, + configuration: { + canUpdateProjectVisibilityToPrivate: true, + canApplyPermissionTemplate: true, + }, + ...override, + })} + /> + ); +} 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`] = ` -<div - className="page page-limited" - id="project-permissions-page" -> - <Helmet - defer={false} - encodeSpecialCharacters={true} - prioritizeSeoTags={false} - title="permissions.page" - /> - <PageHeader - component={ - { - "breadcrumbs": [], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": [ - { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": [], - } - } - loadHolders={[Function]} - loading={true} - /> - <div> - <VisibilitySelector - className="big-spacer-top big-spacer-bottom" - onChange={[Function]} - /> - </div> - <AllHoldersList - filter="all" - grantPermissionToGroup={[Function]} - grantPermissionToUser={[Function]} - groups={[]} - onFilter={[Function]} - onLoadMore={[Function]} - onQuery={[Function]} - onSelectPermission={[Function]} - permissions={ - [ - { - "description": "projects_role.user.desc", - "key": "user", - "name": "projects_role.user", - }, - { - "description": "projects_role.codeviewer.desc", - "key": "codeviewer", - "name": "projects_role.codeviewer", - }, - { - "description": "projects_role.issueadmin.desc", - "key": "issueadmin", - "name": "projects_role.issueadmin", - }, - { - "description": "projects_role.securityhotspotadmin.desc", - "key": "securityhotspotadmin", - "name": "projects_role.securityhotspotadmin", - }, - { - "description": "projects_role.admin.desc", - "key": "admin", - "name": "projects_role.admin", - }, - { - "description": "projects_role.scan.desc", - "key": "scan", - "name": "projects_role.scan", - }, - ] - } - query="" - revokePermissionFromGroup={[Function]} - revokePermissionFromUser={[Function]} - users={[]} - /> -</div> -`; - -exports[`should render correctly 2`] = ` -<div - className="page page-limited" - id="project-permissions-page" -> - <Helmet - defer={false} - encodeSpecialCharacters={true} - prioritizeSeoTags={false} - title="permissions.page" - /> - <PageHeader - component={ - { - "breadcrumbs": [], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": [ - { - "deleted": false, - "key": "my-qp", - "language": "ts", - "name": "Sonar way", - }, - ], - "tags": [], - } - } - loadHolders={[Function]} - loading={false} - /> - <div> - <VisibilitySelector - className="big-spacer-top big-spacer-bottom" - onChange={[Function]} - /> - </div> - <AllHoldersList - filter="all" - grantPermissionToGroup={[Function]} - grantPermissionToUser={[Function]} - groups={ - [ - { - "description": "SonarSource team", - "id": "1", - "name": "SonarSource", - "permissions": [ - "admin", - "codeviewer", - "issueadmin", - ], - }, - { - "name": "Anyone", - "permissions": [], - }, - ] - } - groupsPaging={ - { - "pageIndex": 1, - "pageSize": 100, - "total": 2, - } - } - onFilter={[Function]} - onLoadMore={[Function]} - onQuery={[Function]} - onSelectPermission={[Function]} - permissions={ - [ - { - "description": "projects_role.user.desc", - "key": "user", - "name": "projects_role.user", - }, - { - "description": "projects_role.codeviewer.desc", - "key": "codeviewer", - "name": "projects_role.codeviewer", - }, - { - "description": "projects_role.issueadmin.desc", - "key": "issueadmin", - "name": "projects_role.issueadmin", - }, - { - "description": "projects_role.securityhotspotadmin.desc", - "key": "securityhotspotadmin", - "name": "projects_role.securityhotspotadmin", - }, - { - "description": "projects_role.admin.desc", - "key": "admin", - "name": "projects_role.admin", - }, - { - "description": "projects_role.scan.desc", - "key": "scan", - "name": "projects_role.scan", - }, - ] - } - query="" - revokePermissionFromGroup={[Function]} - revokePermissionFromUser={[Function]} - 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": [], - }, - ] - } - usersPaging={ - { - "pageIndex": 1, - "pageSize": 100, - "total": 3, - } - } - /> -</div> -`; 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`] = ` -<SimpleModal - header="projects_role.apply_template_to_xxx.Foo" - onClose={[MockFunction]} - onSubmit={[Function]} - size="small" -> - <Component /> -</SimpleModal> -`; - -exports[`render correctly 2`] = ` -<Modal - contentLabel="projects_role.apply_template_to_xxx.Foo" - onRequestClose={[MockFunction]} - size="small" -> - <form - id="project-permissions-apply-template-form" - onSubmit={[Function]} - > - <header - className="modal-head" - > - <h2> - projects_role.apply_template_to_xxx.Foo - </h2> - </header> - <div - className="modal-body" - > - <MandatoryFieldsExplanation - className="modal-field" - /> - <div - className="modal-field" - > - <label - htmlFor="project-permissions-template-input" - > - template - <MandatoryFieldMarker /> - </label> - <Select - className="Select" - id="project-permissions-template" - inputId="project-permissions-template-input" - onChange={[Function]} - options={ - [ - { - "label": "SonarSource projects", - "value": "tmp1", - }, - ] - } - value={[]} - /> - </div> - </div> - <footer - className="modal-foot" - > - <DeferredSpinner - className="spacer-right" - loading={false} - /> - <SubmitButton - disabled={true} - > - apply - </SubmitButton> - <ResetButtonLink - onClick={[Function]} - > - cancel - </ResetButtonLink> - </footer> - </form> -</Modal> -`; diff --git a/server/sonar-web/src/main/js/apps/permissions/routes.tsx b/server/sonar-web/src/main/js/apps/permissions/routes.tsx index 8c9f71af595..9670b118c68 100644 --- a/server/sonar-web/src/main/js/apps/permissions/routes.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/routes.tsx @@ -20,12 +20,12 @@ import React from 'react'; import { Route } from 'react-router-dom'; import GlobalPermissionsApp from './global/components/App'; -import ProjectPermissionsApp from './project/components/App'; +import PermissionsProjectApp from './project/components/PermissionsProjectApp'; export const globalPermissionsRoutes = () => ( <Route path="permissions" element={<GlobalPermissionsApp />} /> ); export const projectPermissionsRoutes = () => ( - <Route path="project_roles" element={<ProjectPermissionsApp />} /> + <Route path="project_roles" element={<PermissionsProjectApp />} /> ); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx index 23f159696d4..91d89a431f0 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx @@ -99,7 +99,6 @@ export default class GroupHolder extends React.PureComponent<Props, State> { return ( <PermissionCell disabled={group.name === ANYONE && (isComponentPrivate || isAdminPermission)} - isGroupItem={true} key={permissionKey} loading={this.state.loading} onCheck={this.handleCheck} diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx index 19991ed9e25..3b6d1f407c0 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx @@ -42,7 +42,6 @@ interface Props { permissions: PermissionDefinitions; query?: string; selectedPermission?: string; - showPublicProjectsWarning?: boolean; users: PermissionUser[]; } @@ -124,15 +123,7 @@ export default class HoldersList extends React.PureComponent<Props, State> { } render() { - const { - permissions, - users, - groups, - loading, - children, - selectedPermission, - showPublicProjectsWarning, - } = this.props; + const { permissions, users, groups, loading, children, selectedPermission } = this.props; const items = [...groups, ...users]; const [itemWithPermissions, itemWithoutPermissions] = partition(items, (item) => this.getItemInitialPermissionsCount(item) @@ -152,7 +143,6 @@ export default class HoldersList extends React.PureComponent<Props, State> { onSelectPermission={this.props.onSelectPermission} permission={permission} selectedPermission={selectedPermission} - showPublicProjectsWarning={showPublicProjectsWarning} /> ))} </tr> diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionCell.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionCell.tsx index 0fd32baa708..e8657cd2b05 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionCell.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionCell.tsx @@ -20,6 +20,7 @@ import classNames from 'classnames'; import * as React from 'react'; import Checkbox from '../../../../components/controls/Checkbox'; +import { translateWithParameters } from '../../../../helpers/l10n'; import { PermissionDefinition, PermissionDefinitionGroup, @@ -28,9 +29,8 @@ import { } from '../../../../types/types'; import { isPermissionDefinitionGroup } from '../../utils'; -interface Props { +export interface PermissionCellProps { disabled?: boolean; - isGroupItem?: boolean; loading: string[]; onCheck: (checked: boolean, permission?: string) => void; permission: PermissionDefinition | PermissionDefinitionGroup; @@ -38,72 +38,58 @@ interface Props { selectedPermission?: string; } -export default class PermissionCell extends React.PureComponent<Props> { - render() { - const { - disabled, - isGroupItem, - loading, - onCheck, - permission, - permissionItem, - selectedPermission, - } = this.props; - - const tenant = `${isGroupItem ? 'group' : 'user'} '${permissionItem.name}'`; - - if (isPermissionDefinitionGroup(permission)) { - return ( - <td className="text-middle"> - {permission.permissions.map((permissionDefinition) => { - const isChecked = permissionItem.permissions.includes(permissionDefinition.key); - const isDisabled = disabled || loading.includes(permissionDefinition.key); - let state = isChecked ? 'checked' : 'unchecked'; - - if (isDisabled) { - state = 'disabled'; - } - - return ( - <div key={permissionDefinition.key}> - <Checkbox - checked={isChecked} - disabled={isDisabled} - id={permissionDefinition.key} - label={`${state} permission '${permissionDefinition.name}' for ${tenant}`} - onCheck={onCheck} - > - <span className="little-spacer-left">{permissionDefinition.name}</span> - </Checkbox> - </div> - ); - })} - </td> - ); - } - - const isChecked = permissionItem.permissions.includes(permission.key); - const isDisabled = disabled || loading.includes(permission.key); - let state = isChecked ? 'checked' : 'unchecked'; - - if (isDisabled) { - state = 'disabled'; - } +export default function PermissionCell(props: PermissionCellProps) { + const { disabled, loading, onCheck, permission, permissionItem, selectedPermission } = props; + if (isPermissionDefinitionGroup(permission)) { return ( - <td - className={classNames('permission-column text-center text-middle', { - selected: permission.key === selectedPermission, + <td className="text-middle"> + {permission.permissions.map((permissionDefinition) => { + const isChecked = permissionItem.permissions.includes(permissionDefinition.key); + const isDisabled = disabled || loading.includes(permissionDefinition.key); + + return ( + <div key={permissionDefinition.key}> + <Checkbox + checked={isChecked} + disabled={isDisabled} + id={permissionDefinition.key} + label={translateWithParameters( + 'permission.assign_x_to_y', + permissionDefinition.name, + permissionItem.name + )} + onCheck={onCheck} + > + <span className="little-spacer-left">{permissionDefinition.name}</span> + </Checkbox> + </div> + ); })} - > - <Checkbox - checked={isChecked} - disabled={isDisabled} - id={permission.key} - label={`${state} permission '${permission.name}' for ${tenant}`} - onCheck={onCheck} - /> </td> ); } + + const isChecked = permissionItem.permissions.includes(permission.key); + const isDisabled = disabled || loading.includes(permission.key); + + return ( + <td + className={classNames('permission-column text-center text-middle', { + selected: permission.key === selectedPermission, + })} + > + <Checkbox + checked={isChecked} + disabled={isDisabled} + id={permission.key} + label={translateWithParameters( + 'permission.assign_x_to_y', + permission.name, + permissionItem.name + )} + onCheck={onCheck} + /> + </td> + ); } diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx index 0a91e1d1711..36371d68ec8 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx @@ -22,7 +22,6 @@ import * as React from 'react'; import InstanceMessage from '../../../../components/common/InstanceMessage'; import HelpTooltip from '../../../../components/controls/HelpTooltip'; import Tooltip from '../../../../components/controls/Tooltip'; -import { Alert } from '../../../../components/ui/Alert'; import { translate, translateWithParameters } from '../../../../helpers/l10n'; import { PermissionDefinition, PermissionDefinitionGroup } from '../../../../types/types'; import { isPermissionDefinitionGroup } from '../../utils'; @@ -31,7 +30,6 @@ interface Props { onSelectPermission?: (permission: string) => void; permission: PermissionDefinition | PermissionDefinitionGroup; selectedPermission?: string; - showPublicProjectsWarning?: boolean; } export default class PermissionHeader extends React.PureComponent<Props> { @@ -55,19 +53,9 @@ export default class PermissionHeader extends React.PureComponent<Props> { <br /> </React.Fragment> )); - } else { - if (this.props.showPublicProjectsWarning && ['user', 'codeviewer'].includes(permission.key)) { - return ( - <div> - <InstanceMessage message={permission.description} /> - <Alert className="spacer-top" variant="warning"> - {translate('projects_role.public_projects_warning')} - </Alert> - </div> - ); - } - return <InstanceMessage message={permission.description} />; } + + return <InstanceMessage message={permission.description} />; }; render() { diff --git a/server/sonar-web/src/main/js/apps/permissions/utils.ts b/server/sonar-web/src/main/js/apps/permissions/utils.ts index 8c37db214e8..ea2e8220f3c 100644 --- a/server/sonar-web/src/main/js/apps/permissions/utils.ts +++ b/server/sonar-web/src/main/js/apps/permissions/utils.ts @@ -18,34 +18,42 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { translate } from '../../helpers/l10n'; +import { Permissions } from '../../types/permissions'; import { Dict, PermissionDefinition, PermissionDefinitionGroup } from '../../types/types'; export const PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE = [ - 'user', - 'codeviewer', - 'issueadmin', - 'securityhotspotadmin', - 'admin', - 'scan', + Permissions.Browse, + Permissions.CodeViewer, + Permissions.IssueAdmin, + Permissions.SecurityHotspotAdmin, + Permissions.Admin, + Permissions.Scan, ]; export const PERMISSIONS_ORDER_GLOBAL = [ - 'admin', - { category: 'administer', permissions: ['gateadmin', 'profileadmin'] }, - 'scan', - { category: 'creator', permissions: ['provisioning', 'applicationcreator', 'portfoliocreator'] }, + Permissions.Admin, + { + category: 'administer', + permissions: [Permissions.QualityGateAdmin, Permissions.QualityProfileAdmin], + }, + Permissions.Scan, + { + category: 'creator', + permissions: [ + Permissions.ProjectCreation, + Permissions.ApplicationCreation, + Permissions.PortfolioCreation, + ], + }, ]; -export const PERMISSIONS_ORDER_FOR_VIEW = ['user', 'admin']; - -export const PERMISSIONS_ORDER_FOR_DEV = ['user', 'admin']; +export const PERMISSIONS_ORDER_FOR_VIEW = [Permissions.Browse, Permissions.Admin]; export const PERMISSIONS_ORDER_BY_QUALIFIER: Dict<string[]> = { TRK: PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE, VW: PERMISSIONS_ORDER_FOR_VIEW, SVW: PERMISSIONS_ORDER_FOR_VIEW, APP: PERMISSIONS_ORDER_FOR_VIEW, - DEV: PERMISSIONS_ORDER_FOR_DEV, }; function convertToPermissionDefinition(permission: string, l10nPrefix: string) { @@ -60,7 +68,7 @@ function convertToPermissionDefinition(permission: string, l10nPrefix: string) { } export function filterPermissions( - permissions: Array<string | { category: string; permissions: string[] }>, + permissions: Array<Permissions | { category: string; permissions: Permissions[] }>, hasApplicationsEnabled: boolean, hasPortfoliosEnabled: boolean ) { @@ -70,9 +78,9 @@ export function filterPermissions( ...permission, permissions: permission.permissions.filter((p) => { return ( - p === 'provisioning' || - (p === 'portfoliocreator' && hasPortfoliosEnabled) || - (p === 'applicationcreator' && hasApplicationsEnabled) + p === Permissions.ProjectCreation || + (p === Permissions.PortfolioCreation && hasPortfoliosEnabled) || + (p === Permissions.ApplicationCreation && hasApplicationsEnabled) ); }), }; diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx index 83875c4acbe..9be34752c05 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx @@ -21,7 +21,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { get, save } from '../../../../helpers/storage'; import { mockAppState, mockLocation } from '../../../../helpers/testMocks'; -import { ComponentQualifier } from '../../../../types/component'; +import { ComponentQualifier, Visibility } from '../../../../types/component'; import { AllProjects, LS_PROJECTS_SORT, LS_PROJECTS_VIEW } from '../AllProjects'; jest.mock( @@ -170,7 +170,7 @@ function shallowRender( name: 'Foo', qualifier: ComponentQualifier.Project, tags: [], - visibility: 'public', + visibility: Visibility.Public, }, ], total: 0, diff --git a/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx index 0a48ec21294..52b84fa1333 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx @@ -22,7 +22,7 @@ import * as React from 'react'; import PrivacyBadgeContainer from '../../../../../components/common/PrivacyBadgeContainer'; import TagsList from '../../../../../components/tags/TagsList'; import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks'; -import { ComponentQualifier } from '../../../../../types/component'; +import { ComponentQualifier, Visibility } from '../../../../../types/component'; import { CurrentUser } from '../../../../../types/users'; import { Project } from '../../../types'; import ProjectCard from '../ProjectCard'; @@ -42,7 +42,7 @@ const PROJECT: Project = { name: 'Foo', qualifier: ComponentQualifier.Project, tags: [], - visibility: 'public', + visibility: Visibility.Public, }; const USER_LOGGED_OUT = mockCurrentUser(); @@ -63,7 +63,7 @@ it('should display tags', () => { }); it('should display private badge', () => { - const project: Project = { ...PROJECT, visibility: 'private' }; + const project: Project = { ...PROJECT, visibility: Visibility.Private }; expect(shallowRender(project).find(PrivacyBadgeContainer).exists()).toBe(true); }); diff --git a/server/sonar-web/src/main/js/apps/projects/types.ts b/server/sonar-web/src/main/js/apps/projects/types.ts index 974f231c02e..a56e9d81e00 100644 --- a/server/sonar-web/src/main/js/apps/projects/types.ts +++ b/server/sonar-web/src/main/js/apps/projects/types.ts @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { ComponentQualifier } from '../../types/component'; -import { Dict, Visibility } from '../../types/types'; +import { ComponentQualifier, Visibility } from '../../types/component'; +import { Dict } from '../../types/types'; export interface Project { analysisDate?: string; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ChangeDefaultVisibilityForm.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ChangeDefaultVisibilityForm.tsx index b2cfeebf233..d22fd12ffae 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ChangeDefaultVisibilityForm.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ChangeDefaultVisibilityForm.tsx @@ -23,7 +23,7 @@ import Modal from '../../components/controls/Modal'; import Radio from '../../components/controls/Radio'; import { Alert } from '../../components/ui/Alert'; import { translate } from '../../helpers/l10n'; -import { Visibility } from '../../types/types'; +import { Visibility } from '../../types/component'; export interface Props { defaultVisibility: Visibility; @@ -58,7 +58,7 @@ export default class ChangeDefaultVisibilityForm extends React.PureComponent<Pro </header> <div className="modal-body"> - {['public', 'private'].map((visibility) => ( + {Object.values(Visibility).map((visibility) => ( <div className="big-spacer-bottom" key={visibility}> <Radio value={visibility} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx index ef98661aaea..b12bd6e0217 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx @@ -30,8 +30,8 @@ import MandatoryFieldMarker from '../../components/ui/MandatoryFieldMarker'; import MandatoryFieldsExplanation from '../../components/ui/MandatoryFieldsExplanation'; import { translate } from '../../helpers/l10n'; import { getProjectUrl } from '../../helpers/urls'; +import { Visibility } from '../../types/component'; import { GlobalSettingKeys } from '../../types/settings'; -import { Visibility } from '../../types/types'; interface Props { defaultProjectVisibility?: Visibility; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx index 9d448440e47..9f1b40851f8 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import { Button, EditButton } from '../../components/controls/buttons'; import { translate } from '../../helpers/l10n'; -import { Visibility } from '../../types/types'; +import { Visibility } from '../../types/component'; import ChangeDefaultVisibilityForm from './ChangeDefaultVisibilityForm'; export interface Props { @@ -78,7 +78,7 @@ export default class Header extends React.PureComponent<Props, State> { {visibilityForm && ( <ChangeDefaultVisibilityForm - defaultVisibility={defaultProjectVisibility || 'public'} + defaultVisibility={defaultProjectVisibility || Visibility.Public} onClose={this.closeVisiblityForm} onConfirm={this.props.onChangeDefaultProjectVisibility} /> 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 2688eeede81..2f4a02b75b5 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx @@ -30,9 +30,9 @@ import { toShortNotSoISOString } from '../../helpers/dates'; import { throwGlobalError } from '../../helpers/error'; import { translate } from '../../helpers/l10n'; import { hasGlobalPermission } from '../../helpers/users'; +import { Visibility } from '../../types/component'; import { Permissions } from '../../types/permissions'; import { SettingsKey } from '../../types/settings'; -import { Visibility } from '../../types/types'; import { LoggedInUser } from '../../types/users'; import CreateProjectForm from './CreateProjectForm'; import Header from './Header'; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx index c231ca1f3c6..dee37b24790 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx @@ -31,7 +31,7 @@ import Select, { LabelValueSelectOption } from '../../components/controls/Select import QualifierIcon from '../../components/icons/QualifierIcon'; import { translate } from '../../helpers/l10n'; import { AppState } from '../../types/appstate'; -import { Visibility } from '../../types/types'; +import { Visibility } from '../../types/component'; import BulkApplyTemplateModal from './BulkApplyTemplateModal'; import DeleteModal from './DeleteModal'; @@ -172,8 +172,8 @@ export class Search extends React.PureComponent<Props, State> { renderVisibilityFilter = () => { const options = [ { value: 'all', label: translate('visibility.both') }, - { value: 'public', label: translate('visibility.public') }, - { value: 'private', label: translate('visibility.private') }, + { value: Visibility.Public, label: translate('visibility.public') }, + { value: Visibility.Private, label: translate('visibility.private') }, ]; return ( <td className="thin nowrap text-middle"> diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeDefaultVisibilityForm-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeDefaultVisibilityForm-test.tsx index a2be921363c..e6763d02746 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeDefaultVisibilityForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeDefaultVisibilityForm-test.tsx @@ -21,6 +21,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import Radio from '../../../components/controls/Radio'; import { click } from '../../../helpers/testUtils'; +import { Visibility } from '../../../types/component'; import ChangeDefaultVisibilityForm from '../ChangeDefaultVisibilityForm'; it('closes', () => { @@ -35,17 +36,17 @@ it('changes visibility', () => { const wrapper = shallowRender({ onConfirm }); expect(wrapper).toMatchSnapshot(); - wrapper.find(Radio).first().props().onCheck('private'); + wrapper.find(Radio).first().props().onCheck(Visibility.Private); expect(wrapper).toMatchSnapshot(); click(wrapper.find('.js-confirm')); - expect(onConfirm).toHaveBeenCalledWith('private'); + expect(onConfirm).toHaveBeenCalledWith(Visibility.Private); }); function shallowRender(props: Partial<ChangeDefaultVisibilityForm['props']> = {}) { return shallow( <ChangeDefaultVisibilityForm - defaultVisibility="public" + defaultVisibility={Visibility.Public} onClose={jest.fn()} onConfirm={jest.fn()} {...props} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx index eb4420f6685..6a07ac14fcc 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx @@ -20,6 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { click } from '../../../helpers/testUtils'; +import { Visibility } from '../../../types/component'; import Header, { Props } from '../Header'; jest.mock('../../../helpers/system', () => ({ @@ -27,10 +28,8 @@ jest.mock('../../../helpers/system', () => ({ })); it('renders', () => { - expect(shallowRender()).toMatchSnapshot('default'); - expect(shallowRender({ defaultProjectVisibility: undefined })).toMatchSnapshot( - 'undefined visibility' - ); + expect(shallowRender()).toMatchSnapshot('undefined visibility'); + expect(shallowRender({ defaultProjectVisibility: Visibility.Public })).toMatchSnapshot('default'); }); it('creates project', () => { @@ -48,8 +47,8 @@ it('changes default visibility', () => { const modalWrapper = wrapper.find('ChangeDefaultVisibilityForm'); expect(modalWrapper).toMatchSnapshot(); - modalWrapper.prop<Function>('onConfirm')('private'); - expect(onChangeDefaultProjectVisibility).toHaveBeenCalledWith('private'); + modalWrapper.prop<Function>('onConfirm')(Visibility.Private); + expect(onChangeDefaultProjectVisibility).toHaveBeenCalledWith(Visibility.Private); modalWrapper.prop<Function>('onClose')(); wrapper.update(); @@ -59,7 +58,6 @@ it('changes default visibility', () => { function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { return shallow( <Header - defaultProjectVisibility="public" hasProvisionPermission={true} onChangeDefaultProjectVisibility={jest.fn()} onProjectCreate={jest.fn()} 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 4a9d1f1127a..9e98fa9c417 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 @@ -20,7 +20,7 @@ import { screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { getComponents, SearchProjectsParameters } from '../../../api/components'; -import PermissionTemplateServiceMock from '../../../api/mocks/PermissionTemplateServiceMock'; +import PermissionsServiceMock from '../../../api/mocks/PermissionsServiceMock'; import { renderAppWithAdminContext } from '../../../helpers/testReactTestingUtils'; import { ComponentQualifier, Visibility } from '../../../types/component'; import routes from '../routes'; @@ -38,13 +38,13 @@ jest.mock('../../../api/settings', () => ({ const components = mockComponents(11); -let permissionTemplateMock: PermissionTemplateServiceMock; +let serviceMock: PermissionsServiceMock; beforeAll(() => { - permissionTemplateMock = new PermissionTemplateServiceMock(); + serviceMock = new PermissionsServiceMock(); }); afterEach(() => { - permissionTemplateMock.reset(); + serviceMock.reset(); }); describe('Bulk Apply', () => { diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-test.tsx index bd5ea500119..ee3828034e3 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-test.tsx @@ -24,6 +24,7 @@ import { changeProjectDefaultVisibility } from '../../../api/permissions'; import { getValue } from '../../../api/settings'; import { mockLoggedInUser } from '../../../helpers/testMocks'; import { waitAndUpdate } from '../../../helpers/testUtils'; +import { ComponentQualifier, Visibility } from '../../../types/component'; import { ProjectManagementApp, Props } from '../ProjectManagementApp'; jest.mock('lodash', () => { @@ -60,9 +61,12 @@ beforeEach(() => { it('fetches all projects on mount', async () => { const wrapper = shallowRender(); await waitAndUpdate(wrapper); - expect(getComponents).toHaveBeenLastCalledWith({ ...defaultSearchParameters, qualifiers: 'TRK' }); + expect(getComponents).toHaveBeenLastCalledWith({ + ...defaultSearchParameters, + qualifiers: ComponentQualifier.Project, + }); expect(getValue).toHaveBeenCalled(); - expect(wrapper.state().defaultProjectVisibility).toBe('public'); + expect(wrapper.state().defaultProjectVisibility).toBe(Visibility.Public); }); it('selects provisioned', () => { @@ -109,12 +113,12 @@ it('should handle default project visibility change', async () => { await waitAndUpdate(wrapper); - expect(wrapper.state().defaultProjectVisibility).toBe('public'); - wrapper.instance().handleDefaultProjectVisibilityChange('private'); + expect(wrapper.state().defaultProjectVisibility).toBe(Visibility.Public); + wrapper.instance().handleDefaultProjectVisibilityChange(Visibility.Private); - expect(changeProjectDefaultVisibility).toHaveBeenCalledWith('private'); + expect(changeProjectDefaultVisibility).toHaveBeenCalledWith(Visibility.Private); await waitAndUpdate(wrapper); - expect(wrapper.state().defaultProjectVisibility).toBe('private'); + expect(wrapper.state().defaultProjectVisibility).toBe(Visibility.Private); }); it('loads more', () => { diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx index 2224ebc7013..819bd7ae73d 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx @@ -22,6 +22,7 @@ import * as React from 'react'; import { getComponentNavigation } from '../../../api/navigation'; import { mockLoggedInUser } from '../../../helpers/testMocks'; import { click, waitAndUpdate } from '../../../helpers/testUtils'; +import { ComponentQualifier, Visibility } from '../../../types/component'; import ProjectRowActions, { Props } from '../ProjectRowActions'; jest.mock('../../../api/navigation', () => ({ @@ -123,8 +124,8 @@ function shallowRender(props: Partial<Props> = {}) { id: 'foo', key: 'foo', name: 'Foo', - qualifier: 'TRK', - visibility: 'private', + qualifier: ComponentQualifier.Project, + visibility: Visibility.Private, }} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx index 7b77b8af2e2..aa3c008d100 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx @@ -19,11 +19,12 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { ComponentQualifier, Visibility } from '../../../types/component'; import Projects from '../Projects'; const projects = [ - { key: 'a', name: 'A', qualifier: 'TRK', visibility: 'public' }, - { key: 'b', name: 'B', qualifier: 'TRK', visibility: 'public' }, + { key: 'a', name: 'A', qualifier: ComponentQualifier.Project, visibility: Visibility.Public }, + { key: 'b', name: 'B', qualifier: ComponentQualifier.Project, visibility: Visibility.Public }, ]; const selection = ['a']; diff --git a/server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx b/server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx index 658b98196e4..86a203671ae 100644 --- a/server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx +++ b/server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx @@ -21,7 +21,7 @@ import classNames from 'classnames'; import * as React from 'react'; import Tooltip from '../../components/controls/Tooltip'; import { translate } from '../../helpers/l10n'; -import { Visibility } from '../../types/types'; +import { Visibility } from '../../types/component'; interface PrivacyBadgeContainerProps { className?: string; @@ -34,7 +34,7 @@ export default function PrivacyBadgeContainer({ qualifier, visibility, }: PrivacyBadgeContainerProps) { - if (visibility !== 'private') { + if (visibility !== Visibility.Private) { return null; } diff --git a/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx b/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx index dccc7fad07c..1cf6318108d 100644 --- a/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx +++ b/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx @@ -21,38 +21,38 @@ import classNames from 'classnames'; import * as React from 'react'; import Radio from '../../components/controls/Radio'; import { translate } from '../../helpers/l10n'; -import { Visibility } from '../../types/types'; +import { Visibility } from '../../types/component'; -interface Props { +export interface VisibilitySelectorProps { canTurnToPrivate?: boolean; className?: string; onChange: (visibility: Visibility) => void; showDetails?: boolean; visibility?: Visibility; + loading?: boolean; } -export default class VisibilitySelector extends React.PureComponent<Props> { - render() { - return ( - <div className={classNames(this.props.className)}> - {['public', 'private'].map((visibility) => ( - <Radio - className={`huge-spacer-right visibility-${visibility}`} - key={visibility} - value={visibility} - checked={this.props.visibility === visibility} - onCheck={this.props.onChange} - disabled={visibility === 'private' && !this.props.canTurnToPrivate} - > - <div> - {translate('visibility', visibility)} - {this.props.showDetails && ( - <p className="note">{translate('visibility', visibility, 'description.long')}</p> - )} - </div> - </Radio> - ))} - </div> - ); - } +export default function VisibilitySelector(props: VisibilitySelectorProps) { + const { className, canTurnToPrivate, visibility, showDetails, loading = false } = props; + return ( + <div className={classNames(className)}> + {Object.values(Visibility).map((v) => ( + <Radio + className={`huge-spacer-right visibility-${v}`} + key={v} + value={v} + checked={v === visibility} + onCheck={props.onChange} + disabled={(v === Visibility.Private && !canTurnToPrivate) || loading} + > + <div> + {translate('visibility', v)} + {showDetails && ( + <p className="note">{translate('visibility', v, 'description.long')}</p> + )} + </div> + </Radio> + ))} + </div> + ); } diff --git a/server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx index fb4cdf90552..12e905a51ab 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx +++ b/server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx @@ -19,7 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { ComponentQualifier } from '../../../types/component'; +import { ComponentQualifier, Visibility } from '../../../types/component'; import PrivacyBadge from '../PrivacyBadgeContainer'; it('renders', () => { @@ -27,11 +27,15 @@ it('renders', () => { }); it('do not render', () => { - expect(getWrapper({ visibility: 'public' })).toMatchSnapshot(); + expect(getWrapper({ visibility: Visibility.Public })).toMatchSnapshot(); }); function getWrapper(props = {}) { return shallow( - <PrivacyBadge qualifier={ComponentQualifier.Project} visibility="private" {...props} /> + <PrivacyBadge + qualifier={ComponentQualifier.Project} + visibility={Visibility.Private} + {...props} + /> ); } diff --git a/server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx index 4330257f9cf..babc21706da 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx +++ b/server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx @@ -20,34 +20,35 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import Radio from '../../../components/controls/Radio'; -import VisibilitySelector from '../VisibilitySelector'; +import { Visibility } from '../../../types/component'; +import VisibilitySelector, { VisibilitySelectorProps } from '../VisibilitySelector'; it('changes visibility', () => { const onChange = jest.fn(); const wrapper = shallowRender({ onChange }); expect(wrapper).toMatchSnapshot(); - wrapper.find(Radio).first().props().onCheck('private'); - expect(onChange).toHaveBeenCalledWith('private'); + wrapper.find(Radio).first().props().onCheck(Visibility.Private); + expect(onChange).toHaveBeenCalledWith(Visibility.Private); - wrapper.setProps({ visibility: 'private' }); + wrapper.setProps({ visibility: Visibility.Private }); expect(wrapper).toMatchSnapshot(); - wrapper.find(Radio).first().props().onCheck('public'); - expect(onChange).toHaveBeenCalledWith('public'); + wrapper.find(Radio).first().props().onCheck(Visibility.Public); + expect(onChange).toHaveBeenCalledWith(Visibility.Public); }); it('renders disabled', () => { expect(shallowRender({ canTurnToPrivate: false })).toMatchSnapshot(); }); -function shallowRender(props?: Partial<VisibilitySelector['props']>) { - return shallow<VisibilitySelector>( +function shallowRender(props?: Partial<VisibilitySelectorProps>) { + return shallow<VisibilitySelectorProps>( <VisibilitySelector className="test-classname" canTurnToPrivate={true} onChange={jest.fn()} - visibility="public" + visibility={Visibility.Public} {...props} /> ); diff --git a/server/sonar-web/src/main/js/helpers/mocks/permissions.ts b/server/sonar-web/src/main/js/helpers/mocks/permissions.ts index 2c41ddc8a4c..d7850a004fe 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/permissions.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/permissions.ts @@ -17,7 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { PermissionGroup, PermissionUser } from '../../types/types'; +import { + Permission, + PermissionGroup, + PermissionTemplate, + PermissionTemplateGroup, + PermissionUser, +} from '../../types/types'; import { mockUser } from '../testMocks'; export function mockPermissionGroup(overrides: Partial<PermissionGroup> = {}): PermissionGroup { @@ -37,3 +43,52 @@ export function mockPermissionUser(overrides: Partial<PermissionUser> = {}): Per ...overrides, }; } + +export function mockPermission(override: Partial<Permission> = {}) { + return { + key: 'admin', + name: 'Admin', + description: 'Can do anything he/she wants', + ...override, + }; +} + +export function mockPermissionTemplateGroup(override: Partial<PermissionTemplateGroup> = {}) { + return { + groupsCount: 1, + usersCount: 1, + key: 'admin', + withProjectCreator: true, + ...override, + }; +} + +export function mockPermissionTemplate(override: Partial<PermissionTemplate> = {}) { + return { + id: 'template1', + name: 'Permission Template 1', + createdAt: '', + defaultFor: [], + permissions: [mockPermissionTemplateGroup()], + ...override, + }; +} + +export function mockTemplateUser(override: Partial<PermissionUser> = {}) { + return { + login: 'admin', + name: 'Admin Admin', + permissions: ['admin', 'codeviewer'], + ...override, + }; +} + +export function mockTemplateGroup(override: Partial<PermissionGroup> = {}) { + return { + id: 'Anyone', + name: 'Anyone', + description: 'everyone', + permissions: ['admin', 'codeviewer'], + ...override, + }; +} diff --git a/server/sonar-web/src/main/js/helpers/mocks/projects.ts b/server/sonar-web/src/main/js/helpers/mocks/projects.ts index 791d477568c..84e1a61b351 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/projects.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/projects.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { Project } from '../../apps/projects/types'; -import { ComponentQualifier } from '../../types/component'; +import { ComponentQualifier, Visibility } from '../../types/component'; export function mockProject(overrides: Partial<Project> = {}): Project { return { @@ -27,7 +27,7 @@ export function mockProject(overrides: Partial<Project> = {}): Project { measures: {}, qualifier: ComponentQualifier.Project, tags: [], - visibility: 'public', + visibility: Visibility.Public, ...overrides, }; } diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index 2efd37c0e33..eab7d9b809b 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -42,11 +42,6 @@ import { Metric, Paging, Period, - Permission, - PermissionGroup, - PermissionTemplate, - PermissionTemplateGroup, - PermissionUser, ProfileInheritanceDetails, Rule, RuleActivation, @@ -761,52 +756,3 @@ export function mockDumpStatus(props: Partial<DumpStatus> = {}): DumpStatus { export function mockRuleRepository(override: Partial<RuleRepository> = {}) { return { key: 'css', language: 'css', name: 'SonarQube', ...override }; } - -export function mockPermission(override: Partial<Permission> = {}) { - return { - key: 'admin', - name: 'Admin', - description: 'Can do anything he/she wants', - ...override, - }; -} - -export function mockPermissionTemplateGroup(override: Partial<PermissionTemplateGroup> = {}) { - return { - groupsCount: 1, - usersCount: 1, - key: 'admin', - withProjectCreator: true, - ...override, - }; -} - -export function mockPermissionTemplate(override: Partial<PermissionTemplate> = {}) { - return { - id: 'template1', - name: 'Permission Template 1', - createdAt: '', - defaultFor: [], - permissions: [mockPermissionTemplateGroup()], - ...override, - }; -} - -export function mockTemplateUser(override: Partial<PermissionUser> = {}) { - return { - login: 'admin', - name: 'Admin Admin', - permissions: ['admin', 'codeviewer'], - ...override, - }; -} - -export function mockTemplateGroup(override: Partial<PermissionGroup> = {}) { - return { - id: 'Anyone', - name: 'Anyone', - description: 'everyone', - permissions: ['admin', 'codeviewer'], - ...override, - }; -} diff --git a/server/sonar-web/src/main/js/types/permissions.ts b/server/sonar-web/src/main/js/types/permissions.ts index 3c536432421..9c256d1b4b7 100644 --- a/server/sonar-web/src/main/js/types/permissions.ts +++ b/server/sonar-web/src/main/js/types/permissions.ts @@ -19,8 +19,14 @@ */ export enum Permissions { Admin = 'admin', + Browse = 'user', ProjectCreation = 'provisioning', ApplicationCreation = 'applicationcreator', + PortfolioCreation = 'portfoliocreator', QualityGateAdmin = 'gateadmin', + QualityProfileAdmin = 'profileadmin', Scan = 'scan', + CodeViewer = 'codeviewer', + IssueAdmin = 'issueadmin', + SecurityHotspotAdmin = 'securityhotspotadmin', } diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts index caedb79e1d3..4a7bc7ab2e6 100644 --- a/server/sonar-web/src/main/js/types/types.ts +++ b/server/sonar-web/src/main/js/types/types.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { RuleDescriptionSection } from '../apps/coding-rules/rule'; -import { ComponentQualifier } from './component'; +import { ComponentQualifier, Visibility } from './component'; import { MessageFormatting } from './issues'; import { UserActive, UserBase } from './users'; @@ -772,8 +772,6 @@ export interface UserSelected extends UserActive { selected: boolean; } -export type Visibility = 'public' | 'private'; - export namespace WebApi { export interface Action { key: string; |