change workflow run page to show stream with current task (#971)
Co-authored-by: Muhammed Salih Altun <muhammedsalihaltun@gmail.com>
This commit is contained in:
@@ -5,6 +5,8 @@ import {
|
|||||||
WorkflowRunStatusApiResponse,
|
WorkflowRunStatusApiResponse,
|
||||||
} from "@/api/types";
|
} from "@/api/types";
|
||||||
import { StatusBadge } from "@/components/StatusBadge";
|
import { StatusBadge } from "@/components/StatusBadge";
|
||||||
|
import { ZoomableImage } from "@/components/ZoomableImage";
|
||||||
|
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -25,10 +27,22 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import { toast } from "@/components/ui/use-toast";
|
||||||
|
import { useApiCredential } from "@/hooks/useApiCredential";
|
||||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
import { basicTimeFormat } from "@/util/timeFormat";
|
import { copyText } from "@/util/copyText";
|
||||||
|
import { apiBaseUrl, envCredential } from "@/util/env";
|
||||||
|
import { basicTimeFormat, timeFormatWithShortDate } from "@/util/timeFormat";
|
||||||
import { cn } from "@/util/utils";
|
import { cn } from "@/util/utils";
|
||||||
|
import {
|
||||||
|
CopyIcon,
|
||||||
|
Pencil2Icon,
|
||||||
|
PlayIcon,
|
||||||
|
ReaderIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||||
|
import fetchToCurl from "fetch-to-curl";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Link,
|
Link,
|
||||||
useNavigate,
|
useNavigate,
|
||||||
@@ -37,16 +51,9 @@ import {
|
|||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import { TaskActions } from "../tasks/list/TaskActions";
|
import { TaskActions } from "../tasks/list/TaskActions";
|
||||||
import { TaskListSkeletonRows } from "../tasks/list/TaskListSkeletonRows";
|
import { TaskListSkeletonRows } from "../tasks/list/TaskListSkeletonRows";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { statusIsNotFinalized, statusIsRunningOrQueued } from "../tasks/types";
|
import { statusIsNotFinalized, statusIsRunningOrQueued } from "../tasks/types";
|
||||||
import { apiBaseUrl, envCredential } from "@/util/env";
|
import { CodeEditor } from "./components/CodeEditor";
|
||||||
import { toast } from "@/components/ui/use-toast";
|
|
||||||
import { CopyIcon, Pencil2Icon, PlayIcon } from "@radix-ui/react-icons";
|
|
||||||
import { useWorkflowQuery } from "./hooks/useWorkflowQuery";
|
import { useWorkflowQuery } from "./hooks/useWorkflowQuery";
|
||||||
import fetchToCurl from "fetch-to-curl";
|
|
||||||
import { useApiCredential } from "@/hooks/useApiCredential";
|
|
||||||
import { copyText } from "@/util/copyText";
|
|
||||||
import { ZoomableImage } from "@/components/ZoomableImage";
|
|
||||||
|
|
||||||
type StreamMessage = {
|
type StreamMessage = {
|
||||||
task_id: string;
|
task_id: string;
|
||||||
@@ -112,8 +119,13 @@ function WorkflowRun() {
|
|||||||
},
|
},
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
refetchOnMount: workflowRun?.status === Status.Running,
|
refetchOnMount: workflowRun?.status === Status.Running,
|
||||||
|
refetchOnWindowFocus: workflowRun?.status === Status.Running,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const currentRunningTask = workflowTasks?.find(
|
||||||
|
(task) => task.status === Status.Running,
|
||||||
|
);
|
||||||
|
|
||||||
const workflowRunIsRunningOrQueued =
|
const workflowRunIsRunningOrQueued =
|
||||||
workflowRun && statusIsRunningOrQueued(workflowRun);
|
workflowRun && statusIsRunningOrQueued(workflowRun);
|
||||||
|
|
||||||
@@ -189,7 +201,7 @@ function WorkflowRun() {
|
|||||||
function getStream() {
|
function getStream() {
|
||||||
if (workflowRun?.status === Status.Created) {
|
if (workflowRun?.status === Status.Created) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-8 bg-slate-900 py-8 text-lg">
|
<div className="flex h-full w-full flex-col items-center justify-center gap-8 rounded-md bg-slate-900 py-8 text-lg">
|
||||||
<span>Workflow has been created.</span>
|
<span>Workflow has been created.</span>
|
||||||
<span>Stream will start when the workflow is running.</span>
|
<span>Stream will start when the workflow is running.</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,7 +209,7 @@ function WorkflowRun() {
|
|||||||
}
|
}
|
||||||
if (workflowRun?.status === Status.Queued) {
|
if (workflowRun?.status === Status.Queued) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-8 bg-slate-900 py-8 text-lg">
|
<div className="flex h-full w-full flex-col items-center justify-center gap-8 rounded-md bg-slate-900 py-8 text-lg">
|
||||||
<span>Your workflow run is queued.</span>
|
<span>Your workflow run is queued.</span>
|
||||||
<span>Stream will start when the workflow is running.</span>
|
<span>Stream will start when the workflow is running.</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,7 +218,7 @@ function WorkflowRun() {
|
|||||||
|
|
||||||
if (workflowRun?.status === Status.Running && streamImgSrc.length === 0) {
|
if (workflowRun?.status === Status.Running && streamImgSrc.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center justify-center bg-slate-900 py-8 text-lg">
|
<div className="flex h-full w-full items-center justify-center rounded-md bg-slate-900 py-8 text-lg">
|
||||||
Starting the stream...
|
Starting the stream...
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -215,7 +227,10 @@ function WorkflowRun() {
|
|||||||
if (workflowRun?.status === Status.Running && streamImgSrc.length > 0) {
|
if (workflowRun?.status === Status.Running && streamImgSrc.length > 0) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
<ZoomableImage src={`data:image/png;base64,${streamImgSrc}`} />
|
<ZoomableImage
|
||||||
|
src={`data:image/png;base64,${streamImgSrc}`}
|
||||||
|
className="rounded-md"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -240,7 +255,7 @@ function WorkflowRun() {
|
|||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<header className="flex justify-between">
|
<header className="flex justify-between">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-5">
|
||||||
<h1 className="text-3xl">{workflowRunId}</h1>
|
<h1 className="text-3xl">{workflowRunId}</h1>
|
||||||
{workflowRunIsLoading ? (
|
{workflowRunIsLoading ? (
|
||||||
<Skeleton className="h-8 w-28" />
|
<Skeleton className="h-8 w-28" />
|
||||||
@@ -308,20 +323,72 @@ function WorkflowRun() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{getStream()}
|
{workflowRun && statusIsNotFinalized(workflowRun) && (
|
||||||
<div className="space-y-4">
|
<div className="flex gap-5">
|
||||||
|
<div className="w-3/4 shrink-0">
|
||||||
|
<AspectRatio ratio={16 / 9}>{getStream()}</AspectRatio>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-col gap-4 rounded-md bg-slate-elevation1 p-4">
|
||||||
|
<header className="text-lg">Current Task</header>
|
||||||
|
{workflowRunIsLoading || !currentRunningTask ? (
|
||||||
|
<div>Waiting for a task to start...</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full flex-col gap-2">
|
||||||
|
<div className="flex gap-2 bg-slate-elevation2 p-2">
|
||||||
|
<Label className="text-sm text-slate-400">ID</Label>
|
||||||
|
<span className="text-sm">{currentRunningTask.task_id}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 bg-slate-elevation2 p-2">
|
||||||
|
<Label className="text-sm text-slate-400">URL</Label>
|
||||||
|
<span className="text-sm">
|
||||||
|
{currentRunningTask.request.url}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 bg-slate-elevation2 p-2">
|
||||||
|
<Label className="text-sm text-slate-400">Status</Label>
|
||||||
|
<span className="text-sm">
|
||||||
|
<StatusBadge status={currentRunningTask.status} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 bg-slate-elevation2 p-2">
|
||||||
|
<Label className="text-sm text-slate-400">Created</Label>
|
||||||
|
<span className="text-sm">
|
||||||
|
{currentRunningTask &&
|
||||||
|
timeFormatWithShortDate(currentRunningTask.created_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-auto flex justify-end">
|
||||||
|
<Button asChild>
|
||||||
|
<Link to={`/tasks/${currentRunningTask.task_id}/actions`}>
|
||||||
|
<ReaderIcon className="mr-2 h-4 w-4" />
|
||||||
|
View Actions
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-5">
|
||||||
<header>
|
<header>
|
||||||
<h2 className="text-lg font-semibold">Tasks</h2>
|
<h2 className="text-2xl">
|
||||||
|
{workflowRunIsRunningOrQueued ? "Previous Blocks" : "Blocks"}
|
||||||
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader className="rounded-t-md bg-slate-elevation1">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-1/4">ID</TableHead>
|
<TableHead className="w-1/4 rounded-tl-md text-slate-400">
|
||||||
<TableHead className="w-1/4">URL</TableHead>
|
ID
|
||||||
<TableHead className="w-1/6">Status</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-1/4">Created At</TableHead>
|
<TableHead className="w-1/4 text-slate-400">URL</TableHead>
|
||||||
<TableHead className="w-1/12" />
|
<TableHead className="w-1/6 text-slate-400">Status</TableHead>
|
||||||
|
<TableHead className="w-1/4 text-slate-400">
|
||||||
|
Created At
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="w-1/12 rounded-tr-md" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -332,39 +399,51 @@ function WorkflowRun() {
|
|||||||
<TableCell colSpan={5}>No tasks</TableCell>
|
<TableCell colSpan={5}>No tasks</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
workflowTasks?.map((task) => {
|
workflowTasks
|
||||||
return (
|
?.filter(
|
||||||
<TableRow key={task.task_id}>
|
(task) => task.task_id !== currentRunningTask?.task_id,
|
||||||
<TableCell
|
)
|
||||||
className="w-1/4 cursor-pointer"
|
.map((task) => {
|
||||||
onClick={(event) => handleNavigate(event, task.task_id)}
|
return (
|
||||||
>
|
<TableRow key={task.task_id}>
|
||||||
{task.task_id}
|
<TableCell
|
||||||
</TableCell>
|
className="w-1/4 cursor-pointer"
|
||||||
<TableCell
|
onClick={(event) =>
|
||||||
className="w-1/4 max-w-64 cursor-pointer overflow-hidden overflow-ellipsis whitespace-nowrap"
|
handleNavigate(event, task.task_id)
|
||||||
onClick={(event) => handleNavigate(event, task.task_id)}
|
}
|
||||||
>
|
>
|
||||||
{task.request.url}
|
{task.task_id}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
className="w-1/6 cursor-pointer"
|
className="w-1/4 max-w-64 cursor-pointer overflow-hidden overflow-ellipsis whitespace-nowrap"
|
||||||
onClick={(event) => handleNavigate(event, task.task_id)}
|
onClick={(event) =>
|
||||||
>
|
handleNavigate(event, task.task_id)
|
||||||
<StatusBadge status={task.status} />
|
}
|
||||||
</TableCell>
|
>
|
||||||
<TableCell
|
{task.request.url}
|
||||||
className="w-1/4 cursor-pointer"
|
</TableCell>
|
||||||
onClick={(event) => handleNavigate(event, task.task_id)}
|
<TableCell
|
||||||
>
|
className="w-1/6 cursor-pointer"
|
||||||
{basicTimeFormat(task.created_at)}
|
onClick={(event) =>
|
||||||
</TableCell>
|
handleNavigate(event, task.task_id)
|
||||||
<TableCell className="w-1/12">
|
}
|
||||||
<TaskActions task={task} />
|
>
|
||||||
</TableCell>
|
<StatusBadge status={task.status} />
|
||||||
</TableRow>
|
</TableCell>
|
||||||
);
|
<TableCell
|
||||||
})
|
className="w-1/4 cursor-pointer"
|
||||||
|
onClick={(event) =>
|
||||||
|
handleNavigate(event, task.task_id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{basicTimeFormat(task.created_at)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="w-1/12">
|
||||||
|
<TaskActions task={task} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
@@ -399,23 +478,32 @@ function WorkflowRun() {
|
|||||||
</Pagination>
|
</Pagination>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
{Object.entries(parameters).length > 0 && (
|
||||||
<header>
|
<div className="space-y-4">
|
||||||
<h2 className="text-lg font-semibold">Parameters</h2>
|
<header>
|
||||||
</header>
|
<h2 className="text-lg font-semibold">Parameters</h2>
|
||||||
{Object.entries(parameters).map(([key, value]) => {
|
</header>
|
||||||
return (
|
{Object.entries(parameters).map(([key, value]) => {
|
||||||
<div key={key} className="flex flex-col gap-2">
|
return (
|
||||||
<Label>{key}</Label>
|
<div key={key} className="flex flex-col gap-2">
|
||||||
{typeof value === "string" ? (
|
<Label>{key}</Label>
|
||||||
<Input value={value} readOnly />
|
{typeof value === "string" ? (
|
||||||
) : (
|
<Input value={value} readOnly />
|
||||||
<Input value={JSON.stringify(value)} readOnly />
|
) : (
|
||||||
)}
|
<CodeEditor
|
||||||
</div>
|
value={JSON.stringify(value, null, 2)}
|
||||||
);
|
disabled
|
||||||
})}
|
language="json"
|
||||||
</div>
|
fontSize={12}
|
||||||
|
minHeight="96px"
|
||||||
|
maxHeight="500px"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { cn } from "@/util/utils";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
language: "python" | "json";
|
language: "python" | "json";
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
minHeight?: string;
|
minHeight?: string;
|
||||||
|
|||||||
@@ -10,4 +10,12 @@ function basicTimeFormat(time: string): string {
|
|||||||
return `${dateString} at ${timeString}`;
|
return `${dateString} at ${timeString}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { basicTimeFormat };
|
function timeFormatWithShortDate(time: string): string {
|
||||||
|
const date = new Date(time);
|
||||||
|
const dateString =
|
||||||
|
date.getMonth() + 1 + "/" + date.getDate() + "/" + date.getFullYear();
|
||||||
|
const timeString = date.toLocaleTimeString("en-US");
|
||||||
|
return `${dateString} at ${timeString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { basicTimeFormat, timeFormatWithShortDate };
|
||||||
|
|||||||
Reference in New Issue
Block a user