fix: User permissions on UI (#1570)

This commit is contained in:
Bruno Quaresma
2022-05-19 15:10:18 +00:00
committed by GitHub
parent 4eb0bb6afd
commit 3f770e1111
7 changed files with 88 additions and 60 deletions
+4 -4
View File
@@ -11,10 +11,10 @@ beforeEach(() => {
})
describe("Navbar", () => {
describe("when user has permission to read all users", () => {
describe("when user has permission to update users", () => {
it("displays the admin menu", async () => {
const checkUserPermissionsSpy = jest.spyOn(API, "checkUserPermissions").mockResolvedValueOnce({
[checks.readAllUsers]: true,
[checks.updateUsers]: true,
})
renderWithAuth(<Navbar />)
@@ -25,10 +25,10 @@ describe("Navbar", () => {
})
})
describe("when user has NO permission to read all users", () => {
describe("when user has NO permission to update users", () => {
it("does not display the admin menu", async () => {
const checkUserPermissionsSpy = jest.spyOn(API, "checkUserPermissions").mockResolvedValueOnce({
[checks.readAllUsers]: false,
[checks.updateUsers]: false,
})
renderWithAuth(<Navbar />)
+1 -1
View File
@@ -11,7 +11,7 @@ export const Navbar: React.FC = () => {
const permissions = useSelector(xServices.authXService, selectPermissions)
// When we have more options in the admin dropdown we may want to check this
// for more permissions
const displayAdminDropdown = !!permissions?.readAllUsers
const displayAdminDropdown = !!permissions?.updateUsers
const onSignOut = () => authSend("SIGN_OUT")
return <NavbarView user={me} onSignOut={onSignOut} displayAdminDropdown={displayAdminDropdown} />
@@ -14,6 +14,14 @@ export const Example = Template.bind({})
Example.args = {
users: [MockUser, MockUser2],
roles: MockSiteRoles,
canEditUsers: false,
}
export const Editable = Template.bind({})
Editable.args = {
users: [MockUser, MockUser2],
roles: MockSiteRoles,
canEditUsers: true,
}
export const Empty = Template.bind({})
+38 -33
View File
@@ -8,10 +8,8 @@ import React from "react"
import * as TypesGen from "../../api/typesGenerated"
import { EmptyState } from "../EmptyState/EmptyState"
import { RoleSelect } from "../RoleSelect/RoleSelect"
import { TableHeaderRow } from "../TableHeaders/TableHeaders"
import { TableLoader } from "../TableLoader/TableLoader"
import { TableRowMenu } from "../TableRowMenu/TableRowMenu"
import { TableTitle } from "../TableTitle/TableTitle"
import { UserCell } from "../UserCell/UserCell"
export const Language = {
@@ -28,6 +26,8 @@ export interface UsersTableProps {
users?: TypesGen.User[]
roles?: TypesGen.Role[]
isUpdatingUserRoles?: boolean
canEditUsers?: boolean
isLoading?: boolean
onSuspendUser: (user: TypesGen.User) => void
onResetUserPassword: (user: TypesGen.User) => void
onUpdateUserRoles: (user: TypesGen.User, roles: TypesGen.Role["name"][]) => void
@@ -40,52 +40,57 @@ export const UsersTable: React.FC<UsersTableProps> = ({
onResetUserPassword,
onUpdateUserRoles,
isUpdatingUserRoles,
canEditUsers,
isLoading,
}) => {
const isLoading = !users || !roles
return (
<Table>
<TableHead>
<TableTitle title={Language.usersTitle} />
<TableHeaderRow>
<TableCell size="small">{Language.usernameLabel}</TableCell>
<TableCell size="small">{Language.rolesLabel}</TableCell>
<TableRow>
<TableCell>{Language.usernameLabel}</TableCell>
<TableCell>{Language.rolesLabel}</TableCell>
{/* 1% is a trick to make the table cell width fit the content */}
<TableCell size="small" width="1%" />
</TableHeaderRow>
{canEditUsers && <TableCell width="1%" />}
</TableRow>
</TableHead>
<TableBody>
{isLoading && <TableLoader />}
{users &&
roles &&
{!isLoading &&
users &&
users.map((u) => (
<TableRow key={u.id}>
<TableCell>
<UserCell Avatar={{ username: u.username }} primaryText={u.username} caption={u.email} />{" "}
</TableCell>
<TableCell>
<RoleSelect
roles={roles}
selectedRoles={u.roles}
loading={isUpdatingUserRoles}
onChange={(roles) => onUpdateUserRoles(u, roles)}
/>
</TableCell>
<TableCell>
<TableRowMenu
data={u}
menuItems={[
{
label: Language.suspendMenuItem,
onClick: onSuspendUser,
},
{
label: Language.resetPasswordMenuItem,
onClick: onResetUserPassword,
},
]}
/>
{canEditUsers ? (
<RoleSelect
roles={roles ?? []}
selectedRoles={u.roles}
loading={isUpdatingUserRoles}
onChange={(roles) => onUpdateUserRoles(u, roles)}
/>
) : (
<>{u.roles.map((r) => r.display_name).join(", ")}</>
)}
</TableCell>
{canEditUsers && (
<TableCell>
<TableRowMenu
data={u}
menuItems={[
{
label: Language.suspendMenuItem,
onClick: onSuspendUser,
},
{
label: Language.resetPasswordMenuItem,
onClick: onResetUserPassword,
},
]}
/>
</TableCell>
)}
</TableRow>
))}
+24 -22
View File
@@ -1,8 +1,9 @@
import { useActor } from "@xstate/react"
import { useActor, useSelector } from "@xstate/react"
import React, { useContext, useEffect } from "react"
import { useNavigate } from "react-router"
import { ConfirmDialog } from "../../components/ConfirmDialog/ConfirmDialog"
import { ResetPasswordDialog } from "../../components/ResetPasswordDialog/ResetPasswordDialog"
import { selectPermissions } from "../../xServices/auth/authSelectors"
import { XServiceContext } from "../../xServices/StateContext"
import { UsersPageView } from "./UsersPageView"
@@ -12,39 +13,38 @@ export const Language = {
suspendDialogMessagePrefix: "Do you want to suspend the user",
}
const useRoles = () => {
const xServices = useContext(XServiceContext)
const [rolesState, rolesSend] = useActor(xServices.siteRolesXService)
const { roles } = rolesState.context
/**
* Fetch roles on component mount
*/
useEffect(() => {
rolesSend({
type: "GET_ROLES",
})
}, [rolesSend])
return roles
}
export const UsersPage: React.FC = () => {
const xServices = useContext(XServiceContext)
const [usersState, usersSend] = useActor(xServices.usersXService)
const [rolesState, rolesSend] = useActor(xServices.siteRolesXService)
const { users, getUsersError, userIdToSuspend, userIdToResetPassword, newUserPassword } = usersState.context
const navigate = useNavigate()
const userToBeSuspended = users?.find((u) => u.id === userIdToSuspend)
const userToResetPassword = users?.find((u) => u.id === userIdToResetPassword)
const roles = useRoles()
const permissions = useSelector(xServices.authXService, selectPermissions)
const canEditUsers = permissions && permissions.updateUsers
const { roles } = rolesState.context
// Is loading if
// - permissions are not loaded or
// - users are not loaded or
// - the user can edit the users but the roles are not loaded yet
const isLoading = !permissions || !users || (canEditUsers && !roles)
/**
* Fetch users on component mount
*/
// Fetch users on component mount
useEffect(() => {
usersSend("GET_USERS")
}, [usersSend])
// Fetch roles on component mount
useEffect(() => {
// Only fetch the roles if the user has permission for it
if (canEditUsers) {
rolesSend({
type: "GET_ROLES",
})
}
}, [canEditUsers, rolesSend])
return (
<>
<UsersPageView
@@ -68,6 +68,8 @@ export const UsersPage: React.FC = () => {
}}
error={getUsersError}
isUpdatingUserRoles={usersState.matches("updatingUserRoles")}
isLoading={isLoading}
canEditUsers={canEditUsers}
/>
<ConfirmDialog
@@ -16,6 +16,8 @@ export interface UsersPageViewProps {
roles?: TypesGen.Role[]
error?: unknown
isUpdatingUserRoles?: boolean
canEditUsers?: boolean
isLoading?: boolean
openUserCreationDialog: () => void
onSuspendUser: (user: TypesGen.User) => void
onResetUserPassword: (user: TypesGen.User) => void
@@ -31,6 +33,8 @@ export const UsersPageView: React.FC<UsersPageViewProps> = ({
onUpdateUserRoles,
error,
isUpdatingUserRoles,
canEditUsers,
isLoading,
}) => {
return (
<Stack spacing={4}>
@@ -46,6 +50,8 @@ export const UsersPageView: React.FC<UsersPageViewProps> = ({
onResetUserPassword={onResetUserPassword}
onUpdateUserRoles={onUpdateUserRoles}
isUpdatingUserRoles={isUpdatingUserRoles}
canEditUsers={canEditUsers}
isLoading={isLoading}
/>
)}
</Margins>
+7
View File
@@ -11,6 +11,7 @@ export const Language = {
export const checks = {
readAllUsers: "readAllUsers",
updateUsers: "updateUsers",
createTemplates: "createTemplates",
} as const
@@ -21,6 +22,12 @@ export const permissionsToCheck = {
},
action: "read",
},
[checks.updateUsers]: {
object: {
resource_type: "user",
},
action: "update",
},
[checks.createTemplates]: {
object: {
resource_type: "template",