Bläddra i källkod

SONAR-17810 Use Visibility enum instead of hard-coded strings

tags/10.0.0.68432
Wouter Admiraal 1 år sedan
förälder
incheckning
edc84dc653
46 ändrade filer med 1028 tillägg och 1120 borttagningar
  1. 6
    2
      server/sonar-web/src/main/js/api/components.ts
  2. 0
    191
      server/sonar-web/src/main/js/api/mocks/PermissionTemplateServiceMock.ts
  3. 389
    0
      server/sonar-web/src/main/js/api/mocks/PermissionsServiceMock.ts
  4. 2
    2
      server/sonar-web/src/main/js/api/permissions.ts
  5. 11
    5
      server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
  6. 4
    4
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformation-test.tsx
  7. 8
    2
      server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformationRenderer-test.tsx
  8. 29
    56
      server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/PermissionTemplatesApp-it.tsx
  9. 1
    3
      server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx
  10. 4
    3
      server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx
  11. 40
    61
      server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx
  12. 0
    157
      server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx
  13. 0
    53
      server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/ApplyTemplate-test.tsx
  14. 287
    0
      server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx
  15. 0
    246
      server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/App-test.tsx.snap
  16. 0
    83
      server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/ApplyTemplate-test.tsx.snap
  17. 2
    2
      server/sonar-web/src/main/js/apps/permissions/routes.tsx
  18. 0
    1
      server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx
  19. 1
    11
      server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx
  20. 50
    64
      server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionCell.tsx
  21. 2
    14
      server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx
  22. 26
    18
      server/sonar-web/src/main/js/apps/permissions/utils.ts
  23. 2
    2
      server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
  24. 3
    3
      server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx
  25. 2
    2
      server/sonar-web/src/main/js/apps/projects/types.ts
  26. 2
    2
      server/sonar-web/src/main/js/apps/projectsManagement/ChangeDefaultVisibilityForm.tsx
  27. 1
    1
      server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx
  28. 2
    2
      server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
  29. 1
    1
      server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx
  30. 3
    3
      server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
  31. 4
    3
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeDefaultVisibilityForm-test.tsx
  32. 5
    7
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx
  33. 4
    4
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx
  34. 10
    6
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-test.tsx
  35. 3
    2
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx
  36. 3
    2
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
  37. 2
    2
      server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx
  38. 26
    26
      server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx
  39. 7
    3
      server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx
  40. 10
    9
      server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx
  41. 56
    1
      server/sonar-web/src/main/js/helpers/mocks/permissions.ts
  42. 2
    2
      server/sonar-web/src/main/js/helpers/mocks/projects.ts
  43. 0
    54
      server/sonar-web/src/main/js/helpers/testMocks.ts
  44. 6
    0
      server/sonar-web/src/main/js/types/permissions.ts
  45. 1
    3
      server/sonar-web/src/main/js/types/types.ts
  46. 11
    2
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 6
- 2
server/sonar-web/src/main/js/api/components.ts Visa fil

@@ -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 {

+ 0
- 191
server/sonar-web/src/main/js/api/mocks/PermissionTemplateServiceMock.ts Visa fil

@@ -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));
}
}

+ 389
- 0
server/sonar-web/src/main/js/api/mocks/PermissionsServiceMock.ts Visa fil

@@ -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));
}
}

+ 2
- 2
server/sonar-web/src/main/js/api/permissions.ts Visa fil

@@ -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);
}


+ 11
- 5
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx Visa fil

@@ -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,
});

+ 4
- 4
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformation-test.tsx Visa fil

@@ -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');

+ 8
- 2
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformationRenderer-test.tsx Visa fil

@@ -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}

+ 29
- 56
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/PermissionTemplatesApp-it.tsx Visa fil

@@ -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] }),
});
}

+ 1
- 3
server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx Visa fil

@@ -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
);


+ 4
- 3
server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx Visa fil

@@ -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">

server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx → server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx Visa fil

@@ -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);

+ 0
- 157
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx Visa fil

@@ -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} />);
}

+ 0
- 53
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/ApplyTemplate-test.tsx Visa fil

@@ -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();
});

+ 287
- 0
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx Visa fil

@@ -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,
})}
/>
);
}

+ 0
- 246
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/App-test.tsx.snap Visa fil

@@ -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>
`;

+ 0
- 83
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/ApplyTemplate-test.tsx.snap Visa fil

@@ -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>
`;

