Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit2a07a99

Browse files
committed
restore list ?q query param
1 parentac9864f commit2a07a99

File tree

4 files changed

+270
-49
lines changed

4 files changed

+270
-49
lines changed

‎coderd/aitasks.go‎

Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/coder/coder/v2/coderd/httpmw"
2323
"github.com/coder/coder/v2/coderd/rbac"
2424
"github.com/coder/coder/v2/coderd/rbac/policy"
25+
"github.com/coder/coder/v2/coderd/searchquery"
2526
"github.com/coder/coder/v2/coderd/taskname"
2627
"github.com/coder/coder/v2/codersdk"
2728

@@ -372,8 +373,7 @@ type tasksListResponse struct {
372373
// @ID list-tasks
373374
// @Security CoderSessionToken
374375
// @Tags Experimental
375-
// @Param owner query string false "Filter by owner (username, UUID, or 'me' for current user)"
376-
// @Param status query string false "Filter by task status (pending, initializing, active, paused, error, unknown)"
376+
// @Param q query string false "Search query for filtering tasks. Supports: owner:<username/uuid/me>, organization:<org-name/uuid>, status:<status>"
377377
// @Success 200 {object} coderd.tasksListResponse
378378
// @Router /api/experimental/tasks [get]
379379
//
@@ -384,47 +384,18 @@ func (api *API) tasksList(rw http.ResponseWriter, r *http.Request) {
384384
apiKey:=httpmw.APIKey(r)
385385

386386
// Parse query parameters for filtering tasks.
387-
ownerParam:=r.URL.Query().Get("owner")
388-
statusParam:=r.URL.Query().Get("status")
389-
390-
// Determine owner ID based on the owner parameter.
391-
varownerID uuid.UUID
392-
switchownerParam {
393-
casecodersdk.Me:
394-
ownerID=apiKey.UserID
395-
case"":
396-
// If ownerID is not set, list all tasks the user has access to.
397-
default:
398-
// Try to parse as UUID first.
399-
parsedUUID,err:=uuid.Parse(ownerParam)
400-
iferr==nil {
401-
ownerID=parsedUUID
402-
}elseiferr!=nil {
403-
// Otherwise, look up by username.
404-
user,err:=api.Database.GetUserByEmailOrUsername(ctx, database.GetUserByEmailOrUsernameParams{
405-
Username:ownerParam,
406-
})
407-
iferr!=nil {
408-
ifhttpapi.Is404Error(err) {
409-
httpapi.ResourceNotFound(rw)
410-
return
411-
}
412-
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
413-
Message:"Internal error fetching user.",
414-
Detail:err.Error(),
415-
})
416-
return
417-
}
418-
ownerID=user.ID
419-
}
387+
queryStr:=r.URL.Query().Get("q")
388+
filter,errs:=searchquery.Tasks(ctx,api.Database,queryStr,apiKey.UserID)
389+
iflen(errs)>0 {
390+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
391+
Message:"Invalid task search query.",
392+
Validations:errs,
393+
})
394+
return
420395
}
421396

