Make it easy to return to workflow run from a task (#978)

This commit is contained in:
Shuchang Zheng
2024-10-15 10:12:40 -07:00
committed by GitHub
parent 745b95c4dd
commit 4b2bf0a5f5
2 changed files with 121 additions and 74 deletions

View File

@@ -78,6 +78,7 @@ export type TaskApiResponse = {
failure_reason: string | null; failure_reason: string | null;
errors: Array<Record<string, unknown>>; errors: Array<Record<string, unknown>>;
max_steps_per_run: number | null; max_steps_per_run: number | null;
workflow_run_id: string | null;
}; };
export type CreateTaskRequest = { export type CreateTaskRequest = {

View File

@@ -1,5 +1,9 @@
import { getClient } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { Status, TaskApiResponse } from "@/api/types"; import {
Status,
TaskApiResponse,
WorkflowRunStatusApiResponse,
} from "@/api/types";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -18,7 +22,7 @@ import { toast } from "@/components/ui/use-toast";
import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { cn } from "@/util/utils"; import { cn } from "@/util/utils";
import { CopyIcon, PlayIcon, ReloadIcon } from "@radix-ui/react-icons"; import { CopyIcon, PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Link, NavLink, Outlet, useParams } from "react-router-dom"; import { Link, NavLink, Outlet, useParams } from "react-router-dom";
import { TaskInfo } from "./TaskInfo"; import { TaskInfo } from "./TaskInfo";
import { useTaskQuery } from "./hooks/useTaskQuery"; import { useTaskQuery } from "./hooks/useTaskQuery";
@@ -27,6 +31,7 @@ import fetchToCurl from "fetch-to-curl";
import { apiBaseUrl } from "@/util/env"; import { apiBaseUrl } from "@/util/env";
import { useApiCredential } from "@/hooks/useApiCredential"; import { useApiCredential } from "@/hooks/useApiCredential";
import { copyText } from "@/util/copyText"; import { copyText } from "@/util/copyText";
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
function createTaskRequestObject(values: TaskApiResponse) { function createTaskRequestObject(values: TaskApiResponse) {
return { return {
@@ -54,6 +59,30 @@ function TaskDetails() {
error: taskError, error: taskError,
} = useTaskQuery({ id: taskId }); } = useTaskQuery({ id: taskId });
const { data: workflowRun, isLoading: workflowRunIsLoading } =
useQuery<WorkflowRunStatusApiResponse>({
queryKey: ["workflowRun", task?.workflow_run_id],
queryFn: async () => {
const client = await getClient(credentialGetter);
return client
.get(`/workflows/runs/${task?.workflow_run_id}`)
.then((response) => response.data);
},
enabled: !!task?.workflow_run_id,
});
const { data: workflow, isLoading: workflowIsLoading } =
useQuery<WorkflowApiResponse>({
queryKey: ["workflow", workflowRun?.workflow_id],
queryFn: async () => {
const client = await getClient(credentialGetter);
return client
.get(`/workflows/${workflowRun?.workflow_id}`)
.then((response) => response.data);
},
enabled: !!workflowRun?.workflow_id,
});
const cancelTaskMutation = useMutation({ const cancelTaskMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
const client = await getClient(credentialGetter); const client = await getClient(credentialGetter);
@@ -122,82 +151,99 @@ function TaskDetails() {
return ( return (
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<div className="flex items-center justify-between"> <header className="space-y-3">
<div className="flex items-center gap-4"> <div className="flex items-center justify-between">
<span className="text-lg">{taskId}</span> <div className="flex items-center gap-4">
{taskId && <TaskInfo id={taskId} />} <span className="text-3xl">{taskId}</span>
</div> {taskId && <TaskInfo id={taskId} />}
<div className="flex items-center gap-2"> </div>
<Button <div className="flex items-center gap-2">
variant="secondary" <Button
onClick={() => { variant="secondary"
if (!task) { onClick={() => {
return; if (!task) {
} return;
const curl = fetchToCurl({ }
method: "POST", const curl = fetchToCurl({
url: `${apiBaseUrl}/tasks`, method: "POST",
body: createTaskRequestObject(task), url: `${apiBaseUrl}/tasks`,
headers: { body: createTaskRequestObject(task),
"Content-Type": "application/json", headers: {
"x-api-key": apiCredential ?? "<your-api-key>", "Content-Type": "application/json",
}, "x-api-key": apiCredential ?? "<your-api-key>",
}); },
copyText(curl).then(() => {
toast({
variant: "success",
title: "Copied to Clipboard",
description:
"The cURL command has been copied to your clipboard.",
}); });
}); copyText(curl).then(() => {
}} toast({
> variant: "success",
<CopyIcon className="mr-2 h-4 w-4" /> title: "Copied to Clipboard",
cURL description:
</Button> "The cURL command has been copied to your clipboard.",
{taskIsRunningOrQueued && ( });
<Dialog> });
<DialogTrigger asChild> }}
<Button variant="destructive">Cancel</Button> >
</DialogTrigger> <CopyIcon className="mr-2 h-4 w-4" />
<DialogContent> cURL
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
Are you sure you want to cancel this task?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="secondary">Back</Button>
</DialogClose>
<Button
variant="destructive"
onClick={() => {
cancelTaskMutation.mutate();
}}
disabled={cancelTaskMutation.isPending}
>
{cancelTaskMutation.isPending && (
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
)}
Cancel Task
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
{taskHasTerminalState && (
<Button asChild>
<Link to={`/create/retry/${task.task_id}`}>
<PlayIcon className="mr-2 h-4 w-4" />
Rerun
</Link>
</Button> </Button>
{taskIsRunningOrQueued && (
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Cancel</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
Are you sure you want to cancel this task?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="secondary">Back</Button>
</DialogClose>
<Button
variant="destructive"
onClick={() => {
cancelTaskMutation.mutate();
}}
disabled={cancelTaskMutation.isPending}
>
{cancelTaskMutation.isPending && (
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
)}
Cancel Task
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
{taskHasTerminalState && (
<Button asChild>
<Link to={`/create/retry/${task.task_id}`}>
<PlayIcon className="mr-2 h-4 w-4" />
Rerun
</Link>
</Button>
)}
</div>
</div>
<div className="text-2xl text-slate-400 underline underline-offset-4">
{workflowIsLoading || workflowRunIsLoading ? (
<Skeleton className="h-8 w-64" />
) : (
workflow &&
workflowRun && (
<Link
to={`/workflows/${workflow.workflow_permanent_id}/${workflowRun.workflow_run_id}`}
>
{workflow.title}
</Link>
)
)} )}
</div> </div>
</div> </header>
{taskIsLoading ? ( {taskIsLoading ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Skeleton className="h-32 w-32" /> <Skeleton className="h-32 w-32" />