Expand governed Dataflow editor and node library
This commit is contained in:
@@ -15,12 +15,13 @@ import {
|
||||
} from "@xyflow/react";
|
||||
import type {
|
||||
DataflowDiagnostic,
|
||||
NodeTypeDefinition,
|
||||
NodePreviewDiagnostic,
|
||||
PipelineGraph,
|
||||
PipelineGraphNode
|
||||
} from "../../api/dataflow";
|
||||
import DataflowNode, { type DataflowFlowNode } from "./DataflowNode";
|
||||
import { newNode } from "./model";
|
||||
import { FALLBACK_NODE_LIBRARY, newNode } from "./model";
|
||||
|
||||
const nodeTypes = { dataflow: DataflowNode };
|
||||
|
||||
@@ -28,6 +29,7 @@ type DataflowCanvasProps = {
|
||||
graph: PipelineGraph;
|
||||
diagnostics: DataflowDiagnostic[];
|
||||
nodeDiagnostics: NodePreviewDiagnostic[];
|
||||
nodeLibrary: NodeTypeDefinition[];
|
||||
selectedNodeId: string | null;
|
||||
readOnly: boolean;
|
||||
onGraphChange: (graph: PipelineGraph) => void;
|
||||
@@ -38,6 +40,7 @@ export default function DataflowCanvas({
|
||||
graph,
|
||||
diagnostics,
|
||||
nodeDiagnostics,
|
||||
nodeLibrary,
|
||||
selectedNodeId,
|
||||
readOnly,
|
||||
onGraphChange,
|
||||
@@ -52,27 +55,39 @@ export default function DataflowCanvas({
|
||||
() => new Map(nodeDiagnostics.map((item) => [item.node_id, item.output_rows])),
|
||||
[nodeDiagnostics]
|
||||
);
|
||||
const definitions = useMemo(
|
||||
() => new Map(nodeLibrary.map((definition) => [definition.type, definition])),
|
||||
[nodeLibrary]
|
||||
);
|
||||
const nodes = useMemo<DataflowFlowNode[]>(
|
||||
() => graph.nodes.map((node) => ({
|
||||
id: node.id,
|
||||
type: "dataflow",
|
||||
position: node.position,
|
||||
selected: node.id === selectedNodeId,
|
||||
data: {
|
||||
label: node.label,
|
||||
transformType: node.type,
|
||||
config: node.config,
|
||||
hasError: errorNodeIds.has(node.id),
|
||||
outputRows: rowCounts.get(node.id)
|
||||
}
|
||||
})),
|
||||
[errorNodeIds, graph.nodes, rowCounts, selectedNodeId]
|
||||
() => graph.nodes.map((node) => {
|
||||
const definition = definitions.get(node.type)
|
||||
?? FALLBACK_NODE_LIBRARY.find((item) => item.type === node.type)
|
||||
?? FALLBACK_NODE_LIBRARY[0];
|
||||
return {
|
||||
id: node.id,
|
||||
type: "dataflow",
|
||||
position: node.position,
|
||||
selected: node.id === selectedNodeId,
|
||||
data: {
|
||||
label: node.label,
|
||||
transformType: node.type,
|
||||
definition,
|
||||
config: node.config,
|
||||
hasError: errorNodeIds.has(node.id),
|
||||
outputRows: rowCounts.get(node.id)
|
||||
}
|
||||
};
|
||||
}),
|
||||
[definitions, errorNodeIds, graph.nodes, rowCounts, selectedNodeId]
|
||||
);
|
||||
const edges = useMemo<Edge[]>(
|
||||
() => graph.edges.map((edge) => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
sourceHandle: edge.source_port ?? "output",
|
||||
targetHandle: edge.target_port ?? "input",
|
||||
type: "smoothstep",
|
||||
className: "dataflow-edge"
|
||||
})),
|
||||
@@ -113,10 +128,30 @@ export default function DataflowCanvas({
|
||||
const source = graph.nodes.find((node) => node.id === connection.source);
|
||||
const target = graph.nodes.find((node) => node.id === connection.target);
|
||||
if (!source || !target || source.type === "output" || target.type.startsWith("source.")) return false;
|
||||
const targetAlreadyConnected = graph.edges.some(
|
||||
(edge) => edge.target === connection.target && edge.source !== connection.source
|
||||
const sourcePort = connection.sourceHandle ?? "output";
|
||||
const targetPort = connection.targetHandle ?? "input";
|
||||
const sourceDefinition = definitions.get(source.type);
|
||||
const targetDefinition = definitions.get(target.type);
|
||||
if (
|
||||
!sourceDefinition?.output_ports.some((port) => port.id === sourcePort)
|
||||
|| !targetDefinition?.input_ports.some((port) => port.id === targetPort)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const duplicate = graph.edges.some(
|
||||
(edge) =>
|
||||
edge.source === connection.source
|
||||
&& edge.target === connection.target
|
||||
&& (edge.source_port ?? "output") === sourcePort
|
||||
&& (edge.target_port ?? "input") === targetPort
|
||||
);
|
||||
return !targetAlreadyConnected;
|
||||
if (duplicate) return false;
|
||||
const port = targetDefinition.input_ports.find((item) => item.id === targetPort);
|
||||
const targetPortConnections = graph.edges.filter(
|
||||
(edge) => edge.target === connection.target && (edge.target_port ?? "input") === targetPort
|
||||
);
|
||||
if (!port?.multiple && targetPortConnections.length) return false;
|
||||
return !wouldCreateCycle(graph, connection.source, connection.target);
|
||||
};
|
||||
|
||||
const onConnect = (connection: Connection) => {
|
||||
@@ -138,7 +173,7 @@ export default function DataflowCanvas({
|
||||
const type = event.dataTransfer.getData("application/x-govoplan-dataflow-node");
|
||||
if (!type) return;
|
||||
const position = instance.screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
||||
const node = newNode(type, position);
|
||||
const node = newNode(type, position, nodeLibrary);
|
||||
onGraphChange({ ...graph, nodes: [...graph.nodes, node] });
|
||||
onSelectNode(node.id);
|
||||
};
|
||||
@@ -199,6 +234,23 @@ export default function DataflowCanvas({
|
||||
);
|
||||
}
|
||||
|
||||
function wouldCreateCycle(graph: PipelineGraph, source: string, target: string): boolean {
|
||||
const outgoing = new Map<string, string[]>();
|
||||
graph.edges.forEach((edge) => {
|
||||
outgoing.set(edge.source, [...(outgoing.get(edge.source) ?? []), edge.target]);
|
||||
});
|
||||
const pending = [target];
|
||||
const visited = new Set<string>();
|
||||
while (pending.length) {
|
||||
const nodeId = pending.pop();
|
||||
if (!nodeId || visited.has(nodeId)) continue;
|
||||
if (nodeId === source) return true;
|
||||
visited.add(nodeId);
|
||||
pending.push(...(outgoing.get(nodeId) ?? []));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function updateGraphNode(
|
||||
graph: PipelineGraph,
|
||||
updatedNode: PipelineGraphNode
|
||||
|
||||
Reference in New Issue
Block a user