This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch update-antdpro
in repository https://gitbox.apache.org/repos/asf/incubator-skywalking-ui.git


The following commit(s) were added to refs/heads/update-antdpro by this push:
     new ca313b0  Add components
ca313b0 is described below

commit ca313b0183ec26064bc103f1bc5870094002a842
Author: hanahmily <[email protected]>
AuthorDate: Mon Feb 26 11:59:32 2018 +0800

    Add components
---
 src/components/Charts/Area/index.js        | 132 ++++++++++++
 src/components/Page/Panel/index.js         |  30 +++
 src/components/Page/Ranking/index.js       |  29 +++
 src/components/Page/Search/index.js        |  68 ++++++
 src/components/Page/index.js               |   9 +
 src/components/Topology/AppTopology.js     |  79 +++++++
 src/components/Topology/Base.js            |  66 ++++++
 src/components/Topology/ServiceTopology.js |  77 +++++++
 src/components/Topology/conf.js            |  13 ++
 src/components/Topology/index.js           |   7 +
 src/components/Topology/index.less         |  17 ++
 src/components/TraceStack/index.js         | 334 +++++++++++++++++++++++++++++
 src/components/TraceStack/index.less       |  51 +++++
 src/components/TraceTable/index.js         |  57 +++++
 src/components/TraceTable/index.less       |  13 ++
 15 files changed, 982 insertions(+)

