| 1 | import React from 'react'; |
| 2 | import {Motion, spring} from 'react-motion'; |
| 3 | import dagre from 'dagre'; |
| 4 | // import prettyFormat from 'pretty-format'; |
| 5 | // import reactElement from 'pretty-format/plugins/ReactElement'; |
| 6 | |
| 7 | function getFiberColor(fibers, id) { |
| 8 | if (fibers.currentIDs.indexOf(id) > -1) { |
| 9 | return 'lightgreen'; |
| 10 | } |
| 11 | if (id === fibers.workInProgressID) { |
| 12 | return 'yellow'; |
| 13 | } |
| 14 | return 'lightyellow'; |
| 15 | } |
| 16 | |
| 17 | function Graph(props) { |
| 18 | const {rankdir, trackActive} = props.settings; |
| 19 | var g = new dagre.graphlib.Graph(); |
| 20 | g.setGraph({ |
| 21 | width: 1000, |
| 22 | height: 1000, |
| 23 | nodesep: 50, |
| 24 | edgesep: 150, |
| 25 | ranksep: 100, |
| 26 | marginx: 100, |
| 27 | marginy: 100, |
| 28 | rankdir, |
| 29 | }); |
| 30 | |
| 31 | var edgeLabels = {}; |
| 32 | React.Children.forEach(props.children, function (child) { |
| 33 | if (!child) { |
| 34 | return; |
| 35 | } |
| 36 | if (child.type.isVertex) { |
| 37 | g.setNode(child.key, { |
| 38 | label: child, |
| 39 | width: child.props.width, |
| 40 | height: child.props.height, |
| 41 | }); |
| 42 | } else if (child.type.isEdge) { |
| 43 | const relationshipKey = child.props.source + ':' + child.props.target; |
| 44 | if (!edgeLabels[relationshipKey]) { |
| 45 | edgeLabels[relationshipKey] = []; |
| 46 | } |
| 47 | edgeLabels[relationshipKey].push(child); |
| 48 | } |
| 49 | }); |
| 50 | |
| 51 | Object.keys(edgeLabels).forEach(key => { |
| 52 | const children = edgeLabels[key]; |
| 53 | const child = children[0]; |
| 54 | g.setEdge(child.props.source, child.props.target, { |
| 55 | label: child, |
| 56 | allChildren: children.map(c => c.props.children), |
| 57 | weight: child.props.weight, |
| 58 | }); |
| 59 | }); |
| 60 | |
| 61 | dagre.layout(g); |
| 62 | |
| 63 | var activeNode = g |
| 64 | .nodes() |
| 65 | .map(v => g.node(v)) |
| 66 | .find(node => node.label.props.isActive); |
| 67 | const [winX, winY] = [window.innerWidth / 2, window.innerHeight / 2]; |
| 68 | var focusDx = trackActive && activeNode ? winX - activeNode.x : 0; |
| 69 | var focusDy = trackActive && activeNode ? winY - activeNode.y : 0; |
| 70 | |
| 71 | var nodes = g.nodes().map(v => { |
| 72 | var node = g.node(v); |
| 73 | return ( |
| 74 | <Motion |
| 75 | style={{ |
| 76 | x: props.isDragging ? node.x + focusDx : spring(node.x + focusDx), |
| 77 | y: props.isDragging ? node.y + focusDy : spring(node.y + focusDy), |
| 78 | }} |
| 79 | key={node.label.key}> |
| 80 | {interpolatingStyle => |
| 81 | React.cloneElement(node.label, { |
| 82 | x: interpolatingStyle.x + props.dx, |
| 83 | y: interpolatingStyle.y + props.dy, |
| 84 | vanillaX: node.x, |
| 85 | vanillaY: node.y, |
| 86 | }) |
| 87 | } |
| 88 | </Motion> |
| 89 | ); |
| 90 | }); |
| 91 | |
| 92 | var edges = g.edges().map(e => { |
| 93 | var edge = g.edge(e); |
| 94 | let idx = 0; |
| 95 | return ( |
| 96 | <Motion |
| 97 | style={edge.points.reduce((bag, point) => { |
| 98 | bag[idx + ':x'] = props.isDragging |
| 99 | ? point.x + focusDx |
| 100 | : spring(point.x + focusDx); |
| 101 | bag[idx + ':y'] = props.isDragging |
| 102 | ? point.y + focusDy |
| 103 | : spring(point.y + focusDy); |
| 104 | idx++; |
| 105 | return bag; |
| 106 | }, {})} |
| 107 | key={edge.label.key}> |
| 108 | {interpolatedStyle => { |
| 109 | let points = []; |
| 110 | Object.keys(interpolatedStyle).forEach(key => { |
| 111 | const [idx, prop] = key.split(':'); |
| 112 | if (!points[idx]) { |
| 113 | points[idx] = {x: props.dx, y: props.dy}; |
| 114 | } |
| 115 | points[idx][prop] += interpolatedStyle[key]; |
| 116 | }); |
| 117 | return React.cloneElement(edge.label, { |
| 118 | points, |
| 119 | id: edge.label.key, |
| 120 | children: edge.allChildren.join(', '), |
| 121 | }); |
| 122 | }} |
| 123 | </Motion> |
| 124 | ); |
| 125 | }); |
| 126 | |
| 127 | return ( |
| 128 | <div |
| 129 | style={{ |
| 130 | position: 'relative', |
| 131 | height: '100%', |
| 132 | }}> |
| 133 | {edges} |
| 134 | {nodes} |
| 135 | </div> |
| 136 | ); |
| 137 | } |
| 138 | |
| 139 | function Vertex(props) { |
| 140 | if (Number.isNaN(props.x) || Number.isNaN(props.y)) { |
| 141 | return null; |
| 142 | } |
| 143 | |
| 144 | return ( |
| 145 | <div |
| 146 | style={{ |
| 147 | position: 'absolute', |
| 148 | border: '1px solid black', |
| 149 | left: props.x - props.width / 2, |
| 150 | top: props.y - props.height / 2, |
| 151 | width: props.width, |
| 152 | height: props.height, |
| 153 | overflow: 'hidden', |
| 154 | padding: '4px', |
| 155 | wordWrap: 'break-word', |
| 156 | }}> |
| 157 | {props.children} |
| 158 | </div> |
| 159 | ); |
| 160 | } |
| 161 | Vertex.isVertex = true; |
| 162 | |
| 163 | const strokes = { |
| 164 | alt: 'blue', |
| 165 | child: 'green', |
| 166 | sibling: 'darkgreen', |
| 167 | return: 'red', |
| 168 | fx: 'purple', |
| 169 | }; |
| 170 | |
| 171 | function Edge(props) { |
| 172 | var points = props.points; |
| 173 | var path = 'M' + points[0].x + ' ' + points[0].y + ' '; |
| 174 | |
| 175 | if (!points[0].x || !points[0].y) { |
| 176 | return null; |
| 177 | } |
| 178 | |
| 179 | for (var i = 1; i < points.length; i++) { |
| 180 | path += 'L' + points[i].x + ' ' + points[i].y + ' '; |
| 181 | if (!points[i].x || !points[i].y) { |
| 182 | return null; |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | var lineID = props.id; |
| 187 | |
| 188 | return ( |
| 189 | <svg |
| 190 | width="100%" |
| 191 | height="100%" |
| 192 | style={{ |
| 193 | position: 'absolute', |
| 194 | left: 0, |
| 195 | right: 0, |
| 196 | top: 0, |
| 197 | bottom: 0, |
| 198 | }}> |
| 199 | <defs> |
| 200 | <path d={path} id={lineID} /> |
| 201 | <marker |
| 202 | id="markerCircle" |
| 203 | markerWidth="8" |
| 204 | markerHeight="8" |
| 205 | refX="5" |
| 206 | refY="5"> |
| 207 | <circle cx="5" cy="5" r="3" style={{stroke: 'none', fill: 'black'}} /> |
| 208 | </marker> |
| 209 | <marker |
| 210 | id="markerArrow" |
| 211 | markerWidth="13" |
| 212 | markerHeight="13" |
| 213 | refX="2" |
| 214 | refY="6" |
| 215 | orient="auto"> |
| 216 | <path d="M2,2 L2,11 L10,6 L2,2" style={{fill: 'black'}} /> |
| 217 | </marker> |
| 218 | </defs> |
| 219 | |
| 220 | <use |
| 221 | xlinkHref={`#${lineID}`} |
| 222 | fill="none" |
| 223 | stroke={strokes[props.kind]} |
| 224 | style={{ |
| 225 | markerStart: 'url(#markerCircle)', |
| 226 | markerEnd: 'url(#markerArrow)', |
| 227 | }} |
| 228 | /> |
| 229 | <text> |
| 230 | <textPath xlinkHref={`#${lineID}`}> |
| 231 | {' '} |
| 232 | {props.children} |
| 233 | </textPath> |
| 234 | </text> |
| 235 | </svg> |
| 236 | ); |
| 237 | } |
| 238 | Edge.isEdge = true; |
| 239 | |
| 240 | function formatPriority(priority) { |
| 241 | switch (priority) { |
| 242 | case 1: |
| 243 | return 'synchronous'; |
| 244 | case 2: |
| 245 | return 'task'; |
| 246 | case 3: |
| 247 | return 'hi-pri work'; |
| 248 | case 4: |
| 249 | return 'lo-pri work'; |
| 250 | case 5: |
| 251 | return 'offscreen work'; |
| 252 | default: |
| 253 | throw new Error('Unknown priority.'); |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | export default function Fibers({fibers, show, graphSettings, ...rest}) { |
| 258 | const items = Object.keys(fibers.descriptions).map( |
| 259 | id => fibers.descriptions[id] |
| 260 | ); |
| 261 | |
| 262 | const isDragging = rest.className.indexOf('dragging') > -1; |
| 263 | const [_, sdx, sdy] = |
| 264 | rest.style.transform.match(/translate\((-?\d+)px,(-?\d+)px\)/) || []; |
| 265 | const dx = Number(sdx); |
| 266 | const dy = Number(sdy); |
| 267 | |
| 268 | return ( |
| 269 | <div |
| 270 | {...rest} |
| 271 | style={{ |
| 272 | width: '100%', |
| 273 | height: '100%', |
| 274 | position: 'absolute', |
| 275 | top: 0, |
| 276 | left: 0, |
| 277 | ...rest.style, |
| 278 | transform: null, |
| 279 | }}> |
| 280 | <Graph |
| 281 | className="graph" |
| 282 | dx={dx} |
| 283 | dy={dy} |
| 284 | isDragging={isDragging} |
| 285 | settings={graphSettings}> |
| 286 | {items.map(fiber => [ |
| 287 | <Vertex |
| 288 | key={fiber.id} |
| 289 | width={150} |
| 290 | height={100} |
| 291 | isActive={fiber.id === fibers.workInProgressID}> |
| 292 | <div |
| 293 | style={{ |
| 294 | width: '100%', |
| 295 | height: '100%', |
| 296 | backgroundColor: getFiberColor(fibers, fiber.id), |
| 297 | }} |
| 298 | title={ |
| 299 | /*prettyFormat(fiber, { plugins: [reactElement ]})*/ |
| 300 | 'todo: this was hanging last time I tried to pretty print' |
| 301 | }> |
| 302 | <small> |
| 303 | {fiber.tag} #{fiber.id} |
| 304 | </small> |
| 305 | <br /> |
| 306 | {fiber.type} |
| 307 | <br /> |
| 308 | {fibers.currentIDs.indexOf(fiber.id) === -1 ? ( |
| 309 | <small> |
| 310 | {fiber.pendingWorkPriority !== 0 && [ |
| 311 | <span key="span"> |
| 312 | Needs: {formatPriority(fiber.pendingWorkPriority)} |
| 313 | </span>, |
| 314 | <br key="br" />, |
| 315 | ]} |
| 316 | {fiber.memoizedProps !== null && |
| 317 | fiber.pendingProps !== null && [ |
| 318 | fiber.memoizedProps === fiber.pendingProps |
| 319 | ? 'Can reuse memoized.' |
| 320 | : 'Cannot reuse memoized.', |
| 321 | <br key="br" />, |
| 322 | ]} |
| 323 | </small> |
| 324 | ) : ( |
| 325 | <small>Committed</small> |
| 326 | )} |
| 327 | {fiber.flags && [ |
| 328 | <br key="br" />, |
| 329 | <small key="small">Effect: {fiber.flags}</small>, |
| 330 | ]} |
| 331 | </div> |
| 332 | </Vertex>, |
| 333 | fiber.child && show.child && ( |
| 334 | <Edge |
| 335 | source={fiber.id} |
| 336 | target={fiber.child} |
| 337 | kind="child" |
| 338 | weight={1000} |
| 339 | key={`${fiber.id}-${fiber.child}-child`}> |
| 340 | child |
| 341 | </Edge> |
| 342 | ), |
| 343 | fiber.sibling && show.sibling && ( |
| 344 | <Edge |
| 345 | source={fiber.id} |
| 346 | target={fiber.sibling} |
| 347 | kind="sibling" |
| 348 | weight={2000} |
| 349 | key={`${fiber.id}-${fiber.sibling}-sibling`}> |
| 350 | sibling |
| 351 | </Edge> |
| 352 | ), |
| 353 | fiber.return && show.return && ( |
| 354 | <Edge |
| 355 | source={fiber.id} |
| 356 | target={fiber.return} |
| 357 | kind="return" |
| 358 | weight={1000} |
| 359 | key={`${fiber.id}-${fiber.return}-return`}> |
| 360 | return |
| 361 | </Edge> |
| 362 | ), |
| 363 | fiber.nextEffect && show.fx && ( |
| 364 | <Edge |
| 365 | source={fiber.id} |
| 366 | target={fiber.nextEffect} |
| 367 | kind="fx" |
| 368 | weight={100} |
| 369 | key={`${fiber.id}-${fiber.nextEffect}-nextEffect`}> |
| 370 | nextFx |
| 371 | </Edge> |
| 372 | ), |
| 373 | fiber.firstEffect && show.fx && ( |
| 374 | <Edge |
| 375 | source={fiber.id} |
| 376 | target={fiber.firstEffect} |
| 377 | kind="fx" |
| 378 | weight={100} |
| 379 | key={`${fiber.id}-${fiber.firstEffect}-firstEffect`}> |
| 380 | firstFx |
| 381 | </Edge> |
| 382 | ), |
| 383 | fiber.lastEffect && show.fx && ( |
| 384 | <Edge |
| 385 | source={fiber.id} |
| 386 | target={fiber.lastEffect} |
| 387 | kind="fx" |
| 388 | weight={100} |
| 389 | key={`${fiber.id}-${fiber.lastEffect}-lastEffect`}> |
| 390 | lastFx |
| 391 | </Edge> |
| 392 | ), |
| 393 | fiber.alternate && show.alt && ( |
| 394 | <Edge |
| 395 | source={fiber.id} |
| 396 | target={fiber.alternate} |
| 397 | kind="alt" |
| 398 | weight={10} |
| 399 | key={`${fiber.id}-${fiber.alternate}-alt`}> |
| 400 | alt |
| 401 | </Edge> |
| 402 | ), |
| 403 | ])} |
| 404 | </Graph> |
| 405 | </div> |
| 406 | ); |
| 407 | } |