From 0a36984faf907ea520011edc432ebaeb94ab186c Mon Sep 17 00:00:00 2001 From: copa2 Date: Tue, 18 Dec 2018 22:04:25 +0100 Subject: [PATCH] Add a TraceGraph view (#273) (#276) Add alternative view in TracePage which allows to see count, avg. time, total time and self time for a given trace grouped by service and operation. Signed-off-by: Patrick Coray --- packages/jaeger-ui/config-overrides.js | 6 + packages/jaeger-ui/package.json | 2 + .../TracePage/TraceGraph/OpNode.css | 54 +++ .../components/TracePage/TraceGraph/OpNode.js | 126 +++++++ .../TracePage/TraceGraph/OpNode.test.js | 89 +++++ .../TracePage/TraceGraph/TraceGraph.css | 112 ++++++ .../TracePage/TraceGraph/TraceGraph.js | 342 ++++++++++++++++++ .../TracePage/TraceGraph/TraceGraph.test.js | 123 +++++++ .../TracePage/TraceGraph/testTrace.json | 284 +++++++++++++++ .../components/TracePage/TracePageHeader.css | 1 + .../components/TracePage/TracePageHeader.js | 15 +- .../src/components/TracePage/index.js | 44 ++- .../jaeger-ui/src/model/trace-dag/DagNode.js | 8 +- .../jaeger-ui/src/model/trace-dag/TraceDag.js | 1 + yarn.lock | 13 + 15 files changed, 1201 insertions(+), 19 deletions(-) create mode 100644 packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.css create mode 100644 packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.js create mode 100644 packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.test.js create mode 100644 packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.css create mode 100644 packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.js create mode 100644 packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.test.js create mode 100644 packages/jaeger-ui/src/components/TracePage/TraceGraph/testTrace.json 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
DurationOperationSelf 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} +
+ + + + + + + + + + + + + + + + + + +
+ + ServiceColored by service
+ + TimeColored by total time
+ + SelftimeColored by self time
+
+
+ + + + ChildOf + + + + FollowsFrom + + +
+
+); + +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 = ( + + {traceGraphView ? 'Trace Timeline' : 'Trace Graph'} + + - - {archiveButtonVisible && (