422397
// Fetch all tasks matching the filters from the database.
423-
dbTasks,err:=api.Database.ListTasks(ctx, database.ListTasksParams{
424-
OwnerID:ownerID,
425-
OrganizationID:uuid.Nil,// TODO(mafredri): ?
426-
Status:statusParam,
427-
})
398+
dbTasks,err:=api.Database.ListTasks(ctx,filter)
428399
iferr!=nil {
429400
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
430401
Message:"Internal error fetching tasks.",

‎coderd/searchquery/search.go‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,39 @@ func AIBridgeInterceptions(ctx context.Context, db database.Store, query string,
391391
returnfilter,parser.Errors
392392
}
393393

394+
// Tasks parses a search query for tasks.
395+
//
396+
// Supported query parameters:
397+
// - owner: string (username, UUID, or 'me' for current user)
398+
// - organization: string (organization UUID or name)
399+
// - status: string (pending, initializing, active, paused, error, unknown)
400+
funcTasks(ctx context.Context,db database.Store,querystring,actorID uuid.UUID) (database.ListTasksParams, []codersdk.ValidationError) {
401+
filter:= database.ListTasksParams{}
402+
403+
ifquery=="" {
404+
returnfilter,nil
405+
}
406+
407+
// Always lowercase for all searches.
408+
query=strings.ToLower(query)
409+
values,errors:=searchTerms(query,func(termstring,values url.Values)error {
410+
// Default unqualified terms to owner
411+
values.Add("owner",term)
412+
returnnil
413+
})
414+
iflen(errors)>0 {
415+
returnfilter,errors
416+
}
417+
418+
parser:=httpapi.NewQueryParamParser()
419+
filter.OwnerID=parseUser(ctx,db,parser,values,"owner",actorID)
420+
filter.OrganizationID=parseOrganization(ctx,db,parser,values,"organization")
421+
filter.Status=parser.String(values,"","status")
422+
423+
parser.ErrorExcessParams(values)
424+
returnfilter,parser.Errors
425+
}
426+
394427
funcsearchTerms(querystring,defaultKeyfunc(termstring,values url.Values)error) (url.Values, []codersdk.ValidationError) {
395428
searchValues:=make(url.Values)
396429

‎coderd/searchquery/search_test.go‎

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,3 +944,199 @@ func TestSearchTemplates(t *testing.T) {
944944
})
945945
}
946946
}
947+
948+
funcTestSearchTasks(t*testing.T) {
949+
t.Parallel()
950+
951+
userID:=uuid.MustParse("10000000-0000-0000-0000-000000000001")
952+
orgID:=uuid.MustParse("20000000-0000-0000-0000-000000000001")
953+
954+
testCases:= []struct {
955+
Namestring
956+
Querystring
957+
ActorID uuid.UUID
958+
Expected database.ListTasksParams
959+
ExpectedErrorContainsstring
960+
Setupfunc(t*testing.T,db database.Store)
961+
}{
962+
{
963+
Name:"Empty",
964+
Query:"",
965+
Expected: database.ListTasksParams{},
966+
},
967+
{
968+
Name:"OwnerUsername",
969+
Query:"owner:alice",
970+
Setup:func(t*testing.T,db database.Store) {
971+
dbgen.User(t,db, database.User{
972+
ID:userID,
973+
Username:"alice",
974+
})
975+
},
976+
Expected: database.ListTasksParams{
977+
OwnerID:userID,
978+
},
979+
},
980+
{
981+
Name:"OwnerMe",
982+
Query:"owner:me",
983+
ActorID:userID,
984+
Expected: database.ListTasksParams{
985+
OwnerID:userID,
986+
},
987+
},
988+
{
989+
Name:"OwnerUUID",
990+
Query:fmt.Sprintf("owner:%s",userID),
991+
Expected: database.ListTasksParams{
992+
OwnerID:userID,
993+
},
994+
},
995+
{
996+
Name:"StatusActive",
997+
Query:"status:active",
998+
Expected: database.ListTasksParams{
999+
Status:"active",
1000+
},
1001+
},
1002+
{
1003+
Name:"StatusPending",
1004+
Query:"status:pending",
1005+
Expected: database.ListTasksParams{
1006+
Status:"pending",
1007+
},
1008+
},
1009+
{
1010+
Name:"Organization",
1011+
Query:"organization:acme",
1012+
Setup:func(t*testing.T,db database.Store) {
1013+
dbgen.Organization(t,db, database.Organization{
1014+
ID:orgID,
1015+
Name:"acme",
1016+
})
1017+
},
1018+
Expected: database.ListTasksParams{
1019+
OrganizationID:orgID,
1020+
},
1021+
},
1022+
{
1023+
Name:"OrganizationUUID",
1024+
Query:fmt.Sprintf("organization:%s",orgID),
1025+
Expected: database.ListTasksParams{
1026+
OrganizationID:orgID,
1027+
},
1028+
},
1029+
{
1030+
Name:"Combined",
1031+
Query:"owner:alice organization:acme status:active",
1032+
Setup:func(t*testing.T,db database.Store) {
1033+
dbgen.Organization(t,db, database.Organization{
1034+
ID:orgID,
1035+
Name:"acme",
1036+
})
1037+
dbgen.User(t,db, database.User{
1038+
ID:userID,
1039+
Username:"alice",
1040+
})
1041+
},
1042+
Expected: database.ListTasksParams{
1043+
OwnerID:userID,
1044+
OrganizationID:orgID,
1045+
Status:"active",
1046+
},
1047+
},
1048+
{
1049+
Name:"QuotedOwner",
1050+
Query:`owner:"alice"`,
1051+
Setup:func(t*testing.T,db database.Store) {
1052+
dbgen.User(t,db, database.User{
1053+
ID:userID,
1054+
Username:"alice",
1055+
})
1056+
},
1057+
Expected: database.ListTasksParams{
1058+
OwnerID:userID,
1059+
},
1060+
},
1061+
{
1062+
Name:"QuotedStatus",
1063+
Query:`status:"pending"`,
1064+
Expected: database.ListTasksParams{
1065+
Status:"pending",
1066+
},
1067+
},
1068+
{
1069+
Name:"DefaultToOwner",
1070+
Query:"alice",
1071+
Setup:func(t*testing.T,db database.Store) {
1072+
dbgen.User(t,db, database.User{
1073+
ID:userID,
1074+
Username:"alice",
1075+
})
1076+
},
1077+
Expected: database.ListTasksParams{
1078+
OwnerID:userID,
1079+
},
1080+
},
1081+
{
1082+
Name:"InvalidOwner",
1083+
Query:"owner:nonexistent",
1084+
ExpectedErrorContains:"does not exist",
1085+
},
1086+
{
1087+
Name:"InvalidOrganization",
1088+
Query:"organization:nonexistent",
1089+
ExpectedErrorContains:"does not exist",
1090+
},
1091+
{
1092+
Name:"ExtraParam",
1093+
Query:"owner:alice invalid:param",
1094+
Setup:func(t*testing.T,db database.Store) {
1095+
dbgen.User(t,db, database.User{
1096+
ID:userID,
1097+
Username:"alice",
1098+
})
1099+
},
1100+
ExpectedErrorContains:"is not a valid query param",
1101+
},
1102+
{
1103+
Name:"ExtraColon",
1104+
Query:"owner:alice:extra",
1105+
ExpectedErrorContains:"can only contain 1 ':'",
1106+
},
1107+
{
1108+
Name:"PrefixColon",
1109+
Query:":owner",
1110+
ExpectedErrorContains:"cannot start or end with ':'",
1111+
},
1112+
{
1113+
Name:"SuffixColon",
1114+
Query:"owner:",
1115+
ExpectedErrorContains:"cannot start or end with ':'",
1116+
},
1117+
}
1118+
1119+
for_,c:=rangetestCases {
1120+
t.Run(c.Name,func(t*testing.T) {
1121+
t.Parallel()
1122+
db,_:=dbtestutil.NewDB(t)
1123+
1124+
ifc.Setup!=nil {
1125+
c.Setup(t,db)
1126+
}
1127+
1128+
values,errs:=searchquery.Tasks(context.Background(),db,c.Query,c.ActorID)
1129+
ifc.ExpectedErrorContains!="" {
1130+
require.True(t,len(errs)>0,"expect some errors")
1131+
vars strings.Builder
1132+
for_,err:=rangeerrs {
1133+
_,_=s.WriteString(fmt.Sprintf("%s: %s\n",err.Field,err.Detail))
1134+
}
1135+
require.Contains(t,s.String(),c.ExpectedErrorContains)
1136+
}else {
1137+
require.Len(t,errs,0,"expected no error")
1138+
require.Equal(t,c.Expected,values,"expected values")
1139+
}
1140+
})
1141+
}
1142+
}

