import { useMutation } from "@tanstack/react-query" import { ArrowRight, RotateCcw, Trash2 } from "lucide-react" import { FlowsService } from "@/client" import { Marquee } from "@/components/Common/Marquee" import { Button } from "@/components/ui/button" import { Popover, PopoverAnchor, PopoverContent } from "@/components/ui/popover" import useCustomToast from "@/hooks/useCustomToast" import { displayName } from "./deriveEdges" import { useLiveValue } from "./liveStore" import { MessageSparkline } from "./MessageSparkline" import { ValuePreview } from "./ValuePreview" function relativeTime(ts: number | null | undefined): string { if (!ts) return "not seen yet" const seconds = Math.max(0, Math.round(Date.now() / 1000 - ts)) if (seconds < 5) return "just now" if (seconds < 60) return `${seconds}s ago` if (seconds < 3600) return `${Math.round(seconds / 60)}m ago` return `${Math.round(seconds / 3600)}h ago` } /** * A value short enough to share the summary row, or `null` for the objects and * arrays that need the payload view instead. */ function formatScalar(value: unknown): string | null { // Three decimals, trailing zeros dropped: enough precision to be useful, never // wide enough to wrap the row. if (typeof value === "number") return String(Number(value.toFixed(3))) if (typeof value === "string") return value if (typeof value === "boolean" || value === null) return String(value) return null } export type InspectedEdge = { message: string /** Node titles, so the popover names the two ends in the user's own words. */ from: string to: string /** The producing node's id, which is what replaying the message goes through. */ sourceId: string x: number y: number } /** * What last travelled along an edge, and between which two nodes. */ export function EdgeInspector({ edge, flow, onClose, onUnbind, }: { edge: InspectedEdge | null flow: string onClose: () => void onUnbind: (message: string) => void }) { const live = useLiveValue(edge?.message) const { showErrorToast } = useCustomToast() // Publishing the value again from the node that produced it runs everything // downstream exactly as the original did. const replay = useMutation({ mutationFn: (value: unknown) => FlowsService.triggerNode({ name: flow, nodeId: edge?.sourceId ?? "", requestBody: { values: { [edge?.message ?? ""]: value } }, }), onError: () => showErrorToast("The message could not be sent again."), }) if (!edge) return null const scalar = live === undefined ? null : formatScalar(live.value) return ( !open && onClose()}>
{live === undefined ? null : ( )}
{scalar === null ? null : ( {scalar} )} {relativeTime(live?.ts)} {/* The same curve the node panel draws for this message: one value says little, how it has been moving says the rest. Only for a number, because only numbers are recorded — for anything else the sparkline falls through to its own copy of the value, which the popover already shows below. */} {typeof live?.value === "number" ? (
) : null} {/* Several nodes can publish one message, and so can a dashboard or another flow — so the value alone does not say what caused it. */} {live?.source && live.source.kind !== "node" ? (

Last set from {live.source.label} {live.source.detail ? ` (${live.source.detail})` : ""}.

) : null} {live === undefined ? (

Nothing has come through yet. Run the flow to see a value here.

) : scalar === null ? ( ) : null} ) }