diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 2a0abbccfdd9b..dceddd2e8c3da 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -82,6 +82,8 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate pq.Array(arg.IDs), arg.Deprecated, arg.HasAITask, + arg.AuthorID, + arg.AuthorUsername, ) if err != nil { return nil, err diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2c1381a3b99f1..b078e2dbb29c0 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -12059,6 +12059,19 @@ WHERE tv.has_ai_task = $7 :: boolean ELSE true END + -- Filter by author_id + AND CASE + WHEN $8 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + t.created_by = $8 + ELSE true + END + -- Filter by author_username + AND CASE + WHEN $9 :: text != '' THEN + t.created_by = (SELECT id FROM users WHERE lower(users.username) = lower($9) AND deleted = false) + ELSE true + END + -- Authorize Filter clause will be injected below in GetAuthorizedTemplates -- @authorize_filter ORDER BY (t.name, t.id) ASC @@ -12072,6 +12085,8 @@ type GetTemplatesWithFilterParams struct { IDs []uuid.UUID `db:"ids" json:"ids"` Deprecated sql.NullBool `db:"deprecated" json:"deprecated"` HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"` + AuthorID uuid.UUID `db:"author_id" json:"author_id"` + AuthorUsername string `db:"author_username" json:"author_username"` } func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplatesWithFilterParams) ([]Template, error) { @@ -12083,6 +12098,8 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate pq.Array(arg.IDs), arg.Deprecated, arg.HasAITask, + arg.AuthorID, + arg.AuthorUsername, ) if err != nil { return nil, err diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql index 4a37bd2d1058b..a922a9bef1918 100644 --- a/coderd/database/queries/templates.sql +++ b/coderd/database/queries/templates.sql @@ -59,6 +59,19 @@ WHERE tv.has_ai_task = sqlc.narg('has_ai_task') :: boolean ELSE true END + -- Filter by author_id + AND CASE + WHEN @author_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + t.created_by = @author_id + ELSE true + END + -- Filter by author_username + AND CASE + WHEN @author_username :: text != '' THEN + t.created_by = (SELECT id FROM users WHERE lower(users.username) = lower(@author_username) AND deleted = false) + ELSE true + END + -- Authorize Filter clause will be injected below in GetAuthorizedTemplates -- @authorize_filter ORDER BY (t.name, t.id) ASC diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index d35f3c94b5ff7..4f3f3259c09d9 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -278,12 +278,14 @@ func Templates(ctx context.Context, db database.Store, query string) (database.G parser := httpapi.NewQueryParamParser() filter := database.GetTemplatesWithFilterParams{ Deleted: parser.Boolean(values, false, "deleted"), + OrganizationID: parseOrganization(ctx, db, parser, values, "organization"), ExactName: parser.String(values, "", "exact_name"), FuzzyName: parser.String(values, "", "name"), IDs: parser.UUIDs(values, []uuid.UUID{}, "ids"), Deprecated: parser.NullableBoolean(values, sql.NullBool{}, "deprecated"), - OrganizationID: parseOrganization(ctx, db, parser, values, "organization"), HasAITask: parser.NullableBoolean(values, sql.NullBool{}, "has-ai-task"), + AuthorID: parser.UUID(values, uuid.Nil, "author_id"), + AuthorUsername: parser.String(values, "", "author"), } parser.ErrorExcessParams(values) diff --git a/coderd/templates_test.go b/coderd/templates_test.go index 0858ce83325cc..8cd5a6ba9bf30 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -814,6 +814,46 @@ func TestTemplatesByOrganization(t *testing.T) { require.False(t, templates[0].Deprecated) require.Empty(t, templates[0].DeprecationMessage) }) + + t.Run("ListByAuthor", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + owner := coderdtest.CreateFirstUser(t, client) + adminAlpha, adminAlphaData := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) + adminBravo, adminBravoData := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) + adminCharlie, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) + + versionA := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + versionB := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + versionC := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + foo := coderdtest.CreateTemplate(t, adminAlpha, owner.OrganizationID, versionA.ID, func(request *codersdk.CreateTemplateRequest) { + request.Name = "foo" + }) + bar := coderdtest.CreateTemplate(t, adminBravo, owner.OrganizationID, versionB.ID, func(request *codersdk.CreateTemplateRequest) { + request.Name = "bar" + }) + _ = coderdtest.CreateTemplate(t, adminCharlie, owner.OrganizationID, versionC.ID, func(request *codersdk.CreateTemplateRequest) { + request.Name = "baz" + }) + + ctx := testutil.Context(t, testutil.WaitLong) + + // List alpha + alpha, err := client.Templates(ctx, codersdk.TemplateFilter{ + AuthorUsername: adminAlphaData.Username, + }) + require.NoError(t, err) + require.Len(t, alpha, 1) + require.Equal(t, foo.ID, alpha[0].ID) + + // List bravo + bravo, err := client.Templates(ctx, codersdk.TemplateFilter{ + AuthorUsername: adminBravoData.Username, + }) + require.NoError(t, err) + require.Len(t, bravo, 1) + require.Equal(t, bar.ID, bravo[0].ID) + }) } func TestTemplateByOrganizationAndName(t *testing.T) { diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 86bc47bce2375..f87d0eae188ba 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -541,6 +541,7 @@ type TemplateFilter struct { OrganizationID uuid.UUID `typescript:"-"` ExactName string `typescript:"-"` FuzzyName string `typescript:"-"` + AuthorUsername string `typescript:"-"` SearchQuery string `json:"q,omitempty"` } @@ -562,6 +563,11 @@ func (f TemplateFilter) asRequestOption() RequestOption { if f.FuzzyName != "" { params = append(params, fmt.Sprintf("name:%q", f.FuzzyName)) } + + if f.AuthorUsername != "" { + params = append(params, fmt.Sprintf("author:%q", f.AuthorUsername)) + } + if f.SearchQuery != "" { params = append(params, f.SearchQuery) }