diff --git a/packages/backend/src/graphql/queries/get-users.js b/packages/backend/src/graphql/queries/get-users.js deleted file mode 100644 index 6bff6c29..00000000 --- a/packages/backend/src/graphql/queries/get-users.js +++ /dev/null @@ -1,19 +0,0 @@ -import paginate from '../../helpers/pagination.js'; -import User from '../../models/user.js'; - -const getUsers = async (_parent, params, context) => { - context.currentUser.can('read', 'User'); - - const usersQuery = User.query() - .leftJoinRelated({ - role: true, - }) - .withGraphFetched({ - role: true, - }) - .orderBy('full_name', 'asc'); - - return paginate(usersQuery, params.limit, params.offset); -}; - -export default getUsers; diff --git a/packages/backend/src/graphql/queries/get-users.test.js b/packages/backend/src/graphql/queries/get-users.test.js deleted file mode 100644 index aeb239c3..00000000 --- a/packages/backend/src/graphql/queries/get-users.test.js +++ /dev/null @@ -1,148 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import request from 'supertest'; -import app from '../../app'; -import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id'; -import { createRole } from '../../../test/factories/role'; -import { createPermission } from '../../../test/factories/permission'; -import { createUser } from '../../../test/factories/user'; - -describe('graphQL getUsers query', () => { - const query = ` - query { - getUsers(limit: 10, offset: 0) { - pageInfo { - currentPage - totalPages - } - totalCount - edges { - node { - id - fullName - email - role { - id - name - } - } - } - } - } - `; - - describe('and without permissions', () => { - it('should throw not authorized error', async () => { - const userWithoutPermissions = await createUser(); - const token = createAuthTokenByUserId(userWithoutPermissions.id); - - const response = await request(app) - .post('/graphql') - .set('Authorization', token) - .send({ query }) - .expect(200); - - expect(response.body.errors).toBeDefined(); - expect(response.body.errors[0].message).toEqual('Not authorized!'); - }); - }); - - describe('and with correct permissions', () => { - let role, currentUser, anotherUser, token, requestObject; - - beforeEach(async () => { - role = await createRole({ - key: 'sample', - name: 'sample', - }); - - await createPermission({ - action: 'read', - subject: 'User', - roleId: role.id, - }); - - currentUser = await createUser({ - roleId: role.id, - fullName: 'Current User', - }); - - anotherUser = await createUser({ - roleId: role.id, - fullName: 'Another User', - }); - - token = createAuthTokenByUserId(currentUser.id); - requestObject = request(app).post('/graphql').set('Authorization', token); - }); - - it('should return users data', async () => { - const response = await requestObject.send({ query }).expect(200); - - const expectedResponsePayload = { - data: { - getUsers: { - edges: [ - { - node: { - email: anotherUser.email, - fullName: anotherUser.fullName, - id: anotherUser.id, - role: { - id: role.id, - name: role.name, - }, - }, - }, - { - node: { - email: currentUser.email, - fullName: currentUser.fullName, - id: currentUser.id, - role: { - id: role.id, - name: role.name, - }, - }, - }, - ], - pageInfo: { - currentPage: 1, - totalPages: 1, - }, - totalCount: 2, - }, - }, - }; - - expect(response.body).toEqual(expectedResponsePayload); - }); - - it('should not return users data with password', async () => { - const query = ` - query { - getUsers(limit: 10, offset: 0) { - pageInfo { - currentPage - totalPages - } - totalCount - edges { - node { - id - fullName - password - } - } - } - } - `; - - const response = await requestObject.send({ query }).expect(400); - - expect(response.body.errors).toBeDefined(); - expect(response.body.errors[0].message).toEqual( - 'Cannot query field "password" on type "User".' - ); - }); - }); -}); diff --git a/packages/backend/src/graphql/query-resolvers.js b/packages/backend/src/graphql/query-resolvers.js index 54fdc798..bb8b9286 100644 --- a/packages/backend/src/graphql/query-resolvers.js +++ b/packages/backend/src/graphql/query-resolvers.js @@ -9,7 +9,6 @@ import getDynamicFields from './queries/get-dynamic-fields.js'; import getFlow from './queries/get-flow.js'; import getNotifications from './queries/get-notifications.js'; import getStepWithTestExecutions from './queries/get-step-with-test-executions.js'; -import getUsers from './queries/get-users.js'; import testConnection from './queries/test-connection.js'; const queryResolvers = { @@ -24,7 +23,6 @@ const queryResolvers = { getFlow, getNotifications, getStepWithTestExecutions, - getUsers, testConnection, }; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 1e61d56b..4201af01 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -19,7 +19,6 @@ type Query { getBillingAndUsage: GetBillingAndUsage getConfig(keys: [String]): JSONObject getNotifications: [Notification] - getUsers(limit: Int!, offset: Int!): UserConnection } type Mutation { @@ -288,16 +287,6 @@ type SamlAuthProvidersRoleMapping { remoteRoleName: String } -type UserConnection { - edges: [UserEdge] - pageInfo: PageInfo - totalCount: Int -} - -type UserEdge { - node: User -} - input CreateConnectionInput { key: String! appAuthClientId: String diff --git a/packages/web/src/components/DeleteUserButton/index.ee.jsx b/packages/web/src/components/DeleteUserButton/index.ee.jsx index 40b45d91..34fe15da 100644 --- a/packages/web/src/components/DeleteUserButton/index.ee.jsx +++ b/packages/web/src/components/DeleteUserButton/index.ee.jsx @@ -2,6 +2,8 @@ import PropTypes from 'prop-types'; import { useMutation } from '@apollo/client'; import DeleteIcon from '@mui/icons-material/Delete'; import IconButton from '@mui/material/IconButton'; +import { useQueryClient } from '@tanstack/react-query'; + import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import * as React from 'react'; import ConfirmationDialog from 'components/ConfirmationDialog'; @@ -17,9 +19,13 @@ function DeleteUserButton(props) { }); const formatMessage = useFormatMessage(); const enqueueSnackbar = useEnqueueSnackbar(); + const queryClient = useQueryClient(); + const handleConfirm = React.useCallback(async () => { try { await deleteUser(); + queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }); + queryClient.invalidateQueries({ queryKey: ['admin', 'user', userId] }); setShowConfirmation(false); enqueueSnackbar(formatMessage('deleteUserButton.successfullyDeleted'), { variant: 'success', @@ -31,6 +37,7 @@ function DeleteUserButton(props) { throw new Error('Failed while deleting!'); } }, [deleteUser]); + return ( <> { setPage(newPage); }; - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(+event.target.value); - setPage(0); - }; + return ( <> @@ -68,14 +68,14 @@ export default function UserList() { - {loading && ( + {isLoading && ( )} - {!loading && + {!isLoading && users.map((user) => ( ))} - {totalCount && ( + {!isLoading && typeof count === 'number' && ( diff --git a/packages/web/src/graphql/queries/get-users.js b/packages/web/src/graphql/queries/get-users.js deleted file mode 100644 index e2d8a45f..00000000 --- a/packages/web/src/graphql/queries/get-users.js +++ /dev/null @@ -1,23 +0,0 @@ -import { gql } from '@apollo/client'; -export const GET_USERS = gql` - query GetUsers($limit: Int!, $offset: Int!) { - getUsers(limit: $limit, offset: $offset) { - pageInfo { - currentPage - totalPages - } - totalCount - edges { - node { - id - fullName - email - role { - id - name - } - } - } - } - } -`; diff --git a/packages/web/src/hooks/useUser.js b/packages/web/src/hooks/useAdminUser.js similarity index 77% rename from packages/web/src/hooks/useUser.js rename to packages/web/src/hooks/useAdminUser.js index ec31c0e7..0f8e3ee9 100644 --- a/packages/web/src/hooks/useUser.js +++ b/packages/web/src/hooks/useAdminUser.js @@ -1,9 +1,9 @@ import { useQuery } from '@tanstack/react-query'; import api from 'helpers/api'; -export default function useUser({ userId }) { +export default function useAdminUser({ userId }) { const query = useQuery({ - queryKey: ['user', userId], + queryKey: ['admin', 'user', userId], queryFn: async ({ signal }) => { const { data } = await api.get(`/v1/admin/users/${userId}`, { signal, diff --git a/packages/web/src/hooks/useAdminUsers.js b/packages/web/src/hooks/useAdminUsers.js new file mode 100644 index 00000000..2b1ecb17 --- /dev/null +++ b/packages/web/src/hooks/useAdminUsers.js @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useAdminUsers(page) { + const query = useQuery({ + queryKey: ['admin', 'users', page], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/admin/users`, { + signal, + params: { page }, + }); + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useUsers.js b/packages/web/src/hooks/useUsers.js deleted file mode 100644 index 8b6c9ba2..00000000 --- a/packages/web/src/hooks/useUsers.js +++ /dev/null @@ -1,20 +0,0 @@ -import { useQuery } from '@apollo/client'; -import { GET_USERS } from 'graphql/queries/get-users'; -const getLimitAndOffset = (page, rowsPerPage) => ({ - limit: rowsPerPage, - offset: page * rowsPerPage, -}); -export default function useUsers(page, rowsPerPage) { - const { data, loading } = useQuery(GET_USERS, { - variables: getLimitAndOffset(page, rowsPerPage), - }); - const users = data?.getUsers.edges.map(({ node }) => node) || []; - const pageInfo = data?.getUsers.pageInfo; - const totalCount = data?.getUsers.totalCount; - return { - users, - pageInfo, - totalCount, - loading, - }; -} diff --git a/packages/web/src/pages/CreateUser/index.jsx b/packages/web/src/pages/CreateUser/index.jsx index d841523a..8033d3ab 100644 --- a/packages/web/src/pages/CreateUser/index.jsx +++ b/packages/web/src/pages/CreateUser/index.jsx @@ -6,6 +6,7 @@ import MuiTextField from '@mui/material/TextField'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import * as React from 'react'; import { useNavigate } from 'react-router-dom'; +import { useQueryClient } from '@tanstack/react-query'; import Can from 'components/Can'; import Container from 'components/Container'; @@ -29,6 +30,7 @@ export default function CreateUser() { const { data, loading: isRolesLoading } = useRoles(); const roles = data?.data; const enqueueSnackbar = useEnqueueSnackbar(); + const queryClient = useQueryClient(); const handleUserCreation = async (userData) => { try { @@ -44,7 +46,7 @@ export default function CreateUser() { }, }, }); - + queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }); enqueueSnackbar(formatMessage('createUser.successfullyCreated'), { variant: 'success', persist: true, diff --git a/packages/web/src/pages/EditUser/index.jsx b/packages/web/src/pages/EditUser/index.jsx index 255327cb..e4574f85 100644 --- a/packages/web/src/pages/EditUser/index.jsx +++ b/packages/web/src/pages/EditUser/index.jsx @@ -7,6 +7,7 @@ import MuiTextField from '@mui/material/TextField'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import * as React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; +import { useQueryClient } from '@tanstack/react-query'; import Can from 'components/Can'; import Container from 'components/Container'; @@ -18,7 +19,7 @@ import * as URLS from 'config/urls'; import { UPDATE_USER } from 'graphql/mutations/update-user.ee'; import useFormatMessage from 'hooks/useFormatMessage'; import useRoles from 'hooks/useRoles.ee'; -import useUser from 'hooks/useUser'; +import useAdminUser from 'hooks/useAdminUser'; function generateRoleOptions(roles) { return roles?.map(({ name: label, id: value }) => ({ label, value })); @@ -28,12 +29,13 @@ export default function EditUser() { const formatMessage = useFormatMessage(); const [updateUser, { loading }] = useMutation(UPDATE_USER); const { userId } = useParams(); - const { data: userData, isLoading: isUserLoading } = useUser({ userId }); + const { data: userData, isLoading: isUserLoading } = useAdminUser({ userId }); const user = userData?.data; const { data, isLoading: isRolesLoading } = useRoles(); const roles = data?.data; const enqueueSnackbar = useEnqueueSnackbar(); const navigate = useNavigate(); + const queryClient = useQueryClient(); const handleUserUpdate = async (userDataToUpdate) => { try { @@ -49,6 +51,8 @@ export default function EditUser() { }, }, }); + queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }); + queryClient.invalidateQueries({ queryKey: ['admin', 'user', userId] }); enqueueSnackbar(formatMessage('editUser.successfullyUpdated'), { variant: 'success',