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 };