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:
Shuchang Zheng
2024-10-14 13:07:54 -07:00
committed by GitHub
parent 33c0a5af55
commit dcca1a64d2
3 changed files with 171 additions and 75 deletions

View File

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

View File

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

View File

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