mirror of https://github.com/coder/coder.git
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:
parent
a4fbc74751
commit
f496b149df
|
@ -437,6 +437,7 @@ func New(options *Options) *API {
|
|||
)
|
||||
r.Post("/", api.postUser)
|
||||
r.Get("/", api.users)
|
||||
r.Get("/count", api.userCount)
|
||||
r.Post("/logout", api.postLogout)
|
||||
// These routes query information about site wide roles.
|
||||
r.Route("/roles", func(r chi.Router) {
|
||||
|
|
|
@ -246,6 +246,7 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
|
|||
// Endpoints that use the SQLQuery filter.
|
||||
"GET:/api/v2/workspaces/": {StatusCode: http.StatusOK, NoAuthorize: true},
|
||||
"GET:/api/v2/workspaces/count": {StatusCode: http.StatusOK, NoAuthorize: true},
|
||||
"GET:/api/v2/users/count": {StatusCode: http.StatusOK, NoAuthorize: true},
|
||||
}
|
||||
|
||||
// Routes like proxy routes support all HTTP methods. A helper func to expand
|
||||
|
|
|
@ -457,6 +457,72 @@ func (q *fakeQuerier) GetActiveUserCount(_ context.Context) (int64, error) {
|
|||
return active, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetFilteredUserCount(ctx context.Context, arg database.GetFilteredUserCountParams) (int64, error) {
|
||||
count, err := q.GetAuthorizedUserCount(ctx, arg, nil)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetAuthorizedUserCount(_ context.Context, params database.GetFilteredUserCountParams, authorizedFilter rbac.AuthorizeFilter) (int64, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
users := append([]database.User{}, q.users...)
|
||||
|
||||
if params.Deleted {
|
||||
tmp := make([]database.User, 0, len(users))
|
||||
for _, user := range users {
|
||||
if user.Deleted {
|
||||
tmp = append(tmp, user)
|
||||
}
|
||||
}
|
||||
users = tmp
|
||||
}
|
||||
|
||||
if params.Search != "" {
|
||||
tmp := make([]database.User, 0, len(users))
|
||||
for i, user := range users {
|
||||
if strings.Contains(strings.ToLower(user.Email), strings.ToLower(params.Search)) {
|
||||
tmp = append(tmp, users[i])
|
||||
} else if strings.Contains(strings.ToLower(user.Username), strings.ToLower(params.Search)) {
|
||||
tmp = append(tmp, users[i])
|
||||
}
|
||||
}
|
||||
users = tmp
|
||||
}
|
||||
|
||||
if len(params.Status) > 0 {
|
||||
usersFilteredByStatus := make([]database.User, 0, len(users))
|
||||
for i, user := range users {
|
||||
if slice.ContainsCompare(params.Status, user.Status, func(a, b database.UserStatus) bool {
|
||||
return strings.EqualFold(string(a), string(b))
|
||||
}) {
|
||||
usersFilteredByStatus = append(usersFilteredByStatus, users[i])
|
||||
}
|
||||
}
|
||||
users = usersFilteredByStatus
|
||||
}
|
||||
|
||||
if len(params.RbacRole) > 0 && !slice.Contains(params.RbacRole, rbac.RoleMember()) {
|
||||
usersFilteredByRole := make([]database.User, 0, len(users))
|
||||
for i, user := range users {
|
||||
if slice.OverlapCompare(params.RbacRole, user.RBACRoles, strings.EqualFold) {
|
||||
usersFilteredByRole = append(usersFilteredByRole, users[i])
|
||||
}
|
||||
}
|
||||
|
||||
users = usersFilteredByRole
|
||||
}
|
||||
|
||||
for _, user := range q.workspaces {
|
||||
// If the filter exists, ensure the object is authorized.
|
||||
if authorizedFilter != nil && !authorizedFilter.Eval(user.RBACObject()) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return int64(len(users)), nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) UpdateUserDeletedByID(_ context.Context, params database.UpdateUserDeletedByIDParams) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
type customQuerier interface {
|
||||
templateQuerier
|
||||
workspaceQuerier
|
||||
userQuerier
|
||||
}
|
||||
|
||||
type templateQuerier interface {
|
||||
|
@ -169,8 +170,6 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
|
|||
}
|
||||
|
||||
func (q *sqlQuerier) GetAuthorizedWorkspaceCount(ctx context.Context, arg GetWorkspaceCountParams, authorizedFilter rbac.AuthorizeFilter) (int64, error) {
|
||||
// In order to properly use ORDER BY, OFFSET, and LIMIT, we need to inject the
|
||||
// authorizedFilter between the end of the where clause and those statements.
|
||||
filter := strings.Replace(getWorkspaceCount, "-- @authorize_filter", fmt.Sprintf(" AND %s", authorizedFilter.SQLString(rbac.NoACLConfig())), 1)
|
||||
// The name comment is for metric tracking
|
||||
query := fmt.Sprintf("-- name: GetAuthorizedWorkspaceCount :one\n%s", filter)
|
||||
|
@ -187,3 +186,21 @@ func (q *sqlQuerier) GetAuthorizedWorkspaceCount(ctx context.Context, arg GetWor
|
|||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
type userQuerier interface {
|
||||
GetAuthorizedUserCount(ctx context.Context, arg GetFilteredUserCountParams, authorizedFilter rbac.AuthorizeFilter) (int64, error)
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetAuthorizedUserCount(ctx context.Context, arg GetFilteredUserCountParams, authorizedFilter rbac.AuthorizeFilter) (int64, error) {
|
||||
filter := strings.Replace(getFilteredUserCount, "-- @authorize_filter", fmt.Sprintf(" AND %s", authorizedFilter.SQLString(rbac.NoACLConfig())), 1)
|
||||
query := fmt.Sprintf("-- name: GetAuthorizedUserCount :one\n%s", filter)
|
||||
row := q.db.QueryRowContext(ctx, query,
|
||||
arg.Deleted,
|
||||
arg.Search,
|
||||
pq.Array(arg.Status),
|
||||
pq.Array(arg.RbacRole),
|
||||
)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ type sqlcQuerier interface {
|
|||
GetDeploymentID(ctx context.Context) (string, error)
|
||||
GetFileByHashAndCreator(ctx context.Context, arg GetFileByHashAndCreatorParams) (File, error)
|
||||
GetFileByID(ctx context.Context, id uuid.UUID) (File, error)
|
||||
GetFilteredUserCount(ctx context.Context, arg GetFilteredUserCountParams) (int64, error)
|
||||
GetGitAuthLink(ctx context.Context, arg GetGitAuthLinkParams) (GitAuthLink, error)
|
||||
GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error)
|
||||
GetGroupByID(ctx context.Context, id uuid.UUID) (Group, error)
|
||||
|
|
|
@ -3975,6 +3975,60 @@ func (q *sqlQuerier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.
|
|||
return i, err
|
||||
}
|
||||
|
||||
const getFilteredUserCount = `-- name: GetFilteredUserCount :one
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
users.deleted = $1
|
||||
-- Start filters
|
||||
-- Filter by name, email or username
|
||||
AND CASE
|
||||
WHEN $2 :: text != '' THEN (
|
||||
email ILIKE concat('%', $2, '%')
|
||||
OR username ILIKE concat('%', $2, '%')
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by status
|
||||
AND CASE
|
||||
-- @status needs to be a text because it can be empty, If it was
|
||||
-- user_status enum, it would not.
|
||||
WHEN cardinality($3 :: user_status[]) > 0 THEN
|
||||
status = ANY($3 :: user_status[])
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by rbac_roles
|
||||
AND CASE
|
||||
-- @rbac_role allows filtering by rbac roles. If 'member' is included, show everyone, as everyone is a member.
|
||||
WHEN cardinality($4 :: text[]) > 0 AND 'member' != ANY($4 :: text[])
|
||||
THEN rbac_roles && $4 :: text[]
|
||||
ELSE true
|
||||
END
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedUserCount
|
||||
-- @authorize_filter
|
||||
`
|
||||
|
||||
type GetFilteredUserCountParams struct {
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
Search string `db:"search" json:"search"`
|
||||
Status []UserStatus `db:"status" json:"status"`
|
||||
RbacRole []string `db:"rbac_role" json:"rbac_role"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetFilteredUserCount(ctx context.Context, arg GetFilteredUserCountParams) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, getFilteredUserCount,
|
||||
arg.Deleted,
|
||||
arg.Search,
|
||||
pq.Array(arg.Status),
|
||||
pq.Array(arg.RbacRole),
|
||||
)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
|
||||
SELECT
|
||||
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at
|
||||
|
|
|
@ -39,6 +39,41 @@ FROM
|
|||
WHERE
|
||||
status = 'active'::user_status AND deleted = false;
|
||||
|
||||
-- name: GetFilteredUserCount :one
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
users.deleted = @deleted
|
||||
-- Start filters
|
||||
-- Filter by name, email or username
|
||||
AND CASE
|
||||
WHEN @search :: text != '' THEN (
|
||||
email ILIKE concat('%', @search, '%')
|
||||
OR username ILIKE concat('%', @search, '%')
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by status
|
||||
AND CASE
|
||||
-- @status needs to be a text because it can be empty, If it was
|
||||
-- user_status enum, it would not.
|
||||
WHEN cardinality(@status :: user_status[]) > 0 THEN
|
||||
status = ANY(@status :: user_status[])
|
||||
ELSE true
|
||||
END
|
||||
-- Filter by rbac_roles
|
||||
AND CASE
|
||||
-- @rbac_role allows filtering by rbac roles. If 'member' is included, show everyone, as everyone is a member.
|
||||
WHEN cardinality(@rbac_role :: text[]) > 0 AND 'member' != ANY(@rbac_role :: text[])
|
||||
THEN rbac_roles && @rbac_role :: text[]
|
||||
ELSE true
|
||||
END
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedUserCount
|
||||
-- @authorize_filter
|
||||
;
|
||||
|
||||
-- name: InsertUser :one
|
||||
INSERT INTO
|
||||
users (
|
||||
|
|
|
@ -251,6 +251,42 @@ func (api *API) users(rw http.ResponseWriter, r *http.Request) {
|
|||
render.JSON(rw, r, convertUsers(users, organizationIDsByUserID))
|
||||
}
|
||||
|
||||
func (api *API) userCount(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
query := r.URL.Query().Get("q")
|
||||
params, errs := userSearchQuery(query)
|
||||
if len(errs) > 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid user search query.",
|
||||
Validations: errs,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
sqlFilter, err := api.HTTPAuth.AuthorizeSQLFilter(r, rbac.ActionRead, rbac.ResourceUser.Type)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error preparing sql filter.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
count, err := api.Database.GetAuthorizedUserCount(ctx, database.GetFilteredUserCountParams{
|
||||
Search: params.Search,
|
||||
Status: params.Status,
|
||||
RbacRole: params.RbacRole,
|
||||
}, sqlFilter)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserCountResponse{
|
||||
Count: count,
|
||||
})
|
||||
}
|
||||
|
||||
// Creates a new user.
|
||||
func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
|
|
@ -1255,6 +1255,58 @@ func TestGetUsers(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGetFilteredUserCount(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("AllUsers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
client.CreateUser(ctx, codersdk.CreateUserRequest{
|
||||
Email: "alice@email.com",
|
||||
Username: "alice",
|
||||
Password: "password",
|
||||
OrganizationID: user.OrganizationID,
|
||||
})
|
||||
// No params is all users
|
||||
response, err := client.UserCount(ctx, codersdk.UserCountRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, int(response.Count))
|
||||
})
|
||||
t.Run("ActiveUsers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
_, err := client.User(ctx, first.UserID.String())
|
||||
require.NoError(t, err, "")
|
||||
|
||||
// Alice will be suspended
|
||||
alice, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
|
||||
Email: "alice@email.com",
|
||||
Username: "alice",
|
||||
Password: "password",
|
||||
OrganizationID: first.OrganizationID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.UpdateUserStatus(ctx, alice.Username, codersdk.UserStatusSuspended)
|
||||
require.NoError(t, err)
|
||||
|
||||
response, err := client.UserCount(ctx, codersdk.UserCountRequest{
|
||||
Status: codersdk.UserStatusActive,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, int(response.Count))
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostTokens(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
|
|
|
@ -166,7 +166,7 @@ func (api *API) workspaceCount(rw http.ResponseWriter, r *http.Request) {
|
|||
filter, errs := workspaceSearchQuery(queryStr, codersdk.Pagination{})
|
||||
if len(errs) > 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid audit search query.",
|
||||
Message: "Invalid workspace search query.",
|
||||
Validations: errs,
|
||||
})
|
||||
return
|
||||
|
|
|
@ -47,6 +47,20 @@ type User struct {
|
|||
AvatarURL string `json:"avatar_url"`
|
||||
}
|
||||
|
||||
type UserCountRequest struct {
|
||||
Search string `json:"search,omitempty" typescript:"-"`
|
||||
// Filter users by status.
|
||||
Status UserStatus `json:"status,omitempty" typescript:"-"`
|
||||
// Filter users that have the given role.
|
||||
Role string `json:"role,omitempty" typescript:"-"`
|
||||
|
||||
SearchQuery string `json:"q,omitempty"`
|
||||
}
|
||||
|
||||
type UserCountResponse struct {
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
type CreateFirstUserRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Username string `json:"username" validate:"required,username"`
|
||||
|
@ -345,6 +359,40 @@ func (c *Client) Users(ctx context.Context, req UsersRequest) ([]User, error) {
|
|||
return users, json.NewDecoder(res.Body).Decode(&users)
|
||||
}
|
||||
|
||||
func (c *Client) UserCount(ctx context.Context, req UserCountRequest) (UserCountResponse, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/count", nil,
|
||||
func(r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
var params []string
|
||||
if req.Search != "" {
|
||||
params = append(params, req.Search)
|
||||
}
|
||||
if req.Status != "" {
|
||||
params = append(params, "status:"+string(req.Status))
|
||||
}
|
||||
if req.Role != "" {
|
||||
params = append(params, "role:"+req.Role)
|
||||
}
|
||||
if req.SearchQuery != "" {
|
||||
params = append(params, req.SearchQuery)
|
||||
}
|
||||
q.Set("q", strings.Join(params, " "))
|
||||
r.URL.RawQuery = q.Encode()
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return UserCountResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return UserCountResponse{}, readBodyAsError(res)
|
||||
}
|
||||
|
||||
var count UserCountResponse
|
||||
return count, json.NewDecoder(res.Body).Decode(&count)
|
||||
}
|
||||
|
||||
// OrganizationsByUser returns all organizations the user is a member of.
|
||||
func (c *Client) OrganizationsByUser(ctx context.Context, user string) ([]Organization, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations", user), nil)
|
||||
|
|
|
@ -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> => {
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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={() => {
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
|
|
Loading…
Reference in New Issue