import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { Link } from "@tanstack/react-router" import { AlertCircle, Workflow } from "lucide-react" import { type FlowRollup, type FlowSummary, FlowsService } from "@/client" import { byRecency } from "@/components/Common/DashboardMosaic" import type { Range } from "@/components/Common/RangePicker" import { flowKeys, flowsQueryOptions } from "@/components/Flow/queries" import { PANEL_SECTION } from "@/components/Flow/SidePanel" import { Badge } from "@/components/ui/badge" import { Skeleton } from "@/components/ui/skeleton" import { Switch } from "@/components/ui/switch" import useCustomToast from "@/hooks/useCustomToast" import { dur, si } from "@/lib/utils" import { CARD, flowRollupsQueryOptions } from "./queries" import { Spark } from "./Spark" /** Another tab can stop a flow, and the engine can fail one on its own. */ const REFRESH_INTERVAL = 10_000 /** About six rows. Past that the table scrolls rather than the page. */ const HEIGHT = "max-h-96" /** * Every flow, with what it has been doing. * * One table rather than a list beside a rollup table: they are two halves of * the same question and were previously read by matching rows up by eye. The * join is a left one — the rollups only carry flows that ran inside the * window, and a flow that has never run is still a flow. */ export function FlowTable({ range }: { range: Range }) { const { data, isPending } = useQuery({ ...flowsQueryOptions(), refetchInterval: REFRESH_INTERVAL, }) const { data: rollups } = useQuery(flowRollupsQueryOptions(range)) const flows = [...(data?.data ?? [])].sort(byRecency) const activity = new Map( (rollups ?? []).map((row: FlowRollup) => [row.flow, row]), ) return (

Flows

activity over the last {range.label}

{isPending ? (
) : flows.length === 0 ? (

Flows you build show up here, with what they are doing.

Go to flows
) : (
{/* The name anchors the left, what it is doing sits beside it, and the numbers read down their own centre with the trend closing the row on the right. */} {/* A bounded share rather than all the slack, so the numbers spread across the middle instead of huddling on the left. Its 128px floor is more than a phone has to spare, and a curve that narrow says nothing, so it goes below `sm`. */} {flows.map((flow) => ( ))}
Flow Status Run Executions Errors Avg Lag Trend
)}
) } function Row({ flow, activity, }: { flow: FlowSummary /** Absent when the flow did not run inside the selected window. */ activity?: FlowRollup }) { const queryClient = useQueryClient() const { showErrorToast } = useCustomToast() const enabled = flow.enabled ?? true const toggle = useMutation({ mutationFn: (next: boolean) => next ? FlowsService.startFlow({ name: flow.name }) : FlowsService.stopFlow({ name: flow.name }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: flowKeys.all }) queryClient.invalidateQueries({ queryKey: flowKeys.detail(flow.name) }) }, onError: () => showErrorToast("The flow could not be started or stopped."), }) // Nothing to report is not zero: a flow that never ran in this window and a // flow that ran and did nothing are different answers. const idle = — return ( {/* `max-w-0` is what lets a cell truncate at all: without it the table sizes to the longest name and pushes the page sideways. */}

{flow.title || flow.name}

{flow.node_count === 1 ? "1 node" : `${flow.node_count} nodes`} {flow.has_draft ? " · unpublished changes" : ""}

{(flow.error_count ?? 0) > 0 ? ( {flow.error_count} ) : null} {enabled ? (flow.paused ? "Paused" : "Running") : "Stopped"} toggle.mutate(next)} aria-label={`Run ${flow.title || flow.name}`} data-testid="flow-enabled-switch" /> {activity ? si(activity.executions) : idle} {!activity ? ( idle ) : activity.errors ? ( {si(activity.errors)} failed ) : ( none )} {activity ? dur(activity.avg_ms) : idle} {activity ? dur(activity.avg_lag_ms) : idle} {/* The dot straddles the curve's right edge, so the cell keeps a little room for the half that hangs out. */} {activity ? : idle} ) }