diff --git a/skyvern-frontend/src/components/BadgeLoading.tsx b/skyvern-frontend/src/components/BadgeLoading.tsx new file mode 100644 index 00000000..a81e6398 --- /dev/null +++ b/skyvern-frontend/src/components/BadgeLoading.tsx @@ -0,0 +1,7 @@ +import { Skeleton } from "./ui/skeleton"; + +function BadgeLoading() { + return ; +} + +export { BadgeLoading }; diff --git a/skyvern-frontend/src/index.css b/skyvern-frontend/src/index.css index a0535dfe..79a0f63c 100644 --- a/skyvern-frontend/src/index.css +++ b/skyvern-frontend/src/index.css @@ -69,6 +69,8 @@ --border: 215.3 25% 26.7%; --input: 215.3 25% 26.7%; --ring: 212.7 26.8% 83.9%; + + --slate-elevation-2: 228 37% 11%; } } diff --git a/skyvern-frontend/src/routes/workflows/Workflows.tsx b/skyvern-frontend/src/routes/workflows/Workflows.tsx index 76f59810..b3199391 100644 --- a/skyvern-frontend/src/routes/workflows/Workflows.tsx +++ b/skyvern-frontend/src/routes/workflows/Workflows.tsx @@ -1,17 +1,5 @@ import { getClient } from "@/api/AxiosClient"; import { WorkflowApiResponse, WorkflowRunApiResponse } from "@/api/types"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { useCredentialGetter } from "@/hooks/useCredentialGetter"; -import { useQuery } from "@tanstack/react-query"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import { WorkflowsBetaAlertCard } from "./WorkflowsBetaAlertCard"; import { StatusBadge } from "@/components/StatusBadge"; import { Pagination, @@ -21,9 +9,21 @@ import { PaginationNext, PaginationPrevious, } from "@/components/ui/pagination"; -import { cn } from "@/util/utils"; -import { WorkflowTitle } from "./WorkflowTitle"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { basicTimeFormat } from "@/util/timeFormat"; +import { cn } from "@/util/utils"; +import { useQuery } from "@tanstack/react-query"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { WorkflowsBetaAlertCard } from "./WorkflowsBetaAlertCard"; +import { WorkflowTitle } from "./WorkflowTitle"; function Workflows() { const credentialGetter = useCredentialGetter(); diff --git a/skyvern-frontend/src/routes/workflows/components/LastRunAtTime.tsx b/skyvern-frontend/src/routes/workflows/components/LastRunAtTime.tsx new file mode 100644 index 00000000..af5c781f --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/components/LastRunAtTime.tsx @@ -0,0 +1,27 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { useWorkflowLastRunQuery } from "../hooks/useWorkflowLastRunQuery"; +import { basicTimeFormat } from "@/util/timeFormat"; + +type Props = { + workflowId: string; +}; + +function LastRunAtTime({ workflowId }: Props) { + const { data, isLoading } = useWorkflowLastRunQuery({ workflowId }); + + if (isLoading) { + return ; + } + + if (!data) { + return null; + } + + if (data.status === "N/A") { + return N/A; + } + + return {basicTimeFormat(data.time)}; +} + +export { LastRunAtTime }; diff --git a/skyvern-frontend/src/routes/workflows/components/LastRunStatus.tsx b/skyvern-frontend/src/routes/workflows/components/LastRunStatus.tsx new file mode 100644 index 00000000..d9a65602 --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/components/LastRunStatus.tsx @@ -0,0 +1,27 @@ +import { BadgeLoading } from "@/components/BadgeLoading"; +import { StatusBadge } from "@/components/StatusBadge"; +import { useWorkflowLastRunQuery } from "../hooks/useWorkflowLastRunQuery"; + +type Props = { + workflowId: string; +}; + +function LastRunStatus({ workflowId }: Props) { + const { data, isLoading } = useWorkflowLastRunQuery({ workflowId }); + + if (isLoading) { + return ; + } + + if (!data) { + return null; + } + + if (data.status === "N/A") { + return N/A; + } + + return ; +} + +export { LastRunStatus }; diff --git a/skyvern-frontend/src/routes/workflows/components/WorkflowsTable.tsx b/skyvern-frontend/src/routes/workflows/components/WorkflowsTable.tsx new file mode 100644 index 00000000..f17975d8 --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/components/WorkflowsTable.tsx @@ -0,0 +1,140 @@ +import { getClient } from "@/api/AxiosClient"; +import { WorkflowApiResponse } from "@/api/types"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useCredentialGetter } from "@/hooks/useCredentialGetter"; +import { useQuery } from "@tanstack/react-query"; +import { LastRunStatus } from "./LastRunStatus"; +import { LastRunAtTime } from "./LastRunAtTime"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { cn } from "@/util/utils"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; + +function WorkflowsTable() { + const [page, setPage] = useState(1); + const credentialGetter = useCredentialGetter(); + const navigate = useNavigate(); + + const { data: workflows, isLoading } = useQuery>({ + queryKey: ["workflows", page], + queryFn: async () => { + const client = await getClient(credentialGetter); + const params = new URLSearchParams(); + params.append("page", String(page)); + return client + .get("/workflows", { + params, + }) + .then((response) => response.data); + }, + }); + + const skeleton = Array.from({ length: 5 }).map((_, index) => ( + + + + + + + + + + + + )); + + return ( +
+ + + + Title + + Last Run Status + + + Last Run Time + + + + + + + + + + {isLoading && skeleton} + {workflows?.map((workflow) => { + return ( + { + if (event.ctrlKey || event.metaKey) { + window.open( + window.location.origin + + `/workflows/${workflow.workflow_permanent_id}`, + "_blank", + "noopener,noreferrer", + ); + return; + } + navigate(`${workflow.workflow_permanent_id}`); + }} + > + + {workflow.title} + + + + + + + + + ); + })} + +
+ + + + { + setPage((prev) => Math.max(1, prev - 1)); + }} + /> + + + {page} + + + { + setPage((prev) => prev + 1); + }} + /> + + + +
+ ); +} + +export { WorkflowsTable }; diff --git a/skyvern-frontend/src/routes/workflows/hooks/useWorkflowLastRunQuery.ts b/skyvern-frontend/src/routes/workflows/hooks/useWorkflowLastRunQuery.ts new file mode 100644 index 00000000..1087b83c --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/hooks/useWorkflowLastRunQuery.ts @@ -0,0 +1,40 @@ +import { getClient } from "@/api/AxiosClient"; +import { Status, WorkflowRunApiResponse } from "@/api/types"; +import { useCredentialGetter } from "@/hooks/useCredentialGetter"; +import { useQuery } from "@tanstack/react-query"; + +type Props = { + workflowId: string; +}; + +type LastRunInfo = { + status: Status | "N/A"; + time: string | "N/A"; +}; + +function useWorkflowLastRunQuery({ workflowId }: Props) { + const credentialGetter = useCredentialGetter(); + const queryResult = useQuery({ + queryKey: ["lastRunInfo", workflowId], + queryFn: async () => { + const client = await getClient(credentialGetter); + const data = (await client + .get(`/workflows/${workflowId}/runs?page_size=1`) + .then((response) => response.data)) as Array; + if (data.length === 0) { + return { + status: "N/A", + time: "N/A", + }; + } + return { + status: data[0]!.status, + time: data[0]!.created_at, + }; + }, + }); + + return queryResult; +} + +export { useWorkflowLastRunQuery };