feat: add count endpoint for users, enabling better pagination (#4848)

* Start on backend

* Hook up frontend

* Add to frontend test

* Add go test, wip

* Fix some test bugs

* Fix test

* Format

* Add to authorize.go

* copy user array into local variable

* Authorize route

* Log count error

* Authorize better

* Tweaks to authorization

* More authorization tweaks

* Make gen

* Fix test

Co-authored-by: Garrett <garrett@coder.com>
This commit is contained in:
Presley Pizzo
2022-11-08 10:58:44 -05:00
committed by GitHub
co-authored by Garrett
parent a4fbc74751
commit f496b149df
19 changed files with 620 additions and 216 deletions
+8
View File
@@ -139,6 +139,14 @@ export const getUsers = async (
return response.data
}
export const getUserCount = async (
options: TypesGen.UserCountRequest,
): Promise<TypesGen.UserCountResponse> => {
const url = getURLWithSearchParams("/api/v2/users/count", options)
const response = await axios.get(url.toString())
return response.data
}
export const getOrganization = async (
organizationId: string,
): Promise<TypesGen.Organization> => {
+10
View File
@@ -746,6 +746,16 @@ export interface User {
readonly avatar_url: string
}
// From codersdk/users.go
export interface UserCountRequest {
readonly q?: string
}
// From codersdk/users.go
export interface UserCountResponse {
readonly count: number
}
// From codersdk/users.go
export interface UserRoles {
readonly roles: string[]
+10 -2
View File
@@ -240,7 +240,7 @@ describe("UsersPage", () => {
describe("pagination", () => {
it("goes to next and previous page", async () => {
renderPage()
const { container } = renderPage()
const user = userEvent.setup()
const mock = jest
@@ -248,6 +248,9 @@ describe("UsersPage", () => {
.mockResolvedValueOnce([MockUser, MockUser2])
const nextButton = await screen.findByLabelText("Next page")
expect(nextButton).toBeEnabled()
const previousButton = await screen.findByLabelText("Previous page")
expect(previousButton).toBeDisabled()
await user.click(nextButton)
await waitFor(() =>
@@ -255,12 +258,17 @@ describe("UsersPage", () => {
)
mock.mockClear()
const previousButton = await screen.findByLabelText("Previous page")
await user.click(previousButton)
await waitFor(() =>
expect(API.getUsers).toBeCalledWith({ offset: 0, limit: 25, q: "" }),
)
const pageButtons = await container.querySelectorAll(
`button[name="Page button"]`,
)
// count handler says there are 2 pages of results
expect(pageButtons.length).toBe(2)
})
})
+22 -14
View File
@@ -1,4 +1,5 @@
import { useActor, useMachine } from "@xstate/react"
import { getErrorDetail } from "api/errors"
import { User } from "api/typesGenerated"
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"
import { getPaginationContext } from "components/PaginationWidget/utils"
@@ -44,12 +45,14 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
const {
users,
getUsersError,
getCountError,
usernameToDelete,
usernameToSuspend,
usernameToActivate,
userIdToResetPassword,
newUserPassword,
paginationRef,
count,
} = usersState.context
const { updateUsers: canEditUsers } = usePermissions()
@@ -60,7 +63,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
// - users are loading or
// - the user can edit the users but the roles are loading
const isLoading =
usersState.matches("gettingUsers") ||
usersState.matches("users.gettingUsers") ||
(canEditUsers && rolesState.matches("gettingRoles"))
// Fetch roles on component mount
@@ -73,6 +76,10 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
}
}, [canEditUsers, rolesSend])
if (getCountError) {
console.error(getErrorDetail(getCountError))
}
return (
<>
<Helmet>
@@ -81,6 +88,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
<UsersPageView
roles={roles}
users={users}
count={count}
onListWorkspaces={(user) => {
navigate(
"/workspaces?filter=" +
@@ -119,7 +127,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
})
}}
error={getUsersError}
isUpdatingUserRoles={usersState.matches("updatingUserRoles")}
isUpdatingUserRoles={usersState.matches("users.updatingUserRoles")}
isLoading={isLoading}
canEditUsers={canEditUsers}
filter={usersState.context.filter}
@@ -131,10 +139,10 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
<DeleteDialog
isOpen={
usersState.matches("confirmUserDeletion") ||
usersState.matches("deletingUser")
usersState.matches("users.confirmUserDeletion") ||
usersState.matches("users.deletingUser")
}
confirmLoading={usersState.matches("deletingUser")}
confirmLoading={usersState.matches("users.deletingUser")}
name={usernameToDelete ?? ""}
entity="user"
onConfirm={() => {
@@ -149,10 +157,10 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
type="delete"
hideCancel={false}
open={
usersState.matches("confirmUserSuspension") ||
usersState.matches("suspendingUser")
usersState.matches("users.confirmUserSuspension") ||
usersState.matches("users.suspendingUser")
}
confirmLoading={usersState.matches("suspendingUser")}
confirmLoading={usersState.matches("users.suspendingUser")}
title={Language.suspendDialogTitle}
confirmText={Language.suspendDialogAction}
onConfirm={() => {
@@ -174,10 +182,10 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
type="success"
hideCancel={false}
open={
usersState.matches("confirmUserActivation") ||
usersState.matches("activatingUser")
usersState.matches("users.confirmUserActivation") ||
usersState.matches("users.activatingUser")
}
confirmLoading={usersState.matches("activatingUser")}
confirmLoading={usersState.matches("users.activatingUser")}
title={Language.activateDialogTitle}
confirmText={Language.activateDialogAction}
onConfirm={() => {
@@ -198,10 +206,10 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
{userIdToResetPassword && (
<ResetPasswordDialog
open={
usersState.matches("confirmUserPasswordReset") ||
usersState.matches("resettingUserPassword")
usersState.matches("users.confirmUserPasswordReset") ||
usersState.matches("users.resettingUserPassword")
}
loading={usersState.matches("resettingUserPassword")}
loading={usersState.matches("users.resettingUserPassword")}
user={getSelectedUser(userIdToResetPassword, users)}
newPassword={newUserPassword}
onClose={() => {
+3 -1
View File
@@ -12,6 +12,7 @@ export const Language = {
}
export interface UsersPageViewProps {
users?: TypesGen.User[]
count?: number
roles?: TypesGen.AssignableRoles[]
filter?: string
error?: unknown
@@ -33,6 +34,7 @@ export interface UsersPageViewProps {
export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
users,
count,
roles,
onSuspendUser,
onDeleteUser,
@@ -76,7 +78,7 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
isLoading={isLoading}
/>
<PaginationWidget paginationRef={paginationRef} />
<PaginationWidget numRecords={count} paginationRef={paginationRef} />
</>
)
}
+4
View File
@@ -82,6 +82,10 @@ export const MockUser: TypesGen.User = {
last_seen_at: "",
}
export const MockUserCountResponse: TypesGen.UserCountResponse = {
count: 26,
}
export const MockUserAdmin: TypesGen.User = {
id: "test-user",
username: "TestUser",
+2 -2
View File
@@ -74,8 +74,8 @@ export const handlers = [
ctx.json([M.MockUser, M.MockUser2, M.SuspendedMockUser]),
)
}),
rest.post("/api/v2/users", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockUser))
rest.get("/api/v2/users/count", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockUserCountResponse))
}),
rest.get("/api/v2/users/me/organizations", (req, res, ctx) => {
return res(ctx.status(200), ctx.json([M.MockOrganization]))
+247 -194
View File
@@ -57,6 +57,8 @@ export interface UsersContext {
updateUserRolesError?: Error | unknown
paginationContext: PaginationContext
paginationRef: PaginationMachineRef
count: number
getCountError: Error | unknown
}
export type UsersEvent =
@@ -101,7 +103,7 @@ export type UsersEvent =
| { type: "UPDATE_PAGE"; page: string }
export const usersMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QFdZgE6wMoBcCGOYAdLPujgJYB2UACnlNQRQPZUDEA2gAwC6ioAA4tYFSmwEgAHogCMADiLcA7AFYVAFgCcW5bNkaAbACZDAGhABPOVsNEAzN1XGDhw92PduW+wF9fFqgY2PiERDA4lDQAqmiY7BBsxNQAbiwA1sQRscE8-EggwqLiVJIyCPKyRHryyk4m9lrGzfYW1ggaGqpERvaq9soasqrKfgEgQZi4BFlgkdRQOfEY6CzoRIIANgQAZmsAtuFzS7B5kkVirKUF5QC0ssrGRLV1rs2VyrptiJ3K1cbyHQaUyqSoDVT+QJxEIzIgUCCbMDsLDRLC0ACiADkACIAfVR6IASmcChcSmVEMZOkRDPpgYZ5J4NNx5PZWlY5A8qvZjH15Nx7IZVFp5KoNJCJtDpmF4Yj2Nj0QAZdEAFXR+KwRJJQhElwkN0phi0RAMqncjIZo0Z3wqtSIOgZGg+3HNsglkxhMoRSIAggBhFUASQAaj61RqtXxzrryQaEKZ7Pb7KLBQCTLJ3Kobe5E2adINjE1DE7jO6paFkt72IT0ZqVRHCbjaD6sFgAOoAeUJ2O1hRjVwp8dkxq6A2UFuTyhMWY5CFcdlkhbB8n5vNBZeC0srcuitGxYfVBMbhI7yqwvbJA7jtwBfwF3lG-WHPhtjO4NIZ7lk9Q0fI3UwrOEq13fdw2bABxdEL37fVQDuHk7BUTQnEcAEdGzLoaQtdQ+h8Ywp3-T1tyRECD1xAAxQNFTVYko1JGDrjgxB7m6LQjABfCtBUYduFkV9h2qAZv3w5wuIhcYPS3IgAGM2B2Ch0H2JYsFQQQwCoUQ2HYP0O0xSjCQAWQbXEUTRLEsEDXToOKK8mNtRMCyFXpPjcLQbX0FcTU+EtnFwhlCKk2SqHkxTlNU9TNI4P0fUxP0lWM0yMUxCyrLonUbNg6REFBbpmU+LiHipbhOnc-D3xXUURVpWlioCwCgpCpS4mxMBERKbTdP0oyj1xBVlTVay9UYrKKh8e1vHkbRgRUBobS0fQelFRc9F-Nj5EMOrYQahSmowFq2qubSYrixVjL61UoLSvsMuG8paSeYZ7AzNlDHHN65rcGliv5AVPjNExNrCbbQriH1pMoFJmC0nS9MDQzjP9INQyDVL8nSobBxeD8BnsTops6NzZ0MXHFrZKk2NUfQNok8strknaljBiGoai474p6xGQzDSzMUG2M7OFVjuMXQVeNUSmbTqRNlt-NwnuUR5xRpzdANgcKqAgBYlgSJI4SoNJMhIdWICWPnbJG4ZFxNcX+XHbxhzUOa6i8o1vBFdRaTdZWANhNXYDUjWtbidgVjWDZthwPZFKN-31JNuIzcy8oPPfEYXEm3R8NGPjZ1kRwNCUYU8-w2Xv3EqEVdhCBWrmIOMB1qhkn1jJiGrtqwFNq7LyTuRKeNAxmS0UENEeXjJZGapmRGEfVrTwHW5rqJFmD0P1i2XYDiINu5g7hOu4Ywd9CH6oWVFV4uItdzeONMU6TZWpFxH+eiDwcGKEhpftcSRu9YN4hX+ZoQTuaNroYzjJbRMPgeQujUOob85hZxmlTrSEYLpTAKDNM-AB79mAxBXugVYa8I5R0ONgj+u8MCJ1upyWwk9vwsmTMVAw48C7aAZFyMUU59DP2BrtdA9BYCwAAO5rAgISOAcwOqw3hj1ZsrZOzdlxDWOsVDMZimqPAgwxhKbixFO5HCX1ibaABJTV6ygeH0xBhgARwjRHiLQDgI6sV2aakbHI9sXY8TKNVKouMad7T9FsMTI0Csnr6LtAYUWahFxMnMd7IiRB0ASPmHg6xeBBEiPQBABuTc-6JOSUsGxmSIC+LsnndRecmjqD0KMfC7lxwLlMLofot5RTUwrj7MISSHGfziEU0RIcCFh3XpHTe3Tjh9PSbYrJpSLYPGNCoRwoJ3iihXO5IUTwRS6BqMWAYwJn7IEEBAXBy8MCEhYIiWAOTf4tyIIc45QC4jnMubMu4VoTTFSFG+SpGgPp-CnFA4SzhtDl0lJXMI9yTlLGeXAQZhDw4b2jpCx5ZyLlwFecxL8Dghi6FegoPogp+LGlGHoDwahCxOH8OMKgLBq7wAKJJVWZAl70EYFQFm0YbqDluM4E+nxPgsmLCKXkr5uiFktDob8zJXpKw6QkiIvTgicrAXZW47geiSqGHigEi4ZztBBEQfobIjWeBiVoZ+sowDKv5hbHkzwmiDB5M0NwYpfmzjUAXZwZ9OjfjYk9CxwUGZxBUrHDS5tu7UIQKCRQXEhgjzluUnO7Qj4F0qBgqBeF2lgs6cQXhSx9q10yhGwcJgnhGlpI8IeT5xZzQWk6dQ6h+QPHFmMOVgVLF8KZjgm1xa4zVUnkaZkuMvBDD1YgIxpMeRTUphmZ+fsA6a1Sega15tk4ZkUEXV2TgxTCnkO5YYfwnoKHFVSSccS22AW3oq5d9EuXgIUFUHQvFTCvAMHo2cK4C4eCEn0dwXQHhYLfh-OuN70Y2rXWNbRDribzSNUm8dngTTfkBLyFB8aA2NUKVM4p9i5grp7lG+ahq1AijzsCcl8G5wGPcEYpoS0zHP3GSk05-DsOiPw5GjyC5nBeFZOaDwY65xsI-GoWoDb1pqAOUcqFTy0X0rA6u5inR3xrlsCyOoAohhzSMPaGpr03C8TZPPDj3KGkfKMMswzbEbS3AcgKRcSEnArkGPIKlvggA */
/** @xstate-layout N4IgpgJg5mDOIC5QFdZgE6wMoBcCGOYAdAMYD2yAdjkTDjgJaVQDCF1AxBGZcUwG5kA1sToBVNOjZUcAbQAMAXUSgADmVgNGPFSAAeiAIwBWQ-KIBmABwAWCwHYrAJhcA2YwE4ANCACeiZ0Miew8LBxsrCw8Pe1MAXzifVAxsfEJSdho6RmZpTgx0MnQiVQAbAgAzIoBbWjAcCQw8uSVddU1tSl0DBEMneSDXC0MrQ2G3KysffwRo4yJjYxtlh1c1m2NXBKTJVIJichkOMQAFABEAQQAVAFEAfQAxAEkAGVuAJQVlJBB2rQYdD8en1DB4iE5NvYbCMokNvH5EK4NkRbI5XIYbE5DGNDPZtiBkphcPsiITYERYPh0DkoCc8FAmAQAZQOF82hp-oDQD0ALRQ8zyEIWYz2dwhJw2VzTRGOIiGdyjJwWSXyZV4xIE3bE9Jkur0JhQRqYLg8PiUQQiPVG2Bsn5-TrdRA84wuYLyVxWexRDy2Vz2ezShCRGwLRZ+0VOWzGT34sna4i67IG60cApFErlHBVdC1cS7W1qDkOoFOxxOSxK7HySKReSmQNWd0LEJ9L1OWLGeQeWNatIJ3ZEBgQUpgDhYMRYE43AByZzuE5un1adqLzMdCB5SNcKOhNdcddiHsDGJCKOjHjGStsZicPZS8dJA6HI44ZxuLxut3nWEXBd+q65fQnSReZrEcKwfSsJZNgsY9hkGNUIklRZBQsO8iT7R8UkHYdRwuFgrieAA1a57gXJdvkLDo1xLDcQKId1llcGIoUiJx4RmbEbDBP1QXFGIIVFdC9h1J9cI4d4bh-K5v0XO4TguLAsAAdQAeXeM4-3tGjuWAmx7Dlex2MlCEPCGGxjyGbcbEFDxBRskx7FxYSH11Z9R1OS4v3Iu53lUj8sC0gCulonkfW3eUsRiDEfWMaxjxGAy4rDD1wwxLYNTjTC3PEzzSPki4AHEbiC6jAN5DxJTlDEIOhD15TsQMPE7Bi1n9Vs7MbdUdnvbKB3ISgKgYHMjSwVBVDAShNB4DgWFU6dnneABZWT3jucdJxnLAnnm0rORC3SNzMCxgghCw1ja7ioUMY9uKsBiJU2aElVFDEXL67CBqGkbJDG2AJqm5lZouacWHfVb1onKdp223blyo-b1x5Gz7rsMZ3XkeQbLA49NnmDwJXdF1qz9DKeowkldS+4bqiNM4wBHTpZvmxaVp8t8P1uPbi0OnkuIWJEoQ2DY4vsQU4OFOUlXkFw4osLtIneyn+p4b7ackenGaBlgQbBl4IY5z8Svh-8yoOoCNwcAyYjssJG0lFiLIRXobPLaIImDOFFiV0TPtVmmjQuEhGH4JkZrmhanmWiH8MIkjCLhyjTcR0KQSIAmhlFRsBkjetnexIYiFcdsBn9OEZbJzVeuVv3BoDyQg5DsOWR10HwZ82PiOuHbp25nSLZ5ax7sbSYsRcUZroDfOlW3CVIzM1UK5dH3+2w2BxsmiBk0kE1eEHc1hGIdf-s3o0+-KxAy4WAnVUjEY62hW7C-YjHOyWUYYhXrDMApDfKC35gRpUzoEKMUMolQai-xPv-M+JttIXwQKYAyZYGqhCGC4G6+dzrzFMJEHE1ZpaVyyjXH+EAGb1G3hgXeZoLTEDIYzMAsCk7wPNj0RwJ1oyqkSo4aITg4LYiLohX0KFYhf11PQihgCd5pjAZmbMtQJGECYeyM2644puxGJMJYJkHAcSMFiEM1ZMaRCHpiSUYiBx4GDgwUONIgHcD3gIQ+RArFNyUZIc+rDL6ynOpVAYSIIJlmPF2csY8RieGatCMYFjsKuJsUyKRVCZEZggTmFx1jbGMI8XA4KailQLHlJ2L0LYInBPlBWVU3ExjCkqTEn+1MfoYDpLAWAAB3IoEB3hwHqMzSO0cfIKSUmpDSvkpKfk8UjSqRB9K1X8ZKdwMRLJehRFGOsfirDMTqeSBp6sml4Bae09AnTuk4GBm3fWAzFIqXUnOSS0kJmhUxAZQIWjJhmBdHo3osQTomNMJjGIEE1hbKIOgE5djJDNLaR06h+9aEgpOUaSFhyIAPMOi6E6VYx7u2LljY8IQQwXiMshYu78bDAtBWgfUiT0BIuhck8BWZIEUvqIi-ZUKjmootm-HcUYqx1kjBsSyfQUSxDFtozO1hgXIFUBABJhpJDvDICOWAMKnGWmlbK9xGBFXKs5byPo8si4j0KcsMVx4JSDGvPpSCfQzJSplXKo0Oq4DANASkxlaSNX7CdUquAeqnRjFCNMuyr02JO04r44IJ5+jy09C6dUGpKBkDIfAH4xD0iHGoDhEcKiU6HRMNESwywuwDEqh6eKztmpBFxFBFY8tzpDC-pmrI9QaTNFzTzC22J8lz2hO-Wy2JAw8LlOiT0VazK+ibZkDt-dgSCnmL2kwEQB2YJmGGHcaJkqRj9GhTKvYSHkkpHgakBo6QMkoM3GdCC+aYyCHWasFgXD9BcCEQMSIQzywhMXZw+lVREP3b7H+SZqWpoRp23kxdQkQQhJ6dsEYp4zDWPdAmEFGwE2hLw4F7kr1eLohecEazoxREFGLWC+csYhickhFD3p3TAp2aNP+01zYsKRudcs2dGINrhGEXGqoi1OU9PLfcOd6P+0aegTWFDAKsdCuxhifEnJ+icCBVdRgwjzANTLM68tYpibrhJxu8TO2yd5uda2HpYp+gcjLcNRh5QnWLmLDYlUoSxDJXu6ugHD1-wAfKjAOH1wjDBOh8z0Z0r+mMLdJyCwNhOS7EMdR3ZPMU280QRRlD0CBdosF8E98ohl0bH0KY08YhF2lpU5YUFJjAribYzL2X82nigg+5dthmrFwSpLNYMtlTth6xefTatWUHI6V0yljWB7YKLi6Eu8ozIuhK5xHdKzljcRFA4JUxhyVgsy7So5k22EhnYt+3ORlFgYjxcXQynCRZmGOvazVmXnWgeTuBgNjE5Sobiu6R6dnejCjvS2GW0QLxvRSyJVemBDsBuWEEbigpR2Pv+4sUCMtNj8o2VjZL5NIcw6OhEMECPQ3I8DPMyw6PPQLKhPubbCQ4hAA */
createMachine(
{
tsTypes: {} as import("./usersXService.typegen").Typegen0,
@@ -130,222 +132,261 @@ export const usersMachine =
updateUserRoles: {
data: TypesGen.User
}
getUserCount: {
data: TypesGen.UserCountResponse
}
},
},
predictableActionArguments: true,
id: "usersState",
initial: "startingPagination",
type: "parallel",
states: {
startingPagination: {
entry: "assignPaginationRef",
always: {
target: "gettingUsers",
},
},
gettingUsers: {
entry: "clearGetUsersError",
invoke: {
src: "getUsers",
id: "getUsers",
onDone: [
{
target: "idle",
actions: "assignUsers",
},
],
onError: [
{
target: "idle",
actions: [
"clearUsers",
"assignGetUsersError",
"displayGetUsersErrorMessage",
count: {
initial: "gettingCount",
states: {
idle: {},
gettingCount: {
entry: "clearGetCountError",
invoke: {
src: "getUserCount",
id: "getUserCount",
onDone: [
{
target: "idle",
actions: "assignCount",
},
],
onError: [
{
target: "idle",
actions: "assignGetCountError",
},
],
},
],
},
},
tags: "loading",
},
idle: {
entry: "clearSelectedUser",
on: {
SUSPEND_USER: {
target: "confirmUserSuspension",
actions: "assignUserToSuspend",
},
DELETE_USER: {
target: "confirmUserDeletion",
actions: "assignUserToDelete",
},
ACTIVATE_USER: {
target: "confirmUserActivation",
actions: "assignUserToActivate",
},
RESET_USER_PASSWORD: {
target: "confirmUserPasswordReset",
actions: [
"assignUserIdToResetPassword",
"generateRandomPassword",
],
},
UPDATE_USER_ROLES: {
target: "updatingUserRoles",
actions: "assignUserIdToUpdateRoles",
},
UPDATE_PAGE: {
target: "gettingUsers",
actions: "updateURL",
},
UPDATE_FILTER: {
target: ".gettingCount",
actions: ["assignFilter", "sendResetPage"],
},
},
},
confirmUserSuspension: {
on: {
CONFIRM_USER_SUSPENSION: {
target: "suspendingUser",
},
CANCEL_USER_SUSPENSION: {
target: "idle",
},
},
},
confirmUserDeletion: {
on: {
CONFIRM_USER_DELETE: {
target: "deletingUser",
},
CANCEL_USER_DELETE: {
target: "idle",
},
},
},
confirmUserActivation: {
on: {
CONFIRM_USER_ACTIVATION: {
target: "activatingUser",
},
CANCEL_USER_ACTIVATION: {
target: "idle",
},
},
},
suspendingUser: {
entry: "clearSuspendUserError",
invoke: {
src: "suspendUser",
id: "suspendUser",
onDone: [
{
users: {
initial: "startingPagination",
states: {
startingPagination: {
entry: "assignPaginationRef",
always: {
target: "gettingUsers",
actions: "displaySuspendSuccess",
},
],
onError: [
{
target: "idle",
actions: [
"assignSuspendUserError",
"displaySuspendedErrorMessage",
],
},
],
},
},
deletingUser: {
entry: "clearDeleteUserError",
invoke: {
src: "deleteUser",
id: "deleteUser",
onDone: [
{
target: "gettingUsers",
actions: "displayDeleteSuccess",
},
],
onError: [
{
target: "idle",
actions: ["assignDeleteUserError", "displayDeleteErrorMessage"],
},
],
},
},
activatingUser: {
entry: "clearActivateUserError",
invoke: {
src: "activateUser",
id: "activateUser",
onDone: [
{
target: "gettingUsers",
actions: "displayActivateSuccess",
},
],
onError: [
{
target: "idle",
actions: [
"assignActivateUserError",
"displayActivatedErrorMessage",
],
},
],
},
},
confirmUserPasswordReset: {
on: {
CONFIRM_USER_PASSWORD_RESET: {
target: "resettingUserPassword",
},
CANCEL_USER_PASSWORD_RESET: {
target: "idle",
gettingUsers: {
entry: "clearGetUsersError",
invoke: {
src: "getUsers",
id: "getUsers",
onDone: [
{
target: "idle",
actions: "assignUsers",
},
],
onError: [
{
target: "idle",
actions: [
"clearUsers",
"assignGetUsersError",
"displayGetUsersErrorMessage",
],
},
],
},
tags: "loading",
},
},
},
resettingUserPassword: {
entry: "clearResetUserPasswordError",
invoke: {
src: "resetUserPassword",
id: "resetUserPassword",
onDone: [
{
target: "idle",
actions: "displayResetPasswordSuccess",
idle: {
entry: "clearSelectedUser",
on: {
SUSPEND_USER: {
target: "confirmUserSuspension",
actions: "assignUserToSuspend",
},
DELETE_USER: {
target: "confirmUserDeletion",
actions: "assignUserToDelete",
},
ACTIVATE_USER: {
target: "confirmUserActivation",
actions: "assignUserToActivate",
},
RESET_USER_PASSWORD: {
target: "confirmUserPasswordReset",
actions: [
"assignUserIdToResetPassword",
"generateRandomPassword",
],
},
UPDATE_USER_ROLES: {
target: "updatingUserRoles",
actions: "assignUserIdToUpdateRoles",
},
UPDATE_PAGE: {
target: "gettingUsers",
actions: "updateURL",
},
},
],
onError: [
{
target: "idle",
actions: [
"assignResetUserPasswordError",
"displayResetPasswordErrorMessage",
},
confirmUserSuspension: {
on: {
CONFIRM_USER_SUSPENSION: {
target: "suspendingUser",
},
CANCEL_USER_SUSPENSION: {
target: "idle",
},
},
},
confirmUserDeletion: {
on: {
CONFIRM_USER_DELETE: {
target: "deletingUser",
},
CANCEL_USER_DELETE: {
target: "idle",
},
},
},
confirmUserActivation: {
on: {
CONFIRM_USER_ACTIVATION: {
target: "activatingUser",
},
CANCEL_USER_ACTIVATION: {
target: "idle",
},
},
},
suspendingUser: {
entry: "clearSuspendUserError",
invoke: {
src: "suspendUser",
id: "suspendUser",
onDone: [
{
target: "gettingUsers",
actions: "displaySuspendSuccess",
},
],
onError: [
{
target: "idle",
actions: [
"assignSuspendUserError",
"displaySuspendedErrorMessage",
],
},
],
},
],
},
},
updatingUserRoles: {
entry: "clearUpdateUserRolesError",
invoke: {
src: "updateUserRoles",
id: "updateUserRoles",
onDone: [
{
target: "idle",
actions: "updateUserRolesInTheList",
},
],
onError: [
{
target: "idle",
actions: [
"assignUpdateRolesError",
"displayUpdateRolesErrorMessage",
},
deletingUser: {
entry: "clearDeleteUserError",
invoke: {
src: "deleteUser",
id: "deleteUser",
onDone: [
{
target: "gettingUsers",
actions: "displayDeleteSuccess",
},
],
onError: [
{
target: "idle",
actions: [
"assignDeleteUserError",
"displayDeleteErrorMessage",
],
},
],
},
],
},
activatingUser: {
entry: "clearActivateUserError",
invoke: {
src: "activateUser",
id: "activateUser",
onDone: [
{
target: "gettingUsers",
actions: "displayActivateSuccess",
},
],
onError: [
{
target: "idle",
actions: [
"assignActivateUserError",
"displayActivatedErrorMessage",
],
},
],
},
},
confirmUserPasswordReset: {
on: {
CONFIRM_USER_PASSWORD_RESET: {
target: "resettingUserPassword",
},
CANCEL_USER_PASSWORD_RESET: {
target: "idle",
},
},
},
resettingUserPassword: {
entry: "clearResetUserPasswordError",
invoke: {
src: "resetUserPassword",
id: "resetUserPassword",
onDone: [
{
target: "idle",
actions: "displayResetPasswordSuccess",
},
],
onError: [
{
target: "idle",
actions: [
"assignResetUserPasswordError",
"displayResetPasswordErrorMessage",
],
},
],
},
},
updatingUserRoles: {
entry: "clearUpdateUserRolesError",
invoke: {
src: "updateUserRoles",
id: "updateUserRoles",
onDone: [
{
target: "idle",
actions: "updateUserRolesInTheList",
},
],
onError: [
{
target: "idle",
actions: [
"assignUpdateRolesError",
"displayUpdateRolesErrorMessage",
],
},
],
},
},
},
},
},
@@ -363,6 +404,9 @@ export const usersMachine =
limit,
})
},
getUserCount: (context) => {
return API.getUserCount(queryToFilter(context.filter))
},
suspendUser: (context) => {
if (!context.userIdToSuspend) {
throw new Error("userIdToSuspend is undefined")
@@ -420,6 +464,15 @@ export const usersMachine =
assignUsers: assign({
users: (_, event) => event.data,
}),
assignCount: assign({
count: (_, event) => event.data.count,
}),
assignGetCountError: assign({
getCountError: (_, event) => event.data,
}),
clearGetCountError: assign({
getCountError: (_) => undefined,
}),
assignFilter: assign({
filter: (_, event) => event.query,
}),