Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7c238f1

Browse files
presleypf0ssel
andauthored
feat: paginate workspaces page (coder#4647)
* Start - still needs api call changes * Some xservice changes * Finish adding count to xservice * Mock out api call on frontend * Handle errors * Doctor getWorkspaces * Add types, start writing count function * Hook up route * Use empty page struct * Write interface and database fake * SQL query * Fix params type * Missed a spot * Space after alert banner * Fix model queries * Unpack query correctly * Fix filter-page interaction * Make mobile friendly * Format * Test backend * Fix key * Delete unnecessary conditional * Add test helpers * Use limit constant * Show widget with no count * Add test * Format * make gen from garretts workspace idk why * fix authorize test' * Hide widget with 0 records * Fix tests * Format * Fix types generated * Fix story * Add alert banner story * Format * Fix import * Format * Try removing story * Revert "Fix story" This reverts commit c06765b. * Add counts to page view story * Revert "Try removing story" This reverts commit 476019b. Co-authored-by: Garrett <[email protected]>
1 parent 423ac04 commit 7c238f1

24 files changed

+1073
-186
lines changed

coderd/coderd.go

+1
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ func New(options *Options) *API {
503503
apiKeyMiddleware,
504504
)
505505
r.Get("/", api.workspaces)
506+
r.Get("/count", api.workspaceCount)
506507
r.Route("/{workspace}", func(r chi.Router) {
507508
r.Use(
508509
httpmw.ExtractWorkspaceParam(options.Database),

coderd/coderdtest/authorize.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,8 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
243243
"POST:/api/v2/organizations/{organization}/templateversions": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
244244

245245
// Endpoints that use the SQLQuery filter.
246-
"GET:/api/v2/workspaces/": {StatusCode: http.StatusOK, NoAuthorize: true},
246+
"GET:/api/v2/workspaces/": {StatusCode: http.StatusOK, NoAuthorize: true},
247+
"GET:/api/v2/workspaces/count": {StatusCode: http.StatusOK, NoAuthorize: true},
247248
}
248249

249250
// Routes like proxy routes support all HTTP methods. A helper func to expand

coderd/database/databasefake/databasefake.go

+150
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,156 @@ func (q *fakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
788788
return workspaces, nil
789789
}
790790

791+
func (q *fakeQuerier) GetWorkspaceCount(ctx context.Context, arg database.GetWorkspaceCountParams) (int64, error) {
792+
count, err := q.GetAuthorizedWorkspaceCount(ctx, arg, nil)
793+
return count, err
794+
}
795+
796+
//nolint:gocyclo
797+
func (q *fakeQuerier) GetAuthorizedWorkspaceCount(ctx context.Context, arg database.GetWorkspaceCountParams, authorizedFilter rbac.AuthorizeFilter) (int64, error) {
798+
q.mutex.RLock()
799+
defer q.mutex.RUnlock()
800+
801+
workspaces := make([]database.Workspace, 0)
802+
for _, workspace := range q.workspaces {
803+
if arg.OwnerID != uuid.Nil && workspace.OwnerID != arg.OwnerID {
804+
continue
805+
}
806+
807+
if arg.OwnerUsername != "" {
808+
owner, err := q.GetUserByID(ctx, workspace.OwnerID)
809+
if err == nil && !strings.EqualFold(arg.OwnerUsername, owner.Username) {
810+
continue
811+
}
812+
}
813+
814+
if arg.TemplateName != "" {
815+
template, err := q.GetTemplateByID(ctx, workspace.TemplateID)
816+
if err == nil && !strings.EqualFold(arg.TemplateName, template.Name) {
817+
continue
818+
}
819+
}
820+
821+
if !arg.Deleted && workspace.Deleted {
822+
continue
823+
}
824+
825+
if arg.Name != "" && !strings.Contains(strings.ToLower(workspace.Name), strings.ToLower(arg.Name)) {
826+
continue
827+
}
828+
829+
if arg.Status != "" {
830+
build, err := q.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
831+
if err != nil {
832+
return 0, xerrors.Errorf("get latest build: %w", err)
833+
}
834+
835+
job, err := q.GetProvisionerJobByID(ctx, build.JobID)
836+
if err != nil {
837+
return 0, xerrors.Errorf("get provisioner job: %w", err)
838+
}
839+
840+
switch arg.Status {
841+
case "pending":
842+
if !job.StartedAt.Valid {
843+
continue
844+
}
845+
846+
case "starting":
847+
if !job.StartedAt.Valid &&
848+
!job.CanceledAt.Valid &&
849+
job.CompletedAt.Valid &&
850+
time.Since(job.UpdatedAt) > 30*time.Second ||
851+
build.Transition != database.WorkspaceTransitionStart {
852+
continue
853+
}
854+
855+
case "running":
856+
if !job.CompletedAt.Valid &&
857+
job.CanceledAt.Valid &&
858+
job.Error.Valid ||
859+
build.Transition != database.WorkspaceTransitionStart {
860+
continue
861+
}
862+
863+
case "stopping":
864+
if !job.StartedAt.Valid &&
865+
!job.CanceledAt.Valid &&
866+
job.CompletedAt.Valid &&
867+
time.Since(job.UpdatedAt) > 30*time.Second ||
868+
build.Transition != database.WorkspaceTransitionStop {
869+
continue
870+
}
871+
872+
case "stopped":
873+
if !job.CompletedAt.Valid &&
874+
job.CanceledAt.Valid &&
875+
job.Error.Valid ||
876+
build.Transition != database.WorkspaceTransitionStop {
877+
continue
878+
}
879+
880+
case "failed":
881+
if (!job.CanceledAt.Valid && !job.Error.Valid) ||
882+
(!job.CompletedAt.Valid && !job.Error.Valid) {
883+
continue
884+
}
885+
886+
case "canceling":
887+
if !job.CanceledAt.Valid && job.CompletedAt.Valid {
888+
continue
889+
}
890+
891+
case "canceled":
892+
if !job.CanceledAt.Valid && !job.CompletedAt.Valid {
893+
continue
894+
}
895+
896+
case "deleted":
897+
if !job.StartedAt.Valid &&
898+
job.CanceledAt.Valid &&
899+
!job.CompletedAt.Valid &&
900+
time.Since(job.UpdatedAt) > 30*time.Second ||
901+
build.Transition != database.WorkspaceTransitionDelete {
902+
continue
903+
}
904+
905+
case "deleting":
906+
if !job.CompletedAt.Valid &&
907+
job.CanceledAt.Valid &&
908+
job.Error.Valid &&
909+
build.Transition != database.WorkspaceTransitionDelete {
910+
continue
911+
}
912+
913+
default:
914+
return 0, xerrors.Errorf("unknown workspace status in filter: %q", arg.Status)
915+
}
916+
}
917+
918+
if len(arg.TemplateIds) > 0 {
919+
match := false
920+
for _, id := range arg.TemplateIds {
921+
if workspace.TemplateID == id {
922+
match = true
923+
break
924+
}
925+
}
926+
if !match {
927+
continue
928+
}
929+
}
930+
931+
// If the filter exists, ensure the object is authorized.
932+
if authorizedFilter != nil && !authorizedFilter.Eval(workspace.RBACObject()) {
933+
continue
934+
}
935+
workspaces = append(workspaces, workspace)
936+
}
937+
938+
return int64(len(workspaces)), nil
939+
}
940+
791941
func (q *fakeQuerier) GetWorkspaceByID(_ context.Context, id uuid.UUID) (database.Workspace, error) {
792942
q.mutex.RLock()
793943
defer q.mutex.RUnlock()

coderd/database/modelqueries.go

+21
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ func (q *sqlQuerier) GetTemplateGroupRoles(ctx context.Context, id uuid.UUID) ([
112112

113113
type workspaceQuerier interface {
114114
GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, authorizedFilter rbac.AuthorizeFilter) ([]Workspace, error)
115+
GetAuthorizedWorkspaceCount(ctx context.Context, arg GetWorkspaceCountParams, authorizedFilter rbac.AuthorizeFilter) (int64, error)
115116
}
116117

117118
// GetAuthorizedWorkspaces returns all workspaces that the user is authorized to access.
@@ -166,3 +167,23 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
166167
}
167168
return items, nil
168169
}
170+
171+
func (q *sqlQuerier) GetAuthorizedWorkspaceCount(ctx context.Context, arg GetWorkspaceCountParams, authorizedFilter rbac.AuthorizeFilter) (int64, error) {
172+
// In order to properly use ORDER BY, OFFSET, and LIMIT, we need to inject the
173+
// authorizedFilter between the end of the where clause and those statements.
174+
filter := strings.Replace(getWorkspaceCount, "-- @authorize_filter", fmt.Sprintf(" AND %s", authorizedFilter.SQLString(rbac.NoACLConfig())), 1)
175+
// The name comment is for metric tracking
176+
query := fmt.Sprintf("-- name: GetAuthorizedWorkspaceCount :one\n%s", filter)
177+
row := q.db.QueryRowContext(ctx, query,
178+
arg.Deleted,
179+
arg.Status,
180+
arg.OwnerID,
181+
arg.OwnerUsername,
182+
arg.TemplateName,
183+
pq.Array(arg.TemplateIds),
184+
arg.Name,
185+
)
186+
var count int64
187+
err := row.Scan(&count)
188+
return count, err
189+
}

coderd/database/querier.go

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

+154
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)