diff --git a/src/web/hooks/use-query/__tests__/Groups.test.tsx b/src/web/hooks/use-query/__tests__/Groups.test.tsx
new file mode 100644
index 0000000000..7b4f839651
--- /dev/null
+++ b/src/web/hooks/use-query/__tests__/Groups.test.tsx
@@ -0,0 +1,106 @@
+/* SPDX-FileCopyrightText: 2026 Greenbone AG
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import {describe, expect, test, testing} from '@gsa/testing';
+import {rendererWith, screen, waitFor} from 'web/testing';
+import CollectionCounts from 'gmp/collection/collection-counts';
+import QueryFilter from 'gmp/models/filter/query-filter';
+import {createSession} from 'gmp/testing';
+import {useGetAllGroups, useGetGroups} from 'web/hooks/use-query/groups';
+
+const filter = QueryFilter.fromString('name~group');
+const groups = [{id: 'group-1', name: 'Group 1'}];
+
+const createGmp = () => {
+ const response = {
+ data: groups,
+ meta: {
+ filter,
+ counts: new CollectionCounts({all: 1, filtered: 1, length: 1}),
+ },
+ };
+
+ return {
+ session: createSession({token: 'test-token'}),
+ settings: {},
+ groups: {
+ get: testing.fn().mockResolvedValue(response),
+ getAll: testing.fn().mockResolvedValue(response),
+ },
+ };
+};
+
+describe('group query hooks', () => {
+ test('should fetch groups with a filter', async () => {
+ const gmp = createGmp();
+ const {render} = rendererWith({gmp, router: true});
+
+ const TestComponent = () => {
+ const {data} = useGetGroups({filter});
+ return
{data?.entities[0]?.name}
;
+ };
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('group')).toHaveTextContent('Group 1');
+ });
+
+ expect(gmp.groups.get).toHaveBeenCalledWith({filter});
+ expect(gmp.groups.getAll).not.toHaveBeenCalled();
+ });
+
+ test('should fetch all groups with a filter', async () => {
+ const gmp = createGmp();
+ const {render} = rendererWith({gmp, router: true});
+
+ const TestComponent = () => {
+ const {data} = useGetAllGroups({filter});
+ return {data?.entities[0]?.name}
;
+ };
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('group')).toHaveTextContent('Group 1');
+ });
+
+ expect(gmp.groups.getAll).toHaveBeenCalledWith({filter});
+ expect(gmp.groups.get).not.toHaveBeenCalled();
+ });
+
+ test('should not fetch groups when disabled', () => {
+ const gmp = createGmp();
+ const {render} = rendererWith({gmp, router: true});
+
+ const TestComponent = () => {
+ useGetGroups({enabled: false});
+ useGetAllGroups({enabled: false});
+ return ;
+ };
+
+ render();
+
+ expect(gmp.groups.get).not.toHaveBeenCalled();
+ expect(gmp.groups.getAll).not.toHaveBeenCalled();
+ });
+
+ test('should not fetch groups without a session token', () => {
+ const gmp = createGmp();
+ gmp.session.token = undefined;
+ const {render} = rendererWith({gmp, router: true});
+
+ const TestComponent = () => {
+ useGetGroups({filter});
+ useGetAllGroups({filter});
+ return ;
+ };
+
+ render();
+
+ expect(gmp.groups.get).not.toHaveBeenCalled();
+ expect(gmp.groups.getAll).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/web/hooks/use-query/__tests__/Roles.test.tsx b/src/web/hooks/use-query/__tests__/Roles.test.tsx
new file mode 100644
index 0000000000..bcad03a074
--- /dev/null
+++ b/src/web/hooks/use-query/__tests__/Roles.test.tsx
@@ -0,0 +1,106 @@
+/* SPDX-FileCopyrightText: 2026 Greenbone AG
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import {describe, expect, test, testing} from '@gsa/testing';
+import {rendererWith, screen, waitFor} from 'web/testing';
+import CollectionCounts from 'gmp/collection/collection-counts';
+import QueryFilter from 'gmp/models/filter/query-filter';
+import {createSession} from 'gmp/testing';
+import {useGetAllRoles, useGetRoles} from 'web/hooks/use-query/roles';
+
+const filter = QueryFilter.fromString('name~role');
+const roles = [{id: 'role-1', name: 'Role 1'}];
+
+const createGmp = () => {
+ const response = {
+ data: roles,
+ meta: {
+ filter,
+ counts: new CollectionCounts({all: 1, filtered: 1, length: 1}),
+ },
+ };
+
+ return {
+ session: createSession({token: 'test-token'}),
+ settings: {},
+ roles: {
+ get: testing.fn().mockResolvedValue(response),
+ getAll: testing.fn().mockResolvedValue(response),
+ },
+ };
+};
+
+describe('role query hooks', () => {
+ test('should fetch roles with a filter', async () => {
+ const gmp = createGmp();
+ const {render} = rendererWith({gmp, router: true});
+
+ const TestComponent = () => {
+ const {data} = useGetRoles({filter});
+ return {data?.entities[0]?.name}
;
+ };
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('role')).toHaveTextContent('Role 1');
+ });
+
+ expect(gmp.roles.get).toHaveBeenCalledWith({filter});
+ expect(gmp.roles.getAll).not.toHaveBeenCalled();
+ });
+
+ test('should fetch all roles with a filter', async () => {
+ const gmp = createGmp();
+ const {render} = rendererWith({gmp, router: true});
+
+ const TestComponent = () => {
+ const {data} = useGetAllRoles({filter});
+ return {data?.entities[0]?.name}
;
+ };
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('role')).toHaveTextContent('Role 1');
+ });
+
+ expect(gmp.roles.getAll).toHaveBeenCalledWith({filter});
+ expect(gmp.roles.get).not.toHaveBeenCalled();
+ });
+
+ test('should not fetch roles when disabled', () => {
+ const gmp = createGmp();
+ const {render} = rendererWith({gmp, router: true});
+
+ const TestComponent = () => {
+ useGetRoles({enabled: false});
+ useGetAllRoles({enabled: false});
+ return ;
+ };
+
+ render();
+
+ expect(gmp.roles.get).not.toHaveBeenCalled();
+ expect(gmp.roles.getAll).not.toHaveBeenCalled();
+ });
+
+ test('should not fetch roles without a session token', () => {
+ const gmp = createGmp();
+ gmp.session.token = undefined;
+ const {render} = rendererWith({gmp, router: true});
+
+ const TestComponent = () => {
+ useGetRoles({filter});
+ useGetAllRoles({filter});
+ return ;
+ };
+
+ render();
+
+ expect(gmp.roles.get).not.toHaveBeenCalled();
+ expect(gmp.roles.getAll).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/web/hooks/use-query/groups.ts b/src/web/hooks/use-query/groups.ts
new file mode 100644
index 0000000000..136960abab
--- /dev/null
+++ b/src/web/hooks/use-query/groups.ts
@@ -0,0 +1,36 @@
+/* SPDX-FileCopyrightText: 2026 Greenbone AG
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import {type FilterType} from 'gmp/models/filter';
+import type Group from 'gmp/models/group';
+import useGmp from 'web/hooks/useGmp';
+import useGetEntities from 'web/queries/useGetEntities';
+
+interface UseGetGroupsParams {
+ enabled?: boolean;
+ filter?: FilterType;
+}
+
+export const useGetGroups = ({filter, enabled}: UseGetGroupsParams = {}) => {
+ const gmp = useGmp();
+ return useGetEntities({
+ gmpMethod: gmp.groups.get.bind(gmp.groups),
+ enabled,
+ queryId: 'get_groups',
+ filter,
+ keepPreviousData: true,
+ });
+};
+
+export const useGetAllGroups = ({filter, enabled}: UseGetGroupsParams = {}) => {
+ const gmp = useGmp();
+ return useGetEntities({
+ gmpMethod: gmp.groups.getAll.bind(gmp.groups),
+ enabled,
+ queryId: 'get_groups',
+ filter,
+ keepPreviousData: true,
+ });
+};
diff --git a/src/web/hooks/use-query/roles.ts b/src/web/hooks/use-query/roles.ts
new file mode 100644
index 0000000000..85952613e8
--- /dev/null
+++ b/src/web/hooks/use-query/roles.ts
@@ -0,0 +1,36 @@
+/* SPDX-FileCopyrightText: 2026 Greenbone AG
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import {type FilterType} from 'gmp/models/filter';
+import type Role from 'gmp/models/role';
+import useGmp from 'web/hooks/useGmp';
+import useGetEntities from 'web/queries/useGetEntities';
+
+interface UseGetRolesParams {
+ enabled?: boolean;
+ filter?: FilterType;
+}
+
+export const useGetRoles = ({filter, enabled}: UseGetRolesParams = {}) => {
+ const gmp = useGmp();
+ return useGetEntities({
+ gmpMethod: gmp.roles.get.bind(gmp.roles),
+ enabled,
+ queryId: 'get_roles',
+ filter,
+ keepPreviousData: true,
+ });
+};
+
+export const useGetAllRoles = ({filter, enabled}: UseGetRolesParams = {}) => {
+ const gmp = useGmp();
+ return useGetEntities({
+ gmpMethod: gmp.roles.getAll.bind(gmp.roles),
+ enabled,
+ queryId: 'get_roles',
+ filter,
+ keepPreviousData: true,
+ });
+};
diff --git a/src/web/pages/users/UserComponent.tsx b/src/web/pages/users/UserComponent.tsx
index 179942c837..45d83b09ea 100644
--- a/src/web/pages/users/UserComponent.tsx
+++ b/src/web/pages/users/UserComponent.tsx
@@ -5,7 +5,6 @@
import {type ReactNode, useState} from 'react';
import {type EntityActionData} from 'gmp/commands/entity';
-import type Model from 'gmp/models/model';
import type Settings from 'gmp/models/settings';
import type User from 'gmp/models/user';
import {isDefined} from 'gmp/utils/identity';
@@ -67,12 +66,10 @@ const UserComponent = ({
const [accessHosts, setAccessHosts] = useState([]);
const [comment, setComment] = useState();
const [groupIds, setGroupIds] = useState();
- const [groups, setGroups] = useState();
const [hostsAllow, setHostsAllow] = useState();
const [name, setName] = useState();
const [oldName, setOldName] = useState();
const [roleIds, setRoleIds] = useState();
- const [roles, setRoles] = useState();
const [settings, setSettings] = useState();
const [title, setTitle] = useState();
const [user, setUser] = useState();
@@ -111,17 +108,9 @@ const UserComponent = ({
const openUserDialog = async (user?: User) => {
try {
- const [groupsResponse, rolesResponse, authSettingsResponse] =
- await Promise.all([
- gmp.groups.getAll({
- filter: 'permission=modify_group', // list only groups current user may modify
- }),
- gmp.roles.getAll(),
- gmp.user.currentAuthSettings(),
- ]);
-
- setGroups(groupsResponse.data);
- setRoles(rolesResponse.data);
+ const [authSettingsResponse] = await Promise.all([
+ gmp.user.currentAuthSettings(),
+ ]);
const settings = authSettingsResponse.data;
setSettings(settings);
@@ -200,12 +189,10 @@ const UserComponent = ({
accessHosts={accessHosts}
comment={comment}
groupIds={groupIds}
- groups={groups}
hostsAllow={hostsAllow}
name={name}
oldName={oldName}
roleIds={roleIds}
- roles={roles}
settings={settings}
title={title}
user={user}
diff --git a/src/web/pages/users/UsersDialog.tsx b/src/web/pages/users/UsersDialog.tsx
index 0c9a47cb85..fc3f79e647 100644
--- a/src/web/pages/users/UsersDialog.tsx
+++ b/src/web/pages/users/UsersDialog.tsx
@@ -4,7 +4,7 @@
*/
import {useState} from 'react';
-import type Model from 'gmp/models/model';
+import QueryFilter from 'gmp/models/filter/query-filter';
import type Settings from 'gmp/models/settings';
import {
ACCESS_ALLOW_ALL,
@@ -27,6 +27,8 @@ import PasswordField from 'web/components/form/PasswordField';
import Radio from 'web/components/form/Radio';
import TextField from 'web/components/form/TextField';
import Row from 'web/components/layout/Row';
+import {useGetAllGroups} from 'web/hooks/use-query/groups';
+import {useGetAllRoles} from 'web/hooks/use-query/roles';
import useCapabilities from 'web/hooks/useCapabilities';
import useTranslation from 'web/hooks/useTranslation';
import useUserName from 'web/hooks/useUserName';
@@ -58,13 +60,11 @@ interface UserDialogProps {
roleIds?: string[];
accessHosts?: string[];
comment?: string;
- groups?: Model[];
groupIds?: string[];
hostsAllow?: string;
name?: string;
oldName?: string;
password?: string;
- roles?: Model[];
settings: Settings;
title?: string;
user?: User;
@@ -76,13 +76,11 @@ const UsersDialog = ({
roleIds: initialRoleIds = [],
accessHosts = [],
comment = '',
- groups,
groupIds = [],
hostsAllow = ACCESS_ALLOW_ALL,
name,
oldName,
password = '',
- roles,
settings,
title,
user,
@@ -104,6 +102,12 @@ const UsersDialog = ({
>(undefined);
const [roleIds, setRoleIds] = useState(initialRoleIds);
+ const groupsQuery = useGetAllGroups({
+ filter: QueryFilter.fromString('permission=modify_group'), // list only groups current user may modify
+ });
+
+ const rolesQuery = useGetAllRoles();
+
const dialogName = name || _('Unnamed');
const dialogTitle = title || _('New User');
@@ -184,12 +188,12 @@ const UsersDialog = ({
roleIds,
};
- const rolesOptions = map(roles, role => ({
+ const rolesOptions = map(rolesQuery.data?.entities ?? [], role => ({
label: role.name ?? '',
value: role.id ?? '',
}));
- const groupsOptions = map(groups, group => ({
+ const groupsOptions = map(groupsQuery.data?.entities ?? [], group => ({
label: group.name ?? '',
value: group.id ?? '',
}));
@@ -314,6 +318,7 @@ const UsersDialog = ({
{capabilities.mayAccess('role') && (
({
+ settings: {
+ reloadInterval: 0,
+ reloadIntervalActive: 0,
+ reloadIntervalInactive: 0,
+ },
user: {
create: testing.fn().mockResolvedValue({data: {id: 'created'}}),
save: testing.fn().mockResolvedValue({data: {id: 'saved'}}),
@@ -68,12 +74,24 @@ const createGmp = () => ({
.mockResolvedValue(currentSettingsDefaultResponse),
},
groups: {
- getAll: testing.fn().mockResolvedValue({data: groups}),
+ getAll: testing.fn().mockResolvedValue({
+ data: groups,
+ meta: {
+ filter: undefined,
+ counts: new CollectionCounts({all: groups.length}),
+ },
+ }),
},
roles: {
- getAll: testing.fn().mockResolvedValue({data: roles}),
+ getAll: testing.fn().mockResolvedValue({
+ data: roles,
+ meta: {
+ filter: undefined,
+ counts: new CollectionCounts({all: roles.length}),
+ },
+ }),
},
- session: createSession({username: 'admin'}),
+ session: createSession({username: 'admin', token: 'test-token'}),
});
describe('UserComponent', () => {
@@ -88,8 +106,9 @@ describe('UserComponent', () => {
});
test('should open and close user dialog', async () => {
+ const gmp = createGmp();
const {render} = rendererWith({
- gmp: createGmp(),
+ gmp,
capabilities: true,
store: true,
});
@@ -101,6 +120,10 @@ describe('UserComponent', () => {
fireEvent.click(screen.getByTestId('open'));
await screen.findByText('New User');
+ await waitFor(() => {
+ expect(gmp.groups.getAll).toHaveBeenCalled();
+ expect(gmp.roles.getAll).toHaveBeenCalled();
+ });
fireEvent.click(screen.getDialogCloseButton());
await waitFor(() => {
@@ -121,6 +144,10 @@ describe('UserComponent', () => {
fireEvent.click(screen.getByTestId('open'));
await screen.findByText('Edit User user 1');
+ await waitFor(() => {
+ expect(gmp.groups.getAll).toHaveBeenCalled();
+ expect(gmp.roles.getAll).toHaveBeenCalled();
+ });
fireEvent.click(screen.getDialogSaveButton());
await waitFor(() => {
@@ -158,6 +185,10 @@ describe('UserComponent', () => {
fireEvent.click(screen.getByTestId('open'));
await screen.findByText('Edit User user 2');
+ await waitFor(() => {
+ expect(gmp.groups.getAll).toHaveBeenCalled();
+ expect(gmp.roles.getAll).toHaveBeenCalled();
+ });
fireEvent.click(screen.getDialogSaveButton());
await waitFor(() => {
@@ -181,6 +212,7 @@ describe('UserComponent', () => {
const gmp = createGmp();
const error = new Error('Unable to load authentication settings');
gmp.user.currentAuthSettings.mockRejectedValue(error);
+ testing.spyOn(console, 'error').mockImplementation(() => {});
const onDialogError = testing.fn();
const {render} = rendererWith({gmp, capabilities: true, store: true});
diff --git a/src/web/pages/users/__tests__/UsersDialog.test.tsx b/src/web/pages/users/__tests__/UsersDialog.test.tsx
index cad0129374..5c07eab7cd 100644
--- a/src/web/pages/users/__tests__/UsersDialog.test.tsx
+++ b/src/web/pages/users/__tests__/UsersDialog.test.tsx
@@ -9,8 +9,11 @@ import {
fireEvent,
rendererWith,
screen,
+ waitFor,
within,
+ type RendererOptions,
} from 'web/testing';
+import CollectionCounts from 'gmp/collection/collection-counts';
import Group from 'gmp/models/group';
import Role from 'gmp/models/role';
import Settings from 'gmp/models/settings';
@@ -34,15 +37,42 @@ const createSettings = ({ldap = false, radius = false} = {}) => {
return settings;
};
-const renderDialog = (props = {}, options = {}) => {
+const createGmp = () => ({
+ session: createSession({username: 'admin', token: 'test-token'}),
+ settings: {
+ reloadInterval: 0,
+ reloadIntervalActive: 0,
+ reloadIntervalInactive: 0,
+ },
+ groups: {
+ getAll: testing.fn().mockResolvedValue({
+ data: groups,
+ meta: {
+ filter: undefined,
+ counts: new CollectionCounts({all: groups.length}),
+ },
+ }),
+ },
+ roles: {
+ getAll: testing.fn().mockResolvedValue({
+ data: roles,
+ meta: {
+ filter: undefined,
+ counts: new CollectionCounts({all: roles.length}),
+ },
+ }),
+ },
+});
+
+const renderDialog = (props = {}, options: RendererOptions = {}) => {
const onClose = testing.fn();
const onSave = testing.fn();
+ const gmp = createGmp();
+ const {gmp: optionGmp, ...rendererOptions} = options;
const {render} = rendererWith({
- capabilities: true,
- gmp: {
- session: createSession({username: 'admin'}),
- },
- ...options,
+ ...rendererOptions,
+ capabilities: options.capabilities ?? true,
+ gmp: {...gmp, ...optionGmp},
});
render(
@@ -54,7 +84,7 @@ const renderDialog = (props = {}, options = {}) => {
/>,
);
- return {onClose, onSave};
+ return {gmp, onClose, onSave};
};
describe('UsersDialog tests', () => {
@@ -91,8 +121,6 @@ describe('UsersDialog tests', () => {
oldName: undefined,
password: '',
roleIds: [],
- roles: undefined,
- groups: undefined,
});
});
@@ -125,15 +153,34 @@ describe('UsersDialog tests', () => {
).toBeInTheDocument();
});
- test('saves a user after selecting a role and group', () => {
- const {onSave} = renderDialog({roles, groups});
+ test('saves a user after selecting a role and group', async () => {
+ const {gmp, onSave} = renderDialog();
const dialog = within(screen.getDialog());
+ await waitFor(() => {
+ expect(gmp.roles.getAll).toHaveBeenCalled();
+ expect(gmp.groups.getAll).toHaveBeenCalled();
+ });
+ await waitFor(() => {
+ const selects = dialog.getAllByTestId('multi-select');
+ expect(selects[0]).not.toBeDisabled();
+ expect(selects[1]).not.toBeDisabled();
+ });
const selects = dialog.getAllByTestId('multi-select');
fireEvent.click(selects[0]);
- fireEvent.click(screen.getByRole('option', {name: 'Administrator'}));
+ const roleOptions = screen.getSelectItemElementsForMultiSelect();
+ fireEvent.click(
+ roleOptions.find(
+ option => option.textContent === 'Administrator',
+ ) as HTMLElement,
+ );
fireEvent.click(selects[1]);
- fireEvent.click(screen.getByRole('option', {name: 'Security'}));
+ const groupOptions = screen.getSelectItemElementsForMultiSelect();
+ fireEvent.click(
+ groupOptions.find(
+ option => option.textContent === 'Security',
+ ) as HTMLElement,
+ );
fireEvent.click(screen.getDialogSaveButton());
@@ -204,10 +251,7 @@ describe('UsersDialog tests', () => {
test('confirms saving changes to the current Super Admin user', () => {
const user = new User({id: 'admin-id', name: 'admin'});
- const {onSave} = renderDialog(
- {user, name: 'admin', roleIds: ['role-1']},
- {gmp: {session: createSession({username: 'admin'})}},
- );
+ const {onSave} = renderDialog({user, name: 'admin', roleIds: ['role-1']});
fireEvent.click(screen.getDialogSaveButton());
expect(screen.getByText('Save Super Admin User')).toBeInTheDocument();
@@ -225,7 +269,7 @@ describe('UsersDialog tests', () => {
});
test('hides role and group fields without capabilities', () => {
- renderDialog({roles, groups}, {capabilities: false});
+ renderDialog({}, {capabilities: false});
expect(screen.queryByText('Roles')).toBeNull();
expect(screen.queryByText('Groups')).toBeNull();