feat: Single query for all workspaces with optional filter (#1537)

* feat: Add single query for all workspaces using a filter
This commit is contained in:
Steven Masley
2022-05-18 10:09:07 -05:00
committed by GitHub
parent 894646cb7c
commit a3556b12da
16 changed files with 254 additions and 196 deletions

View File

@ -29,7 +29,7 @@ func list() *cobra.Command {
if err != nil {
return err
}
workspaces, err := client.WorkspacesByUser(cmd.Context(), codersdk.Me)
workspaces, err := client.Workspaces(cmd.Context(), codersdk.WorkspaceFilter{})
if err != nil {
return err
}

View File

@ -253,7 +253,6 @@ func New(options *Options) (http.Handler, func()) {
})
r.Get("/gitsshkey", api.gitSSHKey)
r.Put("/gitsshkey", api.regenerateGitSSHKey)
r.Get("/workspaces", api.workspacesByUser)
})
})
})
@ -289,23 +288,28 @@ func New(options *Options) (http.Handler, func()) {
)
r.Get("/", api.workspaceResource)
})
r.Route("/workspaces/{workspace}", func(r chi.Router) {
r.Route("/workspaces", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
authRolesMiddleware,
httpmw.ExtractWorkspaceParam(options.Database),
)
r.Get("/", api.workspace)
r.Route("/builds", func(r chi.Router) {
r.Get("/", api.workspaceBuilds)
r.Post("/", api.postWorkspaceBuilds)
r.Get("/{workspacebuildname}", api.workspaceBuildByName)
})
r.Route("/autostart", func(r chi.Router) {
r.Put("/", api.putWorkspaceAutostart)
})
r.Route("/autostop", func(r chi.Router) {
r.Put("/", api.putWorkspaceAutostop)
r.Get("/", api.workspaces)
r.Route("/{workspace}", func(r chi.Router) {
r.Use(
httpmw.ExtractWorkspaceParam(options.Database),
)
r.Get("/", api.workspace)
r.Route("/builds", func(r chi.Router) {
r.Get("/", api.workspaceBuilds)
r.Post("/", api.postWorkspaceBuilds)
r.Get("/{workspacebuildname}", api.workspaceBuildByName)
})
r.Route("/autostart", func(r chi.Router) {
r.Put("/", api.putWorkspaceAutostart)
})
r.Route("/autostop", func(r chi.Router) {
r.Put("/", api.putWorkspaceAutostop)
})
})
})
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {

View File

@ -78,7 +78,6 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
"GET:/api/v2/workspaceagents/me/metadata": {NoAuthorize: true},
"GET:/api/v2/workspaceagents/me/turn": {NoAuthorize: true},
"GET:/api/v2/workspaceagents/{workspaceagent}": {NoAuthorize: true},
"GET:/api/v2/workspaceagents/{workspaceagent}/": {NoAuthorize: true},
"GET:/api/v2/workspaceagents/{workspaceagent}/dial": {NoAuthorize: true},
"GET:/api/v2/workspaceagents/{workspaceagent}/iceservers": {NoAuthorize: true},
"GET:/api/v2/workspaceagents/{workspaceagent}/pty": {NoAuthorize: true},
@ -95,7 +94,6 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
"GET:/api/v2/users/oauth2/github/callback": {NoAuthorize: true},
"POST:/api/v2/users/{user}/organizations/": {NoAuthorize: true},
"PUT:/api/v2/organizations/{organization}/members/{user}/roles": {NoAuthorize: true},
"GET:/api/v2/organizations/{organization}/provisionerdaemons": {NoAuthorize: true},
"POST:/api/v2/organizations/{organization}/templates": {NoAuthorize: true},
@ -143,11 +141,21 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
AssertObject: rbac.ResourceWorkspace.InOrg(organization.ID).WithID(workspace.ID.String()).WithOwner(workspace.OwnerID.String()),
},
"GET:/api/v2/organizations/{organization}/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace},
"GET:/api/v2/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace},
// These endpoints need payloads to get to the auth part.
"PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
}
for k, v := range assertRoute {
noTrailSlash := strings.TrimRight(k, "/")
if _, ok := assertRoute[noTrailSlash]; ok && noTrailSlash != k {
t.Errorf("route %q & %q is declared twice", noTrailSlash, k)
t.FailNow()
}
assertRoute[noTrailSlash] = v
}
c, _ := srv.Config.Handler.(*chi.Mux)
err = chi.Walk(c, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
name := method + ":" + route

View File

@ -291,6 +291,27 @@ func (q *fakeQuerier) GetAllUserRoles(_ context.Context, userID uuid.UUID) (data
}, nil
}
func (q *fakeQuerier) GetWorkspacesWithFilter(_ context.Context, arg database.GetWorkspacesWithFilterParams) ([]database.Workspace, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
workspaces := make([]database.Workspace, 0)
for _, workspace := range q.workspaces {
if arg.OrganizationID != uuid.Nil && workspace.OrganizationID != arg.OrganizationID {
continue
}
if arg.OwnerID != uuid.Nil && workspace.OwnerID != arg.OwnerID {
continue
}
if !arg.Deleted && workspace.Deleted {
continue
}
workspaces = append(workspaces, workspace)
}
return workspaces, nil
}
func (q *fakeQuerier) GetWorkspacesByTemplateID(_ context.Context, arg database.GetWorkspacesByTemplateIDParams) ([]database.Workspace, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -486,26 +507,6 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndName(_ context.Context, a
return database.WorkspaceBuild{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetWorkspacesByOrganizationID(_ context.Context, req database.GetWorkspacesByOrganizationIDParams) ([]database.Workspace, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
workspaces := make([]database.Workspace, 0)
for _, workspace := range q.workspaces {
if workspace.OrganizationID != req.OrganizationID {
continue
}
if workspace.Deleted != req.Deleted {
continue
}
workspaces = append(workspaces, workspace)
}
if len(workspaces) == 0 {
return nil, sql.ErrNoRows
}
return workspaces, nil
}
func (q *fakeQuerier) GetWorkspacesByOrganizationIDs(_ context.Context, req database.GetWorkspacesByOrganizationIDsParams) ([]database.Workspace, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -525,23 +526,6 @@ func (q *fakeQuerier) GetWorkspacesByOrganizationIDs(_ context.Context, req data
return workspaces, nil
}
func (q *fakeQuerier) GetWorkspacesByOwnerID(_ context.Context, req database.GetWorkspacesByOwnerIDParams) ([]database.Workspace, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
workspaces := make([]database.Workspace, 0)
for _, workspace := range q.workspaces {
if workspace.OwnerID != req.OwnerID {
continue
}
if workspace.Deleted != req.Deleted {
continue
}
workspaces = append(workspaces, workspace)
}
return workspaces, nil
}
func (q *fakeQuerier) GetOrganizations(_ context.Context) ([]database.Organization, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()

View File

@ -71,10 +71,9 @@ type querier interface {
GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error)
GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error)
GetWorkspacesAutostartAutostop(ctx context.Context) ([]Workspace, error)
GetWorkspacesByOrganizationID(ctx context.Context, arg GetWorkspacesByOrganizationIDParams) ([]Workspace, error)
GetWorkspacesByOrganizationIDs(ctx context.Context, arg GetWorkspacesByOrganizationIDsParams) ([]Workspace, error)
GetWorkspacesByOwnerID(ctx context.Context, arg GetWorkspacesByOwnerIDParams) ([]Workspace, error)
GetWorkspacesByTemplateID(ctx context.Context, arg GetWorkspacesByTemplateIDParams) ([]Workspace, error)
GetWorkspacesWithFilter(ctx context.Context, arg GetWorkspacesWithFilterParams) ([]Workspace, error)
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error)
InsertFile(ctx context.Context, arg InsertFileParams) (File, error)

View File

@ -3314,49 +3314,6 @@ func (q *sqlQuerier) GetWorkspacesAutostartAutostop(ctx context.Context) ([]Work
return items, nil
}
const getWorkspacesByOrganizationID = `-- name: GetWorkspacesByOrganizationID :many
SELECT id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule FROM workspaces WHERE organization_id = $1 AND deleted = $2
`
type GetWorkspacesByOrganizationIDParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
Deleted bool `db:"deleted" json:"deleted"`
}
func (q *sqlQuerier) GetWorkspacesByOrganizationID(ctx context.Context, arg GetWorkspacesByOrganizationIDParams) ([]Workspace, error) {
rows, err := q.db.QueryContext(ctx, getWorkspacesByOrganizationID, arg.OrganizationID, arg.Deleted)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Workspace
for rows.Next() {
var i Workspace
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.AutostopSchedule,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspacesByOrganizationIDs = `-- name: GetWorkspacesByOrganizationIDs :many
SELECT id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule FROM workspaces WHERE organization_id = ANY($1 :: uuid [ ]) AND deleted = $2
`
@ -3400,55 +3357,6 @@ func (q *sqlQuerier) GetWorkspacesByOrganizationIDs(ctx context.Context, arg Get
return items, nil
}
const getWorkspacesByOwnerID = `-- name: GetWorkspacesByOwnerID :many
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule
FROM
workspaces
WHERE
owner_id = $1
AND deleted = $2
`
type GetWorkspacesByOwnerIDParams struct {
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
Deleted bool `db:"deleted" json:"deleted"`
}
func (q *sqlQuerier) GetWorkspacesByOwnerID(ctx context.Context, arg GetWorkspacesByOwnerIDParams) ([]Workspace, error) {
rows, err := q.db.QueryContext(ctx, getWorkspacesByOwnerID, arg.OwnerID, arg.Deleted)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Workspace
for rows.Next() {
var i Workspace
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.AutostopSchedule,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspacesByTemplateID = `-- name: GetWorkspacesByTemplateID :many
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule
@ -3498,6 +3406,68 @@ func (q *sqlQuerier) GetWorkspacesByTemplateID(ctx context.Context, arg GetWorks
return items, nil
}
const getWorkspacesWithFilter = `-- name: GetWorkspacesWithFilter :many
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, autostop_schedule
FROM
workspaces
WHERE
-- Optionally include deleted workspaces
deleted = $1
-- Filter by organization_id
AND CASE
WHEN $2 :: uuid != '00000000-00000000-00000000-00000000' THEN
organization_id = $2
ELSE true
END
-- Filter by owner_id
AND CASE
WHEN $3 :: uuid != '00000000-00000000-00000000-00000000' THEN
owner_id = $3
ELSE true
END
`
type GetWorkspacesWithFilterParams struct {
Deleted bool `db:"deleted" json:"deleted"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
}
func (q *sqlQuerier) GetWorkspacesWithFilter(ctx context.Context, arg GetWorkspacesWithFilterParams) ([]Workspace, error) {
rows, err := q.db.QueryContext(ctx, getWorkspacesWithFilter, arg.Deleted, arg.OrganizationID, arg.OwnerID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Workspace
for rows.Next() {
var i Workspace
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.AutostopSchedule,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspace = `-- name: InsertWorkspace :one
INSERT INTO
workspaces (

View File

@ -8,8 +8,27 @@ WHERE
LIMIT
1;
-- name: GetWorkspacesByOrganizationID :many
SELECT * FROM workspaces WHERE organization_id = $1 AND deleted = $2;
-- name: GetWorkspacesWithFilter :many
SELECT
*
FROM
workspaces
WHERE
-- Optionally include deleted workspaces
deleted = @deleted
-- Filter by organization_id
AND CASE
WHEN @organization_id :: uuid != '00000000-00000000-00000000-00000000' THEN
organization_id = @organization_id
ELSE true
END
-- Filter by owner_id
AND CASE
WHEN @owner_id :: uuid != '00000000-00000000-00000000-00000000' THEN
owner_id = @owner_id
ELSE true
END
;
-- name: GetWorkspacesByOrganizationIDs :many
SELECT * FROM workspaces WHERE organization_id = ANY(@ids :: uuid [ ]) AND deleted = @deleted;
@ -37,15 +56,6 @@ WHERE
template_id = $1
AND deleted = $2;
-- name: GetWorkspacesByOwnerID :many
SELECT
*
FROM
workspaces
WHERE
owner_id = $1
AND deleted = $2;
-- name: GetWorkspaceByOwnerIDAndName :one
SELECT
*

View File

@ -599,7 +599,9 @@ func TestWorkspacesByUser(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
workspaces, err := client.WorkspacesByUser(context.Background(), codersdk.Me)
workspaces, err := client.Workspaces(context.Background(), codersdk.WorkspaceFilter{
Owner: codersdk.Me,
})
require.NoError(t, err)
require.Len(t, workspaces, 0)
})
@ -628,11 +630,11 @@ func TestWorkspacesByUser(t *testing.T) {
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
workspaces, err := newUserClient.WorkspacesByUser(context.Background(), codersdk.Me)
workspaces, err := newUserClient.Workspaces(context.Background(), codersdk.WorkspaceFilter{Owner: codersdk.Me})
require.NoError(t, err)
require.Len(t, workspaces, 0)
workspaces, err = client.WorkspacesByUser(context.Background(), codersdk.Me)
workspaces, err = client.Workspaces(context.Background(), codersdk.WorkspaceFilter{Owner: codersdk.Me})
require.NoError(t, err)
require.Len(t, workspaces, 1)
})

View File

@ -70,7 +70,7 @@ func (api *api) workspace(rw http.ResponseWriter, r *http.Request) {
func (api *api) workspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
organization := httpmw.OrganizationParam(r)
roles := httpmw.UserRoles(r)
workspaces, err := api.Database.GetWorkspacesByOrganizationID(r.Context(), database.GetWorkspacesByOrganizationIDParams{
workspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), database.GetWorkspacesWithFilterParams{
OrganizationID: organization.ID,
Deleted: false,
})
@ -104,30 +104,67 @@ func (api *api) workspacesByOrganization(rw http.ResponseWriter, r *http.Request
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
}
func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
// workspaces returns all workspaces a user can read.
// Optional filters with query params
func (api *api) workspaces(rw http.ResponseWriter, r *http.Request) {
roles := httpmw.UserRoles(r)
apiKey := httpmw.APIKey(r)
allWorkspaces := make([]database.Workspace, 0)
userWorkspaces, err := api.Database.GetWorkspacesByOwnerID(r.Context(), database.GetWorkspacesByOwnerIDParams{
OwnerID: user.ID,
})
// Empty strings mean no filter
orgFilter := r.URL.Query().Get("organization_id")
ownerFilter := r.URL.Query().Get("owner_id")
filter := database.GetWorkspacesWithFilterParams{Deleted: false}
if orgFilter != "" {
orgID, err := uuid.Parse(orgFilter)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("organization_id must be a uuid: %s", err.Error()),
})
return
}
filter.OrganizationID = orgID
}
if ownerFilter == "me" {
filter.OwnerID = apiKey.UserID
} else if ownerFilter != "" {
userID, err := uuid.Parse(ownerFilter)
if err != nil {
// Maybe it's a username
user, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
// Why not just accept 1 arg and use it for both in the sql?
Username: ownerFilter,
Email: ownerFilter,
})
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "owner must be a uuid or username",
})
return
}
userID = user.ID
}
filter.OwnerID = userID
}
allowedWorkspaces := make([]database.Workspace, 0)
allWorkspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), filter)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspaces for user: %s", err),
})
return
}
for _, ws := range userWorkspaces {
for _, ws := range allWorkspaces {
ws := ws
err = api.Authorizer.ByRoleName(r.Context(), user.ID.String(), roles.Roles, rbac.ActionRead,
err = api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, rbac.ActionRead,
rbac.ResourceWorkspace.InOrg(ws.OrganizationID).WithOwner(ws.OwnerID.String()).WithID(ws.ID.String()))
if err == nil {
allWorkspaces = append(allWorkspaces, ws)
allowedWorkspaces = append(allowedWorkspaces, ws)
}
}
apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, allWorkspaces)
apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, allowedWorkspaces)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("convert workspaces: %s", err),
@ -140,8 +177,9 @@ func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) {
func (api *api) workspacesByOwner(rw http.ResponseWriter, r *http.Request) {
owner := httpmw.UserParam(r)
roles := httpmw.UserRoles(r)
workspaces, err := api.Database.GetWorkspacesByOwnerID(r.Context(), database.GetWorkspacesByOwnerIDParams{
workspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), database.GetWorkspacesWithFilterParams{
OwnerID: owner.ID,
Deleted: false,
})
if errors.Is(err, sql.ErrNoRows) {
err = nil

View File

@ -134,16 +134,29 @@ func TestWorkspacesByOwner(t *testing.T) {
_, err := client.WorkspacesByOwner(context.Background(), user.OrganizationID, codersdk.Me)
require.NoError(t, err)
})
t.Run("List", func(t *testing.T) {
t.Run("ListMine", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.NewProvisionerDaemon(t, client)
user := coderdtest.CreateFirstUser(t, client)
me, err := client.User(context.Background(), codersdk.Me)
require.NoError(t, err)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
workspaces, err := client.WorkspacesByOwner(context.Background(), user.OrganizationID, codersdk.Me)
// Create noise workspace that should be filtered out
other := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
_ = coderdtest.CreateWorkspace(t, other, user.OrganizationID, template.ID)
// Use a username
workspaces, err := client.Workspaces(context.Background(), codersdk.WorkspaceFilter{
OrganizationID: user.OrganizationID,
Owner: me.Username,
})
require.NoError(t, err)
require.Len(t, workspaces, 1)
})

View File

@ -438,19 +438,3 @@ func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) {
var userAuth AuthMethods
return userAuth, json.NewDecoder(res.Body).Decode(&userAuth)
}
// WorkspacesByUser returns all workspaces a user has access to.
func (c *Client) WorkspacesByUser(ctx context.Context, user string) ([]Workspace, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspaces", user), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var workspaces []Workspace
return workspaces, json.NewDecoder(res.Body).Decode(&workspaces)
}

View File

@ -131,3 +131,41 @@ func (c *Client) UpdateWorkspaceAutostop(ctx context.Context, id uuid.UUID, req
}
return nil
}
type WorkspaceFilter struct {
OrganizationID uuid.UUID
// Owner can be a user_id (uuid), "me", or a username
Owner string
}
// asRequestOption returns a function that can be used in (*Client).Request.
// It modifies the request query parameters.
func (f WorkspaceFilter) asRequestOption() requestOption {
return func(r *http.Request) {
q := r.URL.Query()
if f.OrganizationID != uuid.Nil {
q.Set("organization_id", f.OrganizationID.String())
}
if f.Owner != "" {
q.Set("owner_id", f.Owner)
}
r.URL.RawQuery = q.Encode()
}
}
// Workspaces returns all workspaces the authenticated user has access to.
func (c *Client) Workspaces(ctx context.Context, filter WorkspaceFilter) ([]Workspace, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspaces", nil, filter.asRequestOption())
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, readBodyAsError(res)
}
var workspaces []Workspace
return workspaces, json.NewDecoder(res.Body).Decode(&workspaces)
}

View File

@ -120,8 +120,10 @@ export const getWorkspace = async (workspaceId: string): Promise<TypesGen.Worksp
return response.data
}
export const getWorkspaces = async (userID = "me"): Promise<TypesGen.Workspace[]> => {
const response = await axios.get<TypesGen.Workspace[]>(`/api/v2/users/${userID}/workspaces`)
// TODO: @emyrk add query params as arguments. Supports 'organization_id' and 'owner'
// 'owner' can be a username, user_id, or 'me'
export const getWorkspaces = async (): Promise<TypesGen.Workspace[]> => {
const response = await axios.get<TypesGen.Workspace[]>(`/api/v2/workspaces`)
return response.data
}

View File

@ -430,6 +430,12 @@ export interface WorkspaceBuild {
readonly job: ProvisionerJob
}
// From codersdk/workspaces.go:135:6
export interface WorkspaceFilter {
readonly OrganizationID: string
readonly OwnerID: string
}
// From codersdk/workspaceresources.go:23:6
export interface WorkspaceResource {
readonly id: string

View File

@ -15,7 +15,7 @@ describe("WorkspacesPage", () => {
it("renders an empty workspaces page", async () => {
// Given
server.use(
rest.get("/api/v2/users/me/workspaces", async (req, res, ctx) => {
rest.get("/api/v2/workspaces", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json([]))
}),
)

View File

@ -36,7 +36,7 @@ export const handlers = [
rest.post("/api/v2/users/me/workspaces", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspace))
}),
rest.get("/api/v2/users/me/workspaces", async (req, res, ctx) => {
rest.get("/api/v2/workspaces", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json([M.MockWorkspace]))
}),
rest.get("/api/v2/users/me/organizations", (req, res, ctx) => {