Skip to content

feat: Add update user roles action #1361

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
May 10, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Load roles and show them in the table
  • Loading branch information
BrunoQuaresma committed May 6, 2022
commit 32cba8367ce1d6cce65edfe68c7ca3dd30f3fa3e
5 changes: 5 additions & 0 deletions site/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,8 @@ export const suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen

export const updateUserPassword = async (password: string, userId: TypesGen.User["id"]): Promise<undefined> =>
axios.put(`/api/v2/users/${userId}/password`, { password })

export const getOrganizationRoles = async (organizationId: string): Promise<Array<string>> => {
const response = await axios.get<Array<string>>(`/api/v2/organizations/${organizationId}/members/roles`)
return response.data
}
10 changes: 7 additions & 3 deletions site/src/components/TableHeaders/TableHeaders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,22 @@ export interface TableHeadersProps {
hasMenu?: boolean
}

export const TableHeaders: React.FC<TableHeadersProps> = ({ columns, hasMenu }) => {
export const TableHeaderRow: React.FC = ({ children }) => {
const styles = useStyles()
return <TableRow className={styles.root}>{children}</TableRow>
}

export const TableHeaders: React.FC<TableHeadersProps> = ({ columns, hasMenu }) => {
return (
<TableRow className={styles.root}>
<TableHeaderRow>
{columns.map((c, idx) => (
<TableCell key={idx} size="small">
{c}
</TableCell>
))}
{/* 1% is a trick to make the table cell width fit the content */}
{hasMenu && <TableCell width="1%" />}
</TableRow>
</TableHeaderRow>
)
}

Expand Down
92 changes: 57 additions & 35 deletions site/src/components/UsersTable/UsersTable.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import Box from "@material-ui/core/Box"
import Table from "@material-ui/core/Table"
import TableBody from "@material-ui/core/TableBody"
import TableCell from "@material-ui/core/TableCell"
import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow"
import React from "react"
import { UserResponse } from "../../api/types"
import { EmptyState } from "../EmptyState/EmptyState"
import { Column, Table } from "../Table/Table"
import { TableHeaderRow } from "../TableHeaders/TableHeaders"
import { TableRowMenu } from "../TableRowMenu/TableRowMenu"
import { TableTitle } from "../TableTitle/TableTitle"
import { UserCell } from "../UserCell/UserCell"

export const Language = {
Expand All @@ -12,48 +19,63 @@ export const Language = {
usernameLabel: "User",
suspendMenuItem: "Suspend",
resetPasswordMenuItem: "Reset password",
rolesLabel: "Roles",
}

const emptyState = <EmptyState message={Language.emptyMessage} />

const columns: Column<UserResponse>[] = [
{
key: "username",
name: Language.usernameLabel,
renderer: (field, data) => {
return <UserCell Avatar={{ username: data.username }} primaryText={data.username} caption={data.email} />
},
},
]

export interface UsersTableProps {
users: UserResponse[]
onSuspendUser: (user: UserResponse) => void
onResetUserPassword: (user: UserResponse) => void
roles: string[]
}

export const UsersTable: React.FC<UsersTableProps> = ({ users, onSuspendUser, onResetUserPassword }) => {
export const UsersTable: React.FC<UsersTableProps> = ({ users, roles, onSuspendUser, onResetUserPassword }) => {
return (
<Table
columns={columns}
data={users}
title={Language.usersTitle}
emptyState={emptyState}
rowMenu={(user) => (
<TableRowMenu
data={user}
menuItems={[
{
label: Language.suspendMenuItem,
onClick: onSuspendUser,
},
{
label: Language.resetPasswordMenuItem,
onClick: onResetUserPassword,
},
]}
/>
)}
/>
<Table>
<TableHead>
<TableTitle title={Language.usersTitle} />
<TableHeaderRow>
<TableCell size="small">{Language.usernameLabel}</TableCell>
<TableCell size="small">{Language.rolesLabel}</TableCell>
{/* 1% is a trick to make the table cell width fit the content */}
<TableCell size="small" width="1%" />
</TableHeaderRow>
</TableHead>
<TableBody>
{users.map((u) => (
<TableRow key={u.id}>
<TableCell>
<UserCell Avatar={{ username: u.username }} primaryText={u.username} caption={u.email} />{" "}
</TableCell>
<TableCell>{roles}</TableCell>
<TableCell>
<TableRowMenu
data={u}
menuItems={[
{
label: Language.suspendMenuItem,
onClick: onSuspendUser,
},
{
label: Language.resetPasswordMenuItem,
onClick: onResetUserPassword,
},
]}
/>
</TableCell>
</TableRow>
))}

{users.length === 0 && (
<TableRow>
<TableCell colSpan={999}>
<Box p={4}>
<EmptyState message={Language.emptyMessage} />
</Box>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)
}
27 changes: 26 additions & 1 deletion site/src/pages/UsersPage/UsersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,37 @@ export const Language = {
suspendDialogMessagePrefix: "Do you want to suspend the user",
}

const useRoles = () => {
const xServices = useContext(XServiceContext)
const [authState] = useActor(xServices.authXService)
const [rolesState, rolesSend] = useActor(xServices.rolesXService)
const { roles } = rolesState.context
const { me } = authState.context

useEffect(() => {
if (!me) {
throw new Error("User is not logged in")
}

const organizationId = me.organization_ids[0]

rolesSend({
type: "GET_ROLES",
organizationId,
})
}, [me, rolesSend])

return roles
}

export const UsersPage: React.FC = () => {
const xServices = useContext(XServiceContext)
const [usersState, usersSend] = useActor(xServices.usersXService)
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()

/**
* Fetch users on component mount
Expand All @@ -28,12 +52,13 @@ export const UsersPage: React.FC = () => {
usersSend("GET_USERS")
}, [usersSend])

if (!users) {
if (!users || !roles) {
return <FullScreenLoader />
} else {
return (
<>
<UsersPageView
roles={roles}
users={users}
openUserCreationDialog={() => {
navigate("/users/create")
Expand Down
9 changes: 8 additions & 1 deletion site/src/pages/UsersPage/UsersPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ export interface UsersPageViewProps {
openUserCreationDialog: () => void
onSuspendUser: (user: UserResponse) => void
onResetUserPassword: (user: UserResponse) => void
roles: string[]
error?: unknown
}

export const UsersPageView: React.FC<UsersPageViewProps> = ({
users,
roles,
openUserCreationDialog,
onSuspendUser,
onResetUserPassword,
Expand All @@ -33,7 +35,12 @@ export const UsersPageView: React.FC<UsersPageViewProps> = ({
{error ? (
<ErrorSummary error={error} />
) : (
<UsersTable users={users} onSuspendUser={onSuspendUser} onResetUserPassword={onResetUserPassword} />
<UsersTable
users={users}
onSuspendUser={onSuspendUser}
onResetUserPassword={onResetUserPassword}
roles={roles}
/>
)}
</Margins>
</Stack>
Expand Down
3 changes: 3 additions & 0 deletions site/src/xServices/StateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useNavigate } from "react-router"
import { ActorRefFrom } from "xstate"
import { authMachine } from "./auth/authXService"
import { buildInfoMachine } from "./buildInfo/buildInfoXService"
import { rolesMachine } from "./roles/rolesXService"
import { usersMachine } from "./users/usersXService"
import { workspaceMachine } from "./workspace/workspaceXService"

Expand All @@ -12,6 +13,7 @@ interface XServiceContextType {
buildInfoXService: ActorRefFrom<typeof buildInfoMachine>
usersXService: ActorRefFrom<typeof usersMachine>
workspaceXService: ActorRefFrom<typeof workspaceMachine>
rolesXService: ActorRefFrom<typeof rolesMachine>
}

/**
Expand All @@ -37,6 +39,7 @@ export const XServiceProvider: React.FC = ({ children }) => {
buildInfoXService: useInterpret(buildInfoMachine),
usersXService: useInterpret(() => usersMachine.withConfig({ actions: { redirectToUsersPage } })),
workspaceXService: useInterpret(workspaceMachine),
rolesXService: useInterpret(rolesMachine),
}}
>
{children}
Expand Down
82 changes: 82 additions & 0 deletions site/src/xServices/roles/rolesXService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { assign, createMachine } from "xstate"
import * as API from "../../api"
import { displayError } from "../../components/GlobalSnackbar/utils"

type RolesContext = {
roles?: string[]
getRolesError: Error | unknown
organizationId?: string
}

type RolesEvent = {
type: "GET_ROLES"
organizationId: string
}

export const rolesMachine = createMachine(
{
id: "rolesState",
initial: "idle",
schema: {
context: {} as RolesContext,
events: {} as RolesEvent,
services: {
getRoles: {
data: {} as string[],
},
},
},
tsTypes: {} as import("./rolesXService.typegen").Typegen0,
states: {
idle: {
on: {
GET_ROLES: {
target: "gettingRoles",
actions: ["assignOrganizationId"],
},
},
},
gettingRoles: {
invoke: {
id: "getRoles",
src: "getRoles",
onDone: {
target: "idle",
actions: ["assignRoles"],
},
onError: {
target: "idle",
actions: ["assignGetRolesError", "displayGetRolesError"],
},
},
},
},
},
{
actions: {
assignRoles: assign({
roles: (_, event) => event.data,
}),
assignGetRolesError: assign({
getRolesError: (_, event) => event.data,
}),
assignOrganizationId: assign({
organizationId: (_, event) => event.organizationId,
}),
displayGetRolesError: () => {
displayError("Error on get the roles.")
},
},
services: {
getRoles: (ctx) => {
const { organizationId } = ctx

if (!organizationId) {
throw new Error("organizationId not defined")
}

return API.getOrganizationRoles(organizationId)
},
},
},
)