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].