+ 2
- 2
server/sonar-web/src/main/js/apps/permissions/routes.tsx Visa fil

@@ -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 />} />
);

+ 0
- 1
server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx Visa fil

@@ -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}

+ 1
- 11
server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx Visa fil

@@ -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>

+ 50
- 64
server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionCell.tsx Visa fil

@@ -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>
);
}

+ 2
- 14
server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx Visa fil

@@ -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() {

+ 26
- 18
server/sonar-web/src/main/js/apps/permissions/utils.ts Visa fil

@@ -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)
);
}),
};

+ 2
- 2
server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx Visa fil

@@ -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,

+ 3
- 3
server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx Visa fil

@@ -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);
});


+ 2
- 2
server/sonar-web/src/main/js/apps/projects/types.ts Visa fil

@@ -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;

+ 2
- 2
server/sonar-web/src/main/js/apps/projectsManagement/ChangeDefaultVisibilityForm.tsx Visa fil

@@ -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}

+ 1
- 1
server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx Visa fil

@@ -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;

+ 2
- 2
server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx Visa fil

@@ -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}
/>

+ 1
- 1
server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx Visa fil

@@ -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';

+ 3
- 3
server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx Visa fil

@@ -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">

+ 4
- 3
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeDefaultVisibilityForm-test.tsx Visa fil

@@ -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}

+ 5
- 7
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx Visa fil

@@ -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()}

+ 4
- 4
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx Visa fil

@@ -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', () => {

+ 10
- 6
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-test.tsx Visa fil

@@ -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', () => {

+ 3
- 2
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx Visa fil

@@ -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}
/>

+ 3
- 2
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx Visa fil

@@ -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'];


+ 2
- 2
server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx Visa fil

@@ -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;
}


+ 26
- 26
server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx Visa fil

@@ -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>
);
}

+ 7
- 3
server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx Visa fil

@@ -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}
/>
);
}

+ 10
- 9
server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx Visa fil

@@ -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}
/>
);

+ 56
- 1
server/sonar-web/src/main/js/helpers/mocks/permissions.ts Visa fil

@@ -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,
};
}

+ 2
- 2
server/sonar-web/src/main/js/helpers/mocks/projects.ts Visa fil

@@ -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,
};
}

+ 0
- 54
server/sonar-web/src/main/js/helpers/testMocks.ts Visa fil

@@ -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,
};
}

+ 6
- 0
server/sonar-web/src/main/js/types/permissions.ts Visa fil

@@ -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',
}

+ 1
- 3
server/sonar-web/src/main/js/types/types.ts Visa fil

@@ -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;

+ 11
- 2
sonar-core/src/main/resources/org/sonar/l10n/core.properties Visa fil

@@ -2732,6 +2732,15 @@ metric.vulnerabilities.name=Vulnerabilities
metric.wont_fix_issues.description=Won't fix issues
metric.wont_fix_issues.name=Won't Fix Issues


#------------------------------------------------------------------------------
#
# PERMISSIONS
#
#------------------------------------------------------------------------------
permission.assign_x_to_y=Assign permission {0} to {1}


#------------------------------------------------------------------------------
#
# GLOBAL PERMISSIONS
@@ -2783,7 +2792,7 @@ projects_role.scan=Execute Analysis
projects_role.scan.desc=Ability to get all settings required to perform an analysis (including the secured settings like passwords) and to push analysis results to the {instance} server.
projects_role.bulk_change=Bulk Change
projects_role.apply_template=Apply Permission Template
projects_role.apply_template_to_xxx=Apply Permission Template To "{0}"
projects_role.apply_template_to_x=Apply Permission Template To "{0}"
projects_role.apply_template.success=Permission template was successfully applied.
projects_role.no_projects=There are currently no results to apply the permission template to.
projects_role.turn_x_to_public=Turn "{0}" to Public
@@ -2803,7 +2812,6 @@ projects_role.portfoliocreator=Create Portfolios
projects_role.portfoliocreator.desc=Allow to create portfolios for non system administrator.



#------------------------------------------------------------------------------
#
# PERMISSION TEMPLATES
@@ -2830,6 +2838,7 @@ permission_templates.bulk_apply_permission_template.apply_to_all=You're about to
permission_templates.select_to_delete=You must select at least one item
permission_templates.delete_selected=Delete all selected items


#------------------------------------------------------------------------------
#
# Promotion

Laddar…
Avbryt
Spara