applyTemplateToProject,
bulkApplyTemplate,
changeProjectVisibility,
+ getGlobalPermissionsGroups,
+ getGlobalPermissionsUsers,
getPermissionsGroupsForComponent,
getPermissionsUsersForComponent,
getPermissionTemplateGroups,
jest.mock('../permissions');
export default class PermissionsServiceMock {
- permissionTemplates: PermissionTemplate[] = [];
- permissions: Permission[];
- defaultTemplates: Array<{ templateId: string; qualifier: string }>;
- groups: PermissionGroup[];
- users: PermissionUser[];
- isAllowedPermissionChange = true;
+ #permissionTemplates: PermissionTemplate[] = [];
+ #permissions: Permission[];
+ #defaultTemplates: Array<{ templateId: string; qualifier: string }>;
+ #groups: PermissionGroup[];
+ #users: PermissionUser[];
+ #isAllowedToChangePermissions = true;
constructor() {
- this.permissionTemplates = cloneDeep(defaultPermissionTemplates);
- this.defaultTemplates = [
+ this.#permissionTemplates = cloneDeep(defaultPermissionTemplates);
+ this.#defaultTemplates = [
ComponentQualifier.Project,
ComponentQualifier.Application,
ComponentQualifier.Portfolio,
- ].map((qualifier) => ({ templateId: this.permissionTemplates[0].id, qualifier }));
- this.permissions = [
+ ].map((qualifier) => ({ templateId: this.#permissionTemplates[0].id, qualifier }));
+ this.#permissions = [
Permissions.Admin,
Permissions.CodeViewer,
Permissions.IssueAdmin,
Permissions.Scan,
Permissions.Browse,
].map((key) => mockPermission({ key, name: key }));
- this.groups = cloneDeep(defaultGroups);
- this.users = cloneDeep(defaultUsers);
+ this.#groups = cloneDeep(defaultGroups);
+ this.#users = cloneDeep(defaultUsers);
jest.mocked(getPermissionTemplates).mockImplementation(this.handleGetPermissionTemplates);
jest.mocked(bulkApplyTemplate).mockImplementation(this.handleBulkApplyTemplate);
jest.mocked(grantTemplatePermissionToUser).mockImplementation(this.handlePermissionChange);
jest.mocked(revokeTemplatePermissionFromUser).mockImplementation(this.handlePermissionChange);
jest.mocked(changeProjectVisibility).mockImplementation(this.handleChangeProjectVisibility);
+ jest.mocked(getGlobalPermissionsUsers).mockImplementation(this.handleGetPermissionUsers);
+ jest.mocked(getGlobalPermissionsGroups).mockImplementation(this.handleGetPermissionGroups);
jest
.mocked(getPermissionsGroupsForComponent)
.mockImplementation(this.handleGetPermissionGroupsForComponent);
handleGetPermissionTemplates = () => {
return this.reply({
- permissionTemplates: this.permissionTemplates,
- defaultTemplates: this.defaultTemplates,
- permissions: this.permissions,
+ permissionTemplates: this.#permissionTemplates,
+ defaultTemplates: this.#defaultTemplates,
+ permissions: this.#permissions,
});
};
return this.reply(undefined);
};
- handleGetPermissionGroupsForComponent = (data: {
- projectKey: string;
+ handleGetPermissionUsers = (data: {
q?: string;
permission?: string;
p?: number;
}) => {
const { ps = PAGE_SIZE, p = DEFAULT_PAGE, q, permission } = data;
- const groups =
+ const users =
q && q.length >= MIN_QUERY_LENGTH
- ? this.groups.filter((group) => group.name.toLowerCase().includes(q.toLowerCase()))
- : this.groups;
+ ? this.#users.filter((user) => user.name.toLowerCase().includes(q.toLowerCase()))
+ : this.#users;
- const groupsChunked = chunk(
- permission ? groups.filter((g) => g.permissions.includes(permission)) : groups,
+ const usersChunked = chunk(
+ permission ? users.filter((u) => u.permissions.includes(permission)) : users,
ps
);
return this.reply({
- paging: { pageSize: ps, total: groups.length, pageIndex: p },
- groups: groupsChunked[p - 1] ?? [],
+ paging: { pageSize: ps, total: users.length, pageIndex: p },
+ users: usersChunked[p - 1] ?? [],
});
};
- handleGetPermissionUsersForComponent = (data: {
- projectKey: string;
+ handleGetPermissionGroups = (data: {
q?: string;
permission?: string;
p?: number;
}) => {
const { ps = PAGE_SIZE, p = DEFAULT_PAGE, q, permission } = data;
- const users =
+ const groups =
q && q.length >= MIN_QUERY_LENGTH
- ? this.users.filter((user) => user.name.toLowerCase().includes(q.toLowerCase()))
- : this.users;
+ ? this.#groups.filter((group) => group.name.toLowerCase().includes(q.toLowerCase()))
+ : this.#groups;
- const usersChunked = chunk(
- permission ? users.filter((u) => u.permissions.includes(permission)) : users,
+ const groupsChunked = chunk(
+ permission ? groups.filter((g) => g.permissions.includes(permission)) : groups,
ps
);
return this.reply({
- paging: { pageSize: ps, total: users.length, pageIndex: p },
- users: usersChunked[p - 1] ?? [],
+ paging: { pageSize: ps, total: groups.length, pageIndex: p },
+ groups: groupsChunked[p - 1] ?? [],
});
};
+ handleGetPermissionGroupsForComponent = (data: {
+ projectKey: string;
+ q?: string;
+ permission?: string;
+ p?: number;
+ ps?: number;
+ }) => {
+ return this.handleGetPermissionGroups(data);
+ };
+
+ handleGetPermissionUsersForComponent = (data: {
+ projectKey: string;
+ q?: string;
+ permission?: string;
+ p?: number;
+ ps?: number;
+ }) => {
+ return this.handleGetPermissionUsers(data);
+ };
+
handleGrantPermissionToGroup = (data: {
projectKey?: string;
groupName: string;
permission: string;
}) => {
- if (!this.isAllowedPermissionChange) {
+ if (!this.#isAllowedToChangePermissions) {
return Promise.reject();
}
const { groupName, permission } = data;
- const group = this.groups.find((g) => g.name === groupName);
+ const group = this.#groups.find((g) => g.name === groupName);
if (group === undefined) {
throw new Error(`Could not find group with name ${groupName}`);
}
groupName: string;
permission: string;
}) => {
- if (!this.isAllowedPermissionChange) {
+ if (!this.#isAllowedToChangePermissions) {
return Promise.reject();
}
const { groupName, permission } = data;
- const group = this.groups.find((g) => g.name === groupName);
+ const group = this.#groups.find((g) => g.name === groupName);
if (group === undefined) {
throw new Error(`Could not find group with name ${groupName}`);
}
login: string;
permission: string;
}) => {
- if (!this.isAllowedPermissionChange) {
+ if (!this.#isAllowedToChangePermissions) {
return Promise.reject();
}
const { login, permission } = data;
- const user = this.users.find((u) => u.login === login);
+ const user = this.#users.find((u) => u.login === login);
if (user === undefined) {
throw new Error(`Could not find user with login ${login}`);
}
login: string;
permission: string;
}) => {
- if (!this.isAllowedPermissionChange) {
+ if (!this.#isAllowedToChangePermissions) {
return Promise.reject();
}
const { login, permission } = data;
- const user = this.users.find((u) => u.login === login);
+ const user = this.#users.find((u) => u.login === login);
if (user === undefined) {
throw new Error(`Could not find user with name ${login}`);
}
};
handlePermissionChange = () => {
- return this.isAllowedPermissionChange ? Promise.resolve() : Promise.reject();
+ return this.#isAllowedToChangePermissions ? Promise.resolve() : Promise.reject();
};
- updatePermissionChangeAllowance = (val: boolean) => {
- this.isAllowedPermissionChange = val;
+ setIsAllowedToChangePermissions = (val: boolean) => {
+ this.#isAllowedToChangePermissions = val;
};
setGroups = (groups: PermissionGroup[]) => {
- this.groups = groups;
+ this.#groups = groups;
};
setUsers = (users: PermissionUser[]) => {
- this.users = users;
+ this.#users = users;
};
reset = () => {
- this.permissionTemplates = cloneDeep(defaultPermissionTemplates);
- this.groups = cloneDeep(defaultGroups);
- this.users = cloneDeep(defaultUsers);
- this.updatePermissionChangeAllowance(true);
+ this.#permissionTemplates = cloneDeep(defaultPermissionTemplates);
+ this.#groups = cloneDeep(defaultGroups);
+ this.#users = cloneDeep(defaultUsers);
+ this.setIsAllowedToChangePermissions(true);
};
reply<T>(response: T): Promise<T> {
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import * as api from '../../../api/permissions';
+import AllHoldersList from '../../../components/permissions/AllHoldersList';
+import { FilterOption } from '../../../components/permissions/SearchForm';
import { translate } from '../../../helpers/l10n';
-import { Paging, PermissionGroup, PermissionTemplate, PermissionUser } from '../../../types/types';
-import AllHoldersList from '../../permissions/shared/components/AllHoldersList';
-import { FilterOption } from '../../permissions/shared/components/SearchForm';
import {
convertToPermissionDefinitions,
PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
-} from '../../permissions/utils';
+} from '../../../helpers/permissions';
+import { Paging, PermissionGroup, PermissionTemplate, PermissionUser } from '../../../types/types';
import TemplateDetails from './TemplateDetails';
import TemplateHeader from './TemplateHeader';
<AllHoldersList
filter={filter}
- grantPermissionToGroup={this.grantPermissionToGroup}
- grantPermissionToUser={this.grantPermissionToUser}
+ onGrantPermissionToGroup={this.grantPermissionToGroup}
+ onGrantPermissionToUser={this.grantPermissionToUser}
groups={groups}
groupsPaging={groupsPaging}
loading={loading}
onLoadMore={this.onLoadMore}
onQuery={this.handleSearch}
query={query}
- revokePermissionFromGroup={this.revokePermissionFromGroup}
- revokePermissionFromUser={this.revokePermissionFromUser}
+ onRevokePermissionFromGroup={this.revokePermissionFromGroup}
+ onRevokePermissionFromUser={this.revokePermissionFromUser}
users={allUsers}
usersPaging={usersPagingWithCreator}
permissions={permissions}
expect(ui.permissionCheckbox('Anyone', Permissions.CodeViewer).get()).not.toBeChecked();
// Handles error on permission change
- serviceMock.updatePermissionChangeAllowance(false);
+ serviceMock.setIsAllowedToChangePermissions(false);
await user.click(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get());
expect(ui.permissionCheckbox('Admin Admin', Permissions.Browse).get()).toBeChecked();
+++ /dev/null
-/*
- * 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 { convertToPermissionDefinitions } from '../utils';
-
-jest.mock('../../../helpers/l10nBundle', () => ({
- getMessages: jest.fn().mockReturnValue({}),
-}));
-
-describe('convertToPermissionDefinitions', () => {
- it('should convert and translate a permission definition', () => {
- const data = convertToPermissionDefinitions(['admin'], 'global_permissions');
- const expected = [
- {
- description: 'global_permissions.admin.desc',
- key: 'admin',
- name: 'global_permissions.admin',
- },
- ];
-
- expect(data).toEqual(expected);
- });
-});
+++ /dev/null
-/*
- * 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 withAppStateContext from '../../../../app/components/app-state/withAppStateContext';
-import Suggestions from '../../../../components/embed-docs-modal/Suggestions';
-import { translate } from '../../../../helpers/l10n';
-import { AppState } from '../../../../types/appstate';
-import { ComponentQualifier } from '../../../../types/component';
-import { Paging, PermissionGroup, PermissionUser } from '../../../../types/types';
-import AllHoldersList from '../../shared/components/AllHoldersList';
-import { FilterOption } from '../../shared/components/SearchForm';
-import '../../styles.css';
-import {
- convertToPermissionDefinitions,
- filterPermissions,
- PERMISSIONS_ORDER_GLOBAL,
-} from '../../utils';
-import PageHeader from './PageHeader';
-
-interface Props {
- appState: AppState;
-}
-interface State {
- filter: FilterOption;
- groups: PermissionGroup[];
- groupsPaging?: Paging;
- loading: boolean;
- query: string;
- users: PermissionUser[];
- usersPaging?: Paging;
-}
-export class App extends React.PureComponent<Props, State> {
- mounted = false;
-
- constructor(props: Props) {
- super(props);
- this.state = {
- filter: 'all',
- groups: [],
- loading: true,
- query: '',
- users: [],
- };
- }
-
- componentDidMount() {
- this.mounted = true;
- this.loadHolders();
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- loadUsersAndGroups = (userPage?: number, groupsPage?: number) => {
- const { filter, query } = this.state;
-
- const getUsers: Promise<{ paging?: Paging; users: PermissionUser[] }> =
- filter !== 'groups'
- ? api.getGlobalPermissionsUsers({
- q: query || undefined,
- p: userPage,
- })
- : Promise.resolve({ paging: undefined, users: [] });
-
- const getGroups: Promise<{ paging?: Paging; groups: PermissionGroup[] }> =
- filter !== 'users'
- ? api.getGlobalPermissionsGroups({
- q: query || undefined,
- 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);
- };
-
- onFilter = (filter: FilterOption) => {
- this.setState({ filter }, this.loadHolders);
- };
-
- onSearch = (query: string) => {
- this.setState({ query }, this.loadHolders);
- };
-
- addPermissionToGroup = (groups: PermissionGroup[], group: string, permission: string) => {
- return groups.map((candidate) =>
- candidate.name === group
- ? { ...candidate, permissions: [...candidate.permissions, permission] }
- : candidate
- );
- };
-
- addPermissionToUser = (users: PermissionUser[], user: string, permission: string) => {
- return users.map((candidate) =>
- candidate.login === user
- ? { ...candidate, permissions: [...candidate.permissions, permission] }
- : candidate
- );
- };
-
- removePermissionFromGroup = (groups: PermissionGroup[], group: string, permission: string) => {
- return groups.map((candidate) =>
- candidate.name === group
- ? { ...candidate, permissions: without(candidate.permissions, permission) }
- : candidate
- );
- };
-
- removePermissionFromUser = (users: PermissionUser[], user: string, permission: string) => {
- return users.map((candidate) =>
- candidate.login === user
- ? { ...candidate, permissions: without(candidate.permissions, permission) }
- : candidate
- );
- };
-
- grantPermissionToGroup = (group: string, permission: string) => {
- if (this.mounted) {
- this.setState(({ groups }) => ({
- groups: this.addPermissionToGroup(groups, group, permission),
- }));
- return api
- .grantPermissionToGroup({
- groupName: group,
- permission,
- })
- .then(
- () => {},
- () => {
- if (this.mounted) {
- this.setState(({ groups }) => ({
- groups: this.removePermissionFromGroup(groups, group, permission),
- }));
- }
- }
- );
- }
- return Promise.resolve();
- };
-
- grantPermissionToUser = (user: string, permission: string) => {
- if (this.mounted) {
- this.setState(({ users }) => ({
- users: this.addPermissionToUser(users, user, permission),
- }));
- return api
- .grantPermissionToUser({
- login: user,
- permission,
- })
- .then(
- () => {},
- () => {
- if (this.mounted) {
- this.setState(({ users }) => ({
- users: this.removePermissionFromUser(users, user, permission),
- }));
- }
- }
- );
- }
- return Promise.resolve();
- };
-
- revokePermissionFromGroup = (group: string, permission: string) => {
- if (this.mounted) {
- this.setState(({ groups }) => ({
- groups: this.removePermissionFromGroup(groups, group, permission),
- }));
- return api
- .revokePermissionFromGroup({
- groupName: group,
- permission,
- })
- .then(
- () => {},
- () => {
- if (this.mounted) {
- this.setState(({ groups }) => ({
- groups: this.addPermissionToGroup(groups, group, permission),
- }));
- }
- }
- );
- }
- return Promise.resolve();
- };
-
- revokePermissionFromUser = (user: string, permission: string) => {
- if (this.mounted) {
- this.setState(({ users }) => ({
- users: this.removePermissionFromUser(users, user, permission),
- }));
- return api
- .revokePermissionFromUser({
- login: user,
- permission,
- })
- .then(
- () => {},
- () => {
- if (this.mounted) {
- this.setState(({ users }) => ({
- users: this.addPermissionToUser(users, user, permission),
- }));
- }
- }
- );
- }
- return Promise.resolve();
- };
-
- stopLoading = () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- };
-
- render() {
- const { appState } = this.props;
- const { filter, groups, groupsPaging, users, usersPaging, loading, query } = this.state;
-
- const hasPortfoliosEnabled = appState.qualifiers.includes(ComponentQualifier.Portfolio);
- const hasApplicationsEnabled = appState.qualifiers.includes(ComponentQualifier.Application);
- const permissions = convertToPermissionDefinitions(
- filterPermissions(PERMISSIONS_ORDER_GLOBAL, hasApplicationsEnabled, hasPortfoliosEnabled),
- 'global_permissions'
- );
- return (
- <main className="page page-limited">
- <Suggestions suggestions="global_permissions" />
- <Helmet defer={false} title={translate('global_permissions.permission')} />
- <PageHeader loading={loading} />
- <AllHoldersList
- permissions={permissions}
- filter={filter}
- grantPermissionToGroup={this.grantPermissionToGroup}
- grantPermissionToUser={this.grantPermissionToUser}
- groups={groups}
- groupsPaging={groupsPaging}
- loading={loading}
- onFilter={this.onFilter}
- onLoadMore={this.onLoadMore}
- onQuery={this.onSearch}
- query={query}
- revokePermissionFromGroup={this.revokePermissionFromGroup}
- revokePermissionFromUser={this.revokePermissionFromUser}
- users={users}
- usersPaging={usersPaging}
- />
- </main>
- );
- }
-}
-
-export default withAppStateContext(App);
--- /dev/null
+/*
+ * 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 withAppStateContext, {
+ WithAppStateContextProps,
+} from '../../../../app/components/app-state/withAppStateContext';
+import Suggestions from '../../../../components/embed-docs-modal/Suggestions';
+import AllHoldersList from '../../../../components/permissions/AllHoldersList';
+import { FilterOption } from '../../../../components/permissions/SearchForm';
+import { translate } from '../../../../helpers/l10n';
+import {
+ convertToPermissionDefinitions,
+ filterPermissions,
+ PERMISSIONS_ORDER_GLOBAL,
+} from '../../../../helpers/permissions';
+import { ComponentQualifier } from '../../../../types/component';
+import { Paging, PermissionGroup, PermissionUser } from '../../../../types/types';
+import '../../styles.css';
+import PageHeader from './PageHeader';
+
+type Props = WithAppStateContextProps;
+
+interface State {
+ filter: FilterOption;
+ groups: PermissionGroup[];
+ groupsPaging?: Paging;
+ loading: boolean;
+ query: string;
+ users: PermissionUser[];
+ usersPaging?: Paging;
+}
+
+class PermissionsGlobalApp extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ filter: 'all',
+ groups: [],
+ loading: true,
+ query: '',
+ users: [],
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ this.loadHolders();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ loadUsersAndGroups = (userPage?: number, groupsPage?: number) => {
+ const { filter, query } = this.state;
+
+ const getUsers: Promise<{ paging?: Paging; users: PermissionUser[] }> =
+ filter !== 'groups'
+ ? api.getGlobalPermissionsUsers({
+ q: query || undefined,
+ p: userPage,
+ })
+ : Promise.resolve({ paging: undefined, users: [] });
+
+ const getGroups: Promise<{ paging?: Paging; groups: PermissionGroup[] }> =
+ filter !== 'users'
+ ? api.getGlobalPermissionsGroups({
+ q: query || undefined,
+ 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);
+ };
+
+ handleLoadMore = () => {
+ 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);
+ };
+
+ handleFilter = (filter: FilterOption) => {
+ this.setState({ filter }, this.loadHolders);
+ };
+
+ handleSearch = (query: string) => {
+ this.setState({ query }, this.loadHolders);
+ };
+
+ addPermissionToGroup = (groups: PermissionGroup[], group: string, permission: string) => {
+ return groups.map((candidate) =>
+ candidate.name === group
+ ? { ...candidate, permissions: [...candidate.permissions, permission] }
+ : candidate
+ );
+ };
+
+ addPermissionToUser = (users: PermissionUser[], user: string, permission: string) => {
+ return users.map((candidate) =>
+ candidate.login === user
+ ? { ...candidate, permissions: [...candidate.permissions, permission] }
+ : candidate
+ );
+ };
+
+ removePermissionFromGroup = (groups: PermissionGroup[], group: string, permission: string) => {
+ return groups.map((candidate) =>
+ candidate.name === group
+ ? { ...candidate, permissions: without(candidate.permissions, permission) }
+ : candidate
+ );
+ };
+
+ removePermissionFromUser = (users: PermissionUser[], user: string, permission: string) => {
+ return users.map((candidate) =>
+ candidate.login === user
+ ? { ...candidate, permissions: without(candidate.permissions, permission) }
+ : candidate
+ );
+ };
+
+ handleGrantPermissionToGroup = (group: string, permission: string) => {
+ this.setState({ loading: true });
+ return api
+ .grantPermissionToGroup({
+ groupName: group,
+ permission,
+ })
+ .then(() => {
+ if (this.mounted) {
+ this.setState(({ groups }) => ({
+ loading: false,
+ groups: this.addPermissionToGroup(groups, group, permission),
+ }));
+ }
+ }, this.stopLoading);
+ };
+
+ handleGrantPermissionToUser = (user: string, permission: string) => {
+ this.setState({ loading: true });
+ return api
+ .grantPermissionToUser({
+ login: user,
+ permission,
+ })
+ .then(() => {
+ if (this.mounted) {
+ this.setState(({ users }) => ({
+ loading: false,
+ users: this.addPermissionToUser(users, user, permission),
+ }));
+ }
+ }, this.stopLoading);
+ };
+
+ handleRevokePermissionFromGroup = (group: string, permission: string) => {
+ this.setState({ loading: true });
+ return api
+ .revokePermissionFromGroup({
+ groupName: group,
+ permission,
+ })
+ .then(() => {
+ if (this.mounted) {
+ this.setState(({ groups }) => ({
+ loading: false,
+ groups: this.removePermissionFromGroup(groups, group, permission),
+ }));
+ }
+ }, this.stopLoading);
+ };
+
+ handleRevokePermissionFromUser = (user: string, permission: string) => {
+ this.setState({ loading: true });
+ return api
+ .revokePermissionFromUser({
+ login: user,
+ permission,
+ })
+ .then(() => {
+ if (this.mounted) {
+ this.setState(({ users }) => ({
+ loading: false,
+ users: this.removePermissionFromUser(users, user, permission),
+ }));
+ }
+ }, this.stopLoading);
+ };
+
+ stopLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
+ render() {
+ const { appState } = this.props;
+ const { filter, groups, groupsPaging, users, usersPaging, loading, query } = this.state;
+
+ const hasPortfoliosEnabled = appState.qualifiers.includes(ComponentQualifier.Portfolio);
+ const hasApplicationsEnabled = appState.qualifiers.includes(ComponentQualifier.Application);
+ const permissions = convertToPermissionDefinitions(
+ filterPermissions(PERMISSIONS_ORDER_GLOBAL, hasApplicationsEnabled, hasPortfoliosEnabled),
+ 'global_permissions'
+ );
+ return (
+ <main className="page page-limited">
+ <Suggestions suggestions="global_permissions" />
+ <Helmet defer={false} title={translate('global_permissions.permission')} />
+ <PageHeader loading={loading} />
+ <AllHoldersList
+ permissions={permissions}
+ filter={filter}
+ onGrantPermissionToGroup={this.handleGrantPermissionToGroup}
+ onGrantPermissionToUser={this.handleGrantPermissionToUser}
+ groups={groups}
+ groupsPaging={groupsPaging}
+ loading={loading}
+ onFilter={this.handleFilter}
+ onLoadMore={this.handleLoadMore}
+ onQuery={this.handleSearch}
+ query={query}
+ onRevokePermissionFromGroup={this.handleRevokePermissionFromGroup}
+ onRevokePermissionFromUser={this.handleRevokePermissionFromUser}
+ users={users}
+ usersPaging={usersPaging}
+ />
+ </main>
+ );
+ }
+}
+
+export default withAppStateContext(PermissionsGlobalApp);
+++ /dev/null
-/*
- * 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 { mockAppState } from '../../../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../../../helpers/testUtils';
-import { ANYONE } from '../../../shared/components/GroupHolder';
-import { App } from '../App';
-
-jest.mock('../../../../../api/permissions', () => ({
- getGlobalPermissionsGroups: jest.fn().mockResolvedValue({
- paging: { pageIndex: 1, pageSize: 100, total: 2 },
- groups: [
- { name: 'Anyone', permissions: ['admin', 'codeviewer', 'issueadmin'] },
- { id: '1', name: 'SonarSource', description: 'SonarSource team', permissions: [] },
- ],
- }),
- getGlobalPermissionsUsers: 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 add and remove permission to a group', async () => {
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- const instance = wrapper.instance();
- const apiPayload = { groupName: ANYONE, permission: 'foo' };
-
- instance.grantPermissionToGroup(ANYONE, '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(ANYONE, '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(ANYONE, '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 = { 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 appState={mockAppState()} {...props} />);
-}
--- /dev/null
+/*
+ * 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 { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { without } from 'lodash';
+import PermissionsServiceMock from '../../../../../api/mocks/PermissionsServiceMock';
+import { mockPermissionGroup, mockPermissionUser } from '../../../../../helpers/mocks/permissions';
+import { PERMISSIONS_ORDER_GLOBAL } from '../../../../../helpers/permissions';
+import { mockAppState } from '../../../../../helpers/testMocks';
+import { renderAppRoutes } from '../../../../../helpers/testReactTestingUtils';
+import { AppState } from '../../../../../types/appstate';
+import { ComponentQualifier } from '../../../../../types/component';
+import { Permissions } from '../../../../../types/permissions';
+import { PermissionGroup, PermissionUser } from '../../../../../types/types';
+import { globalPermissionsRoutes } from '../../../routes';
+import { flattenPermissionsList, getPageObject } from '../../../test-utils';
+
+let serviceMock: PermissionsServiceMock;
+beforeAll(() => {
+ serviceMock = new PermissionsServiceMock();
+});
+
+afterEach(() => {
+ serviceMock.reset();
+});
+
+describe('rendering', () => {
+ it('should render correctly without applications and portfolios', async () => {
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ renderPermissionsGlobalApp();
+ await ui.appLoaded();
+
+ without(
+ flattenPermissionsList(PERMISSIONS_ORDER_GLOBAL),
+ Permissions.ApplicationCreation,
+ Permissions.PortfolioCreation
+ ).forEach((permission) => {
+ expect(ui.globalPermissionCheckbox('johndoe', permission).get()).toBeInTheDocument();
+ });
+ });
+
+ it.each([
+ [
+ ComponentQualifier.Portfolio,
+ without(flattenPermissionsList(PERMISSIONS_ORDER_GLOBAL), Permissions.ApplicationCreation),
+ ],
+ [
+ ComponentQualifier.Application,
+ without(flattenPermissionsList(PERMISSIONS_ORDER_GLOBAL), Permissions.PortfolioCreation),
+ ],
+ ])('should render correctly when %s are enabled', async (qualifier, permissions) => {
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ renderPermissionsGlobalApp(mockAppState({ qualifiers: [qualifier] }));
+ await ui.appLoaded();
+
+ permissions.forEach((permission) => {
+ expect(ui.globalPermissionCheckbox('johndoe', permission).get()).toBeInTheDocument();
+ });
+ });
+});
+
+describe('assigning/revoking permissions', () => {
+ it('should add and remove permissions to/from a group', async () => {
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ renderPermissionsGlobalApp();
+ await ui.appLoaded();
+
+ expect(ui.globalPermissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
+
+ await ui.toggleGlobalPermission('sonar-users', Permissions.Admin);
+ await ui.appLoaded();
+ expect(ui.globalPermissionCheckbox('sonar-users', Permissions.Admin).get()).toBeChecked();
+
+ await ui.toggleGlobalPermission('sonar-users', Permissions.Admin);
+ await ui.appLoaded();
+ expect(ui.globalPermissionCheckbox('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);
+ renderPermissionsGlobalApp();
+ await ui.appLoaded();
+
+ expect(ui.globalPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
+
+ await ui.toggleGlobalPermission('johndoe', Permissions.Scan);
+ await ui.appLoaded();
+ expect(ui.globalPermissionCheckbox('johndoe', Permissions.Scan).get()).toBeChecked();
+
+ await ui.toggleGlobalPermission('johndoe', Permissions.Scan);
+ await ui.appLoaded();
+ expect(ui.globalPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
+ });
+
+ it('should handle errors correctly', async () => {
+ serviceMock.setIsAllowedToChangePermissions(false);
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ renderPermissionsGlobalApp();
+ await ui.appLoaded();
+
+ expect(ui.globalPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
+ await ui.toggleGlobalPermission('johndoe', Permissions.Scan);
+ await ui.appLoaded();
+ expect(ui.globalPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
+ });
+});
+
+it('should allow to filter permission holders', async () => {
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ renderPermissionsGlobalApp();
+ 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 correctly handle pagination', async () => {
+ const groups: PermissionGroup[] = [];
+ const users: PermissionUser[] = [];
+ Array.from(Array(20).keys()).forEach((i) => {
+ groups.push(mockPermissionGroup({ name: `Group ${i}` }));
+ users.push(mockPermissionUser({ login: `user-${i}` }));
+ });
+ serviceMock.setGroups(groups);
+ serviceMock.setUsers(users);
+
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ renderPermissionsGlobalApp();
+ await ui.appLoaded();
+
+ expect(screen.getAllByRole('row').length).toBe(11);
+ await ui.clickLoadMore();
+ expect(screen.getAllByRole('row').length).toBe(21);
+});
+
+function renderPermissionsGlobalApp(appState?: AppState) {
+ return renderAppRoutes('permissions', globalPermissionsRoutes, { appState });
+}
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<main
- className="page page-limited"
->
- <Suggestions
- suggestions="global_permissions"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="global_permissions.permission"
- />
- <PageHeader
- loading={true}
- />
- <AllHoldersList
- filter="all"
- grantPermissionToGroup={[Function]}
- grantPermissionToUser={[Function]}
- groups={[]}
- loading={true}
- onFilter={[Function]}
- onLoadMore={[Function]}
- onQuery={[Function]}
- permissions={
- [
- {
- "description": "global_permissions.admin.desc",
- "key": "admin",
- "name": "global_permissions.admin",
- },
- {
- "category": "administer",
- "permissions": [
- {
- "description": "global_permissions.gateadmin.desc",
- "key": "gateadmin",
- "name": "global_permissions.gateadmin",
- },
- {
- "description": "global_permissions.profileadmin.desc",
- "key": "profileadmin",
- "name": "global_permissions.profileadmin",
- },
- ],
- },
- {
- "description": "global_permissions.scan.desc",
- "key": "scan",
- "name": "global_permissions.scan",
- },
- {
- "category": "creator",
- "permissions": [
- {
- "description": "global_permissions.provisioning.desc",
- "key": "provisioning",
- "name": "global_permissions.provisioning",
- },
- ],
- },
- ]
- }
- query=""
- revokePermissionFromGroup={[Function]}
- revokePermissionFromUser={[Function]}
- users={[]}
- />
-</main>
-`;
-
-exports[`should render correctly 2`] = `
-<main
- className="page page-limited"
->
- <Suggestions
- suggestions="global_permissions"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="global_permissions.permission"
- />
- <PageHeader
- loading={false}
- />
- <AllHoldersList
- filter="all"
- grantPermissionToGroup={[Function]}
- grantPermissionToUser={[Function]}
- groups={
- [
- {
- "name": "Anyone",
- "permissions": [
- "admin",
- "codeviewer",
- "issueadmin",
- ],
- },
- {
- "description": "SonarSource team",
- "id": "1",
- "name": "SonarSource",
- "permissions": [],
- },
- ]
- }
- groupsPaging={
- {
- "pageIndex": 1,
- "pageSize": 100,
- "total": 2,
- }
- }
- loading={false}
- onFilter={[Function]}
- onLoadMore={[Function]}
- onQuery={[Function]}
- permissions={
- [
- {
- "description": "global_permissions.admin.desc",
- "key": "admin",
- "name": "global_permissions.admin",
- },
- {
- "category": "administer",
- "permissions": [
- {
- "description": "global_permissions.gateadmin.desc",
- "key": "gateadmin",
- "name": "global_permissions.gateadmin",
- },
- {
- "description": "global_permissions.profileadmin.desc",
- "key": "profileadmin",
- "name": "global_permissions.profileadmin",
- },
- ],
- },
- {
- "description": "global_permissions.scan.desc",
- "key": "scan",
- "name": "global_permissions.scan",
- },
- {
- "category": "creator",
- "permissions": [
- {
- "description": "global_permissions.provisioning.desc",
- "key": "provisioning",
- "name": "global_permissions.provisioning",
- },
- ],
- },
- ]
- }
- 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,
- }
- }
- />
-</main>
-`;
<header className="page-header">
<h1 className="page-title">{translate('permissions.page')}</h1>
- <DeferredSpinner loading={this.props.loading} />
+ <DeferredSpinner className="spacer-left" loading={this.props.loading} />
{canApplyPermissionTemplate && (
<div className="page-actions">
import * as api from '../../../../api/permissions';
import withComponentContext from '../../../../app/components/componentContext/withComponentContext';
import VisibilitySelector from '../../../../components/common/VisibilitySelector';
+import AllHoldersList from '../../../../components/permissions/AllHoldersList';
+import { FilterOption } from '../../../../components/permissions/SearchForm';
import { translate } from '../../../../helpers/l10n';
-import { Visibility } from '../../../../types/component';
+import {
+ convertToPermissionDefinitions,
+ PERMISSIONS_ORDER_BY_QUALIFIER,
+} from '../../../../helpers/permissions';
+import { ComponentContextShape, 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 {
+interface Props extends ComponentContextShape {
component: Component;
- onComponentChange: (changes: Partial<Component>) => void;
}
interface State {
usersPaging?: Paging;
}
-export class PermissionsProjectApp extends React.PureComponent<Props, State> {
+class PermissionsProjectApp extends React.PureComponent<Props, State> {
mounted = false;
constructor(props: Props) {
}, this.stopLoading);
};
- onLoadMore = () => {
+ handleLoadMore = () => {
const { usersPaging, groupsPaging } = this.state;
this.setState({ loading: true });
return this.loadUsersAndGroups(
);
};
- 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();
+ handleGrantPermissionToGroup = (group: string, permission: string) => {
+ 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);
};
- 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();
+ handleGrantPermissionToUser = (user: string, permission: string) => {
+ 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);
};
- 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();
+ handleRevokePermissionFromGroup = (group: string, permission: string) => {
+ 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);
};
- 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();
+ handleRevokePermissionFromUser = (user: string, permission: string) => {
+ 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);
};
handleVisibilityChange = (visibility: string) => {
}
};
- turnProjectToPublic = () => {
+ handleTurnProjectToPublic = () => {
this.setState({ loading: true });
return api.changeProjectVisibility(this.props.component.key, Visibility.Public).then(() => {
this.props.onComponentChange({ visibility: Visibility.Public });
}
};
- closeDisclaimer = () => {
+ handleCloseDisclaimer = () => {
if (this.mounted) {
this.setState({ disclaimer: false });
}
const permissions = convertToPermissionDefinitions(order, 'projects_role');
return (
- <div className="page page-limited" id="project-permissions-page">
+ <main className="page page-limited" id="project-permissions-page">
<Helmet defer={false} title={translate('permissions.page')} />
<PageHeader component={component} loadHolders={this.loadHolders} loading={loading} />
{disclaimer && (
<PublicProjectDisclaimer
component={component}
- onClose={this.closeDisclaimer}
- onConfirm={this.turnProjectToPublic}
+ onClose={this.handleCloseDisclaimer}
+ onConfirm={this.handleTurnProjectToPublic}
/>
)}
</div>
<AllHoldersList
filter={filter}
- grantPermissionToGroup={this.grantPermissionToGroup}
- grantPermissionToUser={this.grantPermissionToUser}
+ onGrantPermissionToGroup={this.handleGrantPermissionToGroup}
+ onGrantPermissionToUser={this.handleGrantPermissionToUser}
groups={groups}
groupsPaging={groupsPaging}
onFilter={this.handleFilterChange}
- onLoadMore={this.onLoadMore}
+ onLoadMore={this.handleLoadMore}
onSelectPermission={this.handlePermissionSelect}
onQuery={this.handleQueryChange}
query={query}
- revokePermissionFromGroup={this.revokePermissionFromGroup}
- revokePermissionFromUser={this.revokePermissionFromUser}
+ onRevokePermissionFromGroup={this.handleRevokePermissionFromGroup}
+ onRevokePermissionFromUser={this.handleRevokePermissionFromUser}
selectedPermission={selectedPermission}
users={users}
usersPaging={usersPaging}
permissions={permissions}
/>
- </div>
+ </main>
);
}
}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { act, screen, waitFor } from '@testing-library/react';
+import { act, screen } 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 { mockPermissionGroup, mockPermissionUser } from '../../../../../helpers/mocks/permissions';
+import {
+ PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
+ PERMISSIONS_ORDER_FOR_VIEW,
+} from '../../../../../helpers/permissions';
+import { renderAppWithComponentContext } 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';
+import { Component, PermissionGroup, PermissionUser } from '../../../../../types/types';
+import { projectPermissionsRoutes } from '../../../routes';
+import { getPageObject } from '../../../test-utils';
let serviceMock: PermissionsServiceMock;
beforeAll(() => {
expect(screen.getByText(description)).toBeInTheDocument();
permissions.forEach((permission) => {
- expect(ui.permissionCheckbox('johndoe', permission).get()).toBeInTheDocument();
+ expect(ui.projectPermissionCheckbox('johndoe', permission).get()).toBeInTheDocument();
});
});
});
expect(screen.getAllByRole('row').length).toBe(7);
await ui.toggleFilterByPermission(Permissions.Admin);
expect(screen.getAllByRole('row').length).toBe(2);
+ await ui.toggleFilterByPermission(Permissions.Admin);
+ expect(screen.getAllByRole('row').length).toBe(7);
});
});
expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
expect(
- ui.permissionCheckbox('sonar-users', Permissions.Browse).query()
+ ui.projectPermissionCheckbox('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();
+ expect(
+ ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()
+ ).toBeInTheDocument();
await ui.turnProjectPublic();
expect(ui.makePublicDisclaimer.get()).toBeInTheDocument();
renderPermissionsProjectApp();
await ui.appLoaded();
- expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
+ expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
- await ui.togglePermission('sonar-users', Permissions.Admin);
+ await ui.toggleProjectPermission('sonar-users', Permissions.Admin);
await ui.appLoaded();
- expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).toBeChecked();
+ expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Admin).get()).toBeChecked();
- await ui.togglePermission('sonar-users', Permissions.Admin);
+ await ui.toggleProjectPermission('sonar-users', Permissions.Admin);
await ui.appLoaded();
- expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
+ expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
});
it('should add and remove permissions to/from a user', async () => {
renderPermissionsProjectApp();
await ui.appLoaded();
- expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
+ expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
+
+ await ui.toggleProjectPermission('johndoe', Permissions.Scan);
+ await ui.appLoaded();
+ expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).toBeChecked();
- await ui.togglePermission('johndoe', Permissions.Scan);
+ await ui.toggleProjectPermission('johndoe', Permissions.Scan);
+ await ui.appLoaded();
+ expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
+ });
+
+ it('should handle errors correctly', async () => {
+ serviceMock.setIsAllowedToChangePermissions(false);
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ renderPermissionsProjectApp();
await ui.appLoaded();
- expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).toBeChecked();
- await ui.togglePermission('johndoe', Permissions.Scan);
+ expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
+ await ui.toggleProjectPermission('johndoe', Permissions.Scan);
await ui.appLoaded();
- expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
+ expect(ui.projectPermissionCheckbox('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());
- },
- };
-}
+it('should correctly handle pagination', async () => {
+ const groups: PermissionGroup[] = [];
+ const users: PermissionUser[] = [];
+ Array.from(Array(20).keys()).forEach((i) => {
+ groups.push(mockPermissionGroup({ name: `Group ${i}` }));
+ users.push(mockPermissionUser({ login: `user-${i}` }));
+ });
+ serviceMock.setGroups(groups);
+ serviceMock.setUsers(users);
+
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ renderPermissionsProjectApp();
+ await ui.appLoaded();
+
+ expect(screen.getAllByRole('row').length).toBe(11);
+ await ui.clickLoadMore();
+ expect(screen.getAllByRole('row').length).toBe(21);
+});
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({
+ return renderAppWithComponentContext(
+ 'project_roles',
+ projectPermissionsRoutes,
+ {},
+ {
+ component: mockComponent({
visibility: Visibility.Public,
configuration: {
canUpdateProjectVisibilityToPrivate: true,
canApplyPermissionTemplate: true,
},
...override,
- })}
- />
+ }),
+ }
);
}
*/
import React from 'react';
import { Route } from 'react-router-dom';
-import GlobalPermissionsApp from './global/components/App';
+import GlobalPermissionsApp from './global/components/PermissionsGlobalApp';
import PermissionsProjectApp from './project/components/PermissionsProjectApp';
export const globalPermissionsRoutes = () => (
+++ /dev/null
-/*
- * 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 * as React from 'react';
-import ListFooter from '../../../../components/controls/ListFooter';
-import {
- Paging,
- PermissionDefinition,
- PermissionDefinitionGroup,
- PermissionGroup,
- PermissionUser,
-} from '../../../../types/types';
-import HoldersList from '../../shared/components/HoldersList';
-import SearchForm, { FilterOption } from '../../shared/components/SearchForm';
-
-interface Props {
- filter: FilterOption;
- query: string;
- onFilter: (filter: string) => void;
- onQuery: (query: string) => void;
- groups: PermissionGroup[];
- groupsPaging?: Paging;
- revokePermissionFromGroup: (group: string, permission: string) => Promise<void>;
- grantPermissionToGroup: (group: string, permission: string) => Promise<void>;
- users: PermissionUser[];
- usersPaging?: Paging;
- revokePermissionFromUser: (user: string, permission: string) => Promise<void>;
- grantPermissionToUser: (user: string, permission: string) => Promise<void>;
- permissions: Array<PermissionDefinition | PermissionDefinitionGroup>;
- onLoadMore: () => void;
- selectedPermission?: string;
- onSelectPermission?: (permissions?: string) => void;
- loading?: boolean;
-}
-
-export default class AllHoldersList extends React.PureComponent<Props> {
- handleToggleUser = (user: PermissionUser, permission: string) => {
- const hasPermission = user.permissions.includes(permission);
-
- if (hasPermission) {
- return this.props.revokePermissionFromUser(user.login, permission);
- }
- return this.props.grantPermissionToUser(user.login, permission);
- };
-
- handleToggleGroup = (group: PermissionGroup, permission: string) => {
- const hasPermission = group.permissions.includes(permission);
-
- if (hasPermission) {
- return this.props.revokePermissionFromGroup(group.name, permission);
- }
-
- return this.props.grantPermissionToGroup(group.name, permission);
- };
-
- getPaging = () => {
- const { filter, groups, groupsPaging, users, usersPaging } = this.props;
-
- let count = 0;
- let total = 0;
- if (filter !== 'users') {
- count += groups.length;
- total += groupsPaging ? groupsPaging.total : groups.length;
- }
- if (filter !== 'groups') {
- count += users.length;
- total += usersPaging ? usersPaging.total : users.length;
- }
-
- return { count, total };
- };
-
- render() {
- const { filter, query, groups, users, permissions, selectedPermission, loading } = this.props;
- const { count, total } = this.getPaging();
-
- return (
- <>
- <HoldersList
- loading={loading}
- filter={filter}
- groups={groups}
- onSelectPermission={this.props.onSelectPermission}
- onToggleGroup={this.handleToggleGroup}
- onToggleUser={this.handleToggleUser}
- permissions={permissions}
- query={query}
- selectedPermission={selectedPermission}
- users={users}
- >
- <SearchForm
- filter={filter}
- onFilter={this.props.onFilter}
- onSearch={this.props.onQuery}
- query={query}
- />
- </HoldersList>
- <ListFooter count={count} loadMore={this.props.onLoadMore} total={total} />
- </>
- );
- }
-}
+++ /dev/null
-/*
- * 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 GroupIcon from '../../../../components/icons/GroupIcon';
-import { translate } from '../../../../helpers/l10n';
-import { Permissions } from '../../../../types/permissions';
-import { PermissionDefinitions, PermissionGroup } from '../../../../types/types';
-import { isPermissionDefinitionGroup } from '../../utils';
-import PermissionCell from './PermissionCell';
-
-interface Props {
- group: PermissionGroup;
- isComponentPrivate?: boolean;
- onToggle: (group: PermissionGroup, permission: string) => Promise<void>;
- permissions: PermissionDefinitions;
- selectedPermission?: string;
-}
-
-interface State {
- loading: string[];
-}
-
-export const ANYONE = 'Anyone';
-
-export default class GroupHolder extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { loading: [] };
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- stopLoading = (permission: string) => {
- if (this.mounted) {
- this.setState((state) => ({ loading: without(state.loading, permission) }));
- }
- };
-
- handleCheck = (_checked: boolean, permission?: string) => {
- if (permission !== undefined) {
- this.setState((state) => ({ loading: [...state.loading, permission] }));
- this.props.onToggle(this.props.group, permission).then(
- () => this.stopLoading(permission),
- () => this.stopLoading(permission)
- );
- }
- };
-
- render() {
- const { group, isComponentPrivate, permissions, selectedPermission } = this.props;
-
- return (
- <tr>
- <td className="nowrap text-middle">
- <div className="display-flex-center">
- <GroupIcon className="big-spacer-right" />
- <div className="max-width-100">
- <div className="max-width-100 text-ellipsis">
- <strong>{group.name}</strong>
- {group.name === ANYONE && (
- <span className="spacer-left badge badge-error">{translate('deprecated')}</span>
- )}
- </div>
- <div className="little-spacer-top" style={{ whiteSpace: 'normal' }}>
- {group.name === ANYONE
- ? translate('user_groups.anyone.description')
- : group.description}
- </div>
- </div>
- </div>
- </td>
- {permissions.map((permission) => {
- const isPermissionGroup = isPermissionDefinitionGroup(permission);
- const permissionKey = isPermissionGroup ? permission.category : permission.key;
- const isAdminPermission = !isPermissionGroup && permissionKey === Permissions.Admin;
-
- return (
- <PermissionCell
- disabled={group.name === ANYONE && (isComponentPrivate || isAdminPermission)}
- key={permissionKey}
- loading={this.state.loading}
- onCheck={this.handleCheck}
- permission={permission}
- permissionItem={group}
- selectedPermission={selectedPermission}
- />
- );
- })}
- </tr>
- );
- }
-}
+++ /dev/null
-/*
- * 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 { partition } from 'lodash';
-import * as React from 'react';
-import { translate } from '../../../../helpers/l10n';
-import {
- Dict,
- PermissionDefinitions,
- PermissionGroup,
- PermissionUser,
-} from '../../../../types/types';
-import { isPermissionDefinitionGroup } from '../../utils';
-import GroupHolder from './GroupHolder';
-import PermissionHeader from './PermissionHeader';
-import UserHolder from './UserHolder';
-
-interface Props {
- filter?: string;
- groups: PermissionGroup[];
- isComponentPrivate?: boolean;
- loading?: boolean;
- onSelectPermission?: (permission: string) => void;
- onToggleGroup: (group: PermissionGroup, permission: string) => Promise<void>;
- onToggleUser: (user: PermissionUser, permission: string) => Promise<void>;
- permissions: PermissionDefinitions;
- query?: string;
- selectedPermission?: string;
- users: PermissionUser[];
-}
-
-interface State {
- initialPermissionsCount: Dict<number>;
-}
-export default class HoldersList extends React.PureComponent<Props, State> {
- state: State = { initialPermissionsCount: {} };
- componentDidUpdate(prevProps: Props) {
- if (this.props.filter !== prevProps.filter || this.props.query !== prevProps.query) {
- this.setState({ initialPermissionsCount: {} });
- }
- }
-
- isPermissionUser(item: PermissionGroup | PermissionUser): item is PermissionUser {
- return (item as PermissionUser).login !== undefined;
- }
-
- handleGroupToggle = (group: PermissionGroup, permission: string) => {
- const key = group.id || group.name;
- if (this.state.initialPermissionsCount[key] === undefined) {
- this.setState((state) => ({
- initialPermissionsCount: {
- ...state.initialPermissionsCount,
- [key]: group.permissions.length,
- },
- }));
- }
- return this.props.onToggleGroup(group, permission);
- };
-
- handleUserToggle = (user: PermissionUser, permission: string) => {
- if (this.state.initialPermissionsCount[user.login] === undefined) {
- this.setState((state) => ({
- initialPermissionsCount: {
- ...state.initialPermissionsCount,
- [user.login]: user.permissions.length,
- },
- }));
- }
- return this.props.onToggleUser(user, permission);
- };
-
- getItemInitialPermissionsCount = (item: PermissionGroup | PermissionUser) => {
- const key = this.isPermissionUser(item) ? item.login : item.id || item.name;
- return this.state.initialPermissionsCount[key] !== undefined
- ? this.state.initialPermissionsCount[key]
- : item.permissions.length;
- };
-
- renderEmpty() {
- const columns = this.props.permissions.length + 1;
- return (
- <tr>
- <td colSpan={columns}>{translate('no_results_search')}</td>
- </tr>
- );
- }
-
- renderItem(item: PermissionUser | PermissionGroup, permissions: PermissionDefinitions) {
- return this.isPermissionUser(item) ? (
- <UserHolder
- key={`user-${item.login}`}
- onToggle={this.handleUserToggle}
- permissions={permissions}
- selectedPermission={this.props.selectedPermission}
- user={item}
- />
- ) : (
- <GroupHolder
- group={item}
- isComponentPrivate={this.props.isComponentPrivate}
- key={`group-${item.id || item.name}`}
- onToggle={this.handleGroupToggle}
- permissions={permissions}
- selectedPermission={this.props.selectedPermission}
- />
- );
- }
-
- render() {
- const { permissions, users, groups, loading, children, selectedPermission } = this.props;
- const items = [...groups, ...users];
- const [itemWithPermissions, itemWithoutPermissions] = partition(items, (item) =>
- this.getItemInitialPermissionsCount(item)
- );
-
- return (
- <div className="boxed-group boxed-group-inner">
- <table className="data zebra permissions-table">
- <thead>
- <tr>
- <td className="nowrap bordered-bottom">{children}</td>
- {permissions.map((permission) => (
- <PermissionHeader
- key={
- isPermissionDefinitionGroup(permission) ? permission.category : permission.key
- }
- onSelectPermission={this.props.onSelectPermission}
- permission={permission}
- selectedPermission={selectedPermission}
- />
- ))}
- </tr>
- </thead>
- <tbody>
- {items.length === 0 && !loading && this.renderEmpty()}
- {itemWithPermissions.map((item) => this.renderItem(item, permissions))}
- {itemWithPermissions.length > 0 && itemWithoutPermissions.length > 0 && (
- <>
- <tr>
- <td className="divider" colSpan={20} />
- </tr>
- <tr />
- {/* Keep correct zebra colors in the table */}
- </>
- )}
- {itemWithoutPermissions.map((item) => this.renderItem(item, permissions))}
- </tbody>
- </table>
- </div>
- );
- }
-}
+++ /dev/null
-/*
- * 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 classNames from 'classnames';
-import * as React from 'react';
-import Checkbox from '../../../../components/controls/Checkbox';
-import { translateWithParameters } from '../../../../helpers/l10n';
-import {
- PermissionDefinition,
- PermissionDefinitionGroup,
- PermissionGroup,
- PermissionUser,
-} from '../../../../types/types';
-import { isPermissionDefinitionGroup } from '../../utils';
-
-export interface PermissionCellProps {
- disabled?: boolean;
- loading: string[];
- onCheck: (checked: boolean, permission?: string) => void;
- permission: PermissionDefinition | PermissionDefinitionGroup;
- permissionItem: PermissionGroup | PermissionUser;
- selectedPermission?: string;
-}
-
-export default function PermissionCell(props: PermissionCellProps) {
- const { disabled, loading, onCheck, permission, permissionItem, selectedPermission } = props;
-
- 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);
-
- 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>
- );
- })}
- </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>
- );
-}
+++ /dev/null
-/*
- * 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 classNames from 'classnames';
-import * as React from 'react';
-import InstanceMessage from '../../../../components/common/InstanceMessage';
-import HelpTooltip from '../../../../components/controls/HelpTooltip';
-import Tooltip from '../../../../components/controls/Tooltip';
-import { translate, translateWithParameters } from '../../../../helpers/l10n';
-import { PermissionDefinition, PermissionDefinitionGroup } from '../../../../types/types';
-import { isPermissionDefinitionGroup } from '../../utils';
-
-interface Props {
- onSelectPermission?: (permission: string) => void;
- permission: PermissionDefinition | PermissionDefinitionGroup;
- selectedPermission?: string;
-}
-
-export default class PermissionHeader extends React.PureComponent<Props> {
- handlePermissionClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
- event.preventDefault();
- event.currentTarget.blur();
- const { permission } = this.props;
- if (this.props.onSelectPermission && !isPermissionDefinitionGroup(permission)) {
- this.props.onSelectPermission(permission.key);
- }
- };
-
- getTooltipOverlay = () => {
- const { permission } = this.props;
-
- if (isPermissionDefinitionGroup(permission)) {
- return permission.permissions.map((permission) => (
- <React.Fragment key={permission.key}>
- <b className="little-spacer-right">{permission.name}:</b>
- <InstanceMessage key={permission.key} message={permission.description} />
- <br />
- </React.Fragment>
- ));
- }
-
- return <InstanceMessage message={permission.description} />;
- };
-
- render() {
- const { onSelectPermission, permission } = this.props;
- let name;
- if (isPermissionDefinitionGroup(permission)) {
- name = translate('global_permissions', permission.category);
- } else {
- name = onSelectPermission ? (
- <Tooltip
- overlay={translateWithParameters(
- 'global_permissions.filter_by_x_permission',
- permission.name
- )}
- >
- <a href="#" onClick={this.handlePermissionClick}>
- {permission.name}
- </a>
- </Tooltip>
- ) : (
- permission.name
- );
- }
- return (
- <th
- className={classNames('permission-column text-center text-middle', {
- selected:
- !isPermissionDefinitionGroup(permission) &&
- permission.key === this.props.selectedPermission,
- })}
- >
- <div className="permission-column-inner">
- {name}
- <HelpTooltip className="spacer-left" overlay={this.getTooltipOverlay()} />
- </div>
- </th>
- );
- }
-}
+++ /dev/null
-/*
- * 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 * as React from 'react';
-import ButtonToggle from '../../../../components/controls/ButtonToggle';
-import SearchBox from '../../../../components/controls/SearchBox';
-import { translate } from '../../../../helpers/l10n';
-
-export type FilterOption = 'all' | 'users' | 'groups';
-interface Props {
- filter: FilterOption;
- onFilter: (value: FilterOption) => void;
- onSearch: (value: string) => void;
- query: string;
-}
-
-export default function SearchForm(props: Props) {
- const filterOptions = [
- { value: 'all', label: translate('all') },
- { value: 'users', label: translate('users.page') },
- { value: 'groups', label: translate('user_groups.page') },
- ];
-
- return (
- <div className="display-flex-row">
- <ButtonToggle onCheck={props.onFilter} options={filterOptions} value={props.filter} />
-
- <div className="flex-1 spacer-left">
- <SearchBox
- minLength={3}
- onChange={props.onSearch}
- placeholder={translate('search.search_for_users_or_groups')}
- value={props.query}
- />
- </div>
- </div>
- );
-}
+++ /dev/null
-/*
- * 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 Avatar from '../../../../components/ui/Avatar';
-import { translate } from '../../../../helpers/l10n';
-import { PermissionDefinitions, PermissionUser } from '../../../../types/types';
-import { isPermissionDefinitionGroup } from '../../utils';
-import PermissionCell from './PermissionCell';
-
-interface Props {
- onToggle: (user: PermissionUser, permission: string) => Promise<void>;
- permissions: PermissionDefinitions;
- selectedPermission?: string;
- user: PermissionUser;
-}
-
-interface State {
- loading: string[];
-}
-
-export default class UserHolder extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { loading: [] };
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- stopLoading = (permission: string) => {
- if (this.mounted) {
- this.setState((state) => ({ loading: without(state.loading, permission) }));
- }
- };
-
- handleCheck = (_checked: boolean, permission?: string) => {
- if (permission !== undefined) {
- this.setState((state) => ({ loading: [...state.loading, permission] }));
- this.props.onToggle(this.props.user, permission).then(
- () => this.stopLoading(permission),
- () => this.stopLoading(permission)
- );
- }
- };
-
- render() {
- const { user } = this.props;
- const permissionCells = this.props.permissions.map((permission) => (
- <PermissionCell
- key={isPermissionDefinitionGroup(permission) ? permission.category : permission.key}
- loading={this.state.loading}
- onCheck={this.handleCheck}
- permission={permission}
- permissionItem={user}
- selectedPermission={this.props.selectedPermission}
- />
- ));
-
- if (user.login === '<creator>') {
- return (
- <tr>
- <td className="nowrap text-middle">
- <div>
- <strong>{user.name}</strong>
- </div>
- <div className="little-spacer-top" style={{ whiteSpace: 'normal' }}>
- {translate('permission_templates.project_creators.explanation')}
- </div>
- </td>
- {permissionCells}
- </tr>
- );
- }
-
- return (
- <tr>
- <td className="nowrap text-middle">
- <div className="display-flex-center">
- <Avatar
- className="text-middle big-spacer-right flex-0"
- hash={user.avatar}
- name={user.name}
- size={36}
- />
- <div className="max-width-100">
- <div className="max-width-100 text-ellipsis">
- <strong>{user.name}</strong>
- <span className="note spacer-left">{user.login}</span>
- </div>
- <div className="little-spacer-top max-width-100 text-ellipsis">{user.email}</div>
- </div>
- </div>
- </td>
- {permissionCells}
- </tr>
- );
- }
-}
--- /dev/null
+/*
+ * 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 { waitFor } from '@testing-library/react';
+import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
+import selectEvent from 'react-select-event';
+import { byLabelText, byRole, byText } from 'testing-library-selector';
+import { Visibility } from '../../types/component';
+import { Permissions } from '../../types/permissions';
+
+export function getPageObject(user: UserEvent) {
+ const ui = {
+ loading: byLabelText('loading'),
+ projectPermissionCheckbox: (target: string, permission: Permissions) =>
+ byRole('checkbox', {
+ name: `permission.assign_x_to_y.projects_role.${permission}.${target}`,
+ }),
+ globalPermissionCheckbox: (target: string, permission: Permissions) =>
+ byRole('checkbox', {
+ name: `permission.assign_x_to_y.global_permissions.${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' }),
+ loadMoreBtn: byRole('button', { name: 'show_more' }),
+ };
+
+ return {
+ ...ui,
+ async appLoaded() {
+ await waitFor(() => {
+ expect(ui.loading.query()).not.toBeInTheDocument();
+ });
+ },
+ async toggleProjectPermission(target: string, permission: Permissions) {
+ await user.click(ui.projectPermissionCheckbox(target, permission).get());
+ },
+ async toggleGlobalPermission(target: string, permission: Permissions) {
+ await user.click(ui.globalPermissionCheckbox(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());
+ },
+ async clickLoadMore() {
+ await user.click(ui.loadMoreBtn.get());
+ },
+ };
+}
+
+export function flattenPermissionsList(
+ list: Array<
+ | Permissions
+ | {
+ category: string;
+ permissions: Permissions[];
+ }
+ >
+) {
+ function isPermissions(
+ p:
+ | Permissions
+ | {
+ category: string;
+ permissions: Permissions[];
+ }
+ ): p is Permissions {
+ return typeof p === 'string';
+ }
+
+ return list.reduce((acc, item) => {
+ if (isPermissions(item)) {
+ acc.push(item);
+ } else {
+ acc.push(...item.permissions);
+ }
+ return acc;
+ }, [] as Permissions[]);
+}
+++ /dev/null
-/*
- * 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 { translate } from '../../helpers/l10n';
-import { Permissions } from '../../types/permissions';
-import { Dict, PermissionDefinition, PermissionDefinitionGroup } from '../../types/types';
-
-export const PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE = [
- Permissions.Browse,
- Permissions.CodeViewer,
- Permissions.IssueAdmin,
- Permissions.SecurityHotspotAdmin,
- Permissions.Admin,
- Permissions.Scan,
-];
-
-export const PERMISSIONS_ORDER_GLOBAL = [
- 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 = [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,
-};
-
-function convertToPermissionDefinition(permission: string, l10nPrefix: string) {
- const name = translate(`${l10nPrefix}.${permission}`);
- const description = translate(`${l10nPrefix}.${permission}.desc`);
-
- return {
- key: permission,
- name,
- description,
- };
-}
-
-export function filterPermissions(
- permissions: Array<Permissions | { category: string; permissions: Permissions[] }>,
- hasApplicationsEnabled: boolean,
- hasPortfoliosEnabled: boolean
-) {
- return permissions.map((permission) => {
- if (typeof permission === 'object' && permission.category === 'creator') {
- return {
- ...permission,
- permissions: permission.permissions.filter((p) => {
- return (
- p === Permissions.ProjectCreation ||
- (p === Permissions.PortfolioCreation && hasPortfoliosEnabled) ||
- (p === Permissions.ApplicationCreation && hasApplicationsEnabled)
- );
- }),
- };
- }
- return permission;
- });
-}
-
-export function convertToPermissionDefinitions(
- permissions: Array<string | { category: string; permissions: string[] }>,
- l10nPrefix: string
-): Array<PermissionDefinition | PermissionDefinitionGroup> {
- return permissions.map((permission) => {
- if (typeof permission === 'object') {
- return {
- category: permission.category,
- permissions: permission.permissions.map((permission) =>
- convertToPermissionDefinition(permission, l10nPrefix)
- ),
- };
- }
- return convertToPermissionDefinition(permission, l10nPrefix);
- });
-}
-
-export function isPermissionDefinitionGroup(
- permission?: PermissionDefinition | PermissionDefinitionGroup
-): permission is PermissionDefinitionGroup {
- return Boolean(permission && (permission as PermissionDefinitionGroup).category);
-}
--- /dev/null
+/*
+ * 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 * as React from 'react';
+import {
+ Paging,
+ PermissionDefinition,
+ PermissionDefinitionGroup,
+ PermissionGroup,
+ PermissionUser,
+} from '../../types/types';
+import ListFooter from '../controls/ListFooter';
+import HoldersList from './HoldersList';
+import SearchForm, { FilterOption } from './SearchForm';
+
+interface Props {
+ filter: FilterOption;
+ query: string;
+ onFilter: (filter: string) => void;
+ onQuery: (query: string) => void;
+ groups: PermissionGroup[];
+ groupsPaging?: Paging;
+ onRevokePermissionFromGroup: (group: string, permission: string) => Promise<void>;
+ onGrantPermissionToGroup: (group: string, permission: string) => Promise<void>;
+ users: PermissionUser[];
+ usersPaging?: Paging;
+ onRevokePermissionFromUser: (user: string, permission: string) => Promise<void>;
+ onGrantPermissionToUser: (user: string, permission: string) => Promise<void>;
+ permissions: Array<PermissionDefinition | PermissionDefinitionGroup>;
+ onLoadMore: () => void;
+ selectedPermission?: string;
+ onSelectPermission?: (permissions?: string) => void;
+ loading?: boolean;
+}
+
+export default class AllHoldersList extends React.PureComponent<Props> {
+ handleToggleUser = (user: PermissionUser, permission: string) => {
+ const hasPermission = user.permissions.includes(permission);
+
+ if (hasPermission) {
+ return this.props.onRevokePermissionFromUser(user.login, permission);
+ }
+ return this.props.onGrantPermissionToUser(user.login, permission);
+ };
+
+ handleToggleGroup = (group: PermissionGroup, permission: string) => {
+ const hasPermission = group.permissions.includes(permission);
+
+ if (hasPermission) {
+ return this.props.onRevokePermissionFromGroup(group.name, permission);
+ }
+
+ return this.props.onGrantPermissionToGroup(group.name, permission);
+ };
+
+ getPaging = () => {
+ const { filter, groups, groupsPaging, users, usersPaging } = this.props;
+
+ let count = 0;
+ let total = 0;
+ if (filter !== 'users') {
+ count += groups.length;
+ total += groupsPaging ? groupsPaging.total : groups.length;
+ }
+ if (filter !== 'groups') {
+ count += users.length;
+ total += usersPaging ? usersPaging.total : users.length;
+ }
+
+ return { count, total };
+ };
+
+ render() {
+ const { filter, query, groups, users, permissions, selectedPermission, loading } = this.props;
+ const { count, total } = this.getPaging();
+
+ return (
+ <>
+ <HoldersList
+ loading={loading}
+ filter={filter}
+ groups={groups}
+ onSelectPermission={this.props.onSelectPermission}
+ onToggleGroup={this.handleToggleGroup}
+ onToggleUser={this.handleToggleUser}
+ permissions={permissions}
+ query={query}
+ selectedPermission={selectedPermission}
+ users={users}
+ >
+ <SearchForm
+ filter={filter}
+ onFilter={this.props.onFilter}
+ onSearch={this.props.onQuery}
+ query={query}
+ />
+ </HoldersList>
+ <ListFooter count={count} loadMore={this.props.onLoadMore} total={total} />
+ </>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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 { translate } from '../../helpers/l10n';
+import { isPermissionDefinitionGroup } from '../../helpers/permissions';
+import { Permissions } from '../../types/permissions';
+import { PermissionDefinitions, PermissionGroup } from '../../types/types';
+import GroupIcon from '../icons/GroupIcon';
+import PermissionCell from './PermissionCell';
+
+interface Props {
+ group: PermissionGroup;
+ isComponentPrivate?: boolean;
+ onToggle: (group: PermissionGroup, permission: string) => Promise<void>;
+ permissions: PermissionDefinitions;
+ selectedPermission?: string;
+}
+
+interface State {
+ loading: string[];
+}
+
+export const ANYONE = 'Anyone';
+
+export default class GroupHolder extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { loading: [] };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ stopLoading = (permission: string) => {
+ if (this.mounted) {
+ this.setState((state) => ({ loading: without(state.loading, permission) }));
+ }
+ };
+
+ handleCheck = (_checked: boolean, permission?: string) => {
+ if (permission !== undefined) {
+ this.setState((state) => ({ loading: [...state.loading, permission] }));
+ this.props.onToggle(this.props.group, permission).then(
+ () => this.stopLoading(permission),
+ () => this.stopLoading(permission)
+ );
+ }
+ };
+
+ render() {
+ const { group, isComponentPrivate, permissions, selectedPermission } = this.props;
+
+ return (
+ <tr>
+ <td className="nowrap text-middle">
+ <div className="display-flex-center">
+ <GroupIcon className="big-spacer-right" />
+ <div className="max-width-100">
+ <div className="max-width-100 text-ellipsis">
+ <strong>{group.name}</strong>
+ {group.name === ANYONE && (
+ <span className="spacer-left badge badge-error">{translate('deprecated')}</span>
+ )}
+ </div>
+ <div className="little-spacer-top" style={{ whiteSpace: 'normal' }}>
+ {group.name === ANYONE
+ ? translate('user_groups.anyone.description')
+ : group.description}
+ </div>
+ </div>
+ </div>
+ </td>
+ {permissions.map((permission) => {
+ const isPermissionGroup = isPermissionDefinitionGroup(permission);
+ const permissionKey = isPermissionGroup ? permission.category : permission.key;
+ const isAdminPermission = !isPermissionGroup && permissionKey === Permissions.Admin;
+
+ return (
+ <PermissionCell
+ disabled={group.name === ANYONE && (isComponentPrivate || isAdminPermission)}
+ key={permissionKey}
+ loading={this.state.loading}
+ onCheck={this.handleCheck}
+ permission={permission}
+ permissionItem={group}
+ selectedPermission={selectedPermission}
+ />
+ );
+ })}
+ </tr>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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 { partition } from 'lodash';
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import { isPermissionDefinitionGroup } from '../../helpers/permissions';
+import { Dict, PermissionDefinitions, PermissionGroup, PermissionUser } from '../../types/types';
+import GroupHolder from './GroupHolder';
+import PermissionHeader from './PermissionHeader';
+import UserHolder from './UserHolder';
+
+interface Props {
+ filter?: string;
+ groups: PermissionGroup[];
+ isComponentPrivate?: boolean;
+ loading?: boolean;
+ onSelectPermission?: (permission: string) => void;
+ onToggleGroup: (group: PermissionGroup, permission: string) => Promise<void>;
+ onToggleUser: (user: PermissionUser, permission: string) => Promise<void>;
+ permissions: PermissionDefinitions;
+ query?: string;
+ selectedPermission?: string;
+ users: PermissionUser[];
+}
+
+interface State {
+ initialPermissionsCount: Dict<number>;
+}
+export default class HoldersList extends React.PureComponent<Props, State> {
+ state: State = { initialPermissionsCount: {} };
+ componentDidUpdate(prevProps: Props) {
+ if (this.props.filter !== prevProps.filter || this.props.query !== prevProps.query) {
+ this.setState({ initialPermissionsCount: {} });
+ }
+ }
+
+ isPermissionUser(item: PermissionGroup | PermissionUser): item is PermissionUser {
+ return (item as PermissionUser).login !== undefined;
+ }
+
+ handleGroupToggle = (group: PermissionGroup, permission: string) => {
+ const key = group.id || group.name;
+ if (this.state.initialPermissionsCount[key] === undefined) {
+ this.setState((state) => ({
+ initialPermissionsCount: {
+ ...state.initialPermissionsCount,
+ [key]: group.permissions.length,
+ },
+ }));
+ }
+ return this.props.onToggleGroup(group, permission);
+ };
+
+ handleUserToggle = (user: PermissionUser, permission: string) => {
+ if (this.state.initialPermissionsCount[user.login] === undefined) {
+ this.setState((state) => ({
+ initialPermissionsCount: {
+ ...state.initialPermissionsCount,
+ [user.login]: user.permissions.length,
+ },
+ }));
+ }
+ return this.props.onToggleUser(user, permission);
+ };
+
+ getItemInitialPermissionsCount = (item: PermissionGroup | PermissionUser) => {
+ const key = this.isPermissionUser(item) ? item.login : item.id || item.name;
+ return this.state.initialPermissionsCount[key] !== undefined
+ ? this.state.initialPermissionsCount[key]
+ : item.permissions.length;
+ };
+
+ renderEmpty() {
+ const columns = this.props.permissions.length + 1;
+ return (
+ <tr>
+ <td colSpan={columns}>{translate('no_results_search')}</td>
+ </tr>
+ );
+ }
+
+ renderItem(item: PermissionUser | PermissionGroup, permissions: PermissionDefinitions) {
+ return this.isPermissionUser(item) ? (
+ <UserHolder
+ key={`user-${item.login}`}
+ onToggle={this.handleUserToggle}
+ permissions={permissions}
+ selectedPermission={this.props.selectedPermission}
+ user={item}
+ />
+ ) : (
+ <GroupHolder
+ group={item}
+ isComponentPrivate={this.props.isComponentPrivate}
+ key={`group-${item.id || item.name}`}
+ onToggle={this.handleGroupToggle}
+ permissions={permissions}
+ selectedPermission={this.props.selectedPermission}
+ />
+ );
+ }
+
+ render() {
+ const { permissions, users, groups, loading, children, selectedPermission } = this.props;
+ const items = [...groups, ...users];
+ const [itemWithPermissions, itemWithoutPermissions] = partition(items, (item) =>
+ this.getItemInitialPermissionsCount(item)
+ );
+
+ return (
+ <div className="boxed-group boxed-group-inner">
+ <table className="data zebra permissions-table">
+ <thead>
+ <tr>
+ <td className="nowrap bordered-bottom">{children}</td>
+ {permissions.map((permission) => (
+ <PermissionHeader
+ key={
+ isPermissionDefinitionGroup(permission) ? permission.category : permission.key
+ }
+ onSelectPermission={this.props.onSelectPermission}
+ permission={permission}
+ selectedPermission={selectedPermission}
+ />
+ ))}
+ </tr>
+ </thead>
+ <tbody>
+ {items.length === 0 && !loading && this.renderEmpty()}
+ {itemWithPermissions.map((item) => this.renderItem(item, permissions))}
+ {itemWithPermissions.length > 0 && itemWithoutPermissions.length > 0 && (
+ <>
+ <tr>
+ <td className="divider" colSpan={20} />
+ </tr>
+ <tr />
+ {/* Keep correct zebra colors in the table */}
+ </>
+ )}
+ {itemWithoutPermissions.map((item) => this.renderItem(item, permissions))}
+ </tbody>
+ </table>
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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 classNames from 'classnames';
+import * as React from 'react';
+import { translateWithParameters } from '../../helpers/l10n';
+import { isPermissionDefinitionGroup } from '../../helpers/permissions';
+import {
+ PermissionDefinition,
+ PermissionDefinitionGroup,
+ PermissionGroup,
+ PermissionUser,
+} from '../../types/types';
+import Checkbox from '../controls/Checkbox';
+
+export interface PermissionCellProps {
+ disabled?: boolean;
+ loading: string[];
+ onCheck: (checked: boolean, permission?: string) => void;
+ permission: PermissionDefinition | PermissionDefinitionGroup;
+ permissionItem: PermissionGroup | PermissionUser;
+ selectedPermission?: string;
+}
+
+export default function PermissionCell(props: PermissionCellProps) {
+ const { disabled, loading, onCheck, permission, permissionItem, selectedPermission } = props;
+
+ 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);
+
+ 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>
+ );
+ })}
+ </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>
+ );
+}
--- /dev/null
+/*
+ * 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 classNames from 'classnames';
+import * as React from 'react';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { isPermissionDefinitionGroup } from '../../helpers/permissions';
+import { PermissionDefinition, PermissionDefinitionGroup } from '../../types/types';
+import InstanceMessage from '../common/InstanceMessage';
+import HelpTooltip from '../controls/HelpTooltip';
+import Tooltip from '../controls/Tooltip';
+
+interface Props {
+ onSelectPermission?: (permission: string) => void;
+ permission: PermissionDefinition | PermissionDefinitionGroup;
+ selectedPermission?: string;
+}
+
+export default class PermissionHeader extends React.PureComponent<Props> {
+ handlePermissionClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ const { permission } = this.props;
+ if (this.props.onSelectPermission && !isPermissionDefinitionGroup(permission)) {
+ this.props.onSelectPermission(permission.key);
+ }
+ };
+
+ getTooltipOverlay = () => {
+ const { permission } = this.props;
+
+ if (isPermissionDefinitionGroup(permission)) {
+ return permission.permissions.map((permission) => (
+ <React.Fragment key={permission.key}>
+ <b className="little-spacer-right">{permission.name}:</b>
+ <InstanceMessage key={permission.key} message={permission.description} />
+ <br />
+ </React.Fragment>
+ ));
+ }
+
+ return <InstanceMessage message={permission.description} />;
+ };
+
+ render() {
+ const { onSelectPermission, permission } = this.props;
+ let name;
+ if (isPermissionDefinitionGroup(permission)) {
+ name = translate('global_permissions', permission.category);
+ } else {
+ name = onSelectPermission ? (
+ <Tooltip
+ overlay={translateWithParameters(
+ 'global_permissions.filter_by_x_permission',
+ permission.name
+ )}
+ >
+ <a href="#" onClick={this.handlePermissionClick}>
+ {permission.name}
+ </a>
+ </Tooltip>
+ ) : (
+ permission.name
+ );
+ }
+ return (
+ <th
+ className={classNames('permission-column text-center text-middle', {
+ selected:
+ !isPermissionDefinitionGroup(permission) &&
+ permission.key === this.props.selectedPermission,
+ })}
+ >
+ <div className="permission-column-inner">
+ {name}
+ <HelpTooltip className="spacer-left" overlay={this.getTooltipOverlay()} />
+ </div>
+ </th>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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 * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import ButtonToggle from '../controls/ButtonToggle';
+import SearchBox from '../controls/SearchBox';
+
+export type FilterOption = 'all' | 'users' | 'groups';
+interface Props {
+ filter: FilterOption;
+ onFilter: (value: FilterOption) => void;
+ onSearch: (value: string) => void;
+ query: string;
+}
+
+export default function SearchForm(props: Props) {
+ const filterOptions = [
+ { value: 'all', label: translate('all') },
+ { value: 'users', label: translate('users.page') },
+ { value: 'groups', label: translate('user_groups.page') },
+ ];
+
+ return (
+ <div className="display-flex-row">
+ <ButtonToggle onCheck={props.onFilter} options={filterOptions} value={props.filter} />
+
+ <div className="flex-1 spacer-left">
+ <SearchBox
+ minLength={3}
+ onChange={props.onSearch}
+ placeholder={translate('search.search_for_users_or_groups')}
+ value={props.query}
+ />
+ </div>
+ </div>
+ );
+}
--- /dev/null
+/*
+ * 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 { translate } from '../../helpers/l10n';
+import { isPermissionDefinitionGroup } from '../../helpers/permissions';
+import { PermissionDefinitions, PermissionUser } from '../../types/types';
+import Avatar from '../ui/Avatar';
+import PermissionCell from './PermissionCell';
+
+interface Props {
+ onToggle: (user: PermissionUser, permission: string) => Promise<void>;
+ permissions: PermissionDefinitions;
+ selectedPermission?: string;
+ user: PermissionUser;
+}
+
+interface State {
+ loading: string[];
+}
+
+export default class UserHolder extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { loading: [] };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ stopLoading = (permission: string) => {
+ if (this.mounted) {
+ this.setState((state) => ({ loading: without(state.loading, permission) }));
+ }
+ };
+
+ handleCheck = (_checked: boolean, permission?: string) => {
+ if (permission !== undefined) {
+ this.setState((state) => ({ loading: [...state.loading, permission] }));
+ this.props.onToggle(this.props.user, permission).then(
+ () => this.stopLoading(permission),
+ () => this.stopLoading(permission)
+ );
+ }
+ };
+
+ render() {
+ const { user } = this.props;
+ const permissionCells = this.props.permissions.map((permission) => (
+ <PermissionCell
+ key={isPermissionDefinitionGroup(permission) ? permission.category : permission.key}
+ loading={this.state.loading}
+ onCheck={this.handleCheck}
+ permission={permission}
+ permissionItem={user}
+ selectedPermission={this.props.selectedPermission}
+ />
+ ));
+
+ if (user.login === '<creator>') {
+ return (
+ <tr>
+ <td className="nowrap text-middle">
+ <div>
+ <strong>{user.name}</strong>
+ </div>
+ <div className="little-spacer-top" style={{ whiteSpace: 'normal' }}>
+ {translate('permission_templates.project_creators.explanation')}
+ </div>
+ </td>
+ {permissionCells}
+ </tr>
+ );
+ }
+
+ return (
+ <tr>
+ <td className="nowrap text-middle">
+ <div className="display-flex-center">
+ <Avatar
+ className="text-middle big-spacer-right flex-0"
+ hash={user.avatar}
+ name={user.name}
+ size={36}
+ />
+ <div className="max-width-100">
+ <div className="max-width-100 text-ellipsis">
+ <strong>{user.name}</strong>
+ <span className="note spacer-left">{user.login}</span>
+ </div>
+ <div className="little-spacer-top max-width-100 text-ellipsis">{user.email}</div>
+ </div>
+ </div>
+ </td>
+ {permissionCells}
+ </tr>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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 { convertToPermissionDefinitions } from '../permissions';
+
+jest.mock('../l10nBundle', () => ({
+ getMessages: jest.fn().mockReturnValue({}),
+}));
+
+describe('convertToPermissionDefinitions', () => {
+ it('should convert and translate a permission definition', () => {
+ const data = convertToPermissionDefinitions(['admin'], 'global_permissions');
+ const expected = [
+ {
+ description: 'global_permissions.admin.desc',
+ key: 'admin',
+ name: 'global_permissions.admin',
+ },
+ ];
+
+ expect(data).toEqual(expected);
+ });
+});
--- /dev/null
+/*
+ * 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 { Permissions } from '../types/permissions';
+import { Dict, PermissionDefinition, PermissionDefinitionGroup } from '../types/types';
+import { translate } from './l10n';
+
+export const PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE = [
+ Permissions.Browse,
+ Permissions.CodeViewer,
+ Permissions.IssueAdmin,
+ Permissions.SecurityHotspotAdmin,
+ Permissions.Admin,
+ Permissions.Scan,
+];
+
+export const PERMISSIONS_ORDER_GLOBAL = [
+ 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 = [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,
+};
+
+function convertToPermissionDefinition(permission: string, l10nPrefix: string) {
+ const name = translate(`${l10nPrefix}.${permission}`);
+ const description = translate(`${l10nPrefix}.${permission}.desc`);
+
+ return {
+ key: permission,
+ name,
+ description,
+ };
+}
+
+export function filterPermissions(
+ permissions: Array<Permissions | { category: string; permissions: Permissions[] }>,
+ hasApplicationsEnabled: boolean,
+ hasPortfoliosEnabled: boolean
+) {
+ return permissions.map((permission) => {
+ if (typeof permission === 'object' && permission.category === 'creator') {
+ return {
+ ...permission,
+ permissions: permission.permissions.filter((p) => {
+ return (
+ p === Permissions.ProjectCreation ||
+ (p === Permissions.PortfolioCreation && hasPortfoliosEnabled) ||
+ (p === Permissions.ApplicationCreation && hasApplicationsEnabled)
+ );
+ }),
+ };
+ }
+ return permission;
+ });
+}
+
+export function convertToPermissionDefinitions(
+ permissions: Array<string | { category: string; permissions: string[] }>,
+ l10nPrefix: string
+): Array<PermissionDefinition | PermissionDefinitionGroup> {
+ return permissions.map((permission) => {
+ if (typeof permission === 'object') {
+ return {
+ category: permission.category,
+ permissions: permission.permissions.map((permission) =>
+ convertToPermissionDefinition(permission, l10nPrefix)
+ ),
+ };
+ }
+ return convertToPermissionDefinition(permission, l10nPrefix);
+ });
+}
+
+export function isPermissionDefinitionGroup(
+ permission?: PermissionDefinition | PermissionDefinitionGroup
+): permission is PermissionDefinitionGroup {
+ return Boolean(permission && (permission as PermissionDefinitionGroup).category);
+}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { render, RenderResult } from '@testing-library/react';
+import { omit } from 'lodash';
import * as React from 'react';
import { HelmetProvider } from 'react-helmet-async';
import { IntlProvider } from 'react-intl';
import { AppState } from '../types/appstate';
import { ComponentContextShape } from '../types/component';
import { Feature } from '../types/features';
-import { Dict, Extension, Languages, Metric, SysStatus } from '../types/types';
+import { Component, Dict, Extension, Languages, Metric, SysStatus } from '../types/types';
import { CurrentUser } from '../types/users';
+import { mockComponent } from './mocks/component';
import { DEFAULT_METRICS } from './mocks/metrics';
import { mockAppState, mockCurrentUser } from './testMocks';
indexPath: string,
routes: () => JSX.Element,
context: RenderContext = {},
- componentContext?: Partial<ComponentContextShape>
+ componentContext: Partial<ComponentContextShape> = {}
) {
function MockComponentContainer() {
+ const [realComponent, setRealComponent] = React.useState(
+ componentContext?.component ?? mockComponent()
+ );
return (
<ComponentContext.Provider
value={{
branchLikes: [],
onBranchesChange: jest.fn(),
- onComponentChange: jest.fn(),
- ...componentContext,
+ onComponentChange: (changes: Partial<Component>) => {
+ setRealComponent({ ...realComponent, ...changes });
+ },
+ component: realComponent,
+ ...omit(componentContext, 'component'),
}}
>
<Outlet />