]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-17810 Use Visibility enum instead of hard-coded strings
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Tue, 14 Feb 2023 09:59:50 +0000 (10:59 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 20 Feb 2023 20:03:01 +0000 (20:03 +0000)
47 files changed:
server/sonar-web/src/main/js/api/components.ts
server/sonar-web/src/main/js/api/mocks/PermissionTemplateServiceMock.ts [deleted file]
server/sonar-web/src/main/js/api/mocks/PermissionsServiceMock.ts [new file with mode: 0644]
server/sonar-web/src/main/js/api/permissions.ts
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformation-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformationRenderer-test.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/PermissionTemplatesApp-it.tsx
server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx [deleted file]
server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx
server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/ApplyTemplate-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/App-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/ApplyTemplate-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/permissions/routes.tsx
server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx
server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx
server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionCell.tsx
server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx
server/sonar-web/src/main/js/apps/permissions/utils.ts
server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx
server/sonar-web/src/main/js/apps/projects/types.ts
server/sonar-web/src/main/js/apps/projectsManagement/ChangeDefaultVisibilityForm.tsx
server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx
server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx
server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeDefaultVisibilityForm-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRowActions-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx
server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx
server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx
server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx
server/sonar-web/src/main/js/components/common/__tests__/VisibilitySelector-test.tsx
server/sonar-web/src/main/js/helpers/mocks/permissions.ts
server/sonar-web/src/main/js/helpers/mocks/projects.ts
server/sonar-web/src/main/js/helpers/testMocks.ts
server/sonar-web/src/main/js/types/permissions.ts
server/sonar-web/src/main/js/types/types.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 85aa9e48933c9bb0c3fb63b09a62cf6b200e4ae9..ddecc845d70f7b5e03fa4ddb29e93575de00525b 100644 (file)
 import { throwGlobalError } from '../helpers/error';
 import { getJSON, post, postJSON, RequestData } from '../helpers/request';
 import { BranchParameters } from '../types/branch-like';
-import { ComponentQualifier, TreeComponent, TreeComponentWithPath } from '../types/component';
+import {
+  ComponentQualifier,
+  TreeComponent,
+  TreeComponentWithPath,
+  Visibility,
+} from '../types/component';
 import {
   ComponentMeasure,
   Dict,
@@ -31,7 +36,6 @@ import {
   Paging,
   SourceLine,
   SourceViewerFile,
-  Visibility,
 } from '../types/types';
 
 export interface BaseSearchProjectsParameters {
diff --git a/server/sonar-web/src/main/js/api/mocks/PermissionTemplateServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/PermissionTemplateServiceMock.ts
deleted file mode 100644 (file)
index d93999a..0000000
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { chunk, cloneDeep } from 'lodash';
-import {
-  mockPermissionTemplate,
-  mockTemplateGroup,
-  mockTemplateUser,
-} from '../../helpers/testMocks';
-import { PermissionTemplate } from '../../types/types';
-import { BaseSearchProjectsParameters } from '../components';
-import {
-  addProjectCreatorToTemplate,
-  bulkApplyTemplate,
-  getPermissionTemplateGroups,
-  getPermissionTemplates,
-  getPermissionTemplateUsers,
-  grantTemplatePermissionToGroup,
-  grantTemplatePermissionToUser,
-  removeProjectCreatorFromTemplate,
-  revokeTemplatePermissionFromGroup,
-  revokeTemplatePermissionFromUser,
-} from '../permissions';
-
-const MAX_PROJECTS_TO_APPLY_PERMISSION_TEMPLATE = 10;
-
-const defaultPermissionTemplates: PermissionTemplate[] = [
-  mockPermissionTemplate(),
-  mockPermissionTemplate({
-    id: 'template2',
-    name: 'Permission Template 2',
-  }),
-];
-
-const templateUsers = [
-  mockTemplateUser(),
-  mockTemplateUser({
-    login: 'gooduser1',
-    name: 'John',
-    permissions: ['issueadmin', 'securityhotspotadmin', 'user'],
-  }),
-  mockTemplateUser({
-    login: 'gooduser2',
-    name: 'Alexa',
-    permissions: ['issueadmin', 'user'],
-  }),
-  mockTemplateUser({
-    name: 'Siri',
-    login: 'gooduser3',
-  }),
-  mockTemplateUser({
-    login: 'gooduser4',
-    name: 'Cool',
-    permissions: ['user'],
-  }),
-  mockTemplateUser({
-    name: 'White',
-    login: 'baduser1',
-  }),
-  mockTemplateUser({
-    name: 'Green',
-    login: 'baduser2',
-  }),
-];
-
-const templateGroups = [
-  mockTemplateGroup(),
-  mockTemplateGroup({ id: 'admins', name: 'admins', permissions: [] }),
-];
-
-const PAGE_SIZE = 5;
-const MIN_QUERY_LENGTH = 3;
-const DEFAULT_PAGE = 1;
-
-jest.mock('../permissions');
-
-export default class PermissionTemplateServiceMock {
-  permissionTemplates: PermissionTemplate[] = [];
-  isAllowedPermissionChange = true;
-
-  constructor() {
-    this.permissionTemplates = cloneDeep(defaultPermissionTemplates);
-    (getPermissionTemplates as jest.Mock).mockImplementation(this.handleGetPermissionTemplates);
-    (bulkApplyTemplate as jest.Mock).mockImplementation(this.handleBulkApplyTemplate);
-    (getPermissionTemplateUsers as jest.Mock).mockImplementation(
-      this.handleGetPermissionTemplateUsers
-    );
-    (getPermissionTemplateGroups as jest.Mock).mockImplementation(
-      this.handleGetPermissionTemplateGroups
-    );
-    (addProjectCreatorToTemplate as jest.Mock).mockImplementation(this.handlePermissionChange);
-    (removeProjectCreatorFromTemplate as jest.Mock).mockImplementation(this.handlePermissionChange);
-    (grantTemplatePermissionToGroup as jest.Mock).mockImplementation(this.handlePermissionChange);
-    (revokeTemplatePermissionFromGroup as jest.Mock).mockImplementation(
-      this.handlePermissionChange
-    );
-    (grantTemplatePermissionToUser as jest.Mock).mockImplementation(this.handlePermissionChange);
-    (revokeTemplatePermissionFromUser as jest.Mock).mockImplementation(this.handlePermissionChange);
-  }
-
-  handleGetPermissionTemplates = () => {
-    return this.reply({ permissionTemplates: this.permissionTemplates });
-  };
-
-  handleBulkApplyTemplate = (params: BaseSearchProjectsParameters) => {
-    if (
-      params.projects &&
-      params.projects.split(',').length > MAX_PROJECTS_TO_APPLY_PERMISSION_TEMPLATE
-    ) {
-      const response = new Response(
-        JSON.stringify({ errors: [{ msg: 'bulk apply permission template error message' }] })
-      );
-      return Promise.reject(response);
-    }
-
-    return Promise.resolve();
-  };
-
-  handleGetPermissionTemplateUsers = (data: { q?: string | null; p?: number; ps?: number }) => {
-    const { ps = PAGE_SIZE, p = DEFAULT_PAGE, q } = data;
-
-    const users =
-      q && q.length >= MIN_QUERY_LENGTH
-        ? templateUsers.filter((user) =>
-            [user.login, user.name].some((key) => key.toLowerCase().includes(q.toLowerCase()))
-          )
-        : templateUsers;
-
-    const usersChunks = chunk(users, ps);
-
-    return this.reply({
-      paging: { pageSize: ps, total: users.length, pageIndex: p },
-      users: usersChunks[p - 1] ?? [],
-    });
-  };
-
-  handleGetPermissionTemplateGroups = (data: {
-    templateId: string;
-    q?: string | null;
-    permission?: string;
-    p?: number;
-    ps?: number;
-  }) => {
-    const { ps = PAGE_SIZE, p = DEFAULT_PAGE, q } = data;
-
-    const groups =
-      q && q.length >= MIN_QUERY_LENGTH
-        ? templateGroups.filter((group) => group.name.toLowerCase().includes(q.toLowerCase()))
-        : templateGroups;
-
-    const groupsChunks = chunk(groups, ps);
-
-    return this.reply({
-      paging: { pageSize: ps, total: groups.length, pageIndex: p },
-      groups: groupsChunks[p - 1] ?? [],
-    });
-  };
-
-  handlePermissionChange = () => {
-    return this.isAllowedPermissionChange ? Promise.resolve() : Promise.reject();
-  };
-
-  updatePermissionChangeAllowance = (val: boolean) => {
-    this.isAllowedPermissionChange = val;
-  };
-
-  reset = () => {
-    this.permissionTemplates = cloneDeep(defaultPermissionTemplates);
-    this.updatePermissionChangeAllowance(true);
-  };
-
-  reply<T>(response: T): Promise<T> {
-    return Promise.resolve(cloneDeep(response));
-  }
-}
diff --git a/server/sonar-web/src/main/js/api/mocks/PermissionsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/PermissionsServiceMock.ts
new file mode 100644 (file)
index 0000000..96531f5
--- /dev/null
@@ -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));
+  }
+}
index 4247153957fb6309c4ce44767dd7f511ecc822b8..8e65bd6ead781baffecb1baeaa892b9287705b8b 100644 (file)
  */
 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);
 }
 
