diff options
Diffstat (limited to 'src/generic-components/graph')
-rw-r--r-- | src/generic-components/graph/ForceDefinitions.js | 37 | ||||
-rw-r--r-- | src/generic-components/graph/ForceDirectedGraph.jsx | 498 | ||||
-rw-r--r-- | src/generic-components/graph/IconFactory.js | 251 | ||||
-rw-r--r-- | src/generic-components/graph/Link.jsx | 66 | ||||
-rw-r--r-- | src/generic-components/graph/Node.jsx | 58 | ||||
-rw-r--r-- | src/generic-components/graph/NodeFactory.js | 115 | ||||
-rw-r--r-- | src/generic-components/graph/NodeVisualElementConstants.js | 49 | ||||
-rw-r--r-- | src/generic-components/graph/NodeVisualElementFactory.js | 189 | ||||
-rw-r--r-- | src/generic-components/graph/SVGShape.jsx | 65 |
9 files changed, 1328 insertions, 0 deletions
diff --git a/src/generic-components/graph/ForceDefinitions.js b/src/generic-components/graph/ForceDefinitions.js new file mode 100644 index 0000000..92f939f --- /dev/null +++ b/src/generic-components/graph/ForceDefinitions.js @@ -0,0 +1,37 @@ +/* + * ============LICENSE_START=================================================== + * SPARKY (AAI UI service) + * ============================================================================ + * Copyright © 2017 AT&T Intellectual Property. + * Copyright © 2017 Amdocs + * All rights reserved. + * ============================================================================ + * 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. + * ============LICENSE_END===================================================== + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + */ + +const ONE_SECOND = 1000; +const THIRTY_FRAMES_PER_SECOND = 30; + +export const simulationKeys = { + CENTERING_FORCE: {}, + COLLISION_FORCE: {}, + LINK_FORCE: {}, + MANY_BODY_FORCE: {}, + POSITIONING_FORCE: {}, + DEFAULT_FORCE_NAME: 'defaultForce', + DATA_COPY_INTERVAL: ONE_SECOND / THIRTY_FRAMES_PER_SECOND +}; diff --git a/src/generic-components/graph/ForceDirectedGraph.jsx b/src/generic-components/graph/ForceDirectedGraph.jsx new file mode 100644 index 0000000..caad1c0 --- /dev/null +++ b/src/generic-components/graph/ForceDirectedGraph.jsx @@ -0,0 +1,498 @@ +/* + * ============LICENSE_START=================================================== + * SPARKY (AAI UI service) + * ============================================================================ + * Copyright © 2017 AT&T Intellectual Property. + * Copyright © 2017 Amdocs + * All rights reserved. + * ============================================================================ + * 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. + * ============LICENSE_END===================================================== + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + */ + +import {drag} from 'd3-drag'; +import {forceSimulation, forceLink, forceManyBody, forceCenter} from 'd3-force'; +import {interpolateNumber} from 'd3-interpolate'; +import {select, event as currentEvent} from 'd3-selection'; +import React, {Component, PropTypes} from 'react'; +import {interval, now} from 'd3-timer'; +import {zoom, zoomIdentity} from 'd3-zoom'; +import NodeConstants from './NodeVisualElementConstants.js'; + +import {simulationKeys} from './ForceDefinitions.js'; +import NodeFactory from './NodeFactory.js'; +import NodeVisualElementFactory from './NodeVisualElementFactory.js'; + +class ForceDirectedGraph extends Component { + static propTypes = { + viewWidth: PropTypes.number, + viewHeight: PropTypes.number, + graphData: PropTypes.object, + nodeIdKey: PropTypes.string, + linkIdKey: PropTypes.string, + nodeSelectedCallback: PropTypes.func, + nodeButtonSelectedCallback: PropTypes.func, + currentlySelectedNodeView: PropTypes.string + }; + + static defaultProps = { + viewWidth: 0, + viewHeight: 0, + graphData: { + graphCounter: -1, nodeDataArray: [], linkDataArray: [], graphMeta: {} + }, + nodeIdKey: '', + linkIdKey: '', + nodeSelectedCallback: undefined, + nodeButtonSelectedCallback: undefined, + currentlySelectedNodeView: '' + }; + + constructor(props) { + super(props); + + this.state = { + nodes: [], links: [], mainGroupTransform: zoomIdentity + }; + + this.updateSimulationForce = this.updateSimulationForce.bind(this); + this.resetTransform = this.resetTransform.bind(this); + this.applyBufferDataToState = this.applyBufferDataToState.bind(this); + this.createNodePropForState = this.createNodePropForState.bind(this); + this.createLinkPropForState = this.createLinkPropForState.bind(this); + this.startSimulation = this.startSimulation.bind(this); + this.simulationComplete = this.simulationComplete.bind(this); + this.simulationTick = this.simulationTick.bind(this); + this.nodeSelected = this.nodeSelected.bind(this); + this.onZoom = this.onZoom.bind(this); + this.onGraphDrag = this.onGraphDrag.bind(this); + this.onNodeDrag = this.onNodeDrag.bind(this); + this.addNodeInterpolator = this.addNodeInterpolator.bind(this); + this.runInterpolators = this.runInterpolators.bind(this); + + this.nodeBuffer = []; + this.linkBuffer = []; + this.nodeDatum = []; + this.nodeButtonDatum = []; + this.nodeFactory = new NodeFactory(); + this.visualElementFactory = new NodeVisualElementFactory(); + + this.isGraphMounted = false; + + this.listenerGraphCounter = -1; + this.nodeIndexTracker = new Map(); + this.interpolators = new Map(); + this.areInterpolationsRunning = false; + + this.newNodeSelected = true; + this.currentlySelectedNodeButton = undefined; + + this.intervalTimer = interval(this.applyBufferDataToState, simulationKeys.DATA_COPY_INTERVAL); + this.intervalTimer.stop(); + + this.interpolationTimer = interval(this.runInterpolators, simulationKeys.DATA_COPY_INTERVAL); + this.interpolationTimer.stop(); + + this.simulation = forceSimulation(); + this.simulation.on('end', this.simulationComplete); + this.simulation.stop(); + + this.svgZoom = + zoom().scaleExtent([NodeConstants.SCALE_EXTENT_MIN, NodeConstants.SACEL_EXTENT_MAX]); + this.svgZoom.clickDistance(2); + this.nodeDrag = drag().clickDistance(2); + + this.updateSimulationForce(); + + if (props.graphData) { + if (props.graphData.graphCounter !== -1) { + this.startSimulation(props.graphData); + } + } + } + + componentDidMount() { + this.isGraphMounted = true; + } + + componentWillReceiveProps(nextProps) { + if (nextProps.graphData.graphCounter !== this.props.graphData.graphCounter) { + this.listenerGraphCounter = this.props.graphData.graphCounter; + this.newNodeSelected = true; + this.resetTransform(); + this.startSimulation(nextProps.graphData); + } + } + + componentDidUpdate(prevProps) { + let hasNewGraphDataRendered = (prevProps.graphData.graphCounter === + this.props.graphData.graphCounter); + let shouldAttachListeners = (this.listenerGraphCounter !== this.props.graphData.graphCounter); + let nodeCount = this.state.nodes.length; + + if (nodeCount > 0) { + if (hasNewGraphDataRendered && shouldAttachListeners) { + + let nodes = select('.fdgMainSvg').select('.fdgMainG') + .selectAll('.aai-entity-node') + .data(this.nodeDatum); + + nodes.on('click', (d) => { + this.nodeSelected(d); + }); + + nodes.call(this.nodeDrag.on('drag', (d) => { + let xAndY = [currentEvent.x, currentEvent.y]; + this.onNodeDrag(d, xAndY); + })); + + let mainSVG = select('.fdgMainSvg'); + let mainView = mainSVG.select('.fdgMainView'); + this.svgZoom.transform(mainSVG, zoomIdentity); + this.svgZoom.transform(mainView, zoomIdentity); + + mainSVG.call(this.svgZoom.on('zoom', () => { // D3 Zoom also handles panning + this.onZoom(currentEvent.transform); + })).on('dblclick.zoom', null); // Ignore the double-click zoom event + + this.listenerGraphCounter = this.props.graphData.graphCounter; + } + } + } + + componentWillUnmount() { + this.isGraphMounted = false; + + let nodes = select('.fdgMainSvg').select('.fdgMainG') + .selectAll('.aai-entity-node'); + let nodeButtons = nodes.selectAll('.node-button'); + + nodes.on('click', null); + nodeButtons.on('click', null); + + let mainSVG = select('.fdgMainSvg'); + + mainSVG.call(this.svgZoom.on('zoom', null)).on('dblclick.zoom', null); + mainSVG.call(drag().on('drag', null)); + } + + updateSimulationForce() { + this.simulation.force('link', forceLink()); + this.simulation.force('link').id((d) => { + return d.id; + }); + this.simulation.force('link').strength(0.3); + this.simulation.force('link').distance(100); + + this.simulation.force('charge', forceManyBody()); + this.simulation.force('charge').strength(-1250); + this.simulation.alpha(1); + + this.simulation.force('center', + forceCenter(this.props.viewWidth / 2, this.props.viewHeight / 2)); + } + + resetTransform() { + if (this.isGraphMounted) { + this.setState(() => { + return { + mainGroupTransform: zoomIdentity + }; + }); + } + } + + applyBufferDataToState() { + this.nodeIndexTracker.clear(); + + let newNodes = []; + this.nodeBuffer.map((node, i) => { + let nodeProps = this.createNodePropForState(node); + + if (nodeProps.meta.nodeMeta.className === NodeConstants.SELECTED_NODE_CLASS_NAME || + nodeProps.meta.nodeMeta.className === NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME) { + + this.nodeButtonDatum[0].data = nodeProps.meta; + + nodeProps = { + ...nodeProps, + buttons: [this.nodeButtonDatum[0].isSelected] + }; + } + + newNodes.push(this.nodeFactory.buildNode(nodeProps.meta.nodeMeta.className, nodeProps)); + + this.nodeIndexTracker.set(node.id, i); + }); + + let newLinks = []; + this.linkBuffer.map((link) => { + let key = link.id; + let linkProps = this.createLinkPropForState(link); + newLinks.push(this.visualElementFactory.createSvgLine(linkProps, key)); + }); + + if (this.isGraphMounted) { + this.setState(() => { + return { + nodes: newNodes, links: newLinks + }; + }); + } + } + + createNodePropForState(nodeData) { + return { + renderProps: { + key: nodeData.id, x: nodeData.x, y: nodeData.y + }, meta: { + ...nodeData + } + }; + } + + createLinkPropForState(linkData) { + return { + className: 'aai-entity-link', + x1: linkData.source.x, + y1: linkData.source.y, + x2: linkData.target.x, + y2: linkData.target.y + }; + } + + startSimulation(graphData) { + this.nodeFactory.setNodeMeta(graphData.graphMeta); + + // Experiment with removing length = 0... might not be needed as new array + // assignment will likely destroy old reference + this.nodeBuffer.length = 0; + this.nodeBuffer = Array.from(graphData.nodeDataArray); + this.linkBuffer.length = 0; + this.linkBuffer = Array.from(graphData.linkDataArray); + this.nodeDatum.length = 0; + this.nodeDatum = Array.from(graphData.nodeDataArray); + + this.nodeButtonDatum.length = 0; + + let isNodeDetailsSelected = true; + this.nodeButtonDatum.push({ + name: NodeConstants.ICON_ELLIPSES, isSelected: isNodeDetailsSelected + }); + + if (isNodeDetailsSelected) { + this.currentlySelectedNodeButton = NodeConstants.ICON_ELLIPSES; + } + + this.updateSimulationForce(); + + this.simulation.nodes(this.nodeBuffer); + this.simulation.force('link').links(this.linkBuffer); + this.simulation.on('tick', this.simulationTick); + this.simulation.restart(); + } + + simulationComplete() { + this.intervalTimer.stop(); + this.applyBufferDataToState(); + } + + simulationTick() { + this.intervalTimer.restart(this.applyBufferDataToState, simulationKeys.DATA_COPY_INTERVAL); + this.simulation.on('tick', null); + } + + nodeSelected(datum) { + if (this.props.nodeSelectedCallback) { + this.props.nodeSelectedCallback(datum); + } + + let didUpdateNew = false; + let didUpdatePrevious = false; + let isSameNodeSelected = true; + + // Check to see if a default node was previously selected + let selectedDefaultNode = select('.fdgMainSvg').select('.fdgMainG') + .selectAll('.aai-entity-node') + .filter('.selected-node'); + if (!selectedDefaultNode.empty()) { + if (selectedDefaultNode.datum().id !== datum.id) { + this.nodeBuffer[selectedDefaultNode.datum().index].nodeMeta.className = + NodeConstants.GENERAL_NODE_CLASS_NAME; + didUpdatePrevious = true; + isSameNodeSelected = false; + } + } + + // Check to see if a searched node was previously selected + let selectedSearchedNode = select('.fdgMainSvg').select('.fdgMainG') + .selectAll('.aai-entity-node') + .filter('.selected-search-node'); + if (!selectedSearchedNode.empty()) { + if (selectedSearchedNode.datum().id !== datum.id) { + this.nodeBuffer[selectedSearchedNode.datum().index].nodeMeta.className = + NodeConstants.SEARCHED_NODE_CLASS_NAME; + didUpdatePrevious = true; + isSameNodeSelected = false; + } + } + + if (!isSameNodeSelected) { + let newlySelectedNode = select('.fdgMainSvg').select('.fdgMainG') + .selectAll('.aai-entity-node') + .filter((d) => { + return (datum.id === d.id); + }); + if (!newlySelectedNode.empty()) { + + if (newlySelectedNode.datum().nodeMeta.searchTarget) { + this.nodeBuffer[newlySelectedNode.datum().index].nodeMeta.className = + NodeConstants.SELECTED_SEARCHED_NODE_CLASS_NAME; + } else { + this.nodeBuffer[newlySelectedNode.datum().index].nodeMeta.className = + NodeConstants.SELECTED_NODE_CLASS_NAME; + } + didUpdateNew = true; + } + } + + if (didUpdatePrevious && didUpdateNew) { + this.newNodeSelected = true; + this.applyBufferDataToState(); + } + } + + onZoom(eventTransform) { + if (this.isGraphMounted) { + this.setState(() => { + return { + mainGroupTransform: eventTransform + }; + }); + } + } + + onGraphDrag(xAndYCoords) { + let translate = `translate(${xAndYCoords.x}, ${xAndYCoords.y})`; + let oldTransform = this.state.mainGroupTransform; + if (this.isGraphMounted) { + this.setState(() => { + return { + ...oldTransform, translate + }; + }); + } + } + + onNodeDrag(datum, xAndYCoords) { + let nodeIndex = this.nodeIndexTracker.get(datum.id); + if (this.nodeBuffer[nodeIndex]) { + this.nodeBuffer[nodeIndex].x = xAndYCoords[0]; + this.nodeBuffer[nodeIndex].y = xAndYCoords[1]; + this.applyBufferDataToState(); + } + } + + addNodeInterpolator(nodeId, key, startingValue, endingValue, duration) { + let numberInterpolator = interpolateNumber(startingValue, endingValue); + let timeNow = now(); + let interpolationObject = { + nodeId: nodeId, key: key, duration: duration, timeCreated: timeNow, method: numberInterpolator + }; + this.interpolators.set(nodeId, interpolationObject); + + if (!this.areInterpolationsRunning) { + this.interpolationTimer.restart(this.runInterpolators, simulationKeys.DATA_COPY_INTERVAL); + this.areInterpolationsRunning = true; + } + } + + runInterpolators() { + // If we have no more interpolators to run then shut'r down! + if (this.interpolators.size === 0) { + this.interpolationTimer.stop(); + this.areInterpolationsRunning = false; + } + + let iterpolatorsComplete = []; + // Apply interpolation values + this.interpolators.forEach((interpolator) => { + let nodeIndex = this.nodeIndexTracker.get(interpolator.nodeId); + if (nodeIndex) { + let elapsedTime = now() - interpolator.timeCreated; + // Normalize t as D3's interpolateNumber needs a value between 0 and 1 + let t = elapsedTime / interpolator.duration; + if (t >= 1) { + t = 1; + iterpolatorsComplete.push(interpolator.nodeId); + } + this.nodeBuffer[nodeIndex][interpolator.key] = interpolator.method(t); + } + }); + + // Remove any interpolators that are complete + if (iterpolatorsComplete.length > 0) { + for (let i = 0; i < iterpolatorsComplete.length; i++) { + this.interpolators.delete(iterpolatorsComplete[i]); + } + } + + this.applyBufferDataToState(); + } + + render() { + // We will be using these values veru shortly, commenting out for eslint + // reasons so we can build for PV let {viewWidth, viewHeight} = this.props; + let {nodes, links, mainGroupTransform} = this.state; + + return ( + <div className='ts-force-selected-graph'> + <svg className={'fdgMainSvg'} width='100%' height='100%'> + <rect className={'fdgMainView'} x='0.5' y='0.5' width='99%' + height='99%' fill='none'/> + <filter id='selected-node-drop-shadow'> + <feGaussianBlur in='SourceAlpha' stdDeviation='2.2'/> + <feOffset dx='-1' dy='1' result='offsetblur'/> + <feFlood floodColor='rgba(0,0,0,0.5)'/> + <feComposite in2='offsetblur' operator='in'/> + <feMerge> + <feMergeNode/> + <feMergeNode in='SourceGraphic'/> + </feMerge> + </filter> + <g className={'fdgMainG'} transform={mainGroupTransform}> + {links} + {nodes} + </g> + </svg> + </div> + ); + } + + static graphCounter = 0; + + static generateNewProps(nodeArray, linkArray, metaData) { + ForceDirectedGraph.graphCounter += 1; + return { + graphCounter: ForceDirectedGraph.graphCounter, + nodeDataArray: nodeArray, + linkDataArray: linkArray, + graphMeta: metaData + }; + } +} + +export default ForceDirectedGraph; diff --git a/src/generic-components/graph/IconFactory.js b/src/generic-components/graph/IconFactory.js new file mode 100644 index 0000000..772cb1e --- /dev/null +++ b/src/generic-components/graph/IconFactory.js @@ -0,0 +1,251 @@ +/* + * ============LICENSE_START=================================================== + * SPARKY (AAI UI service) + * ============================================================================ + * Copyright © 2017 AT&T Intellectual Property. + * Copyright © 2017 Amdocs + * All rights reserved. + * ============================================================================ + * 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. + * ============LICENSE_END===================================================== + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + */ + +import React from 'react'; + +import NodeVisualElementConstants from './NodeVisualElementConstants.js'; + +class IconFactory { + static createIcon(iconName, iconProps, key, nodeProps) { + switch (iconName) { + case NodeVisualElementConstants.ICON_ELLIPSES: + let iconEllipsesChildren = []; + + if (iconProps.svgAttributes) { + let circleProps = { + ...iconProps.svgAttributes, + key: key + '_ellipsesCircle' + }; + + let ellipsesBackgroundClassName = (iconProps.isSelected === true) + ? 'background-selected' + : 'background-unselected'; + + circleProps = { + ...circleProps, + className: ellipsesBackgroundClassName + }; + + iconEllipsesChildren.push( + React.createElement(NodeVisualElementConstants.SVG_CIRCLE, + circleProps)); + } + + let ellipseOneProps = { + className: 'ellipses-ellipse', + cx: '-4', + cy: '0', + rx: '1.5', + ry: '1.5', + key: key + '_ellipseOne' + }; + iconEllipsesChildren.push( + React.createElement(NodeVisualElementConstants.ELLIPSE, + ellipseOneProps)); + + let ellipseTwoProps = { + className: 'ellipses-ellipse', + cx: '0', + cy: '0', + rx: '1.5', + ry: '1.5', + key: key + '_ellipseTwo' + }; + iconEllipsesChildren.push( + React.createElement(NodeVisualElementConstants.ELLIPSE, + ellipseTwoProps)); + + let ellipseThreeProps = { + className: 'ellipses-ellipse', + cx: '4', + cy: '0', + rx: '1.5', + ry: '1.5', + key: key + '_ellipseThree' + }; + iconEllipsesChildren.push( + React.createElement(NodeVisualElementConstants.ELLIPSE, + ellipseThreeProps)); + + let finalEllipsesProps = { + className: iconProps.class, + key: key + }; + + if (iconProps.shapeAttributes) { + if (iconProps.shapeAttributes.offset) { + finalEllipsesProps = { + ...finalEllipsesProps, + transform: `translate( + ${iconProps.shapeAttributes.offset.x}, + ${iconProps.shapeAttributes.offset.y})` + }; + } + } + + return React.createElement(NodeVisualElementConstants.G, + finalEllipsesProps, iconEllipsesChildren); + + case NodeVisualElementConstants.ICON_TRIANGLE_WARNING: + let iconTriangleWarningChildren = []; + + if (iconProps.svgAttributes) { + let circleProps = { + ...iconProps.svgAttributes, + key: key + '_triangleWarningCircle' + }; + + let triangleWarningBackgrounClassName = (iconProps.isSelected === + true) ? 'background-selected' : 'background-unselected'; + + circleProps = { + ...circleProps, + className: triangleWarningBackgrounClassName + }; + iconTriangleWarningChildren.push( + React.createElement(NodeVisualElementConstants.SVG_CIRCLE, + circleProps)); + } + + let trianglePathProps = { + className: 'triangle-warning', + d: 'M-4.5 4 L 0 -6.5 L 4.5 4 Z M-0.5 3.75 L -0.5 3 L 0.5 3 L 0.5 3.75 Z M-0.35 2.75 L -0.75 -3.5 L 0.75 -3.5 L 0.35 2.75 Z', + key: key + '_triangleWarningPath' + }; + iconTriangleWarningChildren.push( + React.createElement(NodeVisualElementConstants.PATH, + trianglePathProps)); + + let finalTriangleWarningProps = { + className: iconProps.class, + key: key + }; + + if (iconProps.shapeAttributes) { + if (iconProps.shapeAttributes.offset) { + finalTriangleWarningProps = { + ...finalTriangleWarningProps, + transform: `translate( + ${iconProps.shapeAttributes.offset.x}, + ${iconProps.shapeAttributes.offset.y})` + }; + } + } + + return React.createElement(NodeVisualElementConstants.G, + finalTriangleWarningProps, iconTriangleWarningChildren); + + case NodeVisualElementConstants.ICON_TICK: + let tickOverlayMainKey = nodeProps.meta.id + '_overlayTick'; + let iconTickRadius = 5; + let tickNodeClassName = nodeProps.meta.nodeMeta.className; + if (tickNodeClassName === + NodeVisualElementConstants.SELECTED_SEARCHED_NODE_CLASS_NAME || + tickNodeClassName === + NodeVisualElementConstants.SELECTED_NODE_CLASS_NAME) { + iconTickRadius = 8; + } + let tickIconcircleProps = { + className: 'icon_tick_circle', + r: iconTickRadius, + key: key + '_tickCircle' + }; + let iconTickChildren = []; + + iconTickChildren.push( + React.createElement(NodeVisualElementConstants.SVG_CIRCLE, + tickIconcircleProps)); + let tickIconTransformProperty = 'translate(-15, -10)'; + if (tickNodeClassName === + NodeVisualElementConstants.SELECTED_SEARCHED_NODE_CLASS_NAME || + tickNodeClassName === + NodeVisualElementConstants.SELECTED_NODE_CLASS_NAME) { + tickIconTransformProperty = 'translate(-30, -18)'; + + } + let tickPathProps = { + className: 'icon_tick_path', + d: 'M-3 0 L -1.5 1.8 L3 -1.5 L -1.5 1.8', + key: key + '_tickPath' + }; + iconTickChildren.push( + React.createElement(NodeVisualElementConstants.PATH, tickPathProps)); + + let finalTickIconProps = { + className: 'icon_tick', + key: tickOverlayMainKey + '_final', + transform: tickIconTransformProperty + }; + return React.createElement(NodeVisualElementConstants.G, + finalTickIconProps, iconTickChildren); + + case NodeVisualElementConstants.ICON_WARNING: + let warningOverlayMainKey = nodeProps.meta.id + '_overlayTick'; + let iconWarningRadius = 5; + let warningNodeClassName = nodeProps.meta.nodeMeta.className; + if (warningNodeClassName === + NodeVisualElementConstants.SELECTED_SEARCHED_NODE_CLASS_NAME || + warningNodeClassName === + NodeVisualElementConstants.SELECTED_NODE_CLASS_NAME) { + iconWarningRadius = 8; + } + let warningIconcircleProps = { + className: 'icon_warning_circle', + r: iconWarningRadius, + key: key + '_warningCircle' + }; + let iconWarningChildren = []; + + iconWarningChildren.push( + React.createElement(NodeVisualElementConstants.SVG_CIRCLE, + warningIconcircleProps)); + let warningIconTransformProperty = 'translate(-15, -10)'; + if (warningNodeClassName === + NodeVisualElementConstants.SELECTED_SEARCHED_NODE_CLASS_NAME || + warningNodeClassName === + NodeVisualElementConstants.SELECTED_NODE_CLASS_NAME) { + warningIconTransformProperty = 'translate(-30, -18)'; + } + let warningPathProps = { + className: 'icon_warning_path', + d: 'M-0.35 3.8 L -0.35 3.7 L 0.35 3.7 L 0.35 3.8 Z M-0.1 1.8 L -0.6 -3.5 L 0.6 -3.5 L 0.1 1.8 Z', + key: key + '_tickPath' + }; + iconWarningChildren.push( + React.createElement(NodeVisualElementConstants.PATH, + warningPathProps)); + + let finalWarningIconProps = { + className: 'icon_warning', + key: warningOverlayMainKey + '_final', + transform: warningIconTransformProperty + }; + return React.createElement(NodeVisualElementConstants.G, + finalWarningIconProps, iconWarningChildren); + } + } +} + +export default IconFactory; diff --git a/src/generic-components/graph/Link.jsx b/src/generic-components/graph/Link.jsx new file mode 100644 index 0000000..c4ef235 --- /dev/null +++ b/src/generic-components/graph/Link.jsx @@ -0,0 +1,66 @@ +/* + * ============LICENSE_START=================================================== + * SPARKY (AAI UI service) + * ============================================================================ + * Copyright © 2017 AT&T Intellectual Property. + * Copyright © 2017 Amdocs + * All rights reserved. + * ============================================================================ + * 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. + * ============LICENSE_END===================================================== + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + */ + +import React, {Component} from 'react'; + +import TempCreateAttributes from './TempCreateAttributes.js'; + +class Link extends Component { + + static propTypes = { + x1: React.PropTypes.number, + y1: React.PropTypes.number, + x2: React.PropTypes.number, + y2: React.PropTypes.number, + linkAttributes: React.PropTypes.object + }; + + static defaultProps = { + x1: 0, + y1: 0, + x2: 0, + y2: 0, + linkAttributes: {} + }; + + render() { + let {x1, y1, x2, y2, linkAttributes} = this.props; + + let combinedAttributes = { + ...linkAttributes, + x1: x1, + y1: y1, + x2: x2, + y2: y2 + }; + + return ( + <line {...combinedAttributes} + style={TempCreateAttributes.createLineStyle()}/> + ); + } +} + +export default Link; diff --git a/src/generic-components/graph/Node.jsx b/src/generic-components/graph/Node.jsx new file mode 100644 index 0000000..79f7161 --- /dev/null +++ b/src/generic-components/graph/Node.jsx @@ -0,0 +1,58 @@ +/* + * ============LICENSE_START=================================================== + * SPARKY (AAI UI service) + * ============================================================================ + * Copyright © 2017 AT&T Intellectual Property. + * Copyright © 2017 Amdocs + * All rights reserved. + * ============================================================================ + * 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. + * ============LICENSE_END===================================================== + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + */ + +import React, {Component} from 'react'; + +class Node extends Component { + + static propTypes = { + x: React.PropTypes.number, + y: React.PropTypes.number, + nodeClass: React.PropTypes.string, + visualElements: React.PropTypes.array, + meta: React.PropTypes.object + }; + + static defaultProps = { + x: 0, + y: 0, + nodeClass: '', + visualElements: [], + meta: {} + }; + + render() { + let {x, y, nodeClass, visualElements} = this.props; + let translate = `translate(${x}, ${y})`; + + return ( + <g className={nodeClass} transform={translate}> + {visualElements} + </g> + ); + } +} + +export default Node; diff --git a/src/generic-components/graph/NodeFactory.js b/src/generic-components/graph/NodeFactory.js new file mode 100644 index 0000000..6dced1d --- /dev/null +++ b/src/generic-components/graph/NodeFactory.js @@ -0,0 +1,115 @@ +/* + * ============LICENSE_START=================================================== + * SPARKY (AAI UI service) + * ============================================================================ + * Copyright © 2017 AT&T Intellectual Property. + * Copyright © 2017 Amdocs + * All rights reserved. + * ============================================================================ + * 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. + * ============LICENSE_END===================================================== + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + */ + +import React from 'react'; + +import NodeVisualElementConstants from './NodeVisualElementConstants.js'; +import NodeVisualElementFactory from './NodeVisualElementFactory.js'; + +class NodeFactory { + + constructor() { + this.graphMeta = {}; + this.visualElementFactory = new NodeVisualElementFactory(); + + this.setNodeMeta = this.setNodeMeta.bind(this); + } + + setNodeMeta(metaObject) { + this.graphMeta = metaObject; + this.visualElementFactory.setVisualElementMeta(metaObject); + } + + buildNode(nodeType, nodeProps) { + + let translate = `translate( + ${nodeProps.renderProps.x}, + ${nodeProps.renderProps.y})`; + let finalProps = { + ...nodeProps.renderProps, + className: this.graphMeta.aaiEntityNodeDescriptors[nodeType].class, + transform: translate + }; + + let nodeVisualElementsData = this.extractVisualElementArrayFromMeta( + nodeType); + let nodeVisualElements = undefined; + if (nodeVisualElementsData) { + nodeVisualElements = []; + nodeVisualElementsData.map((elementData, index) => { + if (elementData.type === NodeVisualElementConstants.BUTTON) { + if (nodeProps.buttons) { + let isButtonSelected = true; + elementData = { + ...elementData, + isSelected: isButtonSelected + }; + } + } + nodeVisualElements.push( + this.visualElementFactory.buildVisualElement(nodeProps.meta, + elementData.type, elementData, index)); + }); + //Draw overlay only if the node is validated + if (nodeProps.meta.nodeMeta.nodeValidated) { + + if (nodeProps.meta.nodeMeta.nodeIssue) { + let warningOverlayProps = { + name: NodeVisualElementConstants.ICON_WARNING, + }; + nodeVisualElements.push( + this.visualElementFactory.buildVisualElement(nodeProps, + NodeVisualElementConstants.ICON, warningOverlayProps, + nodeVisualElementsData.length + 1)); + } else { + let tickOverlayProps = { + name: NodeVisualElementConstants.ICON_TICK, + }; + nodeVisualElements.push( + this.visualElementFactory.buildVisualElement(nodeProps, + NodeVisualElementConstants.ICON, tickOverlayProps, + nodeVisualElementsData.length + 1)); + } + } + } + + if (nodeVisualElements) { + return React.createElement('g', finalProps, nodeVisualElements); + } + + return React.createElement('g', finalProps); + } + + extractVisualElementArrayFromMeta(nodeClassName) { + let nodeVisualElements = undefined; + if (this.graphMeta.aaiEntityNodeDescriptors) { + nodeVisualElements = + this.graphMeta.aaiEntityNodeDescriptors[nodeClassName].visualElements; + } + return nodeVisualElements; + } +} + +export default NodeFactory; diff --git a/src/generic-components/graph/NodeVisualElementConstants.js b/src/generic-components/graph/NodeVisualElementConstants.js new file mode 100644 index 0000000..d89b6c9 --- /dev/null +++ b/src/generic-components/graph/NodeVisualElementConstants.js @@ -0,0 +1,49 @@ +/* + * ============LICENSE_START=================================================== + * SPARKY (AAI UI service) + * ============================================================================ + * Copyright © 2017 AT&T Intellectual Property. + * Copyright © 2017 Amdocs + * All rights reserved. + * ============================================================================ + * 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. + * ============LICENSE_END===================================================== + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + */ + +export default { + SVG_CIRCLE: 'circle', + SVG_LINE: 'line', + TEXT: 'text', + IMAGE: 'image', + OBJECT: 'object', + ELLIPSE: 'ellipse', + G: 'g', + PATH: 'path', + BUTTON: 'button', + ICON: 'icon', + CSS_CLASS: 'className', + SELECTED_NODE_CLASS_NAME: 'selectedNodeClass', + GENERAL_NODE_CLASS_NAME: 'generalNodeClass', + SEARCHED_NODE_CLASS_NAME: 'searchedNodeClass', + SELECTED_SEARCHED_NODE_CLASS_NAME: 'selectedSearchedNodeClass', + SCALE_EXTENT_MIN: 0.125, + SACEL_EXTENT_MAX: 10, + ICON_ELLIPSES: 'icon_ellipses', + ICON_TRIANGLE_WARNING: 'icon_triangle_warning', + ICON_TICK: 'icon_tick', + ICON_WARNING: 'icon_warning', + BUTTON_CLICK_NODE_DETAILS: 'NODE_DETAILS' +}; diff --git a/src/generic-components/graph/NodeVisualElementFactory.js b/src/generic-components/graph/NodeVisualElementFactory.js new file mode 100644 index 0000000..13e9b7c --- /dev/null +++ b/src/generic-components/graph/NodeVisualElementFactory.js @@ -0,0 +1,189 @@ +/* + * ============LICENSE_START=================================================== + * SPARKY (AAI UI service) + * ============================================================================ + * Copyright © 2017 AT&T Intellectual Property. + * Copyright © 2017 Amdocs + * All rights reserved. + * ============================================================================ + * 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. + * ============LICENSE_END===================================================== + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + */ + +import React from 'react'; + +import IconFactory from './IconFactory.js'; +import NodeVisualElementConstants from './NodeVisualElementConstants.js'; + +class NodeVisualElementFactory { + + constructor() { + this.visualElementMeta = {}; + + this.setVisualElementMeta = this.setVisualElementMeta.bind(this); + this.buildVisualElement = this.buildVisualElement.bind(this); + this.createSvgCircle = this.createSvgCircle.bind(this); + this.createSvgLine = this.createSvgLine.bind(this); + this.createTextElement = this.createTextElement.bind(this); + this.createImageElement = this.createImageElement.bind(this); + this.createObjectElement = this.createObjectElement.bind(this); + this.createButtonElement = this.createButtonElement.bind(this); + this.applySvgAttributes = this.applySvgAttributes.bind(this); + this.applyTransform = this.applyTransform.bind(this); + } + + setVisualElementMeta(metaObject) { + this.visualElementMeta = metaObject; + } + + buildVisualElement(nodeProps, elementType, elementProps, index) { + let elementKey = nodeProps.id + index.toString(); + switch (elementType) { + case NodeVisualElementConstants.SVG_CIRCLE: + return this.createSvgCircle(elementProps, elementKey); + + case NodeVisualElementConstants.SVG_LINE: + return this.createSvgLine(elementProps, elementKey); + + case NodeVisualElementConstants.TEXT: + return this.createTextElement(nodeProps, elementProps, elementKey); + + case NodeVisualElementConstants.IMAGE: + return this.createImageElement(elementProps, elementKey); + + case NodeVisualElementConstants.OBJECT: + return this.createObjectElement(elementProps, elementKey); + + case NodeVisualElementConstants.BUTTON: + return this.createButtonElement(elementProps, elementKey); + + case NodeVisualElementConstants.ICON: + return this.createButtonElement(elementProps, elementKey, nodeProps); + + } + } + + createSvgCircle(circleProps, elementKey) { + let finalProps = {}; + finalProps[NodeVisualElementConstants.CSS_CLASS] = circleProps.class; + + finalProps = this.applyTransform(finalProps, circleProps.shapeAttributes); + finalProps = this.applySvgAttributes(finalProps, circleProps.svgAttributes); + + finalProps = { + ...finalProps, + key: elementKey + }; + + return React.createElement(NodeVisualElementConstants.SVG_CIRCLE, + finalProps); + } + + createSvgLine(lineProps, elementKey) { + + /* Keep this commented code. Will be used again when + proper link construction is added + let finalProps = {}; + finalProps[NodeVisualElementConstants.CSS_CLASS] = lineProps.class; + finalProps = this.applySvgAttributes(finalProps, lineProps.svgAttributes); + finalProps = this.applyTransform(finalProps, lineProps.shapeAttributes); + */ + + let finalProps = { + ...lineProps, + key: elementKey + }; + + return React.createElement(NodeVisualElementConstants.SVG_LINE, finalProps); + } + + createTextElement(nodeProps, textProps, elementKey) { + let finalProps = {}; + finalProps[NodeVisualElementConstants.CSS_CLASS] = textProps.class; + + finalProps = this.applySvgAttributes(finalProps, textProps.svgAttributes); + finalProps = this.applyTransform(finalProps, textProps.shapeAttributes); + + finalProps = { + ...finalProps, + key: elementKey + }; + + return React.createElement(NodeVisualElementConstants.TEXT, finalProps, + nodeProps[textProps.displayKey]); + } + + createImageElement(imageProps, elementKey) { + let finalProps = {}; + finalProps[NodeVisualElementConstants.CSS_CLASS] = imageProps.class; + + finalProps = this.applyTransform(finalProps, imageProps.shapeAttributes); + finalProps = this.applySvgAttributes(finalProps, imageProps.svgAttributes); + + finalProps = { + ...finalProps, + key: elementKey + }; + + return React.createElement(NodeVisualElementConstants.IMAGE, finalProps); + } + + createObjectElement(objectProps, elementKey) { + let finalProps = {}; + finalProps[NodeVisualElementConstants.CSS_CLASS] = objectProps.class; + + finalProps = this.applyTransform(finalProps, objectProps.shapeAttributes); + finalProps = this.applySvgAttributes(finalProps, objectProps.svgAttributes); + + finalProps = { + ...finalProps, + key: elementKey + }; + + return React.createElement(NodeVisualElementConstants.OBJECT, finalProps); + } + + createButtonElement(buttonProps, elementKey, nodeMeta) { + return IconFactory.createIcon(buttonProps.name, buttonProps, elementKey, + nodeMeta); + } + + applySvgAttributes(elementProps, svgAttributes) { + if (svgAttributes) { + return { + ...elementProps, + ...svgAttributes + }; + } + return elementProps; + } + + applyTransform(elementProps, shapeAttributes) { + if (shapeAttributes) { + if (shapeAttributes.offset) { + return { + ...elementProps, + transform: `translate( + ${shapeAttributes.offset.x}, + ${shapeAttributes.offset.y})` + }; + } + } + return elementProps; + } +} + +export default NodeVisualElementFactory; diff --git a/src/generic-components/graph/SVGShape.jsx b/src/generic-components/graph/SVGShape.jsx new file mode 100644 index 0000000..7a3781c --- /dev/null +++ b/src/generic-components/graph/SVGShape.jsx @@ -0,0 +1,65 @@ +/* + * ============LICENSE_START=================================================== + * SPARKY (AAI UI service) + * ============================================================================ + * Copyright © 2017 AT&T Intellectual Property. + * Copyright © 2017 Amdocs + * All rights reserved. + * ============================================================================ + * 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. + * ============LICENSE_END===================================================== + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + */ + +import React, {Component} from 'react'; +import NodeVisualElementConstants from './NodeVisualElementConstants'; + +class SVGShape extends Component { + + static propTypes = { + shapeType: React.PropTypes.string.isRequired, + shapeAttributes: React.PropTypes.object.isRequired, + shapeClass: React.PropTypes.object.isRequired, + textValue: React.PropTypes.string + }; + + static defaultProps = { + shapeType: '', + shapeAttributes: {}, + shapeClass: {}, + textValue: '' + }; + + render() { + let {shapeType, shapeAttributes, shapeClass, textValue} = this.props; + + switch (shapeType) { + case NodeVisualElementConstants.SVG_CIRCLE: + return <circle {...shapeAttributes} className={shapeClass}/>; + + case NodeVisualElementConstants.SVG_LINELINE: + return <line {...shapeAttributes} className={shapeClass}/>; + + case NodeVisualElementConstants.TEXT: + return <text {...shapeAttributes} + className={shapeClass}>{textValue}</text>; + + default: + return undefined; + } + } +} + +export default SVGShape; |