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
{containerType + " " + this.props.nodeData.label}
; + } + + getChevronIcon() { + const icon = this.isContainerResized() ? () : (); + return ( +
+ {icon} +
+ ); + } + + getFieldCount() { + return this.props.nodeData.app_data.table_data.fields.length + " columns"; + } + + getMapping(field) { + const mappingLink = this.props.canvasController.getLinks() + .find((l) => l.trgNodeId === this.props.nodeData.id && l.trgNodePortId === field.id); + if (mappingLink) { + const sourceNode = this.props.nodes + .find((n) => n.id === mappingLink.srcNodeId); + const sourceField = sourceNode.app_data.table_data.fields + .find((f) => f.id === mappingLink.srcNodePortId); + return sourceNode.label + "." + sourceField.label; + } + return ""; + } + + isContainerResized() { + return this.props.nodeData.isResized; + } + + resizeNode(evt) { + window.console.log("Resize node"); + evt.stopPropagation(); + evt.preventDefault(); + + const newNodesProps = this.adjustContainerPositions(evt.movementX, evt.movementY); + this.props.setNodesProperties(newNodesProps); + this.setPortPositions(); + } + + resizeNodeEnd() { + window.console.log("Resize node end"); + + document.removeEventListener("mousemove", this.resizeNode, true); + document.removeEventListener("mouseup", this.resizeNodeEnd, true); + + const nodeSizingObjectsInfo = {}; + + this.props.nodes.forEach((n) => { + const nodeSizingObj = { + id: n.id, + height: n.height, + width: n.width, + x_pos: n.x_pos, + y_pos: n.y_pos + }; + if (n.isResized) { + nodeSizingObj.isResized = true; + nodeSizingObj.resizeWidth = n.resizeWidth; + nodeSizingObj.resizeHeight = n.resizeHeight; + } + + nodeSizingObjectsInfo[n.id] = nodeSizingObj; + }); + + + this.saveNodePositions(nodeSizingObjectsInfo); + } + + saveNodePositions(nodeSizingObjectsInfo) { + this.props.canvasController.editActionHandler({ + editType: "resizeObjects", + editSource: "app", + objectsInfo: nodeSizingObjectsInfo, + detachedLinksInfo: [], + pipelineId: this.props.canvasController.getCurrentPipelineId() + }); + } + + adjustContainerPositions(xInc, yInc) { + const newNodesProps = []; + + const leftNodes = this.getLeftNodes(); + const rightNodes = this.getRightNodes(); + const nodeOnLeft = leftNodes.find((n) => n.id === this.props.nodeData.id); + + if (nodeOnLeft) { + newNodesProps.push({ + id: this.props.nodeData.id, + height: this.props.nodeData.height + yInc, + width: this.props.nodeData.width + xInc + }); + + rightNodes.forEach((n) => { + newNodesProps.push({ + id: n.id, + x_pos: n.x_pos + xInc + }); + }); + + leftNodes.forEach((n) => { + if (n.id !== this.props.nodeData.id) { + const yPosInc = n.y_pos > this.props.nodeData.y_pos ? yInc : 0; + + newNodesProps.push({ + id: n.id, + x_pos: n.x_pos + xInc, + y_pos: n.y_pos + yPosInc + }); + } + }); + + } else { + newNodesProps.push({ + id: this.props.nodeData.id, + height: this.props.nodeData.height + yInc, + width: this.props.nodeData.width + xInc + }); + + const nodesUnderOnRight = rightNodes.filter((n) => n.y_pos > this.props.nodeData.y_pos); + + nodesUnderOnRight.forEach((n) => { + newNodesProps.push({ + id: n.id, + y_pos: n.y_pos + yInc + }); + }); + } + + return newNodesProps; + } + + generateTopLeftIcon() { + if (this.props.nodeData.op === "output_link") { + return ( +
+ +
+ ); + + } else if (this.props.nodeData.op === "input_link") { + return ( +
evt.stopPropagation()}> + +
+ ); + } + // Returning an empty
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 ( +
+ {topLeftIcon} + {containerLabel} + {fieldCount} + {chevronIcon} +
+ ); + } + + 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} /> +