--- /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 { cloneDeep } from 'lodash';
+import {
+ mockClusterSysInfo,
+ mockGroup,
+ mockIdentityProvider,
+ mockPaging,
+ mockUser,
+} from '../../helpers/testMocks';
+import { Group, IdentityProvider, Paging, SysInfoCluster, UserSelected } from '../../types/types';
+import { getSystemInfo } from '../system';
+import { getIdentityProviders } from '../users';
+import {
+ createGroup,
+ deleteGroup,
+ getUsersInGroup,
+ searchUsersGroups,
+ updateGroup,
+} from '../user_groups';
+
+export default class GroupsServiceMock {
+ isManaged = false;
+ paging: Paging;
+ groups: Group[];
+ readOnlyGroups = [
+ mockGroup({ name: 'managed-group', managed: true }),
+ mockGroup({ name: 'local-group', managed: false }),
+ ];
+
+ constructor() {
+ this.groups = cloneDeep(this.readOnlyGroups);
+ this.paging = mockPaging({
+ pageIndex: 1,
+ pageSize: 2,
+ total: 200,
+ });
+
+ jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo);
+ jest.mocked(getIdentityProviders).mockImplementation(this.handleGetIdentityProviders);
+ jest.mocked(searchUsersGroups).mockImplementation((p) => this.handleSearchUsersGroups(p));
+ jest.mocked(createGroup).mockImplementation((g) => this.handleCreateGroup(g));
+ jest.mocked(deleteGroup).mockImplementation((g) => this.handleDeleteGroup(g));
+ jest.mocked(updateGroup).mockImplementation((g) => this.handleUpdateGroup(g));
+ jest.mocked(getUsersInGroup).mockImplementation(this.handlegetUsersInGroup);
+ }
+
+ reset() {
+ this.groups = cloneDeep(this.readOnlyGroups);
+ }
+
+ setIsManaged(managed: boolean) {
+ this.isManaged = managed;
+ }
+
+ setPaging(paging: Partial<Paging>) {
+ this.paging = { ...this.paging, ...paging };
+ }
+
+ handleCreateGroup = (group: { name: string; description?: string }): Promise<Group> => {
+ const newGroup = mockGroup(group);
+ this.groups.push(newGroup);
+ return this.reply(newGroup);
+ };
+
+ handleDeleteGroup = (group: { name: string }): Promise<Record<string, never>> => {
+ if (!this.groups.some((g) => g.name === group.name)) {
+ return Promise.reject();
+ }
+
+ const groupToDelete = this.groups.find((g) => g.name === group.name);
+ if (groupToDelete?.managed) {
+ return Promise.reject();
+ }
+
+ this.groups = this.groups.filter((g) => g.name !== group.name);
+ return this.reply({});
+ };
+
+ handleUpdateGroup = (group: {
+ currentName: string;
+ name?: string;
+ description?: string;
+ }): Promise<Record<string, never>> => {
+ if (!this.groups.some((g) => group.currentName === g.name)) {
+ return Promise.reject();
+ }
+
+ this.groups.map((g) => {
+ if (g.name === group.currentName) {
+ if (group.name !== undefined) {
+ g.name = group.name;
+ }
+ if (group.description !== undefined) {
+ g.description = group.description;
+ }
+ }
+ });
+ return this.reply({});
+ };
+
+ handlegetUsersInGroup = (): Promise<Paging & { users: UserSelected[] }> => {
+ return this.reply({
+ ...this.paging,
+ users: [
+ {
+ ...mockUser({ name: 'alice' }),
+ selected: true,
+ } as UserSelected,
+ {
+ ...mockUser({ name: 'bob' }),
+ selected: false,
+ } as UserSelected,
+ ],
+ });
+ };
+
+ handleSearchUsersGroups = (data: {
+ f?: string;
+ p?: number;
+ ps?: number;
+ q?: string;
+ managed: boolean | undefined;
+ }): Promise<{ groups: Group[]; paging: Paging }> => {
+ const { paging } = this;
+ if (data.p !== undefined && data.p !== paging.pageIndex) {
+ this.setPaging({ pageIndex: paging.pageIndex++ });
+ const groups = [
+ mockGroup({ name: `local-group ${this.groups.length + 4}` }),
+ mockGroup({ name: `local-group ${this.groups.length + 5}` }),
+ ];
+
+ return this.reply({ paging, groups });
+ }
+ if (this.isManaged) {
+ if (data.managed === undefined) {
+ return this.reply({
+ paging,
+ groups: this.groups.filter((g) => (data?.q ? g.name.includes(data.q) : true)),
+ });
+ }
+ const groups = this.groups.filter((group) => group.managed === data.managed);
+ return this.reply({
+ paging,
+ groups: groups.filter((g) => (data?.q ? g.name.includes(data.q) : true)),
+ });
+ }
+ return this.reply({
+ paging,
+ groups: this.groups.filter((g) => (data?.q ? g.name.includes(data.q) : true)),
+ });
+ };
+
+ handleGetIdentityProviders = (): Promise<{ identityProviders: IdentityProvider[] }> => {
+ return this.reply({ identityProviders: [mockIdentityProvider()] });
+ };
+
+ handleGetSystemInfo = (): Promise<SysInfoCluster> => {
+ return this.reply(
+ mockClusterSysInfo(
+ this.isManaged
+ ? {
+ System: {
+ 'High Availability': true,
+ 'Server ID': 'asd564-asd54a-5dsfg45',
+ 'External Users and Groups Provisioning': 'Okta',
+ },
+ }
+ : {}
+ )
+ );
+ };
+
+ reply<T>(response: T): Promise<T> {
+ return Promise.resolve(cloneDeep(response));
+ }
+}
p?: number;
ps?: number;
q?: string;
+ managed: boolean | undefined;
}): Promise<{ groups: Group[]; paging: Paging }> {
return getJSON('/api/user_groups/search', data).catch(throwGlobalError);
}
import { Helmet } from 'react-helmet-async';
import { getSystemInfo } from '../../../api/system';
import { createGroup, deleteGroup, searchUsersGroups, updateGroup } from '../../../api/user_groups';
+import ButtonToggle from '../../../components/controls/ButtonToggle';
import ListFooter from '../../../components/controls/ListFooter';
import SearchBox from '../../../components/controls/SearchBox';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
paging?: Paging;
query: string;
manageProvider?: string;
+ managed: boolean | undefined;
}
export default class App extends React.PureComponent<{}, State> {
mounted = false;
- state: State = { loading: true, query: '' };
+ state: State = {
+ loading: true,
+ query: '',
+ managed: undefined,
+ paging: { pageIndex: 1, pageSize: 100, total: 1000 },
+ };
componentDidMount() {
this.mounted = true;
this.fetchManageInstance();
}
+ componentDidUpdate(_prevProps: {}, prevState: State) {
+ if (prevState.query !== this.state.query || prevState.managed !== this.state.managed) {
+ this.fetchGroups();
+ }
+ if (prevState !== undefined && prevState.paging?.pageIndex !== this.state.paging?.pageIndex) {
+ this.fetchMoreGroups();
+ }
+ }
+
componentWillUnmount() {
this.mounted = false;
}
- makeFetchGroupsRequest = (data?: { p?: number; q?: string }) => {
- this.setState({ loading: true });
- return searchUsersGroups({
- q: this.state.query,
- ...data,
- });
- };
-
async fetchManageInstance() {
const info = (await getSystemInfo()) as SysInfoCluster;
if (this.mounted) {
}
};
- fetchGroups = async (data?: { p?: number; q?: string }) => {
+ fetchGroups = async () => {
+ const { query: q, managed } = this.state;
+ this.setState({ loading: true });
try {
- const { groups, paging } = await this.makeFetchGroupsRequest(data);
+ const { groups, paging } = await searchUsersGroups({
+ q,
+ managed,
+ });
if (this.mounted) {
this.setState({ groups, loading: false, paging });
}
};
fetchMoreGroups = async () => {
- const { paging: currentPaging } = this.state;
+ const { query: q, managed, paging: currentPaging } = this.state;
if (currentPaging && currentPaging.total > currentPaging.pageIndex * currentPaging.pageSize) {
try {
- const { groups, paging } = await this.makeFetchGroupsRequest({
- p: currentPaging.pageIndex + 1,
+ const { groups, paging } = await searchUsersGroups({
+ p: currentPaging.pageIndex,
+ q,
+ managed,
});
if (this.mounted) {
this.setState(({ groups: existingGroups = [] }) => ({
}
};
- search = (query: string) => {
- this.fetchGroups({ q: query });
- this.setState({ query });
- };
-
refresh = async () => {
- const { paging, query } = this.state;
+ const { paging } = this.state;
- await this.fetchGroups({ q: query });
+ await this.fetchGroups();
// reload all pages in order
if (paging && paging.pageIndex > 1) {
}
};
- closeDeleteForm = () => {
- this.setState({ groupToBeDeleted: undefined });
- };
-
- closeEditForm = () => {
- this.setState({ editedGroup: undefined });
- };
-
- openDeleteForm = (group: Group) => {
- this.setState({ groupToBeDeleted: group });
- };
-
- openEditForm = (group: Group) => {
- this.setState({ editedGroup: group });
- };
-
handleCreate = async (data: { description: string; name: string }) => {
await createGroup({ ...data });
};
render() {
- const { editedGroup, groupToBeDeleted, groups, loading, paging, query, manageProvider } =
- this.state;
+ const {
+ editedGroup,
+ groupToBeDeleted,
+ groups,
+ loading,
+ paging,
+ query,
+ manageProvider,
+ managed,
+ } = this.state;
const showAnyone = 'anyone'.includes(query.toLowerCase());
<main className="page page-limited" id="groups-page">
<Header onCreate={this.handleCreate} manageProvider={manageProvider} />
- <SearchBox
- className="big-spacer-bottom"
- id="groups-search"
- minLength={2}
- onChange={this.search}
- placeholder={translate('search.search_by_name')}
- value={query}
- />
+ <div className="display-flex-justify-start big-spacer-bottom big-spacer-top">
+ {manageProvider !== undefined && (
+ <div className="big-spacer-right">
+ <ButtonToggle
+ value={managed === undefined ? 'all' : managed}
+ disabled={loading}
+ options={[
+ { label: translate('all'), value: 'all' },
+ { label: translate('managed'), value: true },
+ { label: translate('local'), value: false },
+ ]}
+ onCheck={(filterOption) => {
+ if (filterOption === 'all') {
+ this.setState({ managed: undefined });
+ } else {
+ this.setState({ managed: filterOption as boolean });
+ }
+ }}
+ />
+ </div>
+ )}
+ <SearchBox
+ className="big-spacer-bottom"
+ id="groups-search"
+ minLength={2}
+ onChange={(q) => this.setState({ query: q })}
+ placeholder={translate('search.search_by_name')}
+ value={query}
+ />
+ </div>
{groups !== undefined && (
<List
groups={groups}
- onDelete={this.openDeleteForm}
- onEdit={this.openEditForm}
+ onDelete={(groupToBeDeleted) => this.setState({ groupToBeDeleted })}
+ onEdit={(editedGroup) => this.setState({ editedGroup })}
onEditMembers={this.refresh}
showAnyone={showAnyone}
+ manageProvider={manageProvider}
/>
)}
<ListFooter
count={showAnyone ? groups.length + 1 : groups.length}
loading={loading}
- loadMore={this.fetchMoreGroups}
+ loadMore={() => {
+ if (paging.total > paging.pageIndex * paging.pageSize) {
+ this.setState({ paging: { ...paging, pageIndex: paging.pageIndex + 1 } });
+ }
+ }}
ready={!loading}
total={showAnyone ? paging.total + 1 : paging.total}
/>
{groupToBeDeleted && (
<DeleteForm
group={groupToBeDeleted}
- onClose={this.closeDeleteForm}
+ onClose={() => this.setState({ groupToBeDeleted: undefined })}
onSubmit={this.handleDelete}
/>
)}
confirmButtonText={translate('update_verb')}
group={editedGroup}
header={translate('groups.update_group')}
- onClose={this.closeEditForm}
+ onClose={() => this.setState({ editedGroup: undefined })}
onSubmit={this.handleEdit}
/>
)}
import * as React from 'react';
import { ButtonIcon } from '../../../components/controls/buttons';
import BulletListIcon from '../../../components/icons/BulletListIcon';
-import { translate } from '../../../helpers/l10n';
+import { translateWithParameters } from '../../../helpers/l10n';
import { Group } from '../../../types/types';
import EditMembersModal from './EditMembersModal';
return (
<>
<ButtonIcon
- aria-label={translate('groups.users.edit')}
+ aria-label={translateWithParameters('groups.users.edit', this.props.group.name)}
className="button-small"
onClick={this.handleMembersClick}
- title={translate('groups.users.edit')}
+ title={translateWithParameters('groups.users.edit', this.props.group.name)}
>
<BulletListIcon />
</ButtonIcon>
import { translate } from '../../../helpers/l10n';
import Form from './Form';
-interface Props {
+interface HeaderProps {
onCreate: (data: { description: string; name: string }) => Promise<void>;
manageProvider?: string;
}
-export default function Header(props: Props) {
+export default function Header(props: HeaderProps) {
const { manageProvider } = props;
const [createModal, setCreateModal] = React.useState(false);
onEdit: (group: Group) => void;
onEditMembers: () => void;
showAnyone: boolean;
+ manageProvider: string | undefined;
}
export default function List(props: Props) {
+ const { groups, manageProvider, showAnyone } = props;
+
return (
<div className="boxed-group boxed-group-inner">
<table className="data zebra zebra-hover" id="groups-list">
</tr>
</thead>
<tbody>
- {props.showAnyone && (
+ {showAnyone && (
<tr className="js-anyone" key="anyone">
<td className="width-20">
<strong className="js-group-name">{translate('groups.anyone')}</strong>
</tr>
)}
- {sortBy(props.groups, (group) => group.name.toLowerCase()).map((group) => (
+ {sortBy(groups, (group) => group.name.toLowerCase()).map((group) => (
<ListItem
group={group}
key={group.name}
onDelete={props.onDelete}
onEdit={props.onEdit}
onEditMembers={props.onEditMembers}
+ manageProvider={manageProvider}
/>
))}
</tbody>
ActionsDropdownDivider,
ActionsDropdownItem,
} from '../../../components/controls/ActionsDropdown';
-import { translate } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Group } from '../../../types/types';
import EditMembers from './EditMembers';
onDelete: (group: Group) => void;
onEdit: (group: Group) => void;
onEditMembers: () => void;
+ manageProvider: string | undefined;
}
export default function ListItem(props: ListItemProps) {
- const { group } = props;
+ const { manageProvider, group } = props;
+ const { name, managed, membersCount, description } = group;
+
+ const isManaged = () => {
+ return manageProvider !== undefined;
+ };
+
+ const isGroupLocal = () => {
+ return isManaged() && !managed;
+ };
return (
- <tr data-id={group.name}>
+ <tr data-id={name}>
<td className="width-20">
- <strong className="js-group-name">{group.name}</strong>
+ <strong className="js-group-name">{name}</strong>
{group.default && <span className="little-spacer-left">({translate('default')})</span>}
+ {isGroupLocal() && <span className="little-spacer-left badge">{translate('local')}</span>}
</td>
- <td className="thin text-middle text-right little-padded-right">{group.membersCount}</td>
+ <td className="thin text-middle text-right little-padded-right">{membersCount}</td>
<td className="little-padded-left">
- {!group.default && <EditMembers group={group} onEdit={props.onEditMembers} />}
+ {!group.default && !isManaged() && (
+ <EditMembers group={group} onEdit={props.onEditMembers} />
+ )}
</td>
<td className="width-40">
- <span className="js-group-description">{group.description}</span>
+ <span className="js-group-description">{description}</span>
</td>
<td className="thin nowrap text-right">
- {!group.default && (
- <ActionsDropdown>
- <ActionsDropdownItem className="js-group-update" onClick={() => props.onEdit(group)}>
- {translate('update_details')}
- </ActionsDropdownItem>
- <ActionsDropdownDivider />
- <ActionsDropdownItem
- className="js-group-delete"
- destructive={true}
- onClick={() => props.onDelete(group)}
- >
- {translate('delete')}
- </ActionsDropdownItem>
+ {!group.default && (!isManaged() || isGroupLocal()) && (
+ <ActionsDropdown label={translateWithParameters('groups.edit', group.name)}>
+ {!isManaged() && (
+ <>
+ <ActionsDropdownItem
+ className="js-group-update"
+ onClick={() => props.onEdit(group)}
+ >
+ {translate('update_details')}
+ </ActionsDropdownItem>
+ <ActionsDropdownDivider />
+ </>
+ )}
+ {(!isManaged() || isGroupLocal()) && (
+ <ActionsDropdownItem
+ className="js-group-delete"
+ destructive={true}
+ onClick={() => props.onDelete(group)}
+ >
+ {translate('delete')}
+ </ActionsDropdownItem>
+ )}
</ActionsDropdown>
)}
</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 { shallow } from 'enzyme';
-import * as React from 'react';
-import {
- createGroup,
- deleteGroup,
- searchUsersGroups,
- updateGroup,
-} from '../../../../api/user_groups';
-import { mockGroup } from '../../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-import App from '../App';
-
-jest.mock('../../../../api/user_groups', () => ({
- createGroup: jest.fn().mockResolvedValue({
- default: false,
- description: 'Desc foo',
- membersCount: 0,
- name: 'Foo',
- }),
- deleteGroup: jest.fn().mockResolvedValue({}),
- searchUsersGroups: jest.fn().mockResolvedValue({
- paging: { pageIndex: 1, pageSize: 2, total: 4 },
- groups: [
- {
- default: false,
- description: 'Owners of organization foo',
- membersCount: 1,
- name: 'Owners',
- },
- {
- default: true,
- description: 'Members of organization foo',
- membersCount: 2,
- name: 'Members',
- },
- ],
- }),
- updateGroup: jest.fn().mockResolvedValue({}),
-}));
-
-jest.mock('../../../../api/system', () => ({
- getSystemInfo: jest.fn().mockResolvedValue({ System: {} }),
-}));
-
-beforeEach(() => {
- jest.clearAllMocks();
-});
-
-it('should render correctly', async () => {
- const wrapper = shallowRender();
- expect(wrapper).toMatchSnapshot();
- await waitAndUpdate(wrapper);
- expect(searchUsersGroups).toHaveBeenCalledWith({ q: '' });
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should correctly handle creation', async () => {
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper.state('groups')).toHaveLength(2);
- wrapper.instance().handleCreate({ description: 'Desc foo', name: 'foo' });
- await waitAndUpdate(wrapper);
- expect(createGroup).toHaveBeenCalled();
-});
-
-it('should correctly handle deletion', async () => {
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper.state('groups')).toHaveLength(2);
- wrapper.setState({ groupToBeDeleted: mockGroup({ name: 'Members' }) });
- wrapper.instance().handleDelete();
- await waitAndUpdate(wrapper);
- expect(deleteGroup).toHaveBeenCalled();
- expect(wrapper.state().groupToBeDeleted).toBeUndefined();
-});
-
-it('should ignore deletion', async () => {
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- wrapper.setState({ groupToBeDeleted: undefined });
- wrapper.instance().handleDelete();
- expect(deleteGroup).not.toHaveBeenCalled();
-});
-
-it('should correctly handle edition', async () => {
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- wrapper.setState({ editedGroup: mockGroup({ name: 'Owners' }) });
- wrapper.instance().handleEdit({ description: 'foo', name: 'bar' });
- await waitAndUpdate(wrapper);
- expect(updateGroup).toHaveBeenCalled();
- expect(wrapper.state('groups')).toContainEqual({
- default: false,
- description: 'foo',
- membersCount: 1,
- name: 'bar',
- });
-});
-
-it('should ignore edition', async () => {
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- wrapper.setState({ editedGroup: undefined });
- wrapper.instance().handleEdit({ description: 'nope', name: 'nuhuh' });
- expect(updateGroup).not.toHaveBeenCalled();
-});
-
-it('should fetch more groups', async () => {
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- wrapper.find('ListFooter').prop<Function>('loadMore')();
- await waitAndUpdate(wrapper);
- expect(searchUsersGroups).toHaveBeenCalledWith({ p: 2, q: '' });
- expect(wrapper.state('groups')).toHaveLength(4);
-});
-
-it('should search for groups', async () => {
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- wrapper.find('SearchBox').prop<Function>('onChange')('foo');
- expect(searchUsersGroups).toHaveBeenCalledWith({ q: 'foo' });
- expect(wrapper.state('query')).toBe('foo');
-});
-
-it('should handle edit modal', async () => {
- const editedGroup = mockGroup();
-
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper.state().editedGroup).toBeUndefined();
-
- wrapper.instance().openEditForm(editedGroup);
- expect(wrapper.state().editedGroup).toEqual(editedGroup);
-
- wrapper.instance().closeEditForm();
- expect(wrapper.state().editedGroup).toBeUndefined();
-});
-
-it('should handle delete modal', async () => {
- const groupToBeDeleted = mockGroup();
-
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper.state().groupToBeDeleted).toBeUndefined();
-
- wrapper.instance().openDeleteForm(groupToBeDeleted);
- expect(wrapper.state().groupToBeDeleted).toEqual(groupToBeDeleted);
-
- wrapper.instance().closeDeleteForm();
- expect(wrapper.state().groupToBeDeleted).toBeUndefined();
-});
-
-it('should refresh correctly', async () => {
- const wrapper = shallowRender();
-
- await waitAndUpdate(wrapper);
-
- const query = 'preserve me';
- wrapper.setState({ paging: { pageIndex: 2, pageSize: 2, total: 5 }, query });
-
- (searchUsersGroups as jest.Mock).mockClear();
-
- wrapper.instance().refresh();
- await waitAndUpdate(wrapper);
-
- expect(searchUsersGroups).toHaveBeenNthCalledWith(1, { q: query });
- expect(searchUsersGroups).toHaveBeenNthCalledWith(2, { q: query, p: 2 });
-});
-
-function shallowRender(props: Partial<App['props']> = {}) {
- return shallow<App>(<App {...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 { shallow } from 'enzyme';
-import * as React from 'react';
-import DeleteForm from '../DeleteForm';
-
-it('should render', () => {
- const group = { id: 3, name: 'Foo', membersCount: 5 };
- expect(
- shallow(<DeleteForm group={group} onClose={jest.fn()} onSubmit={jest.fn()} />).dive()
- ).toMatchSnapshot();
-});
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { mockGroup } from '../../../../helpers/testMocks';
import { click } from '../../../../helpers/testUtils';
import EditMembers from '../EditMembers';
it('should edit members', () => {
- const group = { id: 3, name: 'Foo', membersCount: 5 };
+ const group = mockGroup({ name: 'Foo', membersCount: 5 });
const onEdit = jest.fn();
const wrapper = shallow(<EditMembers group={group} onEdit={onEdit} />);
import * as React from 'react';
import { addUserToGroup, getUsersInGroup, removeUserFromGroup } from '../../../../api/user_groups';
import SelectList, { SelectListFilter } from '../../../../components/controls/SelectList';
+import { mockGroup } from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
import EditMembersModal from '../EditMembersModal';
-const group = { id: 1, name: 'foo', membersCount: 1 };
+const group = mockGroup({ name: 'foo', membersCount: 1 });
jest.mock('../../../../api/user_groups', () => ({
getUsersInGroup: jest.fn().mockResolvedValue({
+++ /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 { change, click, submit } from '../../../../helpers/testUtils';
-import Form from '../Form';
-
-it('should render form', async () => {
- const onClose = jest.fn();
- const onSubmit = jest.fn(() => Promise.resolve());
- const wrapper = shallow(
- <Form
- confirmButtonText="confirmButtonText"
- header="header"
- onClose={onClose}
- onSubmit={onSubmit}
- />
- ).dive();
- expect(wrapper).toMatchSnapshot();
-
- change(wrapper.find('[name="name"]'), 'foo');
- change(wrapper.find('[name="description"]'), 'bar');
- submit(wrapper.find('form'));
- expect(onSubmit).toHaveBeenCalledWith({ description: 'bar', name: 'foo' });
-
- await new Promise(setImmediate);
- expect(onClose).toHaveBeenCalled();
-
- onClose.mockClear();
- click(wrapper.find('ResetButtonLink'));
- expect(onClose).toHaveBeenCalled();
-});
--- /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 * as React from 'react';
+import { act } from 'react-dom/test-utils';
+import { byRole, byText } from 'testing-library-selector';
+import GroupsServiceMock from '../../../../api/mocks/GroupsServiceMock';
+import { renderApp } from '../../../../helpers/testReactTestingUtils';
+import App from '../App';
+
+jest.mock('../../../../api/users');
+jest.mock('../../../../api/system');
+jest.mock('../../../../api/user_groups');
+
+const handler = new GroupsServiceMock();
+
+const ui = {
+ createGroupButton: byRole('button', { name: 'groups.create_group' }),
+ infoManageMode: byText(/groups\.page\.managed_description/),
+ description: byText('user_groups.page.description'),
+ allFilter: byRole('button', { name: 'all' }),
+ managedFilter: byRole('button', { name: 'managed' }),
+ localFilter: byRole('button', { name: 'local' }),
+ searchInput: byRole('searchbox', { name: 'search.search_by_name' }),
+ updateButton: byRole('button', { name: 'update_details' }),
+ updateDialog: byRole('dialog', { name: 'groups.update_group' }),
+ updateDialogButton: byRole('button', { name: 'update_verb' }),
+ deleteButton: byRole('button', { name: 'delete' }),
+ deleteDialog: byRole('dialog', { name: 'groups.delete_group' }),
+ deleteDialogButton: byRole('button', { name: 'delete' }),
+ showMore: byRole('button', { name: 'show_more' }),
+ nameInput: byRole('textbox', { name: 'name field_required' }),
+ descriptionInput: byRole('textbox', { name: 'description' }),
+ createGroupDialogButton: byRole('button', { name: 'create' }),
+ editGroupDialogButton: byRole('button', { name: 'groups.create_group' }),
+
+ createGroupDialog: byRole('dialog', { name: 'groups.create_group' }),
+ membersDialog: byRole('dialog', { name: 'users.update' }),
+
+ managedGroupRow: byRole('row', { name: 'managed-group 1' }),
+ managedGroupEditMembersButton: byRole('button', { name: 'groups.users.edit.managed-group' }),
+ managedEditButton: byRole('button', { name: 'groups.edit.managed-group' }),
+
+ localGroupRow: byRole('row', { name: 'local-group 1' }),
+ localGroupEditMembersButton: byRole('button', { name: 'groups.users.edit.local-group' }),
+ localGroupRow2: byRole('row', { name: 'local-group 2 1 group 2 is loco!' }),
+ editedLocalGroupRow: byRole('row', { name: 'local-group 3 1 group 3 rocks!' }),
+ localEditButton: byRole('button', { name: 'groups.edit.local-group' }),
+ localGroupRowWithLocalBadge: byRole('row', {
+ name: 'local-group local 1',
+ }),
+};
+
+describe('in non managed mode', () => {
+ beforeEach(() => {
+ handler.setIsManaged(false);
+ handler.reset();
+ });
+
+ it('should render all groups', async () => {
+ renderGroupsApp();
+
+ expect(await ui.localGroupRow.find()).toBeInTheDocument();
+ expect(ui.managedGroupRow.get()).toBeInTheDocument();
+ expect(ui.localGroupRowWithLocalBadge.query()).not.toBeInTheDocument();
+ });
+
+ it('should be able to create a group', async () => {
+ const user = userEvent.setup();
+ renderGroupsApp();
+
+ expect(await ui.description.find()).toBeInTheDocument();
+
+ await user.click(ui.createGroupButton.get());
+ expect(ui.createGroupDialog.get()).toBeInTheDocument();
+
+ await user.type(ui.nameInput.get(), 'local-group 2');
+ await user.type(ui.descriptionInput.get(), 'group 2 is loco!');
+
+ await act(async () => {
+ await user.click(ui.createGroupDialogButton.get());
+ });
+
+ expect(await ui.localGroupRow2.find()).toBeInTheDocument();
+ });
+
+ it('should be able to delete a group', async () => {
+ const user = userEvent.setup();
+ renderGroupsApp();
+
+ await user.click(await ui.localEditButton.find());
+ await user.click(await ui.deleteButton.find());
+
+ expect(await ui.deleteDialog.find()).toBeInTheDocument();
+ await act(async () => {
+ await user.click(ui.deleteDialogButton.get());
+ });
+
+ expect(await ui.managedGroupRow.find()).toBeInTheDocument();
+ expect(ui.localGroupRow.query()).not.toBeInTheDocument();
+ });
+
+ it('should be able to edit a group', async () => {
+ const user = userEvent.setup();
+ renderGroupsApp();
+
+ await user.click(await ui.localEditButton.find());
+ await user.click(await ui.updateButton.find());
+
+ expect(ui.updateDialog.get()).toBeInTheDocument();
+
+ await user.clear(ui.nameInput.get());
+ await user.type(ui.nameInput.get(), 'local-group 3');
+ await user.clear(ui.descriptionInput.get());
+ await user.type(ui.descriptionInput.get(), 'group 3 rocks!');
+
+ expect(ui.updateDialog.get()).toBeInTheDocument();
+
+ await act(async () => {
+ await user.click(ui.updateDialogButton.get());
+ });
+
+ expect(await ui.managedGroupRow.find()).toBeInTheDocument();
+ expect(await ui.editedLocalGroupRow.find()).toBeInTheDocument();
+ });
+
+ it('should be able to edit the members of a group', async () => {
+ const user = userEvent.setup();
+ renderGroupsApp();
+
+ expect(await ui.localGroupRow.find()).toBeInTheDocument();
+ expect(await ui.localGroupEditMembersButton.find()).toBeInTheDocument();
+
+ await user.click(ui.localGroupEditMembersButton.get());
+ expect(await ui.membersDialog.find()).toBeInTheDocument();
+ });
+
+ it('should be able search a group', async () => {
+ const user = userEvent.setup();
+ renderGroupsApp();
+
+ expect(await ui.localGroupRow.find()).toBeInTheDocument();
+ expect(ui.managedGroupRow.get()).toBeInTheDocument();
+
+ await user.type(await ui.searchInput.find(), 'local');
+
+ expect(await ui.localGroupRow.find()).toBeInTheDocument();
+ expect(ui.managedGroupRow.query()).not.toBeInTheDocument();
+ });
+
+ it('should be able load more group', async () => {
+ const user = userEvent.setup();
+ renderGroupsApp();
+
+ // including the anyone (deprecated) group
+ expect(await screen.findAllByRole('row')).toHaveLength(4);
+
+ await user.click(await ui.showMore.find());
+
+ expect(await screen.findAllByRole('row')).toHaveLength(6);
+ });
+});
+
+describe('in manage mode', () => {
+ beforeEach(() => {
+ handler.setIsManaged(true);
+ handler.reset();
+ });
+
+ it('should not be able to create a group', async () => {
+ renderGroupsApp();
+ expect(await ui.createGroupButton.find()).toBeDisabled();
+ expect(ui.infoManageMode.get()).toBeInTheDocument();
+ });
+
+ it('should ONLY be able to delete a local group', async () => {
+ const user = userEvent.setup();
+ renderGroupsApp();
+
+ expect(await ui.localGroupRowWithLocalBadge.find()).toBeInTheDocument();
+
+ await user.click(await ui.localFilter.find());
+ await user.click(await ui.localEditButton.find());
+ expect(ui.updateButton.query()).not.toBeInTheDocument();
+
+ await user.click(await ui.deleteButton.find());
+
+ expect(await ui.deleteDialog.find()).toBeInTheDocument();
+ await act(async () => {
+ await user.click(ui.deleteDialogButton.get());
+ });
+ expect(ui.localGroupRowWithLocalBadge.query()).not.toBeInTheDocument();
+ });
+
+ it('should not be able to delete or edit a managed group', async () => {
+ renderGroupsApp();
+
+ expect(await ui.managedGroupRow.find()).toBeInTheDocument();
+ expect(ui.managedEditButton.query()).not.toBeInTheDocument();
+
+ expect(ui.managedGroupEditMembersButton.query()).not.toBeInTheDocument();
+ });
+
+ it('should render list of all groups', async () => {
+ renderGroupsApp();
+
+ expect(await ui.allFilter.find()).toBeInTheDocument();
+
+ expect(ui.localGroupRowWithLocalBadge.get()).toBeInTheDocument();
+ expect(ui.managedGroupRow.get()).toBeInTheDocument();
+ });
+
+ it('should render list of managed groups', async () => {
+ const user = userEvent.setup();
+ renderGroupsApp();
+
+ await user.click(await ui.managedFilter.find());
+
+ expect(ui.localGroupRow.query()).not.toBeInTheDocument();
+ expect(ui.managedGroupRow.get()).toBeInTheDocument();
+ });
+
+ it('should render list of local groups', async () => {
+ const user = userEvent.setup();
+ renderGroupsApp();
+
+ await user.click(await ui.localFilter.find());
+
+ expect(ui.localGroupRowWithLocalBadge.get()).toBeInTheDocument();
+ expect(ui.managedGroupRow.query()).not.toBeInTheDocument();
+ });
+});
+
+function renderGroupsApp() {
+ return renderApp('admin/groups', <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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { click } from '../../../../helpers/testUtils';
-import Header from '../Header';
-
-it('should create new group', () => {
- const onCreate = jest.fn(() => Promise.resolve());
- const wrapper = shallow(<Header onCreate={onCreate} />);
- expect(wrapper).toMatchSnapshot();
-
- click(wrapper.find('[id="groups-create"]'));
- expect(wrapper).toMatchSnapshot();
-
- wrapper.find('Form').prop<Function>('onSubmit')({ name: 'foo', description: 'bar' });
- expect(onCreate).toHaveBeenCalledWith({ name: 'foo', description: 'bar' });
-});
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { mockGroup } from '../../../../helpers/testMocks';
import List from '../List';
it('should render', () => {
function shallowRender(showAnyone = true) {
const groups = [
- { id: 1, name: 'sonar-users', description: '', membersCount: 55, default: true },
- { id: 2, name: 'foo', description: 'foobar', membersCount: 0, default: false },
- { id: 3, name: 'bar', description: 'barbar', membersCount: 1, default: false },
+ mockGroup({ name: 'sonar-users', description: '', membersCount: 55, default: true }),
+ mockGroup({ name: 'foo', description: 'foobar', membersCount: 0, default: false }),
+ mockGroup({ name: 'bar', description: 'barbar', membersCount: 1, default: false }),
];
return shallow(
<List
onEdit={jest.fn()}
onEditMembers={jest.fn()}
showAnyone={showAnyone}
+ manageProvider={undefined}
/>
);
}
onDelete={jest.fn()}
onEdit={jest.fn()}
onEditMembers={jest.fn()}
+ manageProvider={undefined}
{...overrides}
/>
);
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<Fragment>
- <Suggestions
- suggestions="user_groups"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="user_groups.page"
- />
- <main
- className="page page-limited"
- id="groups-page"
- >
- <Header
- onCreate={[Function]}
- />
- <SearchBox
- className="big-spacer-bottom"
- id="groups-search"
- minLength={2}
- onChange={[Function]}
- placeholder="search.search_by_name"
- value=""
- />
- </main>
-</Fragment>
-`;
-
-exports[`should render correctly 2`] = `
-<Fragment>
- <Suggestions
- suggestions="user_groups"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="user_groups.page"
- />
- <main
- className="page page-limited"
- id="groups-page"
- >
- <Header
- onCreate={[Function]}
- />
- <SearchBox
- className="big-spacer-bottom"
- id="groups-search"
- minLength={2}
- onChange={[Function]}
- placeholder="search.search_by_name"
- value=""
- />
- <List
- groups={
- [
- {
- "default": false,
- "description": "Owners of organization foo",
- "membersCount": 1,
- "name": "Owners",
- },
- {
- "default": true,
- "description": "Members of organization foo",
- "membersCount": 2,
- "name": "Members",
- },
- ]
- }
- onDelete={[Function]}
- onEdit={[Function]}
- onEditMembers={[Function]}
- showAnyone={true}
- />
- <div
- id="groups-list-footer"
- >
- <ListFooter
- count={3}
- loadMore={[Function]}
- loading={false}
- ready={true}
- total={5}
- />
- </div>
- </main>
-</Fragment>
-`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render 1`] = `
-<Modal
- contentLabel="groups.delete_group"
- onRequestClose={[MockFunction]}
->
- <form
- onSubmit={[Function]}
- >
- <header
- className="modal-head"
- >
- <h2>
- groups.delete_group
- </h2>
- </header>
- <div
- className="modal-body"
- >
- groups.delete_group.confirmation.Foo
- </div>
- <footer
- className="modal-foot"
- >
- <DeferredSpinner
- className="spacer-right"
- loading={false}
- />
- <SubmitButton
- className="button-red"
- disabled={false}
- >
- delete
- </SubmitButton>
- <ResetButtonLink
- disabled={false}
- onClick={[Function]}
- >
- cancel
- </ResetButtonLink>
- </footer>
- </form>
-</Modal>
-`;
exports[`should edit members 1`] = `
<Fragment>
<ButtonIcon
- aria-label="groups.users.edit"
+ aria-label="groups.users.edit.Foo"
className="button-small"
onClick={[Function]}
- title="groups.users.edit"
+ title="groups.users.edit.Foo"
>
<BulletListIcon />
</ButtonIcon>
exports[`should edit members 2`] = `
<Fragment>
<ButtonIcon
- aria-label="groups.users.edit"
+ aria-label="groups.users.edit.Foo"
className="button-small"
onClick={[Function]}
- title="groups.users.edit"
+ title="groups.users.edit.Foo"
>
<BulletListIcon />
</ButtonIcon>
<EditMembersModal
group={
{
- "id": 3,
+ "managed": false,
"membersCount": 5,
"name": "Foo",
}
exports[`should edit members 3`] = `
<Fragment>
<ButtonIcon
- aria-label="groups.users.edit"
+ aria-label="groups.users.edit.Foo"
className="button-small"
onClick={[Function]}
- title="groups.users.edit"
+ title="groups.users.edit.Foo"
>
<BulletListIcon />
</ButtonIcon>
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render form 1`] = `
-<Modal
- contentLabel="header"
- onRequestClose={[MockFunction]}
- size="small"
->
- <form
- onSubmit={[Function]}
- >
- <header
- className="modal-head"
- >
- <h2>
- header
- </h2>
- </header>
- <div
- className="modal-body"
- >
- <MandatoryFieldsExplanation
- className="modal-field"
- />
- <div
- className="modal-field"
- >
- <label
- htmlFor="create-group-name"
- >
- name
- <MandatoryFieldMarker />
- </label>
- <input
- autoFocus={true}
- id="create-group-name"
- maxLength={255}
- name="name"
- onChange={[Function]}
- required={true}
- size={50}
- type="text"
- value=""
- />
- </div>
- <div
- className="modal-field"
- >
- <label
- htmlFor="create-group-description"
- >
- description
- </label>
- <textarea
- id="create-group-description"
- name="description"
- onChange={[Function]}
- value=""
- />
- </div>
- </div>
- <footer
- className="modal-foot"
- >
- <DeferredSpinner
- className="spacer-right"
- loading={false}
- />
- <SubmitButton
- disabled={false}
- >
- confirmButtonText
- </SubmitButton>
- <ResetButtonLink
- onClick={[Function]}
- >
- cancel
- </ResetButtonLink>
- </footer>
- </form>
-</Modal>
-`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should create new group 1`] = `
-<Fragment>
- <div
- className="page-header"
- id="groups-header"
- >
- <h2
- className="page-title"
- >
- user_groups.page
- </h2>
- <div
- className="page-actions"
- >
- <Button
- disabled={false}
- id="groups-create"
- onClick={[Function]}
- >
- groups.create_group
- </Button>
- </div>
- <p
- className="page-description"
- >
- user_groups.page.description
- </p>
- </div>
-</Fragment>
-`;
-
-exports[`should create new group 2`] = `
-<Fragment>
- <div
- className="page-header"
- id="groups-header"
- >
- <h2
- className="page-title"
- >
- user_groups.page
- </h2>
- <div
- className="page-actions"
- >
- <Button
- disabled={false}
- id="groups-create"
- onClick={[Function]}
- >
- groups.create_group
- </Button>
- </div>
- <p
- className="page-description"
- >
- user_groups.page.description
- </p>
- </div>
- <Form
- confirmButtonText="create"
- header="groups.create_group"
- onClose={[Function]}
- onSubmit={[MockFunction]}
- />
-</Fragment>
-`;
{
"default": false,
"description": "barbar",
- "id": 3,
+ "managed": false,
"membersCount": 1,
"name": "bar",
}
{
"default": false,
"description": "foobar",
- "id": 2,
+ "managed": false,
"membersCount": 0,
"name": "foo",
}
{
"default": true,
"description": "",
- "id": 1,
+ "managed": false,
"membersCount": 55,
"name": "sonar-users",
}
<EditMembers
group={
{
+ "managed": false,
"membersCount": 1,
"name": "Foo",
}
<td
className="thin nowrap text-right"
>
- <ActionsDropdown>
+ <ActionsDropdown
+ label="groups.edit.Foo"
+ >
<ActionsDropdownItem
className="js-group-update"
onClick={[Function]}
import DocLink from '../../components/common/DocLink';
import { Button } from '../../components/controls/buttons';
import { Alert } from '../../components/ui/Alert';
-import DeferredSpinner from '../../components/ui/DeferredSpinner';
import { translate } from '../../helpers/l10n';
import UserForm from './components/UserForm';
interface Props {
- loading: boolean;
onUpdateUsers: () => void;
manageProvider?: string;
}
export default function Header(props: Props) {
const [openUserForm, setOpenUserForm] = React.useState(false);
- const { manageProvider, loading } = props;
+ const { manageProvider } = props;
return (
<div className="page-header null-spacer-bottom">
<h2 className="page-title">{translate('users.page')}</h2>
- <DeferredSpinner loading={loading} />
<div className="page-actions">
<Button
render() {
const { search, managed } = parseQuery(this.props.location.query);
const { loading, paging, users, manageProvider } = this.state;
- // What if we have ONLY managed users? Should we not display the filter toggle?
+
return (
<main className="page page-limited" id="users-page">
<Suggestions suggestions="users" />
<Helmet defer={false} title={translate('users.page')} />
- <Header loading={loading} onUpdateUsers={this.fetchUsers} manageProvider={manageProvider} />
+ <Header onUpdateUsers={this.fetchUsers} manageProvider={manageProvider} />
<div className="display-flex-justify-start big-spacer-bottom big-spacer-top">
{manageProvider !== undefined && (
<div className="big-spacer-right">
<ButtonToggle
value={managed === undefined ? 'all' : managed}
+ disabled={loading}
options={[
{ label: translate('all'), value: 'all' },
- { label: translate('users.managed'), value: true },
- { label: translate('users.local'), value: false },
+ { label: translate('managed'), value: true },
+ { label: translate('local'), value: false },
]}
onCheck={(filterOption) => {
if (filterOption === 'all') {
<th className="nowrap">{translate('users.last_connection')}</th>
<th className="nowrap">{translate('my_profile.groups')}</th>
<th className="nowrap">{translate('users.tokens')}</th>
- <th className="nowrap"> </th>
+ {(manageProvider === undefined || users.some((u) => !u.managed)) && (
+ <th className="nowrap"> </th>
+ )}
</tr>
</thead>
<tbody>
});
function getWrapper(props = {}) {
- return shallow(<Header loading={true} onUpdateUsers={jest.fn()} {...props} />);
+ return shallow(<Header onUpdateUsers={jest.fn()} {...props} />);
}
infoManageMode: byText(/users\.page\.managed_description/),
description: byText('users.page.description'),
allFilter: byRole('button', { name: 'all' }),
- managedFilter: byRole('button', { name: 'users.managed' }),
- localFilter: byRole('button', { name: 'users.local' }),
+ managedFilter: byRole('button', { name: 'managed' }),
+ localFilter: byRole('button', { name: 'local' }),
aliceRow: byRole('row', { name: 'AM Alice Merveille alice.merveille never' }),
aliceRowWithLocalBadge: byRole('row', {
- name: 'AM Alice Merveille alice.merveille users.local never',
+ name: 'AM Alice Merveille alice.merveille local never',
}),
+ aliceUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.alice.merveille' }),
+ aliceUpdateButton: byRole('button', { name: 'users.manage_user.alice.merveille' }),
+ alicedDeactivateButton: byRole('button', { name: 'users.deactivate' }),
+ bobUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.bob.marley' }),
+ bobUpdateButton: byRole('button', { name: 'users.manage_user.bob.marley' }),
bobRow: byRole('row', { name: 'BM Bob Marley bob.marley never' }),
};
expect(ui.createUserButton.get()).toBeEnabled();
});
+ it("should be able to add/remove user's group", async () => {
+ renderUsersApp();
+
+ expect(await ui.aliceUpdateGroupButton.find()).toBeInTheDocument();
+ expect(await ui.bobUpdateGroupButton.find()).toBeInTheDocument();
+ });
+
+ it('should be able to update / change password / deactivate a user', async () => {
+ renderUsersApp();
+
+ expect(await ui.aliceUpdateButton.find()).toBeInTheDocument();
+ expect(await ui.bobUpdateButton.find()).toBeInTheDocument();
+ });
+
it('should render all users', async () => {
renderUsersApp();
expect(await ui.infoManageMode.find()).toBeInTheDocument();
});
+ it("should not be able to add/remove a user's group", async () => {
+ renderUsersApp();
+
+ expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument();
+ expect(ui.aliceUpdateGroupButton.query()).not.toBeInTheDocument();
+
+ expect(await ui.bobRow.find()).toBeInTheDocument();
+ expect(ui.bobUpdateGroupButton.query()).not.toBeInTheDocument();
+ });
+
+ it('should not be able to update / change password / deactivate a managed user', async () => {
+ renderUsersApp();
+
+ expect(await ui.bobRow.find()).toBeInTheDocument();
+ expect(ui.bobUpdateButton.query()).not.toBeInTheDocument();
+ });
+
+ it('should ONLY be able to deactivate a local user', async () => {
+ const user = userEvent.setup();
+ renderUsersApp();
+
+ expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument();
+ await user.click(ui.aliceUpdateButton.get());
+ expect(await ui.alicedDeactivateButton.get()).toBeInTheDocument();
+ });
+
it('should render list of all users', async () => {
renderUsersApp();
const user = userEvent.setup();
renderUsersApp();
- // The click downs't work without this line
- expect(await ui.managedFilter.find()).toBeInTheDocument();
- await user.click(await ui.managedFilter.get());
+ await user.click(await ui.managedFilter.find());
expect(ui.aliceRowWithLocalBadge.query()).not.toBeInTheDocument();
expect(ui.bobRow.get()).toBeInTheDocument();
const user = userEvent.setup();
renderUsersApp();
- // The click downs't work without this line
- expect(await ui.localFilter.find()).toBeInTheDocument();
- await user.click(await ui.localFilter.get());
+ await user.click(await ui.localFilter.find());
expect(ui.aliceRowWithLocalBadge.get()).toBeInTheDocument();
expect(ui.bobRow.query()).not.toBeInTheDocument();
>
users.page
</h2>
- <DeferredSpinner
- loading={true}
- />
<div
className="page-actions"
>
ActionsDropdownDivider,
ActionsDropdownItem,
} from '../../../components/controls/ActionsDropdown';
-import { translate } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { isUserActive, User } from '../../../types/users';
import DeactivateForm from './DeactivateForm';
import PasswordForm from './PasswordForm';
isCurrentUser: boolean;
onUpdateUsers: () => void;
user: User;
+ manageProvider: string | undefined;
}
interface State {
this.setState({ openForm: undefined });
};
+ isInstanceManaged = () => {
+ return this.props.manageProvider !== undefined;
+ };
+
+ isUserLocal = () => {
+ return this.isInstanceManaged() && !this.props.user.managed;
+ };
+
+ isUserManaged = () => {
+ return this.isInstanceManaged() && this.props.user.managed;
+ };
+
renderActions = () => {
const { user } = this.props;
+
return (
- <ActionsDropdown>
- <ActionsDropdownItem className="js-user-update" onClick={this.handleOpenUpdateForm}>
- {translate('update_details')}
- </ActionsDropdownItem>
- {user.local && (
- <ActionsDropdownItem
- className="js-user-change-password"
- onClick={this.handleOpenPasswordForm}
- >
- {translate('my_profile.password.title')}
- </ActionsDropdownItem>
+ <ActionsDropdown label={translateWithParameters('users.manage_user', user.login)}>
+ {!this.isInstanceManaged() && (
+ <>
+ <ActionsDropdownItem className="js-user-update" onClick={this.handleOpenUpdateForm}>
+ {translate('update_details')}
+ </ActionsDropdownItem>
+ {user.local && (
+ <ActionsDropdownItem
+ className="js-user-change-password"
+ onClick={this.handleOpenPasswordForm}
+ >
+ {translate('my_profile.password.title')}
+ </ActionsDropdownItem>
+ )}
+ </>
)}
- <ActionsDropdownDivider />
- {isUserActive(user) && (
+
+ {isUserActive(user) && !this.isInstanceManaged() && <ActionsDropdownDivider />}
+ {isUserActive(user) && (!this.isInstanceManaged() || this.isUserLocal()) && (
<ActionsDropdownItem
className="js-user-deactivate"
destructive={true}
const { openForm } = this.state;
const { isCurrentUser, onUpdateUsers, user } = this.props;
+ if (this.isUserManaged()) {
+ return null;
+ }
+
return (
<>
{this.renderActions()}
groups: string[];
onUpdateUsers: () => void;
user: User;
+ manageProvider: string | undefined;
}
interface State {
};
render() {
- const { groups } = this.props;
+ const { groups, user, manageProvider } = this.props;
+ const { showMore, openForm } = this.state;
const limit = groups.length > GROUPS_LIMIT ? GROUPS_LIMIT - 1 : GROUPS_LIMIT;
return (
<ul>
</li>
))}
{groups.length > GROUPS_LIMIT &&
- this.state.showMore &&
+ showMore &&
groups.slice(limit).map((group) => (
<li className="little-spacer-bottom" key={group}>
{group}
</li>
))}
<li className="little-spacer-bottom">
- {groups.length > GROUPS_LIMIT && !this.state.showMore && (
+ {groups.length > GROUPS_LIMIT && !showMore && (
<a className="js-user-more-groups spacer-right" href="#" onClick={this.toggleShowMore}>
{translateWithParameters('more_x', groups.length - limit)}
</a>
)}
- <ButtonIcon
- className="js-user-groups button-small"
- onClick={this.handleOpenForm}
- tooltip={translate('users.update_groups')}
- >
- <BulletListIcon />
- </ButtonIcon>
+ {manageProvider === undefined && (
+ <ButtonIcon
+ aria-label={translateWithParameters('users.update_users_groups', user.login)}
+ className="js-user-groups button-small"
+ onClick={this.handleOpenForm}
+ tooltip={translate('users.update_groups')}
+ >
+ <BulletListIcon />
+ </ButtonIcon>
+ )}
</li>
- {this.state.openForm && (
+ {openForm && (
<GroupsForm
onClose={this.handleCloseForm}
onUpdateUsers={this.props.onUpdateUsers}
- user={this.props.user}
+ user={user}
/>
)}
</ul>
<DateFromNow date={user.lastConnectionDate} hourPrecision={true} />
</td>
<td className="thin nowrap text-middle">
- <UserGroups groups={user.groups || []} onUpdateUsers={onUpdateUsers} user={user} />
+ <UserGroups
+ groups={user.groups || []}
+ manageProvider={manageProvider}
+ onUpdateUsers={onUpdateUsers}
+ user={user}
+ />
</td>
<td className="thin nowrap text-middle">
{user.tokensCount}
<BulletListIcon />
</ButtonIcon>
</td>
- <td className="thin nowrap text-right text-middle">
- <UserActions isCurrentUser={isCurrentUser} onUpdateUsers={onUpdateUsers} user={user} />
- </td>
+
+ {(manageProvider === undefined || !user.managed) && (
+ <td className="thin nowrap text-right text-middle">
+ <UserActions
+ isCurrentUser={isCurrentUser}
+ onUpdateUsers={onUpdateUsers}
+ user={user}
+ manageProvider={manageProvider}
+ />
+ </td>
+ )}
+
{openTokenForm && (
<TokensFormModal
onClose={() => setOpenTokenForm(false)}
{!user.local && user.externalProvider !== 'sonarqube' && (
<ExternalProvider identityProvider={identityProvider} user={user} />
)}
- {user.managed === false && manageProvider !== undefined && (
- <span className="badge">{translate('users.local')}</span>
+ {!user.managed && manageProvider !== undefined && (
+ <span className="badge">{translate('local')}</span>
)}
</td>
);
function getWrapper(props = {}) {
return shallow(
- <UserActions isCurrentUser={false} onUpdateUsers={jest.fn()} user={user} {...props} />
+ <UserActions
+ isCurrentUser={false}
+ onUpdateUsers={jest.fn()}
+ user={user}
+ manageProvider={undefined}
+ {...props}
+ />
);
}
});
function getWrapper(props = {}) {
- return shallow(<UserGroups groups={groups} onUpdateUsers={jest.fn()} user={user} {...props} />);
+ return shallow(
+ <UserGroups
+ groups={groups}
+ onUpdateUsers={jest.fn()}
+ user={user}
+ manageProvider={undefined}
+ {...props}
+ />
+ );
}
exports[`should render correctly 1`] = `
<Fragment>
- <ActionsDropdown>
+ <ActionsDropdown
+ label="users.manage_user.obi"
+ >
<ActionsDropdownItem
className="js-user-update"
onClick={[Function]}
more_x.2
</a>
<ButtonIcon
+ aria-label="users.update_users_groups.obi"
className="js-user-groups button-small"
onClick={[Function]}
tooltip="users.update_groups"
return {
membersCount: 1,
name: 'Foo',
+ managed: false,
...overrides,
};
}
description?: string;
membersCount: number;
name: string;
+ managed: boolean;
}
export type HealthType = 'RED' | 'YELLOW' | 'GREEN';
go_back_to_homepage=Go back to the homepage
last_analysis_before=Last analysis before
less_than_1_hour_ago=< 1 hour ago
+local=Local
logging_out=You're logging out, please wait...
manage=Manage
+managed=Managed
management=Management
more_information=More information
new_violations=New violations
unauthorized.reason=Reason:
-
-#------------------------------------------------------------------------------
-#
-# USERS & GROUPS PAGE
-#
-#------------------------------------------------------------------------------
-
-groups.users.edit=Change group members
-
#------------------------------------------------------------------------------
#
# MY PROFILE & MY ACCOUNT
users.minimum_x_characters=Minimum {0} characters
users.email=Email
users.last_connection=Last connection
+users.update_users_groups=Update {0}'s group membership
users.update_groups=Update Groups
+users.manage_user=Update {0}
users.update_tokens=Update Tokens
users.add=Add user
users.remove=Remove user
users.search_description=Search users by login or name
-users.update=Update users
users.tokens=Tokens
users.user_X_tokens=Tokens of {user}
users.tokens.sure=Sure?
users.change_admin_password.form.cannot_use_default_password=You must choose a password that is different from the default password.
users.change_admin_password.form.success=The admin user's password was successfully changed.
users.change_admin_password.form.continue_to_app=Continue to SonarQube
-users.local=Local
-users.managed=Managed
#------------------------------------------------------------------------------
#
groups.delete_group.confirmation=Are you sure you want to delete "{0}"?
groups.create_group=Create Group
groups.update_group=Update Group
+groups.users.edit=Change {0} members
groups.anyone=Anyone
+groups.edit=Edit {0}
#------------------------------------------------------------------------------