mirror of
https://github.com/coder/coder.git
synced 2025-07-21 01:28:49 +00:00
feat: add count to get users endpoint (#5016)
This commit is contained in:
@ -102,12 +102,12 @@ func list() *cobra.Command {
|
||||
_, _ = fmt.Fprintln(cmd.ErrOrStderr())
|
||||
return nil
|
||||
}
|
||||
users, err := client.Users(cmd.Context(), codersdk.UsersRequest{})
|
||||
userRes, err := client.Users(cmd.Context(), codersdk.UsersRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
usersByID := map[uuid.UUID]codersdk.User{}
|
||||
for _, user := range users {
|
||||
for _, user := range userRes.Users {
|
||||
usersByID[user.ID] = user
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ func userList() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
users, err := client.Users(cmd.Context(), codersdk.UsersRequest{})
|
||||
res, err := client.Users(cmd.Context(), codersdk.UsersRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -38,12 +38,12 @@ func userList() *cobra.Command {
|
||||
out := ""
|
||||
switch outputFormat {
|
||||
case "table", "":
|
||||
out, err = cliui.DisplayTable(users, "Username", columns)
|
||||
out, err = cliui.DisplayTable(res.Users, "Username", columns)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("render table: %w", err)
|
||||
}
|
||||
case "json":
|
||||
outBytes, err := json.Marshal(users)
|
||||
outBytes, err := json.Marshal(res.Users)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("marshal users to JSON: %w", err)
|
||||
}
|
||||
|
@ -431,7 +431,6 @@ 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) {
|
||||
|
@ -245,7 +245,6 @@ 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/users/count": {StatusCode: http.StatusOK, NoAuthorize: true},
|
||||
}
|
||||
|
||||
// Routes like proxy routes support all HTTP methods. A helper func to expand
|
||||
|
@ -538,7 +538,7 @@ func (q *fakeQuerier) UpdateUserDeletedByID(_ context.Context, params database.U
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams) ([]database.User, error) {
|
||||
func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams) ([]database.GetUsersRow, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
@ -579,7 +579,7 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
|
||||
|
||||
// If no users after the time, then we return an empty list.
|
||||
if !found {
|
||||
return nil, sql.ErrNoRows
|
||||
return []database.GetUsersRow{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -617,9 +617,11 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
|
||||
users = usersFilteredByRole
|
||||
}
|
||||
|
||||
beforePageCount := len(users)
|
||||
|
||||
if params.OffsetOpt > 0 {
|
||||
if int(params.OffsetOpt) > len(users)-1 {
|
||||
return nil, sql.ErrNoRows
|
||||
return []database.GetUsersRow{}, nil
|
||||
}
|
||||
users = users[params.OffsetOpt:]
|
||||
}
|
||||
@ -631,7 +633,30 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
|
||||
users = users[:params.LimitOpt]
|
||||
}
|
||||
|
||||
return users, nil
|
||||
return convertUsers(users, int64(beforePageCount)), nil
|
||||
}
|
||||
|
||||
func convertUsers(users []database.User, count int64) []database.GetUsersRow {
|
||||
rows := make([]database.GetUsersRow, len(users))
|
||||
for i, u := range users {
|
||||
rows[i] = database.GetUsersRow{
|
||||
ID: u.ID,
|
||||
Email: u.Email,
|
||||
Username: u.Username,
|
||||
HashedPassword: u.HashedPassword,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
Status: u.Status,
|
||||
RBACRoles: u.RBACRoles,
|
||||
LoginType: u.LoginType,
|
||||
AvatarURL: u.AvatarURL,
|
||||
Deleted: u.Deleted,
|
||||
LastSeenAt: u.LastSeenAt,
|
||||
Count: count,
|
||||
}
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetUsersByIDs(_ context.Context, ids []uuid.UUID) ([]database.User, error) {
|
||||
|
@ -71,3 +71,25 @@ func (User) RBACObject() rbac.Object {
|
||||
func (License) RBACObject() rbac.Object {
|
||||
return rbac.ResourceLicense
|
||||
}
|
||||
|
||||
func ConvertUserRows(rows []GetUsersRow) []User {
|
||||
users := make([]User, len(rows))
|
||||
for i, r := range rows {
|
||||
users[i] = User{
|
||||
ID: r.ID,
|
||||
Email: r.Email,
|
||||
Username: r.Username,
|
||||
HashedPassword: r.HashedPassword,
|
||||
CreatedAt: r.CreatedAt,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
Status: r.Status,
|
||||
RBACRoles: r.RBACRoles,
|
||||
LoginType: r.LoginType,
|
||||
AvatarURL: r.AvatarURL,
|
||||
Deleted: r.Deleted,
|
||||
LastSeenAt: r.LastSeenAt,
|
||||
}
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ type sqlcQuerier interface {
|
||||
GetUserGroups(ctx context.Context, userID uuid.UUID) ([]Group, error)
|
||||
GetUserLinkByLinkedID(ctx context.Context, linkedID string) (UserLink, error)
|
||||
GetUserLinkByUserIDLoginType(ctx context.Context, arg GetUserLinkByUserIDLoginTypeParams) (UserLink, error)
|
||||
GetUsers(ctx context.Context, arg GetUsersParams) ([]User, error)
|
||||
GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error)
|
||||
// This shouldn't check for deleted, because it's frequently used
|
||||
// to look up references to actions. eg. a user could build a workspace
|
||||
// for another user, then be deleted... we still want them to appear!
|
||||
|
@ -4178,7 +4178,7 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) {
|
||||
|
||||
const getUsers = `-- name: GetUsers :many
|
||||
SELECT
|
||||
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at
|
||||
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, COUNT(*) OVER() AS count
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
@ -4247,7 +4247,23 @@ type GetUsersParams struct {
|
||||
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]User, error) {
|
||||
type GetUsersRow struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Email string `db:"email" json:"email"`
|
||||
Username string `db:"username" json:"username"`
|
||||
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
Status UserStatus `db:"status" json:"status"`
|
||||
RBACRoles pq.StringArray `db:"rbac_roles" json:"rbac_roles"`
|
||||
LoginType LoginType `db:"login_type" json:"login_type"`
|
||||
AvatarURL sql.NullString `db:"avatar_url" json:"avatar_url"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
|
||||
Count int64 `db:"count" json:"count"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getUsers,
|
||||
arg.Deleted,
|
||||
arg.AfterID,
|
||||
@ -4261,9 +4277,9 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]User,
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []User
|
||||
var items []GetUsersRow
|
||||
for rows.Next() {
|
||||
var i User
|
||||
var i GetUsersRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
@ -4277,6 +4293,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]User,
|
||||
&i.AvatarURL,
|
||||
&i.Deleted,
|
||||
&i.LastSeenAt,
|
||||
&i.Count,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -128,7 +128,7 @@ WHERE
|
||||
|
||||
-- name: GetUsers :many
|
||||
SELECT
|
||||
*
|
||||
*, COUNT(*) OVER() AS count
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
|
@ -350,10 +350,11 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) {
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
users, err := r.options.Database.GetUsers(ctx, database.GetUsersParams{})
|
||||
userRows, err := r.options.Database.GetUsers(ctx, database.GetUsersParams{})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get users: %w", err)
|
||||
}
|
||||
users := database.ConvertUserRows(userRows)
|
||||
var firstUser database.User
|
||||
for _, dbUser := range users {
|
||||
if dbUser.Status != database.UserStatusActive {
|
||||
|
@ -198,7 +198,7 @@ func (api *API) users(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
users, err := api.Database.GetUsers(ctx, database.GetUsersParams{
|
||||
userRows, err := api.Database.GetUsers(ctx, database.GetUsersParams{
|
||||
AfterID: paginationParams.AfterID,
|
||||
OffsetOpt: int32(paginationParams.Offset),
|
||||
LimitOpt: int32(paginationParams.Limit),
|
||||
@ -206,10 +206,6 @@ func (api *API) users(rw http.ResponseWriter, r *http.Request) {
|
||||
Status: params.Status,
|
||||
RbacRole: params.RbacRole,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, []codersdk.User{})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching users.",
|
||||
@ -217,8 +213,17 @@ func (api *API) users(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
// GetUsers does not return ErrNoRows because it uses a window function to get the count.
|
||||
// So we need to check if the userRows is empty and return an empty array if so.
|
||||
if len(userRows) == 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.GetUsersResponse{
|
||||
Users: []codersdk.User{},
|
||||
Count: 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
users, err = AuthorizeFilter(api.HTTPAuth, r, rbac.ActionRead, users)
|
||||
users, err := AuthorizeFilter(api.HTTPAuth, r, rbac.ActionRead, database.ConvertUserRows(userRows))
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching users.",
|
||||
@ -248,42 +253,9 @@ func (api *API) users(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
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,
|
||||
render.JSON(rw, r, codersdk.GetUsersResponse{
|
||||
Users: convertUsers(users, organizationIDsByUserID),
|
||||
Count: int(userRows[0].Count),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -78,14 +78,14 @@ func TestFirstUser(t *testing.T) {
|
||||
|
||||
_ = coderdtest.CreateAnotherUser(t, client, firstUserResp.OrganizationID)
|
||||
|
||||
allUsers, err := client.Users(ctx, codersdk.UsersRequest{})
|
||||
allUsersRes, err := client.Users(ctx, codersdk.UsersRequest{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, allUsers, 2)
|
||||
require.Len(t, allUsersRes.Users, 2)
|
||||
|
||||
// We sent the "GET Users" request with the first user, but the second user
|
||||
// should be Never since they haven't performed a request.
|
||||
for _, user := range allUsers {
|
||||
for _, user := range allUsersRes.Users {
|
||||
if user.ID == firstUser.ID {
|
||||
require.WithinDuration(t, firstUser.LastSeenAt, database.Now(), testutil.WaitShort)
|
||||
} else {
|
||||
@ -1186,7 +1186,7 @@ func TestUsersFilter(t *testing.T) {
|
||||
exp = append(exp, made)
|
||||
}
|
||||
}
|
||||
require.ElementsMatch(t, exp, matched, "expected workspaces returned")
|
||||
require.ElementsMatch(t, exp, matched.Users, "expected workspaces returned")
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1208,10 +1208,10 @@ func TestGetUsers(t *testing.T) {
|
||||
OrganizationID: user.OrganizationID,
|
||||
})
|
||||
// No params is all users
|
||||
users, err := client.Users(ctx, codersdk.UsersRequest{})
|
||||
res, err := client.Users(ctx, codersdk.UsersRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, users, 2)
|
||||
require.Len(t, users[0].OrganizationIDs, 1)
|
||||
require.Len(t, res.Users, 2)
|
||||
require.Len(t, res.Users[0].OrganizationIDs, 1)
|
||||
})
|
||||
t.Run("ActiveUsers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@ -1247,64 +1247,66 @@ func TestGetUsers(t *testing.T) {
|
||||
_, err = client.UpdateUserStatus(ctx, alice.Username, codersdk.UserStatusSuspended)
|
||||
require.NoError(t, err)
|
||||
|
||||
users, err := client.Users(ctx, codersdk.UsersRequest{
|
||||
res, err := client.Users(ctx, codersdk.UsersRequest{
|
||||
Status: codersdk.UserStatusActive,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, active, users)
|
||||
require.ElementsMatch(t, active, res.Users)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetFilteredUserCount(t *testing.T) {
|
||||
func TestGetUsersPagination(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("AllUsers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
client := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
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))
|
||||
_, err := client.User(ctx, first.UserID.String())
|
||||
require.NoError(t, err, "")
|
||||
|
||||
_, err = client.CreateUser(ctx, codersdk.CreateUserRequest{
|
||||
Email: "alice@email.com",
|
||||
Username: "alice",
|
||||
Password: "password",
|
||||
OrganizationID: first.OrganizationID,
|
||||
})
|
||||
t.Run("ActiveUsers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, client)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
res, err := client.Users(ctx, codersdk.UsersRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Users, 2)
|
||||
require.Equal(t, res.Count, 2)
|
||||
|
||||
_, 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))
|
||||
res, err = client.Users(ctx, codersdk.UsersRequest{
|
||||
Pagination: codersdk.Pagination{
|
||||
Limit: 1,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Users, 1)
|
||||
require.Equal(t, res.Count, 2)
|
||||
|
||||
res, err = client.Users(ctx, codersdk.UsersRequest{
|
||||
Pagination: codersdk.Pagination{
|
||||
Offset: 1,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Users, 1)
|
||||
require.Equal(t, res.Count, 2)
|
||||
|
||||
// if offset is higher than the count postgres returns an empty array
|
||||
// and not an ErrNoRows error. This also means the count must be 0.
|
||||
res, err = client.Users(ctx, codersdk.UsersRequest{
|
||||
Pagination: codersdk.Pagination{
|
||||
Offset: 3,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Users, 0)
|
||||
require.Equal(t, res.Count, 0)
|
||||
}
|
||||
|
||||
func TestPostTokens(t *testing.T) {
|
||||
@ -1420,7 +1422,7 @@ func TestSuspendedPagination(t *testing.T) {
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, page, "expected page")
|
||||
require.Equal(t, expected, page.Users, "expected page")
|
||||
}
|
||||
|
||||
// TestPaginatedUsers creates a list of users, then tries to paginate through
|
||||
@ -1546,15 +1548,15 @@ func assertPagination(ctx context.Context, t *testing.T, client *codersdk.Client
|
||||
},
|
||||
}))
|
||||
require.NoError(t, err, "first page")
|
||||
require.Equalf(t, page, allUsers[:limit], "first page, limit=%d", limit)
|
||||
count += len(page)
|
||||
require.Equalf(t, page.Users, allUsers[:limit], "first page, limit=%d", limit)
|
||||
count += len(page.Users)
|
||||
|
||||
for {
|
||||
if len(page) == 0 {
|
||||
if len(page.Users) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
afterCursor := page[len(page)-1].ID
|
||||
afterCursor := page.Users[len(page.Users)-1].ID
|
||||
// Assert each page is the next expected page
|
||||
// This is using a cursor, and only works if all users created_at
|
||||
// is unique.
|
||||
@ -1581,8 +1583,8 @@ func assertPagination(ctx context.Context, t *testing.T, client *codersdk.Client
|
||||
} else {
|
||||
expected = allUsers[count : count+limit]
|
||||
}
|
||||
require.Equalf(t, page, expected, "next users, after=%s, limit=%d", afterCursor, limit)
|
||||
require.Equalf(t, offsetPage, expected, "offset users, offset=%d, limit=%d", count, limit)
|
||||
require.Equalf(t, page.Users, expected, "next users, after=%s, limit=%d", afterCursor, limit)
|
||||
require.Equalf(t, offsetPage.Users, expected, "offset users, offset=%d, limit=%d", count, limit)
|
||||
|
||||
// Also check the before
|
||||
prevPage, err := client.Users(ctx, opt(codersdk.UsersRequest{
|
||||
@ -1592,8 +1594,8 @@ func assertPagination(ctx context.Context, t *testing.T, client *codersdk.Client
|
||||
},
|
||||
}))
|
||||
require.NoError(t, err, "prev page")
|
||||
require.Equal(t, allUsers[count-limit:count], prevPage, "prev users")
|
||||
count += len(page)
|
||||
require.Equal(t, allUsers[count-limit:count], prevPage.Users, "prev users")
|
||||
count += len(page.Users)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,18 +47,9 @@ 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 GetUsersResponse struct {
|
||||
Users []User `json:"users"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type CreateFirstUserRequest struct {
|
||||
@ -324,7 +315,7 @@ func (c *Client) User(ctx context.Context, userIdent string) (User, error) {
|
||||
|
||||
// Users returns all users according to the request parameters. If no parameters are set,
|
||||
// the default behavior is to return all users in a single page.
|
||||
func (c *Client) Users(ctx context.Context, req UsersRequest) ([]User, error) {
|
||||
func (c *Client) Users(ctx context.Context, req UsersRequest) (GetUsersResponse, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, "/api/v2/users", nil,
|
||||
req.Pagination.asRequestOption(),
|
||||
func(r *http.Request) {
|
||||
@ -347,50 +338,16 @@ func (c *Client) Users(ctx context.Context, req UsersRequest) ([]User, error) {
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return []User{}, err
|
||||
return GetUsersResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return []User{}, readBodyAsError(res)
|
||||
return GetUsersResponse{}, readBodyAsError(res)
|
||||
}
|
||||
|
||||
var users []User
|
||||
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)
|
||||
var usersRes GetUsersResponse
|
||||
return usersRes, json.NewDecoder(res.Body).Decode(&usersRes)
|
||||
}
|
||||
|
||||
// OrganizationsByUser returns all organizations the user is a member of.
|
||||
|
@ -54,17 +54,17 @@ func groupEdit() *cobra.Command {
|
||||
req.AvatarURL = &avatarURL
|
||||
}
|
||||
|
||||
users, err := client.Users(ctx, codersdk.UsersRequest{})
|
||||
userRes, err := client.Users(ctx, codersdk.UsersRequest{})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get users: %w", err)
|
||||
}
|
||||
|
||||
req.AddUsers, err = convertToUserIDs(addUsers, users)
|
||||
req.AddUsers, err = convertToUserIDs(addUsers, userRes.Users)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("parse add-users: %w", err)
|
||||
}
|
||||
|
||||
req.RemoveUsers, err = convertToUserIDs(rmUsers, users)
|
||||
req.RemoveUsers, err = convertToUserIDs(rmUsers, userRes.Users)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("parse rm-users: %w", err)
|
||||
}
|
||||
|
@ -114,12 +114,12 @@ func TestScim(t *testing.T) {
|
||||
defer res.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
users, err := client.Users(ctx, codersdk.UsersRequest{Search: sUser.Emails[0].Value})
|
||||
userRes, err := client.Users(ctx, codersdk.UsersRequest{Search: sUser.Emails[0].Value})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, users, 1)
|
||||
require.Len(t, userRes.Users, 1)
|
||||
|
||||
assert.Equal(t, sUser.Emails[0].Value, users[0].Email)
|
||||
assert.Equal(t, sUser.UserName, users[0].Username)
|
||||
assert.Equal(t, sUser.Emails[0].Value, userRes.Users[0].Email)
|
||||
assert.Equal(t, sUser.UserName, userRes.Users[0].Username)
|
||||
})
|
||||
})
|
||||
|
||||
@ -194,10 +194,10 @@ func TestScim(t *testing.T) {
|
||||
defer res.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
users, err := client.Users(ctx, codersdk.UsersRequest{Search: sUser.Emails[0].Value})
|
||||
userRes, err := client.Users(ctx, codersdk.UsersRequest{Search: sUser.Emails[0].Value})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, users, 1)
|
||||
assert.Equal(t, codersdk.UserStatusSuspended, users[0].Status)
|
||||
require.Len(t, userRes.Users, 1)
|
||||
assert.Equal(t, codersdk.UserStatusSuspended, userRes.Users[0].Status)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -133,17 +133,9 @@ export const getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
|
||||
|
||||
export const getUsers = async (
|
||||
options: TypesGen.UsersRequest,
|
||||
): Promise<TypesGen.User[]> => {
|
||||
): Promise<TypesGen.GetUsersResponse> => {
|
||||
const url = getURLWithSearchParams("/api/v2/users", options)
|
||||
const response = await axios.get<TypesGen.User[]>(url.toString())
|
||||
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())
|
||||
const response = await axios.get<TypesGen.GetUsersResponse>(url.toString())
|
||||
return response.data
|
||||
}
|
||||
|
||||
|
@ -348,6 +348,12 @@ export interface GetAppHostResponse {
|
||||
readonly host: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
export interface GetUsersResponse {
|
||||
readonly users: User[]
|
||||
readonly count: number
|
||||
}
|
||||
|
||||
// From codersdk/deploymentconfig.go
|
||||
export interface GitAuthConfig {
|
||||
readonly id: string
|
||||
@ -753,16 +759,6 @@ 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[]
|
||||
|
@ -32,7 +32,9 @@ describe("CreateWorkspacePage", () => {
|
||||
})
|
||||
|
||||
it("succeeds with default owner", async () => {
|
||||
jest.spyOn(API, "getUsers").mockResolvedValueOnce([MockUser])
|
||||
jest
|
||||
.spyOn(API, "getUsers")
|
||||
.mockResolvedValueOnce({ users: [MockUser], count: 1 })
|
||||
jest
|
||||
.spyOn(API, "getWorkspaceQuota")
|
||||
.mockResolvedValueOnce(MockWorkspaceQuota)
|
||||
|
@ -199,9 +199,10 @@ describe("UsersPage", () => {
|
||||
|
||||
await suspendUser(() => {
|
||||
jest.spyOn(API, "suspendUser").mockResolvedValueOnce(MockUser)
|
||||
jest
|
||||
.spyOn(API, "getUsers")
|
||||
.mockResolvedValueOnce([SuspendedMockUser, MockUser2])
|
||||
jest.spyOn(API, "getUsers").mockResolvedValueOnce({
|
||||
users: [SuspendedMockUser, MockUser2],
|
||||
count: 2,
|
||||
})
|
||||
})
|
||||
|
||||
// Check if the success message is displayed
|
||||
@ -240,7 +241,7 @@ describe("UsersPage", () => {
|
||||
|
||||
const mock = jest
|
||||
.spyOn(API, "getUsers")
|
||||
.mockResolvedValueOnce([MockUser, MockUser2])
|
||||
.mockResolvedValueOnce({ users: [MockUser, MockUser2], count: 26 })
|
||||
|
||||
const nextButton = await screen.findByLabelText("Next page")
|
||||
expect(nextButton).toBeEnabled()
|
||||
@ -274,9 +275,10 @@ describe("UsersPage", () => {
|
||||
|
||||
await deleteUser(() => {
|
||||
jest.spyOn(API, "deleteUser").mockResolvedValueOnce(undefined)
|
||||
jest
|
||||
.spyOn(API, "getUsers")
|
||||
.mockResolvedValueOnce([MockUser, SuspendedMockUser])
|
||||
jest.spyOn(API, "getUsers").mockResolvedValueOnce({
|
||||
users: [MockUser, SuspendedMockUser],
|
||||
count: 2,
|
||||
})
|
||||
})
|
||||
|
||||
// Check if the success message is displayed
|
||||
@ -320,11 +322,12 @@ describe("UsersPage", () => {
|
||||
jest
|
||||
.spyOn(API, "activateUser")
|
||||
.mockResolvedValueOnce(SuspendedMockUser)
|
||||
jest
|
||||
.spyOn(API, "getUsers")
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve([MockUser, MockUser2, SuspendedMockUser]),
|
||||
)
|
||||
jest.spyOn(API, "getUsers").mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
users: [MockUser, MockUser2, SuspendedMockUser],
|
||||
count: 3,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
// Check if the success message is displayed
|
||||
|
@ -66,7 +66,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("users.gettingUsers") ||
|
||||
usersState.matches("gettingUsers") ||
|
||||
(canEditUsers && rolesState.matches("gettingRoles"))
|
||||
|
||||
// Fetch roles on component mount
|
||||
@ -130,7 +130,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
||||
})
|
||||
}}
|
||||
error={getUsersError}
|
||||
isUpdatingUserRoles={usersState.matches("users.updatingUserRoles")}
|
||||
isUpdatingUserRoles={usersState.matches("updatingUserRoles")}
|
||||
isLoading={isLoading}
|
||||
canEditUsers={canEditUsers}
|
||||
filter={usersState.context.filter}
|
||||
@ -143,10 +143,10 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
||||
|
||||
<DeleteDialog
|
||||
isOpen={
|
||||
usersState.matches("users.confirmUserDeletion") ||
|
||||
usersState.matches("users.deletingUser")
|
||||
usersState.matches("confirmUserDeletion") ||
|
||||
usersState.matches("deletingUser")
|
||||
}
|
||||
confirmLoading={usersState.matches("users.deletingUser")}
|
||||
confirmLoading={usersState.matches("deletingUser")}
|
||||
name={usernameToDelete ?? ""}
|
||||
entity="user"
|
||||
onConfirm={() => {
|
||||
@ -161,10 +161,10 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
||||
type="delete"
|
||||
hideCancel={false}
|
||||
open={
|
||||
usersState.matches("users.confirmUserSuspension") ||
|
||||
usersState.matches("users.suspendingUser")
|
||||
usersState.matches("confirmUserSuspension") ||
|
||||
usersState.matches("suspendingUser")
|
||||
}
|
||||
confirmLoading={usersState.matches("users.suspendingUser")}
|
||||
confirmLoading={usersState.matches("suspendingUser")}
|
||||
title={Language.suspendDialogTitle}
|
||||
confirmText={Language.suspendDialogAction}
|
||||
onConfirm={() => {
|
||||
@ -186,10 +186,10 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
||||
type="success"
|
||||
hideCancel={false}
|
||||
open={
|
||||
usersState.matches("users.confirmUserActivation") ||
|
||||
usersState.matches("users.activatingUser")
|
||||
usersState.matches("confirmUserActivation") ||
|
||||
usersState.matches("activatingUser")
|
||||
}
|
||||
confirmLoading={usersState.matches("users.activatingUser")}
|
||||
confirmLoading={usersState.matches("activatingUser")}
|
||||
title={Language.activateDialogTitle}
|
||||
confirmText={Language.activateDialogAction}
|
||||
onConfirm={() => {
|
||||
@ -210,10 +210,10 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => {
|
||||
{userIdToResetPassword && (
|
||||
<ResetPasswordDialog
|
||||
open={
|
||||
usersState.matches("users.confirmUserPasswordReset") ||
|
||||
usersState.matches("users.resettingUserPassword")
|
||||
usersState.matches("confirmUserPasswordReset") ||
|
||||
usersState.matches("resettingUserPassword")
|
||||
}
|
||||
loading={usersState.matches("users.resettingUserPassword")}
|
||||
loading={usersState.matches("resettingUserPassword")}
|
||||
user={getSelectedUser(userIdToResetPassword, users)}
|
||||
newPassword={newUserPassword}
|
||||
onClose={() => {
|
||||
|
@ -83,10 +83,6 @@ 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",
|
||||
|
@ -71,12 +71,12 @@ export const handlers = [
|
||||
rest.get("/api/v2/users", async (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json([M.MockUser, M.MockUser2, M.SuspendedMockUser]),
|
||||
ctx.json({
|
||||
users: [M.MockUser, M.MockUser2, M.SuspendedMockUser],
|
||||
count: 26,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
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,14 +57,17 @@ export const searchUsersAndGroupsMachine = createMachine(
|
||||
{
|
||||
services: {
|
||||
search: async ({ organizationId }, { query }) => {
|
||||
const [users, groups] = await Promise.all([
|
||||
const [userRes, groups] = await Promise.all([
|
||||
getUsers(queryToFilter(query)),
|
||||
getGroups(organizationId),
|
||||
])
|
||||
|
||||
// The Everyone groups is not returned by the API so we have to add it
|
||||
// manually
|
||||
return { users, groups: [everyOneGroup(organizationId), ...groups] }
|
||||
return {
|
||||
users: userRes.users,
|
||||
groups: [everyOneGroup(organizationId), ...groups],
|
||||
}
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
|
@ -50,7 +50,10 @@ export const searchUserMachine = createMachine(
|
||||
},
|
||||
{
|
||||
services: {
|
||||
searchUsers: (_, { query }) => getUsers(queryToFilter(query)),
|
||||
searchUsers: async (_, { query }) =>
|
||||
await (
|
||||
await getUsers(queryToFilter(query))
|
||||
).users,
|
||||
},
|
||||
actions: {
|
||||
assignSearchResults: assign({
|
||||
|
@ -103,7 +103,7 @@ export type UsersEvent =
|
||||
| { type: "UPDATE_PAGE"; page: string }
|
||||
|
||||
export const usersMachine =
|
||||
/** @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 */
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QFdZgE6wMoBcCGOYAdLPujgJYB2UACnlNQRQPZUDEA2gAwC6ioAA4tYFSmwEgAnogCM3AJzciAZm4AmAKwAOAOwA2TQt0AWfQBoQAD0QBaWbIWyimzepPqtCpd1n6Avv6WqBjY+IREMDiUNACqaJjsEGzE1ABuLADWxFHxoTz8SCDCouJUkjIIsuoq+kRmmrreJtqertqWNgj2stoKRLq13upOspq+AUEgIZi4BDlg0dRQeYkY6CzoRIIANgQAZpsAtpGLq7AFkiVirOVFXT19A0MKIw7jfpaVDtra9YZNBQtNo6QLBBJheZECgQHZgdhYWJYWgAUQAcgARAD6SJRACVLkVrmVJA8HMYBkCFCptH4Rvp9CYvnI1P1qbJTL1DNx9NoVGDphC5hEYXD2BiUQAZFEAFRROKw+MJQhENwk9zs5N0lJM1Np+npjOZVRpKlUxl5HhMul+3G0ApmkJFsPhAEEAMIygCSADVXXKFUq+FdVSSNd0tTq9XSFAymdI5CZNHVtPpauoGWp9NT+VNHcLUi72HiUYqZYG8VjaK6sFgAOoAeTxGOVxVDt1Jmsc2qauppMbjxrGalUI2TSlkKnGoLzQvChbFsVoGP98txlbxDelWFbxI74Z6FN70YNsaNCaq+kcRDTThUfjPDnUDrnUNF8KXK4D1YA4ijd+26qgGS3ZRv2p6DheD5mvI4ymNwvY1LoL6hAW0JFp+q5YgAYl6kpygSwZEoBdzAV2R5UuBhrxt8Bh1I08h+GYnjpihszzkQADGbD7BQ6BHKsWCoIIYBUKIbDsO6DZorheIALIVliiLIuiWBetJAGlPuZERuo3AmEQfiaCoCipoMnj6UO2iuEQtqNE4SYqCYHJsU6xDcVQvH8YJwmieJHDuq6aLulKinKaiaJqRpREqlpQHWJqekGUZJlmSoFk0XISjqDeuieK81nNJOrloR5XkCQkGJgHCZSSdJskKeuWIStKcqaWqpEJbpHJEIoV5+LIznAkOVL-CY3A8o0SbOSVHFlXxFUYFVNW3JJQUhZKiktbK-4xW2cWdWS6g9X1DhXkNrQjbGRBaI0enqH0g3cJos1QvN3kJK6nGUGkzASVJMlevJiket6fretFhSxR1nbdcoagtBoxgTQomiZVUo1mONk2mGjsivRE72LegX0-X9AXraFTWg76-rqWi7Vhjp9jHfD+naEjugo2jV11LdeUaI940vbOqEcbAvlUBAyyrEkKTQlQGTZCQksQKsjPaVBPguKmg3UgyD0KJ0Xb6MovhDPZNRXgTxAS7AIlSzLCTsOsmzbHsOCHPxKv26JasJBr8UgabvWTtmlu1LIVluDr536wafQ20QEDVYsTsYHLVCpIrWTECnNVgOre17kHXa6JoN2xn4jKoy0Fha6YvVuIoHK6oxIvgmLUL52ncTO67Wy7AcxzJ6nhBF1D+0wweDjl5XV5xrXqbGkl2qaL03BOZOjiDUneDfRQv0xCszvJFnCtK8Q+9k+PAfFyRsM9IyN6DQ4NJ8r0k4jY3z16U4phOF8B3QUXcIjX0PswPuGcB7u2Ht7cBR9C530niXQ6JsDLnTfr8e8tIVAr30mvDeW97wOSTkTVY9BYCwAAO6bAgHiOAiw6qA2Bk1astZGzNixCWMsgc0ERl6P0deKNrSfw8OoFeYdbLqHLrGWkHwFBkJ4gtCheAqG0PQPQxhOA1rBSpoqSs7D6xNmxDw2UfDH4-CEfIRQojaTiMkXyF+7Jf5V0UaLdiUJ0DaOPqo9RdDM7Z0vkQbxaAcB+JoXQixRRKhaH0k3QYNonCowcPGECvQbz6AMA4bsaYTAmCTqExYviEiUMiZol26ANiDw9l7E4RTwmlLUeUiA0TmY5L+AybJWo8no3cMmBJKgkmOHXrvDxbkiDIEEBASBJ8MB4hYHCWAgSL650mdM+YqwFlLLaV1Ho94BhNF+MZLJrg0wr0aGvGo6VJwaHSgU8ZaEpkzJKfMxZcBKnVNgZ7EezzNkJG2XAXZIEDm6COdZWo5dkx4IvBmcYRBTKvDTLydetQHmd08YQdgmEAy4XwkGFBD8Yl2F0M4IYTkHqb1RuMDoXUzBmyaPpPJpyVCssCFMKgLAU7wCKPmcWZBj70EYFQcmIYDqWPyc4XUCErx8ncEORQbMtB6EMMYMwScoivMwGK6e7TJX1CUNkuV6Nqi1H+PZIErRbr2keRxd8OqmZ7MGs5A1Mrej3KHKlXq+UHrv2nDajFEzyEJCEr7MSmtUESpddKo1HqoJo2Srrcax127AL5W9ZRH0lpjwjUSvV0bDWyrjd8A2CLdbPXeBzBCSjPIqM+gfI+ubxUz31TGot8qoI2mcDaK8Mj3DyHyQGkBmLbaq3TugB1msyStsLe6jt3xtY6DjrUBO7jA1oR7lqydpcIwzrdca-BBpDImBuXlIZNItB7wbbM1Y27+H2D3bG+diBPA1AGBNB6wjtDWgNDW8qESNFaLCXeqNUrZ0HthTSCuWhOTjUGnlX9tqvE+PHWUwDIGW0Fv3cWl94wDLPUSY9FJYz10cT+VqwFPLoaOunVhp9fSGQV3OnBVojRzo2ww3qp4ba53o3sCHLeRhUWm0GKmdl-ggA */
|
||||
createMachine(
|
||||
{
|
||||
tsTypes: {} as import("./usersXService.typegen").Typegen0,
|
||||
@ -112,7 +112,7 @@ export const usersMachine =
|
||||
events: {} as UsersEvent,
|
||||
services: {} as {
|
||||
getUsers: {
|
||||
data: TypesGen.User[]
|
||||
data: TypesGen.GetUsersResponse
|
||||
}
|
||||
createUser: {
|
||||
data: TypesGen.User
|
||||
@ -132,261 +132,225 @@ export const usersMachine =
|
||||
updateUserRoles: {
|
||||
data: TypesGen.User
|
||||
}
|
||||
getUserCount: {
|
||||
data: TypesGen.UserCountResponse
|
||||
}
|
||||
},
|
||||
},
|
||||
predictableActionArguments: true,
|
||||
id: "usersState",
|
||||
type: "parallel",
|
||||
on: {
|
||||
UPDATE_FILTER: {
|
||||
actions: ["assignFilter", "sendResetPage"],
|
||||
internal: false,
|
||||
},
|
||||
UPDATE_PAGE: {
|
||||
target: "gettingUsers",
|
||||
actions: "updateURL",
|
||||
},
|
||||
},
|
||||
initial: "startingPagination",
|
||||
states: {
|
||||
count: {
|
||||
initial: "gettingCount",
|
||||
states: {
|
||||
idle: {},
|
||||
gettingCount: {
|
||||
entry: "clearGetCountError",
|
||||
invoke: {
|
||||
src: "getUserCount",
|
||||
id: "getUserCount",
|
||||
onDone: [
|
||||
{
|
||||
target: "idle",
|
||||
actions: "assignCount",
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: "idle",
|
||||
actions: "assignGetCountError",
|
||||
},
|
||||
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",
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
tags: "loading",
|
||||
},
|
||||
idle: {
|
||||
entry: "clearSelectedUser",
|
||||
on: {
|
||||
UPDATE_FILTER: {
|
||||
target: ".gettingCount",
|
||||
actions: ["assignFilter", "sendResetPage"],
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
initial: "startingPagination",
|
||||
states: {
|
||||
startingPagination: {
|
||||
entry: "assignPaginationRef",
|
||||
always: {
|
||||
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",
|
||||
},
|
||||
},
|
||||
gettingUsers: {
|
||||
entry: "clearGetUsersError",
|
||||
invoke: {
|
||||
src: "getUsers",
|
||||
id: "getUsers",
|
||||
onDone: [
|
||||
{
|
||||
target: "idle",
|
||||
actions: "assignUsers",
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: "idle",
|
||||
actions: [
|
||||
"clearUsers",
|
||||
"assignGetUsersError",
|
||||
"displayGetUsersErrorMessage",
|
||||
],
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: "idle",
|
||||
actions: [
|
||||
"assignSuspendUserError",
|
||||
"displaySuspendedErrorMessage",
|
||||
],
|
||||
},
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
deletingUser: {
|
||||
entry: "clearDeleteUserError",
|
||||
invoke: {
|
||||
src: "deleteUser",
|
||||
id: "deleteUser",
|
||||
onDone: [
|
||||
{
|
||||
target: "gettingUsers",
|
||||
actions: "displayDeleteSuccess",
|
||||
},
|
||||
},
|
||||
confirmUserSuspension: {
|
||||
on: {
|
||||
CONFIRM_USER_SUSPENSION: {
|
||||
target: "suspendingUser",
|
||||
},
|
||||
CANCEL_USER_SUSPENSION: {
|
||||
target: "idle",
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: "idle",
|
||||
actions: ["assignDeleteUserError", "displayDeleteErrorMessage"],
|
||||
},
|
||||
},
|
||||
confirmUserDeletion: {
|
||||
on: {
|
||||
CONFIRM_USER_DELETE: {
|
||||
target: "deletingUser",
|
||||
},
|
||||
CANCEL_USER_DELETE: {
|
||||
target: "idle",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
activatingUser: {
|
||||
entry: "clearActivateUserError",
|
||||
invoke: {
|
||||
src: "activateUser",
|
||||
id: "activateUser",
|
||||
onDone: [
|
||||
{
|
||||
target: "gettingUsers",
|
||||
actions: "displayActivateSuccess",
|
||||
},
|
||||
},
|
||||
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",
|
||||
],
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: "idle",
|
||||
actions: [
|
||||
"assignActivateUserError",
|
||||
"displayActivatedErrorMessage",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
confirmUserPasswordReset: {
|
||||
on: {
|
||||
CONFIRM_USER_PASSWORD_RESET: {
|
||||
target: "resettingUserPassword",
|
||||
},
|
||||
deletingUser: {
|
||||
entry: "clearDeleteUserError",
|
||||
invoke: {
|
||||
src: "deleteUser",
|
||||
id: "deleteUser",
|
||||
onDone: [
|
||||
{
|
||||
target: "gettingUsers",
|
||||
actions: "displayDeleteSuccess",
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: "idle",
|
||||
actions: [
|
||||
"assignDeleteUserError",
|
||||
"displayDeleteErrorMessage",
|
||||
],
|
||||
},
|
||||
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",
|
||||
],
|
||||
},
|
||||
},
|
||||
activatingUser: {
|
||||
entry: "clearActivateUserError",
|
||||
invoke: {
|
||||
src: "activateUser",
|
||||
id: "activateUser",
|
||||
onDone: [
|
||||
{
|
||||
target: "gettingUsers",
|
||||
actions: "displayActivateSuccess",
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: "idle",
|
||||
actions: [
|
||||
"assignActivateUserError",
|
||||
"displayActivatedErrorMessage",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
updatingUserRoles: {
|
||||
entry: "clearUpdateUserRolesError",
|
||||
invoke: {
|
||||
src: "updateUserRoles",
|
||||
id: "updateUserRoles",
|
||||
onDone: [
|
||||
{
|
||||
target: "idle",
|
||||
actions: "updateUserRolesInTheList",
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: "idle",
|
||||
actions: [
|
||||
"assignUpdateRolesError",
|
||||
"displayUpdateRolesErrorMessage",
|
||||
],
|
||||
},
|
||||
},
|
||||
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",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -404,9 +368,6 @@ export const usersMachine =
|
||||
limit,
|
||||
})
|
||||
},
|
||||
getUserCount: (context) => {
|
||||
return API.getUserCount(queryToFilter(context.filter))
|
||||
},
|
||||
suspendUser: (context) => {
|
||||
if (!context.userIdToSuspend) {
|
||||
throw new Error("userIdToSuspend is undefined")
|
||||
@ -462,17 +423,9 @@ export const usersMachine =
|
||||
userIdToUpdateRoles: (_) => undefined,
|
||||
}),
|
||||
assignUsers: assign({
|
||||
users: (_, event) => event.data,
|
||||
}),
|
||||
assignCount: assign({
|
||||
users: (_, event) => event.data.users,
|
||||
count: (_, event) => event.data.count,
|
||||
}),
|
||||
assignGetCountError: assign({
|
||||
getCountError: (_, event) => event.data,
|
||||
}),
|
||||
clearGetCountError: assign({
|
||||
getCountError: (_) => undefined,
|
||||
}),
|
||||
assignFilter: assign({
|
||||
filter: (_, event) => event.query,
|
||||
}),
|
||||
|
Reference in New Issue
Block a user