import { ArrowPathMini, MinusMini, PlusMini } from "@medusajs/icons" import { Container, DropdownMenu, Heading, Text, clx } from "@medusajs/ui" import { motion, useAnimationControls, useDragControls, useMotionValue, } from "motion/react" import { useEffect, useRef, useState } from "react" import { useTranslation } from "react-i18next" import { Link } from "react-router-dom" import { HttpTypes } from "@medusajs/types" import { STEP_ERROR_STATES, STEP_INACTIVE_STATES, STEP_IN_PROGRESS_STATES, STEP_OK_STATES, STEP_SKIPPED_STATES, } from "../../../constants" import { useDocumentDirection } from "../../../../../hooks/use-document-direction" type WorkflowExecutionTimelineSectionProps = { execution: HttpTypes.AdminWorkflowExecutionResponse["workflow_execution"] } export const WorkflowExecutionTimelineSection = ({ execution, }: WorkflowExecutionTimelineSectionProps) => { const { t } = useTranslation() return (
{t("general.timeline")}
) } const createNodeClusters = ( steps: Record ) => { const actionableSteps = Object.values(steps).filter( (step) => step.id !== "_root" ) const clusters: Record = {} actionableSteps.forEach((step) => { if (!clusters[step.depth]) { clusters[step.depth] = [] } clusters[step.depth].push(step) }) return clusters } const getNextCluster = ( clusters: Record, depth: number ) => { const nextDepth = depth + 1 return clusters[nextDepth] } type ZoomScale = 0.5 | 0.75 | 1 const defaultState = { x: -860, y: -1020, scale: 1, } const MAX_ZOOM = 1.5 const MIN_ZOOM = 0.5 const ZOOM_STEP = 0.25 const Canvas = ({ execution, }: { execution: HttpTypes.AdminWorkflowExecutionResponse["workflow_execution"] }) => { const [zoom, setZoom] = useState(1) const [isDragging, setIsDragging] = useState(false) const direction = useDocumentDirection() const scale = useMotionValue(defaultState.scale) const x = useMotionValue(defaultState.x) const y = useMotionValue(defaultState.y) const controls = useAnimationControls() const dragControls = useDragControls() const dragConstraints = useRef(null) const canZoomIn = zoom < MAX_ZOOM const canZoomOut = zoom > MIN_ZOOM useEffect(() => { const unsubscribe = scale.on("change", (latest) => { setZoom(latest as ZoomScale) }) return () => { unsubscribe() } }, [scale]) const clusters = createNodeClusters(execution.execution?.steps || {}) function scaleXandY( prevScale: number, newScale: number, x: number, y: number ) { const scaleRatio = newScale / prevScale return { x: x * scaleRatio, y: y * scaleRatio, } } const changeZoom = (newScale: number) => { const { x: newX, y: newY } = scaleXandY(zoom, newScale, x.get(), y.get()) setZoom(newScale) controls.set({ scale: newScale, x: newX, y: newY }) } const zoomIn = () => { const curr = scale.get() if (curr < 1.5) { const newScale = curr + ZOOM_STEP changeZoom(newScale) } } const zoomOut = () => { const curr = scale.get() if (curr > 0.5) { const newScale = curr - ZOOM_STEP changeZoom(newScale) } } const resetCanvas = () => { controls.start(defaultState) } return (
setIsDragging(true)} onMouseUp={() => setIsDragging(false)} drag dragConstraints={dragConstraints} dragElastic={0} dragMomentum={false} dragControls={dragControls} initial={false} animate={controls} transition={{ duration: 0.25 }} style={{ x, y, scale, }} className={clx( "bg-ui-bg-subtle relative size-[500rem] origin-top-left items-start justify-start overflow-hidden", "bg-[radial-gradient(var(--border-base)_1.5px,transparent_0)] bg-[length:20px_20px] bg-repeat", { "cursor-grab": !isDragging, "cursor-grabbing": isDragging, } )} >
{Object.entries(clusters).map(([depth, cluster]) => { const next = getNextCluster(clusters, Number(depth)) return (
{cluster.map((step) => ( ))}
) })}
{Math.round(zoom * 100)}% {[50, 75, 100, 125, 150].map((value) => ( changeZoom(value / 100)} > {value}% ))}
) } const HorizontalArrow = () => { return ( ) } const MiddleArrow = () => { return ( ) } const EndArrow = () => { return ( ) } const Arrow = ({ depth }: { depth: number }) => { if (depth === 1) { return } if (depth === 2) { return (
) } const inbetween = Array.from({ length: depth - 2 }).map((_, index) => ( )) return (
{inbetween}
) } const Line = ({ next }: { next?: HttpTypes.AdminWorkflowExecutionStep[] }) => { if (!next) { return null } return (
) } const Node = ({ step }: { step: HttpTypes.AdminWorkflowExecutionStep }) => { if (step.id === "_root") { return null } const stepId = step.id.split(".").pop() /** * We can't rely on the built-in hash scrolling because the collapsible, * so we instead need to manually scroll to the step when the hash changes */ const handleScrollTo = () => { if (!stepId) { return } const historyItem = document.getElementById(stepId) if (!historyItem) { return } /** * Scroll to the step if it's the one we're looking for but * we need to wait for the collapsible to open before scrolling */ setTimeout(() => { historyItem.scrollIntoView({ behavior: "smooth", block: "end", }) }, 100) } return (
{stepId}
) }