Skip to content

feat: add filter by status on GET /users #1206

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions coderd/database/databasefake/databasefake.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,16 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
users = tmp
}

if params.Status != "" {
usersFilteredByStatus := make([]database.User, 0, len(users))
for i, user := range users {
if params.Status == string(user.Status) {
usersFilteredByStatus = append(usersFilteredByStatus, users[i])
}
}
users = usersFilteredByStatus
}

if params.OffsetOpt > 0 {
if int(params.OffsetOpt) > len(users)-1 {
return []database.User{}, nil
Expand All @@ -225,6 +235,7 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
}
users = users[:params.LimitOpt]
}

tmp := make([]database.User, len(users))
copy(tmp, users)

Expand Down
18 changes: 16 additions & 2 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions coderd/database/queries/users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,25 @@ WHERE
)
ELSE true
END
-- Start filters
-- Filter by name, email or username
AND CASE
WHEN @search :: text != '' THEN (
email LIKE concat('%', @search, '%')
OR username LIKE concat('%', @search, '%')
)
ELSE true
END
-- Filter by status
AND CASE
-- @status needs to be a text because it can be empty, If it was
-- user_status enum, it would not.
WHEN @status :: text != '' THEN (
status = @status :: user_status
)
ELSE true
END
-- End of filters
ORDER BY
-- Deterministic and consistent ordering of all users, even if they share
-- a timestamp. This is to ensure consistent pagination.
Expand Down
10 changes: 6 additions & 4 deletions coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,11 @@ func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) {

func (api *api) users(rw http.ResponseWriter, r *http.Request) {
var (
afterArg = r.URL.Query().Get("after_user")
limitArg = r.URL.Query().Get("limit")
offsetArg = r.URL.Query().Get("offset")
searchName = r.URL.Query().Get("search")
afterArg = r.URL.Query().Get("after_user")
limitArg = r.URL.Query().Get("limit")
offsetArg = r.URL.Query().Get("offset")
searchName = r.URL.Query().Get("search")
statusFilter = r.URL.Query().Get("status")
)

// createdAfter is a user uuid.
Expand Down Expand Up @@ -136,6 +137,7 @@ func (api *api) users(rw http.ResponseWriter, r *http.Request) {
OffsetOpt: int32(offset),
LimitOpt: int32(pageLimit),
Search: searchName,
Status: statusFilter,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Expand Down
59 changes: 47 additions & 12 deletions coderd/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,19 +330,54 @@ func TestUserByName(t *testing.T) {

func TestGetUsers(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
client.CreateUser(context.Background(), codersdk.CreateUserRequest{
Email: "alice@email.com",
Username: "alice",
Password: "password",
OrganizationID: user.OrganizationID,
t.Run("AllUsers", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
client.CreateUser(context.Background(), codersdk.CreateUserRequest{
Email: "alice@email.com",
Username: "alice",
Password: "password",
OrganizationID: user.OrganizationID,
})
// No params is all users
users, err := client.Users(context.Background(), codersdk.UsersRequest{})
require.NoError(t, err)
require.Len(t, users, 2)
require.Len(t, users[0].OrganizationIDs, 1)
})
t.Run("ActiveUsers", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
first := coderdtest.CreateFirstUser(t, client)
active := make([]codersdk.User, 0)
alice, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
Email: "alice@email.com",
Username: "alice",
Password: "password",
OrganizationID: first.OrganizationID,
})
require.NoError(t, err)
active = append(active, alice)

bruno, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
Email: "bruno@email.com",
Username: "bruno",
Password: "password",
OrganizationID: first.OrganizationID,
})
require.NoError(t, err)
active = append(active, bruno)

_, err = client.SuspendUser(context.Background(), first.UserID)
require.NoError(t, err)

users, err := client.Users(context.Background(), codersdk.UsersRequest{
Status: string(codersdk.UserStatusActive),
})
require.NoError(t, err)
require.ElementsMatch(t, active, users)
})
// No params is all users
users, err := client.Users(context.Background(), codersdk.UsersRequest{})
require.NoError(t, err)
require.Len(t, users, 2)
require.Len(t, users[0].OrganizationIDs, 1)
}

