diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts
index c6ecedf1f..d9c00a614 100644
--- a/skyvern-frontend/src/api/types.ts
+++ b/skyvern-frontend/src/api/types.ts
@@ -192,6 +192,7 @@ export type WorkflowRunApiResponse = {
webhook_callback_url: string;
created_at: string;
modified_at: string;
+ failure_reason: string | null;
};
export type WorkflowRunStatusApiResponse = {
diff --git a/skyvern-frontend/src/routes/tasks/list/TaskHistory.tsx b/skyvern-frontend/src/routes/tasks/list/TaskHistory.tsx
index 3380ad9c7..c7cfabb95 100644
--- a/skyvern-frontend/src/routes/tasks/list/TaskHistory.tsx
+++ b/skyvern-frontend/src/routes/tasks/list/TaskHistory.tsx
@@ -26,6 +26,9 @@ import { useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { TaskActions } from "./TaskActions";
import { TaskListSkeletonRows } from "./TaskListSkeletonRows";
+import { Button } from "@/components/ui/button";
+import { DownloadIcon } from "@radix-ui/react-icons";
+import { downloadBlob } from "@/util/downloadBlob";
type StatusDropdownItem = {
label: string;
@@ -115,15 +118,47 @@ function TaskHistory() {
}
}
+ function handleExport() {
+ if (!tasks) {
+ return; // should never happen
+ }
+ const data = ["id,url,status,created,failure_reason"];
+ tasks.forEach((task) => {
+ const row = [
+ task.task_id,
+ task.request.url,
+ task.status,
+ task.created_at,
+ task.failure_reason ?? "",
+ ];
+ data.push(
+ row
+ .map(String) // convert every value to String
+ .map((v) => v.replace(new RegExp('"', "g"), '""')) // escape double quotes
+ .map((v) => `"${v}"`) // quote it
+ .join(","), // comma-separated
+ );
+ });
+ const contents = data.join("\r\n");
+
+ downloadBlob(contents, "export.csv", "data:text/csv;charset=utf-8;");
+ }
+
return (
Task Runs
-
+
+
+
+
diff --git a/skyvern-frontend/src/routes/workflows/Workflows.tsx b/skyvern-frontend/src/routes/workflows/Workflows.tsx
index c39f71063..af587e961 100644
--- a/skyvern-frontend/src/routes/workflows/Workflows.tsx
+++ b/skyvern-frontend/src/routes/workflows/Workflows.tsx
@@ -28,6 +28,7 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat";
import { cn } from "@/util/utils";
import {
+ DownloadIcon,
ExclamationTriangleIcon,
Pencil2Icon,
PlayIcon,
@@ -43,6 +44,7 @@ import { WorkflowActions } from "./WorkflowActions";
import { WorkflowTitle } from "./WorkflowTitle";
import { WorkflowApiResponse } from "./types/workflowTypes";
import { WorkflowRunApiResponse } from "@/api/types";
+import { downloadBlob } from "@/util/downloadBlob";
const emptyWorkflowRequest: WorkflowCreateYAMLRequest = {
title: "New Workflow",
@@ -118,6 +120,32 @@ function Workflows() {
},
});
+ function handleExport() {
+ if (!workflowRuns) {
+ return; // should never happen
+ }
+ const data = ["workflow_run_id,workflow_id,status,created,failure_reason"];
+ workflowRuns.forEach((workflowRun) => {
+ const row = [
+ workflowRun.workflow_run_id,
+ workflowRun.workflow_permanent_id,
+ workflowRun.status,
+ workflowRun.created_at,
+ workflowRun.failure_reason ?? "",
+ ];
+ data.push(
+ row
+ .map(String) // convert every value to String
+ .map((v) => v.replace(new RegExp('"', "g"), '""')) // escape double quotes
+ .map((v) => `"${v}"`) // quote it
+ .join(","), // comma-separated
+ );
+ });
+ const contents = data.join("\r\n");
+
+ downloadBlob(contents, "export.csv", "data:text/csv;charset=utf-8;");
+ }
+
function handleRowClick(
event: React.MouseEvent,
workflowPermanentId: string,
@@ -178,258 +206,274 @@ function Workflows() {
)}
-
-
- Workflows
-
-
-
-
-
-
-
-
-
- ID
- Title
- Created At
-
-
-
-
- {isLoading ? (
-
- Loading...
-
- ) : workflows?.length === 0 ? (
+
+
+ Workflows
+
+
+
+
+
+
+
+
- No workflows found
+ ID
+ Title
+ Created At
+
- ) : (
- workflows?.map((workflow) => {
- return (
-
- {
- handleRowClick(event, workflow.workflow_permanent_id);
- }}
+
+
+ {isLoading ? (
+
+ Loading...
+
+ ) : workflows?.length === 0 ? (
+
+ No workflows found
+
+ ) : (
+ workflows?.map((workflow) => {
+ return (
+
- {workflow.workflow_permanent_id}
-
- {
- handleRowClick(event, workflow.workflow_permanent_id);
- }}
- >
- {workflow.title}
-
- {
- handleRowClick(event, workflow.workflow_permanent_id);
- }}
- title={basicTimeFormat(workflow.created_at)}
- >
- {basicLocalTimeFormat(workflow.created_at)}
-
-
-
-
-
-
-
-
- Open in Editor
-
-
-
-
-
-
-
- Create New Run
-
-
-
-
-
-
- );
- })
- )}
-
-
-
-
-
- {
- if (workflowsPage === 1) {
- return;
- }
- const params = new URLSearchParams();
- params.set(
- "workflowsPage",
- String(Math.max(1, workflowsPage - 1)),
+ {
+ handleRowClick(event, workflow.workflow_permanent_id);
+ }}
+ >
+ {workflow.workflow_permanent_id}
+
+ {
+ handleRowClick(event, workflow.workflow_permanent_id);
+ }}
+ >
+ {workflow.title}
+
+ {
+ handleRowClick(event, workflow.workflow_permanent_id);
+ }}
+ title={basicTimeFormat(workflow.created_at)}
+ >
+ {basicLocalTimeFormat(workflow.created_at)}
+
+
+
+
+
+
+
+
+ Open in Editor
+
+
+
+
+
+
+
+ Create New Run
+
+
+
+
+
+
);
- setSearchParams(params, { replace: true });
- }}
- />
-
-
- {workflowsPage}
-
-
- {
- const params = new URLSearchParams();
- params.set("workflowsPage", String(workflowsPage + 1));
- setSearchParams(params, { replace: true });
- }}
- />
-
-
-
+ })
+ )}
+
+
+
+
+
+ {
+ if (workflowsPage === 1) {
+ return;
+ }
+ const params = new URLSearchParams();
+ params.set(
+ "workflowsPage",
+ String(Math.max(1, workflowsPage - 1)),
+ );
+ setSearchParams(params, { replace: true });
+ }}
+ />
+
+
+ {workflowsPage}
+
+
+ {
+ const params = new URLSearchParams();
+ params.set("workflowsPage", String(workflowsPage + 1));
+ setSearchParams(params, { replace: true });
+ }}
+ />
+
+
+
+
-
-
-
-
-
- Workflow Run ID
- Workflow ID
- Workflow Title
- Status
- Created At
-
-
-
- {workflowRunsIsLoading ? (
+
+
+
+
+
- Loading...
+ Workflow Run ID
+ Workflow ID
+ Workflow Title
+ Status
+ Created At
- ) : workflowRuns?.length === 0 ? (
-
- No workflow runs found
-
- ) : (
- workflowRuns?.map((workflowRun) => {
- return (
- {
- if (event.ctrlKey || event.metaKey) {
- window.open(
- window.location.origin +
- `/workflows/${workflowRun.workflow_permanent_id}/${workflowRun.workflow_run_id}`,
- "_blank",
- "noopener,noreferrer",
+
+
+ {workflowRunsIsLoading ? (
+
+ Loading...
+
+ ) : workflowRuns?.length === 0 ? (
+
+ No workflow runs found
+
+ ) : (
+ workflowRuns?.map((workflowRun) => {
+ return (
+ {
+ if (event.ctrlKey || event.metaKey) {
+ window.open(
+ window.location.origin +
+ `/workflows/${workflowRun.workflow_permanent_id}/${workflowRun.workflow_run_id}`,
+ "_blank",
+ "noopener,noreferrer",
+ );
+ return;
+ }
+ navigate(
+ `/workflows/${workflowRun.workflow_permanent_id}/${workflowRun.workflow_run_id}`,
);
- return;
- }
- navigate(
- `/workflows/${workflowRun.workflow_permanent_id}/${workflowRun.workflow_run_id}`,
- );
- }}
- className="cursor-pointer"
- >
-
- {workflowRun.workflow_run_id}
-
-
- {workflowRun.workflow_permanent_id}
-
-
-
-
-
-
-
-
- {basicLocalTimeFormat(workflowRun.created_at)}
-
-
- );
- })
- )}
-
-
-
-
-
- {
- if (workflowRunsPage === 1) {
- return;
- }
- const params = new URLSearchParams();
- params.set(
- "workflowRunsPage",
- String(Math.max(1, workflowRunsPage - 1)),
+
+ {workflowRun.workflow_run_id}
+
+
+ {workflowRun.workflow_permanent_id}
+
+
+
+
+
+
+
+
+ {basicLocalTimeFormat(workflowRun.created_at)}
+
+
);
- setSearchParams(params, { replace: true });
- }}
- />
-
-
- {workflowRunsPage}
-
-
- {
- const params = new URLSearchParams();
- params.set("workflowRunsPage", String(workflowRunsPage + 1));
- setSearchParams(params, { replace: true });
- }}
- />
-
-
-
+ })
+ )}
+
+
+
+
+
+ {
+ if (workflowRunsPage === 1) {
+ return;
+ }
+ const params = new URLSearchParams();
+ params.set(
+ "workflowRunsPage",
+ String(Math.max(1, workflowRunsPage - 1)),
+ );
+ setSearchParams(params, { replace: true });
+ }}
+ />
+
+
+ {workflowRunsPage}
+
+
+ {
+ const params = new URLSearchParams();
+ params.set(
+ "workflowRunsPage",
+ String(workflowRunsPage + 1),
+ );
+ setSearchParams(params, { replace: true });
+ }}
+ />
+
+
+
+
);
diff --git a/skyvern-frontend/src/util/downloadBlob.ts b/skyvern-frontend/src/util/downloadBlob.ts
new file mode 100644
index 000000000..ab824c6dd
--- /dev/null
+++ b/skyvern-frontend/src/util/downloadBlob.ts
@@ -0,0 +1,21 @@
+/**
+ * Download contents as a file
+ * Source: https://stackoverflow.com/questions/14964035/how-to-export-javascript-array-info-to-csv-on-client-side
+ */
+function downloadBlob(content: string, filename: string, contentType: string) {
+ // Create a blob
+ const blob = new Blob([content], { type: contentType });
+ const url = URL.createObjectURL(blob);
+
+ // Create a link to download it
+ const element = document.createElement("a");
+ element.href = url;
+ element.setAttribute("download", filename);
+ element.style.display = "none";
+ document.body.appendChild(element);
+ element.click();
+ document.body.removeChild(element);
+ URL.revokeObjectURL(url);
+}
+
+export { downloadBlob };