Add export as csv in tasks and workflow runs (#1361)
This commit is contained in:
@@ -192,6 +192,7 @@ export type WorkflowRunApiResponse = {
|
||||
webhook_callback_url: string;
|
||||
created_at: string;
|
||||
modified_at: string;
|
||||
failure_reason: string | null;
|
||||
};
|
||||
|
||||
export type WorkflowRunStatusApiResponse = {
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-4">
|
||||
<header className="flex items-center justify-between">
|
||||
<h1 className="text-2xl">Task Runs</h1>
|
||||
<StatusFilterDropdown
|
||||
values={statusFilters}
|
||||
onChange={setStatusFilters}
|
||||
options={statusDropdownItems}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<StatusFilterDropdown
|
||||
values={statusFilters}
|
||||
onChange={setStatusFilters}
|
||||
options={statusDropdownItems}
|
||||
/>
|
||||
<Button variant="secondary" onClick={handleExport}>
|
||||
<DownloadIcon className="mr-2" />
|
||||
Export CSV
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
|
||||
@@ -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<HTMLTableCellElement>,
|
||||
workflowPermanentId: string,
|
||||
@@ -178,258 +206,274 @@ function Workflows() {
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<header className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-semibold">Workflows</h1>
|
||||
<div className="flex gap-2">
|
||||
<ImportWorkflowButton />
|
||||
<Button
|
||||
disabled={createNewWorkflowMutation.isPending}
|
||||
onClick={() => {
|
||||
createNewWorkflowMutation.mutate();
|
||||
}}
|
||||
>
|
||||
{createNewWorkflowMutation.isPending ? (
|
||||
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<PlusIcon className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Create Workflow
|
||||
</Button>
|
||||
<div className="space-y-4">
|
||||
<header className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-semibold">Workflows</h1>
|
||||
<div className="flex gap-2">
|
||||
<ImportWorkflowButton />
|
||||
<Button
|
||||
disabled={createNewWorkflowMutation.isPending}
|
||||
onClick={() => {
|
||||
createNewWorkflowMutation.mutate();
|
||||
}}
|
||||
>
|
||||
{createNewWorkflowMutation.isPending ? (
|
||||
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<PlusIcon className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Create Workflow
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-1/3">ID</TableHead>
|
||||
<TableHead className="w-1/3">Title</TableHead>
|
||||
<TableHead className="w-1/3">Created At</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4}>Loading...</TableCell>
|
||||
</TableRow>
|
||||
) : workflows?.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4}>No workflows found</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
workflows?.map((workflow) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={workflow.workflow_permanent_id}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<TableCell
|
||||
onClick={(event) => {
|
||||
handleRowClick(event, workflow.workflow_permanent_id);
|
||||
}}
|
||||
>
|
||||
{workflow.workflow_permanent_id}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
onClick={(event) => {
|
||||
handleRowClick(event, workflow.workflow_permanent_id);
|
||||
}}
|
||||
>
|
||||
{workflow.title}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
onClick={(event) => {
|
||||
handleRowClick(event, workflow.workflow_permanent_id);
|
||||
}}
|
||||
title={basicTimeFormat(workflow.created_at)}
|
||||
>
|
||||
{basicLocalTimeFormat(workflow.created_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-end gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={(event) => {
|
||||
handleIconClick(
|
||||
event,
|
||||
`/workflows/${workflow.workflow_permanent_id}/edit`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Pencil2Icon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Open in Editor</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={(event) => {
|
||||
handleIconClick(
|
||||
event,
|
||||
`/workflows/${workflow.workflow_permanent_id}/run`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<PlayIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Create New Run</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<WorkflowActions workflow={workflow} />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Pagination className="pt-2">
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
className={cn({ "cursor-not-allowed": workflowsPage === 1 })}
|
||||
onClick={() => {
|
||||
if (workflowsPage === 1) {
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.set(
|
||||
"workflowsPage",
|
||||
String(Math.max(1, workflowsPage - 1)),
|
||||
);
|
||||
setSearchParams(params, { replace: true });
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink>{workflowsPage}</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => {
|
||||
const params = new URLSearchParams();
|
||||
params.set("workflowsPage", String(workflowsPage + 1));
|
||||
setSearchParams(params, { replace: true });
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</header>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-1/3">ID</TableHead>
|
||||
<TableHead className="w-1/3">Title</TableHead>
|
||||
<TableHead className="w-1/3">Created At</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4}>Loading...</TableCell>
|
||||
</TableRow>
|
||||
) : workflows?.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4}>No workflows found</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
workflows?.map((workflow) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={workflow.workflow_permanent_id}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<TableCell
|
||||
onClick={(event) => {
|
||||
handleRowClick(event, workflow.workflow_permanent_id);
|
||||
}}
|
||||
>
|
||||
{workflow.workflow_permanent_id}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
onClick={(event) => {
|
||||
handleRowClick(event, workflow.workflow_permanent_id);
|
||||
}}
|
||||
>
|
||||
{workflow.title}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
onClick={(event) => {
|
||||
handleRowClick(event, workflow.workflow_permanent_id);
|
||||
}}
|
||||
title={basicTimeFormat(workflow.created_at)}
|
||||
>
|
||||
{basicLocalTimeFormat(workflow.created_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-end gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={(event) => {
|
||||
handleIconClick(
|
||||
event,
|
||||
`/workflows/${workflow.workflow_permanent_id}/edit`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Pencil2Icon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Open in Editor</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={(event) => {
|
||||
handleIconClick(
|
||||
event,
|
||||
`/workflows/${workflow.workflow_permanent_id}/run`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<PlayIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Create New Run</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<WorkflowActions workflow={workflow} />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Pagination className="pt-2">
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
className={cn({ "cursor-not-allowed": workflowsPage === 1 })}
|
||||
onClick={() => {
|
||||
if (workflowsPage === 1) {
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.set(
|
||||
"workflowsPage",
|
||||
String(Math.max(1, workflowsPage - 1)),
|
||||
);
|
||||
setSearchParams(params, { replace: true });
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink>{workflowsPage}</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => {
|
||||
const params = new URLSearchParams();
|
||||
params.set("workflowsPage", String(workflowsPage + 1));
|
||||
setSearchParams(params, { replace: true });
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
<header>
|
||||
<h1 className="text-2xl font-semibold">Workflow Runs</h1>
|
||||
</header>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-1/5">Workflow Run ID</TableHead>
|
||||
<TableHead className="w-1/5">Workflow ID</TableHead>
|
||||
<TableHead className="w-1/5">Workflow Title</TableHead>
|
||||
<TableHead className="w-1/5">Status</TableHead>
|
||||
<TableHead className="w-1/5">Created At</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{workflowRunsIsLoading ? (
|
||||
<div className="space-y-4">
|
||||
<header>
|
||||
<div className="flex justify-between">
|
||||
<h1 className="text-2xl font-semibold">Workflow Runs</h1>
|
||||
<Button variant="secondary" onClick={handleExport}>
|
||||
<DownloadIcon className="mr-2" />
|
||||
Export CSV
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell colSpan={5}>Loading...</TableCell>
|
||||
<TableHead className="w-1/5">Workflow Run ID</TableHead>
|
||||
<TableHead className="w-1/5">Workflow ID</TableHead>
|
||||
<TableHead className="w-1/5">Workflow Title</TableHead>
|
||||
<TableHead className="w-1/5">Status</TableHead>
|
||||
<TableHead className="w-1/5">Created At</TableHead>
|
||||
</TableRow>
|
||||
) : workflowRuns?.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5}>No workflow runs found</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
workflowRuns?.map((workflowRun) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={workflowRun.workflow_run_id}
|
||||
onClick={(event) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
window.open(
|
||||
window.location.origin +
|
||||
`/workflows/${workflowRun.workflow_permanent_id}/${workflowRun.workflow_run_id}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{workflowRunsIsLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5}>Loading...</TableCell>
|
||||
</TableRow>
|
||||
) : workflowRuns?.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5}>No workflow runs found</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
workflowRuns?.map((workflowRun) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={workflowRun.workflow_run_id}
|
||||
onClick={(event) => {
|
||||
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"
|
||||
>
|
||||
<TableCell className="w-1/5">
|
||||
{workflowRun.workflow_run_id}
|
||||
</TableCell>
|
||||
<TableCell className="w-1/5">
|
||||
{workflowRun.workflow_permanent_id}
|
||||
</TableCell>
|
||||
<TableCell className="w-1/5">
|
||||
<WorkflowTitle
|
||||
workflowPermanentId={workflowRun.workflow_permanent_id}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="w-1/5">
|
||||
<StatusBadge status={workflowRun.status} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className="w-1/5"
|
||||
title={basicTimeFormat(workflowRun.created_at)}
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{basicLocalTimeFormat(workflowRun.created_at)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Pagination className="pt-2">
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
className={cn({ "cursor-not-allowed": workflowRunsPage === 1 })}
|
||||
onClick={() => {
|
||||
if (workflowRunsPage === 1) {
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.set(
|
||||
"workflowRunsPage",
|
||||
String(Math.max(1, workflowRunsPage - 1)),
|
||||
<TableCell className="w-1/5">
|
||||
{workflowRun.workflow_run_id}
|
||||
</TableCell>
|
||||
<TableCell className="w-1/5">
|
||||
{workflowRun.workflow_permanent_id}
|
||||
</TableCell>
|
||||
<TableCell className="w-1/5">
|
||||
<WorkflowTitle
|
||||
workflowPermanentId={
|
||||
workflowRun.workflow_permanent_id
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="w-1/5">
|
||||
<StatusBadge status={workflowRun.status} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className="w-1/5"
|
||||
title={basicTimeFormat(workflowRun.created_at)}
|
||||
>
|
||||
{basicLocalTimeFormat(workflowRun.created_at)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
setSearchParams(params, { replace: true });
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink>{workflowRunsPage}</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => {
|
||||
const params = new URLSearchParams();
|
||||
params.set("workflowRunsPage", String(workflowRunsPage + 1));
|
||||
setSearchParams(params, { replace: true });
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Pagination className="pt-2">
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
className={cn({
|
||||
"cursor-not-allowed": workflowRunsPage === 1,
|
||||
})}
|
||||
onClick={() => {
|
||||
if (workflowRunsPage === 1) {
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.set(
|
||||
"workflowRunsPage",
|
||||
String(Math.max(1, workflowRunsPage - 1)),
|
||||
);
|
||||
setSearchParams(params, { replace: true });
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink>{workflowRunsPage}</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => {
|
||||
const params = new URLSearchParams();
|
||||
params.set(
|
||||
"workflowRunsPage",
|
||||
String(workflowRunsPage + 1),
|
||||
);
|
||||
setSearchParams(params, { replace: true });
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
21
skyvern-frontend/src/util/downloadBlob.ts
Normal file
21
skyvern-frontend/src/util/downloadBlob.ts
Normal file
@@ -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 };
|
||||
Reference in New Issue
Block a user