]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18657 Add members view for groups in a managed instance
authorMathieu Suen <mathieu.suen@sonarsource.com>
Wed, 29 Mar 2023 08:01:45 +0000 (10:01 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 30 Mar 2023 20:03:08 +0000 (20:03 +0000)
19 files changed:
server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts
server/sonar-web/src/main/js/api/user_groups.ts
server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx [deleted file]
server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx
server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
server/sonar-web/src/main/js/apps/groups/components/Members.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/ViewMembersModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/groups/components/__tests__/GroupsApp-it.tsx
server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/List-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/groups/groups.css
server/sonar-web/src/main/js/components/controls/buttons.css
server/sonar-web/src/main/js/types/types.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 3965397c9d83bfdf020a4864f751cfad7ec89ae3..fcf3191a99ec32be87630b9f359a4b9fb4855669 100644 (file)
@@ -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<Paging & { users: UserSelected[] }> => {
+  handlegetUsersInGroup = (data: {
+    name?: string;
+    p?: number;
+    ps?: number;
+    q?: string;
+    selected?: string;
+  }): Promise<Paging & { users: UserGroupMember[] }> => {
     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 ?? '')),
     });
   };
 
index 9b2ebe32352ce2e318efba308ea3d9160616d930..0e7fd144827bbfa86a454a2f74358680daf29b8a 100644 (file)
@@ -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<Paging & { users: UserSelected[] }> {
+}): 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/EditMembers.tsx b/server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx
deleted file mode 100644 (file)
index 05fe264..0000000
+++ /dev/null
@@ -1,77 +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 * as React from 'react';
-import { ButtonIcon } from '../../../components/controls/buttons';
-import BulletListIcon from '../../../components/icons/BulletListIcon';
-import { translateWithParameters } from '../../../helpers/l10n';
-import { Group } from '../../../types/types';
-import EditMembersModal from './EditMembersModal';
-
-interface Props {
-  group: Group;
-  onEdit: () => void;
-}
-
-interface State {
-  modal: boolean;
-}
-
-export default class EditMembers extends React.PureComponent<Props, State> {
-  container?: HTMLElement | null;
-  mounted = false;
-  state: State = { modal: false };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleMembersClick = () => {
-    this.setState({ modal: true });
-  };
-
-  handleModalClose = () => {
-    if (this.mounted) {
-      this.setState({ modal: false });
-      this.props.onEdit();
-    }
-  };
-
-  render() {
-    return (
-      <>
-        <ButtonIcon
-          aria-label={translateWithParameters('groups.users.edit', this.props.group.name)}
-          className="button-small little-spacer-left little-padded"
-          onClick={this.handleMembersClick}
-          title={translateWithParameters('groups.users.edit', this.props.group.name)}
-        >
-          <BulletListIcon />
-        </ButtonIcon>
-        {this.state.modal && (
-          <EditMembersModal group={this.props.group} onClose={this.handleModalClose} />
-        )}
-      </>
-    );
-  }
-}
index 322294ed3bd26508f220692a22c36cbbf3ce67be..016e04c619f8c039b480546efdc5d90fa6df25a3 100644 (file)
@@ -141,7 +141,11 @@ export default class EditMembersModal extends React.PureComponent<Props, State>
   render() {
     const modalHeader = translate('users.update');
     return (
-      <Modal contentLabel={modalHeader} onRequestClose={this.props.onClose}>
+      <Modal
+        className="group-menbers-modal"
+        contentLabel={modalHeader}
+        onRequestClose={this.props.onClose}
+      >
         <header className="modal-head">
           <h2>{modalHeader}</h2>
         </header>
index 93acc5ca969938664aa4688329248c6b8f3572fe..d6a19a6d2259e8b0bd5bbe90d089464a6fd8f303 100644 (file)
@@ -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) {
       </td>
 
       <td className="group-members display-flex-justify-end" headers="list-group-member">
-        <span
-          className={classNames({ 'big-padded-right spacer-right': group.default && !isManaged() })}
-        >
-          {membersCount}
-        </span>
-        {!group.default && !isManaged() && <EditMembers group={group} onEdit={props.reload} />}
+        <span>{membersCount}</span>
+        <Members group={group} onEdit={props.reload} isManaged={isManaged()} />
       </td>
 
       <td className="width-40" headers="list-group-description">
diff --git a/server/sonar-web/src/main/js/apps/groups/components/Members.tsx b/server/sonar-web/src/main/js/apps/groups/components/Members.tsx
new file mode 100644 (file)
index 0000000..8e77e94
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * 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 { ButtonIcon } from '../../../components/controls/buttons';
+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;
+}
+
+interface State {
+  modal: boolean;
+}
+
+export default class Members extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { modal: false };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleMembersClick = () => {
+    this.setState({ modal: true });
+  };
+
+  handleModalClose = () => {
+    const { isManaged, group } = this.props;
+    if (this.mounted) {
+      this.setState({ modal: false });
+      if (!isManaged && !group.default) {
+        this.props.onEdit();
+      }
+    }
+  };
+
+  render() {
+    const { isManaged, group } = this.props;
+    return (
+      <>
+        <ButtonIcon
+          aria-label={translateWithParameters(
+            isManaged || group.default ? 'groups.users.view' : 'groups.users.edit',
+            group.name
+          )}
+          className="button-small little-spacer-left little-padded"
+          onClick={this.handleMembersClick}
+          title={translateWithParameters('groups.users.edit', group.name)}
+        >
+          <BulletListIcon />
+        </ButtonIcon>
+        {this.state.modal &&
+          (isManaged || group.default ? (
+            <ViewMembersModal isManaged={isManaged} group={group} onClose={this.handleModalClose} />
+          ) : (
+            <EditMembersModal group={group} onClose={this.handleModalClose} />
+          ))}
+      </>
+    );
+  }
+}
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 (file)
index 0000000..799dc10
--- /dev/null
@@ -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<string>();
+  const [total, setTotal] = React.useState<number>();
+  const [users, setUsers] = React.useState<UserGroupMember[]>([]);
+
+  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 (
+    <Modal
+      className="group-menbers-modal"
+      contentLabel={modalHeader}
+      onRequestClose={props.onClose}
+    >
+      <header className="modal-head">
+        <h2>{modalHeader}</h2>
+      </header>
+
+      <div className="modal-body modal-container">
+        <SearchBox
+          className="view-search-box"
+          loading={loading}
+          onChange={(q) => {
+            setQuery(q);
+            setPage(1);
+          }}
+          placeholder={translate('search_verb')}
+          value={query}
+        />
+        <div className="select-list-list-container spacer-top">
+          <DeferredSpinner loading={loading}>
+            <ul className="menu">
+              {users.map((user) => (
+                <li key={user.login} className="display-flex-center">
+                  <span className="little-spacer-left width-100">
+                    <span className="select-list-list-item display-flex-center display-flex-space-between">
+                      <span className="spacer-right">
+                        {user.name}
+                        <br />
+                        <span className="note">{user.login}</span>
+                      </span>
+                      {!user.managed && isManaged && (
+                        <span className="badge">{translate('local')}</span>
+                      )}
+                    </span>
+                  </span>
+                </li>
+              ))}
+            </ul>
+          </DeferredSpinner>
+        </div>
+        {total !== undefined && (
+          <ListFooter count={users.length} loadMore={() => setPage((p) => p + 1)} total={total} />
+        )}
+      </div>
+
+      <footer className="modal-foot">
+        <ResetButtonLink onClick={props.onClose}>{translate('done')}</ResetButtonLink>
+      </footer>
+    </Modal>
+  );
+}
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 (file)
index cce4597..0000000
+++ /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(<EditMembers group={group} onEdit={onEdit} />);
-  expect(wrapper).toMatchSnapshot();
-
-  click(wrapper.find('ButtonIcon'));
-  expect(wrapper).toMatchSnapshot();
-
-  wrapper.find('EditMembersModal').prop<Function>('onClose')();
-  expect(onEdit).toHaveBeenCalled();
-  expect(wrapper).toMatchSnapshot();
-});
index cd66848bd9558a4480a8cd42d196e630ff95e63d..28580e632e2c1ee9f1a391f671e6655150e05fd6 100644 (file)
@@ -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 (file)
index d40207a..0000000
+++ /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(<List groups={groups} manageProvider={undefined} reload={jest.fn()} />);
-}
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 (file)
index e51bc5e..0000000
+++ /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<ListItemProps> = {}) {
-  return shallow(
-    <ListItem group={mockGroup()} reload={jest.fn()} manageProvider={undefined} {...overrides} />
-  );
-}
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 (file)
index 6378ecc..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should edit members 1`] = `
-<Fragment>
-  <ButtonIcon
-    aria-label="groups.users.edit.Foo"
-    className="button-small little-spacer-left little-padded"
-    onClick={[Function]}
-    title="groups.users.edit.Foo"
-  >
-    <BulletListIcon />
-  </ButtonIcon>
-</Fragment>
-`;
-
-exports[`should edit members 2`] = `
-<Fragment>
-  <ButtonIcon
-    aria-label="groups.users.edit.Foo"
-    className="button-small little-spacer-left little-padded"
-    onClick={[Function]}
-    title="groups.users.edit.Foo"
-  >
-    <BulletListIcon />
-  </ButtonIcon>
-  <EditMembersModal
-    group={
-      {
-        "managed": false,
-        "membersCount": 5,
-        "name": "Foo",
-      }
-    }
-    onClose={[Function]}
-  />
-</Fragment>
-`;
-
-exports[`should edit members 3`] = `
-<Fragment>
-  <ButtonIcon
-    aria-label="groups.users.edit.Foo"
-    className="button-small little-spacer-left little-padded"
-    onClick={[Function]}
-    title="groups.users.edit.Foo"
-  >
-    <BulletListIcon />
-  </ButtonIcon>
-</Fragment>
-`;
index ecc4e6f75b2b299ad06b3db583f8b108182a67f0..35cc168d15eb728bbcf0bf81febbf88187dc2100 100644 (file)
@@ -2,6 +2,7 @@
 
 exports[`should render modal properly 1`] = `
 <Modal
