Feature: 'Update the span display' (#210)
authorAllen Wang <Allen.Wang.123@outlook.com>
Thu, 20 Dec 2018 05:45:35 +0000 (13:45 +0800)
committer吴晟 Wu Sheng <wu.sheng@foxmail.com>
Thu, 20 Dec 2018 05:45:35 +0000 (13:45 +0800)
1. Why i update the d3 version 4.12.2 => 5.7.0.
The d3 version 4.12.2 has a d3.event bug. So it cause the event cannot be injected and then the zoom drag event cannot be used.

2. The tree display deficiencies: it cannot show the break traces, because the break trace cannot turn into a right tree struction,so i also hold back the list span display.

package.json
src/components/TraceStack/index.js
src/components/TraceTree/d3-trace.js [new file with mode: 0644]
src/components/TraceTree/index.js [new file with mode: 0644]
src/components/TraceTree/style.less [new file with mode: 0644]

index 764679b..d080282 100755 (executable)
@@ -42,7 +42,7 @@
     "cytoscape-canvas": "^3.0.1",
     "cytoscape-cose-bilkent": "^4.0.0",
     "cytoscape-dagre": "^2.2.1",
-    "d3": "^4.12.2",
+    "d3": "^5.7.0",
     "dva": "^2.2.3",
     "dva-loading": "^2.0.3",
     "enquire-js": "^0.2.1",
index 53ecfbb..d5974b6 100644 (file)
@@ -23,9 +23,11 @@ import moment from 'moment';
 import { formatDuration } from '../../utils/time';
 import DescriptionList from "../DescriptionList";
 import styles from './index.less';
+import TraceTree from '../TraceTree';
 
-const { Description } = DescriptionList;
+const ButtonGroup = Button.Group;
 
+const { Description } = DescriptionList;
 const height = 36;
 const margin = 10;
 const offX = 15;
@@ -39,6 +41,7 @@ class TraceStack extends PureComponent {
     bap: [],
     span: {},
     key: 'tags',
+    treeMode: true,
   }
 
   componentWillMount() {
@@ -329,12 +332,14 @@ class TraceStack extends PureComponent {
   }
 
   render() {
+    const { spans } = this.props;
     const { colorMap, span = {}, position = { width: 100, top: 0 } } = this.state;
     const legendButtons = Object.keys(colorMap).map(key =>
       (<Tag color={colorMap[key]} key={key}>{key}</Tag>));
     const tabList = [];
     const contentList = {};
     if (span.content) {
+      
       tabList.push({
         key: 'tags',
         tab: 'Tags',
@@ -422,10 +427,20 @@ class TraceStack extends PureComponent {
     return (
       <div className={styles.stack}>
         <div style={{ paddingBottom: 10 }}>
+          <ButtonGroup>
+            <Button type={stateData.treeMode ? "primary": ""} onClick={() => this.setState({treeMode:true})}>TreeMode</Button>
+            <Button type={stateData.treeMode ? "": "primary"} onClick={() => this.setState({treeMode: false})}>ListMode</Button>
+          </ButtonGroup>
+        </div>
+       
+        <div style={{ paddingBottom: 10 }}>
           { legendButtons }
         </div>
-        <div className={styles.duration} ref={(el) => { this.duration = el; }} />
-        <div ref={(el) => { this.axis = el; }} />
+        <div style={{display: stateData.treeMode?'none':'block'}} className={styles.duration} ref={(el) => { this.duration = el; }} />
+        <div style={{display: stateData.treeMode?'none':'block'}} ref={(el) => { this.axis = el; }} />
+        <div style={{display: stateData.treeMode?'block':'none'}}>
+          <TraceTree showSpanModal={this.showSpanModal} data={spans} id="" />
+        </div>
         {tabList.length > 0 ? (
           <Card
             type="inner"
diff --git a/src/components/TraceTree/d3-trace.js b/src/components/TraceTree/d3-trace.js
new file mode 100644 (file)
index 0000000..3d8a709
--- /dev/null
@@ -0,0 +1,401 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/* eslint-disable */
+import * as d3 from 'd3';
+
+export default class TraceMap {
+  constructor(el) {
+    this.type = {
+      MQ: '#bf99f8',
+      Http: '#72a5fd',
+      Database: '#ff6732',
+      Unknown: '#ffc107',
+      Cache: '#00bcd4',
+      RPCFramework: '#ee4395',
+    };
+    this.el = el;
+    this.width = el.clientWidth;
+    this.height = el.clientHeight;
+    this.treemap = d3.tree().size([this.height * 0.7, this.width]);
+    this.svg = '';
+    this.timeGroup = '';
+    this.root = '';
+    this.i = 0;
+    this.j = 0;
+  }
+  resize() {
+    d3.select(this.el)
+      .select('svg')
+      .remove();
+    this.width = this.el.clientWidth;
+    this.height = this.el.clientHeight;
+    this.draw(this.data,this.row,this.showSpanModal);
+  }
+  draw(data, row, showSpanModal) {
+    this.showSpanModal = showSpanModal;
+    this.row = row;
+    this.data = data;
+    this.min = d3.min(this.row.map(i => i.startTime));
+    this.max = d3.max(this.row.map(i => i.endTime - this.min));
+    this.list = Array.from(new Set(this.row.map(i => i.serviceCode)));
+    this.sequentialScale = d3
+      .scaleSequential()
+      .domain([0, this.list.length])
+      .interpolator(d3.interpolateCool);
+    this.xScale = d3
+      .scaleLinear()
+      .range([0, this.width - 10])
+      .domain([0, this.max]);
+    this.xAxis = d3.axisTop(this.xScale).tickFormat(d => {
+      if (d === 0) return 0;
+      if (d >= 1000) return d / 1000 + 's';
+      return d + ' ms';
+    });
+
+    this.body = d3
+      .select(this.el)
+      .append('svg')
+      .attr('width', this.width)
+      .attr('height', this.height);
+    this.timeGroup = this.body.append('g').attr('transform', d => 'translate(5,30)');
+    const main = this.body
+      .append('g')
+      .attr('transform', d => 'translate(0,' + this.row.length * 9 + ')');
+    this.svg = main.append('g');
+    this.root = d3.hierarchy(this.data, d => d.children);
+    this.root.x0 = this.height / 2;
+    this.root.y0 = 0;
+    this.body
+      .append('g')
+      .attr('transform', `translate(5,20)`)
+      .call(this.xAxis);
+    this.update(this.root);
+  }
+  update(source) {
+    const treeData = this.treemap(this.root);
+    const nodes = treeData.descendants(),
+      links = treeData.descendants().slice(1);
+    let index = -1;
+    nodes.forEach(function(d) {
+      d.y = d.depth * 200;
+      d.timeX = ++index * 7;
+    });
+
+    this.body.call(this.getZoomBehavior(this.svg));
+
+    const node = this.svg.selectAll('g.node').data(nodes, d => {
+      return d.id || (d.id = ++this.i);
+    });
+    const timeNode = this.timeGroup.selectAll('g.time').data(nodes, d => {
+      return d.id || (d.id = ++this.j);
+    });
+
+    // time
+    const timeEnter = timeNode
+      .enter()
+      .append('g')
+      .attr('class', 'time')
+      .attr('transform', d => 'translate(' + 0 + ',' + d.timeX + ')');
+    timeEnter
+      .append('rect')
+      .attr('height', 5)
+      .attr('width', d => {
+        if (!d.data.endTime || !d.data.startTime) return 0;
+        return this.xScale(d.data.endTime - d.data.startTime) + 1;
+      })
+      .attr('rx', 2)
+      .attr('ry', 2)
+      .attr(
+        'x',
+        d => (!d.data.endTime || !d.data.startTime ? 0 : this.xScale(d.data.startTime - this.min))
+      )
+      .attr('y', -3)
+      .style('fill', d => `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`);
+    var timeUpdate = timeEnter.merge(timeNode);
+
+    timeUpdate
+      .transition()
+      .duration(600)
+      .attr('transform', function(d) {
+        return 'translate(' + 0 + ',' + d.timeX + ')';
+      });
+    const timeExit = timeNode
+      .exit()
+      .transition()
+      .duration(600)
+      .attr('transform', function(d) {
+        return 'translate(' + 0 + ',' + 8 + ')';
+      })
+      .remove();
+    // node
+    const nodeEnter = node
+      .enter()
+      .append('g')
+      .attr('class', 'node')
+      .attr('transform', function(d) {
+        return 'translate(' + source.y0 + ',' + source.x0 + ')';
+      });
+    nodeEnter
+      .append('rect')
+      .attr('class', 'block')
+      .attr('x', '0')
+      .attr('y', '-16')
+      .attr('rx', 4)
+      .attr('ry', 4)
+      .attr('fill', d => (d.data.isError ? '#ff57221a' : '#f7f7f7'))
+      .attr('stroke', d => (d.data.isError ? '#ff5722aa' : '#e4e4e4'))
+      .on('click', (d, i) => {
+        this.showSpanModal(
+          d.data,
+          { width: '100%', top: -10, left: '0' },
+          d3.select(nodeEnter._groups[0][i]).append('rect')
+        );
+      })
+      .on('mouseenter', function(currNode, i) {
+        d3.select(nodeEnter._groups[0][i])
+          .select('g')
+          .attr('opacity', 1);
+      })
+      .on('mouseleave', function(currNode, i) {
+        d3.select(nodeEnter._groups[0][i])
+          .select('g')
+          .attr('opacity', 0);
+      });
+
+    const tooltip = nodeEnter
+      .append('g')
+      .attr('opacity', 0)
+      .attr('transform', function(d) {
+        return 'translate(0,-40)';
+      });
+
+    tooltip
+      .append('rect')
+      .attr('class', 'tooltip-box')
+      .attr('rx', 4)
+      .attr('ry', 4)
+      .attr('width', function(d) {
+        return d.data.label.length * 6 + 20;
+      });
+    tooltip
+      .append('text')
+      .attr('dy', 14)
+      .attr('fill', '#fafafa')
+      .attr('dx', 10)
+      .text(function(d) {
+        return d.data.label;
+      });
+    nodeEnter
+      .append('text')
+      .attr('dy', -4)
+      .attr('x', 5)
+      .attr('text-anchor', function(d) {
+        return 'start';
+      })
+      .text(function(d) {
+        return d.data.label.length > 23 ? d.data.label.slice(0, 23) : d.data.label;
+      })
+      .on('click', (d, i) => {
+        this.showSpanModal(
+          d.data,
+          { width: '100%', top: -10, left: '0' },
+          d3.select(nodeEnter._groups[0][i]).append('rect')
+        );
+        d3.event.stopPropagation();
+      })
+      .on('mouseenter', function(currNode, i) {
+        d3.select(nodeEnter._groups[0][i])
+          .select('g')
+          .attr('opacity', 1);
+      })
+      .on('mouseleave', function(currNode, i) {
+        d3.select(nodeEnter._groups[0][i])
+          .select('g')
+          .attr('opacity', 0);
+      });
+
+    nodeEnter
+      .append('text')
+      .attr('dy', 12)
+      .attr('x', 8)
+      .attr('text-anchor', function(d) {
+        return 'start';
+      })
+      .attr('fill', d => {
+        return this.type[d.data.layer];
+      })
+      .attr('stroke', d => {
+        return this.type[d.data.layer];
+      })
+      .text(function(d) {
+        return d.data.layer;
+      })
+      .on('click', (d, i) => {
+        this.showSpanModal(
+          d.data,
+          { width: '100%', top: -10, left: '0' },
+          d3.select(nodeEnter._groups[0][i]).append('rect')
+        );
+        d3.event.stopPropagation();
+      })
+      .on('mouseenter', function(currNode, i) {
+        d3.select(nodeEnter._groups[0][i])
+          .select('g')
+          .attr('opacity', 1);
+      })
+      .on('mouseleave', function(currNode, i) {
+        d3.select(nodeEnter._groups[0][i])
+          .select('g')
+          .attr('opacity', 0);
+      });
+
+    nodeEnter
+      .append('text')
+      .attr('dy', 12)
+      .attr('x', 70)
+      .attr('text-anchor', function(d) {
+        return 'start';
+      })
+      .text(function(d) {
+        return d.data.endTime ? d.data.endTime - d.data.startTime + ' ms' : d.data.traceId;
+      })
+      .on('click', (d, i) => {
+        this.showSpanModal(
+          d.data,
+          { width: '100%', top: -10, left: '0' },
+          d3.select(nodeEnter._groups[0][i]).append('rect')
+        );
+        d3.event.stopPropagation();
+      })
+      .on('mouseenter', function(currNode, i) {
+        d3.select(nodeEnter._groups[0][i])
+          .select('g')
+          .attr('opacity', 1);
+      })
+      .on('mouseleave', function(currNode, i) {
+        d3.select(nodeEnter._groups[0][i])
+          .select('g')
+          .attr('opacity', 0);
+      });
+
+    nodeEnter
+      .append('circle')
+      .attr('class', 'node')
+      .attr('r', 4)
+      .attr('cx', '150')
+      .style('fill', function(d) {
+        return d._children ? '#8543e0aa' : '#fff';
+      })
+      .on('click', click);
+
+    var nodeUpdate = nodeEnter.merge(node);
+
+    nodeUpdate
+      .transition()
+      .duration(600)
+      .attr('transform', function(d) {
+        return 'translate(' + d.y + ',' + d.x + ')';
+      });
+
+    nodeUpdate
+      .select('circle.node')
+      .attr('r', 4)
+      .attr('cx', '156')
+      .style('fill', function(d) {
+        return d._children ? '#8543e0aa' : '#fff';
+      })
+      .attr('cursor', 'pointer');
+
+    var nodeExit = node
+      .exit()
+      .transition()
+      .duration(600)
+      .attr('transform', function(d) {
+        return 'translate(' + source.y + ',' + source.x + ')';
+      })
+      .remove();
+
+    nodeExit.select('circle').attr('r', 0);
+
+    nodeExit.select('text').style('fill-opacity', 0);
+
+    const link = this.svg.selectAll('path.link').data(links, function(d) {
+      return d.id;
+    });
+
+    const linkEnter = link
+      .enter()
+      .insert('path', 'g')
+      .attr('class', 'link')
+      .attr('d', function(d) {
+        const o = { x: source.x0, y: source.y0 };
+        return diagonal(o, o);
+      });
+
+    const linkUpdate = linkEnter.merge(link);
+
+    linkUpdate
+      .transition()
+      .duration(600)
+      .attr('d', function(d) {
+        return diagonal(d, d.parent);
+      });
+
+    link
+      .exit()
+      .transition()
+      .duration(600)
+      .attr('d', function(d) {
+        var o = { x: source.x, y: source.y };
+        return diagonal(o, o);
+      })
+      .remove();
+
+    nodes.forEach(function(d) {
+      d.x0 = d.x;
+      d.y0 = d.y;
+    });
+
+    function diagonal(s, d) {
+      return `M ${s.y} ${s.x}
+      L  ${d.y + 158} ${d.x}`;
+    }
+    const that = this;
+    function click(d) {
+      if (d.children) {
+        d._children = d.children;
+        d.children = null;
+      } else {
+        d.children = d._children;
+        d._children = null;
+      }
+      that.update(d);
+    }
+  }
+  getZoomBehavior(g) {
+    return d3
+      .zoom()
+      .scaleExtent([0.3, 10])
+      .on('zoom', () => {
+        g.attr(
+          'transform',
+          `translate(${d3.event.transform.x},${d3.event.transform.y})scale(${d3.event.transform.k})`
+        );
+      });
+  }
+}
diff --git a/src/components/TraceTree/index.js b/src/components/TraceTree/index.js
new file mode 100644 (file)
index 0000000..2a495c5
--- /dev/null
@@ -0,0 +1,116 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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, { Component } from 'react';
+import './style.less';
+import Tree from './d3-trace';
+
+export default class Trace extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {}
+  }
+
+  componentDidMount() {
+    this.changeTree();
+    window.addEventListener('resize', this.resize);
+  }
+
+  destroyed() {
+    window.removeEventListener('resize', this.resize);
+  }
+
+  traverseTree(node, spanId, segmentId, data) {
+    if (!node) return;
+    if(node.spanId === spanId && node.segmentId === segmentId) {node.children.push(data);return;}
+    if (node.children && node.children.length > 0) {
+      for (let i = 0; i < node.children.length; i+=1) {
+          this.traverseTree(node.children[i],spanId,segmentId,data);
+      }
+    }
+  }
+
+  changeTree() {
+    const propsData = this.props;
+    this.segmentId = [];
+    const segmentGroup = {}
+    const segmentIdGroup = []
+    const [...treeData] = propsData.data;
+    const [...rowData] = propsData.data;
+    this.traceId = propsData.data[0].traceId;
+    treeData.forEach(i => {
+      /* eslint-disable */
+      if(i.endpointName) {
+        i.label = i.endpointName;
+        i.content = i.endpointName;
+      } else {
+        i.label = 'no operation name';
+      }
+      i.duration = i.endTime - i.startTime;
+      i.spanSegId = `${i.segmentId},${i.spanId}`
+      i.parentSpanSegId = i.parentSpanId === -1 ? null :  `${i.segmentId},${i.spanId}`
+      i.children = [];
+      if(segmentGroup[i.segmentId] === undefined){
+        segmentIdGroup.push(i.segmentId);
+        segmentGroup[i.segmentId] = [];
+        segmentGroup[i.segmentId].push(i);
+      }else{
+        segmentGroup[i.segmentId].push(i);
+      }
+    });
+    segmentIdGroup.forEach(id => {
+      const currentSegment = segmentGroup[id].sort((a,b) => b.parentSpanId-a.parentSpanId);
+      currentSegment.forEach(s =>{
+        const index = currentSegment.findIndex(i => i.spanId === s.parentSpanId);
+        if(index !== -1){
+          currentSegment[index].children.push(s);
+          currentSegment[index].children.sort((a, b) => a.spanId - b.spanId );
+        }
+      })
+      segmentGroup[id] = currentSegment[currentSegment.length-1]
+    })
+    segmentIdGroup.forEach(id => {
+      segmentGroup[id].refs.forEach(ref => {
+        if(ref.traceId === this.traceId) {
+          this.traverseTree(segmentGroup[ref.parentSegmentId],ref.parentSpanId,ref.parentSegmentId,segmentGroup[id])
+        };
+      })
+    })
+    for (const i in segmentGroup) {
+      if(segmentGroup[i].refs.length ===0 )
+      this.segmentId.push(segmentGroup[i]);
+    }
+    this.tree = new Tree(this.echartsElement)
+    this.tree.draw({label:`${this.traceId}`, children: this.segmentId}, rowData,  propsData.showSpanModal);
+    this.resize = this.tree.resize.bind(this.tree);
+  }
+
+  
+  render() {
+    const newStyle = {
+      height: 700,
+      // ...style,
+    };
+    return (
+      <div
+        ref={(e) => { this.echartsElement = e; }}
+        style={newStyle}
+        className="trace-tree"
+      />
+    )
+  }
+}
diff --git a/src/components/TraceTree/style.less b/src/components/TraceTree/style.less
new file mode 100644 (file)
index 0000000..28f95a8
--- /dev/null
@@ -0,0 +1,43 @@
+/*\r
+ * Licensed to the Apache Software Foundation (ASF) under one or more\r
+ * contributor license agreements.  See the NOTICE file distributed with\r
+ * this work for additional information regarding copyright ownership.\r
+ * The ASF licenses this file to You under the Apache License, Version 2.0\r
+ * (the "License"); you may not use this file except in compliance with\r
+ * the License.  You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+ :global(.trace-tree .node) {\r
+  cursor: pointer;\r
+}\r
+ :global(.trace-tree .node circle) {\r
+    stroke: #8543e0aa;\r
+    stroke-width: 1.5px;\r
+  }\r
+:global(.trace-tree .node text) {\r
+    font: 12px sans-serif;\r
+    stroke-width: 0.5;\r
+  }\r
+:global(.trace-tree .node .block) {\r
+    width: 150px;\r
+    height: 34px;\r
+  }\r
+:global(.trace-tree .link) {\r
+    fill: none;\r
+    stroke: #8543e055;\r
+    stroke-width: 2px;\r
+  }\r
+:global(.trace-tree .domain) {\r
+    opacity: 0;\r
+  }\r
+:global(.trace-tree .tooltip-box) {\r
+    fill: #333;\r
+    height: 20px !important;\r
+  }\r