From 4a890c4af46a720b7da263b7b9d46712242220b7 Mon Sep 17 00:00:00 2001 From: Mathieu Suen Date: Wed, 29 Mar 2023 10:01:45 +0200 Subject: [PATCH] SONAR-18657 Add members view for groups in a managed instance --- .../main/js/api/mocks/GroupsServiceMock.ts | 22 +++- .../sonar-web/src/main/js/api/user_groups.ts | 8 +- .../groups/components/EditMembersModal.tsx | 6 +- .../js/apps/groups/components/ListItem.tsx | 11 +- .../{EditMembers.tsx => Members.tsx} | 27 ++-- .../groups/components/ViewMembersModal.tsx | 119 ++++++++++++++++++ .../components/__tests__/EditMembers-test.tsx | 39 ------ .../components/__tests__/GroupsApp-it.tsx | 18 +++ .../groups/components/__tests__/List-test.tsx | 36 ------ .../components/__tests__/ListItem-test.tsx | 34 ----- .../__snapshots__/EditMembers-test.tsx.snap | 50 -------- .../EditMembersModal-test.tsx.snap | 1 + .../__snapshots__/List-test.tsx.snap | 80 ------------ .../__snapshots__/ListItem-test.tsx.snap | 109 ---------------- .../src/main/js/apps/groups/groups.css | 16 +++ .../main/js/components/controls/buttons.css | 4 +- server/sonar-web/src/main/js/types/types.ts | 7 ++ .../resources/org/sonar/l10n/core.properties | 3 + 18 files changed, 216 insertions(+), 374 deletions(-) rename server/sonar-web/src/main/js/apps/groups/components/{EditMembers.tsx => Members.tsx} (69%) create mode 100644 server/sonar-web/src/main/js/apps/groups/components/ViewMembersModal.tsx delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/List-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap diff --git a/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts index 3965397c9d8..fcf3191a99e 100644 --- a/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts @@ -26,7 +26,13 @@ import { mockPaging, mockUser, } from '../../helpers/testMocks'; -import { Group, IdentityProvider, Paging, SysInfoCluster, UserSelected } from '../../types/types'; +import { + Group, + IdentityProvider, + Paging, + SysInfoCluster, + UserGroupMember, +} from '../../types/types'; import { getSystemInfo } from '../system'; import { getIdentityProviders } from '../users'; import { @@ -117,19 +123,25 @@ export default class GroupsServiceMock { return this.reply({}); }; - handlegetUsersInGroup = (): Promise => { + handlegetUsersInGroup = (data: { + name?: string; + p?: number; + ps?: number; + q?: string; + selected?: string; + }): Promise => { return this.reply({ ...this.paging, users: [ { ...mockUser({ name: 'alice' }), selected: true, - } as UserSelected, + } as UserGroupMember, { ...mockUser({ name: 'bob' }), selected: false, - } as UserSelected, - ], + } as UserGroupMember, + ].filter((u) => u.name.includes(data.q ?? '')), }); }; diff --git a/server/sonar-web/src/main/js/api/user_groups.ts b/server/sonar-web/src/main/js/api/user_groups.ts index 9b2ebe32352..0e7fd144827 100644 --- a/server/sonar-web/src/main/js/api/user_groups.ts +++ b/server/sonar-web/src/main/js/api/user_groups.ts @@ -19,7 +19,7 @@ */ import { throwGlobalError } from '../helpers/error'; import { getJSON, post, postJSON } from '../helpers/request'; -import { Group, Paging, UserSelected } from '../types/types'; +import { Group, Paging, UserGroupMember } from '../types/types'; export function searchUsersGroups(data: { f?: string; @@ -37,7 +37,11 @@ export function getUsersInGroup(data: { ps?: number; q?: string; selected?: string; -}): Promise { +}): Promise< + Paging & { + users: UserGroupMember[]; + } +> { return getJSON('/api/user_groups/users', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx b/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx index 322294ed3bd..016e04c619f 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx @@ -141,7 +141,11 @@ export default class EditMembersModal extends React.PureComponent render() { const modalHeader = translate('users.update'); return ( - +

{modalHeader}

diff --git a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx index 93acc5ca969..d6a19a6d225 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx @@ -17,7 +17,6 @@ * 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 { useState } from 'react'; import ActionsDropdown, { @@ -27,8 +26,8 @@ import ActionsDropdown, { import { translate, translateWithParameters } from '../../../helpers/l10n'; import { Group } from '../../../types/types'; import DeleteGroupForm from './DeleteGroupForm'; -import EditMembers from './EditMembers'; import GroupForm from './GroupForm'; +import Members from './Members'; export interface ListItemProps { group: Group; @@ -60,12 +59,8 @@ export default function ListItem(props: ListItemProps) { - - {membersCount} - - {!group.default && !isManaged() && } + {membersCount} + diff --git a/server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx b/server/sonar-web/src/main/js/apps/groups/components/Members.tsx similarity index 69% rename from server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx rename to server/sonar-web/src/main/js/apps/groups/components/Members.tsx index 05fe264ad5b..8e77e94bf8b 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/Members.tsx @@ -23,8 +23,10 @@ import BulletListIcon from '../../../components/icons/BulletListIcon'; import { translateWithParameters } from '../../../helpers/l10n'; import { Group } from '../../../types/types'; import EditMembersModal from './EditMembersModal'; +import ViewMembersModal from './ViewMembersModal'; interface Props { + isManaged: boolean; group: Group; onEdit: () => void; } @@ -33,8 +35,7 @@ interface State { modal: boolean; } -export default class EditMembers extends React.PureComponent { - container?: HTMLElement | null; +export default class Members extends React.PureComponent { mounted = false; state: State = { modal: false }; @@ -51,26 +52,36 @@ export default class EditMembers extends React.PureComponent { }; handleModalClose = () => { + const { isManaged, group } = this.props; if (this.mounted) { this.setState({ modal: false }); - this.props.onEdit(); + if (!isManaged && !group.default) { + this.props.onEdit(); + } } }; render() { + const { isManaged, group } = this.props; return ( <> - {this.state.modal && ( - - )} + {this.state.modal && + (isManaged || group.default ? ( + + ) : ( + + ))} ); } diff --git a/server/sonar-web/src/main/js/apps/groups/components/ViewMembersModal.tsx b/server/sonar-web/src/main/js/apps/groups/components/ViewMembersModal.tsx new file mode 100644 index 00000000000..799dc108c9f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/components/ViewMembersModal.tsx @@ -0,0 +1,119 @@ +/* + * 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 { DeferredSpinner } from 'design-system/lib'; +import * as React from 'react'; +import { getUsersInGroup } from '../../../api/user_groups'; +import { ResetButtonLink } from '../../../components/controls/buttons'; +import ListFooter from '../../../components/controls/ListFooter'; +import Modal from '../../../components/controls/Modal'; +import SearchBox from '../../../components/controls/SearchBox'; +import { SelectListFilter } from '../../../components/controls/SelectList'; +import { translate } from '../../../helpers/l10n'; +import { Group, UserGroupMember } from '../../../types/types'; + +interface Props { + isManaged: boolean; + group: Group; + onClose: () => void; +} + +export default function ViewMembersModal(props: Props) { + const { isManaged, group } = props; + + const [loading, setLoading] = React.useState(false); + const [page, setPage] = React.useState(1); + const [query, setQuery] = React.useState(); + const [total, setTotal] = React.useState(); + const [users, setUsers] = React.useState([]); + + React.useEffect(() => { + (async () => { + setLoading(true); + const data = await getUsersInGroup({ + name: group.name, + p: page, + q: query, + selected: SelectListFilter.Selected, + }); + if (page > 1) { + setUsers([...users, ...data.users]); + } else { + setUsers(data.users); + } + setTotal(data.total); + setLoading(false); + })(); + }, [query, page]); + + const modalHeader = translate('users.list'); + return ( + +
+

{modalHeader}

+
+ +
+ { + setQuery(q); + setPage(1); + }} + placeholder={translate('search_verb')} + value={query} + /> +
+ +
    + {users.map((user) => ( +
  • + + + + {user.name} +
    + {user.login} +
    + {!user.managed && isManaged && ( + {translate('local')} + )} +
    +
    +
  • + ))} +
+
+
+ {total !== undefined && ( + setPage((p) => p + 1)} total={total} /> + )} +
+ +
+ {translate('done')} +
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx deleted file mode 100644 index cce45974640..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockGroup } from '../../../../helpers/testMocks'; -import { click } from '../../../../helpers/testUtils'; -import EditMembers from '../EditMembers'; - -it('should edit members', () => { - const group = mockGroup({ name: 'Foo', membersCount: 5 }); - const onEdit = jest.fn(); - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - - click(wrapper.find('ButtonIcon')); - expect(wrapper).toMatchSnapshot(); - - wrapper.find('EditMembersModal').prop('onClose')(); - expect(onEdit).toHaveBeenCalled(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/GroupsApp-it.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/GroupsApp-it.tsx index cd66848bd95..28580e632e2 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/GroupsApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/GroupsApp-it.tsx @@ -54,10 +54,17 @@ const ui = { editGroupDialogButton: byRole('button', { name: 'groups.create_group' }), createGroupDialog: byRole('dialog', { name: 'groups.create_group' }), + membersViewDialog: byRole('dialog', { name: 'users.list' }), membersDialog: byRole('dialog', { name: 'users.update' }), managedGroupRow: byRole('row', { name: 'managed-group 1' }), managedGroupEditMembersButton: byRole('button', { name: 'groups.users.edit.managed-group' }), + managedGroupViewMembersButton: byRole('button', { name: 'groups.users.view.managed-group' }), + + memberAliceUser: byText('alice'), + memberBobUser: byText('bob'), + memberSearchInput: byRole('searchbox', { name: 'search_verb' }), + managedEditButton: byRole('button', { name: 'groups.edit.managed-group' }), localGroupRow: byRole('row', { name: 'local-group 1' }), @@ -236,6 +243,17 @@ describe('in manage mode', () => { expect(ui.managedEditButton.query()).not.toBeInTheDocument(); expect(ui.managedGroupEditMembersButton.query()).not.toBeInTheDocument(); + + await userEvent.click(ui.managedGroupViewMembersButton.get()); + expect(await ui.membersViewDialog.find()).toBeInTheDocument(); + + expect(ui.memberAliceUser.get()).toBeInTheDocument(); + expect(ui.memberBobUser.get()).toBeInTheDocument(); + + await userEvent.type(ui.memberSearchInput.get(), 'b'); + + expect(await ui.memberBobUser.find()).toBeInTheDocument(); + expect(ui.memberAliceUser.query()).not.toBeInTheDocument(); }); it('should render list of all groups', async () => { diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx deleted file mode 100644 index d40207a484a..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockGroup } from '../../../../helpers/testMocks'; -import List from '../List'; - -it('should render', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -function shallowRender() { - const groups = [ - 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(); -} diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx deleted file mode 100644 index e51bc5eabd9..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockGroup } from '../../../../helpers/testMocks'; -import ListItem, { ListItemProps } from '../ListItem'; - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); - expect(shallowRender({ group: mockGroup({ default: true }) })).toMatchSnapshot('default group'); -}); - -function shallowRender(overrides: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap deleted file mode 100644 index 6378ecc8307..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap +++ /dev/null @@ -1,50 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should edit members 1`] = ` - - - - - -`; - -exports[`should edit members 2`] = ` - - - - - - -`; - -exports[`should edit members 3`] = ` - - - - - -`; diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap index ecc4e6f75b2..35cc168d15e 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap @@ -2,6 +2,7 @@ exports[`should render modal properly 1`] = ` diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/List-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/List-test.tsx.snap deleted file mode 100644 index f79f1f69b91..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/List-test.tsx.snap +++ /dev/null @@ -1,80 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render 1`] = ` -
- - - - - - - - - - - - - - -
- user_groups.page.group_header - - members - - description - - actions -
-
-`; diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap deleted file mode 100644 index 5e4a0009aad..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap +++ /dev/null @@ -1,109 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` - - - - Foo - - - - - 1 - - - - - - - - - - update_details - - - - delete - - - - -`; - -exports[`should render correctly: default group 1`] = ` - - - - Foo - - - ( - default - ) - - - - - 1 - - - - - - - -`; diff --git a/server/sonar-web/src/main/js/apps/groups/groups.css b/server/sonar-web/src/main/js/apps/groups/groups.css index 1bec59205ae..6fa08f30676 100644 --- a/server/sonar-web/src/main/js/apps/groups/groups.css +++ b/server/sonar-web/src/main/js/apps/groups/groups.css @@ -21,3 +21,19 @@ #groups-page .group-members { padding-right: 50%; } + +.group-menbers-modal .modal-container > :last-child { + margin-bottom: 0; +} + +.group-menbers-modal .select-list-list-container { + height: 350px; +} + +.group-menbers-modal .modal-body { + padding: 12px 32px; +} + +.group-members-modal .view-search-box.search-box { + max-width: 100%; +} diff --git a/server/sonar-web/src/main/js/components/controls/buttons.css b/server/sonar-web/src/main/js/components/controls/buttons.css index eb189f27e98..e2ef9955112 100644 --- a/server/sonar-web/src/main/js/components/controls/buttons.css +++ b/server/sonar-web/src/main/js/components/controls/buttons.css @@ -219,12 +219,12 @@ } .button-icon:hover, -.button-icon:focus { +.button-icon:focus-visible { background-color: currentColor; } .button-icon:not(.disabled):hover svg, -.button-icon:not(.disabled):focus svg { +.button-icon:not(.disabled):focus-visible svg { color: var(--white); } diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts index 36cc2434a55..8ee9858b8f1 100644 --- a/server/sonar-web/src/main/js/types/types.ts +++ b/server/sonar-web/src/main/js/types/types.ts @@ -772,6 +772,13 @@ export interface UserSelected extends UserActive { selected: boolean; } +export interface UserGroupMember { + selected: boolean; + login: string; + name: string; + managed: boolean; +} + export namespace WebApi { export interface Action { key: string; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index f54dcdad6ae..444fcd3eb3a 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -4389,6 +4389,8 @@ users.delete_user.documentation=Authentication users.create_user=Create User users.create_user.scm_account_new=New SCM account users.create_user.scm_account_x=SCM account '{0}' +users.update=Update users +users.list=Users list users.update_user=Update User users.cannot_update_delegated_user=You cannot update the name and email of this user, as it is controlled by an external identity provider. users.minimum_x_characters=Minimum {0} characters @@ -4457,6 +4459,7 @@ 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.users.view=View {0} members groups.edit=Edit {0} -- 2.39.5