Merge pull request #1727 from automatisch/AUT-840
refactor: rewrite useRoles with RQ
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
import Role from '../../models/role.js';
|
||||
|
||||
const getRoles = async (_parent, params, context) => {
|
||||
context.currentUser.can('read', 'Role');
|
||||
|
||||
return await Role.query().orderBy('name');
|
||||
};
|
||||
|
||||
export default getRoles;
|
@@ -1,134 +0,0 @@
|
||||
import { vi, 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';
|
||||
import * as license from '../../helpers/license.ee';
|
||||
|
||||
describe('graphQL getRoles query', () => {
|
||||
let currentUserRole,
|
||||
roleOne,
|
||||
roleSecond,
|
||||
query,
|
||||
userWithPermissions,
|
||||
userWithoutPermissions,
|
||||
tokenWithPermissions,
|
||||
tokenWithoutPermissions;
|
||||
|
||||
beforeEach(async () => {
|
||||
currentUserRole = await createRole({ name: 'Current user role' });
|
||||
roleOne = await createRole({ name: 'Role one' });
|
||||
roleSecond = await createRole({ name: 'Role second' });
|
||||
|
||||
query = `
|
||||
query {
|
||||
getRoles {
|
||||
id
|
||||
key
|
||||
name
|
||||
description
|
||||
isAdmin
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
await createPermission({
|
||||
action: 'read',
|
||||
subject: 'Role',
|
||||
roleId: currentUserRole.id,
|
||||
});
|
||||
|
||||
userWithPermissions = await createUser({
|
||||
roleId: currentUserRole.id,
|
||||
});
|
||||
|
||||
userWithoutPermissions = await createUser({
|
||||
roleId: roleOne.id,
|
||||
});
|
||||
|
||||
tokenWithPermissions = createAuthTokenByUserId(userWithPermissions.id);
|
||||
tokenWithoutPermissions = createAuthTokenByUserId(
|
||||
userWithoutPermissions.id
|
||||
);
|
||||
});
|
||||
|
||||
describe('and with valid license', () => {
|
||||
beforeEach(async () => {
|
||||
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
|
||||
});
|
||||
|
||||
describe('and without permissions', () => {
|
||||
it('should throw not authorized error', async () => {
|
||||
const response = await request(app)
|
||||
.post('/graphql')
|
||||
.set('Authorization', tokenWithoutPermissions)
|
||||
.send({ query })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and correct permissions', () => {
|
||||
it('should return roles data', async () => {
|
||||
const response = await request(app)
|
||||
.post('/graphql')
|
||||
.set('Authorization', tokenWithPermissions)
|
||||
.send({ query })
|
||||
.expect(200);
|
||||
|
||||
const expectedResponsePayload = {
|
||||
data: {
|
||||
getRoles: [
|
||||
{
|
||||
description: currentUserRole.description,
|
||||
id: currentUserRole.id,
|
||||
isAdmin: currentUserRole.key === 'admin',
|
||||
key: currentUserRole.key,
|
||||
name: currentUserRole.name,
|
||||
},
|
||||
{
|
||||
description: roleOne.description,
|
||||
id: roleOne.id,
|
||||
isAdmin: roleOne.key === 'admin',
|
||||
key: roleOne.key,
|
||||
name: roleOne.name,
|
||||
},
|
||||
{
|
||||
description: roleSecond.description,
|
||||
id: roleSecond.id,
|
||||
isAdmin: roleSecond.key === 'admin',
|
||||
key: roleSecond.key,
|
||||
name: roleSecond.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(response.body).toEqual(expectedResponsePayload);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and without valid license', () => {
|
||||
beforeEach(async () => {
|
||||
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(false);
|
||||
});
|
||||
|
||||
describe('and correct permissions', () => {
|
||||
it('should throw not authorized error', async () => {
|
||||
const response = await request(app)
|
||||
.post('/graphql')
|
||||
.set('Authorization', tokenWithPermissions)
|
||||
.send({ query })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -13,7 +13,6 @@ import getInvoices from './queries/get-invoices.ee.js';
|
||||
import getNotifications from './queries/get-notifications.js';
|
||||
import getPermissionCatalog from './queries/get-permission-catalog.ee.js';
|
||||
import getRole from './queries/get-role.ee.js';
|
||||
import getRoles from './queries/get-roles.ee.js';
|
||||
import getSamlAuthProviderRoleMappings from './queries/get-saml-auth-provider-role-mappings.ee.js';
|
||||
import getSamlAuthProvider from './queries/get-saml-auth-provider.ee.js';
|
||||
import getStepWithTestExecutions from './queries/get-step-with-test-executions.js';
|
||||
@@ -40,7 +39,6 @@ const queryResolvers = {
|
||||
getNotifications,
|
||||
getPermissionCatalog,
|
||||
getRole,
|
||||
getRoles,
|
||||
getSamlAuthProvider,
|
||||
getSamlAuthProviderRoleMappings,
|
||||
getStepWithTestExecutions,
|
||||
|
@@ -29,7 +29,6 @@ type Query {
|
||||
getInvoices: [Invoice]
|
||||
getPermissionCatalog: PermissionCatalog
|
||||
getRole(id: String!): Role
|
||||
getRoles: [Role]
|
||||
getNotifications: [Notification]
|
||||
getSamlAuthProvider: SamlAuthProvider
|
||||
getSamlAuthProviderRoleMappings(id: String!): [SamlAuthProvidersRoleMapping]
|
||||
|
@@ -4,6 +4,8 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
import * as React from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import Can from 'components/Can';
|
||||
import ConfirmationDialog from 'components/ConfirmationDialog';
|
||||
import { DELETE_ROLE } from 'graphql/mutations/delete-role.ee';
|
||||
@@ -12,15 +14,18 @@ import useFormatMessage from 'hooks/useFormatMessage';
|
||||
function DeleteRoleButton(props) {
|
||||
const { disabled, roleId } = props;
|
||||
const [showConfirmation, setShowConfirmation] = React.useState(false);
|
||||
const [deleteRole] = useMutation(DELETE_ROLE, {
|
||||
variables: { input: { id: roleId } },
|
||||
refetchQueries: ['GetRoles'],
|
||||
});
|
||||
const formatMessage = useFormatMessage();
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [deleteRole] = useMutation(DELETE_ROLE, {
|
||||
variables: { input: { id: roleId } },
|
||||
});
|
||||
|
||||
const handleConfirm = React.useCallback(async () => {
|
||||
try {
|
||||
await deleteRole();
|
||||
queryClient.invalidateQueries({ queryKey: ['roles'] });
|
||||
setShowConfirmation(false);
|
||||
enqueueSnackbar(formatMessage('deleteRoleButton.successfullyDeleted'), {
|
||||
variant: 'success',
|
||||
@@ -31,7 +36,8 @@ function DeleteRoleButton(props) {
|
||||
} catch (error) {
|
||||
throw new Error('Failed while deleting!');
|
||||
}
|
||||
}, [deleteRole]);
|
||||
}, [deleteRole, enqueueSnackbar, formatMessage, queryClient]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Can I="delete" a="Role" passThrough>
|
||||
|
@@ -11,14 +11,18 @@ import Paper from '@mui/material/Paper';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
|
||||
import DeleteRoleButton from 'components/DeleteRoleButton/index.ee';
|
||||
import ListLoader from 'components/ListLoader';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useRoles from 'hooks/useRoles.ee';
|
||||
import * as URLS from 'config/urls';
|
||||
|
||||
export default function RoleList() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { roles, loading } = useRoles();
|
||||
const { data, isLoading: isRolesLoading } = useRoles();
|
||||
const roles = data?.data;
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
@@ -46,15 +50,15 @@ export default function RoleList() {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading && (
|
||||
{isRolesLoading && (
|
||||
<ListLoader
|
||||
rowsNumber={3}
|
||||
columnsNumber={2}
|
||||
data-test="roles-list-loader"
|
||||
/>
|
||||
)}
|
||||
{!loading &&
|
||||
roles.map((role) => (
|
||||
{!isRolesLoading &&
|
||||
roles?.map((role) => (
|
||||
<TableRow
|
||||
key={role.id}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
|
@@ -1,12 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
export const GET_ROLES = gql`
|
||||
query GetRoles {
|
||||
getRoles {
|
||||
id
|
||||
key
|
||||
name
|
||||
description
|
||||
isAdmin
|
||||
}
|
||||
}
|
||||
`;
|
@@ -1,11 +1,18 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { GET_ROLES } from 'graphql/queries/get-roles.ee';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import api from 'helpers/api';
|
||||
|
||||
export default function useRoles() {
|
||||
const { data, loading } = useQuery(GET_ROLES, {
|
||||
context: { autoSnackbar: false },
|
||||
const query = useQuery({
|
||||
staleTime: 0,
|
||||
queryKey: ['roles'],
|
||||
queryFn: async ({ signal }) => {
|
||||
const { data } = await api.get('/v1/admin/roles', {
|
||||
signal,
|
||||
});
|
||||
return {
|
||||
roles: data?.getRoles || [],
|
||||
loading,
|
||||
};
|
||||
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
||||
|
@@ -4,24 +4,32 @@ import Stack from '@mui/material/Stack';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Button from '@mui/material/Button';
|
||||
import { Divider, Typography } from '@mui/material';
|
||||
|
||||
import useRoles from 'hooks/useRoles.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import ControlledAutocomplete from 'components/ControlledAutocomplete';
|
||||
import TextField from 'components/TextField';
|
||||
import { Divider, Typography } from '@mui/material';
|
||||
|
||||
function generateRoleOptions(roles) {
|
||||
return roles?.map(({ name: label, id: value }) => ({ label, value }));
|
||||
}
|
||||
|
||||
function RoleMappingsFieldArray() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { control } = useFormContext();
|
||||
const { roles, loading: rolesLoading } = useRoles();
|
||||
const { data, isLoading: isRolesLoading } = useRoles();
|
||||
const roles = data?.data;
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: 'roleMappings',
|
||||
});
|
||||
|
||||
const handleAppendMapping = () => append({ roleId: '', remoteRoleName: '' });
|
||||
|
||||
const handleRemoveMapping = (index) => () => remove(index);
|
||||
|
||||
return (
|
||||
<>
|
||||
{fields.length === 0 && (
|
||||
@@ -60,7 +68,7 @@ function RoleMappingsFieldArray() {
|
||||
label={formatMessage('roleMappingsForm.role')}
|
||||
/>
|
||||
)}
|
||||
loading={rolesLoading}
|
||||
loading={isRolesLoading}
|
||||
required
|
||||
/>
|
||||
</Stack>
|
||||
|
@@ -2,8 +2,9 @@ import { useMutation } from '@apollo/client';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import MuiTextField from '@mui/material/TextField';
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
import * as React from 'react';
|
||||
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
import ControlledAutocomplete from 'components/ControlledAutocomplete';
|
||||
import Form from 'components/Form';
|
||||
import Switch from 'components/Switch';
|
||||
@@ -11,6 +12,7 @@ import TextField from 'components/TextField';
|
||||
import { UPSERT_SAML_AUTH_PROVIDER } from 'graphql/mutations/upsert-saml-auth-provider';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useRoles from 'hooks/useRoles.ee';
|
||||
|
||||
const defaultValues = {
|
||||
active: false,
|
||||
name: '',
|
||||
@@ -24,16 +26,21 @@ const defaultValues = {
|
||||
roleAttributeName: '',
|
||||
defaultRoleId: '',
|
||||
};
|
||||
|
||||
function generateRoleOptions(roles) {
|
||||
return roles?.map(({ name: label, id: value }) => ({ label, value }));
|
||||
}
|
||||
|
||||
function SamlConfiguration({ provider, providerLoading, refetchProvider }) {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { roles, loading: rolesLoading } = useRoles();
|
||||
const { data, loading: isRolesLoading } = useRoles();
|
||||
const roles = data?.data;
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
|
||||
const [upsertSamlAuthProvider, { loading }] = useMutation(
|
||||
UPSERT_SAML_AUTH_PROVIDER,
|
||||
);
|
||||
|
||||
const handleProviderUpdate = async (providerDataToUpdate) => {
|
||||
try {
|
||||
const {
|
||||
@@ -66,9 +73,11 @@ function SamlConfiguration({ provider, providerLoading, refetchProvider }) {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!provider?.id) {
|
||||
await refetchProvider();
|
||||
}
|
||||
|
||||
enqueueSnackbar(formatMessage('authenticationForm.successfullySaved'), {
|
||||
variant: 'success',
|
||||
SnackbarProps: {
|
||||
@@ -79,9 +88,11 @@ function SamlConfiguration({ provider, providerLoading, refetchProvider }) {
|
||||
throw new Error('Failed while saving!');
|
||||
}
|
||||
};
|
||||
|
||||
if (providerLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
defaultValues={provider || defaultValues}
|
||||
@@ -170,7 +181,7 @@ function SamlConfiguration({ provider, providerLoading, refetchProvider }) {
|
||||
label={formatMessage('authenticationForm.defaultRole')}
|
||||
/>
|
||||
)}
|
||||
loading={rolesLoading}
|
||||
loading={isRolesLoading}
|
||||
/>
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
|
@@ -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 Can from 'components/Can';
|
||||
import Container from 'components/Container';
|
||||
import ControlledAutocomplete from 'components/ControlledAutocomplete';
|
||||
@@ -16,15 +17,19 @@ import * as URLS from 'config/urls';
|
||||
import { CREATE_USER } from 'graphql/mutations/create-user.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useRoles from 'hooks/useRoles.ee';
|
||||
|
||||
function generateRoleOptions(roles) {
|
||||
return roles?.map(({ name: label, id: value }) => ({ label, value }));
|
||||
}
|
||||
|
||||
export default function CreateUser() {
|
||||
const navigate = useNavigate();
|
||||
const formatMessage = useFormatMessage();
|
||||
const [createUser, { loading }] = useMutation(CREATE_USER);
|
||||
const { roles, loading: rolesLoading } = useRoles();
|
||||
const { data, loading: isRolesLoading } = useRoles();
|
||||
const roles = data?.data;
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
|
||||
const handleUserCreation = async (userData) => {
|
||||
try {
|
||||
await createUser({
|
||||
@@ -39,6 +44,7 @@ export default function CreateUser() {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
enqueueSnackbar(formatMessage('createUser.successfullyCreated'), {
|
||||
variant: 'success',
|
||||
persist: true,
|
||||
@@ -46,11 +52,13 @@ export default function CreateUser() {
|
||||
'data-test': 'snackbar-create-user-success',
|
||||
},
|
||||
});
|
||||
|
||||
navigate(URLS.USERS);
|
||||
} catch (error) {
|
||||
throw new Error('Failed while creating!');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||
<Grid container item xs={12} sm={10} md={9}>
|
||||
@@ -101,7 +109,7 @@ export default function CreateUser() {
|
||||
label={formatMessage('userForm.role')}
|
||||
/>
|
||||
)}
|
||||
loading={rolesLoading}
|
||||
loading={isRolesLoading}
|
||||
/>
|
||||
</Can>
|
||||
|
||||
|
@@ -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 Can from 'components/Can';
|
||||
import Container from 'components/Container';
|
||||
import ControlledAutocomplete from 'components/ControlledAutocomplete';
|
||||
@@ -18,17 +19,21 @@ 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';
|
||||
|
||||
function generateRoleOptions(roles) {
|
||||
return roles?.map(({ name: label, id: value }) => ({ label, value }));
|
||||
}
|
||||
|
||||
export default function EditUser() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const [updateUser, { loading }] = useMutation(UPDATE_USER);
|
||||
const { userId } = useParams();
|
||||
const { user, loading: userLoading } = useUser(userId);
|
||||
const { roles, loading: rolesLoading } = useRoles();
|
||||
const { data, loading: isRolesLoading } = useRoles();
|
||||
const roles = data?.data;
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleUserUpdate = async (userDataToUpdate) => {
|
||||
try {
|
||||
await updateUser({
|
||||
@@ -43,6 +48,7 @@ export default function EditUser() {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
enqueueSnackbar(formatMessage('editUser.successfullyUpdated'), {
|
||||
variant: 'success',
|
||||
SnackbarProps: {
|
||||
@@ -50,11 +56,13 @@ export default function EditUser() {
|
||||
persist: true,
|
||||
},
|
||||
});
|
||||
|
||||
navigate(URLS.USERS);
|
||||
} catch (error) {
|
||||
throw new Error('Failed while updating!');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||
<Grid container item xs={12} sm={10} md={9}>
|
||||
@@ -106,7 +114,7 @@ export default function EditUser() {
|
||||
label={formatMessage('userForm.role')}
|
||||
/>
|
||||
)}
|
||||
loading={rolesLoading}
|
||||
loading={isRolesLoading}
|
||||
/>
|
||||
</Can>
|
||||
|
||||
|
Reference in New Issue
Block a user