mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat(cli): add coder exp tasks list (#19496)
Fixes coder/internal#892 Fixes coder/internal#896 Example output: ``` ❯ coder exp task list ID NAME STATUS STATE STATE CHANGED MESSAGE a7a27450-ca16-4553-a6c5-9d6f04808569 task-hardcore-herschel-bd08 running idle 5h22m3s ago Listed root directory contents, working directory reset 50f92138-f463-4f2b-abad-1816264b065f task-musing-dewdney-f058 running idle 6h3m8s ago Completed arithmetic calculation ```
This commit is contained in:
@@ -16,6 +16,7 @@ func (r *RootCmd) expCmd() *serpent.Command {
|
||||
r.mcpCommand(),
|
||||
r.promptExample(),
|
||||
r.rptyCommand(),
|
||||
r.tasksCommand(),
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
func (r *RootCmd) tasksCommand() *serpent.Command {
|
||||
cmd := &serpent.Command{
|
||||
Use: "task",
|
||||
Aliases: []string{"tasks"},
|
||||
Short: "Experimental task commands.",
|
||||
Handler: func(i *serpent.Invocation) error {
|
||||
return i.Command.HelpHandler(i)
|
||||
},
|
||||
Children: []*serpent.Command{
|
||||
r.taskList(),
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
type taskListRow struct {
|
||||
Task codersdk.Task `table:"t,recursive_inline"`
|
||||
|
||||
StateChangedAgo string `table:"state changed"`
|
||||
}
|
||||
|
||||
func taskListRowFromTask(now time.Time, t codersdk.Task) taskListRow {
|
||||
var stateAgo string
|
||||
if t.CurrentState != nil {
|
||||
stateAgo = now.UTC().Sub(t.CurrentState.Timestamp).Truncate(time.Second).String() + " ago"
|
||||
}
|
||||
|
||||
return taskListRow{
|
||||
Task: t,
|
||||
|
||||
StateChangedAgo: stateAgo,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RootCmd) taskList() *serpent.Command {
|
||||
var (
|
||||
statusFilter string
|
||||
all bool
|
||||
user string
|
||||
|
||||
client = new(codersdk.Client)
|
||||
formatter = cliui.NewOutputFormatter(
|
||||
cliui.TableFormat(
|
||||
[]taskListRow{},
|
||||
[]string{
|
||||
"id",
|
||||
"name",
|
||||
"status",
|
||||
"state",
|
||||
"state changed",
|
||||
"message",
|
||||
},
|
||||
),
|
||||
cliui.ChangeFormatterData(
|
||||
cliui.JSONFormat(),
|
||||
func(data any) (any, error) {
|
||||
rows, ok := data.([]taskListRow)
|
||||
if !ok {
|
||||
return nil, xerrors.Errorf("expected []taskListRow, got %T", data)
|
||||
}
|
||||
out := make([]codersdk.Task, len(rows))
|
||||
for i := range rows {
|
||||
out[i] = rows[i].Task
|
||||
}
|
||||
return out, nil
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
cmd := &serpent.Command{
|
||||
Use: "list",
|
||||
Short: "List experimental tasks",
|
||||
Aliases: []string{"ls"},
|
||||
Middleware: serpent.Chain(
|
||||
serpent.RequireNArgs(0),
|
||||
r.InitClient(client),
|
||||
),
|
||||
Options: serpent.OptionSet{
|
||||
{
|
||||
Name: "status",
|
||||
Description: "Filter by task status (e.g. running, failed, etc).",
|
||||
Flag: "status",
|
||||
Default: "",
|
||||
Value: serpent.StringOf(&statusFilter),
|
||||
},
|
||||
{
|
||||
Name: "all",
|
||||
Description: "List tasks for all users you can view.",
|
||||
Flag: "all",
|
||||
FlagShorthand: "a",
|
||||
Default: "false",
|
||||
Value: serpent.BoolOf(&all),
|
||||
},
|
||||
{
|
||||
Name: "user",
|
||||
Description: "List tasks for the specified user (username, \"me\").",
|
||||
Flag: "user",
|
||||
Default: "",
|
||||
Value: serpent.StringOf(&user),
|
||||
},
|
||||
},
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
ctx := inv.Context()
|
||||
exp := codersdk.NewExperimentalClient(client)
|
||||
|
||||
targetUser := strings.TrimSpace(user)
|
||||
if targetUser == "" && !all {
|
||||
targetUser = codersdk.Me
|
||||
}
|
||||
|
||||
tasks, err := exp.Tasks(ctx, &codersdk.TasksFilter{
|
||||
Owner: targetUser,
|
||||
Status: statusFilter,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("list tasks: %w", err)
|
||||
}
|
||||
|
||||
// If no rows and not JSON, show a friendly message.
|
||||
if len(tasks) == 0 && formatter.FormatID() != cliui.JSONFormat().ID() {
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "No tasks found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
rows := make([]taskListRow, len(tasks))
|
||||
now := time.Now()
|
||||
for i := range tasks {
|
||||
rows[i] = taskListRowFromTask(now, tasks[i])
|
||||
}
|
||||
|
||||
out, err := formatter.Format(ctx, rows)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("format tasks: %w", err)
|
||||
}
|
||||
_, _ = fmt.Fprintln(inv.Stdout, out)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
formatter.AttachOptions(&cmd.Options)
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
|
||||
"github.com/coder/coder/v2/cli/clitest"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbfake"
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/pty/ptytest"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
// makeAITask creates an AI-task workspace.
|
||||
func makeAITask(t *testing.T, db database.Store, orgID, adminID, ownerID uuid.UUID, transition database.WorkspaceTransition, prompt string) (workspace database.WorkspaceTable) {
|
||||
t.Helper()
|
||||
|
||||
tv := dbfake.TemplateVersion(t, db).
|
||||
Seed(database.TemplateVersion{
|
||||
OrganizationID: orgID,
|
||||
CreatedBy: adminID,
|
||||
HasAITask: sql.NullBool{
|
||||
Bool: true,
|
||||
Valid: true,
|
||||
},
|
||||
}).Do()
|
||||
|
||||
ws := database.WorkspaceTable{
|
||||
OrganizationID: orgID,
|
||||
OwnerID: ownerID,
|
||||
TemplateID: tv.Template.ID,
|
||||
}
|
||||
build := dbfake.WorkspaceBuild(t, db, ws).
|
||||
Seed(database.WorkspaceBuild{
|
||||
TemplateVersionID: tv.TemplateVersion.ID,
|
||||
Transition: transition,
|
||||
}).WithAgent().Do()
|
||||
dbgen.WorkspaceBuildParameters(t, db, []database.WorkspaceBuildParameter{
|
||||
{
|
||||
WorkspaceBuildID: build.Build.ID,
|
||||
Name: codersdk.AITaskPromptParameterName,
|
||||
Value: prompt,
|
||||
},
|
||||
})
|
||||
agents, err := db.GetWorkspaceAgentsByWorkspaceAndBuildNumber(
|
||||
dbauthz.AsSystemRestricted(context.Background()),
|
||||
database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams{
|
||||
WorkspaceID: build.Workspace.ID,
|
||||
BuildNumber: build.Build.BuildNumber,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, agents)
|
||||
agentID := agents[0].ID
|
||||
|
||||
// Create a workspace app and set it as the sidebar app.
|
||||
app := dbgen.WorkspaceApp(t, db, database.WorkspaceApp{
|
||||
AgentID: agentID,
|
||||
Slug: "task-sidebar",
|
||||
DisplayName: "Task Sidebar",
|
||||
External: false,
|
||||
})
|
||||
|
||||
// Update build flags to reference the sidebar app and HasAITask=true.
|
||||
err = db.UpdateWorkspaceBuildFlagsByID(
|
||||
dbauthz.AsSystemRestricted(context.Background()),
|
||||
database.UpdateWorkspaceBuildFlagsByIDParams{
|
||||
ID: build.Build.ID,
|
||||
HasAITask: sql.NullBool{Bool: true, Valid: true},
|
||||
HasExternalAgent: sql.NullBool{Bool: false, Valid: false},
|
||||
SidebarAppID: uuid.NullUUID{UUID: app.ID, Valid: true},
|
||||
UpdatedAt: build.Build.UpdatedAt,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
return build.Workspace
|
||||
}
|
||||
|
||||
func TestExpTaskList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("NoTasks_Table", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Quiet logger to reduce noise.
|
||||
quiet := slog.Make(sloghuman.Sink(io.Discard))
|
||||
client, _ := coderdtest.NewWithDatabase(t, &coderdtest.Options{Logger: &quiet})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
memberClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
inv, root := clitest.New(t, "exp", "task", "list")
|
||||
clitest.SetupConfig(t, memberClient, root)
|
||||
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
pty.ExpectMatch("No tasks found.")
|
||||
})
|
||||
|
||||
t.Run("Single_Table", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Quiet logger to reduce noise.
|
||||
quiet := slog.Make(sloghuman.Sink(io.Discard))
|
||||
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{Logger: &quiet})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
wantPrompt := "build me a web app"
|
||||
ws := makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStart, wantPrompt)
|
||||
|
||||
inv, root := clitest.New(t, "exp", "task", "list", "--column", "id,name,status,initial prompt")
|
||||
clitest.SetupConfig(t, memberClient, root)
|
||||
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Validate the table includes the task and status.
|
||||
pty.ExpectMatch(ws.Name)
|
||||
pty.ExpectMatch("running")
|
||||
pty.ExpectMatch(wantPrompt)
|
||||
})
|
||||
|
||||
t.Run("StatusFilter_JSON", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Quiet logger to reduce noise.
|
||||
quiet := slog.Make(sloghuman.Sink(io.Discard))
|
||||
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{Logger: &quiet})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
// Create two AI tasks: one running, one stopped.
|
||||
running := makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStart, "keep me running")
|
||||
stopped := makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStop, "stop me please")
|
||||
|
||||
// Use JSON output to reliably validate filtering.
|
||||
inv, root := clitest.New(t, "exp", "task", "list", "--status=stopped", "--output=json")
|
||||
clitest.SetupConfig(t, memberClient, root)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
var stdout bytes.Buffer
|
||||
inv.Stdout = &stdout
|
||||
inv.Stderr = &stdout
|
||||
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
var tasks []codersdk.Task
|
||||
require.NoError(t, json.Unmarshal(stdout.Bytes(), &tasks))
|
||||
|
||||
// Only the stopped task is returned.
|
||||
require.Len(t, tasks, 1, "expected one task after filtering")
|
||||
require.Equal(t, stopped.ID, tasks[0].ID)
|
||||
require.NotEqual(t, running.ID, tasks[0].ID)
|
||||
})
|
||||
|
||||
t.Run("UserFlag_Me_Table", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
quiet := slog.Make(sloghuman.Sink(io.Discard))
|
||||
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{Logger: &quiet})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
_, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
_ = makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStart, "other-task")
|
||||
ws := makeAITask(t, db, owner.OrganizationID, owner.UserID, owner.UserID, database.WorkspaceTransitionStart, "me-task")
|
||||
|
||||
inv, root := clitest.New(t, "exp", "task", "list", "--user", "me")
|
||||
//nolint:gocritic // Owner client is intended here smoke test the member task not showing up.
|
||||
clitest.SetupConfig(t, client, root)
|
||||
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
pty.ExpectMatch(ws.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpTaskList_OwnerCanListOthers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Quiet logger to reduce noise.
|
||||
quiet := slog.Make(sloghuman.Sink(io.Discard))
|
||||
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{Logger: &quiet})
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
|
||||
// Create two additional members in the owner's organization.
|
||||
_, memberAUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
_, memberBUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
|
||||
// Seed an AI task for member A and B.
|
||||
_ = makeAITask(t, db, owner.OrganizationID, owner.UserID, memberAUser.ID, database.WorkspaceTransitionStart, "member-A-task")
|
||||
_ = makeAITask(t, db, owner.OrganizationID, owner.UserID, memberBUser.ID, database.WorkspaceTransitionStart, "member-B-task")
|
||||
|
||||
t.Run("OwnerListsSpecificUserWithUserFlag_JSON", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// As the owner, list only member A tasks.
|
||||
inv, root := clitest.New(t, "exp", "task", "list", "--user", memberAUser.Username, "--output=json")
|
||||
//nolint:gocritic // Owner client is intended here to allow member tasks to be listed.
|
||||
clitest.SetupConfig(t, ownerClient, root)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
inv.Stdout = &stdout
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
var tasks []codersdk.Task
|
||||
require.NoError(t, json.Unmarshal(stdout.Bytes(), &tasks))
|
||||
|
||||
// At least one task to belong to member A.
|
||||
require.NotEmpty(t, tasks, "expected at least one task for member A")
|
||||
// All tasks should belong to member A.
|
||||
for _, task := range tasks {
|
||||
require.Equal(t, memberAUser.ID, task.OwnerID, "expected only member A tasks")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("OwnerListsAllWithAllFlag_JSON", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// As the owner, list all tasks to verify both member tasks are present.
|
||||
// Use JSON output to reliably validate filtering.
|
||||
inv, root := clitest.New(t, "exp", "task", "list", "--all", "--output=json")
|
||||
//nolint:gocritic // Owner client is intended here to allow all tasks to be listed.
|
||||
clitest.SetupConfig(t, ownerClient, root)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
inv.Stdout = &stdout
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
var tasks []codersdk.Task
|
||||
require.NoError(t, json.Unmarshal(stdout.Bytes(), &tasks))
|
||||
|
||||
// Expect at least two tasks and ensure both owners (member A and member B) are represented.
|
||||
require.GreaterOrEqual(t, len(tasks), 2, "expected two or more tasks in --all listing")
|
||||
|
||||
// Use slice.Find for concise existence checks.
|
||||
_, foundA := slice.Find(tasks, func(t codersdk.Task) bool { return t.OwnerID == memberAUser.ID })
|
||||
_, foundB := slice.Find(tasks, func(t codersdk.Task) bool { return t.OwnerID == memberBUser.ID })
|
||||
|
||||
require.True(t, foundA, "expected at least one task for member A in --all listing")
|
||||
require.True(t, foundB, "expected at least one task for member B in --all listing")
|
||||
})
|
||||
}
|
||||
+1
-1
@@ -280,7 +280,7 @@ func (api *API) tasksList(rw http.ResponseWriter, r *http.Request) {
|
||||
// Ensure that we only include AI task workspaces in the results.
|
||||
filter.HasAITask = sql.NullBool{Valid: true, Bool: true}
|
||||
|
||||
if filter.OwnerUsername == "me" || filter.OwnerUsername == "" {
|
||||
if filter.OwnerUsername == "me" {
|
||||
filter.OwnerID = apiKey.UserID
|
||||
filter.OwnerUsername = ""
|
||||
}
|
||||
|
||||
+2
-2
@@ -1008,10 +1008,10 @@ func New(options *Options) *API {
|
||||
r.Route("/tasks", func(r chi.Router) {
|
||||
r.Use(apiRateLimiter)
|
||||
|
||||
r.Get("/", api.tasksList)
|
||||
|
||||
r.Route("/{user}", func(r chi.Router) {
|
||||
r.Use(httpmw.ExtractOrganizationMembersParam(options.Database, api.HTTPAuth.Authorize))
|
||||
|
||||
r.Get("/", api.tasksList)
|
||||
r.Get("/{id}", api.taskGet)
|
||||
r.Post("/", api.tasksCreate)
|
||||
})
|
||||
|
||||
+30
-20
@@ -88,35 +88,41 @@ const (
|
||||
//
|
||||
// Experimental: This type is experimental and may change in the future.
|
||||
type Task struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
|
||||
OwnerID uuid.UUID `json:"owner_id" format:"uuid"`
|
||||
Name string `json:"name"`
|
||||
TemplateID uuid.UUID `json:"template_id" format:"uuid"`
|
||||
WorkspaceID uuid.NullUUID `json:"workspace_id" format:"uuid"`
|
||||
InitialPrompt string `json:"initial_prompt"`
|
||||
Status WorkspaceStatus `json:"status" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted"`
|
||||
CurrentState *TaskStateEntry `json:"current_state"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||
ID uuid.UUID `json:"id" format:"uuid" table:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" format:"uuid" table:"organization id"`
|
||||
OwnerID uuid.UUID `json:"owner_id" format:"uuid" table:"owner id"`
|
||||
Name string `json:"name" table:"name,default_sort"`
|
||||
TemplateID uuid.UUID `json:"template_id" format:"uuid" table:"template id"`
|
||||
WorkspaceID uuid.NullUUID `json:"workspace_id" format:"uuid" table:"workspace id"`
|
||||
InitialPrompt string `json:"initial_prompt" table:"initial prompt"`
|
||||
Status WorkspaceStatus `json:"status" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted" table:"status"`
|
||||
CurrentState *TaskStateEntry `json:"current_state" table:"cs,recursive_inline"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time" table:"created at"`
|
||||
UpdatedAt time.Time `json:"updated_at" format:"date-time" table:"updated at"`
|
||||
}
|
||||
|
||||
// TaskStateEntry represents a single entry in the task's state history.
|
||||
//
|
||||
// Experimental: This type is experimental and may change in the future.
|
||||
type TaskStateEntry struct {
|
||||
Timestamp time.Time `json:"timestamp" format:"date-time"`
|
||||
State TaskState `json:"state" enum:"working,idle,completed,failed"`
|
||||
Message string `json:"message"`
|
||||
URI string `json:"uri"`
|
||||
Timestamp time.Time `json:"timestamp" format:"date-time" table:"-"`
|
||||
State TaskState `json:"state" enum:"working,idle,completed,failed" table:"state"`
|
||||
Message string `json:"message" table:"message"`
|
||||
URI string `json:"uri" table:"-"`
|
||||
}
|
||||
|
||||
// TasksFilter filters the list of tasks.
|
||||
//
|
||||
// Experimental: This type is experimental and may change in the future.
|
||||
type TasksFilter struct {
|
||||
// Owner can be a username, UUID, or "me"
|
||||
// Owner can be a username, UUID, or "me".
|
||||
Owner string `json:"owner,omitempty"`
|
||||
// Status is a task status.
|
||||
Status string `json:"status,omitempty" typescript:"-"`
|
||||
// Offset is the number of tasks to skip before returning results.
|
||||
Offset int `json:"offset,omitempty" typescript:"-"`
|
||||
// Limit is a limit on the number of tasks returned.
|
||||
Limit int `json:"limit,omitempty" typescript:"-"`
|
||||
}
|
||||
|
||||
// Tasks lists all tasks belonging to the user or specified owner.
|
||||
@@ -126,12 +132,16 @@ func (c *ExperimentalClient) Tasks(ctx context.Context, filter *TasksFilter) ([]
|
||||
if filter == nil {
|
||||
filter = &TasksFilter{}
|
||||
}
|
||||
user := filter.Owner
|
||||
if user == "" {
|
||||
user = "me"
|
||||
|
||||
var wsFilter WorkspaceFilter
|
||||
wsFilter.Owner = filter.Owner
|
||||
wsFilter.Status = filter.Status
|
||||
page := Pagination{
|
||||
Offset: filter.Offset,
|
||||
Limit: filter.Limit,
|
||||
}
|
||||
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/tasks/%s", user), nil)
|
||||
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/tasks", nil, wsFilter.asRequestOption(), page.asRequestOption())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user