index b8eefc3a041bf09a18011478d8b257635cc1bcc5..27c68dc4db1e05761c9c0104ae8dd97ebbdef5d1 100644 (file)
@@ -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,
   });
index 54ee4417b5d05d330f416c5685cc54a19f596cab..c6150027ce4060c9d4b523eb56d5e317fec37c7e 100644 (file)
@@ -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');
index 8e8a84e3eefe9a16dc0da9c6d0ed12c282e613ba..796f3e6274d2a83c71886c28ba8d2fed887683fd 100644 (file)
@@ -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}
index ba80b6a6f783eebe9ac6ec51fcf35e01e571b1d8..52e7da56855d62e571d3418b163a88a085ec4e4a 100644 (file)
 import userEvent from '@testing-library/user-event';
 import React from 'react';
 import { byRole } from 'testing-library-selector';
-import PermissionTemplateServiceMock from '../../../../api/mocks/PermissionTemplateServiceMock';
+import PermissionsServiceMock from '../../../../api/mocks/PermissionsServiceMock';
 import { mockAppState } from '../../../../helpers/testMocks';
 import { renderApp } from '../../../../helpers/testReactTestingUtils';
+import { ComponentQualifier } from '../../../../types/component';
+import { Permissions } from '../../../../types/permissions';
 import PermissionTemplatesApp from '../PermissionTemplatesApp';
 
