diff --git a/cli/list.go b/cli/list.go index c06ed5922de25..3afd193dd8aa8 100644 --- a/cli/list.go +++ b/cli/list.go @@ -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 } diff --git a/cli/userlist.go b/cli/userlist.go index b9e9116791c61..1f78854583566 100644 --- a/cli/userlist.go +++ b/cli/userlist.go @@ -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) } diff --git a/coderd/coderd.go b/coderd/coderd.go index 5d9497457783f..197eb261ddaf8 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -437,7 +437,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) { diff --git a/coderd/coderdtest/authorize.go b/coderd/coderdtest/authorize.go index 0443e654ed1cb..14cf2d5514f6e 100644 --- a/coderd/coderdtest/authorize.go +++ b/coderd/coderdtest/authorize.go @@ -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 diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 9d90e182b2a76..0fb64e4141e67 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -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) { diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 523d6e0afea25..b9e9bc57e5626 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -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 +} diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 7ace117d404d0..42029e7d84d1b 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -91,7 +91,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! diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 5fef1c7149cb8..ce3c3c448df55 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4114,7 +4114,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 @@ -4183,7 +4183,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, @@ -4197,9 +4213,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, @@ -4213,6 +4229,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]User, &i.AvatarURL, &i.Deleted, &i.LastSeenAt, + &i.Count, ); err != nil { return nil, err } diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index 2c42aaa5f4188..2655b6b713fa2 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -128,7 +128,7 @@ WHERE -- name: GetUsers :many SELECT - * + *, COUNT(*) OVER() AS count FROM users WHERE diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 0fce2b7f8f92c..3a9b4f34f4b35 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -344,10 +344,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 { diff --git a/coderd/users.go b/coderd/users.go index 91a2512b6ebc9..5733981e98cbd 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -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), }) } diff --git a/coderd/users_test.go b/coderd/users_test.go index c16181562d567..ba6de76659458 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -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)) - }) - t.Run("ActiveUsers", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, client) + _, err := client.User(ctx, first.UserID.String()) + require.NoError(t, err, "") - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + _, err = client.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "alice@email.com", + Username: "alice", + Password: "password", + OrganizationID: first.OrganizationID, + }) + require.NoError(t, err) - _, err := client.User(ctx, first.UserID.String()) - require.NoError(t, err, "") + res, err := client.Users(ctx, codersdk.UsersRequest{}) + require.NoError(t, err) + require.Len(t, res.Users, 2) + require.Equal(t, res.Count, 2) - // 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) + 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) - _, err = client.UpdateUserStatus(ctx, alice.Username, codersdk.UserStatusSuspended) - require.NoError(t, err) + 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) - response, err := client.UserCount(ctx, codersdk.UserCountRequest{ - Status: codersdk.UserStatusActive, - }) - require.NoError(t, err) - require.Equal(t, 1, int(response.Count)) + // 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) } } diff --git a/codersdk/users.go b/codersdk/users.go index c2162a6afd18f..d1ea338410826 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -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 - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return []User{}, 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 + return GetUsersResponse{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return UserCountResponse{}, readBodyAsError(res) + return GetUsersResponse{}, 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. diff --git a/enterprise/cli/groupedit.go b/enterprise/cli/groupedit.go index d2881e98a6afb..fb7f69342f342 100644 --- a/enterprise/cli/groupedit.go +++ b/enterprise/cli/groupedit.go @@ -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) } diff --git a/enterprise/coderd/scim_test.go b/enterprise/coderd/scim_test.go index cf777112faead..79fd0c533ad0e 100644 --- a/enterprise/coderd/scim_test.go +++ b/enterprise/coderd/scim_test.go @@ -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) }) }) } diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 77415b3f8f4b3..8c81c575e4dd1 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -133,17 +133,9 @@ export const getApiKey = async (): Promise => { export const getUsers = async ( options: TypesGen.UsersRequest, -): Promise => { +): Promise => { const url = getURLWithSearchParams("/api/v2/users", options) - const response = await axios.get(url.toString()) - return response.data -} - -export const getUserCount = async ( - options: TypesGen.UserCountRequest, -): Promise => { - const url = getURLWithSearchParams("/api/v2/users/count", options) - const response = await axios.get(url.toString()) + const response = await axios.get(url.toString()) return response.data } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index cb4e186e4d6dc..cce2e50abdd3a 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -347,6 +347,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 @@ -748,16 +754,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[] diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index 8b9f5bdf575e5..820817b2c6d69 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -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) diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx index 7907cc598f923..f0e4fb35b7de9 100644 --- a/site/src/pages/UsersPage/UsersPage.test.tsx +++ b/site/src/pages/UsersPage/UsersPage.test.tsx @@ -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 diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index f7c7e252fdc59..cfb4200b3bef7 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -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 }> = () => { { @@ -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 && ( { diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index f70079a6b1f4c..980f4e0b7287a 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -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", diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index defd8871ce0ce..b195627ce818b 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -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])) }), diff --git a/site/src/xServices/template/searchUsersAndGroupsXService.ts b/site/src/xServices/template/searchUsersAndGroupsXService.ts index ffcac8d3be9ba..5e335c2662444 100644 --- a/site/src/xServices/template/searchUsersAndGroupsXService.ts +++ b/site/src/xServices/template/searchUsersAndGroupsXService.ts @@ -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: { diff --git a/site/src/xServices/users/searchUserXService.ts b/site/src/xServices/users/searchUserXService.ts index 457f0dbd0fe17..4840126895b13 100644 --- a/site/src/xServices/users/searchUserXService.ts +++ b/site/src/xServices/users/searchUserXService.ts @@ -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({ diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index 8c429c82501bb..b6462e15d1f57 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -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: { - target: "gettingUsers", - }, + confirmUserSuspension: { + on: { + CONFIRM_USER_SUSPENSION: { + target: "suspendingUser", }, - gettingUsers: { - entry: "clearGetUsersError", - invoke: { - src: "getUsers", - id: "getUsers", - onDone: [ - { - target: "idle", - actions: "assignUsers", - }, - ], - onError: [ - { - target: "idle", - actions: [ - "clearUsers", - "assignGetUsersError", - "displayGetUsersErrorMessage", - ], - }, - ], - }, - tags: "loading", + CANCEL_USER_SUSPENSION: { + target: "idle", }, - 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", - }, - }, + }, + }, + confirmUserDeletion: { + on: { + CONFIRM_USER_DELETE: { + target: "deletingUser", }, - confirmUserSuspension: { - on: { - CONFIRM_USER_SUSPENSION: { - target: "suspendingUser", - }, - CANCEL_USER_SUSPENSION: { - target: "idle", - }, - }, + CANCEL_USER_DELETE: { + target: "idle", }, - confirmUserDeletion: { - on: { - CONFIRM_USER_DELETE: { - target: "deletingUser", - }, - CANCEL_USER_DELETE: { - target: "idle", - }, - }, + }, + }, + confirmUserActivation: { + on: { + CONFIRM_USER_ACTIVATION: { + target: "activatingUser", }, - confirmUserActivation: { - on: { - CONFIRM_USER_ACTIVATION: { - target: "activatingUser", - }, - CANCEL_USER_ACTIVATION: { - target: "idle", - }, - }, + CANCEL_USER_ACTIVATION: { + target: "idle", }, - suspendingUser: { - entry: "clearSuspendUserError", - invoke: { - src: "suspendUser", - id: "suspendUser", - onDone: [ - { - target: "gettingUsers", - actions: "displaySuspendSuccess", - }, - ], - onError: [ - { - target: "idle", - actions: [ - "assignSuspendUserError", - "displaySuspendedErrorMessage", - ], - }, - ], + }, + }, + suspendingUser: { + entry: "clearSuspendUserError", + invoke: { + src: "suspendUser", + id: "suspendUser", + onDone: [ + { + target: "gettingUsers", + actions: "displaySuspendSuccess", }, - }, - deletingUser: { - entry: "clearDeleteUserError", - invoke: { - src: "deleteUser", - id: "deleteUser", - onDone: [ - { - target: "gettingUsers", - actions: "displayDeleteSuccess", - }, - ], - onError: [ - { - target: "idle", - actions: [ - "assignDeleteUserError", - "displayDeleteErrorMessage", - ], - }, + ], + onError: [ + { + target: "idle", + actions: [ + "assignSuspendUserError", + "displaySuspendedErrorMessage", ], }, - }, - activatingUser: { - entry: "clearActivateUserError", - invoke: { - src: "activateUser", - id: "activateUser", - onDone: [ - { - target: "gettingUsers", - actions: "displayActivateSuccess", - }, - ], - onError: [ - { - target: "idle", - actions: [ - "assignActivateUserError", - "displayActivatedErrorMessage", - ], - }, - ], + ], + }, + }, + deletingUser: { + entry: "clearDeleteUserError", + invoke: { + src: "deleteUser", + id: "deleteUser", + onDone: [ + { + target: "gettingUsers", + actions: "displayDeleteSuccess", }, - }, - confirmUserPasswordReset: { - on: { - CONFIRM_USER_PASSWORD_RESET: { - target: "resettingUserPassword", - }, - CANCEL_USER_PASSWORD_RESET: { - target: "idle", - }, + ], + onError: [ + { + target: "idle", + actions: ["assignDeleteUserError", "displayDeleteErrorMessage"], }, - }, - 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", ], }, + ], + }, + }, + confirmUserPasswordReset: { + on: { + CONFIRM_USER_PASSWORD_RESET: { + target: "resettingUserPassword", + }, + CANCEL_USER_PASSWORD_RESET: { + target: "idle", }, - updatingUserRoles: { - entry: "clearUpdateUserRolesError", - invoke: { - src: "updateUserRoles", - id: "updateUserRoles", - onDone: [ - { - target: "idle", - actions: "updateUserRolesInTheList", - }, + }, + }, + resettingUserPassword: { + entry: "clearResetUserPasswordError", + invoke: { + src: "resetUserPassword", + id: "resetUserPassword", + onDone: [ + { + target: "idle", + actions: "displayResetPasswordSuccess", + }, + ], + onError: [ + { + target: "idle", + actions: [ + "assignResetUserPasswordError", + "displayResetPasswordErrorMessage", ], - onError: [ - { - target: "idle", - actions: [ - "assignUpdateRolesError", - "displayUpdateRolesErrorMessage", - ], - }, + }, + ], + }, + }, + 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, }),