Midday / packages /workbench /src /ui /components /flows /flow-node.tsx
Jules
Final deployment with all fixes and verified content
c09f67c
import { Handle, Position } from "@xyflow/react";
import {
CheckCircle2,
Circle,
Clock,
Loader2,
Pause,
XCircle,
} from "lucide-react";
import { memo } from "react";
import type { FlowNode as FlowNodeType } from "@/core/types";
import { cn } from "@/lib/utils";
export interface FlowNodeData extends Record<string, unknown> {
flowNode: FlowNodeType;
onClick?: (flowNode: FlowNodeType) => void;
}
interface FlowNodeProps {
data: FlowNodeData;
}
const statusConfig: Record<
string,
{
icon: typeof CheckCircle2;
color: string;
bg: string;
border: string;
animate?: boolean;
}
> = {
completed: {
icon: CheckCircle2,
color: "text-emerald-600 dark:text-emerald-400",
bg: "bg-emerald-50 dark:bg-emerald-500/10",
border: "border-emerald-200 dark:border-emerald-500/20",
},
active: {
icon: Loader2,
color: "text-blue-600 dark:text-blue-400",
bg: "bg-blue-50 dark:bg-blue-500/10",
border: "border-blue-200 dark:border-blue-500/20",
animate: true,
},
waiting: {
icon: Circle,
color: "text-muted-foreground",
bg: "bg-muted/30",
border: "border-border",
},
delayed: {
icon: Clock,
color: "text-amber-600 dark:text-amber-400",
bg: "bg-amber-50 dark:bg-amber-500/10",
border: "border-amber-200 dark:border-amber-500/20",
},
failed: {
icon: XCircle,
color: "text-destructive",
bg: "bg-red-50 dark:bg-red-500/10",
border: "border-red-200 dark:border-red-500/20",
},
paused: {
icon: Pause,
color: "text-muted-foreground",
bg: "bg-muted/30",
border: "border-border",
},
unknown: {
icon: Circle,
color: "text-muted-foreground",
bg: "bg-muted/30",
border: "border-border",
},
};
function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
return `${(ms / 60000).toFixed(1)}m`;
}
function FlowNodeComponent({ data }: FlowNodeProps) {
const { flowNode, onClick } = data;
const { job, queueName } = flowNode;
const config = statusConfig[job.status] || statusConfig.unknown;
const Icon = config.icon;
return (
<div
className={cn(
"relative min-w-[180px] px-3 py-2.5 transition-all",
"hover:bg-accent/50 cursor-pointer",
"bg-background border",
config.border,
)}
onClick={() => onClick?.(flowNode)}
>
{/* Input handle (top) */}
<Handle
type="target"
position={Position.Top}
className="!w-2 !h-2 !bg-muted-foreground/40 !border-0"
/>
{/* Content */}
<div className="flex items-start gap-2.5">
<div className={cn("p-1.5 shrink-0", config.bg)}>
<Icon
className={cn(
"h-3.5 w-3.5",
config.color,
config.animate && "animate-spin",
)}
/>
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate text-foreground">
{job.name}
</div>
<div className="text-[11px] text-muted-foreground truncate mt-0.5">
{queueName}
</div>
{job.duration !== undefined && (
<div className="text-[11px] text-muted-foreground mt-1 font-mono tabular-nums">
{formatDuration(job.duration)}
</div>
)}
</div>
</div>
{/* Status badge */}
<div
className={cn(
"absolute -top-1.5 -right-1.5 px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-wide",
"bg-background border",
config.color,
config.border,
)}
>
{job.status}
</div>
{/* Output handle (bottom) */}
<Handle
type="source"
position={Position.Bottom}
className="!w-2 !h-2 !bg-muted-foreground/40 !border-0"
/>
</div>
);
}
export const FlowNodeMemo = memo(FlowNodeComponent);