-const serviceMock = new PermissionTemplateServiceMock();
+const serviceMock = new PermissionsServiceMock();
 
 beforeEach(() => {
   serviceMock.reset();
@@ -33,37 +35,11 @@ beforeEach(() => {
 
 const ui = {
   templateLink1: byRole('link', { name: 'Permission Template 1' }),
-  adminUserBrowseCheckboxChecked: byRole('checkbox', {
-    name: `checked permission 'projects_role.user' for user 'Admin Admin'`,
-  }),
-  adminUserBrowseCheckboxUnchecked: byRole('checkbox', {
-    name: `unchecked permission 'projects_role.user' for user 'Admin Admin'`,
-  }),
-  adminUserAdministerCheckboxChecked: byRole('checkbox', {
-    name: `checked permission 'projects_role.admin' for user 'Admin Admin'`,
-  }),
-  adminUserAdministerCheckboxUnchecked: byRole('checkbox', {
-    name: `unchecked permission 'projects_role.admin' for user 'Admin Admin'`,
-  }),
-
-  anyoneGroupBrowseCheckboxChecked: byRole('checkbox', {
-    name: `checked permission 'projects_role.user' for group 'Anyone'`,
-  }),
-  anyoneGroupBrowseCheckboxUnchecked: byRole('checkbox', {
-    name: `unchecked permission 'projects_role.user' for group 'Anyone'`,
-  }),
-
-  anyoneGroupCodeviewCheckboxChecked: byRole('checkbox', {
-    name: `checked permission 'projects_role.codeviewer' for group 'Anyone'`,
-  }),
-  anyoneGroupCodeviewCheckboxUnchecked: byRole('checkbox', {
-    name: `unchecked permission 'projects_role.codeviewer' for group 'Anyone'`,
-  }),
-
+  permissionCheckbox: (target: string, permission: Permissions) =>
+    byRole('checkbox', {
+      name: `permission.assign_x_to_y.projects_role.${permission}.${target}`,
+    }),
   showMoreButton: byRole('button', { name: 'show_more' }),
-  whiteUserBrowseCheckbox: byRole('checkbox', {
-    name: `unchecked permission 'projects_role.user' for user 'White'`,
-  }),
 };
 
 it('grants/revokes permission from users or groups', async () => {
@@ -73,36 +49,33 @@ it('grants/revokes permission from users or groups', async () => {
   await user.click(await ui.templateLink1.find());
 
   // User
-  expect(ui.adminUserBrowseCheckboxUnchecked.get()).not.toBeChecked();
-  await user.click(ui.adminUserBrowseCheckboxUnchecked.get());
-  expect(ui.adminUserBrowseCheckboxChecked.get()).toBeChecked();
+  expect(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get()).not.toBeChecked();
+  await user.click(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get());
+  expect(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get()).toBeChecked();
 
-  expect(ui.adminUserAdministerCheckboxChecked.get()).toBeChecked();
-  await user.click(ui.adminUserAdministerCheckboxChecked.get());
-  expect(ui.adminUserAdministerCheckboxUnchecked.get()).not.toBeChecked();
+  expect(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get()).toBeChecked();
+  await user.click(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get());
+  expect(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get()).not.toBeChecked();
 
   // Group
-  expect(ui.anyoneGroupBrowseCheckboxUnchecked.get()).not.toBeChecked();
-  await user.click(ui.anyoneGroupBrowseCheckboxUnchecked.get());
-  expect(ui.anyoneGroupBrowseCheckboxChecked.get()).toBeChecked();
+  expect(ui.permissionCheckbox('Anyone', Permissions.Browse).get()).not.toBeChecked();
+  await user.click(ui.permissionCheckbox('Anyone', Permissions.Browse).get());
+  expect(ui.permissionCheckbox('Anyone', Permissions.Browse).get()).toBeChecked();
 
-  expect(ui.anyoneGroupCodeviewCheckboxChecked.get()).toBeChecked();
-  await user.click(ui.anyoneGroupCodeviewCheckboxChecked.get());
-  expect(ui.anyoneGroupCodeviewCheckboxUnchecked.get()).not.toBeChecked();
+  expect(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get()).toBeChecked();
+  await user.click(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get());
+  expect(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get()).not.toBeChecked();
 
   // Handles error on permission change
   serviceMock.updatePermissionChangeAllowance(false);
-  await user.click(ui.adminUserBrowseCheckboxChecked.get());
-  expect(ui.adminUserBrowseCheckboxChecked.get()).toBeChecked();
-
-  await user.click(ui.anyoneGroupCodeviewCheckboxUnchecked.get());
-  expect(ui.anyoneGroupCodeviewCheckboxUnchecked.get()).not.toBeChecked();
+  await user.click(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get());
+  expect(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get()).toBeChecked();
 
-  await user.click(ui.adminUserBrowseCheckboxChecked.get());
-  expect(ui.adminUserBrowseCheckboxChecked.get()).toBeChecked();
+  await user.click(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get());
+  expect(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get()).not.toBeChecked();
 
-  await user.click(ui.adminUserAdministerCheckboxUnchecked.get());
-  expect(ui.adminUserAdministerCheckboxUnchecked.get()).not.toBeChecked();
+  await user.click(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get());
+  expect(ui.permissionCheckbox('Admin Admin', Permissions.Admin).get()).not.toBeChecked();
 });
 
 it('loads more items on Show More', async () => {
@@ -111,13 +84,13 @@ it('loads more items on Show More', async () => {
 
   await user.click(await ui.templateLink1.find());
 
-  expect(ui.whiteUserBrowseCheckbox.query()).not.toBeInTheDocument();
+  expect(ui.permissionCheckbox('White', Permissions.Browse).query()).not.toBeInTheDocument();
   await user.click(ui.showMoreButton.get());
-  expect(ui.whiteUserBrowseCheckbox.get()).toBeInTheDocument();
+  expect(ui.permissionCheckbox('White', Permissions.Browse).get()).toBeInTheDocument();
 });
 
 function renderPermissionTemplatesApp() {
   renderApp('admin/permission_templates', <PermissionTemplatesApp />, {
-    appState: mockAppState({ qualifiers: ['TRK'] }),
+    appState: mockAppState({ qualifiers: [ComponentQualifier.Project] }),
   });
 }
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx
deleted file mode 100644 (file)
index 11e31ff..0000000
+++ /dev/null
@@ -1,408 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { without } from 'lodash';
-import * as React from 'react';
-import { Helmet } from 'react-helmet-async';
-import * as api from '../../../../api/permissions';
-import withComponentContext from '../../../../app/components/componentContext/withComponentContext';
-import VisibilitySelector from '../../../../components/common/VisibilitySelector';
-import { translate } from '../../../../helpers/l10n';
-import { Component, Paging, PermissionGroup, PermissionUser } from '../../../../types/types';
-import AllHoldersList from '../../shared/components/AllHoldersList';
-import { FilterOption } from '../../shared/components/SearchForm';
-import '../../styles.css';
-import { convertToPermissionDefinitions, PERMISSIONS_ORDER_BY_QUALIFIER } from '../../utils';
-import PageHeader from './PageHeader';
-import PublicProjectDisclaimer from './PublicProjectDisclaimer';
-
-interface Props {
-  component: Component;
-  onComponentChange: (changes: Partial<Component>) => void;
-}
-
-interface State {
-  disclaimer: boolean;
-  filter: FilterOption;
-  groups: PermissionGroup[];
-  groupsPaging?: Paging;
-  loading: boolean;
-  query: string;
-  selectedPermission?: string;
-  users: PermissionUser[];
-  usersPaging?: Paging;
-}
-
-export class App extends React.PureComponent<Props, State> {
-  mounted = false;
-
-  constructor(props: Props) {
-    super(props);
-    this.state = {
-      disclaimer: false,
-      filter: 'all',
-      groups: [],
-      loading: true,
-      query: '',
-      users: [],
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    this.loadHolders();
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  stopLoading = () => {
-    if (this.mounted) {
-      this.setState({ loading: false });
-    }
-  };
-
-  loadUsersAndGroups = (userPage?: number, groupsPage?: number) => {
-    const { component } = this.props;
-    const { filter, query, selectedPermission } = this.state;
-
-    const getUsers: Promise<{ paging?: Paging; users: PermissionUser[] }> =
-      filter !== 'groups'
-        ? api.getPermissionsUsersForComponent({
-            projectKey: component.key,
-            q: query || undefined,
-            permission: selectedPermission,
-            p: userPage,
-          })
-        : Promise.resolve({ paging: undefined, users: [] });
-
-    const getGroups: Promise<{ paging?: Paging; groups: PermissionGroup[] }> =
-      filter !== 'users'
-        ? api.getPermissionsGroupsForComponent({
-            projectKey: component.key,
-            q: query || undefined,
-            permission: selectedPermission,
-            p: groupsPage,
-          })
-        : Promise.resolve({ paging: undefined, groups: [] });
-
-    return Promise.all([getUsers, getGroups]);
-  };
-
-  loadHolders = () => {
-    this.setState({ loading: true });
-    return this.loadUsersAndGroups().then(([usersResponse, groupsResponse]) => {
-      if (this.mounted) {
-        this.setState({
-          groups: groupsResponse.groups,
-          groupsPaging: groupsResponse.paging,
-          loading: false,
-          users: usersResponse.users,
-          usersPaging: usersResponse.paging,
-        });
-      }
-    }, this.stopLoading);
-  };
-
-  onLoadMore = () => {
-    const { usersPaging, groupsPaging } = this.state;
-    this.setState({ loading: true });
-    return this.loadUsersAndGroups(
-      usersPaging ? usersPaging.pageIndex + 1 : 1,
-      groupsPaging ? groupsPaging.pageIndex + 1 : 1
-    ).then(([usersResponse, groupsResponse]) => {
-      if (this.mounted) {
-        this.setState(({ groups, users }) => ({
-          groups: [...groups, ...groupsResponse.groups],
-          groupsPaging: groupsResponse.paging,
-          loading: false,
-          users: [...users, ...usersResponse.users],
-          usersPaging: usersResponse.paging,
-        }));
-      }
-    }, this.stopLoading);
-  };
-
-  handleFilterChange = (filter: FilterOption) => {
-    if (this.mounted) {
-      this.setState({ filter }, this.loadHolders);
-    }
-  };
-
-  handleQueryChange = (query: string) => {
-    if (this.mounted) {
-      this.setState({ query }, this.loadHolders);
-    }
-  };
-
-  handlePermissionSelect = (selectedPermission?: string) => {
-    if (this.mounted) {
-      this.setState(
-        (state: State) => ({
-          selectedPermission:
-            state.selectedPermission === selectedPermission ? undefined : selectedPermission,
-        }),
-        this.loadHolders
-      );
-    }
-  };
-
-  addPermissionToGroup = (group: string, permission: string) => {
-    return this.state.groups.map((candidate) =>
-      candidate.name === group
-        ? { ...candidate, permissions: [...candidate.permissions, permission] }
-        : candidate
-    );
-  };
-
-  addPermissionToUser = (user: string, permission: string) => {
-    return this.state.users.map((candidate) =>
-      candidate.login === user
-        ? { ...candidate, permissions: [...candidate.permissions, permission] }
-        : candidate
-    );
-  };
-
-  removePermissionFromGroup = (group: string, permission: string) => {
-    return this.state.groups.map((candidate) =>
-      candidate.name === group
-        ? { ...candidate, permissions: without(candidate.permissions, permission) }
-        : candidate
-    );
-  };
-
-  removePermissionFromUser = (user: string, permission: string) => {
-    return this.state.users.map((candidate) =>
-      candidate.login === user
-        ? { ...candidate, permissions: without(candidate.permissions, permission) }
-        : candidate
-    );
-  };
-
-  grantPermissionToGroup = (group: string, permission: string) => {
-    if (this.mounted) {
-      this.setState({
-        loading: true,
-        groups: this.addPermissionToGroup(group, permission),
-      });
-      return api
-        .grantPermissionToGroup({
-          projectKey: this.props.component.key,
-          groupName: group,
-          permission,
-        })
-        .then(this.stopLoading, () => {
-          if (this.mounted) {
-            this.setState({
-              loading: false,
-              groups: this.removePermissionFromGroup(group, permission),
-            });
-          }
-        });
-    }
-    return Promise.resolve();
-  };
-
-  grantPermissionToUser = (user: string, permission: string) => {
-    if (this.mounted) {
-      this.setState({
-        loading: true,
-        users: this.addPermissionToUser(user, permission),
-      });
-      return api
-        .grantPermissionToUser({
-          projectKey: this.props.component.key,
-          login: user,
-          permission,
-        })
-        .then(this.stopLoading, () => {
-          if (this.mounted) {
-            this.setState({
-              loading: false,
-              users: this.removePermissionFromUser(user, permission),
-            });
-          }
-        });
-    }
-    return Promise.resolve();
-  };
-
-  revokePermissionFromGroup = (group: string, permission: string) => {
-    if (this.mounted) {
-      this.setState({
-        loading: true,
-        groups: this.removePermissionFromGroup(group, permission),
-      });
-      return api
-        .revokePermissionFromGroup({
-          projectKey: this.props.component.key,
-          groupName: group,
-          permission,
-        })
-        .then(this.stopLoading, () => {
-          if (this.mounted) {
-            this.setState({
-              loading: false,
-              groups: this.addPermissionToGroup(group, permission),
-            });
-          }
-        });
-    }
-    return Promise.resolve();
-  };
-
-  revokePermissionFromUser = (user: string, permission: string) => {
-    if (this.mounted) {
-      this.setState({
-        loading: true,
-        users: this.removePermissionFromUser(user, permission),
-      });
-      return api
-        .revokePermissionFromUser({
-          projectKey: this.props.component.key,
-          login: user,
-          permission,
-        })
-        .then(this.stopLoading, () => {
-          if (this.mounted) {
-            this.setState({
-              loading: false,
-              users: this.addPermissionToUser(user, permission),
-            });
-          }
-        });
-    }
-    return Promise.resolve();
-  };
-
-  handleVisibilityChange = (visibility: string) => {
-    if (visibility === 'public') {
-      this.openDisclaimer();
-    } else {
-      this.turnProjectToPrivate();
-    }
-  };
-
-  turnProjectToPublic = () => {
-    this.props.onComponentChange({ visibility: 'public' });
-    api.changeProjectVisibility(this.props.component.key, 'public').then(
-      () => {
-        this.loadHolders();
-      },
-      () => {
-        this.props.onComponentChange({
-          visibility: 'private',
-        });
-      }
-    );
-  };
-
-  turnProjectToPrivate = () => {
-    this.props.onComponentChange({ visibility: 'private' });
-    api.changeProjectVisibility(this.props.component.key, 'private').then(
-      () => {
-        this.loadHolders();
-      },
-      () => {
-        this.props.onComponentChange({
-          visibility: 'public',
-        });
-      }
-    );
-  };
-
-  openDisclaimer = () => {
-    if (this.mounted) {
-      this.setState({ disclaimer: true });
-    }
-  };
-
-  closeDisclaimer = () => {
-    if (this.mounted) {
-      this.setState({ disclaimer: false });
-    }
-  };
-
-  render() {
-    const { component } = this.props;
-    const {
-      filter,
-      groups,
-      disclaimer,
-      loading,
-      selectedPermission,
-      query,
-      users,
-      usersPaging,
-      groupsPaging,
-    } = this.state;
-    const canTurnToPrivate =
-      component.configuration && component.configuration.canUpdateProjectVisibilityToPrivate;
-
-    let order = PERMISSIONS_ORDER_BY_QUALIFIER[component.qualifier];
-    if (component.visibility === 'public') {
-      order = without(order, 'user', 'codeviewer');
-    }
-    const permissions = convertToPermissionDefinitions(order, 'projects_role');
-
-    return (
-      <div className="page page-limited" id="project-permissions-page">
-        <Helmet defer={false} title={translate('permissions.page')} />
-
-        <PageHeader component={component} loadHolders={this.loadHolders} loading={loading} />
-        <div>
-          <VisibilitySelector
-            canTurnToPrivate={canTurnToPrivate}
-            className="big-spacer-top big-spacer-bottom"
-            onChange={this.handleVisibilityChange}
-            visibility={component.visibility}
-          />
-          {disclaimer && (
-            <PublicProjectDisclaimer
-              component={component}
-              onClose={this.closeDisclaimer}
-              onConfirm={this.turnProjectToPublic}
-            />
-          )}
-        </div>
-        <AllHoldersList
-          filter={filter}
-          grantPermissionToGroup={this.grantPermissionToGroup}
-          grantPermissionToUser={this.grantPermissionToUser}
-          groups={groups}
-          groupsPaging={groupsPaging}
-          onFilter={this.handleFilterChange}
-          onLoadMore={this.onLoadMore}
-          onSelectPermission={this.handlePermissionSelect}
-          onQuery={this.handleQueryChange}
-          query={query}
-          revokePermissionFromGroup={this.revokePermissionFromGroup}
-          revokePermissionFromUser={this.revokePermissionFromUser}
-          selectedPermission={selectedPermission}
-          users={users}
-          usersPaging={usersPaging}
-          permissions={permissions}
-        />
-      </div>
-    );
-  }
-}
-
-export default withComponentContext(App);
index 32f435b5bcb4567b12e0ab9aa170a8cf511ef55a..bc1d118871c8986a324b6037ec0b703e99b8c242 100644 (file)
@@ -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
     );
 
index 0816f07df61c9419b29040637d56090f1b382bde..a1d7c39f50c1a0d6ac5b9ef8cfcd966ff0cdd208 100644 (file)
@@ -19,8 +19,9 @@
  */
 import * as React from 'react';
 import { Button } from '../../../../components/controls/buttons';
+import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
 import { translate } from '../../../../helpers/l10n';
-import { isApplication, isPortfolioLike } from '../../../../types/component';
+import { ComponentQualifier, isApplication, isPortfolioLike } from '../../../../types/component';
 import { Component } from '../../../../types/types';
 import ApplyTemplate from './ApplyTemplate';
 
@@ -70,7 +71,7 @@ export default class PageHeader extends React.PureComponent<Props, State> {
     }
 
     const visibilityDescription =
-      component.qualifier === 'TRK' && component.visibility
+      component.qualifier === ComponentQualifier.Project && component.visibility
         ? translate('visibility', component.visibility, 'description', component.qualifier)
         : undefined;
 
@@ -78,7 +79,7 @@ export default class PageHeader extends React.PureComponent<Props, State> {
       <header className="page-header">
         <h1 className="page-title">{translate('permissions.page')}</h1>
 
-        {this.props.loading && <i className="spinner" />}
+        <DeferredSpinner loading={this.props.loading} />
 
         {canApplyPermissionTemplate && (
           <div className="page-actions">
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx
new file mode 100644 (file)
index 0000000..acefdf9
--- /dev/null
@@ -0,0 +1,387 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { without } from 'lodash';
+import * as React from 'react';
+import { Helmet } from 'react-helmet-async';
+import * as api from '../../../../api/permissions';
+import withComponentContext from '../../../../app/components/componentContext/withComponentContext';
+import VisibilitySelector from '../../../../components/common/VisibilitySelector';
+import { translate } from '../../../../helpers/l10n';
+import { Visibility } from '../../../../types/component';
+import { Permissions } from '../../../../types/permissions';
+import { Component, Paging, PermissionGroup, PermissionUser } from '../../../../types/types';
+import AllHoldersList from '../../shared/components/AllHoldersList';
+import { FilterOption } from '../../shared/components/SearchForm';
+import '../../styles.css';
+import { convertToPermissionDefinitions, PERMISSIONS_ORDER_BY_QUALIFIER } from '../../utils';
+import PageHeader from './PageHeader';
+import PublicProjectDisclaimer from './PublicProjectDisclaimer';
+
+interface Props {
+  component: Component;
+  onComponentChange: (changes: Partial<Component>) => void;
+}
+
+interface State {
+  disclaimer: boolean;
+  filter: FilterOption;
+  groups: PermissionGroup[];
+  groupsPaging?: Paging;
+  loading: boolean;
+  query: string;
+  selectedPermission?: string;
+  users: PermissionUser[];
+  usersPaging?: Paging;
+}
+
+export class PermissionsProjectApp extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      disclaimer: false,
+      filter: 'all',
+      groups: [],
+      loading: true,
+      query: '',
+      users: [],
+    };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    this.loadHolders();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  loadUsersAndGroups = (userPage?: number, groupsPage?: number) => {
+    const { component } = this.props;
+    const { filter, query, selectedPermission } = this.state;
+
+    const getUsers: Promise<{ paging?: Paging; users: PermissionUser[] }> =
+      filter !== 'groups'
+        ? api.getPermissionsUsersForComponent({
+            projectKey: component.key,
+            q: query || undefined,
+            permission: selectedPermission,
+            p: userPage,
+          })
+        : Promise.resolve({ paging: undefined, users: [] });
+
+    const getGroups: Promise<{ paging?: Paging; groups: PermissionGroup[] }> =
+      filter !== 'users'
+        ? api.getPermissionsGroupsForComponent({
+            projectKey: component.key,
+            q: query || undefined,
+            permission: selectedPermission,
+            p: groupsPage,
+          })
+        : Promise.resolve({ paging: undefined, groups: [] });
+
+    return Promise.all([getUsers, getGroups]);
+  };
+
+  loadHolders = () => {
+    this.setState({ loading: true });
+    return this.loadUsersAndGroups().then(([usersResponse, groupsResponse]) => {
+      if (this.mounted) {
+        this.setState({
+          groups: groupsResponse.groups,
+          groupsPaging: groupsResponse.paging,
+          loading: false,
+          users: usersResponse.users,
+          usersPaging: usersResponse.paging,
+        });
+      }
+    }, this.stopLoading);
+  };
+
+  onLoadMore = () => {
+    const { usersPaging, groupsPaging } = this.state;
+    this.setState({ loading: true });
+    return this.loadUsersAndGroups(
+      usersPaging ? usersPaging.pageIndex + 1 : 1,
+      groupsPaging ? groupsPaging.pageIndex + 1 : 1
+    ).then(([usersResponse, groupsResponse]) => {
+      if (this.mounted) {
+        this.setState(({ groups, users }) => ({
+          groups: [...groups, ...groupsResponse.groups],
+          groupsPaging: groupsResponse.paging,
+          loading: false,
+          users: [...users, ...usersResponse.users],
+          usersPaging: usersResponse.paging,
+        }));
+      }
+    }, this.stopLoading);
+  };
+
+  handleFilterChange = (filter: FilterOption) => {
+    if (this.mounted) {
+      this.setState({ filter }, this.loadHolders);
+    }
+  };
+
+  handleQueryChange = (query: string) => {
+    if (this.mounted) {
+      this.setState({ query }, this.loadHolders);
+    }
+  };
+
+  handlePermissionSelect = (selectedPermission?: string) => {
+    if (this.mounted) {
+      this.setState(
+        (state: State) => ({
+          selectedPermission:
+            state.selectedPermission === selectedPermission ? undefined : selectedPermission,
+        }),
+        this.loadHolders
+      );
+    }
+  };
+
+  addPermissionToGroup = (group: string, permission: string) => {
+    return this.state.groups.map((candidate) =>
+      candidate.name === group
+        ? { ...candidate, permissions: [...candidate.permissions, permission] }
+        : candidate
+    );
+  };
+
+  addPermissionToUser = (user: string, permission: string) => {
+    return this.state.users.map((candidate) =>
+      candidate.login === user
+        ? { ...candidate, permissions: [...candidate.permissions, permission] }
+        : candidate
+    );
+  };
+
+  removePermissionFromGroup = (group: string, permission: string) => {
+    return this.state.groups.map((candidate) =>
+      candidate.name === group
+        ? { ...candidate, permissions: without(candidate.permissions, permission) }
+        : candidate
+    );
+  };
+
+  removePermissionFromUser = (user: string, permission: string) => {
+    return this.state.users.map((candidate) =>
+      candidate.login === user
+        ? { ...candidate, permissions: without(candidate.permissions, permission) }
+        : candidate
+    );
+  };
+
+  grantPermissionToGroup = (group: string, permission: string) => {
+    if (this.mounted) {
+      this.setState({ loading: true });
+      return api
+        .grantPermissionToGroup({
+          projectKey: this.props.component.key,
+          groupName: group,
+          permission,
+        })
+        .then(() => {
+          if (this.mounted) {
+            this.setState({
+              loading: false,
+              groups: this.addPermissionToGroup(group, permission),
+            });
+          }
+        }, this.stopLoading);
+    }
+    return Promise.resolve();
+  };
+
+  grantPermissionToUser = (user: string, permission: string) => {
+    if (this.mounted) {
+      this.setState({ loading: true });
+      return api
+        .grantPermissionToUser({
+          projectKey: this.props.component.key,
+          login: user,
+          permission,
+        })
+        .then(() => {
+          if (this.mounted) {
+            this.setState({
+              loading: false,
+              users: this.addPermissionToUser(user, permission),
+            });
+          }
+        }, this.stopLoading);
+    }
+    return Promise.resolve();
+  };
+
+  revokePermissionFromGroup = (group: string, permission: string) => {
+    if (this.mounted) {
+      this.setState({ loading: true });
+      return api
+        .revokePermissionFromGroup({
+          projectKey: this.props.component.key,
+          groupName: group,
+          permission,
+        })
+        .then(() => {
+          if (this.mounted) {
+            this.setState({
+              loading: false,
+              groups: this.removePermissionFromGroup(group, permission),
+            });
+          }
+        }, this.stopLoading);
+    }
+    return Promise.resolve();
+  };
+
+  revokePermissionFromUser = (user: string, permission: string) => {
+    if (this.mounted) {
+      this.setState({ loading: true });
+      return api
+        .revokePermissionFromUser({
+          projectKey: this.props.component.key,
+          login: user,
+          permission,
+        })
+        .then(() => {
+          if (this.mounted) {
+            this.setState({
+              loading: false,
+              users: this.removePermissionFromUser(user, permission),
+            });
+          }
+        }, this.stopLoading);
+    }
+    return Promise.resolve();
+  };
+
+  handleVisibilityChange = (visibility: string) => {
+    if (visibility === Visibility.Public) {
+      this.openDisclaimer();
+    } else {
+      this.turnProjectToPrivate();
+    }
+  };
+
+  turnProjectToPublic = () => {
+    this.setState({ loading: true });
+    return api.changeProjectVisibility(this.props.component.key, Visibility.Public).then(() => {
+      this.props.onComponentChange({ visibility: Visibility.Public });
+      this.loadHolders();
+    });
+  };
+
+  turnProjectToPrivate = () => {
+    this.setState({ loading: true });
+    return api.changeProjectVisibility(this.props.component.key, Visibility.Private).then(() => {
+      this.props.onComponentChange({ visibility: Visibility.Private });
+      this.loadHolders();
+    });
+  };
+
+  openDisclaimer = () => {
+    if (this.mounted) {
+      this.setState({ disclaimer: true });
+    }
+  };
+
+  closeDisclaimer = () => {
+    if (this.mounted) {
+      this.setState({ disclaimer: false });
+    }
+  };
+
+  stopLoading = () => {
+    if (this.mounted) {
+      this.setState({ loading: false });
+    }
+  };
+
+  render() {
+    const { component } = this.props;
+    const {
+      filter,
+      groups,
+      disclaimer,
+      loading,
+      selectedPermission,
+      query,
+      users,
+      usersPaging,
+      groupsPaging,
+    } = this.state;
+    const canTurnToPrivate =
+      component.configuration && component.configuration.canUpdateProjectVisibilityToPrivate;
+
+    let order = PERMISSIONS_ORDER_BY_QUALIFIER[component.qualifier];
+    if (component.visibility === Visibility.Public) {
+      order = without(order, Permissions.Browse, Permissions.CodeViewer);
+    }
+    const permissions = convertToPermissionDefinitions(order, 'projects_role');
+
+    return (
+      <div className="page page-limited" id="project-permissions-page">
+        <Helmet defer={false} title={translate('permissions.page')} />
+
+        <PageHeader component={component} loadHolders={this.loadHolders} loading={loading} />
+        <div>
+          <VisibilitySelector
+            canTurnToPrivate={canTurnToPrivate}
+            className="big-spacer-top big-spacer-bottom"
+            onChange={this.handleVisibilityChange}
+            loading={loading}
+            visibility={component.visibility}
+          />
+          {disclaimer && (
+            <PublicProjectDisclaimer
+              component={component}
+              onClose={this.closeDisclaimer}
+              onConfirm={this.turnProjectToPublic}
+            />
+          )}
+        </div>
+        <AllHoldersList
+          filter={filter}
+          grantPermissionToGroup={this.grantPermissionToGroup}
+          grantPermissionToUser={this.grantPermissionToUser}
+          groups={groups}
+          groupsPaging={groupsPaging}
+          onFilter={this.handleFilterChange}
+          onLoadMore={this.onLoadMore}
+          onSelectPermission={this.handlePermissionSelect}
+          onQuery={this.handleQueryChange}
+          query={query}
+          revokePermissionFromGroup={this.revokePermissionFromGroup}
+          revokePermissionFromUser={this.revokePermissionFromUser}
+          selectedPermission={selectedPermission}
+          users={users}
+          usersPaging={usersPaging}
+          permissions={permissions}
+        />
+      </div>
+    );
+  }
+}
+
+export default withComponentContext(PermissionsProjectApp);
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx
deleted file mode 100644 (file)
index dda3c36..0000000
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import {
-  grantPermissionToGroup,
-  grantPermissionToUser,
-  revokePermissionFromGroup,
-  revokePermissionFromUser,
-} from '../../../../../api/permissions';
-import { mockComponent } from '../../../../../helpers/mocks/component';
-import { waitAndUpdate } from '../../../../../helpers/testUtils';
-import { App } from '../App';
-
-jest.mock('../../../../../api/permissions', () => ({
-  getPermissionsGroupsForComponent: jest.fn().mockResolvedValue({
-    paging: { pageIndex: 1, pageSize: 100, total: 2 },
-    groups: [
-      {
-        id: '1',
-        name: 'SonarSource',
-        description: 'SonarSource team',
-        permissions: ['admin', 'codeviewer', 'issueadmin'],
-      },
-      { name: 'Anyone', permissions: [] },
-    ],
-  }),
-  getPermissionsUsersForComponent: jest.fn().mockResolvedValue({
-    paging: { pageIndex: 1, pageSize: 100, total: 3 },
-    users: [
-      {
-        avatar: 'admin-avatar',
-        email: 'admin@gmail.com',
-        login: 'admin',
-        name: 'Admin Admin',
-        permissions: ['admin'],
-      },
-      {
-        avatar: 'user-avatar-1',
-        email: 'user1@gmail.com',
-        login: 'user1',
-        name: 'User Number 1',
-        permissions: [],
-      },
-      {
-        avatar: 'user-avatar-2',
-        email: 'user2@gmail.com',
-        login: 'user2',
-        name: 'User Number 2',
-        permissions: [],
-      },
-    ],
-  }),
-  grantPermissionToGroup: jest.fn().mockResolvedValue({}),
-  grantPermissionToUser: jest.fn().mockResolvedValue({}),
-  revokePermissionFromGroup: jest.fn().mockResolvedValue({}),
-  revokePermissionFromUser: jest.fn().mockResolvedValue({}),
-}));
-
-beforeEach(() => {
-  jest.clearAllMocks();
-});
-
-it('should render correctly', async () => {
-  const wrapper = shallowRender();
-  expect(wrapper).toMatchSnapshot();
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-});
-
-describe('should manage state correctly', () => {
-  it('should handle permission select', async () => {
-    const wrapper = shallowRender();
-    await waitAndUpdate(wrapper);
-    const instance = wrapper.instance();
-    instance.handlePermissionSelect('foo');
-    expect(wrapper.state('selectedPermission')).toBe('foo');
-    instance.handlePermissionSelect('foo');
-    expect(wrapper.state('selectedPermission')).toBeUndefined();
-  });
-
-  it('should add and remove permission to a group', async () => {
-    const wrapper = shallowRender();
-    await waitAndUpdate(wrapper);
-    const instance = wrapper.instance();
-    const apiPayload = {
-      projectKey: 'my-project',
-      groupName: 'SonarSource',
-      permission: 'foo',
-    };
-
-    instance.grantPermissionToGroup('SonarSource', 'foo');
-    const groupState = wrapper.state('groups');
-    expect(groupState[0].permissions).toHaveLength(4);
-    expect(groupState[0].permissions).toContain('foo');
-    await waitAndUpdate(wrapper);
-    expect(grantPermissionToGroup).toHaveBeenCalledWith(apiPayload);
-    expect(wrapper.state('groups')).toBe(groupState);
-
-    (grantPermissionToGroup as jest.Mock).mockRejectedValueOnce({});
-    instance.grantPermissionToGroup('SonarSource', 'bar');
-    expect(wrapper.state('groups')[0].permissions).toHaveLength(5);
-    expect(wrapper.state('groups')[0].permissions).toContain('bar');
-    await waitAndUpdate(wrapper);
-    expect(wrapper.state('groups')[0].permissions).toHaveLength(4);
-    expect(wrapper.state('groups')[0].permissions).not.toContain('bar');
-
-    instance.revokePermissionFromGroup('SonarSource', 'foo');
-    expect(wrapper.state('groups')[0].permissions).toHaveLength(3);
-    expect(wrapper.state('groups')[0].permissions).not.toContain('foo');
-    await waitAndUpdate(wrapper);
-    expect(revokePermissionFromGroup).toHaveBeenCalledWith(apiPayload);
-  });
-
-  it('should add and remove permission to a user', async () => {
-    const wrapper = shallowRender();
-    await waitAndUpdate(wrapper);
-    const instance = wrapper.instance();
-    const apiPayload = {
-      projectKey: 'my-project',
-      login: 'user1',
-      permission: 'foo',
-    };
-
-    instance.grantPermissionToUser('user1', 'foo');
-    expect(wrapper.state('users')[1].permissions).toHaveLength(1);
-    expect(wrapper.state('users')[1].permissions).toContain('foo');
-    await waitAndUpdate(wrapper);
-    expect(grantPermissionToUser).toHaveBeenCalledWith(apiPayload);
-
-    instance.revokePermissionFromUser('user1', 'foo');
-    expect(wrapper.state('users')[1].permissions).toHaveLength(0);
-    await waitAndUpdate(wrapper);
-    expect(revokePermissionFromUser).toHaveBeenCalledWith(apiPayload);
-  });
-});
-
-function shallowRender(props: Partial<App['props']> = {}) {
-  return shallow<App>(<App component={mockComponent()} onComponentChange={jest.fn()} {...props} />);
-}
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/ApplyTemplate-test.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/ApplyTemplate-test.tsx
deleted file mode 100644 (file)
index a6b6c3a..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { waitAndUpdate } from '../../../../../helpers/testUtils';
-import ApplyTemplate from '../ApplyTemplate';
-
-jest.mock('../../../../../api/permissions', () => ({
-  getPermissionTemplates: jest.fn().mockResolvedValue({
-    permissionTemplates: [
-      {
-        id: 'tmp1',
-        name: 'SonarSource projects',
-        createdAt: '2015-11-27T15:20:32+0100',
-        permissions: [
-          { key: 'admin', usersCount: 0, groupsCount: 3 },
-          { key: 'codeviewer', usersCount: 0, groupsCount: 6 },
-        ],
-      },
-    ],
-    defaultTemplates: [{ templateId: 'tmp1', qualifier: 'TRK' }],
-    permissions: [
-      { key: 'admin', name: 'Administer', description: 'Administer access' },
-      { key: 'codeviewer', name: 'See Source Code', description: 'View code' },
-    ],
-  }),
-}));
-
-it('render correctly', async () => {
-  const wrapper = shallow(
-    <ApplyTemplate onClose={jest.fn()} project={{ key: 'foo', name: 'Foo' }} />
-  );
-  expect(wrapper).toMatchSnapshot();
-  await waitAndUpdate(wrapper);
-  expect(wrapper.dive()).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx
new file mode 100644 (file)
index 0000000..0e8d81a
--- /dev/null
@@ -0,0 +1,287 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { act, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
+import * as React from 'react';
+import selectEvent from 'react-select-event';
+import { byLabelText, byRole, byText } from 'testing-library-selector';
+import PermissionsServiceMock from '../../../../../api/mocks/PermissionsServiceMock';
+import { mockComponent } from '../../../../../helpers/mocks/component';
+import { renderApp } from '../../../../../helpers/testReactTestingUtils';
+import { ComponentQualifier, Visibility } from '../../../../../types/component';
+import { Permissions } from '../../../../../types/permissions';
+import { Component } from '../../../../../types/types';
+import { PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE, PERMISSIONS_ORDER_FOR_VIEW } from '../../../utils';
+import { PermissionsProjectApp } from '../PermissionsProjectApp';
+
+let serviceMock: PermissionsServiceMock;
+beforeAll(() => {
+  serviceMock = new PermissionsServiceMock();
+});
+
+afterEach(() => {
+  serviceMock.reset();
+});
+
+describe('rendering', () => {
+  it.each([
+    [ComponentQualifier.Project, 'roles.page.description2', PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE],
+    [ComponentQualifier.Portfolio, 'roles.page.description_portfolio', PERMISSIONS_ORDER_FOR_VIEW],
+    [
+      ComponentQualifier.Application,
+      'roles.page.description_application',
+      PERMISSIONS_ORDER_FOR_VIEW,
+    ],
+  ])('should render correctly for %s', async (qualifier, description, permissions) => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionsProjectApp({ qualifier, visibility: Visibility.Private });
+    await ui.appLoaded();
+
+    expect(screen.getByText(description)).toBeInTheDocument();
+    permissions.forEach((permission) => {
+      expect(ui.permissionCheckbox('johndoe', permission).get()).toBeInTheDocument();
+    });
+  });
+});
+
+describe('filtering', () => {
+  it('should allow to filter permission holders', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionsProjectApp();
+    await ui.appLoaded();
+
+    expect(screen.getByText('sonar-users')).toBeInTheDocument();
+    expect(screen.getByText('johndoe')).toBeInTheDocument();
+
+    await ui.showOnlyUsers();
+    expect(screen.queryByText('sonar-users')).not.toBeInTheDocument();
+    expect(screen.getByText('johndoe')).toBeInTheDocument();
+
+    await ui.showOnlyGroups();
+    expect(screen.getByText('sonar-users')).toBeInTheDocument();
+    expect(screen.queryByText('johndoe')).not.toBeInTheDocument();
+
+    await ui.showAll();
+    expect(screen.getByText('sonar-users')).toBeInTheDocument();
+    expect(screen.getByText('johndoe')).toBeInTheDocument();
+
+    await ui.searchFor('sonar-adm');
+    expect(screen.getByText('sonar-admins')).toBeInTheDocument();
+    expect(screen.queryByText('sonar-users')).not.toBeInTheDocument();
+    expect(screen.queryByText('johndoe')).not.toBeInTheDocument();
+
+    await ui.clearSearch();
+    expect(screen.getByText('sonar-users')).toBeInTheDocument();
+    expect(screen.getByText('johndoe')).toBeInTheDocument();
+  });
+
+  it('should allow to show only permission holders with a specific permission', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionsProjectApp();
+    await ui.appLoaded();
+
+    expect(screen.getAllByRole('row').length).toBe(7);
+    await ui.toggleFilterByPermission(Permissions.Admin);
+    expect(screen.getAllByRole('row').length).toBe(2);
+  });
+});
+
+describe('assigning/revoking permissions', () => {
+  it('should allow to apply a permission template', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionsProjectApp();
+    await ui.appLoaded();
+
+    await ui.openTemplateModal();
+    expect(ui.confirmApplyTemplateBtn.get()).toBeDisabled();
+    await ui.chooseTemplate('Permission Template 2');
+    expect(ui.templateSuccessfullyApplied.get()).toBeInTheDocument();
+    await ui.closeTemplateModal();
+    expect(ui.templateSuccessfullyApplied.query()).not.toBeInTheDocument();
+  });
+
+  it('should allow to turn a public project private (and vice-versa)', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionsProjectApp();
+    await ui.appLoaded();
+
+    expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
+    expect(
+      ui.permissionCheckbox('sonar-users', Permissions.Browse).query()
+    ).not.toBeInTheDocument();
+    await act(async () => {
+      await ui.turnProjectPrivate();
+    });
+    expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
+    expect(ui.permissionCheckbox('sonar-users', Permissions.Browse).get()).toBeInTheDocument();
+
+    await ui.turnProjectPublic();
+    expect(ui.makePublicDisclaimer.get()).toBeInTheDocument();
+    await act(async () => {
+      await ui.confirmTurnProjectPublic();
+    });
+    expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
+  });
+
+  it('should add and remove permissions to/from a group', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionsProjectApp();
+    await ui.appLoaded();
+
+    expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
+
+    await ui.togglePermission('sonar-users', Permissions.Admin);
+    await ui.appLoaded();
+    expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).toBeChecked();
+
+    await ui.togglePermission('sonar-users', Permissions.Admin);
+    await ui.appLoaded();
+    expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
+  });
+
+  it('should add and remove permissions to/from a user', async () => {
+    const user = userEvent.setup();
+    const ui = getPageObject(user);
+    renderPermissionsProjectApp();
+    await ui.appLoaded();
+
+    expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
+
+    await ui.togglePermission('johndoe', Permissions.Scan);
+    await ui.appLoaded();
+    expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).toBeChecked();
+
+    await ui.togglePermission('johndoe', Permissions.Scan);
+    await ui.appLoaded();
+    expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
+  });
+});
+
+function getPageObject(user: UserEvent) {
+  const ui = {
+    loading: byLabelText('loading'),
+    permissionCheckbox: (target: string, permission: Permissions) =>
+      byRole('checkbox', {
+        name: `permission.assign_x_to_y.projects_role.${permission}.${target}`,
+      }),
+    visibilityRadio: (visibility: Visibility) =>
+      byRole('radio', { name: `visibility.${visibility}` }),
+    makePublicDisclaimer: byText(
+      'projects_role.are_you_sure_to_turn_project_to_public.warning.TRK'
+    ),
+    confirmPublicBtn: byRole('button', { name: 'projects_role.turn_project_to_public.TRK' }),
+    openModalBtn: byRole('button', { name: 'projects_role.apply_template' }),
+    closeModalBtn: byRole('button', { name: 'close' }),
+    templateSelect: byRole('combobox', { name: /template/ }),
+    templateSuccessfullyApplied: byText('projects_role.apply_template.success'),
+    confirmApplyTemplateBtn: byRole('button', { name: 'apply' }),
+    tableHeaderFilter: (permission: Permissions) =>
+      byRole('link', { name: `projects_role.${permission}` }),
+    onlyUsersBtn: byRole('button', { name: 'users.page' }),
+    onlyGroupsBtn: byRole('button', { name: 'user_groups.page' }),
+    showAllBtn: byRole('button', { name: 'all' }),
+    searchInput: byRole('searchbox', { name: 'search.search_for_users_or_groups' }),
+  };
+
+  return {
+    ...ui,
+    async appLoaded() {
+      await waitFor(() => {
+        expect(ui.loading.query()).not.toBeInTheDocument();
+      });
+    },
+    async togglePermission(target: string, permission: Permissions) {
+      await user.click(ui.permissionCheckbox(target, permission).get());
+    },
+    async turnProjectPrivate() {
+      await user.click(ui.visibilityRadio(Visibility.Private).get());
+    },
+    async turnProjectPublic() {
+      await user.click(ui.visibilityRadio(Visibility.Public).get());
+    },
+    async confirmTurnProjectPublic() {
+      await user.click(ui.confirmPublicBtn.get());
+    },
+    async openTemplateModal() {
+      await user.click(ui.openModalBtn.get());
+    },
+    async closeTemplateModal() {
+      await user.click(ui.closeModalBtn.get());
+    },
+    async chooseTemplate(name: string) {
+      await selectEvent.select(ui.templateSelect.get(), [name]);
+      await user.click(ui.confirmApplyTemplateBtn.get());
+    },
+    async toggleFilterByPermission(permission: Permissions) {
+      await user.click(ui.tableHeaderFilter(permission).get());
+    },
+    async showOnlyUsers() {
+      await user.click(ui.onlyUsersBtn.get());
+    },
+    async showOnlyGroups() {
+      await user.click(ui.onlyGroupsBtn.get());
+    },
+    async showAll() {
+      await user.click(ui.showAllBtn.get());
+    },
+    async searchFor(name: string) {
+      await user.type(ui.searchInput.get(), name);
+    },
+    async clearSearch() {
+      await user.clear(ui.searchInput.get());
+    },
+  };
+}
+
+function renderPermissionsProjectApp(override?: Partial<Component>) {
+  function App({ component }: { component: Component }) {
+    const [realComponent, setRealComponent] = React.useState(component);
+    return (
+      <PermissionsProjectApp
+        component={realComponent}
+        onComponentChange={(changes: Partial<Component>) => {
+          setRealComponent({ ...realComponent, ...changes });
+        }}
+      />
+    );
+  }
+
+  return renderApp(
+    '/',
+    <App
+      component={mockComponent({
+        visibility: Visibility.Public,
+        configuration: {
+          canUpdateProjectVisibilityToPrivate: true,
+          canApplyPermissionTemplate: true,
+        },
+        ...override,
+      })}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/App-test.tsx.snap
deleted file mode 100644 (file)
index de21fda..0000000
+++ /dev/null
@@ -1,246 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<div
-  className="page page-limited"
-  id="project-permissions-page"
->
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    prioritizeSeoTags={false}
-    title="permissions.page"
-  />
-  <PageHeader
-    component={
-      {
-        "breadcrumbs": [],
-        "key": "my-project",
-        "name": "MyProject",
-        "qualifier": "TRK",
-        "qualityGate": {
-          "isDefault": true,
-          "key": "30",
-          "name": "Sonar way",
-        },
-        "qualityProfiles": [
-          {
-            "deleted": false,
-            "key": "my-qp",
-            "language": "ts",
-            "name": "Sonar way",
-          },
-        ],
-        "tags": [],
-      }
-    }
-    loadHolders={[Function]}
-    loading={true}
-  />
-  <div>
-    <VisibilitySelector
-      className="big-spacer-top big-spacer-bottom"
-      onChange={[Function]}
-    />
-  </div>
-  <AllHoldersList
-    filter="all"
-    grantPermissionToGroup={[Function]}
-    grantPermissionToUser={[Function]}
-    groups={[]}
-    onFilter={[Function]}
-    onLoadMore={[Function]}
-    onQuery={[Function]}
-    onSelectPermission={[Function]}
-    permissions={
-      [
-        {
-          "description": "projects_role.user.desc",
-          "key": "user",
-          "name": "projects_role.user",
-        },
-        {
-          "description": "projects_role.codeviewer.desc",
-          "key": "codeviewer",
-          "name": "projects_role.codeviewer",
-        },
-        {
-          "description": "projects_role.issueadmin.desc",
-          "key": "issueadmin",
-          "name": "projects_role.issueadmin",
-        },
-        {
-          "description": "projects_role.securityhotspotadmin.desc",
-          "key": "securityhotspotadmin",
-          "name": "projects_role.securityhotspotadmin",
-        },
-        {
-          "description": "projects_role.admin.desc",
-          "key": "admin",
-          "name": "projects_role.admin",
-        },
-        {
-          "description": "projects_role.scan.desc",
-          "key": "scan",
-          "name": "projects_role.scan",
-        },
-      ]
-    }
-    query=""
-    revokePermissionFromGroup={[Function]}
-    revokePermissionFromUser={[Function]}
-    users={[]}
-  />
-</div>
-`;
-
-exports[`should render correctly 2`] = `
-<div
-  className="page page-limited"
-  id="project-permissions-page"
->
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    prioritizeSeoTags={false}
-    title="permissions.page"
-  />
-  <PageHeader
-    component={
-      {
-        "breadcrumbs": [],
-        "key": "my-project",
-        "name": "MyProject",
-        "qualifier": "TRK",
-        "qualityGate": {
-          "isDefault": true,
-          "key": "30",
-          "name": "Sonar way",
-        },
-        "qualityProfiles": [
-          {
-            "deleted": false,
-            "key": "my-qp",
-            "language": "ts",
-            "name": "Sonar way",
-          },
-        ],
-        "tags": [],
-      }
-    }
-    loadHolders={[Function]}
-    loading={false}
-  />
-  <div>
-    <VisibilitySelector
-      className="big-spacer-top big-spacer-bottom"
-      onChange={[Function]}
-    />
-  </div>
-  <AllHoldersList
-    filter="all"
-    grantPermissionToGroup={[Function]}
-    grantPermissionToUser={[Function]}
-    groups={
-      [
-        {
-          "description": "SonarSource team",
-          "id": "1",
-          "name": "SonarSource",
-          "permissions": [
-            "admin",
-            "codeviewer",
-            "issueadmin",
-          ],
-        },
-        {
-          "name": "Anyone",
-          "permissions": [],
-        },
-      ]
-    }
-    groupsPaging={
-      {
-        "pageIndex": 1,
-        "pageSize": 100,
-        "total": 2,
-      }
-    }
-    onFilter={[Function]}
-    onLoadMore={[Function]}
-    onQuery={[Function]}
-    onSelectPermission={[Function]}
-    permissions={
-      [
-        {
-          "description": "projects_role.user.desc",
-          "key": "user",
-          "name": "projects_role.user",
-        },
-        {
-          "description": "projects_role.codeviewer.desc",
-          "key": "codeviewer",
-          "name": "projects_role.codeviewer",
-        },
-        {
-          "description": "projects_role.issueadmin.desc",
-          "key": "issueadmin",
-          "name": "projects_role.issueadmin",
-        },
-        {
-          "description": "projects_role.securityhotspotadmin.desc",
-          "key": "securityhotspotadmin",
-          "name": "projects_role.securityhotspotadmin",
-        },
-        {
-          "description": "projects_role.admin.desc",
-          "key": "admin",
-          "name": "projects_role.admin",
-        },
-        {
-          "description": "projects_role.scan.desc",
-          "key": "scan",
-          "name": "projects_role.scan",
-        },
-      ]
-    }
-    query=""
-    revokePermissionFromGroup={[Function]}
-    revokePermissionFromUser={[Function]}
-    users={
-      [
-        {
-          "avatar": "admin-avatar",
-          "email": "admin@gmail.com",
-          "login": "admin",
-          "name": "Admin Admin",
-          "permissions": [
-            "admin",
-          ],
-        },
-        {
-          "avatar": "user-avatar-1",
-          "email": "user1@gmail.com",
-          "login": "user1",
-          "name": "User Number 1",
-          "permissions": [],
-        },
-        {
-          "avatar": "user-avatar-2",
-          "email": "user2@gmail.com",
-          "login": "user2",
-          "name": "User Number 2",
-          "permissions": [],
-        },
-      ]
-    }
-    usersPaging={
-      {
-        "pageIndex": 1,
-        "pageSize": 100,
-        "total": 3,
-      }
-    }
-  />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/ApplyTemplate-test.tsx.snap b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/ApplyTemplate-test.tsx.snap
deleted file mode 100644 (file)
index 78bf796..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render correctly 1`] = `
-<SimpleModal
-  header="projects_role.apply_template_to_xxx.Foo"
-  onClose={[MockFunction]}
-  onSubmit={[Function]}
-  size="small"
->
-  <Component />
-</SimpleModal>
-`;
-
-exports[`render correctly 2`] = `
-<Modal
-  contentLabel="projects_role.apply_template_to_xxx.Foo"
-  onRequestClose={[MockFunction]}
-  size="small"
->
-  <form
-    id="project-permissions-apply-template-form"
-    onSubmit={[Function]}
-  >
-    <header
-      className="modal-head"
-    >
-      <h2>
-        projects_role.apply_template_to_xxx.Foo
-      </h2>
-    </header>
-    <div
-      className="modal-body"
-    >
-      <MandatoryFieldsExplanation
-        className="modal-field"
-      />
-      <div
-        className="modal-field"
-      >
-        <label
-          htmlFor="project-permissions-template-input"
-        >
-          template
-          <MandatoryFieldMarker />
-        </label>
-        <Select
-          className="Select"
-          id="project-permissions-template"
-          inputId="project-permissions-template-input"
-          onChange={[Function]}
-          options={
-            [
-              {
-                "label": "SonarSource projects",
-                "value": "tmp1",
-              },
-            ]
-          }
-          value={[]}
-        />
-      </div>
-    </div>
-    <footer
-      className="modal-foot"
-    >
-      <DeferredSpinner
-        className="spacer-right"
-        loading={false}
-      />
-      <SubmitButton
-        disabled={true}
-      >
-        apply
-      </SubmitButton>
-      <ResetButtonLink
-        onClick={[Function]}
-      >
-        cancel
-      </ResetButtonLink>
-    </footer>
-  </form>
-</Modal>
-`;
index 8c9f71af5959522ad9f44c271aa0165d3a38abe1..9670b118c6840b8d7240b496273c35cf6779d591 100644 (file)
 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 />} />
 );
index 23f159696d492a165e04a7cf39870a36b539e04c..91d89a431f064650dea8e23f4e8dc0cfd140d2ee 100644 (file)
@@ -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}
index 19991ed9e2572012282763954d8dcf9dbf7b00d0..3b6d1f407c0e851d157d5d39168e51424944606d 100644 (file)
@@ -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>
index 0fd32baa7082450cc86708a9c489f46e932eafa5..e8657cd2b050e4def2868866f942f25cae0e4471 100644 (file)
@@ -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>
+  );
 }
index 0a91e1d1711796a4ecab56e33ef2377c675725fa..36371d68ec832b395d5792d4225a4346c2a212c0 100644 (file)
@@ -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() {
index 8c37db214e85180f9192e1b24a9dbfbeb5b291ba..ea2e8220f3c014299aca3b789470296bffa12e90 100644 (file)
  * 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)
           );
         }),
       };
index 83875c4acbeafbc9a5e48055be2c53b3f09d0625..9be34752c05dcd0a11fce626ae6ba975c141b147 100644 (file)
@@ -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,
index 0a48ec21294f72e2a98f1f7996135015c8854035..52b84fa133307ccc863887b9b760dcfd5e7e1204 100644 (file)
@@ -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);
 });
 
index 974f231c02e1fc9ef5c4b9be772620a0d9d9d851..a56e9d81e00d6b1dd426a1a83fff6f6e90d092c6 100644 (file)
@@ -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;
index b2cfeebf233114de2255c5ee276da5587206ed7b..d22fd12ffae5e3c96d352528a52bf364c28c84de 100644 (file)
@@ -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}
index ef98661aaeaa2e154236178b0460b121b18f871a..b12bd6e02172066b7321194c0eda65e9a3e0291f 100644 (file)
@@ -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;
index 9d448440e472192ec733f0a3352a1c896b54f111..9f1b40851f8c2980160d581a83cb0bfa83121506 100644 (file)
@@ -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}
           />
index 2688eeede813139d1d503bf7729fa41b228edc0a..2f4a02b75b56ec2f1e314775b431a554c95ec882 100644 (file)
@@ -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';
index c231ca1f3c68b165b95ce23c935308ffb952b33d..dee37b24790d106d4ea15d52085a10de6f4e2493 100644 (file)
@@ -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">
index a2be921363cfbc20b83c383372c2f95e33a68a7c..e6763d0274630dbf830d203cc7474e534334d864 100644 (file)
@@ -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}
index eb4420f66853d6c5f44db15e0c34e5499d95b402..6a07ac14fcc810ecc075cb274fe2ae47c8287dde 100644 (file)
@@ -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()}
index 4a9d1f1127a0a31ec0b8de2431584d50518ab021..9e98fa9c417cb5ad7c6b3dbb3a9998c2ba6810d4 100644 (file)
@@ -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', () => {
index bd5ea500119b8f163635fe5148acb2dff9d458f8..ee3828034e3aad4231c3718a985cd51dd9987925 100644 (file)
@@ -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', () => {
index 2224ebc70133f645e4b6004e4458f2f2a1c75b5a..819bd7ae73d8fbaafe806fe3f82543f9cf395c1b 100644 (file)
@@ -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}
     />
index 7b77b8af2e295f7c75dd05c1e90197844566a3a1..aa3c008d1001610459659f71bdf9ff9998b00dda 100644 (file)
  */
 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'];
 
index 658b98196e45f9ebdea8593651a3a45ed4e6f331..86a203671aed7823ae3a710ba3823a7672091aef 100644 (file)
@@ -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;
   }
 
index dccc7fad07c9b6991c6854001206c3c2a6ae5fca..1cf6318108dbd8763f17607e3feac5357f0e8e50 100644 (file)
@@ -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>
+  );
 }
index fb4cdf90552f4067abbce65532b0fcd5cc645449..12e905a51abddc7d5ca17bb6c99e137488fac7c3 100644 (file)
@@ -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}
+    />
   );
 }
index 4330257f9cf9c2761234640ba823127adde37e7f..babc21706da7eebcfa2db3edccc8f478ed8207dc 100644 (file)
 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}
     />
   );
index 2c41ddc8a4ced831c91db943668a4e10b131e910..d7850a004fe4a92738ec227c14cd63bd3ba957e5 100644 (file)
  * 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,
+  };
+}
index 791d477568cba423665379adaa2968863e0abf83..84e1a61b35119cdf194c3ef39a6c19cd3b7793ff 100644 (file)
@@ -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,
   };
 }
index 2efd37c0e33831204af6bf28f52d41d9e9a87859..eab7d9b809bc67d0d81e42d1096c760ef408b930 100644 (file)
@@ -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,
-  };
-}
index 3c53643242101855581f9f81bcc872c7721336cf..9c256d1b4b7d2bcbecc0d74b5d82eb48707a7cfc 100644 (file)
  */
 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',
 }
index caedb79e1d3771919dc7a1b904fbb209cb76e6a8..4a7bc7ab2e6b11fc77ced5f5951d412a3e92f79b 100644 (file)
@@ -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;
index c358a27b23f6b6758ec55de9307b27a23ba50091..724098d8599ff1854b40dfe7fd0471bd5a2cf371 100644 (file)
@@ -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