‎codersdk/aitasks.go‎

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,37 @@ type TaskStateEntry struct {
171171
typeTasksFilterstruct {
172172
// Owner can be a username, UUID, or "me".
173173
Ownerstring`json:"owner,omitempty"`
174+
// Organization can be an organization name or UUID.
175+
Organizationstring`json:"organization,omitempty"`
174176
// Status filters the tasks by their task status.
175177
StatusTaskStatus`json:"status,omitempty"`
178+
// FilterQuery allows specifying a raw filter query.
179+
FilterQuerystring`json:"filter_query,omitempty"`
180+
}
181+
182+
func (fTasksFilter)asRequestOption()RequestOption {
183+
returnfunc(r*http.Request) {
184+
varparams []string
185+
// Make sure all user input is quoted to ensure it's parsed as a single
186+
// string.
187+
iff.Owner!="" {
188+
params=append(params,fmt.Sprintf("owner:%q",f.Owner))
189+
}
190+
iff.Organization!="" {
191+
params=append(params,fmt.Sprintf("organization:%q",f.Organization))
192+
}
193+
iff.Status!="" {
194+
params=append(params,fmt.Sprintf("status:%q",string(f.Status)))
195+
}
196+
iff.FilterQuery!="" {
197+
// If custom stuff is added, just add it on here.
198+
params=append(params,f.FilterQuery)
199+
}
200+
201+
q:=r.URL.Query()
202+
q.Set("q",strings.Join(params," "))
203+
r.URL.RawQuery=q.Encode()
204+
}
176205
}
177206

178207
// Tasks lists all tasks belonging to the user or specified owner.
@@ -183,15 +212,7 @@ func (c *ExperimentalClient) Tasks(ctx context.Context, filter *TasksFilter) ([]
183212
filter=&TasksFilter{}
184213
}
185214

186-
varopts []RequestOption
187-
iffilter.Owner!="" {
188-
opts=append(opts,WithQueryParam("owner",filter.Owner))
189-
}
190-
iffilter.Status!="" {
191-
opts=append(opts,WithQueryParam("status",string(filter.Status)))
192-
}
193-
194-
res,err:=c.Request(ctx,http.MethodGet,"/api/experimental/tasks",nil,opts...)
215+
res,err:=c.Request(ctx,http.MethodGet,"/api/experimental/tasks",nil,filter.asRequestOption())
195216
iferr!=nil {
196217
returnnil,err
197218
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp