From 977cd70d6774d29a95b5e193c27c6acfab85ffa1 Mon Sep 17 00:00:00 2001
From: Douglas Fabris <devfabris@gmail.com>
Date: Thu, 14 Apr 2022 16:30:04 -0300
Subject: [PATCH] [IMPROVE] Rewrite Admin Permissions to Typescript

Co-authored-by: gabriellsh <gabriel.henriques@rocket.chat>
---
 apps/meteor/app/api/server/v1/roles.ts        |   2 +
 .../components/GenericTable/HeaderCell.tsx    |   1 +
 .../GenericTable/hooks/usePagination.ts       |  24 ++--
 .../{EditRolePage.js => EditRolePage.tsx}     |  71 +++++++----
 .../permissions/EditRolePageContainer.js      |  19 ---
 .../permissions/EditRolePageWithData.tsx      |  22 ++++
 .../views/admin/permissions/NewRolePage.js    |  62 ---------
 .../views/admin/permissions/PermissionRow.js  |  71 -----------
 ...ontextBar.js => PermissionsContextBar.tsx} |  18 ++-
 ...issionsRouter.js => PermissionsRouter.tsx} |   8 +-
 .../admin/permissions/PermissionsTable.js     | 117 -----------------
 .../PermissionsTable/PermissionRow.tsx        |  63 ++++++++++
 .../PermissionsTable/PermissionsTable.tsx     | 119 ++++++++++++++++++
 .../PermissionsTableFilter.tsx}               |   9 +-
 .../RoleCell.tsx}                             |  23 +++-
 .../RoleHeader.tsx}                           |  22 +++-
 .../permissions/PermissionsTable/index.ts     |   1 +
 .../views/admin/permissions/RoleForm.js       |  55 --------
 .../views/admin/permissions/RoleForm.tsx      |  71 +++++++++++
 .../client/views/admin/permissions/UserRow.js |  45 -------
 .../UsersInRolePage.tsx}                      |  56 ++++-----
 .../UsersInRole/UsersInRolePageWithData.tsx   |  18 +++
 .../UsersInRoleTable/UsersInRoleTable.tsx     |  91 ++++++++++++++
 .../UsersInRoleTable/UsersInRoleTableRow.tsx  |  52 ++++++++
 .../UsersInRoleTableWithData.tsx              |  66 ++++++++++
 .../UsersInRole/UsersInRoleTable/index.ts     |   1 +
 .../admin/permissions/UsersInRole/index.ts    |   1 +
 .../permissions/UsersInRolePageContainer.js   |  19 ---
 .../admin/permissions/UsersInRoleTable.js     |  59 ---------
 .../permissions/UsersInRoleTableContainer.js  |  43 -------
 .../admin/permissions/hooks/useChangeRole.ts  |  30 +++++
 .../hooks/usePermissionsAndRoles.ts           |  15 ++-
 .../views/admin/permissions/hooks/useRole.ts  |   7 ++
 .../client/views/admin/permissions/useRole.js |   6 -
 packages/core-typings/src/IUser.ts            |   5 +
 packages/rest-typings/src/v1/roles.ts         |  10 +-
 36 files changed, 693 insertions(+), 609 deletions(-)
 rename apps/meteor/client/views/admin/permissions/{EditRolePage.js => EditRolePage.tsx} (58%)
 delete mode 100644 apps/meteor/client/views/admin/permissions/EditRolePageContainer.js
 create mode 100644 apps/meteor/client/views/admin/permissions/EditRolePageWithData.tsx
 delete mode 100644 apps/meteor/client/views/admin/permissions/NewRolePage.js
 delete mode 100644 apps/meteor/client/views/admin/permissions/PermissionRow.js
 rename apps/meteor/client/views/admin/permissions/{PermissionsContextBar.js => PermissionsContextBar.tsx} (57%)
 rename apps/meteor/client/views/admin/permissions/{PermissionsRouter.js => PermissionsRouter.tsx} (75%)
 delete mode 100644 apps/meteor/client/views/admin/permissions/PermissionsTable.js
 create mode 100644 apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionRow.tsx
 create mode 100644 apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx
 rename apps/meteor/client/views/admin/permissions/{FilterComponent.js => PermissionsTable/PermissionsTableFilter.tsx} (65%)
 rename apps/meteor/client/views/admin/permissions/{RoleCell.js => PermissionsTable/RoleCell.tsx} (55%)
 rename apps/meteor/client/views/admin/permissions/{RoleHeader.js => PermissionsTable/RoleHeader.tsx} (51%)
 create mode 100644 apps/meteor/client/views/admin/permissions/PermissionsTable/index.ts
 delete mode 100644 apps/meteor/client/views/admin/permissions/RoleForm.js
 create mode 100644 apps/meteor/client/views/admin/permissions/RoleForm.tsx
 delete mode 100644 apps/meteor/client/views/admin/permissions/UserRow.js
 rename apps/meteor/client/views/admin/permissions/{UsersInRolePage.js => UsersInRole/UsersInRolePage.tsx} (61%)
 create mode 100644 apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePageWithData.tsx
 create mode 100644 apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTable.tsx
 create mode 100644 apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTableRow.tsx
 create mode 100644 apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTableWithData.tsx
 create mode 100644 apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/index.ts
 create mode 100644 apps/meteor/client/views/admin/permissions/UsersInRole/index.ts
 delete mode 100644 apps/meteor/client/views/admin/permissions/UsersInRolePageContainer.js
 delete mode 100644 apps/meteor/client/views/admin/permissions/UsersInRoleTable.js
 delete mode 100644 apps/meteor/client/views/admin/permissions/UsersInRoleTableContainer.js
 create mode 100644 apps/meteor/client/views/admin/permissions/hooks/useChangeRole.ts
 create mode 100644 apps/meteor/client/views/admin/permissions/hooks/useRole.ts
 delete mode 100644 apps/meteor/client/views/admin/permissions/useRole.js

diff --git a/apps/meteor/app/api/server/v1/roles.ts b/apps/meteor/app/api/server/v1/roles.ts
index 6ebf23aab8c..9785741326d 100644
--- a/apps/meteor/app/api/server/v1/roles.ts
+++ b/apps/meteor/app/api/server/v1/roles.ts
@@ -150,6 +150,8 @@ API.v1.addRoute(
 				username: 1,
 				emails: 1,
 				avatarETag: 1,
+				createdAt: 1,
+				_updatedAt: 1,
 			};
 
 			if (!role) {
diff --git a/apps/meteor/client/components/GenericTable/HeaderCell.tsx b/apps/meteor/client/components/GenericTable/HeaderCell.tsx
index 414dc1fa1be..b7161614c56 100644
--- a/apps/meteor/client/components/GenericTable/HeaderCell.tsx
+++ b/apps/meteor/client/components/GenericTable/HeaderCell.tsx
@@ -7,6 +7,7 @@ type HeaderCellProps = {
 	active?: boolean;
 	direction?: 'asc' | 'desc';
 	sort?: string;
+	clickable?: boolean;
 	onClick?: (sort: string) => void;
 } & ComponentProps<typeof Box>;
 
diff --git a/apps/meteor/client/components/GenericTable/hooks/usePagination.ts b/apps/meteor/client/components/GenericTable/hooks/usePagination.ts
index 3f0558f4ac9..31276fd57ce 100644
--- a/apps/meteor/client/components/GenericTable/hooks/usePagination.ts
+++ b/apps/meteor/client/components/GenericTable/hooks/usePagination.ts
@@ -1,3 +1,5 @@
+import { useMemo } from 'react';
+
 import { useCurrent } from './useCurrent';
 import { useItemsPerPage } from './useItemsPerPage';
 import { useItemsPerPageLabel } from './useItemsPerPageLabel';
@@ -12,19 +14,19 @@ export const usePagination = (): {
 	showingResultsLabel: ReturnType<typeof useShowingResultsLabel>;
 } => {
 	const [itemsPerPage, setItemsPerPage] = useItemsPerPage();
-
 	const [current, setCurrent] = useCurrent();
-
 	const itemsPerPageLabel = useItemsPerPageLabel();
-
 	const showingResultsLabel = useShowingResultsLabel();
 
-	return {
-		itemsPerPage,
-		setItemsPerPage,
-		current,
-		setCurrent,
-		itemsPerPageLabel,
-		showingResultsLabel,
-	};
+	return useMemo(
+		() => ({
+			itemsPerPage,
+			setItemsPerPage,
+			current,
+			setCurrent,
+			itemsPerPageLabel,
+			showingResultsLabel,
+		}),
+		[itemsPerPage, setItemsPerPage, current, setCurrent, itemsPerPageLabel, showingResultsLabel],
+	);
 };
diff --git a/apps/meteor/client/views/admin/permissions/EditRolePage.js b/apps/meteor/client/views/admin/permissions/EditRolePage.tsx
similarity index 58%
rename from apps/meteor/client/views/admin/permissions/EditRolePage.js
rename to apps/meteor/client/views/admin/permissions/EditRolePage.tsx
index 512f6157da1..73966022002 100644
--- a/apps/meteor/client/views/admin/permissions/EditRolePage.js
+++ b/apps/meteor/client/views/admin/permissions/EditRolePage.tsx
@@ -1,6 +1,8 @@
+import { IRole } from '@rocket.chat/core-typings';
 import { Box, ButtonGroup, Button, Margins } from '@rocket.chat/fuselage';
 import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import React from 'react';
+import React, { ReactElement } from 'react';
+import { FormProvider, useForm } from 'react-hook-form';
 
 import GenericModal from '../../../components/GenericModal';
 import VerticalBar from '../../../components/VerticalBar';
@@ -9,37 +11,47 @@ import { useRoute } from '../../../contexts/RouterContext';
 import { useEndpoint } from '../../../contexts/ServerContext';
 import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
 import { useTranslation } from '../../../contexts/TranslationContext';
-import { useForm } from '../../../hooks/useForm';
 import RoleForm from './RoleForm';
 
-const EditRolePage = ({ data }) => {
+const EditRolePage = ({ role }: { role?: IRole }): ReactElement => {
 	const t = useTranslation();
 	const dispatchToastMessage = useToastMessageDispatch();
 	const setModal = useSetModal();
 	const usersInRoleRouter = useRoute('admin-permissions');
 	const router = useRoute('admin-permissions');
 
-	const { values, handlers, hasUnsavedChanges } = useForm({
-		roleId: data._id,
-		name: data.name,
-		description: data.description || '',
-		scope: data.scope || 'Users',
-		mandatory2fa: !!data.mandatory2fa,
-	});
-
-	const saveRole = useEndpoint('POST', 'roles.update');
+	const createRole = useEndpoint('POST', 'roles.create');
+	const updateRole = useEndpoint('POST', 'roles.update');
 	const deleteRole = useEndpoint('POST', 'roles.delete');
 
+	const methods = useForm({
+		defaultValues: {
+			roleId: role?._id,
+			name: role?.name,
+			description: role?.description,
+			scope: role?.scope || 'Users',
+			mandatory2fa: !!role?.mandatory2fa,
+		},
+	});
+
 	const handleManageUsers = useMutableCallback(() => {
-		usersInRoleRouter.push({
-			context: 'users-in-role',
-			_id: data._id,
-		});
+		if (role?._id) {
+			usersInRoleRouter.push({
+				context: 'users-in-role',
+				_id: role._id,
+			});
+		}
 	});
 
-	const handleSave = useMutableCallback(async () => {
+	const handleSave = useMutableCallback(async (data) => {
 		try {
-			await saveRole(values);
+			if (data.roleId) {
+				await updateRole(data);
+				dispatchToastMessage({ type: 'success', message: t('Saved') });
+				return router.push({});
+			}
+
+			await createRole(data);
 			dispatchToastMessage({ type: 'success', message: t('Saved') });
 			router.push({});
 		} catch (error) {
@@ -48,11 +60,16 @@ const EditRolePage = ({ data }) => {
 	});
 
 	const handleDelete = useMutableCallback(async () => {
-		const deleteRoleAction = async () => {
+		if (!role?._id) {
+			return;
+		}
+
+		const deleteRoleAction = async (): Promise<void> => {
 			try {
-				await deleteRole({ roleId: data._id });
+				await deleteRole({ roleId: role._id });
 				dispatchToastMessage({ type: 'success', message: t('Role_removed') });
 				setModal();
+
 				router.push({});
 			} catch (error) {
 				dispatchToastMessage({ type: 'error', message: error });
@@ -64,8 +81,8 @@ const EditRolePage = ({ data }) => {
 			<GenericModal
 				variant='danger'
 				onConfirm={deleteRoleAction}
-				onClose={() => setModal()}
-				onCancel={() => setModal()}
+				onClose={(): void => setModal()}
+				onCancel={(): void => setModal()}
 				confirmText={t('Delete')}
 			>
 				{t('Delete_Role_Warning')}
@@ -78,21 +95,23 @@ const EditRolePage = ({ data }) => {
 			<VerticalBar.ScrollableContent>
 				<Box w='full' alignSelf='center' mb='neg-x8'>
 					<Margins block='x8'>
-						<RoleForm values={values} handlers={handlers} editing isProtected={data.protected} />
+						<FormProvider {...methods}>
+							<RoleForm editing={Boolean(role?._id)} isProtected={role?.protected} />
+						</FormProvider>
 					</Margins>
 				</Box>
 			</VerticalBar.ScrollableContent>
 			<VerticalBar.Footer>
 				<ButtonGroup vertical stretch>
-					<Button primary disabled={!hasUnsavedChanges} onClick={handleSave}>
+					<Button primary disabled={!methods.formState.isDirty} onClick={methods.handleSubmit(handleSave)}>
 						{t('Save')}
 					</Button>
-					{!data.protected && (
+					{!role?.protected && role?._id && (
 						<Button danger onClick={handleDelete}>
 							{t('Delete')}
 						</Button>
 					)}
-					<Button onClick={handleManageUsers}>{t('Users_in_role')}</Button>
+					{role?._id && <Button onClick={handleManageUsers}>{t('Users_in_role')}</Button>}
 				</ButtonGroup>
 			</VerticalBar.Footer>
 		</>
diff --git a/apps/meteor/client/views/admin/permissions/EditRolePageContainer.js b/apps/meteor/client/views/admin/permissions/EditRolePageContainer.js
deleted file mode 100644
index d98cfd881aa..00000000000
--- a/apps/meteor/client/views/admin/permissions/EditRolePageContainer.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Callout } from '@rocket.chat/fuselage';
-import React from 'react';
-
-import { useTranslation } from '../../../contexts/TranslationContext';
-import EditRolePage from './EditRolePage';
-import { useRole } from './useRole';
-
-const EditRolePageContainer = ({ _id }) => {
-	const t = useTranslation();
-	const role = useRole(_id);
-
-	if (!role) {
-		return <Callout type='danger'>{t('error-invalid-role')}</Callout>;
-	}
-
-	return <EditRolePage key={_id} data={role} />;
-};
-
-export default EditRolePageContainer;
diff --git a/apps/meteor/client/views/admin/permissions/EditRolePageWithData.tsx b/apps/meteor/client/views/admin/permissions/EditRolePageWithData.tsx
new file mode 100644
index 00000000000..d4ca593553a
--- /dev/null
+++ b/apps/meteor/client/views/admin/permissions/EditRolePageWithData.tsx
@@ -0,0 +1,22 @@
+import { IRole } from '@rocket.chat/core-typings';
+import { Callout } from '@rocket.chat/fuselage';
+import React, { ReactElement } from 'react';
+
+import { useRouteParameter } from '../../../contexts/RouterContext';
+import { useTranslation } from '../../../contexts/TranslationContext';
+import EditRolePage from './EditRolePage';
+import { useRole } from './hooks/useRole';
+
+const EditRolePageWithData = ({ roleId }: { roleId?: IRole['_id'] }): ReactElement => {
+	const t = useTranslation();
+	const role = useRole(roleId);
+	const context = useRouteParameter('context');
+
+	if (!role && context === 'edit') {
+		return <Callout type='danger'>{t('error-invalid-role')}</Callout>;
+	}
+
+	return <EditRolePage key={roleId} role={role} />;
+};
+
+export default EditRolePageWithData;
diff --git a/apps/meteor/client/views/admin/permissions/NewRolePage.js b/apps/meteor/client/views/admin/permissions/NewRolePage.js
deleted file mode 100644
index 4ce60a700d2..00000000000
--- a/apps/meteor/client/views/admin/permissions/NewRolePage.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Box, ButtonGroup, Button, Margins } from '@rocket.chat/fuselage';
-import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import React, { useState } from 'react';
-
-import VerticalBar from '../../../components/VerticalBar';
-import { useRoute } from '../../../contexts/RouterContext';
-import { useEndpoint } from '../../../contexts/ServerContext';
-import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
-import { useTranslation } from '../../../contexts/TranslationContext';
-import { useForm } from '../../../hooks/useForm';
-import RoleForm from './RoleForm';
-
-const NewRolePage = () => {
-	const t = useTranslation();
-	const router = useRoute('admin-permissions');
-	const dispatchToastMessage = useToastMessageDispatch();
-	const [errors, setErrors] = useState();
-
-	const { values, handlers } = useForm({
-		name: '',
-		description: '',
-		scope: 'Users',
-		mandatory2fa: false,
-	});
-
-	const saveRole = useEndpoint('POST', 'roles.create');
-
-	const handleSave = useMutableCallback(async () => {
-		if (values.name === '') {
-			return setErrors({ name: t('error-the-field-is-required', { field: t('Role') }) });
-		}
-
-		try {
-			await saveRole(values);
-			dispatchToastMessage({ type: 'success', message: t('Saved') });
-			router.push({});
-		} catch (error) {
-			dispatchToastMessage({ type: 'error', message: error });
-		}
-	});
-
-	return (
-		<>
-			<VerticalBar.ScrollableContent>
-				<Box w='full' alignSelf='center' mb='neg-x8'>
-					<Margins block='x8'>
-						<RoleForm values={values} handlers={handlers} errors={errors} />
-					</Margins>
-				</Box>
-			</VerticalBar.ScrollableContent>
-			<VerticalBar.Footer>
-				<ButtonGroup stretch w='full'>
-					<Button primary onClick={handleSave}>
-						{t('Save')}
-					</Button>
-				</ButtonGroup>
-			</VerticalBar.Footer>
-		</>
-	);
-};
-
-export default NewRolePage;
diff --git a/apps/meteor/client/views/admin/permissions/PermissionRow.js b/apps/meteor/client/views/admin/permissions/PermissionRow.js
deleted file mode 100644
index d5de63fa7a2..00000000000
--- a/apps/meteor/client/views/admin/permissions/PermissionRow.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import { Table } from '@rocket.chat/fuselage';
-import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import React, { useState, memo } from 'react';
-
-import { CONSTANTS } from '../../../../app/authorization/lib';
-import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
-import RoleCell from './RoleCell';
-
-const useChangeRole = ({ onGrant, onRemove, permissionId }) => {
-	const dispatchToastMessage = useToastMessageDispatch();
-	return useMutableCallback(async (roleId, granted) => {
-		try {
-			if (granted) {
-				await onRemove(permissionId, roleId);
-			} else {
-				await onGrant(permissionId, roleId);
-			}
-			return !granted;
-		} catch (error) {
-			dispatchToastMessage({ type: 'error', message: error });
-		}
-		return granted;
-	});
-};
-
-const getName = (t, permission) => {
-	if (permission.level === CONSTANTS.SETTINGS_LEVEL) {
-		let path = '';
-		if (permission.group) {
-			path = `${t(permission.group)} > `;
-		}
-		if (permission.section) {
-			path = `${path}${t(permission.section)} > `;
-		}
-		return `${path}${t(permission.settingId)}`;
-	}
-
-	return t(permission._id);
-};
-
-const PermissionRow = ({ permission, t, roleList, onGrant, onRemove, ...props }) => {
-	const { _id, roles } = permission;
-
-	const [hovered, setHovered] = useState(false);
-
-	const onMouseEnter = useMutableCallback(() => setHovered(true));
-	const onMouseLeave = useMutableCallback(() => setHovered(false));
-
-	const changeRole = useChangeRole({ onGrant, onRemove, permissionId: _id });
-	return (
-		<Table.Row key={_id} role='link' action tabIndex={0} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} {...props}>
-			<Table.Cell maxWidth='x300' withTruncatedText title={t(`${_id}_description`)}>
-				{getName(t, permission)}
-			</Table.Cell>
-			{roleList.map(({ _id, name, description }) => (
-				<RoleCell
-					key={_id}
-					_id={_id}
-					name={name}
-					description={description}
-					grantedRoles={roles}
-					onChange={changeRole}
-					lineHovered={hovered}
-					permissionId={_id}
-				/>
-			))}
-		</Table.Row>
-	);
-};
-
-export default memo(PermissionRow);
diff --git a/apps/meteor/client/views/admin/permissions/PermissionsContextBar.js b/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx
similarity index 57%
rename from apps/meteor/client/views/admin/permissions/PermissionsContextBar.js
rename to apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx
index 992a903173c..8eb9de3434e 100644
--- a/apps/meteor/client/views/admin/permissions/PermissionsContextBar.js
+++ b/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx
@@ -1,20 +1,18 @@
 import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import React from 'react';
+import React, { ReactElement } from 'react';
 
 import VerticalBar from '../../../components/VerticalBar';
 import { useRouteParameter, useRoute } from '../../../contexts/RouterContext';
 import { useTranslation } from '../../../contexts/TranslationContext';
-import EditRolePage from './EditRolePageContainer';
-import NewRolePage from './NewRolePage';
+import EditRolePageWithData from './EditRolePageWithData';
 
-const PermissionsContextBar = () => {
+const PermissionsContextBar = (): ReactElement | null => {
 	const t = useTranslation();
 	const _id = useRouteParameter('_id');
 	const context = useRouteParameter('context');
-
 	const router = useRoute('admin-permissions');
 
-	const handleVerticalBarCloseButton = useMutableCallback(() => {
+	const handleCloseVerticalBar = useMutableCallback(() => {
 		router.push({});
 	});
 
@@ -22,12 +20,10 @@ const PermissionsContextBar = () => {
 		(context && (
 			<VerticalBar>
 				<VerticalBar.Header>
-					{context === 'new' && t('New_role')}
-					{context === 'edit' && t('Role_Editing')}
-					<VerticalBar.Close onClick={handleVerticalBarCloseButton} />
+					{context === 'edit' ? t('Role_Editing') : t('New_role')}
+					<VerticalBar.Close onClick={handleCloseVerticalBar} />
 				</VerticalBar.Header>
-				{context === 'new' && <NewRolePage />}
-				{context === 'edit' && <EditRolePage _id={_id} />}
+				<EditRolePageWithData roleId={_id} />
 			</VerticalBar>
 		)) ||
 		null
diff --git a/apps/meteor/client/views/admin/permissions/PermissionsRouter.js b/apps/meteor/client/views/admin/permissions/PermissionsRouter.tsx
similarity index 75%
rename from apps/meteor/client/views/admin/permissions/PermissionsRouter.js
rename to apps/meteor/client/views/admin/permissions/PermissionsRouter.tsx
index 6903c9dc18f..8203b92f4fd 100644
--- a/apps/meteor/client/views/admin/permissions/PermissionsRouter.js
+++ b/apps/meteor/client/views/admin/permissions/PermissionsRouter.tsx
@@ -1,12 +1,12 @@
-import React from 'react';
+import React, { ReactElement } from 'react';
 
 import { usePermission } from '../../../contexts/AuthorizationContext';
 import { useRouteParameter } from '../../../contexts/RouterContext';
 import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage';
-import PermissionsTable from './PermissionsTable';
-import UsersInRole from './UsersInRolePageContainer';
+import PermissionsTable from './PermissionsTable/PermissionsTable';
+import UsersInRole from './UsersInRole';
 
-const PermissionsRouter = () => {
+const PermissionsRouter = (): ReactElement => {
 	const canViewPermission = usePermission('access-permissions');
 	const canViewSettingPermission = usePermission('access-setting-permissions');
 	const context = useRouteParameter('context');
diff --git a/apps/meteor/client/views/admin/permissions/PermissionsTable.js b/apps/meteor/client/views/admin/permissions/PermissionsTable.js
deleted file mode 100644
index f5f39f3e395..00000000000
--- a/apps/meteor/client/views/admin/permissions/PermissionsTable.js
+++ /dev/null
@@ -1,117 +0,0 @@
-import { Margins, Icon, Tabs, Button } from '@rocket.chat/fuselage';
-import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import React, { useState, useCallback } from 'react';
-
-import GenericTable from '../../../components/GenericTable';
-import Page from '../../../components/Page';
-import { usePermission } from '../../../contexts/AuthorizationContext';
-import { useRoute } from '../../../contexts/RouterContext';
-import { useMethod } from '../../../contexts/ServerContext';
-import { useTranslation } from '../../../contexts/TranslationContext';
-import FilterComponent from './FilterComponent';
-import PermissionRow from './PermissionRow';
-import PermissionsContextBar from './PermissionsContextBar';
-import RoleHeader from './RoleHeader';
-import { usePermissionsAndRoles } from './hooks/usePermissionsAndRoles';
-
-const PermissionsTable = () => {
-	const t = useTranslation();
-	const [filter, setFilter] = useState('');
-	const canViewPermission = usePermission('access-permissions');
-	const canViewSettingPermission = usePermission('access-setting-permissions');
-	const defaultType = canViewPermission ? 'permissions' : 'settings';
-	const [type, setType] = useState(defaultType);
-	const [params, setParams] = useState({ limit: 25, skip: 0 });
-
-	const router = useRoute('admin-permissions');
-
-	const grantRole = useMethod('authorization:addPermissionToRole');
-	const removeRole = useMethod('authorization:removeRoleFromPermission');
-
-	const permissionsData = usePermissionsAndRoles(type, filter, params.limit, params.skip);
-
-	const [permissions, total, roleList] = permissionsData;
-
-	const handleParams = useMutableCallback(({ current, itemsPerPage }) => {
-		setParams({ skip: current, limit: itemsPerPage });
-	});
-
-	const handlePermissionsTab = useMutableCallback(() => {
-		if (type === 'permissions') {
-			return;
-		}
-		setType('permissions');
-	});
-
-	const handleSettingsTab = useMutableCallback(() => {
-		if (type === 'settings') {
-			return;
-		}
-		setType('settings');
-	});
-
-	const handleAdd = useMutableCallback(() => {
-		router.push({
-			context: 'new',
-		});
-	});
-
-	return (
-		<Page flexDirection='row'>
-			<Page>
-				<Page.Header title={t('Permissions')}>
-					<Button primary onClick={handleAdd} aria-label={t('New')}>
-						<Icon name='plus' /> {t('New_role')}
-					</Button>
-				</Page.Header>
-				<Margins blockEnd='x16'>
-					<Tabs>
-						<Tabs.Item selected={type === 'permissions'} onClick={handlePermissionsTab} disabled={!canViewPermission}>
-							{t('Permissions')}
-						</Tabs.Item>
-						<Tabs.Item selected={type === 'settings'} onClick={handleSettingsTab} disabled={!canViewSettingPermission}>
-							{t('Settings')}
-						</Tabs.Item>
-					</Tabs>
-				</Margins>
-				<Page.Content mb='neg-x8'>
-					<Margins block='x8'>
-						<FilterComponent onChange={setFilter} />
-						<GenericTable
-							header={
-								<>
-									<GenericTable.HeaderCell width='x120'>{t('Name')}</GenericTable.HeaderCell>
-									{roleList.map(({ _id, name, description }) => (
-										<RoleHeader key={_id} _id={_id} name={name} description={description} router={router} />
-									))}
-								</>
-							}
-							total={total}
-							results={permissions}
-							params={params}
-							setParams={handleParams}
-							fixed={false}
-						>
-							{useCallback(
-								(permission) => (
-									<PermissionRow
-										key={permission._id}
-										permission={permission}
-										t={t}
-										roleList={roleList}
-										onGrant={grantRole}
-										onRemove={removeRole}
-									/>
-								),
-								[grantRole, removeRole, roleList, t],
-							)}
-						</GenericTable>
-					</Margins>
-				</Page.Content>
-			</Page>
-			<PermissionsContextBar />
-		</Page>
-	);
-};
-
-export default PermissionsTable;
diff --git a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionRow.tsx b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionRow.tsx
new file mode 100644
index 00000000000..40444a54445
--- /dev/null
+++ b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionRow.tsx
@@ -0,0 +1,63 @@
+import { IRole, IPermission } from '@rocket.chat/core-typings';
+import { TableRow, TableCell } from '@rocket.chat/fuselage';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import React, { useState, memo, ReactElement } from 'react';
+
+import { CONSTANTS } from '../../../../../app/authorization/lib';
+import { useTranslation, TranslationKey } from '../../../../contexts/TranslationContext';
+import { useChangeRole } from '../hooks/useChangeRole';
+import RoleCell from './RoleCell';
+
+const getName = (t: ReturnType<typeof useTranslation>, permission: IPermission): string => {
+	if (permission.level === CONSTANTS.SETTINGS_LEVEL) {
+		let path = '';
+		if (permission.group) {
+			path = `${t(permission.group as TranslationKey)} > `;
+		}
+		if (permission.section) {
+			path = `${path}${t(permission.section as TranslationKey)} > `;
+		}
+		return `${path}${t(permission.settingId as TranslationKey)}`;
+	}
+
+	return t(permission._id as TranslationKey);
+};
+
+type PermissionRowProps = {
+	permission: IPermission;
+	roleList: IRole[];
+	onGrant: (permissionId: IPermission['_id'], roleId: IRole['_id']) => Promise<void>;
+	onRemove: (permissionId: IPermission['_id'], roleId: IRole['_id']) => Promise<void>;
+};
+
+const PermissionRow = ({ permission, roleList, onGrant, onRemove }: PermissionRowProps): ReactElement => {
+	const t = useTranslation();
+	const { _id, roles } = permission;
+	const [hovered, setHovered] = useState(false);
+	const changeRole = useChangeRole({ onGrant, onRemove, permissionId: _id });
+
+	const onMouseEnter = useMutableCallback(() => setHovered(true));
+	const onMouseLeave = useMutableCallback(() => setHovered(false));
+
+	return (
+		<TableRow key={_id} role='link' action tabIndex={0} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
+			<TableCell maxWidth='x300' withTruncatedText title={t(`${_id}_description` as TranslationKey)}>
+				{getName(t, permission)}
+			</TableCell>
+			{roleList.map(({ _id, name, description }) => (
+				<RoleCell
+					key={_id}
+					_id={_id}
+					name={name}
+					description={description}
+					grantedRoles={roles}
+					onChange={changeRole}
+					lineHovered={hovered}
+					permissionId={_id}
+				/>
+			))}
+		</TableRow>
+	);
+};
+
+export default memo(PermissionRow);
diff --git a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx
new file mode 100644
index 00000000000..bb29255e569
--- /dev/null
+++ b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx
@@ -0,0 +1,119 @@
+import { Margins, Icon, Tabs, Button, Pagination, Tile } from '@rocket.chat/fuselage';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import React, { useState, ReactElement } from 'react';
+
+import { GenericTable, GenericTableHeader, GenericTableHeaderCell, GenericTableBody } from '../../../../components/GenericTable';
+import { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
+import Page from '../../../../components/Page';
+import { usePermission } from '../../../../contexts/AuthorizationContext';
+import { useRoute } from '../../../../contexts/RouterContext';
+import { useMethod } from '../../../../contexts/ServerContext';
+import { useTranslation } from '../../../../contexts/TranslationContext';
+import PermissionsContextBar from '../PermissionsContextBar';
+import { usePermissionsAndRoles } from '../hooks/usePermissionsAndRoles';
+import PermissionRow from './PermissionRow';
+import PermissionsTableFilter from './PermissionsTableFilter';
+import RoleHeader from './RoleHeader';
+
+const PermissionsTable = (): ReactElement => {
+	const t = useTranslation();
+	const [filter, setFilter] = useState('');
+	const canViewPermission = usePermission('access-permissions');
+	const canViewSettingPermission = usePermission('access-setting-permissions');
+	const defaultType = canViewPermission ? 'permissions' : 'settings';
+	const [type, setType] = useState(defaultType);
+	const router = useRoute('admin-permissions');
+
+	const grantRole = useMethod('authorization:addPermissionToRole');
+	const removeRole = useMethod('authorization:removeRoleFromPermission');
+
+	const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
+	const { permissions, total, roleList } = usePermissionsAndRoles(type, filter, itemsPerPage, current);
+
+	const handlePermissionsTab = useMutableCallback(() => {
+		if (type === 'permissions') {
+			return;
+		}
+		setType('permissions');
+	});
+
+	const handleSettingsTab = useMutableCallback(() => {
+		if (type === 'settings') {
+			return;
+		}
+		setType('settings');
+	});
+
+	const handleAdd = useMutableCallback(() => {
+		router.push({
+			context: 'new',
+		});
+	});
+
+	return (
+		<Page flexDirection='row'>
+			<Page>
+				<Page.Header title={t('Permissions')}>
+					<Button primary onClick={handleAdd} aria-label={t('New')}>
+						<Icon name='plus' /> {t('New_role')}
+					</Button>
+				</Page.Header>
+				<Margins blockEnd='x16'>
+					<Tabs>
+						<Tabs.Item selected={type === 'permissions'} onClick={handlePermissionsTab} disabled={!canViewPermission}>
+							{t('Permissions')}
+						</Tabs.Item>
+						<Tabs.Item selected={type === 'settings'} onClick={handleSettingsTab} disabled={!canViewSettingPermission}>
+							{t('Settings')}
+						</Tabs.Item>
+					</Tabs>
+				</Margins>
+				<Page.Content mb='neg-x8'>
+					<Margins block='x8'>
+						<PermissionsTableFilter onChange={setFilter} />
+						{permissions?.length === 0 && (
+							<Tile fontScale='p2' elevation='0' color='info' textAlign='center'>
+								{t('No_data_found')}
+							</Tile>
+						)}
+						{permissions?.length > 0 && (
+							<>
+								<GenericTable fixed={false}>
+									<GenericTableHeader>
+										<GenericTableHeaderCell width='x120'>{t('Name')}</GenericTableHeaderCell>
+										{roleList?.map(({ _id, name, description }) => (
+											<RoleHeader key={_id} _id={_id} name={name} description={description} />
+										))}
+									</GenericTableHeader>
+									<GenericTableBody>
+										{permissions?.map((permission) => (
+											<PermissionRow
+												key={permission._id}
+												permission={permission}
+												roleList={roleList}
+												onGrant={grantRole}
+												onRemove={removeRole}
+											/>
+										))}
+									</GenericTableBody>
+								</GenericTable>
+								<Pagination
+									divider
+									current={current}
+									itemsPerPage={itemsPerPage}
+									count={total}
+									onSetItemsPerPage={onSetItemsPerPage}
+									onSetCurrent={onSetCurrent}
+									{...paginationProps}
+								/>
+							</>
+						)}
+					</Margins>
+				</Page.Content>
+			</Page>
+			<PermissionsContextBar />
+		</Page>
+	);
+};
+
+export default PermissionsTable;
diff --git a/apps/meteor/client/views/admin/permissions/FilterComponent.js b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTableFilter.tsx
similarity index 65%
rename from apps/meteor/client/views/admin/permissions/FilterComponent.js
rename to apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTableFilter.tsx
index 4987f4e9c42..cc6122cb80e 100644
--- a/apps/meteor/client/views/admin/permissions/FilterComponent.js
+++ b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTableFilter.tsx
@@ -1,13 +1,12 @@
 import { TextInput } from '@rocket.chat/fuselage';
 import { useMutableCallback, useDebouncedValue } from '@rocket.chat/fuselage-hooks';
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, ReactElement } from 'react';
 
-import { useTranslation } from '../../../contexts/TranslationContext';
+import { useTranslation } from '../../../../contexts/TranslationContext';
 
-const FilterComponent = ({ onChange }) => {
+const PermissionsTableFilter = ({ onChange }: { onChange: (debouncedFilter: string) => void }): ReactElement => {
 	const t = useTranslation();
 	const [filter, setFilter] = useState('');
-
 	const debouncedFilter = useDebouncedValue(filter, 500);
 
 	useEffect(() => {
@@ -21,4 +20,4 @@ const FilterComponent = ({ onChange }) => {
 	return <TextInput value={filter} onChange={handleFilter} placeholder={t('Search')} flexGrow={0} />;
 };
 
-export default FilterComponent;
+export default PermissionsTableFilter;
diff --git a/apps/meteor/client/views/admin/permissions/RoleCell.js b/apps/meteor/client/views/admin/permissions/PermissionsTable/RoleCell.tsx
similarity index 55%
rename from apps/meteor/client/views/admin/permissions/RoleCell.js
rename to apps/meteor/client/views/admin/permissions/PermissionsTable/RoleCell.tsx
index d8c41cf3039..275879c560d 100644
--- a/apps/meteor/client/views/admin/permissions/RoleCell.js
+++ b/apps/meteor/client/views/admin/permissions/PermissionsTable/RoleCell.tsx
@@ -1,10 +1,21 @@
-import { Table, Margins, Box, CheckBox, Throbber } from '@rocket.chat/fuselage';
+import { IRole } from '@rocket.chat/core-typings';
+import { TableCell, Margins, Box, CheckBox, Throbber } from '@rocket.chat/fuselage';
 import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import React, { useState, memo } from 'react';
+import React, { useState, memo, ReactElement } from 'react';
 
-import { AuthorizationUtils } from '../../../../app/authorization/lib';
+import { AuthorizationUtils } from '../../../../../app/authorization/lib';
 
-const RoleCell = ({ grantedRoles = [], _id, name, description, onChange, lineHovered, permissionId }) => {
+type RoleCellProps = {
+	_id: IRole['_id'];
+	name: IRole['name'];
+	description: IRole['description'];
+	onChange: (roleId: IRole['_id'], granted: boolean) => Promise<boolean>;
+	lineHovered: boolean;
+	permissionId: string;
+	grantedRoles: IRole['_id'][];
+};
+
+const RoleCell = ({ _id, name, description, onChange, lineHovered, permissionId, grantedRoles = [] }: RoleCellProps): ReactElement => {
 	const [granted, setGranted] = useState(() => !!grantedRoles.includes(_id));
 	const [loading, setLoading] = useState(false);
 
@@ -20,7 +31,7 @@ const RoleCell = ({ grantedRoles = [], _id, name, description, onChange, lineHov
 	const isDisabled = !!loading || !!isRestrictedForRole;
 
 	return (
-		<Table.Cell withTruncatedText>
+		<TableCell withTruncatedText>
 			<Margins inline='x2'>
 				<CheckBox checked={granted} onChange={handleChange} disabled={isDisabled} />
 				{!loading && (
@@ -30,7 +41,7 @@ const RoleCell = ({ grantedRoles = [], _id, name, description, onChange, lineHov
 				)}
 				{loading && <Throbber size='x12' display='inline-block' />}
 			</Margins>
-		</Table.Cell>
+		</TableCell>
 	);
 };
 
diff --git a/apps/meteor/client/views/admin/permissions/RoleHeader.js b/apps/meteor/client/views/admin/permissions/PermissionsTable/RoleHeader.tsx
similarity index 51%
rename from apps/meteor/client/views/admin/permissions/RoleHeader.js
rename to apps/meteor/client/views/admin/permissions/PermissionsTable/RoleHeader.tsx
index ddaa47fe2f2..a95671baa7d 100644
--- a/apps/meteor/client/views/admin/permissions/RoleHeader.js
+++ b/apps/meteor/client/views/admin/permissions/PermissionsTable/RoleHeader.tsx
@@ -1,12 +1,22 @@
+import { IRole } from '@rocket.chat/core-typings';
 import { css } from '@rocket.chat/css-in-js';
 import { Margins, Box, Icon } from '@rocket.chat/fuselage';
 import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import React, { memo } from 'react';
+import React, { memo, ReactElement } from 'react';
 
-import GenericTable from '../../../components/GenericTable';
+import GenericTable from '../../../../components/GenericTable';
+import { useRoute } from '../../../../contexts/RouterContext';
 
-const RoleHeader = ({ router, _id, name, description, ...props }) => {
-	const onClick = useMutableCallback(() => {
+type RoleHeaderProps = {
+	_id: IRole['_id'];
+	name: IRole['name'];
+	description: IRole['description'];
+};
+
+const RoleHeader = ({ _id, name, description }: RoleHeaderProps): ReactElement => {
+	const router = useRoute('admin-permissions');
+
+	const handleEditRole = useMutableCallback(() => {
 		router.push({
 			context: 'edit',
 			_id,
@@ -14,7 +24,7 @@ const RoleHeader = ({ router, _id, name, description, ...props }) => {
 	});
 
 	return (
-		<GenericTable.HeaderCell clickable pi='x4' p='x8' {...props}>
+		<GenericTable.HeaderCell clickable pi='x4' p='x8'>
 			<Box
 				className={css`
 					white-space: nowrap;
@@ -26,7 +36,7 @@ const RoleHeader = ({ router, _id, name, description, ...props }) => {
 				borderWidth='x2'
 				borderRadius='x2'
 				borderColor='neutral-300'
-				onClick={onClick}
+				onClick={handleEditRole}
 			>
 				<Margins inline='x2'>
 					<span>{description || name}</span>
diff --git a/apps/meteor/client/views/admin/permissions/PermissionsTable/index.ts b/apps/meteor/client/views/admin/permissions/PermissionsTable/index.ts
new file mode 100644
index 00000000000..1dc0ba4dfcc
--- /dev/null
+++ b/apps/meteor/client/views/admin/permissions/PermissionsTable/index.ts
@@ -0,0 +1 @@
+export { default } from './PermissionsTable';
diff --git a/apps/meteor/client/views/admin/permissions/RoleForm.js b/apps/meteor/client/views/admin/permissions/RoleForm.js
deleted file mode 100644
index ba718c5e2cf..00000000000
--- a/apps/meteor/client/views/admin/permissions/RoleForm.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { Box, Field, TextInput, Select, ToggleSwitch } from '@rocket.chat/fuselage';
-import React, { useMemo } from 'react';
-
-import { useTranslation } from '../../../contexts/TranslationContext';
-
-const RoleForm = ({ values, handlers, className, errors, editing = false, isProtected = false }) => {
-	const t = useTranslation();
-
-	const { name, description, scope, mandatory2fa } = values;
-
-	const { handleName, handleDescription, handleScope, handleMandatory2fa } = handlers;
-
-	const options = useMemo(
-		() => [
-			['Users', t('Global')],
-			['Subscriptions', t('Rooms')],
-		],
-		[t],
-	);
-
-	return (
-		<>
-			<Field className={className}>
-				<Field.Label>{t('Role')}</Field.Label>
-				<Field.Row>
-					<TextInput disabled={editing} value={name} onChange={handleName} placeholder={t('Role')} />
-				</Field.Row>
-				<Field.Error>{errors?.name}</Field.Error>
-			</Field>
-			<Field className={className}>
-				<Field.Label>{t('Description')}</Field.Label>
-				<Field.Row>
-					<TextInput value={description} onChange={handleDescription} placeholder={t('Description')} />
-				</Field.Row>
-				<Field.Hint>{'Leave the description field blank if you dont want to show the role'}</Field.Hint>
-			</Field>
-			<Field className={className}>
-				<Field.Label>{t('Scope')}</Field.Label>
-				<Field.Row>
-					<Select disabled={isProtected} options={options} value={scope} onChange={handleScope} placeholder={t('Scope')} />
-				</Field.Row>
-			</Field>
-			<Field className={className}>
-				<Box display='flex' flexDirection='row'>
-					<Field.Label>{t('Users must use Two Factor Authentication')}</Field.Label>
-					<Field.Row>
-						<ToggleSwitch checked={mandatory2fa} onChange={handleMandatory2fa} />
-					</Field.Row>
-				</Box>
-			</Field>
-		</>
-	);
-};
-
-export default RoleForm;
diff --git a/apps/meteor/client/views/admin/permissions/RoleForm.tsx b/apps/meteor/client/views/admin/permissions/RoleForm.tsx
new file mode 100644
index 00000000000..011a84e6c17
--- /dev/null
+++ b/apps/meteor/client/views/admin/permissions/RoleForm.tsx
@@ -0,0 +1,71 @@
+import { Box, Field, TextInput, Select, ToggleSwitch, SelectOption } from '@rocket.chat/fuselage';
+import React, { useMemo, ReactElement } from 'react';
+import { useFormContext, Controller } from 'react-hook-form';
+
+import { useTranslation } from '../../../contexts/TranslationContext';
+
+type RoleFormProps = {
+	className?: string;
+	editing?: boolean;
+	isProtected?: boolean;
+};
+
+const RoleForm = ({ className, editing = false, isProtected = false }: RoleFormProps): ReactElement => {
+	const t = useTranslation();
+	const {
+		register,
+		control,
+		formState: { errors },
+	} = useFormContext();
+
+	const options: SelectOption[] = useMemo(
+		() => [
+			['Users', t('Global')],
+			['Subscriptions', t('Rooms')],
+		],
+		[t],
+	);
+
+	return (
+		<>
+			<Field className={className}>
+				<Field.Label>{t('Role')}</Field.Label>
+				<Field.Row>
+					<TextInput disabled={editing} placeholder={t('Role')} {...register('name', { required: true })} />
+				</Field.Row>
+				{errors?.name && <Field.Error>{t('error-the-field-is-required', { field: t('Role') })}</Field.Error>}
+			</Field>
+			<Field className={className}>
+				<Field.Label>{t('Description')}</Field.Label>
+				<Field.Row>
+					<TextInput placeholder={t('Description')} {...register('description')} />
+				</Field.Row>
+				<Field.Hint>{'Leave the description field blank if you dont want to show the role'}</Field.Hint>
+			</Field>
+			<Field className={className}>
+				<Field.Label>{t('Scope')}</Field.Label>
+				<Field.Row>
+					<Controller
+						name='scope'
+						control={control}
+						render={({ field }): ReactElement => <Select {...field} options={options} disabled={isProtected} placeholder={t('Scope')} />}
+					/>
+				</Field.Row>
+			</Field>
+			<Field className={className}>
+				<Box display='flex' flexDirection='row'>
+					<Field.Label>{t('Users must use Two Factor Authentication')}</Field.Label>
+					<Field.Row>
+						<Controller
+							name='mandatory2fa'
+							control={control}
+							render={({ field }): ReactElement => <ToggleSwitch {...field} checked={field.value} />}
+						/>
+					</Field.Row>
+				</Box>
+			</Field>
+		</>
+	);
+};
+
+export default RoleForm;
diff --git a/apps/meteor/client/views/admin/permissions/UserRow.js b/apps/meteor/client/views/admin/permissions/UserRow.js
deleted file mode 100644
index 7b89a85d37b..00000000000
--- a/apps/meteor/client/views/admin/permissions/UserRow.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { Box, Table, Button, Icon } from '@rocket.chat/fuselage';
-import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import React, { memo } from 'react';
-
-import { getUserEmailAddress } from '../../../../lib/getUserEmailAddress';
-import UserAvatar from '../../../components/avatar/UserAvatar';
-
-const UserRow = ({ _id, username, name, avatarETag, emails, onRemove }) => {
-	const email = getUserEmailAddress({ emails });
-
-	const handleRemove = useMutableCallback(() => {
-		onRemove(username);
-	});
-
-	return (
-		<Table.Row key={_id} tabIndex={0} role='link'>
-			<Table.Cell withTruncatedText>
-				<Box display='flex' alignItems='center'>
-					<UserAvatar size='x40' title={username} username={username} etag={avatarETag} />
-					<Box display='flex' withTruncatedText mi='x8'>
-						<Box display='flex' flexDirection='column' alignSelf='center' withTruncatedText>
-							<Box fontScale='p2m' withTruncatedText color='default'>
-								{name || username}
-							</Box>
-							{name && (
-								<Box fontScale='p2' color='hint' withTruncatedText>
-									{' '}
-									{`@${username}`}{' '}
-								</Box>
-							)}
-						</Box>
-					</Box>
-				</Box>
-			</Table.Cell>
-			<Table.Cell withTruncatedText>{email}</Table.Cell>
-			<Table.Cell withTruncatedText>
-				<Button small square danger onClick={handleRemove}>
-					<Icon name='trash' size='x20' />
-				</Button>
-			</Table.Cell>
-		</Table.Row>
-	);
-};
-
-export default memo(UserRow);
diff --git a/apps/meteor/client/views/admin/permissions/UsersInRolePage.js b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx
similarity index 61%
rename from apps/meteor/client/views/admin/permissions/UsersInRolePage.js
rename to apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx
index 74843b6c0c8..1dc75dd9bdc 100644
--- a/apps/meteor/client/views/admin/permissions/UsersInRolePage.js
+++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx
@@ -1,30 +1,27 @@
+import { IRole } from '@rocket.chat/core-typings';
 import { Box, Field, Margins, ButtonGroup, Button, Callout } from '@rocket.chat/fuselage';
 import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import React, { useState, useRef } from 'react';
+import React, { useState, useRef, ReactElement } from 'react';
 
-import Page from '../../../components/Page';
-import RoomAutoComplete from '../../../components/RoomAutoComplete';
-import UserAutoComplete from '../../../components/UserAutoComplete';
-import { useRoute } from '../../../contexts/RouterContext';
-import { useEndpoint } from '../../../contexts/ServerContext';
-import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
-import { useTranslation } from '../../../contexts/TranslationContext';
-import UsersInRoleTableContainer from './UsersInRoleTableContainer';
+import Page from '../../../../components/Page';
+import RoomAutoComplete from '../../../../components/RoomAutoComplete';
+import UserAutoComplete from '../../../../components/UserAutoComplete';
+import { useRoute } from '../../../../contexts/RouterContext';
+import { useEndpoint } from '../../../../contexts/ServerContext';
+import { useToastMessageDispatch } from '../../../../contexts/ToastMessagesContext';
+import { useTranslation } from '../../../../contexts/TranslationContext';
+import UsersInRoleTable from './UsersInRoleTable';
 
-const UsersInRolePage = ({ data }) => {
+const UsersInRolePage = ({ role }: { role: IRole }): ReactElement => {
 	const t = useTranslation();
+	const reload = useRef<() => void>(() => undefined);
+	const [user, setUser] = useState<string | undefined>('');
+	const [rid, setRid] = useState<string>();
+	const [userError, setUserError] = useState<string>();
 	const dispatchToastMessage = useToastMessageDispatch();
 
-	const reload = useRef();
-
-	const [user, setUser] = useState('');
-	const [rid, setRid] = useState();
-	const [userError, setUserError] = useState();
-
-	const { _id, name, description } = data;
-
+	const { _id, name, description } = role;
 	const router = useRoute('admin-permissions');
-
 	const addUser = useEndpoint('POST', 'roles.addUserToRole');
 
 	const handleReturn = useMutableCallback(() => {
@@ -42,8 +39,8 @@ const UsersInRolePage = ({ data }) => {
 		try {
 			await addUser({ roleId: _id, username: user, roomId: rid });
 			dispatchToastMessage({ type: 'success', message: t('User_added') });
-			setUser();
-			reload.current();
+			setUser(undefined);
+			reload.current?.();
 		} catch (error) {
 			dispatchToastMessage({ type: 'error', message: error });
 		}
@@ -51,7 +48,7 @@ const UsersInRolePage = ({ data }) => {
 
 	const handleUserChange = useMutableCallback((user) => {
 		if (user !== '') {
-			setUserError();
+			setUserError(undefined);
 		}
 
 		return setUser(user);
@@ -67,7 +64,7 @@ const UsersInRolePage = ({ data }) => {
 			<Page.Content>
 				<Box display='flex' flexDirection='column' w='full' mi='neg-x4'>
 					<Margins inline='x4'>
-						{data.scope !== 'Users' && (
+						{role.scope !== 'Users' && (
 							<Field mbe='x4'>
 								<Field.Label>{t('Choose_a_room')}</Field.Label>
 								<Field.Row>
@@ -91,17 +88,10 @@ const UsersInRolePage = ({ data }) => {
 					</Margins>
 				</Box>
 				<Margins blockStart='x8'>
-					{(data.scope === 'Users' || rid) && (
-						<UsersInRoleTableContainer
-							reloadRef={reload}
-							scope={data.scope}
-							rid={rid}
-							roleId={_id}
-							roleName={name}
-							description={description}
-						/>
+					{(role.scope === 'Users' || rid) && (
+						<UsersInRoleTable reloadRef={reload} rid={rid} roleId={_id} roleName={name} description={description} />
 					)}
-					{data.scope !== 'Users' && !rid && <Callout type='info'>{t('Select_a_room')}</Callout>}
+					{role.scope !== 'Users' && !rid && <Callout type='info'>{t('Select_a_room')}</Callout>}
 				</Margins>
 			</Page.Content>
 		</Page>
diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePageWithData.tsx b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePageWithData.tsx
new file mode 100644
index 00000000000..9db04e6d5dc
--- /dev/null
+++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePageWithData.tsx
@@ -0,0 +1,18 @@
+import React, { ReactElement } from 'react';
+
+import { useRouteParameter } from '../../../../contexts/RouterContext';
+import { useRole } from '../hooks/useRole';
+import UsersInRolePage from './UsersInRolePage';
+
+const UsersInRolePageWithData = (): ReactElement | null => {
+	const _id = useRouteParameter('_id');
+	const role = useRole(_id);
+
+	if (!role) {
+		return null;
+	}
+
+	return <UsersInRolePage role={role} />;
+};
+
+export default UsersInRolePageWithData;
diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTable.tsx b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTable.tsx
new file mode 100644
index 00000000000..4936091951f
--- /dev/null
+++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTable.tsx
@@ -0,0 +1,91 @@
+import { IRole, IRoom, IUserInRole } from '@rocket.chat/core-typings';
+import { Tile, Pagination } from '@rocket.chat/fuselage';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import React, { ReactElement } from 'react';
+
+import GenericModal from '../../../../../components/GenericModal';
+import { GenericTable, GenericTableHeader, GenericTableHeaderCell, GenericTableBody } from '../../../../../components/GenericTable';
+import { usePagination } from '../../../../../components/GenericTable/hooks/usePagination';
+import { useSetModal } from '../../../../../contexts/ModalContext';
+import { useEndpoint } from '../../../../../contexts/ServerContext';
+import { useToastMessageDispatch } from '../../../../../contexts/ToastMessagesContext';
+import { useTranslation } from '../../../../../contexts/TranslationContext';
+import UsersInRoleTableRow from './UsersInRoleTableRow';
+
+type UsersInRoleTable = {
+	users: IUserInRole[];
+	reload: () => void;
+	roleName: IRole['name'];
+	roleId: IRole['_id'];
+	description: IRole['description'];
+	total: number;
+	rid?: IRoom['_id'];
+	paginationData: ReturnType<typeof usePagination>;
+};
+
+const UsersInRoleTable = ({ users, reload, roleName, roleId, description, total, rid, paginationData }: UsersInRoleTable): ReactElement => {
+	const t = useTranslation();
+	const setModal = useSetModal();
+	const dispatchToastMessage = useToastMessageDispatch();
+	const removeUser = useEndpoint('POST', 'roles.removeUserFromRole');
+	const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = paginationData;
+
+	const closeModal = (): void => setModal();
+
+	const handleRemove = useMutableCallback((username) => {
+		const remove = async (): Promise<void> => {
+			try {
+				await removeUser({ roleId, username, scope: rid });
+				dispatchToastMessage({ type: 'success', message: t('User_removed') });
+			} catch (error) {
+				dispatchToastMessage({ type: 'error', message: error });
+			} finally {
+				closeModal();
+				reload();
+			}
+		};
+
+		setModal(
+			<GenericModal variant='danger' onConfirm={remove} onClose={closeModal} onCancel={closeModal} confirmText={t('Delete')}>
+				{t('The_user_s_will_be_removed_from_role_s', username, description || roleName)}
+			</GenericModal>,
+		);
+	});
+
+	return (
+		<>
+			{users.length === 0 && (
+				<Tile fontScale='p2' elevation='0' color='info' textAlign='center'>
+					{t('No_data_found')}
+				</Tile>
+			)}
+			{users.length > 0 && (
+				<>
+					<GenericTable>
+						<GenericTableHeader>
+							<GenericTableHeaderCell>{t('Name')}</GenericTableHeaderCell>
+							<GenericTableHeaderCell>{t('Email')}</GenericTableHeaderCell>
+							<GenericTableHeaderCell w='x80'></GenericTableHeaderCell>
+						</GenericTableHeader>
+						<GenericTableBody>
+							{users.map((user) => (
+								<UsersInRoleTableRow onRemove={handleRemove} key={user?._id} user={user} />
+							))}
+						</GenericTableBody>
+					</GenericTable>
+					<Pagination
+						divider
+						current={current}
+						itemsPerPage={itemsPerPage}
+						count={total}
+						onSetItemsPerPage={onSetItemsPerPage}
+						onSetCurrent={onSetCurrent}
+						{...paginationProps}
+					/>
+				</>
+			)}
+		</>
+	);
+};
+
+export default UsersInRoleTable;
diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTableRow.tsx b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTableRow.tsx
new file mode 100644
index 00000000000..553b7aaed17
--- /dev/null
+++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTableRow.tsx
@@ -0,0 +1,52 @@
+import { IUserInRole } from '@rocket.chat/core-typings';
+import { Box, TableRow, TableCell, Button, Icon } from '@rocket.chat/fuselage';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import React, { memo, ReactElement } from 'react';
+
+import { getUserEmailAddress } from '../../../../../../lib/getUserEmailAddress';
+import UserAvatar from '../../../../../components/avatar/UserAvatar';
+
+type UsersInRoleTableRowProps = {
+	user: IUserInRole;
+	onRemove: (username: IUserInRole['username']) => void;
+};
+
+const UsersInRoleTableRow = ({ user, onRemove }: UsersInRoleTableRowProps): ReactElement => {
+	const { _id, name, username, avatarETag } = user;
+	const email = getUserEmailAddress(user);
+
+	const handleRemove = useMutableCallback(() => {
+		onRemove(username);
+	});
+
+	return (
+		<TableRow key={_id} tabIndex={0} role='link'>
+			<TableCell withTruncatedText>
+				<Box display='flex' alignItems='center'>
+					<UserAvatar size='x40' username={username ?? ''} etag={avatarETag} />
+					<Box display='flex' withTruncatedText mi='x8'>
+						<Box display='flex' flexDirection='column' alignSelf='center' withTruncatedText>
+							<Box fontScale='p2m' withTruncatedText color='default'>
+								{name || username}
+							</Box>
+							{name && (
+								<Box fontScale='p2' color='hint' withTruncatedText>
+									{' '}
+									{`@${username}`}{' '}
+								</Box>
+							)}
+						</Box>
+					</Box>
+				</Box>
+			</TableCell>
+			<TableCell withTruncatedText>{email}</TableCell>
+			<TableCell withTruncatedText>
+				<Button small square danger onClick={handleRemove}>
+					<Icon name='trash' size='x20' />
+				</Button>
+			</TableCell>
+		</TableRow>
+	);
+};
+
+export default memo(UsersInRoleTableRow);
diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTableWithData.tsx b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTableWithData.tsx
new file mode 100644
index 00000000000..91dedabe172
--- /dev/null
+++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/UsersInRoleTableWithData.tsx
@@ -0,0 +1,66 @@
+import { IRole, IRoom, IUserInRole } from '@rocket.chat/core-typings';
+import React, { useEffect, useMemo, ReactElement, MutableRefObject } from 'react';
+
+import { usePagination } from '../../../../../components/GenericTable/hooks/usePagination';
+import { useEndpointData } from '../../../../../hooks/useEndpointData';
+import { AsyncStatePhase } from '../../../../../lib/asyncState';
+import UsersInRoleTable from './UsersInRoleTable';
+
+type UsersInRoleTableWithDataProps = {
+	rid?: IRoom['_id'];
+	roleId: IRole['_id'];
+	roleName: IRole['name'];
+	description: IRole['description'];
+	reloadRef: MutableRefObject<() => void>;
+};
+
+const UsersInRoleTableWithData = ({
+	rid,
+	roleId,
+	roleName,
+	description,
+	reloadRef,
+}: UsersInRoleTableWithDataProps): ReactElement | null => {
+	const { itemsPerPage, current, ...paginationData } = usePagination();
+
+	const query = useMemo(
+		() => ({
+			role: roleId,
+			...(rid && { roomId: rid }),
+			...(itemsPerPage && { count: itemsPerPage }),
+			...(current && { offset: current }),
+		}),
+		[itemsPerPage, current, rid, roleId],
+	);
+
+	const { reload, ...result } = useEndpointData('roles.getUsersInRole', query);
+
+	useEffect(() => {
+		reloadRef.current = reload;
+	}, [reload, reloadRef]);
+
+	if (result.phase === AsyncStatePhase.LOADING || result.phase === AsyncStatePhase.REJECTED) {
+		return null;
+	}
+
+	const users: IUserInRole[] = result.value?.users.map((user) => ({
+		...user,
+		createdAt: new Date(user.createdAt),
+		_updatedAt: new Date(user._updatedAt),
+	}));
+
+	return (
+		<UsersInRoleTable
+			users={users}
+			total={result.value.total}
+			reload={reload}
+			roleName={roleName}
+			roleId={roleId}
+			description={description}
+			rid={rid}
+			paginationData={{ itemsPerPage, current, ...paginationData }}
+		/>
+	);
+};
+
+export default UsersInRoleTableWithData;
diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/index.ts b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/index.ts
new file mode 100644
index 00000000000..e7195afe4fd
--- /dev/null
+++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/index.ts
@@ -0,0 +1 @@
+export { default } from './UsersInRoleTableWithData';
diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/index.ts b/apps/meteor/client/views/admin/permissions/UsersInRole/index.ts
new file mode 100644
index 00000000000..70d786d00a9
--- /dev/null
+++ b/apps/meteor/client/views/admin/permissions/UsersInRole/index.ts
@@ -0,0 +1 @@
+export { default } from './UsersInRolePageWithData';
diff --git a/apps/meteor/client/views/admin/permissions/UsersInRolePageContainer.js b/apps/meteor/client/views/admin/permissions/UsersInRolePageContainer.js
deleted file mode 100644
index d921f53ebef..00000000000
--- a/apps/meteor/client/views/admin/permissions/UsersInRolePageContainer.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-
-import { useRouteParameter } from '../../../contexts/RouterContext';
-import UsersInRolePage from './UsersInRolePage';
-import { useRole } from './useRole';
-
-const UsersInRolePageContainer = () => {
-	const _id = useRouteParameter('_id');
-
-	const role = useRole(_id);
-
-	if (!role) {
-		return null;
-	}
-
-	return <UsersInRolePage data={role} />;
-};
-
-export default UsersInRolePageContainer;
diff --git a/apps/meteor/client/views/admin/permissions/UsersInRoleTable.js b/apps/meteor/client/views/admin/permissions/UsersInRoleTable.js
deleted file mode 100644
index 91fc71bab55..00000000000
--- a/apps/meteor/client/views/admin/permissions/UsersInRoleTable.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import React from 'react';
-
-import GenericModal from '../../../components/GenericModal';
-import GenericTable from '../../../components/GenericTable';
-import { useSetModal } from '../../../contexts/ModalContext';
-import { useEndpoint } from '../../../contexts/ServerContext';
-import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
-import { useTranslation } from '../../../contexts/TranslationContext';
-import UserRow from './UserRow';
-
-function UsersInRoleTable({ data, reload, roleName, roleId, description, total, params, setParams, rid }) {
-	const t = useTranslation();
-	const dispatchToastMessage = useToastMessageDispatch();
-
-	const setModal = useSetModal();
-	const closeModal = () => setModal();
-
-	const removeUser = useEndpoint('POST', 'roles.removeUserFromRole');
-
-	const onRemove = useMutableCallback((username) => {
-		const remove = async () => {
-			try {
-				await removeUser({ roleId, username, scope: rid });
-				dispatchToastMessage({ type: 'success', message: t('User_removed') });
-			} catch (error) {
-				dispatchToastMessage({ type: 'error', message: error });
-			}
-			closeModal();
-			reload();
-		};
-
-		setModal(
-			<GenericModal variant='danger' onConfirm={remove} onCancel={closeModal} confirmText={t('Delete')}>
-				{t('The_user_s_will_be_removed_from_role_s', username, description || roleName || roleId)}
-			</GenericModal>,
-		);
-	});
-
-	return (
-		<GenericTable
-			header={
-				<>
-					<GenericTable.HeaderCell>{t('Name')}</GenericTable.HeaderCell>
-					<GenericTable.HeaderCell>{t('Email')}</GenericTable.HeaderCell>
-					<GenericTable.HeaderCell w='x80'></GenericTable.HeaderCell>
-				</>
-			}
-			results={data}
-			params={params}
-			setParams={setParams}
-			total={total}
-		>
-			{(props) => <UserRow onRemove={onRemove} key={props._id} {...props} />}
-		</GenericTable>
-	);
-}
-
-export default UsersInRoleTable;
diff --git a/apps/meteor/client/views/admin/permissions/UsersInRoleTableContainer.js b/apps/meteor/client/views/admin/permissions/UsersInRoleTableContainer.js
deleted file mode 100644
index daea43d37b9..00000000000
--- a/apps/meteor/client/views/admin/permissions/UsersInRoleTableContainer.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
-import React, { useState, useMemo } from 'react';
-
-import { useEndpointData } from '../../../hooks/useEndpointData';
-import UsersInRoleTable from './UsersInRoleTable';
-
-const UsersInRoleTableContainer = ({ rid, roleId, roleName, description, reloadRef }) => {
-	const [params, setParams] = useState({ current: 0, itemsPerPage: 25 });
-
-	const debouncedParams = useDebouncedValue(params, 500);
-
-	const query = useMemo(
-		() => ({
-			roomId: rid,
-			role: roleId,
-			...(debouncedParams.itemsPerPage && { count: debouncedParams.itemsPerPage }),
-			...(debouncedParams.current && { offset: debouncedParams.current }),
-		}),
-		[debouncedParams, rid, roleId],
-	);
-
-	const { value: data = {}, reload } = useEndpointData('roles.getUsersInRole', query);
-
-	reloadRef.current = reload;
-
-	const tableData = data?.users || [];
-
-	return (
-		<UsersInRoleTable
-			data={tableData}
-			total={data?.total}
-			reload={reload}
-			params={params}
-			setParams={setParams}
-			roleName={roleName}
-			roleId={roleId}
-			description={description}
-			rid={rid}
-		/>
-	);
-};
-
-export default UsersInRoleTableContainer;
diff --git a/apps/meteor/client/views/admin/permissions/hooks/useChangeRole.ts b/apps/meteor/client/views/admin/permissions/hooks/useChangeRole.ts
new file mode 100644
index 00000000000..7ae624fd3aa
--- /dev/null
+++ b/apps/meteor/client/views/admin/permissions/hooks/useChangeRole.ts
@@ -0,0 +1,30 @@
+import type { IRole, IPermission } from '@rocket.chat/core-typings';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+
+import { useToastMessageDispatch } from '../../../../contexts/ToastMessagesContext';
+
+export const useChangeRole = ({
+	onGrant,
+	onRemove,
+	permissionId,
+}: {
+	onGrant: (permissionId: IPermission['_id'], roleId: IRole['_id']) => Promise<void>;
+	onRemove: (permissionId: IPermission['_id'], roleId: IRole['_id']) => Promise<void>;
+	permissionId: IPermission['_id'];
+}): ((roleId: IRole['_id'], granted: boolean) => Promise<boolean>) => {
+	const dispatchToastMessage = useToastMessageDispatch();
+
+	return useMutableCallback(async (roleId, granted) => {
+		try {
+			if (granted) {
+				await onRemove(permissionId, roleId);
+			} else {
+				await onGrant(permissionId, roleId);
+			}
+			return !granted;
+		} catch (error) {
+			dispatchToastMessage({ type: 'error', message: error });
+		}
+		return granted;
+	});
+};
diff --git a/apps/meteor/client/views/admin/permissions/hooks/usePermissionsAndRoles.ts b/apps/meteor/client/views/admin/permissions/hooks/usePermissionsAndRoles.ts
index a77bb7bdb35..e021674962f 100644
--- a/apps/meteor/client/views/admin/permissions/hooks/usePermissionsAndRoles.ts
+++ b/apps/meteor/client/views/admin/permissions/hooks/usePermissionsAndRoles.ts
@@ -1,3 +1,4 @@
+import type { IRole, IPermission } from '@rocket.chat/core-typings';
 import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
 import { useCallback } from 'react';
 
@@ -6,7 +7,12 @@ import { CONSTANTS } from '../../../../../app/authorization/lib';
 import { Roles } from '../../../../../app/models/client';
 import { useReactiveValue } from '../../../../hooks/useReactiveValue';
 
-export const usePermissionsAndRoles = (type = 'permissions', filter = '', limit = 25, skip = 0): Array<any> => {
+export const usePermissionsAndRoles = (
+	type = 'permissions',
+	filter = '',
+	limit = 25,
+	skip = 0,
+): { permissions: IPermission[]; total: number; roleList: IRole[]; reload: () => void } => {
 	const getPermissions = useCallback(() => {
 		const filterRegExp = new RegExp(filter, 'i');
 
@@ -25,12 +31,9 @@ export const usePermissionsAndRoles = (type = 'permissions', filter = '', limit
 		);
 	}, [filter, limit, skip, type]);
 
-	const getRoles = useMutableCallback(() => Roles.find().fetch());
-
 	const permissions = useReactiveValue(getPermissions);
+	const getRoles = useMutableCallback(() => Roles.find().fetch());
 	const roles = useReactiveValue(getRoles);
 
-	const reloadRoles = getRoles();
-
-	return [permissions.fetch(), permissions.count(false), roles, reloadRoles];
+	return { permissions: permissions.fetch(), total: permissions.count(false), roleList: roles, reload: getRoles };
 };
diff --git a/apps/meteor/client/views/admin/permissions/hooks/useRole.ts b/apps/meteor/client/views/admin/permissions/hooks/useRole.ts
new file mode 100644
index 00000000000..1b96c4ed43f
--- /dev/null
+++ b/apps/meteor/client/views/admin/permissions/hooks/useRole.ts
@@ -0,0 +1,7 @@
+import type { IRole } from '@rocket.chat/core-typings';
+import { useCallback } from 'react';
+
+import { Roles } from '../../../../../app/models/client';
+import { useReactiveValue } from '../../../../hooks/useReactiveValue';
+
+export const useRole = (_id?: IRole['_id']): IRole => useReactiveValue(useCallback(() => Roles.findOne({ _id }), [_id]));
diff --git a/apps/meteor/client/views/admin/permissions/useRole.js b/apps/meteor/client/views/admin/permissions/useRole.js
deleted file mode 100644
index 539b45b06d1..00000000000
--- a/apps/meteor/client/views/admin/permissions/useRole.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import { useCallback } from 'react';
-
-import { Roles } from '../../../../app/models/client';
-import { useReactiveValue } from '../../../hooks/useReactiveValue';
-
-export const useRole = (_id) => useReactiveValue(useCallback(() => Roles.findOne({ _id }), [_id]));
diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts
index bfde7eb9bb0..3cd4cdf08c5 100644
--- a/packages/core-typings/src/IUser.ts
+++ b/packages/core-typings/src/IUser.ts
@@ -171,3 +171,8 @@ export type IUserDataEvent = {
       unset: Record<keyof IUser, boolean | 0 | 1>;
     }
 );
+
+export type IUserInRole = Pick<
+	IUser,
+	'_id' | 'name' | 'username' | 'emails' | 'avatarETag' | 'createdAt' | 'roles' | 'type' | 'active' | '_updatedAt'
+>;
diff --git a/packages/rest-typings/src/v1/roles.ts b/packages/rest-typings/src/v1/roles.ts
index 307cff1d830..b6709c996a1 100644
--- a/packages/rest-typings/src/v1/roles.ts
+++ b/packages/rest-typings/src/v1/roles.ts
@@ -1,7 +1,7 @@
 import Ajv, { JSONSchemaType } from "ajv";
 
 import type { RocketChatRecordDeleted } from "@rocket.chat/core-typings";
-import type { IRole, IUser } from "@rocket.chat/core-typings";
+import type { IRole, IUser, IUserInRole } from "@rocket.chat/core-typings";
 
 const ajv = new Ajv();
 
@@ -191,12 +191,12 @@ export type RolesEndpoints = {
 
   "roles.getUsersInRole": {
     GET: (params: {
-      roomId: string;
+      roomId?: string;
       role: string;
-      offset: number;
-      count: number;
+      offset?: number;
+      count?: number;
     }) => {
-      users: IUser[];
+      users: IUserInRole[];
       total: number;
     };
   };
-- 
GitLab