mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: modify task creation endpoint to return a task, not workspace (#19637)
Relates to https://github.com/coder/internal/issues/898 Refactor the `POST /api/experimental/tasks/{user}` endpoint to return a codersdk.Task instead of a codersdk.Workspace
This commit is contained in:
@@ -104,7 +104,7 @@ func (r *RootCmd) taskCreate() *serpent.Command {
|
||||
templateVersionPresetID = preset.ID
|
||||
}
|
||||
|
||||
workspace, err := expClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
|
||||
task, err := expClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
|
||||
TemplateVersionID: templateVersionID,
|
||||
TemplateVersionPresetID: templateVersionPresetID,
|
||||
Prompt: taskInput,
|
||||
@@ -116,8 +116,8 @@ func (r *RootCmd) taskCreate() *serpent.Command {
|
||||
_, _ = fmt.Fprintf(
|
||||
inv.Stdout,
|
||||
"The task %s has been created at %s!\n",
|
||||
cliui.Keyword(workspace.Name),
|
||||
cliui.Timestamp(workspace.CreatedAt),
|
||||
cliui.Keyword(task.Name),
|
||||
cliui.Timestamp(task.CreatedAt),
|
||||
)
|
||||
|
||||
return nil
|
||||
|
||||
+60
-56
@@ -188,7 +188,6 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) {
|
||||
WorkspaceOwner: owner.Username,
|
||||
},
|
||||
})
|
||||
|
||||
defer commitAudit()
|
||||
w, err := createWorkspace(ctx, aReq, apiKey.UserID, api, owner, createReq, r)
|
||||
if err != nil {
|
||||
@@ -196,7 +195,65 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, w)
|
||||
task := taskFromWorkspace(w, req.Prompt)
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, task)
|
||||
}
|
||||
|
||||
func taskFromWorkspace(ws codersdk.Workspace, initialPrompt string) codersdk.Task {
|
||||
// TODO(DanielleMaywood):
|
||||
// This just picks up the first agent it discovers.
|
||||
// This approach _might_ break when a task has multiple agents,
|
||||
// depending on which agent was found first.
|
||||
//
|
||||
// We explicitly do not have support for running tasks
|
||||
// inside of a sub agent at the moment, so we can be sure
|
||||
// that any sub agents are not the agent we're looking for.
|
||||
var taskAgentID uuid.NullUUID
|
||||
var taskAgentLifecycle *codersdk.WorkspaceAgentLifecycle
|
||||
var taskAgentHealth *codersdk.WorkspaceAgentHealth
|
||||
for _, resource := range ws.LatestBuild.Resources {
|
||||
for _, agent := range resource.Agents {
|
||||
if agent.ParentID.Valid {
|
||||
continue
|
||||
}
|
||||
|
||||
taskAgentID = uuid.NullUUID{Valid: true, UUID: agent.ID}
|
||||
taskAgentLifecycle = &agent.LifecycleState
|
||||
taskAgentHealth = &agent.Health
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var currentState *codersdk.TaskStateEntry
|
||||
if ws.LatestAppStatus != nil {
|
||||
currentState = &codersdk.TaskStateEntry{
|
||||
Timestamp: ws.LatestAppStatus.CreatedAt,
|
||||
State: codersdk.TaskState(ws.LatestAppStatus.State),
|
||||
Message: ws.LatestAppStatus.Message,
|
||||
URI: ws.LatestAppStatus.URI,
|
||||
}
|
||||
}
|
||||
|
||||
return codersdk.Task{
|
||||
ID: ws.ID,
|
||||
OrganizationID: ws.OrganizationID,
|
||||
OwnerID: ws.OwnerID,
|
||||
OwnerName: ws.OwnerName,
|
||||
Name: ws.Name,
|
||||
TemplateID: ws.TemplateID,
|
||||
TemplateName: ws.TemplateName,
|
||||
TemplateDisplayName: ws.TemplateDisplayName,
|
||||
TemplateIcon: ws.TemplateIcon,
|
||||
WorkspaceID: uuid.NullUUID{Valid: true, UUID: ws.ID},
|
||||
WorkspaceAgentID: taskAgentID,
|
||||
WorkspaceAgentLifecycle: taskAgentLifecycle,
|
||||
WorkspaceAgentHealth: taskAgentHealth,
|
||||
CreatedAt: ws.CreatedAt,
|
||||
UpdatedAt: ws.UpdatedAt,
|
||||
InitialPrompt: initialPrompt,
|
||||
Status: ws.LatestBuild.Status,
|
||||
CurrentState: currentState,
|
||||
}
|
||||
}
|
||||
|
||||
// tasksFromWorkspaces converts a slice of API workspaces into tasks, fetching
|
||||
@@ -221,60 +278,7 @@ func (api *API) tasksFromWorkspaces(ctx context.Context, apiWorkspaces []codersd
|
||||
|
||||
tasks := make([]codersdk.Task, 0, len(apiWorkspaces))
|
||||
for _, ws := range apiWorkspaces {
|
||||
// TODO(DanielleMaywood):
|
||||
// This just picks up the first agent it discovers.
|
||||
// This approach _might_ break when a task has multiple agents,
|
||||
// depending on which agent was found first.
|
||||
//
|
||||
// We explicitly do not have support for running tasks
|
||||
// inside of a sub agent at the moment, so we can be sure
|
||||
// that any sub agents are not the agent we're looking for.
|
||||
var taskAgentID uuid.NullUUID
|
||||
var taskAgentLifecycle *codersdk.WorkspaceAgentLifecycle
|
||||
var taskAgentHealth *codersdk.WorkspaceAgentHealth
|
||||
for _, resource := range ws.LatestBuild.Resources {
|
||||
for _, agent := range resource.Agents {
|
||||
if agent.ParentID.Valid {
|
||||
continue
|
||||
}
|
||||
|
||||
taskAgentID = uuid.NullUUID{Valid: true, UUID: agent.ID}
|
||||
taskAgentLifecycle = &agent.LifecycleState
|
||||
taskAgentHealth = &agent.Health
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var currentState *codersdk.TaskStateEntry
|
||||
if ws.LatestAppStatus != nil {
|
||||
currentState = &codersdk.TaskStateEntry{
|
||||
Timestamp: ws.LatestAppStatus.CreatedAt,
|
||||
State: codersdk.TaskState(ws.LatestAppStatus.State),
|
||||
Message: ws.LatestAppStatus.Message,
|
||||
URI: ws.LatestAppStatus.URI,
|
||||
}
|
||||
}
|
||||
|
||||
tasks = append(tasks, codersdk.Task{
|
||||
ID: ws.ID,
|
||||
OrganizationID: ws.OrganizationID,
|
||||
OwnerID: ws.OwnerID,
|
||||
OwnerName: ws.OwnerName,
|
||||
Name: ws.Name,
|
||||
TemplateID: ws.TemplateID,
|
||||
TemplateName: ws.TemplateName,
|
||||
TemplateDisplayName: ws.TemplateDisplayName,
|
||||
TemplateIcon: ws.TemplateIcon,
|
||||
WorkspaceID: uuid.NullUUID{Valid: true, UUID: ws.ID},
|
||||
WorkspaceAgentID: taskAgentID,
|
||||
WorkspaceAgentLifecycle: taskAgentLifecycle,
|
||||
WorkspaceAgentHealth: taskAgentHealth,
|
||||
CreatedAt: ws.CreatedAt,
|
||||
UpdatedAt: ws.UpdatedAt,
|
||||
InitialPrompt: promptsByBuildID[ws.LatestBuild.ID],
|
||||
Status: ws.LatestBuild.Status,
|
||||
CurrentState: currentState,
|
||||
})
|
||||
tasks = append(tasks, taskFromWorkspace(ws, promptsByBuildID[ws.LatestBuild.ID]))
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
|
||||
@@ -419,19 +419,23 @@ func TestTasksCreate(t *testing.T) {
|
||||
expClient := codersdk.NewExperimentalClient(client)
|
||||
|
||||
// When: We attempt to create a Task.
|
||||
workspace, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{
|
||||
task, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{
|
||||
TemplateVersionID: template.ActiveVersionID,
|
||||
Prompt: taskPrompt,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
require.True(t, task.WorkspaceID.Valid)
|
||||
|
||||
ws, err := client.Workspace(ctx, task.WorkspaceID.UUID)
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||||
|
||||
// Then: We expect a workspace to have been created.
|
||||
assert.NotEmpty(t, workspace.Name)
|
||||
assert.Equal(t, template.ID, workspace.TemplateID)
|
||||
assert.NotEmpty(t, task.Name)
|
||||
assert.Equal(t, template.ID, task.TemplateID)
|
||||
|
||||
// And: We expect it to have the "AI Prompt" parameter correctly set.
|
||||
parameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
|
||||
parameters, err := client.WorkspaceBuildParameters(ctx, ws.LatestBuild.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, parameters, 1)
|
||||
assert.Equal(t, codersdk.AITaskPromptParameterName, parameters[0].Name)
|
||||
|
||||
+7
-7
@@ -53,23 +53,23 @@ type CreateTaskRequest struct {
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
|
||||
func (c *ExperimentalClient) CreateTask(ctx context.Context, user string, request CreateTaskRequest) (Workspace, error) {
|
||||
func (c *ExperimentalClient) CreateTask(ctx context.Context, user string, request CreateTaskRequest) (Task, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/tasks/%s", user), request)
|
||||
if err != nil {
|
||||
return Workspace{}, err
|
||||
return Task{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return Workspace{}, ReadBodyAsError(res)
|
||||
return Task{}, ReadBodyAsError(res)
|
||||
}
|
||||
|
||||
var workspace Workspace
|
||||
if err := json.NewDecoder(res.Body).Decode(&workspace); err != nil {
|
||||
return Workspace{}, err
|
||||
var task Task
|
||||
if err := json.NewDecoder(res.Body).Decode(&task); err != nil {
|
||||
return Task{}, err
|
||||
}
|
||||
|
||||
return workspace, nil
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// TaskState represents the high-level lifecycle of a task.
|
||||
|
||||
+2
-2
@@ -2686,8 +2686,8 @@ class ExperimentalApiMethods {
|
||||
createTask = async (
|
||||
user: string,
|
||||
req: TypesGen.CreateTaskRequest,
|
||||
): Promise<TypesGen.Workspace> => {
|
||||
const response = await this.axios.post<TypesGen.Workspace>(
|
||||
): Promise<TypesGen.Task> => {
|
||||
const response = await this.axios.post<TypesGen.Task>(
|
||||
`/api/experimental/tasks/${user}`,
|
||||
req,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { API } from "api/api";
|
||||
import { getErrorDetail, getErrorMessage } from "api/errors";
|
||||
import { templateVersionPresets } from "api/queries/templates";
|
||||
import type {
|
||||
Preset,
|
||||
Task,
|
||||
Template,
|
||||
TemplateVersionExternalAuth,
|
||||
} from "api/typesGenerated";
|
||||
@@ -28,13 +30,12 @@ import {
|
||||
import { useAuthenticated } from "hooks/useAuthenticated";
|
||||
import { useExternalAuth } from "hooks/useExternalAuth";
|
||||
import { RedoIcon, RotateCcwIcon, SendIcon } from "lucide-react";
|
||||
import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks";
|
||||
import { AI_PROMPT_PARAMETER_NAME } from "modules/tasks/tasks";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { useNavigate } from "react-router";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import { docs } from "utils/docs";
|
||||
import { data } from "./data";
|
||||
|
||||
const textareaPlaceholder = "Prompt your AI agent to start a task...";
|
||||
|
||||
@@ -64,7 +65,7 @@ export const TaskPrompt: FC<TaskPromptProps> = ({
|
||||
<CreateTaskForm
|
||||
templates={templates}
|
||||
onSuccess={(task) => {
|
||||
navigate(`/tasks/${task.workspace.owner_name}/${task.workspace.name}`);
|
||||
navigate(`/tasks/${task.owner_name}/${task.name}`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -188,12 +189,11 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
|
||||
|
||||
const createTaskMutation = useMutation({
|
||||
mutationFn: async ({ prompt }: CreateTaskMutationFnProps) =>
|
||||
data.createTask(
|
||||
API.experimental.createTask(user.id, {
|
||||
prompt,
|
||||
user.id,
|
||||
selectedTemplate.active_version_id,
|
||||
selectedPresetId,
|
||||
),
|
||||
template_version_id: selectedTemplate.active_version_id,
|
||||
template_version_preset_id: selectedPresetId,
|
||||
}),
|
||||
onSuccess: async (task) => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["tasks"],
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
MockAIPromptPresets,
|
||||
MockNewTaskData,
|
||||
MockPresets,
|
||||
MockTask,
|
||||
MockTasks,
|
||||
MockTemplate,
|
||||
MockTemplateVersionExternalAuthGithub,
|
||||
@@ -19,7 +20,6 @@ import { API } from "api/api";
|
||||
import { MockUsers } from "pages/UsersPage/storybookData/users";
|
||||
import { expect, spyOn, userEvent, waitFor, within } from "storybook/test";
|
||||
import { reactRouterParameters } from "storybook-addon-remix-react-router";
|
||||
import { data } from "./data";
|
||||
import TasksPage from "./TasksPage";
|
||||
|
||||
const meta: Meta<typeof TasksPage> = {
|
||||
@@ -248,7 +248,7 @@ export const CreateTaskSuccessfully: Story = {
|
||||
spyOn(API.experimental, "getTasks")
|
||||
.mockResolvedValueOnce(MockTasks)
|
||||
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
|
||||
spyOn(data, "createTask").mockResolvedValue(MockNewTaskData);
|
||||
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
|
||||
},
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const canvas = within(canvasElement);
|
||||
@@ -272,7 +272,7 @@ export const CreateTaskError: Story = {
|
||||
beforeEach: () => {
|
||||
spyOn(API, "getTemplates").mockResolvedValue([MockTemplate]);
|
||||
spyOn(API.experimental, "getTasks").mockResolvedValue(MockTasks);
|
||||
spyOn(data, "createTask").mockRejectedValue(
|
||||
spyOn(API.experimental, "createTask").mockRejectedValue(
|
||||
mockApiError({
|
||||
message: "Failed to create task",
|
||||
detail: "You don't have permission to create tasks.",
|
||||
@@ -301,7 +301,7 @@ export const WithAuthenticatedExternalAuth: Story = {
|
||||
spyOn(API.experimental, "getTasks")
|
||||
.mockResolvedValueOnce(MockTasks)
|
||||
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
|
||||
spyOn(data, "createTask").mockResolvedValue(MockNewTaskData);
|
||||
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
|
||||
spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([
|
||||
MockTemplateVersionExternalAuthGithubAuthenticated,
|
||||
]);
|
||||
@@ -327,7 +327,7 @@ export const MissingExternalAuth: Story = {
|
||||
spyOn(API.experimental, "getTasks")
|
||||
.mockResolvedValueOnce(MockTasks)
|
||||
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
|
||||
spyOn(data, "createTask").mockResolvedValue(MockNewTaskData);
|
||||
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
|
||||
spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([
|
||||
MockTemplateVersionExternalAuthGithub,
|
||||
]);
|
||||
@@ -353,7 +353,7 @@ export const ExternalAuthError: Story = {
|
||||
spyOn(API.experimental, "getTasks")
|
||||
.mockResolvedValueOnce(MockTasks)
|
||||
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
|
||||
spyOn(data, "createTask").mockResolvedValue(MockNewTaskData);
|
||||
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
|
||||
spyOn(API, "getTemplateVersionExternalAuth").mockRejectedValue(
|
||||
mockApiError({
|
||||
message: "Failed to load external auth",
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { API } from "api/api";
|
||||
import type { Task } from "modules/tasks/tasks";
|
||||
|
||||
// TODO: This is a temporary solution while the BE does not return the Task in a
|
||||
// right shape with a custom name. This should be removed once the BE is fixed.
|
||||
export const data = {
|
||||
async createTask(
|
||||
prompt: string,
|
||||
userId: string,
|
||||
templateVersionId: string,
|
||||
presetId: string | undefined,
|
||||
): Promise<Task> {
|
||||
const workspace = await API.experimental.createTask(userId, {
|
||||
template_version_id: templateVersionId,
|
||||
template_version_preset_id: presetId,
|
||||
prompt,
|
||||
});
|
||||
|
||||
return {
|
||||
workspace,
|
||||
prompt,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -4903,6 +4903,32 @@ export const MockTasks = [
|
||||
},
|
||||
];
|
||||
|
||||
export const MockTask: TypesGen.Task = {
|
||||
id: "test-task",
|
||||
name: "task-wild-test-123",
|
||||
organization_id: MockOrganization.id,
|
||||
owner_id: MockUserOwner.id,
|
||||
owner_name: MockUserOwner.username,
|
||||
template_id: MockTemplate.id,
|
||||
template_name: MockTemplate.name,
|
||||
template_display_name: MockTemplate.display_name,
|
||||
template_icon: MockTemplate.icon,
|
||||
workspace_id: MockWorkspace.id,
|
||||
workspace_agent_id: MockWorkspaceAgent.id,
|
||||
workspace_agent_lifecycle: MockWorkspaceAgent.lifecycle_state,
|
||||
workspace_agent_health: MockWorkspaceAgent.health,
|
||||
initial_prompt: "Perform some task",
|
||||
status: "running",
|
||||
current_state: {
|
||||
timestamp: "2022-05-17T17:39:01.382927298Z",
|
||||
state: "idle",
|
||||
message: "Should I continue?",
|
||||
uri: "https://dev.coder.com",
|
||||
},
|
||||
created_at: "2022-05-17T17:39:01.382927298Z",
|
||||
updated_at: "2022-05-17T17:39:01.382927298Z",
|
||||
};
|
||||
|
||||
export const MockNewTaskData = {
|
||||
prompt: "Create a new task",
|
||||
workspace: {
|
||||
|
||||
Reference in New Issue
Block a user