aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/groups
diff options
context:
space:
mode:
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>2023-11-21 16:05:27 +0100
committersonartech <sonartech@sonarsource.com>2023-11-23 20:02:58 +0000
commit94a8b446ce8a36f3410a4c6c92da737f93bce5f8 (patch)
treeb161c30a4e7f2cfa12b4ffa7f621a3e46f807471 /server/sonar-web/src/main/js/apps/groups
parent5925e543d8a2771bf0dda3c0ecd60308eefbae8b (diff)
downloadsonarqube-94a8b446ce8a36f3410a4c6c92da737f93bce5f8.tar.gz
sonarqube-94a8b446ce8a36f3410a4c6c92da737f93bce5f8.zip
SONAR-21086 Refactor using ReactQuery
Diffstat (limited to 'server/sonar-web/src/main/js/apps/groups')
-rw-r--r--server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx77
-rw-r--r--server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/DeleteGroupForm.tsx23
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/GroupForm.tsx56
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/Header.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/List.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx26
7 files changed, 83 insertions, 132 deletions
diff --git a/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx b/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx
index 5a18957633c..aa4f890f768 100644
--- a/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx
@@ -18,9 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { useCallback, useEffect, useState } from 'react';
+import { useState } from 'react';
import { Helmet } from 'react-helmet-async';
-import { searchUsersGroups } from '../../api/user_groups';
import GitHubSynchronisationWarning from '../../app/components/GitHubSynchronisationWarning';
import ListFooter from '../../components/controls/ListFooter';
import { ManagedFilter } from '../../components/controls/ManagedFilter';
@@ -28,67 +27,37 @@ import SearchBox from '../../components/controls/SearchBox';
import Suggestions from '../../components/embed-docs-modal/Suggestions';
import { Provider, useManageProvider } from '../../components/hooks/useManageProvider';
import { translate } from '../../helpers/l10n';
-import { Group, Paging } from '../../types/types';
+import { useGroupsQueries } from '../../queries/groups';
import Header from './components/Header';
import List from './components/List';
import './groups.css';
export default function GroupsApp() {
- const [loading, setLoading] = useState<boolean>(true);
- const [paging, setPaging] = useState<Paging>();
+ const [numberOfPages, setNumberOfPages] = useState<number>(1);
const [search, setSearch] = useState<string>('');
- const [groups, setGroups] = useState<Group[]>([]);
const [managed, setManaged] = useState<boolean | undefined>();
const manageProvider = useManageProvider();
- const fetchGroups = useCallback(async () => {
- setLoading(true);
- try {
- const { groups, paging } = await searchUsersGroups({
- q: search,
- managed,
- });
- setGroups(groups);
- setPaging(paging);
- } finally {
- setLoading(false);
- }
- }, [search, managed]);
-
- const fetchMoreGroups = useCallback(async () => {
- if (!paging) {
- return;
- }
- setLoading(true);
- try {
- const { groups: nextGroups, paging: nextPage } = await searchUsersGroups({
- q: search,
- managed,
- p: paging.pageIndex + 1,
- });
- setPaging(nextPage);
- setGroups([...groups, ...nextGroups]);
- } finally {
- setLoading(false);
- }
- }, [groups, search, managed, paging]);
-
- useEffect(() => {
- fetchGroups();
- }, [search, managed]);
+ const { groups, total, isLoading } = useGroupsQueries(
+ {
+ q: search,
+ managed,
+ },
+ numberOfPages,
+ );
return (
<>
<Suggestions suggestions="user_groups" />
<Helmet defer={false} title={translate('user_groups.page')} />
<main className="page page-limited" id="groups-page">
- <Header reload={fetchGroups} manageProvider={manageProvider} />
+ <Header manageProvider={manageProvider} />
{manageProvider === Provider.Github && <GitHubSynchronisationWarning short />}
<div className="display-flex-justify-start big-spacer-bottom big-spacer-top">
<ManagedFilter
manageProvider={manageProvider}
- loading={loading}
+ loading={isLoading}
managed={managed}
setManaged={setManaged}
/>
@@ -101,19 +70,17 @@ export default function GroupsApp() {
/>
</div>
- <List groups={groups} reload={fetchGroups} manageProvider={manageProvider} />
+ <List groups={groups} manageProvider={manageProvider} />
- {paging !== undefined && (
- <div id="groups-list-footer">
- <ListFooter
- count={groups.length}
- loading={loading}
- loadMore={fetchMoreGroups}
- ready={!loading}
- total={paging.total}
- />
- </div>
- )}
+ <div id="groups-list-footer">
+ <ListFooter
+ count={groups.length}
+ loading={isLoading}
+ loadMore={() => setNumberOfPages((n) => n + 1)}
+ ready={!isLoading}
+ total={total}
+ />
+ </div>
</main>
</>
);
diff --git a/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx b/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx
index d6e591c004b..34263fe216e 100644
--- a/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx
@@ -64,8 +64,8 @@ const ui = {
membersDialog: byRole('dialog', { name: 'users.update' }),
getMembers: () => within(ui.membersDialog.get()).getAllByRole('checkbox'),
- managedGroupRow: byRole('row', { name: 'managed-group 1' }),
- githubManagedGroupRow: byRole('row', { name: 'managed-group github 1' }),
+ managedGroupRow: byRole('row', { name: 'managed-group 3' }),
+ githubManagedGroupRow: byRole('row', { name: 'managed-group github 3' }),
managedGroupEditMembersButton: byRole('button', { name: 'groups.users.edit.managed-group' }),
managedGroupViewMembersButton: byRole('button', { name: 'groups.users.view.managed-group' }),
@@ -75,13 +75,13 @@ const ui = {
managedEditButton: byRole('button', { name: 'groups.edit.managed-group' }),
- localGroupRow: byRole('row', { name: 'local-group 1' }),
+ localGroupRow: byRole('row', { name: 'local-group 3' }),
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!' }),
+ localGroupRow2: byRole('row', { name: 'local-group 2 3 group 2 is loco!' }),
+ editedLocalGroupRow: byRole('row', { name: 'local-group 3 3 group 3 rocks!' }),
localEditButton: byRole('button', { name: 'groups.edit.local-group' }),
localGroupRowWithLocalBadge: byRole('row', {
- name: 'local-group local 1',
+ name: 'local-group local 3',
}),
githubProvisioningPending: byText(/synchronization_pending/),
@@ -299,7 +299,7 @@ describe('in manage mode', () => {
await act(async () => expect(await ui.localAndManagedFilter.find()).toBeInTheDocument());
- expect(ui.localGroupRowWithLocalBadge.get()).toBeInTheDocument();
+ expect(await ui.localGroupRowWithLocalBadge.find()).toBeInTheDocument();
expect(ui.managedGroupRow.get()).toBeInTheDocument();
});
@@ -311,8 +311,8 @@ describe('in manage mode', () => {
await user.click(await ui.managedFilter.find());
});
+ expect(await ui.managedGroupRow.find()).toBeInTheDocument();
expect(ui.localGroupRow.query()).not.toBeInTheDocument();
- expect(ui.managedGroupRow.get()).toBeInTheDocument();
});
it('should render list of local groups', async () => {
@@ -323,7 +323,7 @@ describe('in manage mode', () => {
await user.click(await ui.localFilter.find());
});
- expect(ui.localGroupRowWithLocalBadge.get()).toBeInTheDocument();
+ expect(await ui.localGroupRowWithLocalBadge.find()).toBeInTheDocument();
expect(ui.managedGroupRow.query()).not.toBeInTheDocument();
});
@@ -395,7 +395,7 @@ describe('in manage mode', () => {
});
expect(
- within(ui.githubManagedGroupRow.get()).getByRole('img', { name: 'github' }),
+ within(await ui.githubManagedGroupRow.find()).getByRole('img', { name: 'github' }),
).toBeInTheDocument();
});
});
diff --git a/server/sonar-web/src/main/js/apps/groups/components/DeleteGroupForm.tsx b/server/sonar-web/src/main/js/apps/groups/components/DeleteGroupForm.tsx
index 3345f248d98..6b491854dbe 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/DeleteGroupForm.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/components/DeleteGroupForm.tsx
@@ -18,30 +18,33 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { useCallback } from 'react';
-import { deleteGroup } from '../../../api/user_groups';
import SimpleModal from '../../../components/controls/SimpleModal';
import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
import Spinner from '../../../components/ui/Spinner';
import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { useDeleteGroupMutation } from '../../../queries/groups';
import { Group } from '../../../types/types';
interface Props {
group: Group;
onClose: () => void;
- reload: () => void;
}
export default function DeleteGroupForm(props: Props) {
- const header = translate('groups.delete_group');
- const { group, reload, onClose } = props;
+ const { group } = props;
+
+ const { mutate: deleteGroup } = useDeleteGroupMutation();
- const onSubmit = useCallback(async () => {
- await deleteGroup({ name: group.name });
- reload();
- onClose();
- }, [group, reload, onClose]);
+ const onSubmit = () => {
+ deleteGroup(
+ { name: group.name },
+ {
+ onSuccess: props.onClose,
+ },
+ );
+ };
+ const header = translate('groups.delete_group');
return (
<SimpleModal header={header} onClose={props.onClose} onSubmit={onSubmit}>
{({ onCloseClick, onFormSubmit, submitting }) => (
diff --git a/server/sonar-web/src/main/js/apps/groups/components/GroupForm.tsx b/server/sonar-web/src/main/js/apps/groups/components/GroupForm.tsx
index b13b23bb7f2..84eff7ad012 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/GroupForm.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/components/GroupForm.tsx
@@ -18,8 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { useCallback, useEffect, useState } from 'react';
-import { createGroup, updateGroup } from '../../../api/user_groups';
+import { useState } from 'react';
import SimpleModal from '../../../components/controls/SimpleModal';
import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
@@ -27,6 +26,7 @@ import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsEx
import Spinner from '../../../components/ui/Spinner';
import { translate } from '../../../helpers/l10n';
import { omitNil } from '../../../helpers/request';
+import { useCreateGroupMutation, useUpdateGroupMutation } from '../../../queries/groups';
import { Group } from '../../../types/types';
type Props =
@@ -34,52 +34,46 @@ type Props =
create: true;
group?: undefined;
onClose: () => void;
- reload: () => void;
}
| {
create: false;
group: Group;
onClose: () => void;
- reload: () => void;
};
export default function GroupForm(props: Props) {
- const { group, create, reload, onClose } = props;
+ const { group, create } = props;
- const [name, setName] = useState<string>('');
- const [description, setDescription] = useState<string>('');
+ const [name, setName] = useState<string>(create ? '' : group.name);
+ const [description, setDescription] = useState<string>(create ? '' : group.description ?? '');
- const handleSubmit = useCallback(async () => {
- try {
- if (create) {
- await createGroup({ name, description });
- } else {
- const data = {
- currentName: group.name,
- description,
- // pass `name` only if it has changed, otherwise the WS fails
- ...omitNil({ name: name !== group.name ? name : undefined }),
- };
- await updateGroup(data);
- }
- } finally {
- reload();
- onClose();
- }
- }, [name, description, group, create, reload, onClose]);
+ const { mutate: createGroup } = useCreateGroupMutation();
+ const { mutate: updateGroup } = useUpdateGroupMutation();
+
+ const handleCreateGroup = () => {
+ createGroup({ name, description }, { onSuccess: props.onClose });
+ };
- useEffect(() => {
- if (!create) {
- setDescription(group.description ?? '');
- setName(group.name);
+ const handleUpdateGroup = () => {
+ if (!group) {
+ return;
}
- }, []);
+ updateGroup(
+ {
+ currentName: group.name,
+ description,
+ // pass `name` only if it has changed, otherwise the WS fails
+ ...omitNil({ name: name !== group.name ? name : undefined }),
+ },
+ { onSuccess: props.onClose },
+ );
+ };
return (
<SimpleModal
header={create ? translate('groups.create_group') : translate('groups.update_group')}
onClose={props.onClose}
- onSubmit={handleSubmit}
+ onSubmit={create ? handleCreateGroup : handleUpdateGroup}
size="small"
>
{({ onCloseClick, onFormSubmit, submitting }) => (
diff --git a/server/sonar-web/src/main/js/apps/groups/components/Header.tsx b/server/sonar-web/src/main/js/apps/groups/components/Header.tsx
index 5f7f7d83c36..f285ff37c38 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/Header.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/components/Header.tsx
@@ -26,7 +26,6 @@ import { translate } from '../../../helpers/l10n';
import GroupForm from './GroupForm';
interface HeaderProps {
- reload: () => void;
manageProvider?: string;
}
@@ -68,9 +67,7 @@ export default function Header(props: HeaderProps) {
</Alert>
)}
</div>
- {createModal && (
- <GroupForm onClose={() => setCreateModal(false)} create reload={props.reload} />
- )}
+ {createModal && <GroupForm onClose={() => setCreateModal(false)} create />}
</>
);
}
diff --git a/server/sonar-web/src/main/js/apps/groups/components/List.tsx b/server/sonar-web/src/main/js/apps/groups/components/List.tsx
index b15de183e9c..b699b7eb70a 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/List.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/components/List.tsx
@@ -25,7 +25,6 @@ import ListItem from './ListItem';
interface Props {
groups: Group[];
- reload: () => void;
manageProvider: string | undefined;
}
@@ -49,12 +48,7 @@ export default function List(props: Props) {
</thead>
<tbody>
{sortBy(groups, (group) => group.name.toLowerCase()).map((group) => (
- <ListItem
- group={group}
- key={group.name}
- reload={props.reload}
- manageProvider={manageProvider}
- />
+ <ListItem group={group} key={group.name} manageProvider={manageProvider} />
))}
</tbody>
</table>
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 4c1ccea5f31..561c46536ce 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,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { Spinner } from 'design-system';
import * as React from 'react';
import { useState } from 'react';
import ActionsDropdown, {
@@ -26,6 +27,7 @@ import ActionsDropdown, {
import { Provider } from '../../../components/hooks/useManageProvider';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/system';
+import { useMembersCountQuery } from '../../../queries/groups';
import { Group } from '../../../types/types';
import DeleteGroupForm from './DeleteGroupForm';
import GroupForm from './GroupForm';
@@ -33,17 +35,18 @@ import Members from './Members';
export interface ListItemProps {
group: Group;
- reload: () => void;
manageProvider: string | undefined;
}
export default function ListItem(props: ListItemProps) {
const { manageProvider, group } = props;
- const { name, managed, membersCount, description } = group;
+ const { name, managed, description } = group;
const [groupToDelete, setGroupToDelete] = useState<Group | undefined>();
const [groupToEdit, setGroupToEdit] = useState<Group | undefined>();
+ const { data: membersCount, isLoading, refetch } = useMembersCountQuery(group.name);
+
const isManaged = () => {
return manageProvider !== undefined;
};
@@ -73,8 +76,10 @@ export default function ListItem(props: ListItemProps) {
</td>
<td className="group-members display-flex-justify-end" headers="list-group-member">
- <span>{membersCount}</span>
- <Members group={group} onEdit={props.reload} isManaged={isManaged()} />
+ <Spinner loading={isLoading}>
+ <span>{membersCount}</span>
+ </Spinner>
+ <Members group={group} onEdit={refetch} isManaged={isManaged()} />
</td>
<td className="width-40" headers="list-group-description">
@@ -107,19 +112,10 @@ export default function ListItem(props: ListItemProps) {
</ActionsDropdown>
)}
{groupToDelete && (
- <DeleteGroupForm
- group={groupToDelete}
- reload={props.reload}
- onClose={() => setGroupToDelete(undefined)}
- />
+ <DeleteGroupForm group={groupToDelete} onClose={() => setGroupToDelete(undefined)} />
)}
{groupToEdit && (
- <GroupForm
- create={false}
- group={groupToEdit}
- reload={props.reload}
- onClose={() => setGroupToEdit(undefined)}
- />
+ <GroupForm create={false} group={groupToEdit} onClose={() => setGroupToEdit(undefined)} />
)}
</td>
</tr>