diff --git a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js
index 31aa941784..81242cd3f0 100644
--- a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js
+++ b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js
@@ -2304,7 +2304,6 @@ export default class SVGCanvasRenderer {
this.removeContextToolbar();
}
-
// Note: Comment and Node resizing is started by the comment/node highlight rectangle.
if (this.commentSizing) {
this.initializeResizeVariables(d);
@@ -2819,7 +2818,14 @@ export default class SVGCanvasRenderer {
.attr("height", (d) => d.height)
.attr("x", 0)
.attr("y", 0)
- .each(addNodeExternalObject.bind(this));
+ .each(addNodeExternalObject.bind(
+ this,
+ this.canvasController,
+ this.activePipeline.nodes,
+ this.setPortPositions.bind(this),
+ this.setNodesProperties.bind(this),
+ this.raiseNodeToTopById.bind(this)
+ ));
// Node Image
nonBindingNodeGrps
@@ -2918,6 +2924,54 @@ export default class SVGCanvasRenderer {
removeSel.remove();
}
+ setPortPositions(info) {
+ const node = this.activePipeline.getNode(info.nodeId);
+
+ if (info.inputPositions) {
+ info.inputPositions.forEach((inputPos) => {
+ const inp = node.inputs.find((input) => input.id === inputPos.id);
+
+ // TODO - Decide if zoomTransform should be passed to react object or not.
+ inp.cx = inputPos.cx / this.zoomTransform.k;
+ inp.cy = inputPos.cy / this.zoomTransform.k;
+ });
+ }
+ if (info.outputPositions) {
+ info.outputPositions.forEach((outputPos) => {
+ const out = node.outputs.find((output) => output.id === outputPos.id);
+ out.cx = outputPos.cx / this.zoomTransform.k;
+ out.cy = outputPos.cy / this.zoomTransform.k;
+ });
+ }
+ this.displayLinks();
+ }
+
+ setNodesProperties(newProps) {
+ if (newProps) {
+ newProps.forEach((np) => {
+ const node = this.activePipeline.getNode(np.id);
+ if (np.height) {
+ node.height = np.height;
+ }
+ if (np.width) {
+ node.width = np.width;
+ }
+ if (np.x_pos) {
+ node.x_pos = np.x_pos;
+ }
+ if (np.y_pos) {
+ node.y_pos = np.y_pos;
+ }
+ });
+
+ this.displayNodes();
+ }
+ }
+
+ raiseNodeToTopById(nodeId) {
+ this.getNodeGroupSelectionById(nodeId).raise();
+ }
+
// Handles the display of a supernode sub-flow contents or hides the contents
// as necessary.
displaySupernodeContents(d, supernodeD3Object) {
@@ -6238,6 +6292,7 @@ export default class SVGCanvasRenderer {
// Creates all newly created links specified in the enter selection.
createLinks(enter) {
+ this.logger.logStartTimer("createLinks");
// Add groups for links
const newLinkGrps = enter.append("g")
.attr("data-id", (d) => this.getId("link_grp", d.id))
@@ -6281,6 +6336,8 @@ export default class SVGCanvasRenderer {
});
}
+ this.logger.logEndTimer("createLinks");
+
return newLinkGrps;
}
@@ -6288,6 +6345,7 @@ export default class SVGCanvasRenderer {
// selection object. The selection object will contain newly created links
// as well as existing links.
updateLinks(joinedLinkGrps, lineArray) {
+ this.logger.logStartTimer("updateLinks");
// Update link selection area
joinedLinkGrps
.selectAll(".d3-link-selection-area")
@@ -6336,6 +6394,8 @@ export default class SVGCanvasRenderer {
if (!this.dragging) {
this.setDisplayOrder(joinedLinkGrps);
}
+
+ this.logger.logEndTimer("updateLinks");
}
attachLinkGroupListeners(linkGrps) {
@@ -6672,6 +6732,8 @@ export default class SVGCanvasRenderer {
}
buildLinksArray() {
+ this.logger.logStartTimer("buildLinksArray");
+
let linksArray = [];
if (this.canvasLayout.linkType === LINK_TYPE_STRAIGHT) {
@@ -6703,6 +6765,8 @@ export default class SVGCanvasRenderer {
// Add connection path info to the links.
linksArray = this.linkUtils.addConnectionPaths(linksArray);
+ this.logger.logEndTimer("buildLinksArray");
+
return linksArray;
}
diff --git a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-external.js b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-external.js
index b49f99f16f..34987e4d22 100644
--- a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-external.js
+++ b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-external.js
@@ -16,10 +16,15 @@
import React from "react";
import ReactDOM from "react-dom";
-export const addNodeExternalObject = (node, i, foreignObjects) => {
+export const addNodeExternalObject = (canvasController, nodes, setPortPositions, setNodesProperties, raiseNodeToTopById, node, i, foreignObjects) => {
ReactDOM.render(
,
foreignObjects[i]
);
diff --git a/canvas_modules/harness/assets/images/vector.svg b/canvas_modules/harness/assets/images/vector.svg
new file mode 100644
index 0000000000..55c7586059
--- /dev/null
+++ b/canvas_modules/harness/assets/images/vector.svg
@@ -0,0 +1,3 @@
+
diff --git a/canvas_modules/harness/src/client/App.js b/canvas_modules/harness/src/client/App.js
index 1848c733aa..2891194486 100644
--- a/canvas_modules/harness/src/client/App.js
+++ b/canvas_modules/harness/src/client/App.js
@@ -51,6 +51,7 @@ import ExplainCanvas from "./components/custom-canvases/explain/explain-canvas";
import Explain2Canvas from "./components/custom-canvases/explain2/explain2-canvas";
import StreamsCanvas from "./components/custom-canvases/streams/streams-canvas";
import ReactNodesCarbonCanvas from "./components/custom-canvases/react-nodes-carbon/react-nodes-carbon";
+import ReactNodesMappingCanvas from "./components/custom-canvases/react-nodes-mapping/react-nodes-mapping";
import Breadcrumbs from "./components/breadcrumbs.jsx";
import Console from "./components/console/console.jsx";
@@ -109,6 +110,7 @@ import {
EXAMPLE_APP_READ_ONLY,
EXAMPLE_APP_PROGRESS,
EXAMPLE_APP_REACT_NODES_CARBON,
+ EXAMPLE_APP_REACT_NODES_MAPPING,
CUSTOM,
PALETTE_FLYOUT,
PROPERTIES_FLYOUT,
@@ -224,7 +226,7 @@ class App extends React.Component {
selectedExternalPipelineFlows: true,
selectedEditingActions: true,
selectedMoveNodesOnSupernodeResize: true,
- selectedRaiseNodesToTopOnHover: false,
+ selectedRaiseNodesToTopOnHover: true,
selectedResizableNodes: false,
selectedDisplayFullLabelOnHover: false,
selectedPositionNodeOnRightFlyoutOpen: false,
@@ -2577,6 +2579,13 @@ class App extends React.Component {
config={commonCanvasConfig}
/>
);
+ } else if (this.state.selectedExampleApp === EXAMPLE_APP_REACT_NODES_MAPPING) {
+ firstCanvas = (
+
+ );
}
let commonCanvas;
diff --git a/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/linkInputToOutputAction.js b/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/linkInputToOutputAction.js
new file mode 100644
index 0000000000..5ea4a0dcdf
--- /dev/null
+++ b/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/linkInputToOutputAction.js
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2023 Elyra Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// import Action from "../command-stack/action.js";
+import { v4 as uuid4 } from "uuid";
+import { get } from "lodash";
+
+export default class LinkInputToOutputAction {
+ constructor(srcNodeId, srcFields, trgNodeId, canvasController) {
+ // super(data);
+ this.srcNodeId = srcNodeId;
+ this.srcFields = srcFields;
+ this.trgNodeId = trgNodeId;
+ this.canvasController = canvasController;
+ this.oldTrgNode = canvasController.getNode(trgNodeId);
+
+ this.oldTrgInputs = [...this.oldTrgNode.inputs];
+ this.oldLinks = [...this.canvasController.getLinks()];
+ this.oldTrgAppData = this.oldTrgNode.app_data;
+ this.oldTrgFields = get(this, "oldTrgNode.app_data.table_data.fields", []);
+
+ this.newTrgFields = [...this.oldTrgFields];
+ this.newTrgInputs = [...this.oldTrgInputs];
+ const startingLinks = this.oldLinks.filter((l) => l.srcNodeId === srcNodeId && l.trgNodeId === trgNodeId);
+ this.newLinks = [...startingLinks];
+
+ srcFields.forEach((sf) => {
+ let trgField = this.getMatchingTrgField(sf);
+ if (!trgField) {
+ const trgId = uuid4();
+ trgField = { id: trgId, label: sf.label, type: sf.type };
+ this.newTrgFields.push(trgField);
+ this.newTrgInputs.push({ id: trgId });
+ }
+
+ // Create what would be the link to the new, or existing, target field
+ const link = {
+ id: uuid4(),
+ srcNodeId: srcNodeId,
+ srcNodePortId: sf.id,
+ trgNodeId: trgNodeId,
+ trgNodePortId: trgField.id,
+ type: "nodeLink"
+ };
+
+ // If the link doesn't exist, add it to those to be added to the camvas.
+ if (!this.linkExists(link)) {
+ this.newLinks.push(link);
+ }
+ });
+
+ this.newTrgAppData = {
+ table_data: {
+ fields: this.newTrgFields
+ }
+ };
+ }
+
+ getMatchingTrgField(sourceField) {
+ return this.oldTrgFields.find((tf) => tf.label === sourceField.label);
+ }
+
+ linkExists(link) {
+ const result = this.oldLinks.find((l) =>
+ l.srcNodeId === link.srcNodeId &&
+ l.srcNodePortId === link.srcNodePortId &&
+ l.trgNodeId === link.trgNodeId &&
+ l.trgNodePortId === link.trgNodePortId);
+ return result;
+ }
+
+ // Returns true if there is something to execute. No need to check the
+ // inputs array because it should be in-sync with the fields array.
+ isDoable() {
+ return this.newTrgFields.length > this.oldTrgFields.length || this.newLinks.length > this.oldLinks.length;
+ }
+
+ // Standard methods
+ do() {
+ this.canvasController.setNodeProperties(this.trgNodeId, { inputs: this.newTrgInputs, app_data: this.newTrgAppData });
+ this.canvasController.addLinks(this.newLinks);
+ }
+
+ undo() {
+ this.canvasController.setNodeProperties(this.trgNodeId, { inputs: this.oldTrgInputs, app_data: this.oldTrgAppData });
+ this.canvasController.setLinks(this.oldLinks);
+ }
+
+ redo() {
+ this.do();
+ }
+
+ getLabel() {
+ return "Create " + this.newLinks.length + " mappings";
+ }
+}
diff --git a/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/mapping-container-node.jsx b/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/mapping-container-node.jsx
new file mode 100644
index 0000000000..746c50402f
--- /dev/null
+++ b/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/mapping-container-node.jsx
@@ -0,0 +1,688 @@
+/*
+* Copyright 2023 Elyra Authors
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+import React from "react";
+import PropTypes from "prop-types";
+
+import { get } from "lodash";
+import { ChevronUp16, ChevronDown16, Draggable16, DragVertical16 } from "@carbon/icons-react";
+
+import LinkInputToOutputAction from "./linkInputToOutputAction.js";
+
+// The top-most y coordinate for the containers
+const TOP_Y = 180;
+
+// The gap between the containers
+const CONTAINER_GAP = 48;
+
+// Defaut node height and width - must be the same as values in enableNodeLayout
+const DEFAUT_NODE_WIDTH = 400;
+const DEFAUT_NODE_HEIGHT = 30;
+
+
+class MappingContainerNode extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onScroll = this.onScroll.bind(this);
+ this.onMouseEnterOnContainer = this.onMouseEnterOnContainer.bind(this);
+ this.onMouseDownOnContainer = this.onMouseDownOnContainer.bind(this);
+ this.onMouseDownOnHeaderChevron = this.onMouseDownOnHeaderChevron.bind(this);
+ this.onMouseDownOnResizeIcon = this.onMouseDownOnResizeIcon.bind(this);
+ this.onMouseDownOnDragContainerIcon = this.onMouseDownOnDragContainerIcon.bind(this);
+ this.onMouseMoveOnDragContainerIcon = this.onMouseMoveOnDragContainerIcon.bind(this);
+ this.onMouseUpOnDragContainerIcon = this.onMouseUpOnDragContainerIcon.bind(this);
+ this.adjustContainerPositions = this.adjustContainerPositions.bind(this);
+ this.onInputFieldDragStart = this.onInputFieldDragStart.bind(this);
+ this.onFieldMoveDragStart = this.onFieldMoveDragStart.bind(this);
+ this.onFieldDrop = this.onFieldDrop.bind(this);
+ this.onDragStartOnContainerDataIcon = this.onDragStartOnContainerDataIcon.bind(this);
+ this.setPortPositions = this.setPortPositions.bind(this);
+ this.getFieldElementId = this.getFieldElementId.bind(this);
+ this.resizeNode = this.resizeNode.bind(this);
+ this.resizeNodeEnd = this.resizeNodeEnd.bind(this);
+ }
+
+ componentDidMount() {
+ window.console.log("TableNode - componentDidMount");
+ setTimeout(this.setPortPositions, 500);
+ }
+
+ componentDidUpdate() {
+ window.console.log("TableNode - componentDidUpdate");
+ this.setPortPositions();
+ }
+
+ onMouseDownOnHeaderChevron(evt) {
+ window.console.log("TableNode - onMouseDownOnHeaderChevron");
+
+ let newNodesProps = null;
+
+ if (this.isContainerResized()) {
+ const xInc = DEFAUT_NODE_WIDTH - this.props.nodeData.width;
+ const yInc = DEFAUT_NODE_HEIGHT - this.props.nodeData.height;
+ newNodesProps = this.adjustContainerPositions(xInc, yInc);
+
+ } else {
+ const xInc = this.savedResizeWidth - DEFAUT_NODE_WIDTH;
+ const yInc = this.savedResizeHeight - DEFAUT_NODE_HEIGHT;
+ newNodesProps = this.adjustContainerPositions(xInc, yInc);
+ }
+
+ const nodeSizingObjectsInfo = {};
+
+ this.props.nodes.forEach((n) => {
+ const newNodeProp = newNodesProps.find((np) => np.id === n.id);
+ if (newNodeProp) {
+ const nodeSizingObj = {
+ id: n.id,
+ height: newNodeProp.height ? newNodeProp.height : n.height,
+ width: newNodeProp.width ? newNodeProp.width : n.width,
+ x_pos: newNodeProp.x_pos ? newNodeProp.x_pos : n.x_pos,
+ y_pos: newNodeProp.y_pos ? newNodeProp.y_pos : n.y_pos
+ };
+
+ if (n.id === this.props.nodeData.id) {
+ if (this.isContainerResized()) {
+ nodeSizingObj.isResized = false;
+ this.savedResizeWidth = n.width;
+ this.savedResizeHeight = n.height;
+ } else {
+ nodeSizingObj.isResized = true;
+ nodeSizingObj.resizeWidth = this.savedResizeWidth;
+ nodeSizingObj.resizeHeight = this.savedResizeHeight;
+ }
+ } else {
+ nodeSizingObj.isResized = n.isResized;
+ nodeSizingObj.resizeWidth = n.resizeWidth;
+ nodeSizingObj.resizeHeight = n.resizeHeight;
+ }
+
+ nodeSizingObjectsInfo[n.id] = nodeSizingObj;
+ }
+ });
+
+
+ this.saveNodePositions(nodeSizingObjectsInfo);
+ }
+
+ onScroll(evt) {
+ window.console.log("onScroll");
+ // Must stop propogation of scroll gesture to the zoom behavior of
+ // common-canvas otherwise scroll doesn't work.
+ evt.stopPropagation();
+
+ this.setPortPositions();
+ }
+
+ onDragStartOnContainerDataIcon(evt) {
+ window.console.log("onDragStartOnContainerDataIcon");
+ evt.stopPropagation();
+
+ evt.dataTransfer.clearData();
+ const data = JSON.stringify({
+ srcNodeId: this.props.nodeData.id,
+ srcFields: this.props.nodeData.app_data.table_data.fields
+ });
+ evt.dataTransfer.setData("text/plain", data);
+ }
+
+ // Called when the field is moved up and down in an output link.
+ onFieldMoveDragStart(evt, col) {
+ //
+ }
+
+ onInputFieldDragStart(evt, field) {
+ evt.dataTransfer.clearData();
+ const data = JSON.stringify({
+ srcNodeId: this.props.nodeData.id,
+ srcFields: [field]
+ });
+ evt.dataTransfer.setData("text/plain", data);
+ }
+
+ onFieldDrop(evt) {
+ evt.stopPropagation();
+ evt.preventDefault();
+ if (this.props.nodeData.op !== "input_link") {
+ const data = evt.dataTransfer.getData("text/plain");
+ if (data) {
+ const srcInfo = JSON.parse(data);
+
+ // Add fields, create ports and add links
+ const command = new LinkInputToOutputAction(
+ srcInfo.srcNodeId, srcInfo.srcFields,
+ this.props.nodeData.id, this.props.canvasController);
+ if (command.isDoable()) {
+ const commandStack = this.props.canvasController.getCommandStack();
+ commandStack.do(command);
+ }
+ }
+ }
+ }
+
+ onMouseEnterOnContainer() {
+ // console.log("onMouseEnterOnContainer");
+
+ this.props.canvasController.closeContextMenu();
+ }
+
+ // Stop propagation will prevent the node/container from being dragged to a
+ // new position.
+ onMouseDownOnContainer(evt) {
+ evt.stopPropagation();
+ }
+
+ // We need to stop propagation so the mouse down event does not go through
+ // to the canvas node.
+ onMouseDownOnDragContainerIcon(evt) {
+ window.console.log("onMouseDownOnDragContainerIcon");
+ evt.stopPropagation();
+
+ this.props.canvasController.closeContextMenu();
+
+ this.rightNodes = this.onSortRightNodes();
+ this.targetAreas = this.onCreateTargetAreas();
+
+ document.addEventListener("mousemove", this.onMouseMoveOnDragContainerIcon, true);
+ document.addEventListener("mouseup", this.onMouseUpOnDragContainerIcon, true);
+ }
+
+ onMouseMoveOnDragContainerIcon(evt) {
+ window.console.log("onMouseMoveOnDragContainerIcon");
+ evt.stopPropagation();
+
+ if (evt.movementY === 0) {
+ return;
+ }
+
+ const newYPos = Math.max(this.props.nodeData.y_pos + evt.movementY, TOP_Y);
+
+ let newNodesProps = [];
+ newNodesProps.push({ id: this.props.nodeData.id, y_pos: newYPos });
+
+ // Mouse pointer is moving down
+ if (evt.movementY > 0) {
+ const targetAreaUnderYPos = this.targetAreas.find(
+ (sp) => sp.node.id !== this.props.nodeData.id && newYPos + this.props.nodeData.height > sp.top && newYPos < sp.top);
+ if (targetAreaUnderYPos) {
+ this.props.nodeData.y_pos = targetAreaUnderYPos.top;
+ targetAreaUnderYPos.node.y_pos = this.targetAreas[targetAreaUnderYPos.pos - 1].top;
+
+ this.rightNodes = this.onSortRightNodes();
+ newNodesProps = this.onSetNodePositions(newYPos);
+ this.targetAreas = this.onCreateTargetAreas();
+ }
+
+ // Mouse pointer is moving up
+ } else if (evt.movementY < 0) {
+ const targetAreaUnderYPos = this.targetAreas.find(
+ (sp) => sp.node.id !== this.props.nodeData.id && newYPos < sp.bottom && newYPos + this.props.nodeData.height > sp.bottom);
+ if (targetAreaUnderYPos) {
+ this.props.nodeData.y_pos = targetAreaUnderYPos.top;
+ targetAreaUnderYPos.node.y_pos = this.targetAreas[targetAreaUnderYPos.pos + 1].top;
+
+ this.rightNodes = this.onSortRightNodes();
+ newNodesProps = this.onSetNodePositions(newYPos);
+ this.targetAreas = this.onCreateTargetAreas();
+ }
+ }
+
+ this.props.setNodesProperties(newNodesProps);
+ this.props.raiseNodeToTopById(this.props.nodeData.id);
+ }
+
+ onSortRightNodes() {
+ return this.getRightNodes().sort((a, b) => a.y_pos - b.y_pos);
+ }
+
+ onCreateTargetAreas() {
+ return this.rightNodes.map((rn, i) => ({
+ pos: i,
+ node: rn,
+ top: rn.y_pos + (rn.height * 0.2),
+ bottom: rn.y_pos + (rn.height * 0.8)
+ }));
+ }
+
+ onSetNodePositions(ourPos) {
+ let yPos = TOP_Y; // Start postion
+
+ const newNodesProps = [];
+ this.rightNodes.forEach((rn) => {
+ if (rn.id !== this.props.nodeData.id) {
+ newNodesProps.push({ id: rn.id, y_pos: yPos });
+ } else {
+ newNodesProps.push({ id: rn.id, y_pos: ourPos });
+ }
+ yPos += rn.height + CONTAINER_GAP; // Height plus gap
+ });
+ return newNodesProps;
+ }
+
+ onMouseUpOnDragContainerIcon(evt) {
+ evt.stopPropagation();
+
+ if (this.rightNodes) {
+ const nodeSizingObjectsInfo = {};
+
+ let yPos = TOP_Y;
+ this.rightNodes.forEach((n) => {
+ const nodeSizingObj = {
+ id: n.id,
+ height: n.height,
+ width: n.width,
+ x_pos: n.x_pos,
+ y_pos: yPos
+ };
+ if (n.isResized) {
+ nodeSizingObj.isResized = true;
+ nodeSizingObj.resizeWidth = n.resizeWidth;
+ nodeSizingObj.resizeHeight = n.resizeHeight;
+ }
+ yPos += n.height + CONTAINER_GAP;
+
+ nodeSizingObjectsInfo[n.id] = nodeSizingObj;
+ });
+
+ this.saveNodePositions(nodeSizingObjectsInfo);
+ }
+
+ document.removeEventListener("mousemove", this.onMouseMoveOnDragContainerIcon, true);
+ document.removeEventListener("mouseup", this.onMouseUpOnDragContainerIcon, true);
+ }
+
+ onMouseDownOnResizeIcon(evt) {
+ window.console.log("onMouseDownOnResizeIcon");
+ evt.stopPropagation();
+
+ this.props.canvasController.closeContextMenu();
+
+ document.addEventListener("mousemove", this.resizeNode, true);
+ document.addEventListener("mouseup", this.resizeNodeEnd, true);
+ }
+
+ setPortPositions() {
+ const nodeDivRect = this.getNodeDivRect();
+ const headerDivRect = this.getHeaderDivRect();
+ const scrollDivRect = this.isContainerResized() ? this.getScrollDivRect() : {};
+
+ const inputPositions = !this.props.nodeData.inputs || this.props.nodeData.inputs.length === 0
+ ? []
+ : this.props.nodeData.inputs.map((port) =>
+ ({
+ id: port.id,
+ cx: 0,
+ cy: this.isContainerResized() ? this.getFieldElementPortPosY(port.id, nodeDivRect, scrollDivRect) : (headerDivRect.height / 2)
+ }));
+
+ const outputPositions = !this.props.nodeData.outputs || this.props.nodeData.outputs.length === 0
+ ? []
+ : this.props.nodeData.outputs.map((port) =>
+ ({
+ id: port.id,
+ cx: nodeDivRect.width,
+ cy: this.isContainerResized() ? this.getFieldElementPortPosY(port.id, nodeDivRect, scrollDivRect) : (headerDivRect.height / 2)
+ }));
+
+ this.props.setPortPositions({
+ nodeId: this.props.nodeData.id,
+ inputPositions,
+ outputPositions
+ });
+ }
+
+ getNodeDivRect() {
+ const nodeDivId = this.getNodeDivId();
+ const nodeDiv = document.getElementById(nodeDivId);
+ const nodeDivRect = nodeDiv.getBoundingClientRect();
+ return nodeDivRect;
+ }
+
+ getHeaderDivRect() {
+ const headerDivId = this.getHeaderDivId();
+ const headerDiv = document.getElementById(headerDivId);
+ const headerDivRect = headerDiv.getBoundingClientRect();
+ return headerDivRect;
+ }
+
+ getScrollDivRect() {
+ const scrollDivId = this.getScrollDivId();
+ const scrollDiv = document.getElementById(scrollDivId);
+ const scrollDivRect = scrollDiv.getBoundingClientRect();
+ return scrollDivRect;
+ }
+
+ getFieldElementPortPosY(portId, nodeDivRect, scrollDivRect) {
+ const colElementId = this.getFieldElementId(portId);
+ const colElement = document.getElementById(colElementId);
+ const colElementRect = colElement.getBoundingClientRect();
+ let colElemetCenterY = colElementRect.top - nodeDivRect.top + (colElementRect.height / 2);
+ const headerBottom = scrollDivRect.top - nodeDivRect.top;
+ const footerTop = scrollDivRect.bottom - nodeDivRect.top;
+ const footerHeight = nodeDivRect.height - footerTop;
+
+ if (colElemetCenterY < headerBottom) {
+ colElemetCenterY = headerBottom / 2;
+
+ } else if (colElemetCenterY > footerTop) {
+ colElemetCenterY = footerTop + (footerHeight / 2);
+ }
+ return colElemetCenterY;
+ }
+
+ getNodeDivId() {
+ return "node_div_" + this.props.nodeData.id;
+ }
+
+ getHeaderDivId() {
+ return "header_div_" + this.props.nodeData.id;
+ }
+
+ getScrollDivId() {
+ return "scroll_div_" + this.props.nodeData.id;
+ }
+
+ getFieldElementId(colId) {
+ return this.props.nodeData.id + "--" + colId;
+ }
+
+ getLeftNodes() {
+ return this.props.nodes.filter((n) => n.op !== "output_link");
+ }
+
+ getRightNodes() {
+ return this.props.nodes.filter((n) => n.op === "output_link");
+ }
+
+ getContainerLabel() {
+ let containerType = "";
+ if (this.props.nodeData.op === "input_link") {
+ containerType = "Input:";
+ } else if (this.props.nodeData.op === "output_link") {
+ containerType = "Output:";
+ }
+ return
for 'stage variables' and 'loop condition' allows
+ // the grid-template-columns setting for for the header to work correctly.
+ return (
);
+ }
+
+ generateHeader() {
+ const topLeftIcon = this.generateTopLeftIcon();
+ const chevronIcon = this.getChevronIcon();
+ const containerLabel = this.getContainerLabel();
+ const fieldCount = this.getFieldCount();
+ return (
+
+ );
+ }
+
+ generateScrollDiv() {
+ const fields = get(this.props.nodeData, "app_data.table_data.fields", []);
+ const content = fields.length > 0
+ ? this.generateFields()
+ : (
No columns
);
+
+ return (
+
+ {content}
+
+ );
+ }
+
+ generateFields() {
+ const cols = this.props.nodeData.app_data.table_data.fields.map((field, index) => {
+ let beforeLabel = null;
+ let mapping = null;
+ let className = "scrollable-row";
+
+ if (this.props.nodeData.op === "output_link") {
+ beforeLabel = (
+
+
+
+ );
+ mapping = (
+
{this.getMapping(field)}
+ );
+ className += " with-mapping";
+
+ // Input link
+ } else {
+ beforeLabel = (
+
this.onInputFieldDragStart(evt, field)}>
+
+
+ );
+ }
+
+ return (
+
this.onFieldMoveDragStart(evt, field)}
+ >
+ {beforeLabel}
+
{index + 1}
+
{field.label}
+ {mapping}
+
{field.type}
+
{" "}
+
+ );
+ });
+ return cols;
+ }
+
+ generateFooter() {
+ return (
+
+
{"View 60 more"}
+
+
+
+
+ );
+ }
+
+ render() {
+ // window.console.log("render " + this.props.nodeData.label);
+
+ const header = this.generateHeader();
+ const scrollDiv = this.isContainerResized() ? this.generateScrollDiv() : null;
+ const footer = this.isContainerResized() ? this.generateFooter() : null;
+
+ return (
+
+ {header}
+ {scrollDiv}
+ {footer}
+
+ );
+ }
+}
+
+MappingContainerNode.propTypes = {
+ nodeData: PropTypes.object,
+ nodes: PropTypes.array,
+ setPortPositions: PropTypes.func,
+ setNodesProperties: PropTypes.func,
+ raiseNodeToTopById: PropTypes.func,
+ canvasController: PropTypes.object
+};
+
+export default MappingContainerNode;
diff --git a/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/mapping.json b/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/mapping.json
new file mode 100644
index 0000000000..85a85dbc47
--- /dev/null
+++ b/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/mapping.json
@@ -0,0 +1,97 @@
+{
+ "input_link": {
+ "id": "11111",
+ "label": "Link_1",
+ "x_pos": 110,
+ "y_pos": 180,
+ "width": 320,
+ "height": 200,
+ "minimized": false,
+ "fields": [
+ { "id": "111", "name": "Transaction Status" },
+ { "id": "222", "name": "Quantity" },
+ { "id": "333", "name": "other_OrderEvent" },
+ { "id": "444", "name": "ActualDateTime" },
+ { "id": "555", "name": "BusinessDayDate" },
+ { "id": "666", "name": "SpecialOrderNum" },
+ { "id": "777", "name": "other" },
+ { "id": "888", "name": "TransactionId" },
+ { "id": "999", "name": "TransactionDate" },
+ { "id": "000", "name": "Status" }
+ ]
+ },
+ "stage_variables": {
+ "id": "22222",
+ "label": "Stage Variables",
+ "x_pos": 110,
+ "y_pos": 400,
+ "width": 320,
+ "height": 200,
+ "minimized": false,
+ "fields": [
+ { "id": "111", "name": "svOrderStatus" },
+ { "id": "222", "name": "svOrderEvent" },
+ { "id": "333", "name": "svItemType" },
+ { "id": "444", "name": "svMandateConfederation" },
+ { "id": "555", "name": "svUpc" },
+ { "id": "666", "name": "svOrderNull" },
+ { "id": "777", "name": "NEWSTAGEVAR" },
+ { "id": "888", "name": "NEWSTAGETOP" },
+ { "id": "999", "name": "NEWSTAGECONST" },
+ { "id": "000", "name": "pmAssociate" },
+ { "id": "123", "name": "pmAgreed" },
+ { "id": "456", "name": "fnOut" },
+ { "id": "789", "name": "fnIngress" },
+ { "id": "012", "name": "pxTopical" },
+ { "id": "345", "name": "pxReduction" }
+ ]
+ },
+ "loop_condition": {
+ "id": "33333",
+ "label": "Loop condition",
+ "x_pos": 110,
+ "y_pos": 640,
+ "width": 320,
+ "height": 200,
+ "minimized": false,
+ "fields": [
+ ]
+ },
+ "output_links": [
+ {
+ "id": "44444",
+ "label": "Link_2",
+ "x_pos": 540,
+ "y_pos": 180,
+ "width": 320,
+ "height": 200,
+ "minimized": false,
+ "fields": [
+ { "id": "111", "name": "Transaction Status" },
+ { "id": "222", "name": "Quantity" },
+ { "id": "333", "name": "other_OrderEvent" },
+ { "id": "444", "name": "ActualDateTime" },
+ { "id": "555", "name": "BusinessDayDate" },
+ { "id": "666", "name": "SpecialOrderNum" },
+ { "id": "777", "name": "other" },
+ { "id": "888", "name": "TransactionId" },
+ { "id": "999", "name": "TransactionDate" },
+ { "id": "000", "name": "Status" }
+ ]
+ },
+ {
+ "id": "55555",
+ "label": "Link_3",
+ "x_pos": 540,
+ "y_pos": 400,
+ "width": 320,
+ "height": 200,
+ "minimized": false,
+ "fields": [
+ ]
+ }
+ ],
+ "mappings": [
+ { "srcContainerId": "11111", "srcFieldId": "777", "trgContainerId": "44444", "trgFieldId": "888" }
+ ]
+}
diff --git a/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/react-nodes-mapping-flow.json b/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/react-nodes-mapping-flow.json
new file mode 100644
index 0000000000..84c51c0369
--- /dev/null
+++ b/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/react-nodes-mapping-flow.json
@@ -0,0 +1,252 @@
+{
+ "doc_type": "pipeline",
+ "version": "3.0",
+ "json_schema": "https://api.dataplatform.ibm.com/schemas/common-pipeline/pipeline-flow/pipeline-flow-v3-schema.json",
+ "id": "ac3d3e04-c3d2-4da7-ab5a-2b9573e5e159",
+ "primary_pipeline": "3ae0efae-9a3c-4a1a-9fd9-185f442a81aa",
+ "pipelines": [
+ {
+ "id": "3ae0efae-9a3c-4a1a-9fd9-185f442a81aa",
+ "nodes": [
+ {
+ "id": "11111",
+ "type": "execution_node",
+ "op": "input_link",
+ "app_data": {
+ "ui_data": {
+ "label": "Link_1",
+ "image": "",
+ "x_pos": 40,
+ "y_pos": 180,
+ "description": "Input link columns for this transformer",
+ "is_resized": true,
+ "resize_width": 400,
+ "resize_height": 200
+ },
+ "table_data": {
+ "fields": [
+ { "id": "111", "label": "TransactionId", "type": "VARCHAR(1024), NULL, Key" },
+ { "id": "222", "label": "Quantity", "type": "INTEGER, 0" },
+ { "id": "333", "label": "other_OrderEvent", "type": "VARCHAR(1024), NULL" },
+ { "id": "444", "label": "ActualDateTime", "type": "TIMESTAMP, NULL" },
+ { "id": "555", "label": "BusinessDayDate", "type": "VARCHAR(1024), NULL" },
+ { "id": "666", "label": "SpecialOrderNum", "type": "VARCHAR(1024), NULL" },
+ { "id": "777", "label": "other", "type": "VARCHAR(1024), NULL" },
+ { "id": "888", "label": "TransactionStatus", "type": "VARCHAR(1024), NULL" },
+ { "id": "999", "label": "TransactionDate", "type": "TIMESTAMP, NULL" },
+ { "id": "000", "label": "Status", "type": "VARCHAR(1024), NULL" }
+ ]
+ }
+ },
+ "inputs": [
+ ],
+ "outputs": [
+ { "id": "111" },
+ { "id": "222" },
+ { "id": "333"},
+ { "id": "444" },
+ { "id": "555" },
+ { "id": "666" },
+ { "id": "777" },
+ { "id": "888" },
+ { "id": "999" },
+ { "id": "000" }
+ ]
+ },
+ {
+ "id": "22222",
+ "type": "execution_node",
+ "op": "stage_variables",
+ "app_data": {
+ "ui_data": {
+ "label": "Stage Variables",
+ "image": "",
+ "x_pos": 40,
+ "y_pos": 428,
+ "description": "Stage varibales for this transformer.",
+ "is_resized": true,
+ "resize_width": 400,
+ "resize_height": 200
+ },
+ "table_data": {
+ "fields": [
+ { "id": "111", "label": "svOrderStatus", "type": "VARCHAR(1024), NULL, Key" },
+ { "id": "222", "label": "svOrderEvent", "type": "VARCHAR(1024), NULL, Key" },
+ { "id": "333", "label": "svItemType", "type": "VARCHAR(1024), NULL, Key" },
+ { "id": "444", "label": "svMandateCo...", "type": "VARCHAR(1024), NULL, Key" },
+ { "id": "555", "label": "svUpc", "type": "VARCHAR(1024), NULL, Key" },
+ { "id": "666", "label": "svOrderNull", "type": "VARCHAR(1024), NULL, Key" },
+ { "id": "777", "label": "NEWSTAGEVAR", "type": "VARCHAR(1024), NULL, Key" },
+ { "id": "888", "label": "NEWSTAGETOP", "type": "VARCHAR(1024), NULL, Key" },
+ { "id": "999", "label": "NEWSTAGECONST", "type": "VARCHAR(1024), NULL, Key" },
+ { "id": "000", "label": "pmAssociate", "type": "VARCHAR(1024), NULL, Key" },
+ { "id": "123", "label": "pmAgreed", "type": "VARCHAR(1024), NULL, Key" },
+ { "id": "456", "label": "fnOut", "type": "VARCHAR(1024), NULL, Key" },
+ { "id": "789", "label": "fnIngress", "type": "VARCHAR(1024), NULL, Key" },
+ { "id": "012", "label": "pxTopical", "type": "VARCHAR(1024), NULL, Key" },
+ { "id": "345", "label": "pxReduction", "type": "VARCHAR(1024), NULL, Key" }
+ ]
+ }
+ },
+ "inputs": [
+ { "id": "111" },
+ { "id": "222" },
+ {
+ "id": "333",
+ "links": [
+ {
+ "id": "504aa60b-7422-4adb-adf4-2b6215f46a15",
+ "node_id_ref": "11111",
+ "port_id_ref": "222"
+ }
+ ]
+ },
+ { "id": "444" },
+ { "id": "555" },
+ { "id": "666" },
+ { "id": "777" },
+ { "id": "888" },
+ { "id": "999" },
+ { "id": "000" },
+ { "id": "123" },
+ { "id": "456" },
+ { "id": "789" },
+ { "id": "012" },
+ { "id": "345" }
+ ],
+ "outputs": [
+ ]
+ },
+ {
+ "id": "33333",
+ "type": "execution_node",
+ "op": "loop_condition",
+ "app_data": {
+ "ui_data": {
+ "label": "Loop condition",
+ "image": "",
+ "x_pos": 40,
+ "y_pos": 676,
+ "description": "The loop conditions for this transformer.",
+ "is_resized": true,
+ "resize_width": 400,
+ "resize_height": 200
+ },
+ "table_data": {
+ "fields": [
+ ]
+ }
+ },
+ "inputs": [
+ ],
+ "outputs": [
+ ]
+ },
+ {
+ "id": "44444",
+ "type": "execution_node",
+ "op": "output_link",
+ "app_data": {
+ "ui_data": {
+ "label": "Link_2",
+ "image": "",
+ "x_pos": 608,
+ "y_pos": 180,
+ "description": "Output link columns for this transformer.",
+ "is_resized": true,
+ "resize_width": 600,
+ "resize_height": 200
+ },
+ "table_data": {
+ "fields": [
+ { "id": "777", "label": "other", "type": "VARCHAR(1024), NULL, Key" }
+ ]
+ }
+ },
+ "inputs": [
+ {
+ "id": "777",
+ "links": [
+ {
+ "id": "178354559-8739-3adb-adf7-62897ad862a5",
+ "node_id_ref": "11111",
+ "port_id_ref": "777"
+ }
+ ]
+ }
+ ],
+ "outputs": [
+ ]
+ },
+ {
+ "id": "55555",
+ "type": "execution_node",
+ "op": "output_link",
+ "app_data": {
+ "ui_data": {
+ "label": "Link_3",
+ "image": "",
+ "x_pos": 608,
+ "y_pos": 428,
+ "description": "Output link columns for this transformer.",
+ "is_resized": true,
+ "resize_width": 600,
+ "resize_height": 200
+ },
+ "table_data": {
+ "fields": [
+ ]
+ }
+ },
+ "inputs": [
+ ],
+ "outputs": [
+ ]
+ },
+ {
+ "id": "666666",
+ "type": "execution_node",
+ "op": "output_link",
+ "app_data": {
+ "ui_data": {
+ "label": "Link_4",
+ "image": "",
+ "x_pos": 608,
+ "y_pos": 676,
+ "description": "Output link columns for this transformer.",
+ "is_resized": true,
+ "resize_width": 600,
+ "resize_height": 200
+ },
+ "table_data": {
+ "fields": [
+ ]
+ }
+ },
+ "inputs": [
+ ],
+ "outputs": [
+ ]
+ }
+ ],
+ "app_data": {
+ "ui_data": {
+ "comments": [
+ {
+ "id": "0bcaa069-7d21-43a5-ae84-cbc9680cb135",
+ "x_pos": 55,
+ "y_pos": 20,
+ "width": 400,
+ "height": 112,
+ "class_name": "bkg-col-cyan-20",
+ "content": "## React Nodes - Mapping sample app\n\nThis sample app shows nodes constructed with React objects which contain a scrollable area where the constituent elements in the area map to links from the containing node to other nodes.",
+ "associated_id_refs": []
+ }
+ ]
+ }
+ },
+ "runtime_ref": ""
+ }
+ ],
+ "schemas": []
+}
diff --git a/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/react-nodes-mapping.jsx b/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/react-nodes-mapping.jsx
new file mode 100644
index 0000000000..dac5282790
--- /dev/null
+++ b/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/react-nodes-mapping.jsx
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2023 Elyra Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from "react";
+import PropTypes from "prop-types";
+
+import { Add32, Edit32 } from "@carbon/icons-react";
+
+import { CommonCanvas, CanvasController } from "common-canvas"; // eslint-disable-line import/no-unresolved
+
+import MappingContainerNode from "./mapping-container-node.jsx";
+
+import ReactNodesMappingFlow from "./react-nodes-mapping-flow.json";
+
+
+export default class ReactNodesMappingCanvas extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.canvasController = new CanvasController();
+ this.canvasController.setPipelineFlow(ReactNodesMappingFlow);
+
+ this.toolbarConfig = {
+ leftBar: [
+ { action: "undo", label: "Undo", enable: true },
+ { action: "redo", label: "Redo", enable: true }
+ ]
+ };
+
+ this.getConfig = this.getConfig.bind(this);
+ this.contextMenuHandler = this.contextMenuHandler.bind(this);
+ }
+
+ getConfig() {
+ const config = Object.assign({}, this.props.config, {
+ enableParentClass: "react-nodes-scrollable",
+ enableNodeFormatType: "Vertical",
+ enableLinkType: "Curve",
+ enableLinkSelection: "LinkOnly",
+ enablePaletteLayout: "None",
+ enableDropZoneOnExternalDrag: false,
+ enableEditingActions: true,
+ enableRaiseNodesToTopOnHover: false,
+ enableMarkdownInComments: true,
+ enableContextToolbar: true,
+ tipConfig: {
+ palette: false,
+ nodes: false,
+ ports: false,
+ links: false
+ },
+ enableNodeLayout: {
+ drawNodeLinkLineFromTo: "node_center",
+ drawCommentLinkLineTo: "node_center",
+ nodeShapeDisplay: false,
+ nodeExternalObject: MappingContainerNode,
+ defaultNodeWidth: 400,
+ defaultNodeHeight: 30,
+ contextToolbarPosition: "topRight",
+ ellipsisDisplay: false,
+ imageDisplay: false,
+ labelDisplay: false,
+ inputPortDisplay: false,
+ outputPortDisplay: false,
+ autoSizeNode: false
+ },
+ enableCanvasLayout: {
+ dataLinkArrowHead: true,
+ linkGap: 4
+ }
+ });
+ return config;
+ }
+
+ getCanvasController() {
+ return this.canvasController;
+ }
+
+ contextMenuHandler(source, defaultMenu) {
+ if (source.type === "node") {
+ return [
+ { action: "add", label: "Add", icon: (
), enable: true, toolbarItem: true },
+ { action: "edit", label: "Edit", icon: (
), enable: true, toolbarItem: true },
+ { action: "select", label: "Select", enable: true },
+ { action: "select_All", label: "Select All", enable: true },
+ { action: "findReplace", label: "Find/Replace", enable: true }
+ ];
+ }
+ return [];
+ }
+
+ render() {
+ const config = this.getConfig();
+ return (
+
+ );
+ }
+}
+
+ReactNodesMappingCanvas.propTypes = {
+ config: PropTypes.object
+};
diff --git a/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/react-nodes-mapping.scss b/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/react-nodes-mapping.scss
new file mode 100644
index 0000000000..1fb3997521
--- /dev/null
+++ b/canvas_modules/harness/src/client/components/custom-canvases/react-nodes-mapping/react-nodes-mapping.scss
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2023 Elyra Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* Override styles for react-nodes sample application. */
+.react-nodes-scrollable {
+
+ .d3-node-group {
+ .d3-foreign-object-external-node {
+ outline: none; // Suppress the tab highlighting
+ }
+
+ .node-container {
+ display: grid;
+ grid-template-rows: auto 1fr auto;
+ height: 100%;
+ overflow: auto;
+ // resize: both;
+ user-select: none; /* Prevent elements from being selectable */
+ border-width: 1px;
+ border-color: #E4E4E4;
+ border-style: solid;
+
+ }
+
+ .node-header {
+ height: 30px;
+ padding: 5px;
+ background-color: #FFFFFF;
+ display: grid;
+ grid-template-columns: auto 1fr auto 30px;
+
+ .node-header-chevron {
+ padding-left: 10px;
+ cursor: pointer;
+ }
+
+ .node-header-drag-icon {
+ cursor: pointer;
+ }
+ }
+
+ .scroll-div {
+ height: 100%;
+ overflow-y: scroll;
+ background-color: #FFFFFF;
+ }
+
+ .scrollable-row {
+ height: 30px;
+ padding: 5px;
+ background-color: #F4F4F4;
+ border-width: 2px;
+ border-color: #FFFFFF;
+ border-style: solid;
+ display: grid;
+ grid-template-columns: 16px 20px 130px 190px 20px;
+ &.with-mapping {
+ grid-template-columns: 16px 20px 130px 175px 190px 20px;
+ }
+ &:hover {
+ background-color: #E5E5E5;
+ }
+ }
+
+ .node-no-columns {
+ height: 30px;
+ padding: 5px;
+ background-color: #F4F4F4;
+ }
+
+ .node-footer {
+ height: 30px;
+ padding: 5px;
+ background-color: #FFFFFF;
+
+ .footer-label {
+ font-size: 12px;
+ padding-top: 3px;
+ }
+ }
+
+ .node-footer-chevron {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ padding: 3px;
+ cursor: se-resize;
+ }
+
+ .d3-node-selection-highlight[data-selected="yes"] {
+ stroke: $ui-05;
+ stroke-dasharray: 4, 4;
+ stroke-width: 1;
+ fill: transparent;
+ pointer-events: none;
+ }
+ }
+
+ .d3-link-group .d3-link-line,
+ .d3-link-group .d3-link-line-arrow-head {
+ stroke: #AFAFAF;
+ stroke-width: 1;
+ }
+
+ .d3-link-group:hover .d3-link-line,
+ .d3-link-group:hover .d3-link-line-arrow-head {
+ stroke: #95C3E9;
+ stroke-width: 2;
+ }
+
+ // Style line and arrow head when link is selected.
+ .d3-link-group[data-selected] .d3-link-line,
+ .d3-link-group[data-selected] .d3-link-line-arrow-head {
+ stroke: #4291E1;
+ stroke-width: 2;
+ }
+}
diff --git a/canvas_modules/harness/src/client/components/sidepanel/canvas/sidepanel-canvas.jsx b/canvas_modules/harness/src/client/components/sidepanel/canvas/sidepanel-canvas.jsx
index d84f35b97c..5fdf6398ef 100644
--- a/canvas_modules/harness/src/client/components/sidepanel/canvas/sidepanel-canvas.jsx
+++ b/canvas_modules/harness/src/client/components/sidepanel/canvas/sidepanel-canvas.jsx
@@ -65,6 +65,7 @@ import {
EXAMPLE_APP_READ_ONLY,
EXAMPLE_APP_PROGRESS,
EXAMPLE_APP_REACT_NODES_CARBON,
+ EXAMPLE_APP_REACT_NODES_MAPPING,
PALETTE_FLYOUT,
PALETTE_MODAL,
PALETTE_NONE,
@@ -1135,6 +1136,10 @@ export default class SidePanelForms extends React.Component {
value={EXAMPLE_APP_REACT_NODES_CARBON}
labelText={EXAMPLE_APP_REACT_NODES_CARBON}
/>
+