feat(autogpt_builder/monitor): Monitor page v0.2 (#7385)
- Improve responsive grid layout - Remove `container` class from `<main>` to utilize full screen width - Improve detail views & add view for run details - Make flow run list entries selectable - Create `FlowInfo` and `FlowRunInfo` components - Improve layout of `FlowRunsStats` - Improve ScrollableLegend spacing & styling - Hide scroll bar of scrollable legend - Center legend items if there is space left - Round icons - Vertically align icons with labels - FIX: Add condition to not display legend items for series with `legendType="none"` - Add periodic 5s refresh of non-terminal flow run statuses - Split off `refreshFlowRuns(flowID)` from `fetchFlowsAndRuns()` - Add effect to run `refreshFlowRuns` every 5 seconds - Improve and expand FlowRun info - Add `FlowRun.totalRunTime`: sum of the individual execution durations of all nodes - Add `FlowRun.endTime` - Use `NodeExecutionResult.add_time` instead of `start_time` as `FlowRun.startTime` - Sort Flows by last executed - Add icons to navbar items & hide unused items Backtrack and Explore - Change UI mentions of "(Agent) Flow" to "Agent"pull/7395/head
parent
b7096e01fb
commit
a2d8d9bac9
|
@ -4,6 +4,7 @@ import { ThemeProvider as NextThemeProvider } from "next-themes";
|
|||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||
import { Inter } from "next/font/google";
|
||||
import Link from "next/link";
|
||||
import { CubeIcon, Pencil1Icon, ReaderIcon, TimerIcon } from "@radix-ui/react-icons";
|
||||
|
||||
import "./globals.css";
|
||||
|
||||
|
@ -27,10 +28,12 @@ function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
|||
const NavBar = () => (
|
||||
<nav className="bg-white dark:bg-slate-800 p-4 flex justify-between items-center shadow">
|
||||
<div className="flex space-x-4">
|
||||
<Link href="/monitor" className={buttonVariants({ variant: "ghost" })}>Monitor</Link>
|
||||
<Link href="/build" className={buttonVariants({ variant: "ghost" })}>Build</Link>
|
||||
<Link href="/backtrack" className={buttonVariants({ variant: "ghost" })}>Backtrack</Link>
|
||||
<Link href="/explore" className={buttonVariants({ variant: "ghost" })}>Explore</Link>
|
||||
<Link href="/monitor" className={buttonVariants({ variant: "ghost" })}>
|
||||
<TimerIcon className="mr-1" /> Monitor
|
||||
</Link>
|
||||
<Link href="/build" className={buttonVariants({ variant: "ghost" })}>
|
||||
<Pencil1Icon className="mr-1" /> Build
|
||||
</Link>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
@ -66,7 +69,7 @@ export default function RootLayout({
|
|||
>
|
||||
<div className="min-h-screen bg-gray-200 text-gray-900">
|
||||
<NavBar />
|
||||
<main className="container mx-auto p-4">
|
||||
<main className="mx-auto p-4">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
@ -2,10 +2,20 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import moment from 'moment';
|
||||
import { ComposedChart, Legend, Line, ResponsiveContainer, Scatter, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
import {
|
||||
ComposedChart,
|
||||
DefaultLegendContentProps,
|
||||
Legend,
|
||||
Line,
|
||||
ResponsiveContainer,
|
||||
Scatter,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
import { Pencil2Icon } from '@radix-ui/react-icons';
|
||||
import AutoGPTServerAPI, { Flow, NodeExecutionResult } from '@/lib/autogpt_server_api';
|
||||
import { hashString } from '@/lib/utils';
|
||||
import { cn, hashString } from '@/lib/utils';
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
|
@ -17,20 +27,42 @@ const Monitor = () => {
|
|||
const [flows, setFlows] = useState<Flow[]>([]);
|
||||
const [flowRuns, setFlowRuns] = useState<FlowRun[]>([]);
|
||||
const [selectedFlow, setSelectedFlow] = useState<Flow | null>(null);
|
||||
const [selectedRun, setSelectedRun] = useState<FlowRun | null>(null);
|
||||
|
||||
const api = new AutoGPTServerAPI();
|
||||
|
||||
useEffect(() => fetchFlowsAndRuns(), []);
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => flows.map(f => refreshFlowRuns(f.id)), 5000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
function fetchFlowsAndRuns() {
|
||||
// Fetch flow IDs
|
||||
api.listFlowIDs()
|
||||
.then(flowIDs => {
|
||||
Promise.all(flowIDs.map(flowID => {
|
||||
refreshFlowRuns(flowID);
|
||||
|
||||
// Fetch flow
|
||||
return api.getFlow(flowID);
|
||||
}))
|
||||
.then(flows => setFlows(flows));
|
||||
});
|
||||
}
|
||||
|
||||
function refreshFlowRuns(flowID: string) {
|
||||
// Fetch flow run IDs
|
||||
api.listFlowRunIDs(flowID)
|
||||
.then(runIDs => {
|
||||
runIDs.map(runID => {
|
||||
.then(runIDs => runIDs.map(runID => {
|
||||
let run;
|
||||
if (
|
||||
(run = flowRuns.find(fr => fr.id == runID))
|
||||
&& !["waiting", "running"].includes(run.status)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch flow run
|
||||
api.getFlowExecutionInfo(flowID, runID)
|
||||
.then(execInfo => setFlowRuns(flowRuns => {
|
||||
|
@ -44,28 +76,23 @@ const Monitor = () => {
|
|||
}
|
||||
return flowRuns
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
// Fetch flow
|
||||
return api.getFlow(flowID);
|
||||
}))
|
||||
.then(flows => setFlows(flows));
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 xl:grid-cols-10 gap-4">
|
||||
<div className="lg:col-span-2 xl:col-span-2">
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10 gap-4">
|
||||
<AgentFlowList
|
||||
className="md:col-span-2 xl:col-span-3 xxl:col-span-2"
|
||||
flows={flows}
|
||||
flowRuns={flowRuns}
|
||||
selectedFlow={selectedFlow}
|
||||
onSelectFlow={f => setSelectedFlow(f.id == selectedFlow?.id ? null : f)}
|
||||
onSelectFlow={f => {
|
||||
setSelectedRun(null);
|
||||
setSelectedFlow(f.id == selectedFlow?.id ? null : f);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="lg:col-span-2 xl:col-span-2 space-y-4">
|
||||
<FlowRunsList
|
||||
className="md:col-span-3 lg:col-span-2 xl:col-span-3 space-y-4"
|
||||
flows={flows}
|
||||
runs={
|
||||
(
|
||||
|
@ -75,29 +102,24 @@ const Monitor = () => {
|
|||
)
|
||||
.toSorted((a, b) => Number(a.startTime) - Number(b.startTime))
|
||||
}
|
||||
selectedRun={selectedRun}
|
||||
onSelectRun={r => setSelectedRun(r.id == selectedRun?.id ? null : r)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 lg:col-span-4 xl:col-span-6">
|
||||
{selectedFlow && (
|
||||
<Card>
|
||||
<CardHeader className="flex-row items-center justify-between space-y-0 space-x-3">
|
||||
<div>
|
||||
<CardTitle>{selectedFlow.name}</CardTitle>
|
||||
<p className="mt-2"><code>{selectedFlow.id}</code></p>
|
||||
</div>
|
||||
<Link className={buttonVariants({ variant: "outline" })} href={`/build?flowID=${selectedFlow.id}`}>
|
||||
<Pencil2Icon className="mr-2" /> Edit Flow
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FlowRunsStats
|
||||
flows={flows}
|
||||
flowRuns={flowRuns.filter(v => v.flowID == selectedFlow.id)}
|
||||
<div className="col-span-full xl:col-span-4 xxl:col-span-5">
|
||||
{selectedRun && (
|
||||
<FlowRunInfo
|
||||
flow={selectedFlow || flows.find(f => f.id == selectedRun.flowID)!}
|
||||
flowRun={selectedRun}
|
||||
/>
|
||||
) || selectedFlow && (
|
||||
<FlowInfo
|
||||
flow={selectedFlow}
|
||||
flowRuns={flowRuns.filter(r => r.flowID == selectedFlow.id)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) || (
|
||||
<Card className="p-6">
|
||||
<FlowRunsStats flows={flows} flowRuns={flowRuns} />
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -109,7 +131,9 @@ type FlowRun = {
|
|||
flowID: string
|
||||
status: 'running' | 'waiting' | 'success' | 'failed'
|
||||
startTime: number // unix timestamp (ms)
|
||||
endTime: number // unix timestamp (ms)
|
||||
duration: number // seconds
|
||||
totalRunTime: number // seconds
|
||||
|
||||
nodeExecutionResults: NodeExecutionResult[]
|
||||
};
|
||||
|
@ -131,38 +155,47 @@ function flowRunFromNodeExecutionResults(
|
|||
}
|
||||
}
|
||||
|
||||
// Determine aggregate startTime and duration
|
||||
// Determine aggregate startTime, endTime, and totalRunTime
|
||||
const now = Date.now();
|
||||
const startTime = Math.min(
|
||||
...nodeExecutionResults.map(ner => ner.start_time?.getTime() || Date.now())
|
||||
...nodeExecutionResults.map(ner => ner.add_time.getTime()), now
|
||||
);
|
||||
const endTime = (
|
||||
['success', 'failed'].includes(status)
|
||||
? Math.max(...nodeExecutionResults.map(ner => ner.end_time?.getTime() || 0))
|
||||
: Date.now()
|
||||
? Math.max(
|
||||
...nodeExecutionResults.map(ner => ner.end_time?.getTime() || 0), startTime
|
||||
)
|
||||
: now
|
||||
);
|
||||
const duration = (endTime - startTime) / 1000; // Convert to seconds
|
||||
const totalRunTime = nodeExecutionResults.reduce((cum, node) => (
|
||||
cum + ((node.end_time?.getTime() ?? now) - (node.start_time?.getTime() ?? now))
|
||||
), 0) / 1000;
|
||||
|
||||
return {
|
||||
id: runID,
|
||||
flowID: flowID,
|
||||
status,
|
||||
startTime,
|
||||
endTime,
|
||||
duration,
|
||||
totalRunTime,
|
||||
nodeExecutionResults: nodeExecutionResults
|
||||
};
|
||||
}
|
||||
|
||||
const AgentFlowList = (
|
||||
{ flows, flowRuns, selectedFlow, onSelectFlow }: {
|
||||
{ flows, flowRuns, selectedFlow, onSelectFlow, className }: {
|
||||
flows: Flow[],
|
||||
flowRuns?: FlowRun[],
|
||||
selectedFlow: Flow | null,
|
||||
onSelectFlow: (f: Flow) => void,
|
||||
className?: string,
|
||||
}
|
||||
) => (
|
||||
<Card>
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>Agent Flows</CardTitle>
|
||||
<CardTitle>Agents</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
|
@ -171,21 +204,30 @@ const AgentFlowList = (
|
|||
<TableHead>Name</TableHead>
|
||||
{/* <TableHead>Status</TableHead> */}
|
||||
{/* <TableHead>Last updated</TableHead> */}
|
||||
{flowRuns && <TableHead># of runs</TableHead>}
|
||||
{flowRuns && <TableHead className="md:hidden lg:table-cell"># of runs</TableHead>}
|
||||
{flowRuns && <TableHead>Last run</TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{flows.map((flow) => {
|
||||
let runCount, lastRun: FlowRun | null;
|
||||
{flows
|
||||
.map((flow) => {
|
||||
let runCount = 0, lastRun: FlowRun | null = null;
|
||||
if (flowRuns) {
|
||||
const _flowRuns = flowRuns.filter(r => r.flowID == flow.id);
|
||||
runCount = _flowRuns.length;
|
||||
lastRun = runCount == 0 ? null : _flowRuns.reduce(
|
||||
(a, c) => a.startTime < c.startTime ? a : c
|
||||
(a, c) => a.startTime > c.startTime ? a : c
|
||||
);
|
||||
}
|
||||
return (
|
||||
return { flow, runCount, lastRun };
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (!a.lastRun && !b.lastRun) return 0;
|
||||
if (!a.lastRun) return 1;
|
||||
if (!b.lastRun) return -1;
|
||||
return b.lastRun.startTime - a.lastRun.startTime;
|
||||
})
|
||||
.map(({ flow, runCount, lastRun }) => (
|
||||
<TableRow
|
||||
key={flow.id}
|
||||
className="cursor-pointer"
|
||||
|
@ -197,14 +239,14 @@ const AgentFlowList = (
|
|||
{/* <TableCell>
|
||||
{flow.updatedAt ?? "???"}
|
||||
</TableCell> */}
|
||||
{flowRuns && <TableCell>{runCount}</TableCell>}
|
||||
{flowRuns && <TableCell className="md:hidden lg:table-cell">{runCount}</TableCell>}
|
||||
{flowRuns && (!lastRun ? <TableCell /> :
|
||||
<TableCell title={moment(lastRun.startTime).toString()}>
|
||||
{moment(lastRun.startTime).fromNow()}
|
||||
</TableCell>)}
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
))
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
|
@ -224,16 +266,22 @@ const FlowStatusBadge = ({ status }: { status: "active" | "disabled" | "failing"
|
|||
</Badge>
|
||||
);
|
||||
|
||||
const FlowRunsList = ({ flows, runs }: { flows: Flow[], runs: FlowRun[] }) => (
|
||||
<Card>
|
||||
const FlowRunsList: React.FC<{
|
||||
flows: Flow[];
|
||||
runs: FlowRun[];
|
||||
className?: string;
|
||||
selectedRun?: FlowRun | null;
|
||||
onSelectRun: (r: FlowRun) => void;
|
||||
}> = ({ flows, runs, selectedRun, onSelectRun, className }) => (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>Flow Runs</CardTitle>
|
||||
<CardTitle>Runs</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Flow</TableHead>
|
||||
<TableHead>Agent</TableHead>
|
||||
<TableHead>Started</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Duration</TableHead>
|
||||
|
@ -241,7 +289,12 @@ const FlowRunsList = ({ flows, runs }: { flows: Flow[], runs: FlowRun[] }) => (
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
{runs.map((run) => (
|
||||
<TableRow key={run.id}>
|
||||
<TableRow
|
||||
key={run.id}
|
||||
className="cursor-pointer"
|
||||
onClick={() => onSelectRun(run)}
|
||||
data-state={selectedRun?.id == run.id ? "selected" : null}
|
||||
>
|
||||
<TableCell>{flows.find(f => f.id == run.flowID)!.name}</TableCell>
|
||||
<TableCell>{moment(run.startTime).format("HH:mm")}</TableCell>
|
||||
<TableCell><FlowRunStatusBadge status={run.status} /></TableCell>
|
||||
|
@ -254,54 +307,82 @@ const FlowRunsList = ({ flows, runs }: { flows: Flow[], runs: FlowRun[] }) => (
|
|||
</Card>
|
||||
);
|
||||
|
||||
const FlowRunStatusBadge = ({ status }: { status: FlowRun['status'] }) => (
|
||||
const FlowRunStatusBadge: React.FC<{
|
||||
status: FlowRun['status'];
|
||||
className?: string;
|
||||
}> = ({ status, className }) => (
|
||||
<Badge
|
||||
variant="default"
|
||||
className={
|
||||
className={cn(
|
||||
status === 'running' ? 'bg-blue-500 dark:bg-blue-700' :
|
||||
status === 'waiting' ? 'bg-yellow-500 dark:bg-yellow-600' :
|
||||
status === 'success' ? 'bg-green-500 dark:bg-green-600' :
|
||||
'bg-red-500 dark:bg-red-700'
|
||||
}
|
||||
'bg-red-500 dark:bg-red-700',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
const ScrollableLegend = ({ payload }) => {
|
||||
return (
|
||||
<div style={{
|
||||
overflowX: 'auto',
|
||||
overflowY: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
padding: '10px 0',
|
||||
fontSize: '0.75em'
|
||||
}}>
|
||||
{payload.map((entry, index) => (
|
||||
<span key={`item-${index}`} style={{ display: 'inline-block', marginRight: '10px' }}>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
marginRight: '5px',
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
backgroundColor: entry.color,
|
||||
}}
|
||||
/>
|
||||
<span>{entry.value}</span>
|
||||
</span>
|
||||
))}
|
||||
const FlowInfo: React.FC<{
|
||||
flow: Flow;
|
||||
flowRuns: FlowRun[];
|
||||
}> = ({ flow, flowRuns }) => {
|
||||
return <Card>
|
||||
<CardHeader className="flex-row items-center justify-between space-y-0 space-x-3">
|
||||
<div>
|
||||
<CardTitle>{flow.name}</CardTitle>
|
||||
<p className="mt-2">Agent ID: <code>{flow.id}</code></p>
|
||||
</div>
|
||||
);
|
||||
<Link className={buttonVariants({ variant: "outline" })} href={`/build?flowID=${flow.id}`}>
|
||||
<Pencil2Icon className="mr-2" /> Edit Agent
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FlowRunsStats
|
||||
flows={[flow]}
|
||||
flowRuns={flowRuns.filter(r => r.flowID == flow.id)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>;
|
||||
};
|
||||
|
||||
const FlowRunInfo: React.FC<{
|
||||
flow: Flow;
|
||||
flowRun: FlowRun;
|
||||
}> = ({ flow, flowRun }) => {
|
||||
if (flowRun.flowID != flow.id) {
|
||||
throw new Error(`FlowRunInfo can't be used with non-matching flowRun.flowID and flow.id`)
|
||||
}
|
||||
|
||||
const FlowRunsStats = (
|
||||
{ flows, flowRuns }: {
|
||||
return <Card>
|
||||
<CardHeader className="flex-row items-center justify-between space-y-0 space-x-3">
|
||||
<div>
|
||||
<CardTitle>{flow.name}</CardTitle>
|
||||
<p className="mt-2">Agent ID: <code>{flow.id}</code></p>
|
||||
<p className="mt-1">Run ID: <code>{flowRun.id}</code></p>
|
||||
</div>
|
||||
<Link className={buttonVariants({ variant: "outline" })} href={`/build?flowID=${flow.id}`}>
|
||||
<Pencil2Icon className="mr-2" /> Edit Agent
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p><strong>Status:</strong> <FlowRunStatusBadge status={flowRun.status} /></p>
|
||||
<p><strong>Started:</strong> {moment(flowRun.startTime).format('YYYY-MM-DD HH:mm:ss')}</p>
|
||||
<p><strong>Finished:</strong> {moment(flowRun.endTime).format('YYYY-MM-DD HH:mm:ss')}</p>
|
||||
<p><strong>Duration (run time):</strong> {flowRun.duration} ({flowRun.totalRunTime}) seconds</p>
|
||||
{/* <p><strong>Total cost:</strong> €1,23</p> */}
|
||||
</CardContent>
|
||||
</Card>;
|
||||
};
|
||||
|
||||
const FlowRunsStats: React.FC<{
|
||||
flows: Flow[],
|
||||
flowRuns: FlowRun[],
|
||||
}
|
||||
) => {
|
||||
title?: string,
|
||||
className?: string,
|
||||
}> = ({ flows, flowRuns, title, className }) => {
|
||||
/* "dateMin": since the first flow in the dataset
|
||||
* number > 0: custom date (unix timestamp)
|
||||
* number < 0: offset relative to Date.now() (in seconds) */
|
||||
|
@ -318,9 +399,9 @@ const FlowRunsStats = (
|
|||
: flowRuns;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Flow Run Stats</CardTitle>
|
||||
<div className={className}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<CardTitle>{ title || "Stats" }</CardTitle>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm" onClick={() => setStatsSince(-2*3600)}>2h</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => setStatsSince(-8*3600)}>8h</Button>
|
||||
|
@ -340,20 +421,19 @@ const FlowRunsStats = (
|
|||
</Popover>
|
||||
<Button variant="outline" size="sm" onClick={() => setStatsSince("dataMin")}>All</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FlowRunsTimeline flows={flows} flowRuns={flowRuns} dataMin={statsSince} className={"mb-6"} />
|
||||
<Card className="p-3">
|
||||
</div>
|
||||
<FlowRunsTimeline flows={flows} flowRuns={flowRuns} dataMin={statsSince} className="mt-3" />
|
||||
<hr className="my-4" />
|
||||
<div>
|
||||
<p><strong>Total runs:</strong> {filteredFlowRuns.length}</p>
|
||||
<p>
|
||||
<strong>Total duration:</strong> {
|
||||
filteredFlowRuns.reduce((total, run) => total + run.duration, 0)
|
||||
<strong>Total run time:</strong> {
|
||||
filteredFlowRuns.reduce((total, run) => total + run.totalRunTime, 0)
|
||||
} seconds
|
||||
</p>
|
||||
{/* <p><strong>Total cost:</strong> €1,23</p> */}
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -401,13 +481,16 @@ const FlowRunsTimeline = (
|
|||
const data: FlowRun & { time: number, _duration: number } = payload[0].payload;
|
||||
const flow = flows.find(f => f.id === data.flowID);
|
||||
return (
|
||||
<Card className="p-3">
|
||||
<p><strong>Flow:</strong> {flow ? flow.name : 'Unknown'}</p>
|
||||
<p><strong>Start Time:</strong> {moment(data.startTime).format('YYYY-MM-DD HH:mm:ss')}</p>
|
||||
<Card className="p-2 text-xs leading-normal">
|
||||
<p><strong>Agent:</strong> {flow ? flow.name : 'Unknown'}</p>
|
||||
<p>
|
||||
<strong>Duration:</strong> {formatDuration(data.duration)}
|
||||
<strong>Status:</strong>
|
||||
<FlowRunStatusBadge status={data.status} className="px-1.5 py-0" />
|
||||
</p>
|
||||
<p><strong>Status:</strong> <FlowRunStatusBadge status={data.status} /></p>
|
||||
<p><strong>Started:</strong> {moment(data.startTime).format('YYYY-MM-DD HH:mm:ss')}</p>
|
||||
<p><strong>Duration / run time:</strong> {
|
||||
formatDuration(data.duration)} / {formatDuration(data.totalRunTime)
|
||||
}</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
@ -419,8 +502,8 @@ const FlowRunsTimeline = (
|
|||
key={flow.id}
|
||||
data={flowRuns.filter(fr => fr.flowID == flow.id).map(fr => ({
|
||||
...fr,
|
||||
time: fr.startTime + (fr.duration * 1000),
|
||||
_duration: fr.duration,
|
||||
time: fr.startTime + (fr.totalRunTime * 1000),
|
||||
_duration: fr.totalRunTime,
|
||||
}))}
|
||||
name={flow.name}
|
||||
fill={`hsl(${hashString(flow.id) * 137.5 % 360}, 70%, 50%)`}
|
||||
|
@ -433,7 +516,7 @@ const FlowRunsTimeline = (
|
|||
dataKey="_duration"
|
||||
data={[
|
||||
{ ...run, time: run.startTime, _duration: 0 },
|
||||
{ ...run, time: run.startTime + (run.duration * 1000), _duration: run.duration }
|
||||
{ ...run, time: run.endTime, _duration: run.totalRunTime }
|
||||
]}
|
||||
stroke={`hsl(${hashString(run.flowID) * 137.5 % 360}, 70%, 50%)`}
|
||||
strokeWidth={2}
|
||||
|
@ -443,12 +526,46 @@ const FlowRunsTimeline = (
|
|||
))}
|
||||
<Legend
|
||||
content={<ScrollableLegend />}
|
||||
wrapperStyle={{ bottom: 0, left: 0, right: 0 }}
|
||||
wrapperStyle={{
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
|
||||
const ScrollableLegend: React.FC<DefaultLegendContentProps & { className?: string }> = (
|
||||
{ payload, className }
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"whitespace-nowrap px-4 text-sm overflow-x-auto space-x-3",
|
||||
className,
|
||||
)}
|
||||
style={{ scrollbarWidth: "none" }}
|
||||
>
|
||||
{payload.map((entry, index) => {
|
||||
if (entry.type == "none") return;
|
||||
return (
|
||||
<span key={`item-${index}`} className="inline-flex items-center">
|
||||
<span
|
||||
className="size-2.5 inline-block mr-1 rounded-full"
|
||||
style={{backgroundColor: entry.color}}
|
||||
/>
|
||||
<span>{entry.value}</span>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
return (
|
||||
seconds < 100
|
||||
|
|
Loading…
Reference in New Issue