Show steps in task page (#585)
This commit is contained in:
9
skyvern-frontend/src/hooks/useCostCalculator.ts
Normal file
9
skyvern-frontend/src/hooks/useCostCalculator.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { CostCalculatorContext } from "@/store/CostCalculatorContext";
|
||||
import { useContext } from "react";
|
||||
|
||||
function useCostCalculator() {
|
||||
const costCalculator = useContext(CostCalculatorContext);
|
||||
return costCalculator;
|
||||
}
|
||||
|
||||
export { useCostCalculator };
|
||||
@@ -30,11 +30,7 @@ function StepArtifactsLayout() {
|
||||
return <div>Error: {error?.message}</div>;
|
||||
}
|
||||
|
||||
if (!steps) {
|
||||
return <div>No steps found</div>;
|
||||
}
|
||||
|
||||
const activeStep = steps[activeIndex];
|
||||
const activeStep = steps?.[activeIndex];
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
|
||||
@@ -41,13 +41,9 @@ function StepNavigation({ activeIndex, onActiveIndexChange }: Props) {
|
||||
return <div>Error: {error?.message}</div>;
|
||||
}
|
||||
|
||||
if (!steps) {
|
||||
return <div>No steps found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="flex flex-col gap-4">
|
||||
{steps.map((step, index) => {
|
||||
{steps?.map((step, index) => {
|
||||
const isActive = activeIndex === index;
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { Status, TaskApiResponse } from "@/api/types";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
import { Status } from "@/api/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -19,13 +18,10 @@ import { toast } from "@/components/ui/use-toast";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { cn } from "@/util/utils";
|
||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
keepPreviousData,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { NavLink, Outlet, useParams } from "react-router-dom";
|
||||
import { TaskInfo } from "./TaskInfo";
|
||||
import { useTaskQuery } from "./hooks/useTaskQuery";
|
||||
|
||||
function TaskDetails() {
|
||||
const { taskId } = useParams();
|
||||
@@ -37,23 +33,7 @@ function TaskDetails() {
|
||||
isLoading: taskIsLoading,
|
||||
isError: taskIsError,
|
||||
error: taskError,
|
||||
} = useQuery<TaskApiResponse>({
|
||||
queryKey: ["task", taskId],
|
||||
queryFn: async () => {
|
||||
const client = await getClient(credentialGetter);
|
||||
return client.get(`/tasks/${taskId}`).then((response) => response.data);
|
||||
},
|
||||
refetchInterval: (query) => {
|
||||
if (
|
||||
query.state.data?.status === Status.Running ||
|
||||
query.state.data?.status === Status.Queued
|
||||
) {
|
||||
return 10000;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
} = useTaskQuery({ id: taskId });
|
||||
|
||||
const cancelTaskMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
@@ -124,11 +104,7 @@ function TaskDetails() {
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-lg">{taskId}</span>
|
||||
{taskIsLoading ? (
|
||||
<Skeleton className="w-28 h-8" />
|
||||
) : task ? (
|
||||
<StatusBadge status={task?.status} />
|
||||
) : null}
|
||||
{taskId && <TaskInfo id={taskId} />}
|
||||
</div>
|
||||
{taskIsRunningOrQueued && (
|
||||
<Dialog>
|
||||
|
||||
90
skyvern-frontend/src/routes/tasks/detail/TaskInfo.tsx
Normal file
90
skyvern-frontend/src/routes/tasks/detail/TaskInfo.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Status, StepApiResponse } from "@/api/types";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { useTaskQuery } from "./hooks/useTaskQuery";
|
||||
import { useCostCalculator } from "@/hooks/useCostCalculator";
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const formatter = Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
});
|
||||
|
||||
function TaskInfo({ id }: Props) {
|
||||
const credentialGetter = useCredentialGetter();
|
||||
const costCalculator = useCostCalculator();
|
||||
const {
|
||||
data: task,
|
||||
isLoading: taskIsLoading,
|
||||
isError: taskIsError,
|
||||
} = useTaskQuery({ id });
|
||||
|
||||
const taskIsRunningOrQueued =
|
||||
task?.status === Status.Running || task?.status === Status.Queued;
|
||||
|
||||
const {
|
||||
data: steps,
|
||||
isLoading: stepsIsLoading,
|
||||
isError: stepsIsError,
|
||||
} = useQuery<Array<StepApiResponse>>({
|
||||
queryKey: ["task", id, "steps"],
|
||||
queryFn: async () => {
|
||||
const client = await getClient(credentialGetter);
|
||||
return client.get(`/tasks/${id}/steps`).then((response) => response.data);
|
||||
},
|
||||
refetchOnWindowFocus: taskIsRunningOrQueued,
|
||||
refetchInterval: taskIsRunningOrQueued ? 5000 : false,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
if (stepsIsLoading || taskIsLoading) {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<Skeleton className="w-20 h-6" />
|
||||
<Skeleton className="w-20 h-6" />
|
||||
<Skeleton className="w-20 h-6" />
|
||||
<Skeleton className="w-20 h-6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (stepsIsError || taskIsError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const actionCount = steps?.reduce((acc, step) => {
|
||||
const actionsAndResults = step.output?.actions_and_results ?? [];
|
||||
|
||||
const actionCount = actionsAndResults.reduce((acc, actionAndResult) => {
|
||||
const actionResult = actionAndResult[1];
|
||||
if (actionResult.length === 0) {
|
||||
return acc;
|
||||
}
|
||||
return acc + 1;
|
||||
}, 0);
|
||||
return acc + actionCount;
|
||||
}, 0);
|
||||
|
||||
const showCost = typeof costCalculator === "function";
|
||||
const notRunningSteps = steps?.filter((step) => step.status !== "running");
|
||||
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
{task && <StatusBadge status={task.status} />}
|
||||
<Badge>Steps: {notRunningSteps?.length}</Badge>
|
||||
<Badge>Actions: {actionCount}</Badge>
|
||||
{showCost && (
|
||||
<Badge>Cost: {formatter.format(costCalculator(steps ?? []))}</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TaskInfo };
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { Status, TaskApiResponse } from "@/api/types";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
|
||||
type Props = {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
function useTaskQuery({ id }: Props) {
|
||||
const credentialGetter = useCredentialGetter();
|
||||
|
||||
return useQuery<TaskApiResponse>({
|
||||
queryKey: ["task", id],
|
||||
queryFn: async () => {
|
||||
const client = await getClient(credentialGetter);
|
||||
return client.get(`/tasks/${id}`).then((response) => response.data);
|
||||
},
|
||||
enabled: !!id,
|
||||
refetchInterval: (query) => {
|
||||
if (
|
||||
query.state.data?.status === Status.Running ||
|
||||
query.state.data?.status === Status.Queued
|
||||
) {
|
||||
return 5000;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
}
|
||||
|
||||
export { useTaskQuery };
|
||||
8
skyvern-frontend/src/store/CostCalculatorContext.ts
Normal file
8
skyvern-frontend/src/store/CostCalculatorContext.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { StepApiResponse } from "@/api/types";
|
||||
import { createContext } from "react";
|
||||
|
||||
type TaskCostCalculator = (steps: Array<StepApiResponse>) => number;
|
||||
|
||||
const CostCalculatorContext = createContext<TaskCostCalculator | null>(null);
|
||||
|
||||
export { CostCalculatorContext };
|
||||
Reference in New Issue
Block a user