func TestOrganizationsByUser(t *testing.T) {
Expand Down
18 changes: 11 additions & 7 deletions codersdk/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import (
// Me is used as a replacement for your own ID.
var Me = uuid.Nil

type UserStatus string

const (
UserStatusActive UserStatus = "active"
UserStatusSuspended UserStatus = "suspended"
)

type UsersRequest struct {
AfterUser uuid.UUID `json:"after_user"`
Search string `json:"search"`
Expand All @@ -26,15 +33,10 @@ type UsersRequest struct {
// To get the next page, use offset=<limit>*<page_number>.
// Offset is 0 indexed, so the first record sits at offset 0.
Offset int `json:"offset"`
// Filter users by status
Status string `json:"status"`
}

type UserStatus string

const (
UserStatusActive UserStatus = "active"
UserStatusSuspended UserStatus = "suspended"
)

// User represents a user in Coder.
type User struct {
ID uuid.UUID `json:"id" validate:"required"`
Expand Down Expand Up @@ -165,6 +167,7 @@ func (c *Client) SuspendUser(ctx context.Context, userID uuid.UUID) (User, error
if res.StatusCode != http.StatusOK {
return User{}, readBodyAsError(res)
}

var user User
return user, json.NewDecoder(res.Body).Decode(&user)
}
Expand Down Expand Up @@ -243,6 +246,7 @@ func (c *Client) Users(ctx context.Context, req UsersRequest) ([]User, error) {
}
q.Set("offset", strconv.Itoa(req.Offset))
q.Set("search", req.Search)
q.Set("status", req.Status)
r.URL.RawQuery = q.Encode()
})
if err != nil {
Expand Down
25 changes: 13 additions & 12 deletions site/src/api/typesGenerated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface AgentGitSSHKey {
readonly private_key: string
}

// From codersdk/users.go:94:6
// From codersdk/users.go:96:6
export interface AuthMethods {
readonly password: boolean
readonly github: boolean
Expand All @@ -30,21 +30,21 @@ export interface BuildInfoResponse {
readonly version: string
}

// From codersdk/users.go:48:6
// From codersdk/users.go:50:6
export interface CreateFirstUserRequest {
readonly email: string
readonly username: string
readonly password: string
readonly organization: string
}

// From codersdk/users.go:56:6
// From codersdk/users.go:58:6
export interface CreateFirstUserResponse {
readonly user_id: string
readonly organization_id: string
}

// From codersdk/users.go:89:6
// From codersdk/users.go:91:6
export interface CreateOrganizationRequest {
readonly name: string
}
Expand Down Expand Up @@ -77,7 +77,7 @@ export interface CreateTemplateVersionRequest {
readonly parameter_values: CreateParameterRequest[]
}

// From codersdk/users.go:61:6
// From codersdk/users.go:63:6
export interface CreateUserRequest {
readonly email: string
readonly username: string
Expand All @@ -100,7 +100,7 @@ export interface CreateWorkspaceRequest {
readonly parameter_values: CreateParameterRequest[]
}

// From codersdk/users.go:85:6
// From codersdk/users.go:87:6
export interface GenerateAPIKeyResponse {
readonly key: string
}
Expand All @@ -118,13 +118,13 @@ export interface GoogleInstanceIdentityToken {
readonly json_web_token: string
}

// From codersdk/users.go:74:6
// From codersdk/users.go:76:6
export interface LoginWithPasswordRequest {
readonly email: string
readonly password: string
}

// From codersdk/users.go:80:6
// From codersdk/users.go:82:6
export interface LoginWithPasswordResponse {
readonly session_token: string
}
Expand Down Expand Up @@ -245,7 +245,7 @@ export interface UpdateActiveTemplateVersion {
readonly id: string
}

// From codersdk/users.go:68:6
// From codersdk/users.go:70:6
export interface UpdateUserProfileRequest {
readonly email: string
readonly username: string
Expand All @@ -266,7 +266,7 @@ export interface UploadResponse {
readonly hash: string
}

// From codersdk/users.go:39:6
// From codersdk/users.go:41:6
export interface User {
readonly id: string
readonly email: string
Expand All @@ -276,12 +276,13 @@ export interface User {
readonly organization_ids: string[]
}

// From codersdk/users.go:17:6
// From codersdk/users.go:24:6
export interface UsersRequest {
readonly after_user: string
readonly search: string
readonly limit: number
readonly offset: number
readonly status: string
}

// From codersdk/workspaces.go:18:6
Expand Down Expand Up @@ -378,7 +379,7 @@ export type ParameterScope = "organization" | "template" | "user" | "workspace"
// From codersdk/provisionerdaemons.go:26:6
export type ProvisionerJobStatus = "canceled" | "canceling" | "failed" | "pending" | "running" | "succeeded"

// From codersdk/users.go:31:6
// From codersdk/users.go:17:6
export type UserStatus = "active" | "suspended"

// From codersdk/workspaceresources.go:15:6
Expand Down