+  className="group-menbers-modal"
   contentLabel="users.update"
   onRequestClose={[MockFunction]}
 >
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 (file)
index f79f1f6..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render 1`] = `
-<div
-  className="boxed-group boxed-group-inner"
->
-  <table
-    className="data zebra zebra-hover"
-    id="groups-list"
-  >
-    <thead>
-      <tr>
-        <th
-          id="list-group-name"
-        >
-          user_groups.page.group_header
-        </th>
-        <th
-          className="nowrap width-10"
-          id="list-group-member"
-        >
-          members
-        </th>
-        <th
-          className="nowrap"
-          id="list-group-description"
-        >
-          description
-        </th>
-        <th
-          id="list-group-actions"
-        >
-          actions
-        </th>
-      </tr>
-    </thead>
-    <tbody>
-      <ListItem
-        group={
-          {
-            "default": false,
-            "description": "barbar",
-            "managed": false,
-            "membersCount": 1,
-            "name": "bar",
-          }
-        }
-        key="bar"
-        reload={[MockFunction]}
-      />
-      <ListItem
-        group={
-          {
-            "default": false,
-            "description": "foobar",
-            "managed": false,
-            "membersCount": 0,
-            "name": "foo",
-          }
-        }
-        key="foo"
-        reload={[MockFunction]}
-      />
-      <ListItem
-        group={
-          {
-            "default": true,
-            "description": "",
-            "managed": false,
-            "membersCount": 55,
-            "name": "sonar-users",
-          }
-        }
-        key="sonar-users"
-        reload={[MockFunction]}
-      />
-    </tbody>
-  </table>
-</div>
-`;
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 (file)
index 5e4a000..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<tr
-  data-id="Foo"
->
-  <td
-    className="width-20"
-    headers="list-group-name"
-  >
-    <strong>
-      Foo
-    </strong>
-  </td>
-  <td
-    className="group-members display-flex-justify-end"
-    headers="list-group-member"
-  >
-    <span>
-      1
-    </span>
-    <EditMembers
-      group={
-        {
-          "managed": false,
-          "membersCount": 1,
-          "name": "Foo",
-        }
-      }
-      onEdit={[MockFunction]}
-    />
-  </td>
-  <td
-    className="width-40"
-    headers="list-group-description"
-  >
-    <span
-      className="js-group-description"
-    />
-  </td>
-  <td
-    className="thin nowrap text-right"
-    headers="list-group-actions"
-  >
-    <ActionsDropdown
-      label="groups.edit.Foo"
-    >
-      <ActionsDropdownItem
-        className="js-group-update"
-        onClick={[Function]}
-      >
-        update_details
-      </ActionsDropdownItem>
-      <ActionsDropdownDivider />
-      <ActionsDropdownItem
-        className="js-group-delete"
-        destructive={true}
-        onClick={[Function]}
-      >
-        delete
-      </ActionsDropdownItem>
-    </ActionsDropdown>
-  </td>
-</tr>
-`;
-
-exports[`should render correctly: default group 1`] = `
-<tr
-  data-id="Foo"
->
-  <td
-    className="width-20"
-    headers="list-group-name"
-  >
-    <strong>
-      Foo
-    </strong>
-    <span
-      className="little-spacer-left"
-    >
-      (
-      default
-      )
-    </span>
-  </td>
-  <td
-    className="group-members display-flex-justify-end"
-    headers="list-group-member"
-  >
-    <span
-      className="big-padded-right spacer-right"
-    >
-      1
-    </span>
-  </td>
-  <td
-    className="width-40"
-    headers="list-group-description"
-  >
-    <span
-      className="js-group-description"
-    />
-  </td>
-  <td
-    className="thin nowrap text-right"
-    headers="list-group-actions"
-  />
-</tr>
-`;
index 1bec59205ae5a40065032cb1c015f0d97034fea8..6fa08f30676d7cad536874fd6804ab7472b5adcd 100644 (file)
 #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%;
+}
index eb189f27e98c1d8f7eda662323b95875e77d7956..e2ef99551124119a4c38fa799736e90ceb294afa 100644 (file)
 }
 
 .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);
 }
 
index 36cc2434a555097924c52ae01506b78451729a1c..8ee9858b8f188f6abbdfd7ed392077c3294194be 100644 (file)
@@ -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;
index f54dcdad6aefb6e0a85d4c3ad24a8cdbc6142585..444fcd3eb3af6a293f160c2a953815c1600544bd 100644 (file)
@@ -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}