Upgrade Eslint & Prettier
[sdc/sdc-workflow-designer.git] / workflow-designer-ui / src / main / frontend / src / shared / tree / Tree.jsx
1 /*
2 * Copyright © 2018 European Support Limited
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 import React, { Component } from 'react';
18 import PropTypes from 'prop-types';
19 import { select } from 'd3-selection';
20 import { tree, stratify } from 'd3-hierarchy';
21 function diagonal(d) {
22     const offset = 50;
23     return (
24         'M' +
25         d.y +
26         ',' +
27         d.x +
28         'C' +
29         (d.parent.y + offset) +
30         ',' +
31         d.x +
32         ' ' +
33         (d.parent.y + offset) +
34         ',' +
35         d.parent.x +
36         ' ' +
37         d.parent.y +
38         ',' +
39         d.parent.x
40     );
41 }
42
43 const nodeRadius = 8;
44 const verticalSpaceBetweenNodes = 70;
45 const NARROW_HORIZONTAL_SPACES = 47;
46 const WIDE_HORIZONTAL_SPACES = 65;
47
48 const stratifyFn = stratify()
49     .id(d => d.id)
50     .parentId(d => d.parent);
51
52 class Tree extends Component {
53     static propTypes = {
54         name: PropTypes.string,
55         width: PropTypes.number,
56         allowScaleWidth: PropTypes.bool,
57         nodes: PropTypes.arrayOf(
58             PropTypes.shape({
59                 id: PropTypes.string,
60                 name: PropTypes.string,
61                 parent: PropTypes.string
62             })
63         ),
64         selectedNodeId: PropTypes.string,
65         onNodeClick: PropTypes.func,
66         onRenderedBeyondWidth: PropTypes.func,
67         scrollable: PropTypes.bool,
68         toWiden: PropTypes.bool
69     };
70
71     static defaultProps = {
72         width: 500,
73         allowScaleWidth: true,
74         name: 'default-name'
75     };
76
77     render() {
78         let { width, name, scrollable = false } = this.props;
79         return (
80             <div
81                 className={`tree-view ${name}-container ${
82                     scrollable ? 'scrollable' : ''
83                 }`}>
84                 <svg width={width} className={name} />
85             </div>
86         );
87     }
88
89     componentDidMount() {
90         this.renderTree();
91     }
92
93     componentDidUpdate(prevProps) {
94         if (
95             this.props.nodes.length !== prevProps.nodes.length ||
96             this.props.selectedNodeId !== prevProps.selectedNodeId
97         ) {
98             this.renderTree();
99         }
100     }
101
102     renderTree() {
103         let {
104             width,
105             nodes,
106             name,
107             allowScaleWidth,
108             selectedNodeId,
109             onRenderedBeyondWidth,
110             toWiden
111         } = this.props;
112         if (nodes.length > 0) {
113             let horizontalSpaceBetweenLeaves = toWiden
114                 ? WIDE_HORIZONTAL_SPACES
115                 : NARROW_HORIZONTAL_SPACES;
116             const treeFn = tree().nodeSize([
117                 horizontalSpaceBetweenLeaves,
118                 verticalSpaceBetweenNodes
119             ]); //.size([width - 50, height - 50])
120             let root = stratifyFn(nodes).sort((a, b) =>
121                 a.data.name.localeCompare(b.data.name)
122             );
123             let svgHeight =
124                 verticalSpaceBetweenNodes * root.height + nodeRadius * 6;
125
126             treeFn(root);
127
128             let nodesXValue = root.descendants().map(node => node.x);
129             let maxX = Math.max(...nodesXValue);
130             let minX = Math.min(...nodesXValue);
131
132             let svgTempWidth =
133                 ((maxX - minX) / 30) * horizontalSpaceBetweenLeaves;
134             let svgWidth = svgTempWidth < width ? width - 5 : svgTempWidth;
135             const svgEL = select(`svg.${name}`);
136             const container = select(`.tree-view.${name}-container`);
137             svgEL.html('');
138             svgEL.attr('height', svgHeight);
139             let canvasWidth = width;
140             if (svgTempWidth > width) {
141                 if (allowScaleWidth) {
142                     canvasWidth = svgTempWidth;
143                 }
144                 // we seems to have a margin of 25px that we can still see with text
145                 if (
146                     svgTempWidth - 25 > width &&
147                     onRenderedBeyondWidth !== undefined
148                 ) {
149                     onRenderedBeyondWidth();
150                 }
151             }
152             svgEL.attr('width', canvasWidth);
153             let rootGroup = svgEL
154                 .append('g')
155                 .attr(
156                     'transform',
157                     `translate(${svgWidth / 2 + nodeRadius},${nodeRadius *
158                         4}) rotate(90)`
159                 );
160
161             // handle link
162             rootGroup
163                 .selectAll('.link')
164                 .data(root.descendants().slice(1))
165                 .enter()
166                 .append('path')
167                 .attr('class', 'link')
168                 .attr('d', diagonal);
169
170             let node = rootGroup
171                 .selectAll('.node')
172                 .data(root.descendants())
173                 .enter()
174                 .append('g')
175                 .attr(
176                     'class',
177                     node =>
178                         `node ${node.children ? ' has-children' : ' leaf'} ${
179                             node.id === selectedNodeId ? 'selectedNode' : ''
180                         } ${this.props.onNodeClick ? 'clickable' : ''}`
181                 )
182                 .attr(
183                     'transform',
184                     node => 'translate(' + node.y + ',' + node.x + ')'
185                 )
186                 .on('click', node => this.onNodeClick(node));
187
188             node.append('circle')
189                 .attr('r', nodeRadius)
190                 .attr('class', 'outer-circle');
191             node.append('circle')
192                 .attr('r', nodeRadius - 3)
193                 .attr('class', 'inner-circle');
194
195             node.append('text')
196                 .attr('y', nodeRadius / 4 + 1)
197                 .attr('x', -nodeRadius * 1.8)
198                 .text(node => node.data.name)
199                 .attr('transform', 'rotate(-90)');
200
201             let selectedNode = selectedNodeId
202                 ? root.descendants().find(node => node.id === selectedNodeId)
203                 : null;
204             if (selectedNode) {
205                 container.property(
206                     'scrollLeft',
207                     svgWidth / 4 +
208                         (svgWidth / 4 - 100) -
209                         (selectedNode.x / 30) * horizontalSpaceBetweenLeaves
210                 );
211                 container.property(
212                     'scrollTop',
213                     (selectedNode.y / 100) * verticalSpaceBetweenNodes
214                 );
215             } else {
216                 container.property(
217                     'scrollLeft',
218                     svgWidth / 4 + (svgWidth / 4 - 100)
219                 );
220             }
221         }
222     }
223
224     onNodeClick(node) {
225         if (this.props.onNodeClick) {
226             this.props.onNodeClick(node.data);
227         }
228     }
229 }
230
231 export default Tree;