diff --git a/src/components/Charts/Area/index.js 
b/src/components/Charts/Area/index.js
new file mode 100644
index 0000000..a15f5d6
--- /dev/null
+++ b/src/components/Charts/Area/index.js
@@ -0,0 +1,132 @@
+import React, { PureComponent } from 'react';
+import G2 from 'g2';
+import Debounce from 'lodash-decorators/debounce';
+import Bind from 'lodash-decorators/bind';
+import equal from '../equal';
+import styles from '../index.less';
+
+class Area extends PureComponent {
+  static defaultProps = {
+    limitColor: 'rgb(255, 144, 24)',
+    color: 'rgb(24, 144, 255)',
+  };
+  componentDidMount() {
+    this.renderChart(this.props.data);
+
+    window.addEventListener('resize', this.resize);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (!equal(this.props, nextProps)) {
+      const { data = [] } = nextProps;
+      this.renderChart(data);
+    }
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('resize', this.resize);
+    if (this.chart) {
+      this.chart.destroy();
+    }
+    this.resize.cancel();
+  }
+
+  @Bind()
+  @Debounce(200)
+  resize() {
+    if (!this.node) {
+      return;
+    }
+    const { data = [] } = this.props;
+    this.renderChart(data);
+  }
+
+  handleRef = (n) => {
+    this.node = n;
+  }
+
+  renderChart(data) {
+    const {
+      height = 0,
+      fit = true,
+      margin = [32, 60, 32, 60],
+      limitColor,
+      color,
+    } = this.props;
+
+    if (!data || (data && data.length < 1)) {
+      return;
+    }
+
+    // clean
+    this.node.innerHTML = '';
+
+    const chart = new G2.Chart({
+      container: this.node,
+      forceFit: fit,
+      height: height - 22,
+      plotCfg: {
+        margin,
+      },
+      legend: false,
+    });
+    chart.legend(false);
+    chart.axis('x', {
+      title: false,
+    });
+    chart.axis('y', {
+      title: false,
+    });
+    chart.tooltip({
+      crosshairs: {
+        type: 'line',
+      },
+      title: null,
+      map: {
+        name: 'x',
+        value: 'y',
+      },
+    });
+    const dataConfig = {
+      x: {
+        type: 'cat',
+        tickCount: 3,
+        range: [0, 1],
+      },
+      y: {
+        min: 0,
+      },
+    };
+    const view = chart.createView();
+    const offset = Math.floor(data.length / 2);
+    const xData = data.slice(0, offset);
+    const yData = data.slice(offset).map((v, i) => ({ ...v, y: v.y - 
xData[i].y }));
+    view.source(yData.concat(xData), dataConfig);
+    view.areaStack().position('x*y').color('type', [limitColor, 
color]).shape('smooth')
+      .style({ fillOpacity: 0.2 });
+    view.tooltip(false);
+    view.axis(false);
+    const view2 = chart.createView();
+    view2.source(data, dataConfig);
+    view2.line().position('x*y').color('type', [color, 
limitColor]).shape('smooth')
+      .size(2);
+    chart.render();
+
+    this.chart = chart;
+  }
+
+  render() {
+    const { height, title } = this.props;
+
+    return (
+      <div className={styles.chart} style={{ height }}>
+        <div>
+          { title && <h4>{title}</h4>}
+          <div ref={this.handleRef} />
+        </div>
+      </div>
+    );
+  }
+}
+
+export default Area;
diff --git a/src/components/Page/Panel/index.js 
b/src/components/Page/Panel/index.js
new file mode 100644
index 0000000..ba88752
--- /dev/null
+++ b/src/components/Page/Panel/index.js
@@ -0,0 +1,30 @@
+import React, { Component } from 'react';
+
+export default class Panel extends Component {
+  componentDidMount() {
+    const { globalVariables, variables, onChange } = this.props;
+    if (!this.isRender(this.props)) {
+      return;
+    }
+    onChange({ ...globalVariables, ...variables });
+  }
+  shouldComponentUpdate(nextProps) {
+    const { globalVariables, variables, onChange } = nextProps;
+    if (!this.isRender(nextProps)) {
+      return false;
+    }
+    if (globalVariables !== this.props.globalVariables || variables !== 
this.props.variables) {
+      onChange({ ...globalVariables, ...variables });
+      return false;
+    }
+    return true;
+  }
+  isRender = props => [props.variables, props.globalVariables]
+    .reduce((acc, curr) =>
+      (acc && (curr === undefined
+        || (curr !== undefined && Object.keys(curr).length > 0))), true);
+  render() {
+    const { children } = this.props;
+    return children && (<div> {children} </div>);
+  }
+}
diff --git a/src/components/Page/Ranking/index.js 
b/src/components/Page/Ranking/index.js
new file mode 100644
index 0000000..e4400d3
--- /dev/null
+++ b/src/components/Page/Ranking/index.js
@@ -0,0 +1,29 @@
+import React, { PureComponent } from 'react';
+import { List, Avatar } from 'antd';
+
+export default class Ranking extends PureComponent {
+
+  render() {
+    let index = 0;
+    const { data, title, content, unit } = this.props;
+    return (<List
+      size="small"
+      itemLayout="horizontal"
+      dataSource={data}
+      renderItem={item => (
+        <List.Item>
+          <List.Item.Meta
+            avatar={
+              <Avatar
+                style={{ color: '#ff3333', backgroundColor: '#ffb84d' }}
+              >
+                {(() => { index += 1; return index; })()}
+              </Avatar>}
+            title={item[title]}
+            description={`${item[content]} ${unit}`}
+          />
+        </List.Item>
+      )}
+    />);
+  }
+}
diff --git a/src/components/Page/Search/index.js 
b/src/components/Page/Search/index.js
new file mode 100644
index 0000000..dd019d6
--- /dev/null
+++ b/src/components/Page/Search/index.js
@@ -0,0 +1,68 @@
+import React, { PureComponent } from 'react';
+import { Select, Spin } from 'antd';
+import debounce from 'lodash.debounce';
+import request from '../../../utils/request';
+
+const { Option } = Select;
+
+export default class Search extends PureComponent {
+  constructor(props) {
+    super(props);
+    this.lastFetchId = 0;
+    this.fetchServer = debounce(this.fetchServer, 800);
+  }
+  state = {
+    data: [],
+    fetching: false,
+  }
+  fetchServer = (value) => {
+    if (!value || value.length < 1) {
+      return;
+    }
+    const { url, query, variables = {} } = this.props;
+    this.lastFetchId += 1;
+    const fetchId = this.lastFetchId;
+    this.setState({ data: [], fetching: true });
+    request(`/api${url}`, {
+      method: 'POST',
+      body: {
+        variables: {
+          ...variables,
+          keyword: value,
+        },
+        query,
+      },
+    })
+      .then((body) => {
+        if (!body.data || fetchId !== this.lastFetchId) { // for fetch 
callback order
+          return;
+        }
+        this.setState({ data: body.data[Object.keys(body.data)[0]], fetching: 
false });
+      });
+  }
+  handleSelect = (value) => {
+    const { onSelect } = this.props;
+    const selected = this.state.data.find(_ => _.key === value.key);
+    onSelect(selected);
+  }
+  render() {
+    const { placeholder, value } = this.props;
+    return (
+      <Select
+        showSearch
+        style={{ width: 400 }}
+        placeholder={placeholder}
+        notFoundContent={this.state.fetching ? <Spin size="small" /> : null}
+        filterOption={false}
+        labelInValue
+        onSelect={this.handleSelect.bind(this)}
+        onSearch={this.fetchServer}
+        value={value}
+      >
+        {this.state.data.map((_) => {
+            return (<Option key={_.key} value={_.key}>{_.label}</Option>);
+          })}
+      </Select>
+    );
+  }
+}
diff --git a/src/components/Page/index.js b/src/components/Page/index.js
new file mode 100644
index 0000000..983d127
--- /dev/null
+++ b/src/components/Page/index.js
@@ -0,0 +1,9 @@
+import Panel from './Panel';
+import Search from './Search';
+import Ranking from './Ranking';
+
+export default {
+  Panel,
+  Search,
+  Ranking,
+};
diff --git a/src/components/Topology/AppTopology.js 
b/src/components/Topology/AppTopology.js
new file mode 100644
index 0000000..e22e12c
--- /dev/null
+++ b/src/components/Topology/AppTopology.js
@@ -0,0 +1,79 @@
+import styles from './index.less';
+import Base from './Base';
+
+export default class AppTopology extends Base {
+  getStyle = () => {
+    return [
+      {
+        selector: 'node[sla]',
+        style: {
+          width: 120,
+          height: 120,
+          'text-valign': 'bottom',
+          'text-halign': 'center',
+          'background-color': '#fff',
+          'border-width': 3,
+          'border-color': ele => (ele.data('isAlarm') ? 'red' : 'rgb(99, 160, 
167)'),
+          'font-family': 'Microsoft YaHei',
+          label: 'data(name)',
+        },
+      },
+      {
+        selector: 'node[!sla]',
+        style: {
+          width: 60,
+          height: 60,
+          'text-valign': 'bottom',
+          'text-halign': 'center',
+          'background-color': '#fff',
+          'background-image': ele => `img/node/${ele.data('type') ? 
ele.data('type') : 'UNDEFINED'}.png`,
+          'background-width': '60%',
+          'background-height': '60%',
+          'border-width': 1,
+          'font-family': 'Microsoft YaHei',
+          label: 'data(name)',
+        },
+      },
+      {
+        selector: 'edge',
+        style: {
+          'curve-style': 'bezier',
+          'control-point-step-size': 100,
+          'target-arrow-shape': 'triangle',
+          'target-arrow-color': ele => (ele.data('isAlarm') ? 'red' : 
'rgb(147, 198, 174)'),
+          'line-color': ele => (ele.data('isAlarm') ? 'red' : 'rgb(147, 198, 
174)'),
+          width: 2,
+          label: ele => `${ele.data('callType')} \n ${ele.data('callsPerSec')} 
tps / ${ele.data('responseTimePerSec')} ms`,
+          'text-wrap': 'wrap',
+          color: 'rgb(110, 112, 116)',
+          'text-rotation': 'autorotate',
+        },
+      },
+    ];
+  }
+  getNodeLabel = () => {
+    return [
+      {
+        query: 'node[sla]',
+        halign: 'center',
+        valign: 'center',
+        halignBox: 'center',
+        valignBox: 'center',
+        cssClass: `${styles.node}`,
+        tpl(data) {
+          return `
+          <div class="${styles.circle}">
+            <div class="node-percentage">${data.sla}%</div>
+            <div>${data.callsPerSec} calls/s</div>
+            <div>
+              <img src="data.png" class="${styles.logo}"/>${data.numOfServer}
+              <img src="alert.png" class="${styles.logo}"/>
+              <span class="${styles.alert}">${data.numOfServerAlarm}</span>
+            </div>
+            <div>${data.apdex} Apdex</div>
+          </div>`;
+        },
+      },
+    ];
+  }
+}
diff --git a/src/components/Topology/Base.js b/src/components/Topology/Base.js
new file mode 100644
index 0000000..d895616
--- /dev/null
+++ b/src/components/Topology/Base.js
@@ -0,0 +1,66 @@
+import React, { Component } from 'react';
+import cytoscape from 'cytoscape';
+import coseBilkent from 'cytoscape-cose-bilkent';
+import nodeHtmlLabel from 'cytoscape-node-html-label';
+import conf from './conf';
+
+cytoscape.use(coseBilkent);
+cytoscape.use(nodeHtmlLabel);
+
+export default class Base extends Component {
+  state= {
+    height: '600px',
+    display: 'block',
+  }
+  componentDidMount() {
+    this.cy = cytoscape({
+      ...conf,
+      elements: this.transform(this.props.elements),
+      style: this.getStyle(),
+    });
+    this.cy.nodeHtmlLabel(this.getNodeLabel());
+  }
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.elements === this.elements) {
+      return;
+    }
+    const { layout: layoutConfig = {
+      name: 'cose-bilkent',
+      animate: false,
+      idealEdgeLength: 200,
+      edgeElasticity: 0.1,
+    } } = this.props;
+    this.cy.json({ elements: this.transform(nextProps.elements), style: 
this.getStyle() });
+    const layout = this.cy.layout(layoutConfig);
+    layout.pon('layoutstop').then(() => {
+      this.cy.minZoom(this.cy.zoom() - 0.3);
+    });
+    layout.run();
+  }
+  shouldComponentUpdate() {
+    return false;
+  }
+  componentWillUnmount() {
+    this.cy.destroy();
+  }
+  getCy() {
+    return this.cy;
+  }
+  transform(elements) {
+    if (!elements) {
+      return [];
+    }
+    this.elements = elements;
+    const { nodes, calls } = elements;
+    return {
+      nodes: nodes.map(node => ({ data: node })),
+      edges: calls.filter(call => (nodes.findIndex(node => node.id === 
call.source) > -1
+        && nodes.findIndex(node => node.id === call.target) > -1))
+        .map(call => ({ data: { ...call, id: `${call.source}-${call.target}` } 
})),
+    };
+  }
+  render() {
+    const { height = this.state.height } = this.props;
+    return (<div style={{ ...this.state, height }} ref={(el) => { 
conf.container = el; }} />);
+  }
+}
diff --git a/src/components/Topology/ServiceTopology.js 
b/src/components/Topology/ServiceTopology.js
new file mode 100644
index 0000000..bb80c34
--- /dev/null
+++ b/src/components/Topology/ServiceTopology.js
@@ -0,0 +1,77 @@
+import styles from './index.less';
+import Base from './Base';
+
+export default class ServiceTopology extends Base {
+  getStyle = () => {
+    return [
+      {
+        selector: 'node[calls]',
+        style: {
+          width: 120,
+          height: 120,
+          'text-valign': 'bottom',
+          'text-halign': 'center',
+          'background-color': '#fff',
+          'border-width': 3,
+          'border-color': ele => (ele.data('numOfServiceAlarm') > 0 ? 'red' : 
'rgb(99, 160, 167)'),
+          'font-family': 'Microsoft YaHei',
+          label: 'data(name)',
+        },
+      },
+      {
+        selector: 'node[!calls]',
+        style: {
+          width: 60,
+          height: 60,
+          'text-valign': 'bottom',
+          'text-halign': 'center',
+          'background-color': '#fff',
+          'background-image': ele => `img/node/${ele.data('type') ? 
ele.data('type') : 'UNDEFINED'}.png`,
+          'background-width': '60%',
+          'background-height': '60%',
+          'border-width': 1,
+          'font-family': 'Microsoft YaHei',
+          label: 'data(name)',
+        },
+      },
+      {
+        selector: 'edge',
+        style: {
+          'curve-style': 'bezier',
+          'control-point-step-size': 100,
+          'target-arrow-shape': 'triangle',
+          'target-arrow-color': ele => (ele.data('isAlarm') ? 'red' : 
'rgb(147, 198, 174)'),
+          'line-color': ele => (ele.data('isAlarm') ? 'red' : 'rgb(147, 198, 
174)'),
+          width: 2,
+          label: ele => `${ele.data('callType')} \n ${ele.data('callsPerSec')} 
tps / ${ele.data('responseTimePerSec')} ms`,
+          'text-wrap': 'wrap',
+          color: 'rgb(110, 112, 116)',
+          'text-rotation': 'autorotate',
+        },
+      },
+    ];
+  }
+  getNodeLabel = () => {
+    return [
+      {
+        query: 'node[calls]',
+        halign: 'center',
+        valign: 'center',
+        halignBox: 'center',
+        valignBox: 'center',
+        cssClass: `${styles.node}`,
+        tpl(data) {
+          return `
+          <div class="${styles.circle}">
+            <div class="node-percentage">${data.sla}%</div>
+            <div>${data.calls} calls/s</div>
+            <div>
+              <img src="alert.png" class="${styles.logo}"/>
+              <span class="${styles.alert}">${data.numOfServiceAlarm}</span>
+            </div>
+          </div>`;
+        },
+      },
+    ];
+  }
+}
diff --git a/src/components/Topology/conf.js b/src/components/Topology/conf.js
new file mode 100644
index 0000000..685b68d
--- /dev/null
+++ b/src/components/Topology/conf.js
@@ -0,0 +1,13 @@
+const conf = {
+  zoom: 1,
+  maxZoom: 1,
+  boxSelectionEnabled: true,
+  autounselectify: true,
+  layout: {
+    name: 'cose-bilkent',
+    animate: true,
+    idealEdgeLength: 100,
+  },
+};
+
+export default conf;
diff --git a/src/components/Topology/index.js b/src/components/Topology/index.js
new file mode 100644
index 0000000..96bb101
--- /dev/null
+++ b/src/components/Topology/index.js
@@ -0,0 +1,7 @@
+import AppTopology from './AppTopology';
+import ServiceTopology from './ServiceTopology';
+
+export default {
+  AppTopology,
+  ServiceTopology,
+};
diff --git a/src/components/Topology/index.less 
b/src/components/Topology/index.less
new file mode 100644
index 0000000..4dca934
--- /dev/null
+++ b/src/components/Topology/index.less
@@ -0,0 +1,17 @@
+.node {
+  text-align: center;
+}
+
+.logo {
+  height: 15px;
+  vertical-align: middle;
+  margin: -3px 2px 0px 0px;
+}
+
+.circle {
+  font-size: 16px;
+}
+
+.alert {
+  color: red;
+}
diff --git a/src/components/TraceStack/index.js 
b/src/components/TraceStack/index.js
new file mode 100644
index 0000000..1023109
--- /dev/null
+++ b/src/components/TraceStack/index.js
@@ -0,0 +1,334 @@
+import React, { PureComponent } from 'react';
+import { Tag, Modal, List, Tabs } from 'antd';
+import * as d3 from 'd3';
+import moment from 'moment';
+import styles from './index.less';
+
+const { TabPane } = Tabs;
+
+const colors = [
+  '#F2C2CE',
+  '#A7D8F0',
+  '#FADDA2',
+  '#8691C5',
+  '#E8DB62',
+  '#BDC8E7',
+  '#F2A7A8',
+  '#F5E586',
+  '#91C3ED',
+  '#96B87F',
+  '#EE8D87',
+  '#BDDCAB',
+  '#68B9C7',
+  '#93DAD6',
+  '#EEBE84',
+  '#83B085',
+  '#8CCCD2',
+  '#C5DFE8',
+  '#F2B75B',
+  '#C8DC60',
+];
+const height = 36;
+const margin = 10;
+const offX = 15;
+const offY = 6;
+const timeFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
+class TraceStack extends PureComponent {
+  state = {
+    nodes: [],
+    idMap: {},
+    colorMap: {},
+    bap: [],
+    visible: false,
+    span: {},
+  }
+  componentWillMount() {
+    const { spans } = this.props;
+    spans.forEach(this.buildNode);
+    const { nodes } = this.state;
+    const minStartTimeNode = nodes.reduce((acc, n) => (acc.startTime > 
n.startTime ? n : acc));
+    this.state.nodes = nodes.map(n =>
+      ({ ...n, startOffset: n.startTime - minStartTimeNode.startTime }));
+  }
+  componentDidMount() {
+    this.state.width = this.axis.parentNode.clientWidth - 50;
+    this.drawAxis();
+    this.displayData();
+    window.addEventListener('resize', this.resize);
+  }
+  buildNode = (span, index) => {
+    const { nodes, colorMap, idMap } = this.state;
+    const node = {};
+    node.applicationCode = span.applicationCode;
+    node.startTime = span.startTime;
+    node.endTime = span.endTime;
+    node.duration = span.endTime - span.startTime;
+    node.content = span.operationName;
+    node.spanSegId = this.id(span.segmentId, span.spanId);
+    node.parentSpanSegId = this.findParent(span);
+    node.refs = span.refs;
+    node.type = span.type;
+    node.peer = span.peer;
+    node.component = span.component;
+    node.isError = span.isError;
+    node.layer = span.layer;
+    node.tags = span.tags;
+    node.logs = span.logs;
+    nodes.push(node);
+
+    if (!colorMap[span.applicationCode]) {
+      colorMap[span.applicationCode] = colors[index];
+    }
+    idMap[node.spanSegId] = nodes.length - 1;
+  }
+  id = (...seg) => seg.join();
+  findParent = (span) => {
+    const { spans } = this.props;
+    if (span.refs) {
+      const ref = span.refs.find(_ => spans.findIndex(s =>
+        this.id(_.parentSegmentId, _.parentSpanId) === this.id(s.segmentId, 
s.spanId)) > -1);
+      if (ref) {
+        return this.id(ref.parentSegmentId, ref.parentSpanId);
+      }
+    }
+    const result = this.id(span.segmentId, span.parentSpanId);
+    if (spans.findIndex(s => result === this.id(s.segmentId, s.spanId)) > -1) {
+      return result;
+    }
+    return null;
+  }
+  drawAxis = () => {
+    const { width } = this.state;
+    const { nodes, bap } = this.state;
+    const dataSet = nodes.map(node => node.startOffset + node.duration);
+    const bits = d3.max(dataSet).toString().length;
+    const percentScale = Math.ceil(d3.max(dataSet) / (10 ** (bits - 2)));
+    const axisHeight = 20;
+
+    const svg = d3.select(this.axis).append('svg')
+      .attr('width', width)
+      .attr('height', axisHeight)
+      .attr('style', 'overflow: visible');
+
+    const xScale = d3.scaleLinear()
+      .domain([0, percentScale * (10 ** (bits - 2))])
+      .range([0, width]);
+
+    const axis = d3.axisTop(xScale).ticks(20);
+
+    svg.append('g')
+      .attr('class', styles.axis)
+      .attr('transform', `translate(0, ${axisHeight})`)
+      .call(axis);
+
+    bap.push(bits);
+    bap.push(percentScale);
+    return bap;
+  }
+  displayData = () => {
+    const { nodes, bap, width, colorMap } = this.state;
+    const svgContainer = d3.select(this.duration).append('svg').attr('height', 
height * nodes.length).attr('style', 'overflow: visible');
+    const positionMap = {};
+    nodes.forEach((node, index) => {
+      const { startOffset: startTime, duration, content,
+        applicationCode, spanSegId, parentSpanSegId } = node;
+
+      const rectWith = ((duration * width) / (bap[1] * (10 ** (bap[0] - 4)))) 
/ 100;
+      const beginX = ((startTime * width) / (bap[1] * (10 ** (bap[0] - 4)))) / 
100;
+      const bar = svgContainer.append('g').attr('transform', (d, i) => 
`translate(0,${i * height})`);
+
+      const beginY = index * height;
+      positionMap[spanSegId] = { x: beginX, y: beginY };
+
+      bar.append('rect').attr('x', beginX).attr('y', beginY).attr('width', 
rectWith)
+        .attr('height', height - margin)
+        .style('fill', colorMap[applicationCode]);
+
+      bar.append('rect').attr('spanSegId', spanSegId).attr('x', 0).attr('y', 
beginY)
+        .attr('width', width)
+        .attr('height', height - margin)
+        .style('opacity', '0')
+        .on('click', () => { this.showSpanModal(node); });
+
+      bar.append('text')
+        .attr('x', beginX + 5)
+        .attr('y', (index * height) + (height / 2))
+        .attr('class', styles.rectText)
+        .text(content);
+      if (index > 0 && positionMap[parentSpanSegId]) {
+        const parentX = positionMap[parentSpanSegId].x;
+        const parentY = positionMap[parentSpanSegId].y;
+
+        const defs = svgContainer.append('defs');
+        const arrowMarker = defs.append('marker')
+          .attr('id', 'arrow')
+          .attr('markerUnits', 'strokeWidth')
+          .attr('markerWidth', 12)
+          .attr('markerHeight', 12)
+          .attr('viewBox', '0 0 12 12')
+          .attr('refX', 6)
+          .attr('refY', 6)
+          .attr('orient', 'auto');
+        arrowMarker.append('path')
+          .attr('d', 'M2,2 L10,6 L2,10 L6,6 L2,2')
+          .attr('fill', '#333');
+
+        const parentLeftBottomX = parentX;
+        const parentLeftBottomY = (Number(parentY) + Number(height)) - 
Number(margin);
+        const selfMiddleX = beginX;
+        const selfMiddleY = beginY + ((height - margin) / 2);
+        if ((beginX - parentX) < 10) {
+          svgContainer.append('line').attr('x1', parentLeftBottomX - 
offX).attr('y1', parentLeftBottomY - offY).attr('class', styles.connlines)
+            .attr('x2', parentLeftBottomX)
+            .attr('y2', parentLeftBottomY - offY);
+
+          svgContainer.append('line').attr('x1', parentLeftBottomX - 
offX).attr('y1', parentLeftBottomY - offY).attr('class', styles.connlines)
+            .attr('x2', parentLeftBottomX - offX)
+            .attr('y2', selfMiddleY);
+
+          svgContainer.append('line').attr('x1', parentLeftBottomX - 
offX).attr('y1', selfMiddleY).attr('class', styles.connlines)
+            .attr('x2', selfMiddleX)
+            .attr('y2', selfMiddleY)
+            .attr('marker-end', 'url(#arrow)');
+        } else {
+          svgContainer.append('line').attr('x1', parentLeftBottomX).attr('y1', 
parentLeftBottomY).attr('class', styles.connlines)
+            .attr('x2', parentLeftBottomX)
+            .attr('y2', selfMiddleY);
+
+          svgContainer.append('line').attr('x1', parentLeftBottomX).attr('y1', 
selfMiddleY).attr('class', styles.connlines)
+            .attr('x2', selfMiddleX)
+            .attr('y2', selfMiddleY)
+            .attr('marker-end', 'url(#arrow)');
+        }
+      }
+    });
+  }
+  handleCancel = () => {
+    this.setState({
+      ...this.state,
+      visible: false,
+    });
+  }
+  showSpanModal = (span) => {
+    this.setState({
+      ...this.state,
+      visible: true,
+      span,
+    });
+  }
+  resize = () => {
+    this.state.width = this.axis.parentNode.clientWidth - 50;
+    if (!this.axis || this.state.width <= 0) {
+      return;
+    }
+    this.axis.innerHTML = '';
+    this.duration.innerHTML = '';
+    this.drawAxis();
+    this.displayData();
+  }
+  render() {
+    const { colorMap, span = {} } = this.state;
+    const legendButtons = Object.keys(colorMap).map(key =>
+      (<Tag color={colorMap[key]} key={key}>{key}</Tag>));
+    let data;
+    if (span.content) {
+      const base = [
+        {
+          title: 'operation name',
+          content: span.content,
+        },
+        {
+          title: 'duration',
+          content: `${moment(span.startTime).format(timeFormat)} - 
${moment(span.endTime).format(timeFormat)}`,
+        },
+        {
+          title: 'span type',
+          content: span.type,
+        },
+        {
+          title: 'component',
+          content: span.component,
+        },
+        {
+          title: 'peer',
+          content: span.peer,
+        },
+        {
+          title: 'is error',
+          content: span.isError,
+        },
+      ];
+      data = base.concat(span.tags.map(t => ({ title: t.key, content: t.value 
})));
+    }
+    const logs = span.logs ? span.logs.map(l => (
+      <TabPane tab={moment(l.time).format('mm:ss.SSS')} key={l.time}>
+        <List
+          itemLayout="horizontal"
+          dataSource={l.data}
+          renderItem={item => (
+            <List.Item>
+              <List.Item.Meta
+                size="small"
+                title={item.key}
+                description={item.value}
+              />
+            </List.Item>
+          )}
+        />
+      </TabPane>
+    )) : null;
+    const relatedTraces = span.parentSpanSegId ? null : (
+      <TabPane tab="Related Trace" key={3}>
+        <List
+          itemLayout="horizontal"
+          dataSource={span.refs}
+          renderItem={item => (
+            <List.Item>
+              <List.Item.Meta
+                size="small"
+                title={item.type}
+                description={item.traceId}
+              />
+            </List.Item>
+          )}
+        />
+      </TabPane>
+    );
+    return (
+      <div className={styles.stack}>
+        <div style={{ paddingBottom: 10 }}>
+          { legendButtons }
+        </div>
+        <div ref={(el) => { this.axis = el; }} />
+        <div className={styles.duration} ref={(el) => { this.duration = el; }} 
/>
+        <Modal
+          title="Span Info"
+          visible={this.state.visible}
+          onCancel={this.handleCancel}
+          footer={null}
+        >
+          <Tabs defaultActiveKey="1" tabPosition="left">
+            <TabPane tab="Tags" key="1">
+              <List
+                itemLayout="horizontal"
+                dataSource={data}
+                renderItem={item => (
+                  <List.Item>
+                    <List.Item.Meta
+                      title={item.title}
+                      description={item.content}
+                    />
+                  </List.Item>
+                )}
+              />
+            </TabPane>
+            {logs}
+            {relatedTraces}
+          </Tabs>
+        </Modal>
+      </div>
+    );
+  }
+}
+
+export default TraceStack;
diff --git a/src/components/TraceStack/index.less 
b/src/components/TraceStack/index.less
new file mode 100644
index 0000000..7b3cc59
--- /dev/null
+++ b/src/components/TraceStack/index.less
@@ -0,0 +1,51 @@
+.axis {
+  position: absolute;
+  right: 0;
+  color: #333;
+  text-align: right;
+  font-size: 12px;
+  path {
+    fill: none;
+    stroke: black;
+  }
+  line {
+    fill: none;
+    stroke: black;
+  }
+  text {
+    font-size: 11px;
+  }
+}
+
+.rectText {
+  font: 10px sans-serif;
+  font-weight: 100;
+  fill: #393939;
+}
+
+.stack {
+  font-family: 'Microsoft Yahei';
+  border-radius: 5px;
+  position: relative;
+  display: block;
+  background: rgba(255, 255, 255, 9);
+}
+
+.duration {
+  padding-top: 25px;
+  margin: 0;
+  width: 100%;
+}
+
+.connlines {
+  stroke: #333;
+  shape-rendering: crispEdges;
+  stroke-width: 1px;
+  stroke-opacity: 1;
+}
+
+.legend {
+  color: #fff;
+  margin-right: 10px;
+  margin-top: 5px;
+}
diff --git a/src/components/TraceTable/index.js 
b/src/components/TraceTable/index.js
new file mode 100644
index 0000000..a9858fb
--- /dev/null
+++ b/src/components/TraceTable/index.js
@@ -0,0 +1,57 @@
+import React, { PureComponent } from 'react';
+import { Table } from 'antd';
+import TraceStack from '../../components/TraceStack';
+import styles from './index.less';
+
+class TraceTable extends PureComponent {
+  render() {
+    const { data: traces, pagination, loading, onExpand, onChange } = 
this.props;
+
+    const columns = [
+      {
+        title: 'OperationName',
+        dataIndex: 'operationName',
+      },
+      {
+        title: 'Duration',
+        dataIndex: 'duration',
+      },
+      {
+        title: 'StartTime',
+        dataIndex: 'start',
+      },
+      {
+        title: 'State',
+        dataIndex: 'isError',
+        render: (text, record) => {
+          if (record.isError) {
+            return 'Success';
+          } else {
+            return 'Error';
+          }
+        },
+      },
+      {
+        title: 'GlobalTraceId',
+        dataIndex: 'traceId',
+      },
+    ];
+
+    return (
+      <div className={styles.standardTable}>
+        <Table
+          loading={loading}
+          rowKey={record => record.traceId}
+          dataSource={traces}
+          columns={columns}
+          pagination={pagination}
+          onChange={onChange}
+          onExpand={onExpand}
+          expandedRowRender={record => (record.spans ? <TraceStack 
spans={record.spans} /> : null)}
+        />
+      </div>
+    );
+  }
+}
+
+export default TraceTable;
diff --git a/src/components/TraceTable/index.less 
b/src/components/TraceTable/index.less
new file mode 100644
index 0000000..7e6d10b
--- /dev/null
+++ b/src/components/TraceTable/index.less
@@ -0,0 +1,13 @@
+@import "~antd/lib/style/themes/default.less";
+
+.standardTable {
+  :global {
+    .ant-table-pagination {
+      margin-top: 24px;
+    }
+  }
+
+  .tableAlert {
+    margin-bottom: 16px;
+  }
+}

-- 
To stop receiving notification emails like this one, please contact
[email protected].

Reply via email to