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;
|
webhook_callback_url: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
modified_at: string;
|
modified_at: string;
|
||||||
|
failure_reason: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowRunStatusApiResponse = {
|
export type WorkflowRunStatusApiResponse = {
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ import { useState } from "react";
|
|||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { TaskActions } from "./TaskActions";
|
import { TaskActions } from "./TaskActions";
|
||||||
import { TaskListSkeletonRows } from "./TaskListSkeletonRows";
|
import { TaskListSkeletonRows } from "./TaskListSkeletonRows";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { DownloadIcon } from "@radix-ui/react-icons";
|
||||||
|
import { downloadBlob } from "@/util/downloadBlob";
|
||||||
|
|
||||||
type StatusDropdownItem = {
|
type StatusDropdownItem = {
|
||||||
label: string;
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<header className="flex items-center justify-between">
|
<header className="flex items-center justify-between">
|
||||||
<h1 className="text-2xl">Task Runs</h1>
|
<h1 className="text-2xl">Task Runs</h1>
|
||||||
<StatusFilterDropdown
|
<div className="flex gap-2">
|
||||||
values={statusFilters}
|
<StatusFilterDropdown
|
||||||
onChange={setStatusFilters}
|
values={statusFilters}
|
||||||
options={statusDropdownItems}
|
onChange={setStatusFilters}
|
||||||
/>
|
options={statusDropdownItems}
|
||||||
|
/>
|
||||||
|
<Button variant="secondary" onClick={handleExport}>
|
||||||
|
<DownloadIcon className="mr-2" />
|
||||||
|
Export CSV
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
|||||||
import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat";
|
import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat";
|
||||||
import { cn } from "@/util/utils";
|
import { cn } from "@/util/utils";
|
||||||
import {
|
import {
|
||||||
|
DownloadIcon,
|
||||||
ExclamationTriangleIcon,
|
ExclamationTriangleIcon,
|
||||||
Pencil2Icon,
|
Pencil2Icon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
@@ -43,6 +44,7 @@ import { WorkflowActions } from "./WorkflowActions";
|
|||||||
import { WorkflowTitle } from "./WorkflowTitle";
|
import { WorkflowTitle } from "./WorkflowTitle";
|
||||||
import { WorkflowApiResponse } from "./types/workflowTypes";
|
import { WorkflowApiResponse } from "./types/workflowTypes";
|
||||||
import { WorkflowRunApiResponse } from "@/api/types";
|
import { WorkflowRunApiResponse } from "@/api/types";
|
||||||
|
import { downloadBlob } from "@/util/downloadBlob";
|
||||||
|
|
||||||
const emptyWorkflowRequest: WorkflowCreateYAMLRequest = {
|
const emptyWorkflowRequest: WorkflowCreateYAMLRequest = {
|
||||||
title: "New Workflow",
|
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(
|
function handleRowClick(
|
||||||
event: React.MouseEvent<HTMLTableCellElement>,
|
event: React.MouseEvent<HTMLTableCellElement>,
|
||||||
workflowPermanentId: string,
|
workflowPermanentId: string,
|
||||||
@@ -178,258 +206,274 @@ function Workflows() {
|
|||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
<div className="space-y-4">
|
||||||
<header className="flex items-center justify-between">
|
<header className="flex items-center justify-between">
|
||||||
<h1 className="text-2xl font-semibold">Workflows</h1>
|
<h1 className="text-2xl font-semibold">Workflows</h1>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<ImportWorkflowButton />
|
<ImportWorkflowButton />
|
||||||
<Button
|
<Button
|
||||||
disabled={createNewWorkflowMutation.isPending}
|
disabled={createNewWorkflowMutation.isPending}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
createNewWorkflowMutation.mutate();
|
createNewWorkflowMutation.mutate();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{createNewWorkflowMutation.isPending ? (
|
{createNewWorkflowMutation.isPending ? (
|
||||||
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<PlusIcon className="mr-2 h-4 w-4" />
|
<PlusIcon className="mr-2 h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
Create Workflow
|
Create Workflow
|
||||||
</Button>
|
</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>
|
</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>
|
</div>
|
||||||
<header>
|
<div className="space-y-4">
|
||||||
<h1 className="text-2xl font-semibold">Workflow Runs</h1>
|
<header>
|
||||||
</header>
|
<div className="flex justify-between">
|
||||||
<div className="rounded-md border">
|
<h1 className="text-2xl font-semibold">Workflow Runs</h1>
|
||||||
<Table>
|
<Button variant="secondary" onClick={handleExport}>
|
||||||
<TableHeader>
|
<DownloadIcon className="mr-2" />
|
||||||
<TableRow>
|
Export CSV
|
||||||
<TableHead className="w-1/5">Workflow Run ID</TableHead>
|
</Button>
|
||||||
<TableHead className="w-1/5">Workflow ID</TableHead>
|
</div>
|
||||||
<TableHead className="w-1/5">Workflow Title</TableHead>
|
</header>
|
||||||
<TableHead className="w-1/5">Status</TableHead>
|
<div className="rounded-md border">
|
||||||
<TableHead className="w-1/5">Created At</TableHead>
|
<Table>
|
||||||
</TableRow>
|
<TableHeader>
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{workflowRunsIsLoading ? (
|
|
||||||
<TableRow>
|
<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>
|
</TableRow>
|
||||||
) : workflowRuns?.length === 0 ? (
|
</TableHeader>
|
||||||
<TableRow>
|
<TableBody>
|
||||||
<TableCell colSpan={5}>No workflow runs found</TableCell>
|
{workflowRunsIsLoading ? (
|
||||||
</TableRow>
|
<TableRow>
|
||||||
) : (
|
<TableCell colSpan={5}>Loading...</TableCell>
|
||||||
workflowRuns?.map((workflowRun) => {
|
</TableRow>
|
||||||
return (
|
) : workflowRuns?.length === 0 ? (
|
||||||
<TableRow
|
<TableRow>
|
||||||
key={workflowRun.workflow_run_id}
|
<TableCell colSpan={5}>No workflow runs found</TableCell>
|
||||||
onClick={(event) => {
|
</TableRow>
|
||||||
if (event.ctrlKey || event.metaKey) {
|
) : (
|
||||||
window.open(
|
workflowRuns?.map((workflowRun) => {
|
||||||
window.location.origin +
|
return (
|
||||||
`/workflows/${workflowRun.workflow_permanent_id}/${workflowRun.workflow_run_id}`,
|
<TableRow
|
||||||
"_blank",
|
key={workflowRun.workflow_run_id}
|
||||||
"noopener,noreferrer",
|
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;
|
}}
|
||||||
}
|
className="cursor-pointer"
|
||||||
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)}
|
|
||||||
>
|
>
|
||||||
{basicLocalTimeFormat(workflowRun.created_at)}
|
<TableCell className="w-1/5">
|
||||||
</TableCell>
|
{workflowRun.workflow_run_id}
|
||||||
</TableRow>
|
</TableCell>
|
||||||
);
|
<TableCell className="w-1/5">
|
||||||
})
|
{workflowRun.workflow_permanent_id}
|
||||||
)}
|
</TableCell>
|
||||||
</TableBody>
|
<TableCell className="w-1/5">
|
||||||
</Table>
|
<WorkflowTitle
|
||||||
<Pagination className="pt-2">
|
workflowPermanentId={
|
||||||
<PaginationContent>
|
workflowRun.workflow_permanent_id
|
||||||
<PaginationItem>
|
}
|
||||||
<PaginationPrevious
|
/>
|
||||||
className={cn({ "cursor-not-allowed": workflowRunsPage === 1 })}
|
</TableCell>
|
||||||
onClick={() => {
|
<TableCell className="w-1/5">
|
||||||
if (workflowRunsPage === 1) {
|
<StatusBadge status={workflowRun.status} />
|
||||||
return;
|
</TableCell>
|
||||||
}
|
<TableCell
|
||||||
const params = new URLSearchParams();
|
className="w-1/5"
|
||||||
params.set(
|
title={basicTimeFormat(workflowRun.created_at)}
|
||||||
"workflowRunsPage",
|
>
|
||||||
String(Math.max(1, workflowRunsPage - 1)),
|
{basicLocalTimeFormat(workflowRun.created_at)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
);
|
);
|
||||||
setSearchParams(params, { replace: true });
|
})
|
||||||
}}
|
)}
|
||||||
/>
|
</TableBody>
|
||||||
</PaginationItem>
|
</Table>
|
||||||
<PaginationItem>
|
<Pagination className="pt-2">
|
||||||
<PaginationLink>{workflowRunsPage}</PaginationLink>
|
<PaginationContent>
|
||||||
</PaginationItem>
|
<PaginationItem>
|
||||||
<PaginationItem>
|
<PaginationPrevious
|
||||||
<PaginationNext
|
className={cn({
|
||||||
onClick={() => {
|
"cursor-not-allowed": workflowRunsPage === 1,
|
||||||
const params = new URLSearchParams();
|
})}
|
||||||
params.set("workflowRunsPage", String(workflowRunsPage + 1));
|
onClick={() => {
|
||||||
setSearchParams(params, { replace: true });
|
if (workflowRunsPage === 1) {
|
||||||
}}
|
return;
|
||||||
/>
|
}
|
||||||
</PaginationItem>
|
const params = new URLSearchParams();
|
||||||
</PaginationContent>
|
params.set(
|
||||||
</Pagination>
|
"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>
|
||||||
</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