Files
patel-lyzr 64a857d1b7 feat: add NodePalette and PipelineCanvas components for enhanced flow node management
- Introduced NodePalette component to display draggable nodes categorized by type.
- Implemented PipelineCanvas component to manage the flow of nodes and connections.
- Integrated drag-and-drop functionality for adding nodes to the canvas.
- Created a catalog of node types with associated metadata for rendering in the palette.
- Established conversion functions between pipeline definitions and React Flow state.
- Added inspector for editing node properties and managing connections.
- Updated Next.js configuration for production and development environments.
- Removed obsolete embed.go file and added nginx configuration for serving the app.
- Updated package dependencies including @xyflow/react for enhanced functionality.
- Added TypeScript definitions for CSS imports to support styling in components.
2026-05-13 22:15:08 +05:30

112 lines
3.6 KiB
TypeScript

// Conversion between the n8n-format pipeline JSON the Go server stores and
// the {nodes, edges} shape React Flow renders. Keeping this in one place
// keeps the canvas dumb (React Flow state in, React Flow state out).
import type { Edge, Node } from "@xyflow/react";
// --- Pipeline JSON shape (matches pkg/models.NodeDef + n8n connection map) ---
export type PipelineNode = {
id: string;
name: string;
type: string;
typeVersion?: number;
parameters?: Record<string, unknown>;
credentials?: Record<string, unknown>;
settings?: Record<string, unknown>;
position: [number, number];
};
type ConnectionTarget = { node: string; type: string; index: number };
export type PipelineDefinition = {
name?: string;
nodes: PipelineNode[];
connections: Record<string, { main: ConnectionTarget[][] }>;
settings?: Record<string, unknown>;
};
export type FlowNodeData = {
pipelineNode: PipelineNode;
};
// --- to React Flow --------------------------------------------------------
export function toReactFlow(def: PipelineDefinition | null | undefined): {
nodes: Node<FlowNodeData>[];
edges: Edge[];
} {
const nodes: Node<FlowNodeData>[] = (def?.nodes ?? []).map((n) => ({
id: n.name, // n8n keys connections by name, so use name as RF id
type: "flowNode",
position: { x: n.position?.[0] ?? 0, y: n.position?.[1] ?? 0 },
data: { pipelineNode: n },
}));
const edges: Edge[] = [];
const conns = def?.connections ?? {};
for (const sourceName of Object.keys(conns)) {
const main = conns[sourceName]?.main ?? [];
main.forEach((targets, sourceOutputIndex) => {
(targets ?? []).forEach((t) => {
if (!t?.node) return;
edges.push({
id: `e:${sourceName}:${sourceOutputIndex}->${t.node}:${t.index ?? 0}`,
source: sourceName,
target: t.node,
sourceHandle: `o-${sourceOutputIndex}`,
targetHandle: `i-${t.index ?? 0}`,
});
});
});
}
return { nodes, edges };
}
// --- from React Flow ------------------------------------------------------
export function fromReactFlow(
rfNodes: Node<FlowNodeData>[],
rfEdges: Edge[],
base: { name?: string; settings?: Record<string, unknown> } = {}
): PipelineDefinition {
const nodes: PipelineNode[] = rfNodes.map((rn) => {
const pn = rn.data?.pipelineNode;
return {
id: pn?.id ?? rn.id,
name: pn?.name ?? rn.id,
type: pn?.type ?? "flow-nodes-base.noOp",
typeVersion: pn?.typeVersion ?? 1,
parameters: pn?.parameters ?? {},
...(pn?.credentials ? { credentials: pn.credentials } : {}),
...(pn?.settings ? { settings: pn.settings } : {}),
position: [Math.round(rn.position.x), Math.round(rn.position.y)],
};
});
// Build connections map keyed by source node name. n8n shape:
// connections[src].main[outputIndex] = [{ node, type: "main", index }, ...]
const connections: PipelineDefinition["connections"] = {};
for (const e of rfEdges) {
const outIdx = parseHandleIndex(e.sourceHandle, "o-");
const inIdx = parseHandleIndex(e.targetHandle, "i-");
const slot = (connections[e.source] ??= { main: [] });
while (slot.main.length <= outIdx) slot.main.push([]);
slot.main[outIdx].push({ node: e.target, type: "main", index: inIdx });
}
return {
...(base.name !== undefined ? { name: base.name } : {}),
nodes,
connections,
...(base.settings ? { settings: base.settings } : {}),
};
}
function parseHandleIndex(h: string | null | undefined, prefix: string): number {
if (!h || !h.startsWith(prefix)) return 0;
const n = Number(h.slice(prefix.length));
return Number.isFinite(n) && n >= 0 ? n : 0;
}