Task cancelling frontend (#522)
This commit is contained in:
@@ -19,6 +19,7 @@ export const Status = {
|
|||||||
Completed: "completed",
|
Completed: "completed",
|
||||||
Queued: "queued",
|
Queued: "queued",
|
||||||
TimedOut: "timed_out",
|
TimedOut: "timed_out",
|
||||||
|
Canceled: "canceled",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type Status = (typeof Status)[keyof typeof Status];
|
export type Status = (typeof Status)[keyof typeof Status];
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ function StatusBadge({ status }: Props) {
|
|||||||
} else if (
|
} else if (
|
||||||
status === "failed" ||
|
status === "failed" ||
|
||||||
status === "terminated" ||
|
status === "terminated" ||
|
||||||
status === "timed_out"
|
status === "timed_out" ||
|
||||||
|
status === "canceled"
|
||||||
) {
|
) {
|
||||||
variant = "destructive";
|
variant = "destructive";
|
||||||
} else if (status === "running") {
|
} else if (status === "running") {
|
||||||
|
|||||||
@@ -1,17 +1,36 @@
|
|||||||
import { getClient } from "@/api/AxiosClient";
|
import { getClient } from "@/api/AxiosClient";
|
||||||
import { Status, TaskApiResponse } from "@/api/types";
|
import { Status, TaskApiResponse } from "@/api/types";
|
||||||
import { StatusBadge } from "@/components/StatusBadge";
|
import { StatusBadge } from "@/components/StatusBadge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
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 { keepPreviousData, useQuery } from "@tanstack/react-query";
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||||
|
import {
|
||||||
|
keepPreviousData,
|
||||||
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
useQueryClient,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
import { NavLink, Outlet, useParams } from "react-router-dom";
|
import { NavLink, Outlet, useParams } from "react-router-dom";
|
||||||
|
|
||||||
function TaskDetails() {
|
function TaskDetails() {
|
||||||
const { taskId } = useParams();
|
const { taskId } = useParams();
|
||||||
const credentialGetter = useCredentialGetter();
|
const credentialGetter = useCredentialGetter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: task,
|
data: task,
|
||||||
@@ -36,6 +55,35 @@ function TaskDetails() {
|
|||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cancelTaskMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const client = await getClient(credentialGetter);
|
||||||
|
return client
|
||||||
|
.post(`/tasks/${taskId}/cancel`)
|
||||||
|
.then((response) => response.data);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["task", taskId],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["tasks"],
|
||||||
|
});
|
||||||
|
toast({
|
||||||
|
variant: "success",
|
||||||
|
title: "Task Canceled",
|
||||||
|
description: "The task has been successfully canceled.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Error",
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (taskIsError) {
|
if (taskIsError) {
|
||||||
return <div>Error: {taskError?.message}</div>;
|
return <div>Error: {taskError?.message}</div>;
|
||||||
}
|
}
|
||||||
@@ -53,6 +101,9 @@ function TaskDetails() {
|
|||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
const taskIsRunningOrQueued =
|
||||||
|
task?.status === Status.Running || task?.status === Status.Queued;
|
||||||
|
|
||||||
const showFailureReason =
|
const showFailureReason =
|
||||||
task?.status === Status.Failed ||
|
task?.status === Status.Failed ||
|
||||||
task?.status === Status.Terminated ||
|
task?.status === Status.Terminated ||
|
||||||
@@ -70,13 +121,47 @@ function TaskDetails() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-lg">{taskId}</span>
|
<div className="flex items-center gap-4">
|
||||||
{taskIsLoading ? (
|
<span className="text-lg">{taskId}</span>
|
||||||
<Skeleton className="w-28 h-8" />
|
{taskIsLoading ? (
|
||||||
) : task ? (
|
<Skeleton className="w-28 h-8" />
|
||||||
<StatusBadge status={task?.status} />
|
) : task ? (
|
||||||
) : null}
|
<StatusBadge status={task?.status} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{taskIsLoading ? (
|
{taskIsLoading ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ function TaskHistory() {
|
|||||||
params.append("task_status", "failed");
|
params.append("task_status", "failed");
|
||||||
params.append("task_status", "terminated");
|
params.append("task_status", "terminated");
|
||||||
params.append("task_status", "timed_out");
|
params.append("task_status", "timed_out");
|
||||||
|
params.append("task_status", "canceled");
|
||||||
|
|
||||||
return client
|
return client
|
||||||
.get("/tasks", {
|
.get("/tasks", {
|
||||||
params,
|
params,
|
||||||
|
|||||||
Reference in New Issue
Block a user