diff --git a/packages/jaeger-ui/config-overrides.js b/packages/jaeger-ui/config-overrides.js
index 972a70845b..239008a32b 100644
--- a/packages/jaeger-ui/config-overrides.js
+++ b/packages/jaeger-ui/config-overrides.js
@@ -14,10 +14,15 @@
/* eslint-disable import/no-extraneous-dependencies */
+const path = require('path');
const fs = require('fs');
const { injectBabelPlugin } = require('react-app-rewired');
const rewireLess = require('react-app-rewire-less');
const lessToJs = require('less-vars-to-js');
+const rewireBabelLoader = require('react-app-rewire-babel-loader');
+
+const appDirectory = fs.realpathSync(process.cwd());
+const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
// Read the less file in as string
const loadedVarOverrides = fs.readFileSync('config-overrides-antd-vars.less', 'utf8');
@@ -29,5 +34,6 @@ module.exports = function override(_config, env) {
let config = _config;
config = injectBabelPlugin(['import', { libraryName: 'antd', style: true }], config);
config = rewireLess.withLoaderOptions({ modifyVars })(config, env);
+ config = rewireBabelLoader.include(config, resolveApp('../../node_modules/drange'));
return config;
};
diff --git a/packages/jaeger-ui/package.json b/packages/jaeger-ui/package.json
index 712a9db375..eec832bb28 100644
--- a/packages/jaeger-ui/package.json
+++ b/packages/jaeger-ui/package.json
@@ -22,6 +22,7 @@
"enzyme-adapter-react-16": "^1.1.0",
"enzyme-to-json": "^3.3.0",
"less-vars-to-js": "^1.2.1",
+ "react-app-rewire-babel-loader": "^0.1.1",
"react-app-rewire-less": "^2.1.0",
"react-app-rewired": "^1.4.0",
"react-scripts": "^1.0.11",
@@ -39,6 +40,7 @@
"d3-scale": "^1.0.6",
"dagre": "^0.7.4",
"deep-freeze": "^0.0.1",
+ "drange": "^2.0.0",
"fuzzy": "^0.1.3",
"global": "^4.3.2",
"history": "^4.6.3",
diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.css b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.css
new file mode 100644
index 0000000000..75e2a545cf
--- /dev/null
+++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.css
@@ -0,0 +1,54 @@
+/*
+Copyright (c) 2018 The Jaeger Authors.
+
+Licensed 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.
+*/
+
+.OpNode {
+ width: 100%;
+ border: 1px solid #111;
+ cursor: pointer;
+ white-space: nowrap;
+ border-collapse: separate;
+ border-radius: 2px;
+}
+
+.OpNode td,
+.OpNode th {
+ border: none;
+}
+
+.OpMode--mode-service {
+ background: #bbb;
+}
+
+.OpNode--mode-time {
+ background: #eee;
+}
+
+.OpNode--metricCell {
+ text-align: right;
+ padding: 0.3rem 0.5rem;
+ background: rgba(255, 255, 255, 0.3);
+}
+
+.OpNode--labelCell {
+ padding: 0.3rem 0.5rem 0.3rem 0.75rem;
+}
+
+/* Tweak the popover aesthetics - unfortunate but necessary */
+
+.OpNode--popover .ant-popover-inner-content {
+ padding: 0;
+ position: relative;
+}
diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.js b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.js
new file mode 100644
index 0000000000..43d7df6bf4
--- /dev/null
+++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.js
@@ -0,0 +1,126 @@
+// @flow
+
+// Copyright (c) 2018 The Jaeger Authors.
+//
+// Licensed 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 * as React from 'react';
+import { Popover } from 'antd';
+import colorGenerator from '../../../utils/color-generator';
+
+import type { PVertex } from '../../../model/trace-dag/types';
+
+import './OpNode.css';
+
+type Props = {
+ count: number,
+ errors: number,
+ time: number,
+ percent: number,
+ selfTime: number,
+ percentSelfTime: number,
+ operation: string,
+ service: string,
+ mode: string,
+};
+
+export const MODE_SERVICE = 'service';
+export const MODE_TIME = 'time';
+export const MODE_SELFTIME = 'selftime';
+
+export const HELP_TABLE = (
+
+
+
+ Count / Error |
+
+ Service
+ |
+ Avg |
+
+
+ Duration |
+ Operation |
+ Self time |
+
+
+
+);
+
+export function round2(percent: number) {
+ return Math.round(percent * 100) / 100;
+}
+
+export default class OpNode extends React.PureComponent {
+ props: Props;
+
+ render() {
+ const { count, errors, time, percent, selfTime, percentSelfTime, operation, service, mode } = this.props;
+
+ // Spans over 20 % time are full red - we have probably to reconsider better approach
+ let backgroundColor;
+ if (mode === MODE_TIME) {
+ const percentBoosted = Math.min(percent / 20, 1);
+ backgroundColor = [255, 0, 0, percentBoosted].join();
+ } else if (mode === MODE_SELFTIME) {
+ backgroundColor = [255, 0, 0, percentSelfTime / 100].join();
+ } else {
+ backgroundColor = colorGenerator
+ .getRgbColorByKey(service)
+ .concat(0.8)
+ .join();
+ }
+
+ const table = (
+
+
+
+
+ {count} / {errors}
+ |
+
+ {service}
+ |
+ {round2(time / 1000 / count)} ms |
+
+
+
+ {time / 1000} ms ({round2(percent)} %)
+ |
+ {operation} |
+
+ {selfTime / 1000} ms ({round2(percentSelfTime)} %)
+ |
+
+
+
+ );
+
+ return (
+
+ {table}
+
+ );
+ }
+}
+
+export function getNodeDrawer(mode: string) {
+ return function drawNode(vertex: PVertex) {
+ const { data, operation, service } = vertex.data;
+ return ;
+ };
+}
diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.test.js b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.test.js
new file mode 100644
index 0000000000..4d10931edd
--- /dev/null
+++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.test.js
@@ -0,0 +1,89 @@
+// Copyright (c) 2018 The Jaeger Authors.
+//
+// Licensed 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 from 'react';
+import { shallow } from 'enzyme';
+
+import OpNode, { getNodeDrawer, MODE_SERVICE, MODE_TIME, MODE_SELFTIME } from './OpNode';
+
+describe('', () => {
+ let wrapper;
+ let mode;
+ let props;
+
+ beforeEach(() => {
+ mode = MODE_SERVICE;
+ props = {
+ count: 5,
+ errors: 0,
+ time: 200000,
+ percent: 7.89,
+ selfTime: 180000,
+ percentSelfTime: 90,
+ operation: 'op1',
+ service: 'service1',
+ };
+ wrapper = shallow();
+ });
+
+ it('it does not explode', () => {
+ expect(wrapper).toBeDefined();
+ expect(wrapper.find('.OpNode').length).toBe(1);
+ expect(wrapper.find('.OpNode--mode-service').length).toBe(1);
+ });
+
+ it('it renders OpNode', () => {
+ expect(wrapper.find('.OpNode--count').text()).toBe('5 / 0');
+ expect(wrapper.find('.OpNode--time').text()).toBe('200 ms (7.89 %)');
+ expect(wrapper.find('.OpNode--avg').text()).toBe('40 ms');
+ expect(wrapper.find('.OpNode--selfTime').text()).toBe('180 ms (90 %)');
+ expect(wrapper.find('.OpNode--op').text()).toBe('op1');
+ expect(wrapper.find('.OpNode--service').text()).toBe('service1');
+ });
+
+ it('it switches mode', () => {
+ mode = MODE_SERVICE;
+ wrapper = shallow();
+ expect(wrapper.find('.OpNode--mode-service').length).toBe(1);
+ expect(wrapper.find('.OpNode--mode-time').length).toBe(0);
+ expect(wrapper.find('.OpNode--mode-selftime').length).toBe(0);
+
+ mode = MODE_TIME;
+ wrapper = shallow();
+ expect(wrapper.find('.OpNode--mode-service').length).toBe(0);
+ expect(wrapper.find('.OpNode--mode-time').length).toBe(1);
+ expect(wrapper.find('.OpNode--mode-selftime').length).toBe(0);
+
+ mode = MODE_SELFTIME;
+ wrapper = shallow();
+ expect(wrapper.find('.OpNode--mode-service').length).toBe(0);
+ expect(wrapper.find('.OpNode--mode-time').length).toBe(0);
+ expect(wrapper.find('.OpNode--mode-selftime').length).toBe(1);
+ });
+
+ describe('getNodeDrawer()', () => {
+ it('it creates OpNode', () => {
+ const vertex = {
+ data: {
+ service: 'service1',
+ operation: 'op1',
+ data: {},
+ },
+ };
+ const drawNode = getNodeDrawer(MODE_SERVICE);
+ const opNode = drawNode(vertex);
+ expect(opNode.type === 'OpNode');
+ });
+ });
+});
diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.css b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.css
new file mode 100644
index 0000000000..ef16455ae1
--- /dev/null
+++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.css
@@ -0,0 +1,112 @@
+/*
+Copyright (c) 2018 The Jaeger Authors.
+
+Licensed 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.
+*/
+
+.TraceGraph--experimental {
+ background-color: #a00;
+ color: #fff;
+ position: absolute;
+ top: 122px;
+ padding: 1px 15px;
+}
+
+.TraceGraph--graphWrapper {
+ bottom: 0;
+ cursor: move;
+ left: 0;
+ overflow: auto;
+ position: absolute;
+ right: 0;
+ top: 0;
+ display: flex;
+}
+
+.TraceGraph--sidebar-container {
+ display: flex;
+ z-index: 1;
+}
+
+.TraceGraph--menu {
+ cursor: pointer;
+ padding-right: 1rem;
+ padding-top: 1rem;
+}
+
+.TraceGraph--menu > li {
+ list-style-type: none;
+ text-align: right;
+ padding-bottom: 0.3rem;
+}
+
+.TraceGraph--sidebar {
+ cursor: default;
+ box-shadow: -1px 0 rgba(0, 0, 0, 0.2);
+}
+
+.TraceGraph--help-content > div {
+ margin-top: 1rem;
+}
+
+.TraceGraph--dag {
+ stroke-width: 1.2;
+}
+
+.TraceGraph--dag.is-small {
+ stroke-width: 0.7;
+}
+
+/* DAG minimap */
+
+.TraceGraph--miniMap {
+ align-items: flex-end;
+ bottom: 1rem;
+ display: flex;
+ left: 1rem;
+ position: absolute;
+ z-index: 1;
+}
+
+.TraceGraph--miniMap > .plexus-MiniMap--item {
+ border: 1px solid #777;
+ background: #999;
+ box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
+ margin-right: 1rem;
+ position: relative;
+}
+
+.TraceGraph--miniMap > .plexus-MiniMap--map {
+ /* dynamic widht, height */
+ box-sizing: content-box;
+ cursor: not-allowed;
+}
+
+.TraceGraph--miniMap .plexus-MiniMap--mapActive {
+ /* dynamic: width, height, transform */
+ background: #ccc;
+ position: absolute;
+}
+
+.TraceGraph--miniMap > .plexus-MiniMap--button {
+ background: #ccc;
+ color: #888;
+ cursor: pointer;
+ font-size: 1.6em;
+ line-height: 0;
+ padding: 0.1rem;
+}
+
+.TraceGraph--miniMap > .plexus-MiniMap--button:hover {
+ background: #ddd;
+}
diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.js b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.js
new file mode 100644
index 0000000000..f6ee4a5fa6
--- /dev/null
+++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.js
@@ -0,0 +1,342 @@
+// @flow
+
+// Copyright (c) 2018 The Jaeger Authors.
+//
+// Licensed 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 * as React from 'react';
+import { Card, Icon, Button, Tooltip } from 'antd';
+import { DirectedGraph, LayoutManager } from '@jaegertracing/plexus';
+import DRange from 'drange';
+
+import { getNodeDrawer, MODE_SERVICE, MODE_TIME, MODE_SELFTIME, HELP_TABLE } from './OpNode';
+import convPlexus from '../../../model/trace-dag/convPlexus';
+import TraceDag from '../../../model/trace-dag/TraceDag';
+
+import type { Trace, Span, KeyValuePair } from '../../../types/trace';
+
+import './TraceGraph.css';
+
+type SumSpan = {
+ count: number,
+ errors: number,
+ time: number,
+ percent: number,
+ selfTime: number,
+ percentSelfTime: number,
+};
+
+type Props = {
+ headerHeight: number,
+ trace: Trace,
+};
+type State = {
+ showHelp: boolean,
+ mode: string,
+};
+
+const { classNameIsSmall } = DirectedGraph.propsFactories;
+
+export function setOnEdgePath(e: any) {
+ return e.followsFrom ? { strokeDasharray: 4 } : {};
+}
+
+function extendFollowsFrom(edges: any, nodes: any) {
+ return edges.map(e => {
+ let hasChildOf = true;
+ if (typeof e.to === 'number') {
+ const n = nodes[e.to];
+ hasChildOf = n.members.some(
+ m => m.span.references && m.span.references.some(r => r.refType === 'CHILD_OF')
+ );
+ }
+ return { ...e, followsFrom: !hasChildOf };
+ });
+}
+
+export function isError(tags: Array) {
+ if (tags) {
+ const errorTag = tags.find(t => t.key === 'error');
+ if (errorTag) {
+ return errorTag.value;
+ }
+ }
+ return false;
+}
+
+function setOnEdgesContainer(state: Object) {
+ const { zoomTransform } = state;
+ if (!zoomTransform) {
+ return null;
+ }
+ const { k } = zoomTransform;
+ const opacity = 0.1 + k * 0.9;
+ return { style: { opacity } };
+}
+
+const HELP_CONTENT = (
+
+ {HELP_TABLE}
+
+
+
+
+
+
+ |
+ Service |
+ Colored by service |
+
+
+
+
+ |
+ Time |
+ Colored by total time |
+
+
+
+
+ |
+ Selftime |
+ Colored by self time |
+
+
+
+
+
+
+
+
+);
+
+export default class TraceGraph extends React.PureComponent {
+ props: Props;
+ state: State;
+
+ parentChildOfMap: { [string]: Span[] };
+ cache: any;
+
+ layoutManager: LayoutManager;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ showHelp: false,
+ mode: MODE_SERVICE,
+ };
+ this.layoutManager = new LayoutManager({ useDotEdges: true, splines: 'polyline' });
+ }
+
+ componentWillUnmount() {
+ this.layoutManager.stopAndRelease();
+ }
+
+ calculateTraceDag(): TraceDag {
+ const traceDag: TraceDag = new TraceDag();
+ traceDag._initFromTrace(this.props.trace, {
+ count: 0,
+ errors: 0,
+ time: 0,
+ percent: 0,
+ selfTime: 0,
+ percentSelfTime: 0,
+ });
+
+ traceDag.nodesMap.forEach(n => {
+ const ntime = n.members.reduce((p, m) => p + m.span.duration, 0);
+ const numErrors = n.members.reduce((p, m) => (p + isError(m.span.tags) ? 1 : 0), 0);
+ const childDurationsDRange = n.members.reduce((p, m) => {
+ // Using DRange to handle overlapping spans (fork-join)
+ const cdr = new DRange(m.span.startTime, m.span.startTime + m.span.duration).intersect(
+ this.getChildOfDrange(m.span.spanID)
+ );
+ return p + cdr.length;
+ }, 0);
+ const stime = ntime - childDurationsDRange;
+ const nd = {
+ count: n.members.length,
+ errors: numErrors,
+ time: ntime,
+ percent: 100 / this.props.trace.duration * ntime,
+ selfTime: stime,
+ percentSelfTime: 100 / ntime * stime,
+ };
+ // eslint-disable-next-line no-param-reassign
+ n.data = nd;
+ });
+ return traceDag;
+ }
+
+ getChildOfDrange(parentID: string): number {
+ const childrenDrange = new DRange();
+ this.getChildOfSpans(parentID).forEach(s => {
+ // -1 otherwise it will take for each child a micro (incluse,exclusive)
+ childrenDrange.add(s.startTime, s.startTime + (s.duration <= 0 ? 0 : s.duration - 1));
+ });
+ return childrenDrange;
+ }
+
+ getChildOfSpans(parentID: string): Span[] {
+ if (!this.parentChildOfMap) {
+ this.parentChildOfMap = {};
+ this.props.trace.spans.forEach(s => {
+ if (s.references) {
+ // Filter for CHILD_OF we don't want to calculate FOLLOWS_FROM (prod-cons)
+ const parentIDs = s.references.filter(r => r.refType === 'CHILD_OF').map(r => r.spanID);
+ parentIDs.forEach((pID: string) => {
+ this.parentChildOfMap[pID] = this.parentChildOfMap[pID] || [];
+ this.parentChildOfMap[pID].push(s);
+ });
+ }
+ });
+ }
+ return this.parentChildOfMap[parentID] || [];
+ }
+
+ toggleNodeMode(newMode: string) {
+ this.setState({ mode: newMode });
+ }
+
+ showHelp = () => {
+ this.setState({ showHelp: true });
+ };
+
+ closeSidebar = () => {
+ this.setState({ showHelp: false });
+ };
+
+ render() {
+ const { headerHeight, trace } = this.props;
+ const { showHelp, mode } = this.state;
+ if (!trace) {
+ return No trace found
;
+ }
+
+ // Caching edges/vertices so that DirectedGraph is not redrawn
+ let ev = this.cache;
+ if (!ev) {
+ const traceDag = this.calculateTraceDag();
+ const nodes = [...traceDag.nodesMap.values()];
+ ev = convPlexus(traceDag.nodesMap);
+ ev.edges = extendFollowsFrom(ev.edges, nodes);
+ this.cache = ev;
+ }
+
+ return (
+
+
+
+ Experimental
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+
+ {showHelp && (
+
+
+
+
+ }
+ >
+ {HELP_CONTENT}
+
+
+ )}
+
+
+ );
+ }
+}
diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.test.js b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.test.js
new file mode 100644
index 0000000000..ad58f6f272
--- /dev/null
+++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.test.js
@@ -0,0 +1,123 @@
+// Copyright (c) 2018 The Jaeger Authors.
+//
+// Licensed 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 from 'react';
+import { shallow } from 'enzyme';
+
+import transformTraceData from '../../../model/transform-trace-data';
+
+import TraceGraph, { setOnEdgePath } from './TraceGraph';
+import { MODE_SERVICE, MODE_TIME, MODE_SELFTIME } from './OpNode';
+
+const testTrace = require('./testTrace.json');
+
+function assertData(nodes, service, operation, count, errors, time, percent, selfTime) {
+ const d = nodes.find(n => n.service === service && n.operation === operation).data;
+ expect(d).toBeDefined();
+ expect(d.count).toBe(count);
+ expect(d.errors).toBe(errors);
+ expect(d.time).toBe(time * 1000);
+ expect(d.percent).toBeCloseTo(percent, 2);
+ expect(d.selfTime).toBe(selfTime * 1000);
+}
+
+describe('', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ const props = {
+ headerHeight: 60,
+ trace: transformTraceData(testTrace),
+ };
+ wrapper = shallow();
+ });
+
+ it('it does not explode', () => {
+ expect(wrapper).toBeDefined();
+ expect(wrapper.find('.TraceGraph--menu').length).toBe(1);
+ expect(wrapper.find('Button').length).toBe(3);
+ });
+
+ it('it calculates TraceGraph', () => {
+ const traceDag = wrapper.instance().calculateTraceDag();
+ expect(traceDag.nodesMap.size).toBe(9);
+ const nodes = [...traceDag.nodesMap.values()];
+ assertData(nodes, 'service1', 'op1', 1, 0, 390, 39, 224);
+ // accumulate data (count,times)
+ assertData(nodes, 'service1', 'op2', 2, 1, 70, 7, 70);
+ // self-time is substracted from child
+ assertData(nodes, 'service1', 'op3', 1, 0, 66, 6.6, 46);
+ assertData(nodes, 'service2', 'op1', 1, 0, 20, 2, 2);
+ assertData(nodes, 'service2', 'op2', 1, 0, 18, 1.8, 18);
+ // follows_from relation will not influence self-time
+ assertData(nodes, 'service1', 'op4', 1, 0, 20, 2, 20);
+ assertData(nodes, 'service2', 'op3', 1, 0, 200, 20, 200);
+ // fork-join self-times are calculated correctly (self-time drange)
+ assertData(nodes, 'service1', 'op6', 1, 0, 10, 1, 1);
+ assertData(nodes, 'service1', 'op7', 2, 0, 17, 1.7, 17);
+ });
+
+ it('it may show no traces', () => {
+ const props = {};
+ wrapper = shallow();
+ expect(wrapper).toBeDefined();
+ expect(wrapper.find('h1').text()).toBe('No trace found');
+ });
+
+ it('it toggle nodeMode to time', () => {
+ const mode = MODE_SERVICE;
+ wrapper.setState({ mode });
+ wrapper.instance().toggleNodeMode(MODE_TIME);
+ const modeState = wrapper.state('mode');
+ expect(modeState).toEqual(MODE_TIME);
+ });
+
+ it('it validates button nodeMode change click', () => {
+ const toggleNodeMode = jest.spyOn(wrapper.instance(), 'toggleNodeMode');
+ const btnService = wrapper.find('.TraceGraph--btn-service');
+ expect(btnService.length).toBe(1);
+ btnService.simulate('click');
+ expect(toggleNodeMode).toHaveBeenCalledWith(MODE_SERVICE);
+ const btnTime = wrapper.find('.TraceGraph--btn-time');
+ expect(btnTime.length).toBe(1);
+ btnTime.simulate('click');
+ expect(toggleNodeMode).toHaveBeenCalledWith(MODE_TIME);
+ const btnSelftime = wrapper.find('.TraceGraph--btn-selftime');
+ expect(btnSelftime.length).toBe(1);
+ btnSelftime.simulate('click');
+ expect(toggleNodeMode).toHaveBeenCalledWith(MODE_SELFTIME);
+ });
+
+ it('it shows help', () => {
+ const showHelp = false;
+ wrapper.setState({ showHelp });
+ wrapper.instance().showHelp();
+ expect(wrapper.state('showHelp')).toBe(true);
+ });
+
+ it('it hides help', () => {
+ const showHelp = true;
+ wrapper.setState({ showHelp });
+ wrapper.instance().closeSidebar();
+ expect(wrapper.state('showHelp')).toBe(false);
+ });
+
+ it('it uses stroke-dash edges for followsFrom', () => {
+ const edge = { from: 0, to: 1, followsFrom: true };
+ expect(setOnEdgePath(edge)).toEqual({ strokeDasharray: 4 });
+
+ const edge2 = { from: 0, to: 1, followsFrom: false };
+ expect(setOnEdgePath(edge2)).toEqual({});
+ });
+});
diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/testTrace.json b/packages/jaeger-ui/src/components/TracePage/TraceGraph/testTrace.json
new file mode 100644
index 0000000000..684bf5c868
--- /dev/null
+++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/testTrace.json
@@ -0,0 +1,284 @@
+{
+ "traceID": "trace-123",
+ "spans": [
+ {
+ "traceID": "trace-123",
+ "spanID": "span-1",
+ "flags": 1,
+ "operationName": "op1",
+ "startTime": 1542666452979000,
+ "duration": 390000,
+ "references": [],
+ "tags": [
+ {
+ "key": "span.kind",
+ "type": "string",
+ "value": "server"
+ }
+ ],
+ "logs": [],
+ "processID": "p1",
+ "warnings": null
+ },
+ {
+ "traceID": "trace-123",
+ "spanID": "span-2",
+ "flags": 1,
+ "operationName": "op2",
+ "startTime": 1542666453104000,
+ "duration": 33000,
+ "references": [
+ {
+ "refType": "CHILD_OF",
+ "traceID": "trace-123",
+ "spanID": "span-1"
+ }
+ ],
+ "tags": [
+ {
+ "key": "span.kind",
+ "type": "string",
+ "value": "client"
+ },
+ {
+ "key": "error",
+ "type": "bool",
+ "value": "true"
+ }
+ ],
+ "logs": [],
+ "processID": "p1",
+ "warnings": null
+ },
+ {
+ "traceID": "trace-123",
+ "spanID": "span-2_1",
+ "flags": 1,
+ "operationName": "op2",
+ "startTime": 1542666453229000,
+ "duration": 37000,
+ "references": [
+ {
+ "refType": "CHILD_OF",
+ "traceID": "trace-123",
+ "spanID": "span-1"
+ }
+ ],
+ "tags": [
+ {
+ "key": "span.kind",
+ "type": "string",
+ "value": "client"
+ }
+ ],
+ "logs": [],
+ "processID": "p1",
+ "warnings": null
+ },
+ {
+ "traceID": "trace-123",
+ "spanID": "span-3",
+ "flags": 1,
+ "operationName": "op3",
+ "startTime": 1542666453159000,
+ "duration": 66000,
+ "references": [
+ {
+ "refType": "CHILD_OF",
+ "traceID": "trace-123",
+ "spanID": "span-1"
+ }
+ ],
+ "tags": [
+ {
+ "key": "span.kind",
+ "type": "string",
+ "value": "client"
+ }
+ ],
+ "logs": [],
+ "processID": "p1",
+ "warnings": null
+ },
+ {
+ "traceID": "trace-123",
+ "spanID": "span-4",
+ "flags": 1,
+ "operationName": "op1",
+ "startTime": 1542666453179000,
+ "duration": 20000,
+ "references": [
+ {
+ "refType": "CHILD_OF",
+ "traceID": "trace-123",
+ "spanID": "span-3"
+ }
+ ],
+ "tags": [
+ {
+ "key": "span.kind",
+ "type": "string",
+ "value": "server"
+ }
+ ],
+ "logs": [],
+ "processID": "p2",
+ "warnings": null
+ },
+ {
+ "traceID": "trace-123",
+ "spanID": "span-5",
+ "flags": 1,
+ "operationName": "op2",
+ "startTime": 1542666453180000,
+ "duration": 18000,
+ "references": [
+ {
+ "refType": "CHILD_OF",
+ "traceID": "trace-123",
+ "spanID": "span-4"
+ }
+ ],
+ "tags": [
+ {
+ "key": "db.type",
+ "type": "string",
+ "value": "sql"
+ }
+ ],
+ "logs": [],
+ "processID": "p2",
+ "warnings": null
+ },
+ {
+ "traceID": "trace-123",
+ "spanID": "span-6",
+ "flags": 1,
+ "operationName": "op4",
+ "startTime": 1542666453279000,
+ "duration": 20000,
+ "references": [
+ {
+ "refType": "CHILD_OF",
+ "traceID": "trace-123",
+ "spanID": "span-1"
+ }
+ ],
+ "tags": [
+ {
+ "key": "span.kind",
+ "type": "string",
+ "value": "producer"
+ }
+ ],
+ "logs": [],
+ "processID": "p1",
+ "warnings": null
+ },
+ {
+ "traceID": "trace-123",
+ "spanID": "span-7",
+ "flags": 1,
+ "operationName": "op3",
+ "startTime": 1542666453779000,
+ "duration": 200000,
+ "references": [
+ {
+ "refType": "FOLLOWS_FROM",
+ "traceID": "trace-123",
+ "spanID": "span-6"
+ }
+ ],
+ "tags": [
+ {
+ "key": "span.kind",
+ "type": "string",
+ "value": "consumer"
+ }
+ ],
+ "logs": [],
+ "processID": "p2",
+ "warnings": null
+ },
+ {
+ "traceID": "trace-123",
+ "spanID": "span-12",
+ "flags": 1,
+ "operationName": "op6",
+ "startTime": 1542666453309000,
+ "duration": 10000,
+ "references": [
+ {
+ "refType": "CHILD_OF",
+ "traceID": "trace-123",
+ "spanID": "span-1"
+ }
+ ],
+ "tags": [],
+ "logs": [],
+ "processID": "p1",
+ "warnings": null
+ },
+ {
+ "traceID": "trace-123",
+ "spanID": "span-13",
+ "flags": 1,
+ "operationName": "op7",
+ "startTime": 1542666453310000,
+ "duration": 9000,
+ "references": [
+ {
+ "refType": "CHILD_OF",
+ "traceID": "trace-123",
+ "spanID": "span-12"
+ }
+ ],
+ "tags": [],
+ "logs": [],
+ "processID": "p1",
+ "warnings": null
+ },
+ {
+ "traceID": "trace-123",
+ "spanID": "span-14",
+ "flags": 1,
+ "operationName": "op7",
+ "startTime": 1542666453311000,
+ "duration": 8000,
+ "references": [
+ {
+ "refType": "CHILD_OF",
+ "traceID": "trace-123",
+ "spanID": "span-12"
+ }
+ ],
+ "tags": [],
+ "logs": [],
+ "processID": "p1",
+ "warnings": null
+ }
+ ],
+ "processes": {
+ "p1": {
+ "serviceName": "service1",
+ "tags": [
+ {
+ "key": "hostname",
+ "type": "string",
+ "value": "foobar.org"
+ }
+ ]
+ },
+ "p2": {
+ "serviceName": "service2",
+ "tags": [
+ {
+ "key": "hostname",
+ "type": "string",
+ "value": "foobar.org"
+ }
+ ]
+ }
+ },
+ "warnings": null
+}
diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader.css b/packages/jaeger-ui/src/components/TracePage/TracePageHeader.css
index ec5db972bb..c247d775b5 100644
--- a/packages/jaeger-ui/src/components/TracePage/TracePageHeader.css
+++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader.css
@@ -17,6 +17,7 @@ limitations under the License.
.TracePageHeader--titleRow {
align-items: center;
display: flex;
+ padding-right: 0.5rem;
}
.TracePageHeader--titleRowEmbed {
diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader.js
index 8689264b7c..5b0c120d4e 100644
--- a/packages/jaeger-ui/src/components/TracePage/TracePageHeader.js
+++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader.js
@@ -15,7 +15,7 @@
// limitations under the License.
import * as React from 'react';
-import { Button, Dropdown, Icon, Input, Menu } from 'antd';
+import { Button, Dropdown, Input, Menu, Icon } from 'antd';
import IoChevronDown from 'react-icons/lib/io/chevron-down';
import IoChevronRight from 'react-icons/lib/io/chevron-right';
import IoIosFilingOutline from 'react-icons/lib/io/ios-filing-outline';
@@ -35,7 +35,9 @@ type TracePageHeaderProps = {
traceID: string,
name: String,
slimView: boolean,
+ traceGraphView: boolean,
onSlimViewClicked: () => void,
+ onTraceGraphViewClicked: () => void,
updateTextFilter: string => void,
textFilter: string,
prevResult: () => void,
@@ -103,7 +105,9 @@ export function TracePageHeaderFn(props: TracePageHeaderProps) {
traceID,
name,
slimView,
+ traceGraphView,
onSlimViewClicked,
+ onTraceGraphViewClicked,
updateTextFilter,
textFilter,
prevResult,
@@ -119,6 +123,9 @@ export function TracePageHeaderFn(props: TracePageHeaderProps) {
const viewMenu = (