From bdc0220d4edc22bf0d286c0f2bb6afb05ee0256d Mon Sep 17 00:00:00 2001 From: copa2 Date: Tue, 18 Dec 2018 22:04:25 +0100 Subject: [PATCH 01/46] 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 Signed-off-by: Everett Ross --- 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 && ( - - )} -

+

{traces.length} Trace{traces.length > 1 && 's'}

+ + {showStandaloneLink && ( + + + + )} @@ -172,16 +177,10 @@ export default class SearchResults extends React.PureComponent ))} diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.test.js b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.test.js index 4642818d52..1ca213e1ee 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.test.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.test.js @@ -54,8 +54,8 @@ describe('', () => { expect(wrapper.find(ScatterPlot).length).toBe(0); }); - it('hide DiffSelection if is an embedded component', () => { - wrapper.setProps({ embed: true, getSearchURL: () => 'SEARCH_URL' }); + it('hide DiffSelection when disableComparisons = true', () => { + wrapper.setProps({ disableComparisons: true }); expect(wrapper.find(DiffSelection).length).toBe(0); }); diff --git a/packages/jaeger-ui/src/components/SearchTracePage/index.js b/packages/jaeger-ui/src/components/SearchTracePage/index.js index 2875723a6f..184a0289c9 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/index.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/index.js @@ -20,17 +20,17 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import store from 'store'; -import * as jaegerApiActions from '../../actions/jaeger-api'; -import { actions as traceDiffActions } from '../TraceDiff/duck'; import SearchForm from './SearchForm'; import SearchResults, { sortFormSelector } from './SearchResults'; +import { isSameQuery } from './url'; +import * as jaegerApiActions from '../../actions/jaeger-api'; import ErrorMessage from '../common/ErrorMessage'; import LoadingIndicator from '../common/LoadingIndicator'; +import { getLocation as getTraceLocation } from '../TracePage/url'; +import { actions as traceDiffActions } from '../TraceDiff/duck'; import { fetchedState } from '../../constants'; import { sortTraces } from '../../model/search'; import getLastXformCacher from '../../utils/get-last-xform-cacher'; -import prefixUrl from '../../utils/prefix-url'; -import { isEmbed, VERSION_API } from '../../utils/embedded'; import './index.css'; import JaegerLogo from '../../img/jaeger-logo.svg'; @@ -41,12 +41,14 @@ export class SearchTracePageImpl extends Component { const { diffCohort, fetchMultipleTraces, + fetchServiceOperations, + fetchServices, + isHomepage, + queryOfResults, searchTraces, urlQueryParams, - fetchServices, - fetchServiceOperations, } = this.props; - if (urlQueryParams.service || urlQueryParams.traceID) { + if (!isHomepage && urlQueryParams && !isSameQuery(urlQueryParams, queryOfResults)) { searchTraces(urlQueryParams); } const needForDiffs = diffCohort.filter(ft => ft.state == null).map(ft => ft.id); @@ -61,15 +63,8 @@ export class SearchTracePageImpl extends Component { } goToTrace = traceID => { - const url = this.props.embed - ? `/trace/${traceID}?embed=${VERSION_API}&fromSearch=${encodeURIComponent(this.getSearchURL())}` - : `/trace/${traceID}`; - this.props.history.push(prefixUrl(url)); - }; - - getSearchURL = () => { - const { embed: _, ...urlQuery } = this.props.query; - return `/search?${queryString.stringify(urlQuery)}`; + const { queryOfResults } = this.props; + this.props.history.push(getTraceLocation(traceID, { fromSearch: queryOfResults })); }; render() { @@ -77,6 +72,7 @@ export class SearchTracePageImpl extends Component { cohortAddTrace, cohortRemoveTrace, diffCohort, + embedded, errors, isHomepage, loadingServices, @@ -84,8 +80,7 @@ export class SearchTracePageImpl extends Component { maxTraceDuration, services, traceResults, - embed, - hideGraph, + queryOfResults, } = this.props; const hasTraceResults = traceResults && traceResults.length > 0; const showErrors = errors && !loadingTraces; @@ -93,7 +88,7 @@ export class SearchTracePageImpl extends Component { return (
- {!embed && ( + {!embedded && (

Find Traces

@@ -101,7 +96,7 @@ export class SearchTracePageImpl extends Component {
)} - + {showErrors && (

There was an error querying for traces:

@@ -111,44 +106,43 @@ export class SearchTracePageImpl extends Component { {!showErrors && ( )} - {showLogo && - !embed && ( - presentation - )} + {showLogo && ( + presentation + )}
); } } - SearchTracePageImpl.propTypes = { - query: PropTypes.object, isHomepage: PropTypes.bool, - embed: PropTypes.bool, - hideGraph: PropTypes.bool, // eslint-disable-next-line react/forbid-prop-types traceResults: PropTypes.array, diffCohort: PropTypes.array, cohortAddTrace: PropTypes.func, cohortRemoveTrace: PropTypes.func, + embedded: PropTypes.shape({ + searchHideGraph: PropTypes.bool, + }), maxTraceDuration: PropTypes.number, loadingServices: PropTypes.bool, loadingTraces: PropTypes.bool, @@ -156,6 +150,10 @@ SearchTracePageImpl.propTypes = { service: PropTypes.string, limit: PropTypes.string, }), + queryOfResults: PropTypes.shape({ + service: PropTypes.string, + limit: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + }), services: PropTypes.arrayOf( PropTypes.shape({ name: PropTypes.string, @@ -178,11 +176,12 @@ SearchTracePageImpl.propTypes = { const stateTraceXformer = getLastXformCacher(stateTrace => { const { traces: traceMap, search } = stateTrace; - const { results, state, error: traceError } = search; + const { query, results, state, error: traceError } = search; + const loadingTraces = state === fetchedState.LOADING; const traces = results.map(id => traceMap[id].data); const maxDuration = Math.max.apply(null, traces.map(tr => tr.duration)); - return { traces, maxDuration, traceError, loadingTraces }; + return { traces, maxDuration, traceError, loadingTraces, query }; }); const stateTraceDiffXformer = getLastXformCacher((stateTrace, stateTraceDiff) => { @@ -215,12 +214,14 @@ const stateServicesXformer = getLastXformCacher(stateServices => { // export to test export function mapStateToProps(state) { - const query = queryString.parse(state.router.location.search); - const { hideGraph } = queryString.parse(state.router.location.search); + const { embedded, router, services: stServices, traceDiff } = state; + const query = queryString.parse(router.location.search); const isHomepage = !Object.keys(query).length; - const { traces, maxDuration, traceError, loadingTraces } = stateTraceXformer(state.trace); - const diffCohort = stateTraceDiffXformer(state.trace, state.traceDiff); - const { loadingServices, services, serviceError } = stateServicesXformer(state.services); + const { query: queryOfResults, traces, maxDuration, traceError, loadingTraces } = stateTraceXformer( + state.trace + ); + const diffCohort = stateTraceDiffXformer(state.trace, traceDiff); + const { loadingServices, services, serviceError } = stateServicesXformer(stServices); const errors = []; if (traceError) { errors.push(traceError); @@ -231,10 +232,9 @@ export function mapStateToProps(state) { const sortBy = sortFormSelector(state, 'sortBy'); const traceResults = sortedTracesXformer(traces, sortBy); return { - query, + queryOfResults, diffCohort, - embed: isEmbed(state.router.location.search), - hideGraph: hideGraph !== undefined, + embedded, isHomepage, loadingServices, loadingTraces, @@ -243,7 +243,7 @@ export function mapStateToProps(state) { errors: errors.length ? errors : null, maxTraceDuration: maxDuration, sortTracesBy: sortBy, - urlQueryParams: query, + urlQueryParams: Object.keys(query).length > 0 ? query : null, }; } diff --git a/packages/jaeger-ui/src/components/SearchTracePage/index.test.js b/packages/jaeger-ui/src/components/SearchTracePage/index.test.js index ae55e8f774..07a541705b 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/index.test.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/index.test.js @@ -28,7 +28,6 @@ jest.mock('store'); /* eslint-disable import/first */ import React from 'react'; -import queryString from 'query-string'; import { shallow, mount } from 'enzyme'; import store from 'store'; @@ -39,9 +38,9 @@ import { fetchedState } from '../../constants'; import traceGenerator from '../../demo/trace-generators'; import { MOST_RECENT } from '../../model/order-by'; import transformTraceData from '../../model/transform-trace-data'; -import { VERSION_API } from '../../utils/embedded'; describe('', () => { + const queryOfResults = {}; let wrapper; let traceResults; let props; @@ -49,13 +48,13 @@ describe('', () => { beforeEach(() => { traceResults = [{ traceID: 'a', spans: [], processes: {} }, { traceID: 'b', spans: [], processes: {} }]; props = { + queryOfResults, traceResults, - embed: false, + diffCohort: [], isHomepage: false, loadingServices: false, loadingTraces: false, maxTraceDuration: 100, - diffCohort: [], numberOfTraceResults: traceResults.length, services: null, sortTracesBy: MOST_RECENT, @@ -83,31 +82,18 @@ describe('', () => { store.get = oldFn; }); - it('return the searchpath if call getSearchURL', () => { - const query = - 'end=1542906238737000&limit=20&lookback=1h&maxDuration&minDuration&service=productpage&start=1542902638737000'; - wrapper = mount(); - expect(wrapper.instance().getSearchURL()).toBe(`/search?${queryString.stringify(query)}`); - }); - - it('Push to history the correct url when goToTrace', () => { - const query = - 'end=1542906238737000&limit=20&lookback=1h&maxDuration&minDuration&service=productpage&start=1542902638737000'; - const historyMock = { push: jest.fn() }; + it('goToTrace pushes the trace URL with {fromSearch: true} to history', () => { const traceID = '15810714d6a27450'; + const query = 'some-query'; + const historyPush = jest.fn(); + const historyMock = { push: historyPush }; wrapper = mount(); wrapper.instance().goToTrace(traceID); - expect(historyMock.push.mock.calls.length).toBe(1); - expect(historyMock.push.mock.calls[0][0]).toBe(`/trace/${traceID}`); - - // Embed Mode - wrapper.setProps({ embed: true }); - wrapper.instance().goToTrace(traceID); - expect(historyMock.push.mock.calls[1][0]).toBe( - `/trace/${traceID}?embed=${VERSION_API}&fromSearch=${encodeURIComponent( - wrapper.instance().getSearchURL() - )}` - ); + expect(historyPush.mock.calls.length).toBe(1); + expect(historyPush.mock.calls[0][0]).toEqual({ + pathname: `/trace/${traceID}`, + state: { fromSearch: queryOfResults }, + }); }); it('shows a loading indicator if loading services', () => { @@ -180,13 +166,12 @@ describe('mapStateToProps()', () => { expect(diffCohort[0].data.traceID).toBe(trace.traceID); expect(rest).toEqual({ - embed: false, - hideGraph: false, - query: {}, + embedded: undefined, + queryOfResults: undefined, isHomepage: true, // the redux-form `formValueSelector` mock returns `null` for "sortBy" sortTracesBy: null, - urlQueryParams: {}, + urlQueryParams: null, services: [ { name: stateServices.services[0], diff --git a/packages/jaeger-ui/src/components/SearchTracePage/url.js b/packages/jaeger-ui/src/components/SearchTracePage/url.js index 130485c8e6..44b2193778 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/url.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/url.js @@ -19,6 +19,12 @@ import { matchPath } from 'react-router-dom'; import prefixUrl from '../../utils/prefix-url'; +import type { SearchQuery } from '../../types/search'; + +function eqEq(a: ?(string | number), b: ?(string | number)) { + return (a == null && b == null) || String(a) === String(b); +} + export const ROUTE_PATH = prefixUrl('/search'); const ROUTE_MATCHER = { path: ROUTE_PATH, strict: true, exact: true }; @@ -28,6 +34,23 @@ export function matches(path: string) { } export function getUrl(query?: ?Object) { - const search = query ? queryString.stringify(query) : ''; - return prefixUrl(`/search?${search}`); + const search = query ? `?${queryString.stringify(query)}` : ''; + return prefixUrl(`/search${search}`); +} + +export function isSameQuery(a: SearchQuery, b: SearchQuery) { + if (Boolean(a) !== Boolean(b)) { + return false; + } + return ( + eqEq(a.end, b.end) && + eqEq(a.limit, b.limit) && + eqEq(a.lookback, b.lookback) && + eqEq(a.maxDuration, b.maxDuration) && + eqEq(a.minDuration, b.minDuration) && + eqEq(a.operation, b.operation) && + eqEq(a.service, b.service) && + eqEq(a.start, b.start) && + eqEq(a.tags, b.tags) + ); } diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.css b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.css index f09ba89814..13c1c79fd9 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.css +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.css @@ -27,7 +27,7 @@ limitations under the License. } .DiffNode.is-changed { - color: rgba(0, 0, 0, 0.85); + color: var(--tx-color-title); } .DiffNode.is-more { diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.css b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.css index 2e6348c6c1..3ba96767e2 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.css +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.css @@ -15,7 +15,7 @@ limitations under the License. */ .CohortTable--traceName { - color: rgba(0, 0, 0, 0.85); + color: var(--tx-color-title); } .CohortTable--needMoreMsg { diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceHeader.css b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceHeader.css index 8e4b005cce..d7fd1aa430 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceHeader.css +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceHeader.css @@ -40,7 +40,7 @@ limitations under the License. } .TraecDiffHeader--traceTitleChevron { - color: rgba(0, 0, 0, 0.85); + color: var(--tx-color-title); font-size: 0.75em; margin-right: 0.75em; position: absolute; diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceHeader.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceHeader.js index 35a313c2ef..089f079356 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceHeader.js +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceHeader.js @@ -80,7 +80,7 @@ export default function TraceHeader(props: Props) { {traceID ? ( - {' '} + {' '} {(traceID || '').slice(0, 7)} diff --git a/packages/jaeger-ui/src/components/TracePage/KeyboardShortcutsHelp.js b/packages/jaeger-ui/src/components/TracePage/KeyboardShortcutsHelp.js deleted file mode 100644 index 98a282d2ca..0000000000 --- a/packages/jaeger-ui/src/components/TracePage/KeyboardShortcutsHelp.js +++ /dev/null @@ -1,107 +0,0 @@ -// @flow - -// Copyright (c) 2017 Uber Technologies, Inc. -// -// 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 { Button, Modal, Table } from 'antd'; - -import { kbdMappings } from './keyboard-shortcuts'; -import track from './KeyboardShortcutsHelp.track'; - -import './KeyboardShortcutsHelp.css'; - -type KeyboardShortcutsHelpProps = { - className: ?string, -}; - -const { Column } = Table; - -const symbolConv = { - up: '↑', - right: '→', - down: '↓', - left: '←', - shift: '⇧', -}; - -const descriptions = { - scrollPageDown: 'Scroll down', - scrollPageUp: 'Scroll up', - scrollToNextVisibleSpan: 'Scroll to the next visible span', - scrollToPrevVisibleSpan: 'Scroll to the previous visible span', - panLeft: 'Pan left', - panLeftFast: 'Pan left — Large', - panRight: 'Pan right', - panRightFast: 'Pan right — Large', - zoomIn: 'Zoom in', - zoomInFast: 'Zoom in — Large', - zoomOut: 'Zoom out', - zoomOutFast: 'Zoom out — Large', - collapseAll: 'Collapse All', - expandAll: 'Expand All', - collapseOne: 'Collapse One Level', - expandOne: 'Expand One Level', - searchSpans: 'Search Spans', - clearSearch: 'Clear Search', -}; - -function convertKeys(keyConfig: string | string[]): string[][] { - const config = Array.isArray(keyConfig) ? keyConfig : [keyConfig]; - return config.map(str => str.split('+').map(part => symbolConv[part] || part.toUpperCase())); -} - -function helpModal() { - track(); - const data = []; - Object.keys(kbdMappings).forEach(title => { - const keyConfigs = convertKeys(kbdMappings[title]); - data.push( - ...keyConfigs.map(config => ({ - key: String(config), - kbds: {config.join(' ')}, - description: descriptions[title], - })) - ); - }); - - const content = ( - - - -
- ); - - Modal.info({ - content, - maskClosable: true, - title: 'Keyboard Shortcuts', - width: '50%', - }); -} - -export default function KeyboardShortcutsHelp(props: KeyboardShortcutsHelpProps) { - const { className } = props; - return ( - - ); -} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.css b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.css index ef16455ae1..3a11034d1f 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.css @@ -18,7 +18,6 @@ limitations under the License. background-color: #a00; color: #fff; position: absolute; - top: 122px; padding: 1px 15px; } diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader.css b/packages/jaeger-ui/src/components/TracePage/TracePageHeader.css deleted file mode 100644 index c247d775b5..0000000000 --- a/packages/jaeger-ui/src/components/TracePage/TracePageHeader.css +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright (c) 2017 Uber Technologies, Inc. - -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. -*/ - -.TracePageHeader--titleRow { - align-items: center; - display: flex; - padding-right: 0.5rem; -} - -.TracePageHeader--titleRowEmbed { - align-items: center; - background-color: #ececec; - border-bottom: 1px solid #d8d8d8; - display: flex; -} - -.TracePageHeader--title, -.TracePageHeader--titleEmbed { - margin: 0; - padding: 0.25rem 0.5rem; -} - -.TracePageHeader--title:hover { - background: #f5f5f5; -} - -.TracePageHeader--overviewItems { - padding: 0 0.5rem 0.25rem 0.5rem; -} - -.TracePageHeader--archiveIcon { - font-size: 1.78em; - margin-right: 0.15em; -} diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader.js deleted file mode 100644 index 5b0c120d4e..0000000000 --- a/packages/jaeger-ui/src/components/TracePage/TracePageHeader.js +++ /dev/null @@ -1,219 +0,0 @@ -// @flow - -// Copyright (c) 2017 Uber Technologies, Inc. -// -// 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 { 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'; -import { Link } from 'react-router-dom'; - -import KeyboardShortcutsHelp from './KeyboardShortcutsHelp'; -import { trackAltViewOpen } from './TracePageHeader.track'; -import TracePageSearchBar from './TracePageSearchBar'; -import LabeledList from '../common/LabeledList'; -import { FALLBACK_TRACE_NAME } from '../../constants'; -import { formatDatetime, formatDuration } from '../../utils/date'; -import prefixUrl from '../../utils/prefix-url'; - -import './TracePageHeader.css'; - -type TracePageHeaderProps = { - traceID: string, - name: String, - slimView: boolean, - traceGraphView: boolean, - onSlimViewClicked: () => void, - onTraceGraphViewClicked: () => void, - updateTextFilter: string => void, - textFilter: string, - prevResult: () => void, - nextResult: () => void, - clearSearch: () => void, - forwardedRef: { current: Input | null }, - resultCount: number, - archiveButtonVisible: boolean, - onArchiveClicked: () => void, - // these props are used by the `HEADER_ITEMS` - // eslint-disable-next-line react/no-unused-prop-types - timestamp: number, - // eslint-disable-next-line react/no-unused-prop-types - duration: number, - // eslint-disable-next-line react/no-unused-prop-types - numServices: number, - // eslint-disable-next-line react/no-unused-prop-types - maxDepth: number, - // eslint-disable-next-line react/no-unused-prop-types - numSpans: number, -}; - -export const HEADER_ITEMS = [ - { - key: 'timestamp', - title: 'Trace Start', - propName: null, - renderer: (props: TracePageHeaderProps) => formatDatetime(props.timestamp), - }, - { - key: 'duration', - title: 'Duration', - propName: null, - renderer: (props: TracePageHeaderProps) => formatDuration(props.duration), - }, - { - key: 'service-count', - title: 'Services', - propName: 'numServices', - renderer: null, - }, - { - key: 'depth', - title: 'Depth', - propName: 'maxDepth', - renderer: null, - }, - { - key: 'span-count', - title: 'Total Spans', - propName: 'numSpans', - renderer: null, - }, -]; - -export function TracePageHeaderFn(props: TracePageHeaderProps) { - const { - archiveButtonVisible, - onArchiveClicked, - duration, - maxDepth, - numSpans, - timestamp, - numServices, - traceID, - name, - slimView, - traceGraphView, - onSlimViewClicked, - onTraceGraphViewClicked, - updateTextFilter, - textFilter, - prevResult, - nextResult, - clearSearch, - resultCount, - forwardedRef, - } = props; - - if (!traceID) { - return null; - } - - const viewMenu = ( - - - {traceGraphView ? 'Trace Timeline' : 'Trace Graph'} - - - - Trace JSON - - - - - Trace JSON (unadjusted) - - - - ); - - const overviewItems = [ - { - key: 'start', - label: 'Trace Start:', - value: formatDatetime(timestamp), - }, - { - key: 'duration', - label: 'Duration:', - value: formatDuration(duration), - }, - { - key: 'svc-count', - label: 'Services:', - value: numServices, - }, - { - key: 'depth', - label: 'Depth:', - value: maxDepth, - }, - { - key: 'span-count', - label: 'Total Spans:', - value: numSpans, - }, - ]; - - return ( -
-
- -

- {slimView ? : } - {name || FALLBACK_TRACE_NAME} -

-
- - - - - - - {archiveButtonVisible && ( - - )} -
- {!slimView && } -
- ); -} - -// ghetto fabulous cast because the 16.3 API is not in flow yet -// https://github.com/facebook/flow/issues/6103 -export default (React: any).forwardRef((props, ref) => ); diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader.test.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader.test.js deleted file mode 100644 index 237b3b3906..0000000000 --- a/packages/jaeger-ui/src/components/TracePage/TracePageHeader.test.js +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// 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, mount } from 'enzyme'; - -import { TracePageHeaderFn as TracePageHeader, HEADER_ITEMS } from './TracePageHeader'; - -describe('', () => { - const defaultProps = { - traceID: 'some-trace-id', - name: 'some-trace-name', - textFilter: '', - updateTextFilter: () => {}, - }; - - let wrapper; - - beforeEach(() => { - wrapper = shallow(); - }); - - it('renders a
', () => { - expect(wrapper.find('header').length).toBe(1); - }); - - it('renders an empty
if no traceID is present', () => { - wrapper = mount(); - expect(wrapper.children().length).toBe(0); - }); - - it('renders the trace title', () => { - const h1 = wrapper.find('h1').first(); - expect(h1.contains(defaultProps.name)).toBeTruthy(); - }); - - it('renders the header items', () => { - wrapper.find('.horizontal .item').forEach((item, i) => { - expect(item.contains(HEADER_ITEMS[i].title)).toBeTruthy(); - expect(item.contains(HEADER_ITEMS[i].renderer(defaultProps.trace))).toBeTruthy(); - }); - }); -}); diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/AltViewOptions.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/AltViewOptions.js new file mode 100644 index 0000000000..de488cbf19 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/AltViewOptions.js @@ -0,0 +1,66 @@ +// @flow + +// Copyright (c) 2018 Uber Technologies, Inc. +// +// 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 { Button, Dropdown, Icon, Menu } from 'antd'; +import { Link } from 'react-router-dom'; + +import { trackAltViewOpen } from './TracePageHeader.track'; +import prefixUrl from '../../../utils/prefix-url'; + +type Props = { + onTraceGraphViewClicked: () => void, + traceGraphView: boolean, + traceID: string, +}; + +export default function AltViewOptions(props: Props) { + const { onTraceGraphViewClicked, traceGraphView, traceID } = props; + const menu = ( + + + {traceGraphView ? 'Trace Timeline' : 'Trace Graph'} + + + + Trace JSON + + + + + Trace JSON (unadjusted) + + + + ); + return ( + + + + ); +} diff --git a/packages/jaeger-ui/src/components/TracePage/KeyboardShortcutsHelp.css b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/KeyboardShortcutsHelp.css similarity index 73% rename from packages/jaeger-ui/src/components/TracePage/KeyboardShortcutsHelp.css rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/KeyboardShortcutsHelp.css index 1cafbe6a76..688f0c4cb0 100644 --- a/packages/jaeger-ui/src/components/TracePage/KeyboardShortcutsHelp.css +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/KeyboardShortcutsHelp.css @@ -19,10 +19,21 @@ limitations under the License. margin: 0 -7px; } +.KeyboardShortcutsHelp--table { + background-color: #fafafa; + max-height: 500px; + overflow: auto; +} + +.KeyboardShortcutsHelp--oddRow { + background-color: #f6f6f6; +} + .KeyboardShortcutsHelp--table kbd { - background: #f5f5f5; - border: 1px solid #e8e8e8; - border-bottom: 1px solid #ddd; + background: #f0f0f0; + border-bottom: 1px solid #bbb; + border-radius: 3px; + border: 1px solid #d4d4d4; color: #000; font-family: monospace; padding: 0.25em 0.3em; diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/KeyboardShortcutsHelp.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/KeyboardShortcutsHelp.js new file mode 100644 index 0000000000..e6cf9299c0 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/KeyboardShortcutsHelp.js @@ -0,0 +1,121 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// 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 { Button, Modal, Table } from 'antd'; + +import keyboardMappings from '../keyboard-mappings'; +import track from './KeyboardShortcutsHelp.track'; + +import './KeyboardShortcutsHelp.css'; + +type Props = { + className: ?string, +}; + +type State = { + visible: boolean, +}; + +const { Column } = Table; + +const SYMBOL_CONV = { + up: '↑', + right: '→', + down: '↓', + left: '←', + shift: '⇧', +}; + +const ODD_ROW_CLASS = 'KeyboardShortcutsHelp--oddRow'; + +function convertKeys(keyConfig: string | string[]): string[][] { + const config = Array.isArray(keyConfig) ? keyConfig : [keyConfig]; + return config.map(str => str.split('+').map(part => SYMBOL_CONV[part] || part.toUpperCase())); +} + +const padLeft = (text: string) => {text}; +const padRight = (text: string) => {text}; +const getRowClass = (_: any, index: number) => (index % 2 > 0 ? ODD_ROW_CLASS : ''); + +let kbdTable: React.Node | null = null; + +function getHelpModal() { + track(); + if (kbdTable) { + return kbdTable; + } + const data = []; + Object.keys(keyboardMappings).forEach(handle => { + const { binding, label } = keyboardMappings[handle]; + const keyConfigs = convertKeys(binding); + data.push( + ...keyConfigs.map(config => ({ + key: String(config), + kbds: {config.join(' ')}, + description: label, + })) + ); + }); + + kbdTable = ( + + + +
+ ); + return kbdTable; +} + +export default class KeyboardShortcutsHelp extends React.PureComponent { + props: Props; + + state = { + visible: false, + }; + + onCtaClicked = () => this.setState({ visible: true }); + + onCloserClicked = () => this.setState({ visible: false }); + + render() { + const { className } = this.props; + return ( + + + + {getHelpModal()} + + + ); + } +} diff --git a/packages/jaeger-ui/src/components/TracePage/KeyboardShortcutsHelp.track.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/KeyboardShortcutsHelp.track.js similarity index 86% rename from packages/jaeger-ui/src/components/TracePage/KeyboardShortcutsHelp.track.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/KeyboardShortcutsHelp.track.js index 4655d0c9df..63f17b3318 100644 --- a/packages/jaeger-ui/src/components/TracePage/KeyboardShortcutsHelp.track.js +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/KeyboardShortcutsHelp.track.js @@ -14,8 +14,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { OPEN } from '../../utils/tracking/common'; -import { trackEvent } from '../../utils/tracking'; +import { OPEN } from '../../../utils/tracking/common'; +import { trackEvent } from '../../../utils/tracking'; const CATEGORY = 'jaeger/ux/trace/kbd-modal'; diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/CanvasSpanGraph.css b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/CanvasSpanGraph.css similarity index 100% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/CanvasSpanGraph.css rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/CanvasSpanGraph.css diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/CanvasSpanGraph.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/CanvasSpanGraph.js similarity index 96% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/CanvasSpanGraph.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/CanvasSpanGraph.js index b6479e59b7..a37e13498b 100644 --- a/packages/jaeger-ui/src/components/TracePage/SpanGraph/CanvasSpanGraph.js +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/CanvasSpanGraph.js @@ -17,7 +17,7 @@ import * as React from 'react'; import renderIntoCanvas from './render-into-canvas'; -import colorGenerator from '../../../utils/color-generator'; +import colorGenerator from '../../../../utils/color-generator'; import './CanvasSpanGraph.css'; diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/CanvasSpanGraph.test.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/CanvasSpanGraph.test.js similarity index 100% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/CanvasSpanGraph.test.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/CanvasSpanGraph.test.js diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/GraphTicks.css b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/GraphTicks.css similarity index 100% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/GraphTicks.css rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/GraphTicks.css diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/GraphTicks.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/GraphTicks.js similarity index 100% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/GraphTicks.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/GraphTicks.js diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/GraphTicks.test.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/GraphTicks.test.js similarity index 100% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/GraphTicks.test.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/GraphTicks.test.js diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/Scrubber.css b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/Scrubber.css similarity index 100% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/Scrubber.css rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/Scrubber.css diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/Scrubber.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/Scrubber.js similarity index 100% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/Scrubber.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/Scrubber.js diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/Scrubber.test.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/Scrubber.test.js similarity index 100% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/Scrubber.test.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/Scrubber.test.js diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/TickLabels.css b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/TickLabels.css similarity index 100% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/TickLabels.css rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/TickLabels.css diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/TickLabels.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/TickLabels.js similarity index 95% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/TickLabels.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/TickLabels.js index 51b52f5de8..9f9e364b6b 100644 --- a/packages/jaeger-ui/src/components/TracePage/SpanGraph/TickLabels.js +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/TickLabels.js @@ -16,7 +16,7 @@ import React from 'react'; -import { formatDuration } from '../../../utils/date'; +import { formatDuration } from '../../../../utils/date'; import './TickLabels.css'; diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/TickLabels.test.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/TickLabels.test.js similarity index 100% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/TickLabels.test.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/TickLabels.test.js diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/ViewingLayer.css b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.css similarity index 98% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/ViewingLayer.css rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.css index 2bdc6ae022..48481be342 100644 --- a/packages/jaeger-ui/src/components/TracePage/SpanGraph/ViewingLayer.css +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.css @@ -15,10 +15,9 @@ limitations under the License. */ .ViewingLayer { + cursor: vertical-text; position: relative; z-index: 1; - margin-bottom: 0.5em; - cursor: vertical-text; } .ViewingLayer--graph { diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/ViewingLayer.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.js similarity index 97% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/ViewingLayer.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.js index 6c2a063b0a..c35df3e88d 100644 --- a/packages/jaeger-ui/src/components/TracePage/SpanGraph/ViewingLayer.js +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.js @@ -19,9 +19,10 @@ import cx from 'classnames'; import GraphTicks from './GraphTicks'; import Scrubber from './Scrubber'; -import type { ViewRange, ViewRangeTimeUpdate } from '../types'; -import type { DraggableBounds, DraggingUpdate } from '../../../utils/DraggableManager'; -import DraggableManager, { updateTypes } from '../../../utils/DraggableManager'; +import DraggableManager, { updateTypes } from '../../../../utils/DraggableManager'; + +import type { ViewRange, ViewRangeTimeUpdate } from '../../types'; +import type { DraggableBounds, DraggingUpdate } from '../../../../utils/DraggableManager'; import './ViewingLayer.css'; diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/ViewingLayer.test.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.test.js similarity index 98% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/ViewingLayer.test.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.test.js index 3e574898c8..d9a17f7af6 100644 --- a/packages/jaeger-ui/src/components/TracePage/SpanGraph/ViewingLayer.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.test.js @@ -18,8 +18,8 @@ import { shallow } from 'enzyme'; import GraphTicks from './GraphTicks'; import Scrubber from './Scrubber'; import ViewingLayer, { dragTypes } from './ViewingLayer'; -import { updateTypes } from '../../../utils/DraggableManager'; -import { polyfill as polyfillAnimationFrame } from '../../../utils/test/requestAnimationFrame'; +import { updateTypes } from '../../../../utils/DraggableManager'; +import { polyfill as polyfillAnimationFrame } from '../../../../utils/test/requestAnimationFrame'; function getViewRange(viewStart, viewEnd) { return { diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/index.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/index.js similarity index 94% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/index.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/index.js index dfbed2cded..dfae541c7d 100644 --- a/packages/jaeger-ui/src/components/TracePage/SpanGraph/index.js +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/index.js @@ -19,8 +19,8 @@ import * as React from 'react'; import CanvasSpanGraph from './CanvasSpanGraph'; import TickLabels from './TickLabels'; import ViewingLayer from './ViewingLayer'; -import type { ViewRange, ViewRangeTimeUpdate } from '../types'; -import type { Span, Trace } from '../../../types/trace'; +import type { ViewRange, ViewRangeTimeUpdate } from '../../types'; +import type { Span, Trace } from '../../../../types/trace'; const TIMELINE_TICK_INTERVAL = 4; @@ -84,7 +84,7 @@ export default class SpanGraph extends React.PureComponent +
diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/index.test.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/index.test.js similarity index 90% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/index.test.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/index.test.js index 5b09425342..88376f8ce7 100644 --- a/packages/jaeger-ui/src/components/TracePage/SpanGraph/index.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/index.test.js @@ -19,9 +19,9 @@ import CanvasSpanGraph from './CanvasSpanGraph'; import SpanGraph from './index'; import TickLabels from './TickLabels'; import ViewingLayer from './ViewingLayer'; -import traceGenerator from '../../../../src/demo/trace-generators'; -import transformTraceData from '../../../model/transform-trace-data'; -import { polyfill as polyfillAnimationFrame } from '../../../utils/test/requestAnimationFrame'; +import traceGenerator from '../../../../demo/trace-generators'; +import transformTraceData from '../../../../model/transform-trace-data'; +import { polyfill as polyfillAnimationFrame } from '../../../../utils/test/requestAnimationFrame'; describe('', () => { polyfillAnimationFrame(window); diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/render-into-canvas.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/render-into-canvas.js similarity index 98% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/render-into-canvas.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/render-into-canvas.js index a76a7d9d50..a1b0e5a944 100644 --- a/packages/jaeger-ui/src/components/TracePage/SpanGraph/render-into-canvas.js +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/render-into-canvas.js @@ -15,7 +15,7 @@ // limitations under the License. // exported for tests -export const BG_COLOR = '#f5f5f5'; +export const BG_COLOR = '#fff'; export const ITEM_ALPHA = 0.8; export const MIN_ITEM_HEIGHT = 2; export const MAX_TOTAL_HEIGHT = 200; diff --git a/packages/jaeger-ui/src/components/TracePage/SpanGraph/render-into-canvas.test.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/render-into-canvas.test.js similarity index 100% rename from packages/jaeger-ui/src/components/TracePage/SpanGraph/render-into-canvas.test.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/render-into-canvas.test.js diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.css b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.css new file mode 100644 index 0000000000..1450ba471c --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.css @@ -0,0 +1,102 @@ +/* +Copyright (c) 2017 Uber Technologies, Inc. + +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. +*/ + +.TracePageHeader > :first-child { + border-bottom: 1px solid #e8e8e8; +} + +.TracePageHeader > :nth-child(2) { + background-color: #eee; + border-bottom: 1px solid #e4e4e4; +} + +.TracePageHeader > :last-child { + background-color: #f8f8f8; + border-bottom: 1px solid #ccc; +} + +/* boost the specificity for cases with only one row -- bg should be white */ +.TracePageHeader > .TracePageHeader--titleRow { + align-items: center; + background-color: #fff; + display: flex; +} + +.TracePageHeader--back { + align-items: center; + align-self: stretch; + background-color: #fafafa; + border-bottom: 1px solid #ddd; + border-right: 1px solid #ddd; + color: inherit; + display: flex; + font-size: 1.4rem; + padding: 0 1rem; + margin-bottom: -1px; +} + +.TracePageHeader--back:hover { + background-color: #f0f0f0; + border-color: #ccc; +} + +.TracePageHeader--titleLink { + align-items: center; + color: var(--tx-color-title); + display: flex; + flex: 1; +} + +.TracePageHeader--titleLink:hover * { + text-decoration: underline; +} + +.TracePageHeader--titleLink:hover > *, +.TracePageHeader--titleLink:hover small { + text-decoration: none; +} + +.TracePageHeader--detailToggle { + font-size: 2.5rem; + transition: transform 0.07s ease-out; +} + +.TracePageHeader--detailToggle.is-expanded { + transform: rotate(90deg); +} + +.TracePageHeader--title { + color: inherit; + flex: 1; + font-size: 1.7em; + line-height: 1em; + margin: 0 0 0 0.5em; + padding: 0.5em 0; +} + +.TracePageHeader--title.is-collapsible { + margin-left: 0; +} + +.TracePageHeader--overviewItems { + border-bottom: 1px solid #e4e4e4; + padding: 0.25rem 0.5rem; +} + +.TracePageHeader--archiveIcon { + font-size: 1.78em; + margin-right: 0.15em; +} diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.js new file mode 100644 index 0000000000..3497c604d7 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.js @@ -0,0 +1,219 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// 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 { Button, Input } from 'antd'; +import _maxBy from 'lodash/maxBy'; +import _values from 'lodash/values'; +import IoAndroidArrowBack from 'react-icons/lib/io/android-arrow-back'; +import IoIosFilingOutline from 'react-icons/lib/io/ios-filing-outline'; +import MdKeyboardArrowRight from 'react-icons/lib/md/keyboard-arrow-right'; +import { Link } from 'react-router-dom'; + +import AltViewOptions from './AltViewOptions'; +import KeyboardShortcutsHelp from './KeyboardShortcutsHelp'; +import SpanGraph from './SpanGraph'; +import TracePageSearchBar from './TracePageSearchBar'; +import LabeledList from '../../common/LabeledList'; +import NewWindowIcon from '../../common/NewWindowIcon'; +import TraceName from '../../common/TraceName'; +import { getTraceName } from '../../../model/trace-viewer'; +import { formatDatetime, formatDuration } from '../../../utils/date'; + +import type { ViewRange, ViewRangeTimeUpdate } from '../types'; +import type { Trace } from '../../../types/trace'; + +import './TracePageHeader.css'; + +type TracePageHeaderEmbedProps = { + canCollapse: boolean, + clearSearch: () => void, + forwardedRef: { current: Input | null }, + hideMap: boolean, + hideSummary: boolean, + linkToStandalone: string, + nextResult: () => void, + onArchiveClicked: () => void, + onSlimViewClicked: () => void, + onTraceGraphViewClicked: () => void, + prevResult: () => void, + resultCount: number, + showArchiveButton: boolean, + showShortcutsHelp: boolean, + showStandaloneLink: boolean, + showViewOptions: boolean, + slimView: boolean, + textFilter: string, + toSearch: string | null, + trace: Trace, + traceGraphView: boolean, + updateNextViewRangeTime: ViewRangeTimeUpdate => void, + updateTextFilter: string => void, + updateViewRangeTime: (number, number) => void, + viewRange: ViewRange, +}; + +export const HEADER_ITEMS = [ + { + key: 'timestamp', + label: 'Trace Start', + renderer: (trace: Trace) => formatDatetime(trace.startTime), + }, + { + key: 'duration', + label: 'Duration', + renderer: (trace: Trace) => formatDuration(trace.duration), + }, + { + key: 'service-count', + label: 'Services', + renderer: (trace: Trace) => new Set(_values(trace.processes).map(p => p.serviceName)).size, + }, + { + key: 'depth', + label: 'Depth', + renderer: (trace: Trace) => _maxBy(trace.spans, 'depth').depth + 1, + }, + { + key: 'span-count', + label: 'Total Spans', + renderer: (trace: Trace) => trace.spans.length, + }, +]; + +export function TracePageHeaderFn(props: TracePageHeaderEmbedProps) { + const { + canCollapse, + clearSearch, + forwardedRef, + hideMap, + hideSummary, + linkToStandalone, + nextResult, + onArchiveClicked, + onSlimViewClicked, + onTraceGraphViewClicked, + prevResult, + resultCount, + showArchiveButton, + showShortcutsHelp, + showStandaloneLink, + showViewOptions, + slimView, + textFilter, + toSearch, + trace, + traceGraphView, + updateNextViewRangeTime, + updateTextFilter, + updateViewRangeTime, + viewRange, + } = props; + + if (!trace) { + return null; + } + + const summaryItems = + !hideSummary && + !slimView && + HEADER_ITEMS.map(item => { + const { renderer, ...rest } = item; + return { ...rest, value: renderer(trace) }; + }); + + const title = ( +

+ {' '} + {trace.traceID.slice(0, 7)} +

+ ); + + return ( +
+
+ {toSearch && ( + + + + )} + {canCollapse ? ( + + + {title} + + ) : ( + title + )} + {showShortcutsHelp && } + + + {showViewOptions && ( + + )} + {showArchiveButton && ( + + )} + {showStandaloneLink && ( + + + + )} +
+ {summaryItems && } + {!hideMap && + !slimView && ( + + )} +
+ ); +} + +// ghetto fabulous cast because the 16.3 API is not in flow yet +// https://github.com/facebook/flow/issues/6103 +export default (React: any).forwardRef((props, ref) => ); diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.test.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.test.js new file mode 100644 index 0000000000..eeaa7d988e --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.test.js @@ -0,0 +1,126 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// 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, mount } from 'enzyme'; + +import AltViewOptions from './AltViewOptions'; +import KeyboardShortcutsHelp from './KeyboardShortcutsHelp'; +import SpanGraph from './SpanGraph'; +import { TracePageHeaderFn as TracePageHeader, HEADER_ITEMS } from './TracePageHeader'; +import LabeledList from '../../common/LabeledList'; +import traceGenerator from '../../../demo/trace-generators'; +import { getTraceName } from '../../../model/trace-viewer'; +import transformTraceData from '../../../model/transform-trace-data'; + +describe('', () => { + const trace = transformTraceData(traceGenerator.trace({})); + const defaultProps = { + trace, + showArchiveButton: false, + showShortcutsHelp: false, + showStandaloneLink: false, + showViewOptions: false, + textFilter: '', + updateTextFilter: () => {}, + }; + + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('renders a
', () => { + expect(wrapper.find('header').length).toBe(1); + }); + + it('renders an empty
if a trace is not present', () => { + wrapper = mount(); + expect(wrapper.children().length).toBe(0); + }); + + it('renders the trace title', () => { + expect(wrapper.find({ traceName: getTraceName(trace.spans) })).toBeTruthy(); + }); + + it('renders the header items', () => { + wrapper.find('.horizontal .item').forEach((item, i) => { + expect(item.contains(HEADER_ITEMS[i].title)).toBeTruthy(); + expect(item.contains(HEADER_ITEMS[i].renderer(defaultProps.trace))).toBeTruthy(); + }); + }); + + it('renders a ', () => { + expect(wrapper.find(SpanGraph).length).toBe(1); + }); + + describe('observes the visibility toggles for various UX elements', () => { + it('hides the minimap when hideMap === true', () => { + expect(wrapper.find(SpanGraph).length).toBe(1); + wrapper.setProps({ hideMap: true }); + expect(wrapper.find(SpanGraph).length).toBe(0); + }); + + fit('hides the summary when hideSummary === true', () => { + expect(wrapper.find(LabeledList).length).toBe(1); + wrapper.setProps({ hideSummary: true }); + expect(wrapper.find(LabeledList).length).toBe(0); + }); + + it('toggles the archive button', () => { + const onArchiveClicked = () => {}; + const props = { + onArchiveClicked, + showArchiveButton: true, + }; + wrapper.setProps(props); + expect(wrapper.find({ onClick: onArchiveClicked }).length).toBe(1); + props.showArchiveButton = false; + wrapper.setProps(props); + expect(wrapper.find({ onClick: onArchiveClicked }).length).toBe(0); + }); + + it('toggles ', () => { + const props = { showShortcutsHelp: true }; + wrapper.setProps(props); + expect(wrapper.find(KeyboardShortcutsHelp).length).toBe(1); + props.showShortcutsHelp = false; + wrapper.setProps(props); + expect(wrapper.find(KeyboardShortcutsHelp).length).toBe(0); + }); + + it('toggles ', () => { + const props = { showViewOptions: true }; + wrapper.setProps(props); + expect(wrapper.find(AltViewOptions).length).toBe(1); + props.showViewOptions = false; + wrapper.setProps(props); + expect(wrapper.find(AltViewOptions).length).toBe(0); + }); + + it('toggles the standalone link', () => { + const linkToStandalone = 'some-link'; + const props = { + linkToStandalone, + showStandaloneLink: true, + }; + wrapper.setProps(props); + expect(wrapper.find({ to: linkToStandalone }).length).toBe(1); + props.showStandaloneLink = false; + wrapper.setProps(props); + expect(wrapper.find({ to: linkToStandalone }).length).toBe(0); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader.track.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.track.js similarity index 88% rename from packages/jaeger-ui/src/components/TracePage/TracePageHeader.track.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.track.js index 023b18f749..ff707ab3ea 100644 --- a/packages/jaeger-ui/src/components/TracePage/TracePageHeader.track.js +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.track.js @@ -14,8 +14,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { getToggleValue, OPEN } from '../../utils/tracking/common'; -import { trackEvent } from '../../utils/tracking'; +import { getToggleValue, OPEN } from '../../../utils/tracking/common'; +import { trackEvent } from '../../../utils/tracking'; const CATEGORY_ALT_VIEW = 'jaeger/ux/trace/alt-view'; const CATEGORY_SLIM_HEADER = 'jaeger/ux/trace/slim-header'; diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageSearchBar.css b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageSearchBar.css similarity index 100% rename from packages/jaeger-ui/src/components/TracePage/TracePageSearchBar.css rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageSearchBar.css diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageSearchBar.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageSearchBar.js similarity index 97% rename from packages/jaeger-ui/src/components/TracePage/TracePageSearchBar.js rename to packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageSearchBar.js index 3eb31997e2..742d0bf862 100644 --- a/packages/jaeger-ui/src/components/TracePage/TracePageSearchBar.js +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageSearchBar.js @@ -52,7 +52,7 @@ export function TracePageSearchBarFn(props: TracePageSearchBarProps) { const btnClass = `TracePageSearchBar--btn${textFilter ? '' : ' is-disabled'}`; return ( -
+
{/* style inline because compact overwrites the display */} void, - textFilter: string, - prevResult: () => void, - nextResult: () => void, - clearSearch: () => void, - forwardedRef: { current: Input | null }, - resultCount: number, - fromSearch: string, - onGoFullViewClicked: () => void, - enableDetails: boolean, - // these props are used by the `HEADER_ITEMS` - // eslint-disable-next-line react/no-unused-prop-types - timestamp: number, - // eslint-disable-next-line react/no-unused-prop-types - duration: number, - // eslint-disable-next-line react/no-unused-prop-types - numServices: number, - // eslint-disable-next-line react/no-unused-prop-types - maxDepth: number, - // eslint-disable-next-line react/no-unused-prop-types - numSpans: number, -}; - -export const HEADER_ITEMS = [ - { - key: 'timestamp', - title: 'Trace Start', - propName: null, - renderer: (props: TracePageHeaderEmbedProps) => formatDatetime(props.timestamp), - }, - { - key: 'duration', - title: 'Duration', - propName: null, - renderer: (props: TracePageHeaderEmbedProps) => formatDuration(props.duration), - }, - { - key: 'service-count', - title: 'Services', - propName: 'numServices', - renderer: null, - }, - { - key: 'depth', - title: 'Depth', - propName: 'maxDepth', - renderer: null, - }, - { - key: 'span-count', - title: 'Total Spans', - propName: 'numSpans', - renderer: null, - }, -]; - -export function TracePageHeaderEmbedFn(props: TracePageHeaderEmbedProps) { - const { - duration, - maxDepth, - numSpans, - timestamp, - numServices, - traceID, - name, - slimView, - onGoFullViewClicked, - updateTextFilter, - textFilter, - prevResult, - nextResult, - clearSearch, - resultCount, - forwardedRef, - enableDetails, - fromSearch, - } = props; - - if (!traceID) { - return null; - } - - const overviewItems = [ - { - key: 'start', - label: 'Trace Start:', - value: formatDatetime(timestamp), - }, - { - key: 'duration', - label: 'Duration:', - value: formatDuration(duration), - }, - { - key: 'svc-count', - label: 'Services:', - value: numServices, - }, - { - key: 'depth', - label: 'Depth:', - value: maxDepth, - }, - { - key: 'span-count', - label: 'Total Spans:', - value: numSpans, - }, - ]; - - return ( -
-
- {fromSearch !== undefined && - fromSearch !== '' && ( - - )} -

- {name || FALLBACK_TRACE_NAME} -

- - -
- {enableDetails && - !slimView && } -
- ); -} - -// ghetto fabulous cast because the 16.3 API is not in flow yet -// https://github.com/facebook/flow/issues/6103 -export default (React: any).forwardRef((props, ref) => ( - -)); diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeaderEmbed.test.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeaderEmbed.test.js deleted file mode 100644 index e67cc23e55..0000000000 --- a/packages/jaeger-ui/src/components/TracePage/TracePageHeaderEmbed.test.js +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// 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, mount } from 'enzyme'; - -import { TracePageHeaderEmbedFn as TracePageHeaderEmbed, HEADER_ITEMS } from './TracePageHeaderEmbed'; -import LabeledList from '../common/LabeledList'; - -describe('', () => { - const defaultProps = { - traceID: 'some-trace-id', - name: 'some-trace-name', - textFilter: '', - updateTextFilter: () => {}, - }; - - let wrapper; - - beforeEach(() => { - wrapper = shallow(); - }); - - it('renders a
', () => { - expect(wrapper.find('header').length).toBe(1); - }); - - it('renders an empty
if no traceID is present', () => { - wrapper = mount(); - expect(wrapper.children().length).toBe(0); - }); - - it('renders the trace title', () => { - const h1 = wrapper.find('h1').first(); - expect(h1.contains(defaultProps.name)).toBeTruthy(); - }); - - it('renders the header items', () => { - wrapper.find('.horizontal .item').forEach((item, i) => { - expect(item.contains(HEADER_ITEMS[i].title)).toBeTruthy(); - expect(item.contains(HEADER_ITEMS[i].renderer(defaultProps.trace))).toBeTruthy(); - }); - }); - - it('show details if queryparam enableDetails', () => { - wrapper.setProps({ enableDetails: true }); - expect(wrapper.find(LabeledList).length).toBe(1); - }); - -}); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.css index da1281ac0c..a5ddfada21 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.css @@ -16,7 +16,7 @@ limitations under the License. .TimelineHeaderRow { background: #ececec; - border-bottom: 1px solid #d8d8d8; + border-bottom: 1px solid #ccc; height: 38px; line-height: 38px; position: fixed; diff --git a/packages/jaeger-ui/src/components/TracePage/index.css b/packages/jaeger-ui/src/components/TracePage/index.css index 17a16eba7b..defafd71e2 100644 --- a/packages/jaeger-ui/src/components/TracePage/index.css +++ b/packages/jaeger-ui/src/components/TracePage/index.css @@ -16,7 +16,6 @@ limitations under the License. .Tracepage--headerSection { background: #fff; - border-bottom: 1px solid #d8d8d8; position: fixed; width: 100%; z-index: 3; diff --git a/packages/jaeger-ui/src/components/TracePage/index.js b/packages/jaeger-ui/src/components/TracePage/index.js index 6e478e3abc..948f9ffe8e 100644 --- a/packages/jaeger-ui/src/components/TracePage/index.js +++ b/packages/jaeger-ui/src/components/TracePage/index.js @@ -15,15 +15,13 @@ // limitations under the License. import * as React from 'react'; +import { Input } from 'antd'; import _clamp from 'lodash/clamp'; import _mapValues from 'lodash/mapValues'; -import _maxBy from 'lodash/maxBy'; -import _values from 'lodash/values'; import { connect } from 'react-redux'; -import queryString from 'query-string'; -import type { RouterHistory, Match } from 'react-router-dom'; import { bindActionCreators } from 'redux'; -import { Input } from 'antd'; + +import type { Location, Match, RouterHistory } from 'react-router-dom'; import ArchiveNotifier from './ArchiveNotifier'; import { actions as archiveActions } from './ArchiveNotifier/duck'; @@ -31,51 +29,45 @@ import { trackFilter, trackRange } from './index.track'; import { merge as mergeShortcuts, reset as resetShortcuts } from './keyboard-shortcuts'; import { cancel as cancelScroll, scrollBy, scrollTo } from './scroll-page'; import ScrollManager from './ScrollManager'; -import SpanGraph from './SpanGraph'; +import TraceGraph from './TraceGraph/TraceGraph'; +import { trackSlimHeaderToggle } from './TracePageHeader/TracePageHeader.track'; import TracePageHeader from './TracePageHeader'; -import TracePageHeaderEmbed from './TracePageHeaderEmbed'; -import { trackSlimHeaderToggle } from './TracePageHeader.track'; import TraceTimelineViewer from './TraceTimelineViewer'; +import { getLocation, getUrl } from './url'; import ErrorMessage from '../common/ErrorMessage'; import LoadingIndicator from '../common/LoadingIndicator'; import * as jaegerApiActions from '../../actions/jaeger-api'; import { fetchedState } from '../../constants'; -import { getTraceName } from '../../model/trace-viewer'; -import prefixUrl from '../../utils/prefix-url'; -import { isEmbed } from '../../utils/embedded'; import type { CombokeysHandler, ShortcutCallbacks } from './keyboard-shortcuts'; import type { ViewRange, ViewRangeTimeUpdate } from './types'; import type { FetchedTrace, ReduxState } from '../../types'; -import type { KeyValuePair, Span } from '../../types/trace'; import type { TraceArchive } from '../../types/archive'; - -import TraceGraph from './TraceGraph/TraceGraph'; +import type { EmbeddedState } from '../../types/embedded'; +import type { KeyValuePair, Span } from '../../types/trace'; import './index.css'; type TracePageProps = { + acknowledgeArchive: string => void, archiveEnabled: boolean, - archiveTraceState: ?TraceArchive, archiveTrace: string => void, - acknowledgeArchive: string => void, + archiveTraceState: ?TraceArchive, + embedded: null | EmbeddedState, fetchTrace: string => void, history: RouterHistory, - searchParams: string, id: string, + location: Location, + searchUrl: null | string, trace: ?FetchedTrace, - embed?: boolean, - enableDetails?: boolean, - mapCollapsed?: boolean, - fromSearch?: string, }; type TracePageState = { + findMatchesIDs: ?Set, headerHeight: ?number, slimView: boolean, traceGraphView: boolean, textFilter: string, - findMatchesIDs: ?Set, viewRange: ViewRange, }; @@ -118,12 +110,13 @@ export class TracePageImpl extends React.PureComponent { - window.open(prefixUrl(`/trace/${this.props.id.toLowerCase()}`), '_blank'); - }; - render() { - const { - archiveEnabled, - archiveTraceState, - trace, - embed, - mapCollapsed, - enableDetails, - searchParams, - fromSearch, - } = this.props; + const { archiveEnabled, archiveTraceState, embedded, id, searchUrl, trace } = this.props; const { slimView, traceGraphView, headerHeight, textFilter, viewRange, findMatchesIDs } = this.state; if (!trace || trace.state === fetchedState.LOADING) { return ; @@ -366,37 +345,34 @@ export class TracePageImpl extends React.PureComponent; } - const { duration, processes, spans, startTime, traceID } = data; - const maxSpanDepth = _maxBy(spans, 'depth').depth + 1; - const numberOfServices = new Set(_values(processes).map(p => p.serviceName)).size; - const tracePageProps = { - duration, - maxDepth: maxSpanDepth, - name: getTraceName(spans), - numServices: numberOfServices, - numSpans: spans.length, + + const isEmbedded = Boolean(embedded); + const headerProps = { slimView, + textFilter, traceGraphView, - timestamp: startTime, - traceID, + viewRange, + canCollapse: !embedded || !embedded.timeline.hideSummary || !embedded.timeline.hideMinimap, + clearSearch: this.clearSearch, + hideMap: Boolean(traceGraphView || (embedded && embedded.timeline.hideMinimap)), + hideSummary: Boolean(embedded && embedded.timeline.hideSummary), + linkToStandalone: getUrl(id), + nextResult: this._scrollManager.scrollToNextVisibleSpan, + onArchiveClicked: this.archiveTrace, onSlimViewClicked: this.toggleSlimView, onTraceGraphViewClicked: this.toggleTraceGraphView, - textFilter, prevResult: this._scrollManager.scrollToPrevVisibleSpan, - nextResult: this._scrollManager.scrollToNextVisibleSpan, - clearSearch: this.clearSearch, + ref: this._searchBar, resultCount: findMatchesIDs ? findMatchesIDs.size : 0, + showArchiveButton: !isEmbedded && archiveEnabled, + showShortcutsHelp: !isEmbedded, + showStandaloneLink: isEmbedded, + showViewOptions: !isEmbedded, + toSearch: searchUrl, + trace: data, + updateNextViewRangeTime: this.updateNextViewRangeTime, updateTextFilter: this.updateTextFilter, - archiveButtonVisible: archiveEnabled, - onArchiveClicked: this.archiveTrace, - ref: this._searchBar, - }; - - const tracePageEmbedProps = { - searchParams, - fromSearch, - enableDetails, - onGoFullViewClicked: this.goFullView, + updateViewRangeTime: this.updateViewRangeTime, }; return ( @@ -405,19 +381,7 @@ export class TracePageImpl extends React.PureComponent )}
- {embed ? ( - - ) : ( - - )} - {((!slimView && !embed && !traceGraphView) || (embed && !mapCollapsed)) && ( - - )} +
{headerHeight && (traceGraphView ? ( @@ -444,19 +408,20 @@ export class TracePageImpl extends React.PureComponent', () => { expect(wrapper.find(TracePageHeader).get(0)).toBeTruthy(); }); - it('renders a ', () => { - expect(wrapper.find(SpanGraph).length).toBe(1); - }); - it('renders a a loading indicator when not provided a fetched trace', () => { wrapper.setProps({ trace: null }); const loading = wrapper.find(LoadingIndicator); @@ -153,23 +148,6 @@ describe('', () => { expect(cancelScroll.mock.calls).toEqual([[]]); }); - it('no render TracePageHeader if queryparam embed', () => { - wrapper.setProps({ embed: true }); - expect(wrapper.find(TracePageHeader).length).toBe(0); - }); - - it('collapse map if queryparam mapCollapsed', () => { - wrapper.setProps({ mapCollapsed: true, embed: true }); - expect(wrapper.find(SpanGraph).length).toBe(0); - }); - - it('open a window when goFullView is called', () => { - wrapper.setProps({ id: '12345' }); - global.open = jest.fn(); - wrapper.instance().goFullView(); - expect(global.open).toBeCalledWith(prefixUrl('/trace/12345'), '_blank'); - }); - describe('_adjustViewRange()', () => { let instance; let time; @@ -369,13 +347,21 @@ describe('mapDispatchToProps()', () => { }); describe('mapStateToProps()', () => { - it('maps state to props correctly', () => { - const id = 'abc'; - const trace = {}; - const state = { + const traceID = 'trace-id'; + const trace = {}; + const embedded = 'a-faux-embedded-config'; + const ownProps = { + match: { + params: { id: traceID }, + }, + }; + let state; + beforeEach(() => { + state = { + embedded, trace: { traces: { - [id]: { data: trace, state: fetchedState.DONE }, + [traceID]: { data: trace, state: fetchedState.DONE }, }, }, router: { @@ -388,57 +374,29 @@ describe('mapStateToProps()', () => { }, archive: {}, }; - const ownProps = { - match: { - params: { id }, - }, - }; + }); + it('maps state to props correctly', () => { const props = mapStateToProps(state, ownProps); expect(props).toEqual({ - id, + id: traceID, + embedded, archiveEnabled: false, - embed: false, - enableDetails: false, - fromSearch: undefined, - mapCollapsed: false, archiveTraceState: undefined, + searchUrl: null, trace: { data: {}, state: fetchedState.DONE }, }); }); - it('maps state to props correctly with query embed', () => { - const id = 'abc'; - const trace = {}; - const state = { - trace: { - traces: { - [id]: { data: trace, state: fetchedState.DONE }, - }, - }, - router: { - location: { - search: 'embed=v0&enableDetails&mapCollapsed&fromSearch=%2Fsearch%3Fend%3D1542902040794000%26limit%3D20%26lookback%3D1h%26maxDuration%26minDuration%26service%3Dproductpage%26start%3D1542898440794000', - }, - }, - config: { - archiveEnabled: false, - }, - archive: {}, - }; - const ownProps = { - match: { - params: { id }, - }, - }; + it('propagates fromSearch correctly', () => { + const fakeUrl = 'fake-url'; + state.router.location.state = { fromSearch: fakeUrl }; const props = mapStateToProps(state, ownProps); expect(props).toEqual({ - id, + id: traceID, + embedded, archiveEnabled: false, - embed: true, - enableDetails: true, - fromSearch: '/search?end=1542902040794000&limit=20&lookback=1h&maxDuration&minDuration&service=productpage&start=1542898440794000', - mapCollapsed: true, archiveTraceState: undefined, + searchUrl: fakeUrl, trace: { data: {}, state: fetchedState.DONE }, }); }); diff --git a/packages/jaeger-ui/src/components/TracePage/keyboard-mappings.js b/packages/jaeger-ui/src/components/TracePage/keyboard-mappings.js new file mode 100644 index 0000000000..664e691bca --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/keyboard-mappings.js @@ -0,0 +1,36 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// 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. + +export default { + scrollPageDown: { binding: 's', label: 'Scroll down' }, + scrollPageUp: { binding: 'w', label: 'Scroll up' }, + scrollToNextVisibleSpan: { binding: 'f', label: 'Scroll to the next visible span' }, + scrollToPrevVisibleSpan: { binding: 'b', label: 'Scroll to the previous visible span' }, + panLeft: { binding: ['a', 'left'], label: 'Pan left' }, + panLeftFast: { binding: ['shift+a', 'shift+left'], label: 'Pan left — Large' }, + panRight: { binding: ['d', 'right'], label: 'Pan right' }, + panRightFast: { binding: ['shift+d', 'shift+right'], label: 'Pan right — Large' }, + zoomIn: { binding: 'up', label: 'Zoom in' }, + zoomInFast: { binding: 'shift+up', label: 'Zoom in — Large' }, + zoomOut: { binding: 'down', label: 'Zoom out' }, + zoomOutFast: { binding: 'shift+down', label: 'Zoom out — Large' }, + collapseAll: { binding: ']', label: 'Collapse All' }, + expandAll: { binding: '[', label: 'Expand All' }, + collapseOne: { binding: 'p', label: 'Collapse One Level' }, + expandOne: { binding: 'o', label: 'Expand One Level' }, + searchSpans: { binding: 'ctrl+b', label: 'Search Spans' }, + clearSearch: { binding: 'escape', label: 'Clear Search' }, +}; diff --git a/packages/jaeger-ui/src/components/TracePage/keyboard-shortcuts.js b/packages/jaeger-ui/src/components/TracePage/keyboard-shortcuts.js index 3ab3738936..05237b9e8f 100644 --- a/packages/jaeger-ui/src/components/TracePage/keyboard-shortcuts.js +++ b/packages/jaeger-ui/src/components/TracePage/keyboard-shortcuts.js @@ -16,6 +16,8 @@ import Combokeys from 'combokeys'; +import keyboardMappings from './keyboard-mappings'; + export type CombokeysHandler = | (() => void) | ((SyntheticKeyboardEvent) => void) @@ -50,27 +52,6 @@ export type ShortcutCallbacks = { clearSearch?: CombokeysHandler, }; -export const kbdMappings = { - scrollPageDown: 's', - scrollPageUp: 'w', - scrollToNextVisibleSpan: 'f', - scrollToPrevVisibleSpan: 'b', - panLeft: ['a', 'left'], - panLeftFast: ['shift+a', 'shift+left'], - panRight: ['d', 'right'], - panRightFast: ['shift+d', 'shift+right'], - zoomIn: 'up', - zoomInFast: 'shift+up', - zoomOut: 'down', - zoomOutFast: 'shift+down', - collapseAll: ']', - expandAll: '[', - collapseOne: 'p', - expandOne: 'o', - searchSpans: 'ctrl+b', - clearSearch: 'escape', -}; - let instance: ?CombokeysType; function getInstance(): CombokeysType { @@ -85,7 +66,7 @@ export function merge(callbacks: ShortcutCallbacks) { Object.keys(callbacks).forEach(name => { const keysHandler = callbacks[name]; if (keysHandler) { - inst.bind(kbdMappings[name], keysHandler); + inst.bind(keyboardMappings[name].binding, keysHandler); } }); } diff --git a/packages/jaeger-ui/src/components/TracePage/url.js b/packages/jaeger-ui/src/components/TracePage/url.js index 885b0c4182..2b782f80a4 100644 --- a/packages/jaeger-ui/src/components/TracePage/url.js +++ b/packages/jaeger-ui/src/components/TracePage/url.js @@ -21,3 +21,10 @@ export const ROUTE_PATH = prefixUrl('/trace/:id'); export function getUrl(id: string) { return prefixUrl(`/trace/${id}`); } + +export function getLocation(id: string, state: ?Object) { + return { + state, + pathname: prefixUrl(`/trace/${id}`), + }; +} diff --git a/packages/jaeger-ui/src/components/common/NewWindowIcon.css b/packages/jaeger-ui/src/components/common/NewWindowIcon.css new file mode 100644 index 0000000000..e6bd881cb2 --- /dev/null +++ b/packages/jaeger-ui/src/components/common/NewWindowIcon.css @@ -0,0 +1,19 @@ +/* +Copyright (c) 2018 Uber Technologies, Inc. + +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. +*/ + +.NewWindowIcon { + font-size: 1.5em; +} diff --git a/packages/jaeger-ui/src/components/common/NewWindowIcon.js b/packages/jaeger-ui/src/components/common/NewWindowIcon.js new file mode 100644 index 0000000000..3d7bbc16e1 --- /dev/null +++ b/packages/jaeger-ui/src/components/common/NewWindowIcon.js @@ -0,0 +1,34 @@ +// @flow + +// Copyright (c) 2018 Uber Technologies, Inc. +// +// 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 IoAndroidOpen from 'react-icons/lib/io/android-open'; + +import './NewWindowIcon.css'; + +type Props = { + className?: string, +}; + +export default function NewWindowIcon(props: Props) { + const { className, ...rest } = props; + const cls = `NewWindowIcon ${className || ''}`; + return ; +} + +NewWindowIcon.defaultProps = { + className: undefined, +}; diff --git a/packages/jaeger-ui/src/components/common/TraceName.js b/packages/jaeger-ui/src/components/common/TraceName.js index cdc4b7bb0b..431c26eaf4 100644 --- a/packages/jaeger-ui/src/components/common/TraceName.js +++ b/packages/jaeger-ui/src/components/common/TraceName.js @@ -26,15 +26,14 @@ import type { ApiError } from '../../types/api-error'; import './TraceName.css'; type Props = { - breakable?: boolean, className?: string, - error: ?ApiError, - state: ?FetchedState, - traceName: ?string, + error?: ?ApiError, + state?: ?FetchedState, + traceName?: ?string, }; export default function TraceName(props: Props) { - const { breakable, className, error, state, traceName } = props; + const { className, error, state, traceName } = props; const isErred = state === fetchedState.ERROR; let title = traceName || FALLBACK_TRACE_NAME; let errorCssClass = ''; @@ -52,12 +51,14 @@ export default function TraceName(props: Props) { title = ; } else { const text = traceName || FALLBACK_TRACE_NAME; - title = breakable ? : text; + title = ; } return {title}; } TraceName.defaultProps = { - breakable: false, className: '', + error: null, + state: null, + traceName: null, }; diff --git a/packages/jaeger-ui/src/components/common/utils.css b/packages/jaeger-ui/src/components/common/utils.css index 9f5992ba65..68291391a2 100644 --- a/packages/jaeger-ui/src/components/common/utils.css +++ b/packages/jaeger-ui/src/components/common/utils.css @@ -34,6 +34,10 @@ limitations under the License. color: #aaa; } +.u-tx-inherit { + color: inherit; +} + .u-tx-ellipsis { text-overflow: ellipsis; } diff --git a/packages/jaeger-ui/src/components/common/vars.css b/packages/jaeger-ui/src/components/common/vars.css new file mode 100644 index 0000000000..f22464950a --- /dev/null +++ b/packages/jaeger-ui/src/components/common/vars.css @@ -0,0 +1,19 @@ +/* +Copyright (c) 2018 Uber Technologies, Inc. + +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. +*/ + +:root { + --tx-color-title: rgba(0, 0, 0, 0.85); +} diff --git a/packages/jaeger-ui/src/reducers/embedded.js b/packages/jaeger-ui/src/reducers/embedded.js new file mode 100644 index 0000000000..8c9f6f1673 --- /dev/null +++ b/packages/jaeger-ui/src/reducers/embedded.js @@ -0,0 +1,29 @@ +// @flow + +// Copyright (c) 2018 Uber Technologies, Inc. +// +// 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 _get from 'lodash/get'; + +import { getEmbeddedState } from '../utils/embedded-url'; + +import type { EmbeddedState } from '../types/embedded'; + +export default function embeddedConfig(state: ?EmbeddedState) { + if (state === undefined) { + const search = _get(window, 'location.search'); + return search ? getEmbeddedState(search) : null; + } + return state; +} diff --git a/packages/jaeger-ui/src/reducers/index.js b/packages/jaeger-ui/src/reducers/index.js index 442c4fd4bb..c53afa4ad7 100644 --- a/packages/jaeger-ui/src/reducers/index.js +++ b/packages/jaeger-ui/src/reducers/index.js @@ -16,12 +16,14 @@ import { reducer as formReducer } from 'redux-form'; import config from './config'; import dependencies from './dependencies'; +import embedded from './embedded'; import services from './services'; import trace from './trace'; export default { config, dependencies, + embedded, services, trace, form: formReducer, diff --git a/packages/jaeger-ui/src/reducers/trace.js b/packages/jaeger-ui/src/reducers/trace.js index 42f167dff2..d8f5199f7b 100644 --- a/packages/jaeger-ui/src/reducers/trace.js +++ b/packages/jaeger-ui/src/reducers/trace.js @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import _isEqual from 'lodash/isEqual'; import { handleActions } from 'redux-actions'; import { fetchTrace, fetchMultipleTraces, searchTraces } from '../actions/jaeger-api'; @@ -21,6 +22,7 @@ import transformTraceData from '../model/transform-trace-data'; const initialState = { traces: {}, search: { + query: null, results: [], }, }; @@ -86,15 +88,20 @@ function fetchMultipleTracesErred(state, { meta, payload }) { return { ...state, traces }; } -function fetchSearchStarted(state) { +function fetchSearchStarted(state, { meta }) { + const { query } = meta; const search = { + query, results: [], state: fetchedState.LOADING, }; return { ...state, search }; } -function searchDone(state, { payload }) { +function searchDone(state, { meta, payload }) { + if (!_isEqual(state.search.query, meta.query)) { + return state; + } const processed = payload.data.map(transformTraceData); const resultTraces = {}; const results = []; @@ -105,12 +112,15 @@ function searchDone(state, { payload }) { results.push(id); } const traces = { ...state.traces, ...resultTraces }; - const search = { results, state: fetchedState.DONE }; + const search = { ...state.search, results, state: fetchedState.DONE }; return { ...state, search, traces }; } -function searchErred(state, { payload }) { - const search = { error: payload, results: [], state: fetchedState.ERROR }; +function searchErred(state, { meta, payload }) { + if (!_isEqual(state.search.query, meta.query)) { + return state; + } + const search = { ...state.search, error: payload, results: [], state: fetchedState.ERROR }; return { ...state, search }; } diff --git a/packages/jaeger-ui/src/reducers/trace.test.js b/packages/jaeger-ui/src/reducers/trace.test.js index 95fab6d28b..7ed64647f1 100644 --- a/packages/jaeger-ui/src/reducers/trace.test.js +++ b/packages/jaeger-ui/src/reducers/trace.test.js @@ -18,56 +18,194 @@ import traceGenerator from '../demo/trace-generators'; import transformTraceData from '../model/transform-trace-data'; import traceReducer from '../reducers/trace'; +const ACTION_POSTFIX_FULFILLED = '_FULFILLED'; +const ACTION_POSTFIX_PENDING = '_PENDING'; +const ACTION_POSTFIX_REJECTED = '_REJECTED'; + const trace = traceGenerator.trace({ numberOfSpans: 1 }); const { traceID: id } = trace; -it('trace reducer should set loading true on a fetch', () => { - const state = traceReducer(undefined, { - type: `${jaegerApiActions.fetchTrace}_PENDING`, - meta: { id }, +describe('fetch a trace', () => { + it('sets loading true on a fetch', () => { + const state = traceReducer(undefined, { + type: `${jaegerApiActions.fetchTrace}${ACTION_POSTFIX_PENDING}`, + meta: { id }, + }); + const outcome = { [id]: { id, state: fetchedState.LOADING } }; + expect(state.traces).toEqual(outcome); }); - const outcome = { [id]: { id, state: fetchedState.LOADING } }; - expect(state.traces).toEqual(outcome); -}); -it('trace reducer should handle a successful FETCH_TRACE', () => { - const state = traceReducer(undefined, { - type: `${jaegerApiActions.fetchTrace}_FULFILLED`, - payload: { data: [trace] }, - meta: { id }, + it('handles a successful FETCH_TRACE', () => { + const state = traceReducer(undefined, { + type: `${jaegerApiActions.fetchTrace}${ACTION_POSTFIX_FULFILLED}`, + payload: { data: [trace] }, + meta: { id }, + }); + expect(state.traces).toEqual({ [id]: { id, data: transformTraceData(trace), state: fetchedState.DONE } }); + }); + + it('handles a failed FETCH_TRACE', () => { + const error = new Error(); + const state = traceReducer(undefined, { + type: `${jaegerApiActions.fetchTrace}${ACTION_POSTFIX_REJECTED}`, + payload: error, + meta: { id }, + }); + expect(state.traces).toEqual({ [id]: { error, id, state: fetchedState.ERROR } }); + expect(state.traces[id].error).toBe(error); }); - expect(state.traces).toEqual({ [id]: { id, data: transformTraceData(trace), state: fetchedState.DONE } }); }); -it('trace reducer should handle a failed FETCH_TRACE', () => { - const error = new Error(); - const state = traceReducer(undefined, { - type: `${jaegerApiActions.fetchTrace}_REJECTED`, - payload: error, - meta: { id }, +describe('fetch multiple traces', () => { + const traceB = traceGenerator.trace({ numberOfSpans: 1 }); + const { traceID: idB } = traceB; + + it('sets loading to true for all pending IDs', () => { + const traces = { preExisting: 'this-trace-is-pre-existing' }; + const state = traceReducer( + { traces }, + { + type: `${jaegerApiActions.fetchMultipleTraces}${ACTION_POSTFIX_PENDING}`, + meta: { ids: [id, idB] }, + } + ); + const outcome = { + ...traces, + [id]: { id, state: fetchedState.LOADING }, + [idB]: { id: idB, state: fetchedState.LOADING }, + }; + expect(state.traces).toEqual(outcome); + }); + + describe('handles a successful request', () => { + it('transforms and saves all trace data', () => { + const traces = { preExisting: 'this-trace-is-pre-existing' }; + const state = traceReducer( + { traces }, + { + type: `${jaegerApiActions.fetchMultipleTraces}${ACTION_POSTFIX_FULFILLED}`, + payload: { data: [trace, traceB] }, + } + ); + const outcome = { + ...traces, + [id]: { id, data: transformTraceData(trace), state: fetchedState.DONE }, + [idB]: { id: idB, data: transformTraceData(traceB), state: fetchedState.DONE }, + }; + expect(state.traces).toEqual(outcome); + }); + + it('saves all error data', () => { + const msg = 'a-message'; + const traceID = 'a-trace-id'; + const traces = { preExisting: 'this-trace-is-pre-existing' }; + const state = traceReducer( + { traces }, + { + type: `${jaegerApiActions.fetchMultipleTraces}${ACTION_POSTFIX_FULFILLED}`, + payload: { data: [], errors: [{ msg, traceID }] }, + } + ); + const outcome = { + ...traces, + [traceID]: { id: traceID, error: expect.any(Error), state: fetchedState.ERROR }, + }; + expect(state.traces).toEqual(outcome); + }); + }); + + it('handles a failed request', () => { + const error = 'error-info'; + const traces = { preExisting: 'this-trace-is-pre-existing' }; + const state = traceReducer( + { traces }, + { + type: `${jaegerApiActions.fetchMultipleTraces}${ACTION_POSTFIX_REJECTED}`, + payload: error, + meta: { ids: [id, idB] }, + } + ); + const outcome = { + ...traces, + [id]: { id, error, state: fetchedState.ERROR }, + [idB]: { id: idB, error, state: fetchedState.ERROR }, + }; + expect(state.traces).toEqual(outcome); }); - expect(state.traces).toEqual({ [id]: { error, id, state: fetchedState.ERROR } }); - expect(state.traces[id].error).toBe(error); }); -it('trace reducer should handle a successful SEARCH_TRACES', () => { - const state = traceReducer(undefined, { - type: `${jaegerApiActions.searchTraces}_FULFILLED`, - payload: { data: [trace] }, - meta: { query: 'whatever' }, +describe('search traces', () => { + const query = 'some-query'; + + it('handles a pending request', () => { + const state = traceReducer(undefined, { + type: `${jaegerApiActions.searchTraces}${ACTION_POSTFIX_PENDING}`, + meta: { query }, + }); + const outcome = { + query, + results: [], + state: fetchedState.LOADING, + }; + expect(state.search).toEqual(outcome); }); - const outcome = { - traces: { - [id]: { - id, - data: transformTraceData(trace), + + it('handles a successful request', () => { + const state = traceReducer( + { search: { query } }, + { + type: `${jaegerApiActions.searchTraces}${ACTION_POSTFIX_FULFILLED}`, + payload: { data: [trace] }, + meta: { query }, + } + ); + const outcome = { + traces: { + [id]: { + id, + data: transformTraceData(trace), + state: fetchedState.DONE, + }, + }, + search: { + query, state: fetchedState.DONE, + results: [id], }, - }, - search: { - state: fetchedState.DONE, - results: [id], - }, - }; - expect(state).toEqual(outcome); + }; + expect(state).toEqual(outcome); + }); + + it('handles a failed request', () => { + const error = 'some-error'; + const state = traceReducer( + { search: { query } }, + { + type: `${jaegerApiActions.searchTraces}${ACTION_POSTFIX_REJECTED}`, + payload: error, + meta: { query }, + } + ); + const outcome = { + error, + query, + results: [], + state: fetchedState.ERROR, + }; + expect(state.search).toEqual(outcome); + }); + + it('ignores the results with the wrong query', () => { + const otherQuery = 'some-other-query'; + [ACTION_POSTFIX_FULFILLED, ACTION_POSTFIX_REJECTED].forEach(postfix => { + const state = traceReducer( + { search: { query } }, + { + type: `${jaegerApiActions.searchTraces}${postfix}`, + meta: { query: otherQuery }, + } + ); + expect(state.search).toEqual({ query }); + }); + }); }); diff --git a/packages/jaeger-ui/src/types/embedded.js b/packages/jaeger-ui/src/types/embedded.js new file mode 100644 index 0000000000..dda27aa331 --- /dev/null +++ b/packages/jaeger-ui/src/types/embedded.js @@ -0,0 +1,27 @@ +// @flow + +// Copyright (c) 2018 Uber Technologies, Inc. +// +// 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. + +type EmbeddedStateV0 = { + version: 'v0', + searchHideGraph: boolean, + timeline: { + collapseTitle: boolean, + hideMinimap: boolean, + hideSummary: boolean, + }, +}; + +export type EmbeddedState = EmbeddedStateV0; diff --git a/packages/jaeger-ui/src/types/index.js b/packages/jaeger-ui/src/types/index.js index a08112c927..c6e255714d 100644 --- a/packages/jaeger-ui/src/types/index.js +++ b/packages/jaeger-ui/src/types/index.js @@ -19,6 +19,8 @@ import type { ContextRouter } from 'react-router-dom'; import type { ApiError } from './api-error'; import type { TracesArchive } from './archive'; import type { Config } from './config'; +import type { EmbeddedState } from './embedded'; +import type { SearchQuery } from './search'; import type { Trace } from './trace'; import type { TraceDiffState } from './trace-diff'; import type { TraceTimeline } from './trace-timeline'; @@ -40,19 +42,21 @@ export type ReduxState = { loading: boolean, error: ?ApiError, }, + embedded: EmbeddedState, + router: ContextRouter, services: { services: ?(string[]), operationsForService: { [string]: string[] }, loading: boolean, error: ?ApiError, }, - router: ContextRouter, trace: { traces: { [string]: FetchedTrace }, search: { error?: ApiError, results: string[], state?: FetchedState, + query?: SearchQuery, }, }, traceDiff: TraceDiffState, diff --git a/packages/jaeger-ui/src/types/search.js b/packages/jaeger-ui/src/types/search.js index 0cf2100d12..cd1a625024 100644 --- a/packages/jaeger-ui/src/types/search.js +++ b/packages/jaeger-ui/src/types/search.js @@ -14,6 +14,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +export type SearchQuery = { + end: number | string, + limit: number | string, + lookback: string, + maxDuration: null | string, + minDuration: null | string, + operation: ?string, + service: string, + start: number | string, + tags: ?string, +}; + /** * Type used to summarize traces for the search page. */ diff --git a/packages/jaeger-ui/src/utils/configure-store.js b/packages/jaeger-ui/src/utils/configure-store.js index c6593d60c1..bcfadec691 100644 --- a/packages/jaeger-ui/src/utils/configure-store.js +++ b/packages/jaeger-ui/src/utils/configure-store.js @@ -38,8 +38,8 @@ export default function configureStore(history) { .filter(Boolean), routerMiddleware(history) ), - process.env.NODE_ENV !== 'production' && window && window.devToolsExtension - ? window.devToolsExtension() + process.env.NODE_ENV !== 'production' && window && window.__REDUX_DEVTOOLS_EXTENSION__ + ? window.__REDUX_DEVTOOLS_EXTENSION__() : noop => noop ) ); diff --git a/packages/jaeger-ui/src/utils/embedded-url.js b/packages/jaeger-ui/src/utils/embedded-url.js new file mode 100644 index 0000000000..33d820b6bd --- /dev/null +++ b/packages/jaeger-ui/src/utils/embedded-url.js @@ -0,0 +1,65 @@ +// @flow + +// Copyright (c) 2018 Uber Technologies, Inc. +// +// 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 _flatMap from 'lodash/flatMap'; +import queryString from 'query-string'; + +import type { EmbeddedState } from '../types/embedded'; + +const getStrings = value => (typeof value === 'string' ? value : _flatMap(value, getStrings)); + +const VALUE_ENABLED = '1'; +export const VERSION_0 = 'v0'; + +// uiEmbed=v0 +// uiSearchHideGraph=1 +// uiTimelineCollapseTitle=1 +// uiTimelineHideMinimap=1 +// uiTimelineHideSummary=1 +const STATE_PARAMS_V0 = { + searchHideGraph: 'uiSearchHideGraph', + timeline: { + collapseTitle: 'uiTimelineCollapseTitle', + hideMinimap: 'uiTimelineHideMinimap', + hideSummary: 'uiTimelineHideSummary', + }, +}; + +const PARAM_KEYS_V0 = getStrings(STATE_PARAMS_V0); + +export function getEmbeddedState(search: string): null | EmbeddedState { + const { uiEmbed, ...rest } = queryString.parse(search); + if (uiEmbed !== VERSION_0) { + return null; + } + return { + version: VERSION_0, + searchHideGraph: rest[STATE_PARAMS_V0.searchHideGraph] === VALUE_ENABLED, + timeline: { + collapseTitle: rest[STATE_PARAMS_V0.timeline.collapseTitle] === VALUE_ENABLED, + hideMinimap: rest[STATE_PARAMS_V0.timeline.hideMinimap] === VALUE_ENABLED, + hideSummary: rest[STATE_PARAMS_V0.timeline.hideSummary] === VALUE_ENABLED, + }, + }; +} + +export function stripEmbeddedState(state: Object): Object { + const { uiEmbed, ...rv } = state || {}; + if (uiEmbed === VERSION_0) { + PARAM_KEYS_V0.forEach(Reflect.deleteProperty.bind(null, rv)); + } + return rv; +} diff --git a/packages/jaeger-ui/src/utils/embedded/index.test.js b/packages/jaeger-ui/src/utils/embedded/index.test.js deleted file mode 100644 index f996d9504d..0000000000 --- a/packages/jaeger-ui/src/utils/embedded/index.test.js +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// 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 embedded from './index'; - -describe('isEmbed', () => { - it('query request a embed component', () => { - const query = 'embed=v0&hideGraph&mapCollapsed'; - expect(embedded.isEmbed(query)).toBeTruthy(); - }); - - it('query request a embed component with an icorrect version', () => { - const query = 'embed=v1&hideGraph&mapCollapsed'; - expect(embedded.isEmbed(query)).toBeFalsy(); - }); - - it('query not request a embed component', () => { - const query = 'hideGraph&mapCollapsed'; - expect(embedded.isEmbed(query)).toBeFalsy(); - }); -}); diff --git a/yarn.lock b/yarn.lock index c443d09a17..90e800ed26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1843,6 +1843,10 @@ bser@^2.0.0: dependencies: node-int64 "^0.4.0" +btoa@^1.1.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" + buffer-from@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531" @@ -2657,6 +2661,12 @@ conventional-recommended-bump@^1.2.1: meow "^3.3.0" object-assign "^4.0.1" +convert-source-map@^1.1.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" + dependencies: + safe-buffer "~5.1.1" + convert-source-map@^1.4.0, convert-source-map@^1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" @@ -3485,6 +3495,10 @@ dns-txt@^2.0.2: dependencies: buffer-indexof "^1.0.0" +docopt@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/docopt/-/docopt-0.6.2.tgz#b28e9e2220da5ec49f7ea5bb24a47787405eeb11" + doctrine@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -8756,6 +8770,12 @@ opn@5.2.0, opn@^5.1.0: dependencies: is-wsl "^1.1.0" +opn@^5.3.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.4.0.tgz#cb545e7aab78562beb11aa3bfabc7042e1761035" + dependencies: + is-wsl "^1.1.0" + optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -10287,7 +10307,7 @@ react-icon-base@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/react-icon-base/-/react-icon-base-2.1.0.tgz#a196e33fdf1e7aaa1fda3aefbb68bdad9e82a79d" -react-icons@^2.2.7: +react-icons@2.2.7: version "2.2.7" resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-2.2.7.tgz#d7860826b258557510dac10680abea5ca23cf650" dependencies: @@ -11145,6 +11165,10 @@ rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2. dependencies: glob "^7.0.5" +rimraf@~2.2.6: + version "2.2.8" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" @@ -11648,6 +11672,19 @@ source-list-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" +source-map-explorer@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/source-map-explorer/-/source-map-explorer-1.6.0.tgz#46d6de167b7aff1c4f14d0ee3b6c8beca186f075" + dependencies: + btoa "^1.1.2" + convert-source-map "^1.1.1" + docopt "^0.6.2" + glob "^7.1.2" + opn "^5.3.0" + source-map "^0.5.1" + temp "^0.8.3" + underscore "^1.8.3" + source-map-resolve@^0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.1.tgz#7ad0f593f2281598e854df80f19aae4b92d7a11a" @@ -12181,6 +12218,13 @@ temp-write@^3.3.0: temp-dir "^1.0.0" uuid "^3.0.1" +temp@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59" + dependencies: + os-tmpdir "^1.0.0" + rimraf "~2.2.6" + tempfile@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/tempfile/-/tempfile-1.1.1.tgz#5bcc4eaecc4ab2c707d8bc11d99ccc9a2cb287f2" @@ -12501,6 +12545,10 @@ umd@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf" +underscore@^1.8.3: + version "1.9.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" + underscore@~1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604" From 2baf0751ab676474d927baa1cce732b622e0528b Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Thu, 20 Dec 2018 00:27:30 -0500 Subject: [PATCH 03/46] Unfocus unit test (#298) Signed-off-by: Joe Farro Signed-off-by: Everett Ross --- .../TracePage/TracePageHeader/TracePageHeader.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.test.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.test.js index eeaa7d988e..0d5d1f9de7 100644 --- a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.test.js @@ -73,7 +73,7 @@ describe('', () => { expect(wrapper.find(SpanGraph).length).toBe(0); }); - fit('hides the summary when hideSummary === true', () => { + it('hides the summary when hideSummary === true', () => { expect(wrapper.find(LabeledList).length).toBe(1); wrapper.setProps({ hideSummary: true }); expect(wrapper.find(LabeledList).length).toBe(0); From d90d0425b2a2b61d257db563d5871db65f64a75b Mon Sep 17 00:00:00 2001 From: Everett Date: Thu, 3 Jan 2019 18:16:51 -0500 Subject: [PATCH 04/46] Add a Button to Reset Viewing Layer Zoom (#215) (#290) * Add button to reset viewing layer zoom (#215) Signed-off-by: Everett Ross * Adhere to className pattern, sort imports, remove event handling Signed-off-by: Everett Ross Signed-off-by: Everett Ross --- .../SpanGraph/ViewingLayer.css | 12 +++++++ .../TracePageHeader/SpanGraph/ViewingLayer.js | 17 +++++++-- .../SpanGraph/ViewingLayer.test.js | 35 ++++++++++++++++++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.css b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.css index 48481be342..5d64fa7081 100644 --- a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.css +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.css @@ -61,3 +61,15 @@ limitations under the License. top: 0; user-select: none; } + +.ViewingLayer--resetZoom { + display: none; + position: absolute; + right: 1%; + top: 10%; + z-index: 1; +} + +.ViewingLayer:hover > .ViewingLayer--resetZoom { + display: unset; +} diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.js index c35df3e88d..44065ddb2c 100644 --- a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.js +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/SpanGraph/ViewingLayer.js @@ -14,8 +14,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import * as React from 'react'; +import { Button } from 'antd'; import cx from 'classnames'; +import * as React from 'react'; import GraphTicks from './GraphTicks'; import Scrubber from './Scrubber'; @@ -225,7 +226,14 @@ export default class ViewingLayer extends React.PureComponent { + this.props.updateViewRangeTime(0, 1); + }; + + /** + * Renders the difference between where the drag started and the current * position, e.g. the red or blue highlight. * * @returns React.Node[] @@ -277,6 +285,11 @@ export default class ViewingLayer extends React.PureComponent + {(viewStart !== 0 || viewEnd !== 1) && ( + + )} ', () => { }); }); }); + + describe('.ViewingLayer--resetZoom', () => { + it('should not render .ViewingLayer--resetZoom if props.viewRange.time.current = [0,1]', () => { + expect(wrapper.find('.ViewingLayer--resetZoom').length).toBe(0); + wrapper.setProps({ viewRange: { time: { current: [0, 1] } } }); + expect(wrapper.find('.ViewingLayer--resetZoom').length).toBe(0); + }); + + it('should render ViewingLayer--resetZoom if props.viewRange.time.current[0] !== 0', () => { + // If the test fails on the following expect statement, this may be a false negative + expect(wrapper.find('.ViewingLayer--resetZoom').length).toBe(0); + wrapper.setProps({ viewRange: { time: { current: [0.1, 1] } } }); + expect(wrapper.find('.ViewingLayer--resetZoom').length).toBe(1); + }); + + it('should render ViewingLayer--resetZoom if props.viewRange.time.current[1] !== 1', () => { + // If the test fails on the following expect statement, this may be a false negative + expect(wrapper.find('.ViewingLayer--resetZoom').length).toBe(0); + wrapper.setProps({ viewRange: { time: { current: [0, 0.9] } } }); + expect(wrapper.find('.ViewingLayer--resetZoom').length).toBe(1); + }); + + it('should call props.updateViewRangeTime when clicked', () => { + wrapper.setProps({ viewRange: { time: { current: [0.1, 0.9] } } }); + const resetZoomButton = wrapper.find('.ViewingLayer--resetZoom'); + // If the test fails on the following expect statement, this may be a false negative caused + // by a regression to rendering. + expect(resetZoomButton.length).toBe(1); + + resetZoomButton.simulate('click'); + expect(props.updateViewRangeTime).lastCalledWith(0, 1); + }); + }); }); it('renders a ', () => { From 29b340dd13224267cbd581feb3fa329408987f63 Mon Sep 17 00:00:00 2001 From: Everett Date: Fri, 4 Jan 2019 13:46:44 -0500 Subject: [PATCH 05/46] Add a copy icon to entries in KeyValuesTable (#204) (#292) * Add a copy icon to entries in KeyValuesTable (#204) Signed-off-by: Everett Ross * Add a tooltip to copy icon in KeyValuesTable Signed-off-by: Everett Ross * Fix copied test name, add test for KeyValuesTable state change on tooltip hide Signed-off-by: Everett Ross * Add eslint rule to prevent unnecessary braces in jsx Signed-off-by: Everett Ross * Add classname to tr to remove element selector, fix yarn.lock Signed-off-by: Everett Ross Signed-off-by: Everett Ross --- .eslintrc | 1 + package.json | 2 +- packages/jaeger-ui/package.json | 1 + .../jaeger-ui/src/components/App/NotFound.js | 2 +- .../TraceDiff/TraceDiffHeader/TraceHeader.js | 2 +- .../SpanDetail/KeyValuesTable.css | 18 ++- .../SpanDetail/KeyValuesTable.js | 153 ++++++++++++------ .../SpanDetail/KeyValuesTable.test.js | 55 ++++++- .../TraceTimelineViewer/SpanDetailRow.js | 2 +- yarn.lock | 120 +++++++++++++- 10 files changed, 286 insertions(+), 70 deletions(-) diff --git a/.eslintrc b/.eslintrc index 7c29c80399..026f9040c9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -27,6 +27,7 @@ "jsx-a11y/interactive-supports-focus": 0, /* react */ + "react/jsx-curly-brace-presence": [2, 'never'], "react/jsx-filename-extension": 0, "react/forbid-prop-types": 1, "react/require-default-props": 1, diff --git a/package.json b/package.json index 05dc0dc24c..7858e7e877 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "eslint-plugin-flowtype": "^2.35.0", "eslint-plugin-import": "^2.7.0", "eslint-plugin-jsx-a11y": "^6.0.2", - "eslint-plugin-react": "^7.2.1", + "eslint-plugin-react": "^7.12.2", "flow-bin": "^0.71.0", "glow": "^1.2.2", "husky": "^0.14.3", diff --git a/packages/jaeger-ui/package.json b/packages/jaeger-ui/package.json index 7a77eb9f5e..e74c80a541 100644 --- a/packages/jaeger-ui/package.json +++ b/packages/jaeger-ui/package.json @@ -56,6 +56,7 @@ "query-string": "^5.0.0", "raven-js": "^3.22.1", "react": "^16.3.2", + "react-copy-to-clipboard": "^5.0.1", "react-dimensions": "^1.3.0", "react-dom": "^16.3.2", "react-ga": "^2.4.1", diff --git a/packages/jaeger-ui/src/components/App/NotFound.js b/packages/jaeger-ui/src/components/App/NotFound.js index bc2aa7f70a..fa63c6708c 100644 --- a/packages/jaeger-ui/src/components/App/NotFound.js +++ b/packages/jaeger-ui/src/components/App/NotFound.js @@ -29,7 +29,7 @@ export default function NotFound({ error }: NotFoundProps) {

Error

{error && } - {'Back home'} + Back home ); } diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceHeader.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceHeader.js index 089f079356..0d64f8e36e 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceHeader.js +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceHeader.js @@ -76,7 +76,7 @@ export default function TraceHeader(props: Props) { const AttrsComponent = state === fetchedState.DONE ? Attrs : EmptyAttrs; return (
-

+

{traceID ? ( diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.css index 826cd97a05..04a80ad144 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.css @@ -26,11 +26,11 @@ limitations under the License. vertical-align: baseline; } -.KeyValueTable--body > tr > td { +.KeyValueTable--row > td { padding: 0.25rem 0.5rem; } -.KeyValueTable--body > tr:nth-child(2n) > td { +.KeyValueTable--row:nth-child(2n) > td { background: #f5f5f5; } @@ -40,7 +40,19 @@ limitations under the License. width: 125px; } -.KeyValueTable--body > tr > td { +.KeyValueTable--copyColumn { + text-align: right; +} + +.KeyValueTable--copyIcon { + visibility: hidden; +} + +.KeyValueTable--row:hover > .KeyValueTable--copyColumn > .KeyValueTable--copyIcon { + visibility: unset; +} + +.KeyValueTable--row > td { padding: 0.25rem 0.5rem; vertical-align: top; } diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.js index 229f2c977b..c80f3b7ff8 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.js @@ -16,7 +16,8 @@ import * as React from 'react'; import jsonMarkup from 'json-markup'; -import { Dropdown, Icon, Menu } from 'antd'; +import { Dropdown, Icon, Menu, Tooltip } from 'antd'; +import { CopyToClipboard } from 'react-copy-to-clipboard'; import type { KeyValuePair, Link } from '../../../../types/trace'; import './KeyValuesTable.css'; @@ -32,7 +33,7 @@ function parseIfJson(value) { return value; } -const LinkValue = (props: { href: string, title?: string, children: React.Node }) => ( +export const LinkValue = (props: { href: string, title?: string, children: React.Node }) => ( {props.children} @@ -55,54 +56,104 @@ type KeyValuesTableProps = { linksGetter: ?(KeyValuePair[], number) => Link[], }; -export default function KeyValuesTable(props: KeyValuesTableProps) { - const { data, linksGetter } = props; - return ( -
- - - {data.map((row, i) => { - const markup = { - __html: jsonMarkup(parseIfJson(row.value)), - }; - // eslint-disable-next-line react/no-danger - const jsonTable =
; - const links = linksGetter ? linksGetter(data, i) : null; - let valueMarkup; - if (links && links.length === 1) { - valueMarkup = ( -
- - {jsonTable} - -
- ); - } else if (links && links.length > 1) { - valueMarkup = ( - +type KeyValuesTableState = { + copiedRows: Set, +}; + +export default class KeyValuesTable extends React.PureComponent { + props: KeyValuesTableProps; + + constructor(props: KeyValuesTableProps) { + super(props); + + this.state = { + copiedRows: new Set(), + }; + } + + handleCopyIconClick = (row: KeyValuePair) => { + const newCopiedRows = new Set(this.state.copiedRows); + newCopiedRows.add(row); + this.setState({ + copiedRows: newCopiedRows, + }); + }; + + handleTooltipVisibilityChange = (row: KeyValuePair, visible: boolean) => { + if (!visible && this.state.copiedRows.has(row)) { + const newCopiedRows = new Set(this.state.copiedRows); + newCopiedRows.delete(row); + this.setState({ + copiedRows: newCopiedRows, + }); + } + }; + + render() { + const { data, linksGetter } = this.props; + return ( +
+
+ + {data.map((row, i) => { + const tooltipTitle = this.state.copiedRows.has(row) ? 'Copied' : 'Copy JSON'; + const markup = { + __html: jsonMarkup(parseIfJson(row.value)), + }; + // eslint-disable-next-line react/no-danger + const jsonTable =
; + const links = linksGetter ? linksGetter(data, i) : null; + let valueMarkup; + if (links && links.length === 1) { + valueMarkup = ( +
+ + {jsonTable} + +
+ ); + } else if (links && links.length > 1) { + valueMarkup = ( + + ); + } else { + valueMarkup = jsonTable; + } + return ( + // `i` is necessary in the key because row.key can repeat + // eslint-disable-next-line react/no-array-index-key +
+ + + + ); - } else { - valueMarkup = jsonTable; - } - return ( - // `i` is necessary in the key because row.key can repeat - // eslint-disable-next-line react/no-array-index-key - - - - - ); - })} - -
{row.key}{valueMarkup} + this.handleTooltipVisibilityChange(row, visible)} + placement="left" + title={tooltipTitle} + > + + this.handleCopyIconClick(row)} + type="copy" + /> + + +
{row.key}{valueMarkup}
-
- ); + })} + + +

+ ); + } } - -KeyValuesTable.LinkValue = LinkValue; diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.js index 653c8104af..5ece890e86 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.js @@ -14,9 +14,10 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Dropdown } from 'antd'; +import { Dropdown, Icon, Tooltip } from 'antd'; +import { CopyToClipboard } from 'react-copy-to-clipboard'; -import KeyValuesTable from './KeyValuesTable'; +import KeyValuesTable, { LinkValue } from './KeyValuesTable'; describe('', () => { let wrapper; @@ -53,7 +54,7 @@ describe('', () => { : [], }); - const anchor = wrapper.find(KeyValuesTable.LinkValue); + const anchor = wrapper.find(LinkValue); expect(anchor).toHaveLength(1); expect(anchor.prop('href')).toBe('http://example.com/?kind=client'); expect(anchor.prop('title')).toBe('More info about client'); @@ -78,7 +79,7 @@ describe('', () => { }); const dropdown = wrapper.find(Dropdown); const menu = shallow(dropdown.prop('overlay')); - const anchors = menu.find(KeyValuesTable.LinkValue); + const anchors = menu.find(LinkValue); expect(anchors).toHaveLength(2); const firstAnchor = anchors.first(); expect(firstAnchor.prop('href')).toBe('http://example.com/1?kind=client'); @@ -94,4 +95,50 @@ describe('', () => { .text() ).toBe('span.kind'); }); + + describe('CopyIcon', () => { + const indexToCopy = 1; + + it('should render a Copy icon with and for each data element', () => { + const trs = wrapper.find('tr'); + expect(trs.length).toBe(data.length); + trs.forEach((tr, i) => { + const copyColumn = tr.find('.KeyValueTable--copyColumn'); + expect(copyColumn.find(CopyToClipboard).prop('text')).toBe(JSON.stringify(data[i], null, 2)); + expect(copyColumn.find(Tooltip).length).toBe(1); + expect(copyColumn.find({ type: 'copy' }).length).toBe(1); + }); + }); + + it('should add correct data entry to state when icon is clicked', () => { + expect(wrapper.state().copiedRows.size).toBe(0); + wrapper + .find('tr') + .at(indexToCopy) + .find(Icon) + .simulate('click'); + expect(wrapper.state().copiedRows.size).toBe(1); + expect(wrapper.state().copiedRows.has(data[indexToCopy])).toBe(true); + }); + + it('should remove correct data entry to state when tooltip hides', () => { + wrapper.setState({ copiedRows: new Set(data) }); + wrapper + .find('tr') + .at(indexToCopy) + .find(Tooltip) + .prop('onVisibleChange')(false); + expect(wrapper.state().copiedRows.size).toBe(data.length - 1); + expect(wrapper.state().copiedRows.has(data[indexToCopy])).toBe(false); + }); + + it('should render correct tooltip title for each row', () => { + wrapper.setState({ copiedRows: new Set([data[indexToCopy]]) }); + const tooltips = wrapper.find(Tooltip); + tooltips.forEach((tooltip, i) => + expect(tooltip.prop('title')).toBe(i === indexToCopy ? 'Copied' : 'Copy JSON') + ); + expect.assertions(data.length); + }); + }); }); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.js index 660eeb229c..4a3954d06f 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.js @@ -63,7 +63,7 @@ export default class SpanDetailRow extends React.PureComponent + diff --git a/yarn.lock b/yarn.lock index 90e800ed26..c37c41c4fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2712,6 +2712,13 @@ copy-template-dir@1.3.0: readdirp "^2.0.0" run-parallel "^1.1.4" +copy-to-clipboard@^3: + version "3.0.8" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9" + integrity sha512-c3GdeY8qxCHGezVb1EFQfHYK/8NZRemgcTIzPq7PuxjHAf/raKibn2QdhHPb/y6q74PMgH6yizaDZlRmw6QyKw== + dependencies: + toggle-selection "^1.0.3" + copy-webpack-plugin@4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.3.1.tgz#19ba6370bf6f8e263cbd66185a2b79f2321a9302" @@ -3506,7 +3513,7 @@ doctrine@1.5.0: esutils "^2.0.2" isarray "^1.0.0" -doctrine@^2.0.0: +doctrine@^2.0.0, doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" dependencies: @@ -3861,6 +3868,18 @@ error-stack-parser@1.3.6: dependencies: stackframe "^0.3.1" +es-abstract@^1.11.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" + integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== + dependencies: + es-to-primitive "^1.2.0" + function-bind "^1.1.1" + has "^1.0.3" + is-callable "^1.1.4" + is-regex "^1.0.4" + object-keys "^1.0.12" + es-abstract@^1.5.1, es-abstract@^1.7.0: version "1.10.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864" @@ -3889,6 +3908,15 @@ es-to-primitive@^1.1.1: is-date-object "^1.0.1" is-symbol "^1.0.1" +es-to-primitive@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" + integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: version "0.10.38" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.38.tgz#fa7d40d65bbc9bb8a67e1d3f9cc656a00530eed3" @@ -4128,13 +4156,18 @@ eslint-plugin-react@7.4.0: jsx-ast-utils "^2.0.0" prop-types "^15.5.10" -eslint-plugin-react@^7.2.1: - version "7.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.2.1.tgz#c2673526ed6571b08c69c5f453d03f5f13e8ddbe" +eslint-plugin-react@^7.12.2: + version "7.12.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.12.2.tgz#95a4d8117011787824625ea57be9e38401d33548" + integrity sha512-6F8uOJXOsWWWD3Mg8cvz4onsqEYp2LFZCFlgjaTAzbPLwqdwRCeK78unbuEaMXYPxq8paXCzTpoaDCwXBvC/gg== dependencies: - doctrine "^2.0.0" - has "^1.0.1" - jsx-ast-utils "^2.0.0" + array-includes "^3.0.3" + doctrine "^2.1.0" + has "^1.0.3" + jsx-ast-utils "^2.0.1" + object.fromentries "^2.0.0" + prop-types "^15.6.2" + resolve "^1.9.0" eslint-restricted-globals@^0.1.1: version "0.1.1" @@ -5410,6 +5443,11 @@ has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" +has-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" + integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -5447,6 +5485,13 @@ has@^1.0.0, has@^1.0.1: dependencies: function-bind "^1.0.2" +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + hash-base@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" @@ -5979,6 +6024,11 @@ is-callable@^1.0.4, is-callable@^1.1.1, is-callable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" +is-callable@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" + integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== + is-ci@^1.0.10: version "1.1.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.1.0.tgz#247e4162e7860cebbdaf30b774d6b0ac7dcfe7a5" @@ -6265,6 +6315,13 @@ is-symbol@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" +is-symbol@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" + integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== + dependencies: + has-symbols "^1.0.0" + is-text-path@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e" @@ -7175,6 +7232,13 @@ jsx-ast-utils@^2.0.0: dependencies: array-includes "^3.0.3" +jsx-ast-utils@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f" + integrity sha1-6AGxs5mF4g//yHtA43SAgOLcrH8= + dependencies: + array-includes "^3.0.3" + just-extend@^1.1.22: version "1.1.22" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.22.tgz#3330af756cab6a542700c64b2e4e4aa062d52fff" @@ -8676,6 +8740,11 @@ object-keys@^1.0.10, object-keys@^1.0.8, object-keys@^1.0.9: version "1.0.11" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" +object-keys@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" + integrity sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag== + object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" @@ -8699,6 +8768,16 @@ object.entries@^1.0.4: function-bind "^1.1.0" has "^1.0.1" +object.fromentries@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.0.tgz#49a543d92151f8277b3ac9600f1e930b189d30ab" + integrity sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA== + dependencies: + define-properties "^1.1.2" + es-abstract "^1.11.0" + function-bind "^1.1.1" + has "^1.0.1" + object.getownpropertydescriptors@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" @@ -9061,6 +9140,11 @@ path-parse@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + path-platform@~0.11.15: version "0.11.15" resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" @@ -9602,7 +9686,7 @@ prop-types@15.x, prop-types@^15.5.7, prop-types@^15.6.0: loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@^15.5.0: +prop-types@^15.5.0, prop-types@^15.6.2: version "15.6.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" dependencies: @@ -10241,6 +10325,14 @@ react-app-rewired@^1.4.0: cross-spawn "^5.1.0" dotenv "^4.0.0" +react-copy-to-clipboard@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e" + integrity sha512-ELKq31/E3zjFs5rDWNCfFL4NvNFQvGRoJdAKReD/rUPA+xxiLPQmZBZBvy2vgH7V0GE9isIQpT9WXbwIVErYdA== + dependencies: + copy-to-clipboard "^3" + prop-types "^15.5.8" + react-deep-force-update@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/react-deep-force-update/-/react-deep-force-update-1.1.1.tgz#bcd31478027b64b3339f108921ab520b4313dc2c" @@ -11135,6 +11227,13 @@ resolve@^1.1.3, resolve@^1.1.4, resolve@^1.2.0: dependencies: path-parse "^1.0.5" +resolve@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.9.0.tgz#a14c6fdfa8f92a7df1d996cb7105fa744658ea06" + integrity sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ== + dependencies: + path-parse "^1.0.6" + restore-cursor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" @@ -12373,6 +12472,11 @@ to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +toggle-selection@^1.0.3: + version "1.0.6" + resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" + integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI= + toposort@^1.0.0: version "1.0.6" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.6.tgz#c31748e55d210effc00fdcdc7d6e68d7d7bb9cec" From e0f34e4de7118c68b23ea66de5a5c45c0389670f Mon Sep 17 00:00:00 2001 From: Everett Date: Fri, 4 Jan 2019 15:48:16 -0500 Subject: [PATCH 06/46] Add popover and prevent submit if duration params are invalid (#244) (#291) * Add validation for duration fields in SearchForm (#244) Signed-off-by: Everett Ross * Add tests for redux-form-field-adapter Signed-off-by: Everett Ross * Fix boolean prop type Signed-off-by: Everett Ross * Add boolean for input validation, change popover to show when inactive Signed-off-by: Everett Ross * Add tests for onChangeAdapter Signed-off-by: Everett Ross * Create separate ValidatedAdaptedInput for duration fields Signed-off-by: Everett Ross * Remove unnecessary curly braces Signed-off-by: Everett Ross Signed-off-by: Everett Ross --- .../components/SearchTracePage/SearchForm.js | 43 +++++-- .../SearchTracePage/SearchForm.test.js | 31 +++++ .../SearchTracePage/SearchResults/index.js | 2 +- .../redux-form-field-adapter.test.js.snap | 117 ++++++++++++++++++ .../src/utils/redux-form-field-adapter.css | 20 +++ .../src/utils/redux-form-field-adapter.js | 35 +++++- .../utils/redux-form-field-adapter.test.js | 95 ++++++++++++++ 7 files changed, 329 insertions(+), 14 deletions(-) create mode 100644 packages/jaeger-ui/src/utils/__snapshots__/redux-form-field-adapter.test.js.snap create mode 100644 packages/jaeger-ui/src/utils/redux-form-field-adapter.css create mode 100644 packages/jaeger-ui/src/utils/redux-form-field-adapter.test.js diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.js b/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.js index 5db0e88b80..57657f48a6 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.js @@ -38,9 +38,13 @@ import './SearchForm.css'; const FormItem = Form.Item; const Option = Select.Option; -const AdaptedInput = reduxFormFieldAdapter(Input); -const AdaptedSelect = reduxFormFieldAdapter(Select); -const AdaptedVirtualSelect = reduxFormFieldAdapter(VirtSelect, option => (option ? option.value : null)); +const AdaptedInput = reduxFormFieldAdapter({ AntInputComponent: Input }); +const AdaptedSelect = reduxFormFieldAdapter({ AntInputComponent: Select }); +const AdaptedVirtualSelect = reduxFormFieldAdapter({ + AntInputComponent: VirtSelect, + onChangeAdapter: option => (option ? option.value : null), +}); +const ValidatedAdaptedInput = reduxFormFieldAdapter({ AntInputComponent: Input, isValidatedInput: true }); export function getUnixTimeStampInMSFromForm({ startDate, startDateTime, endDate, endDateTime }) { const start = `${startDate} ${startDateTime}`; @@ -74,6 +78,17 @@ export function traceIDsToQuery(traceIDs) { return traceIDs.split(','); } +export const placeholderDurationFields = 'e.g. 1.2s, 100ms, 500us'; +export function validateDurationFields(value) { + if (!value) return undefined; + return /\d[\d\\.]*(us|ms|s|m|h)$/.test(value) + ? undefined + : { + content: `Please enter a number followed by a duration unit, ${placeholderDurationFields}`, + title: 'Please match the requested format.', + }; +} + export function convertQueryParamsToFormDates({ start, end }) { let queryStartDate; let queryStartDateTime; @@ -155,6 +170,7 @@ export class SearchFormImpl extends React.PureComponent { render() { const { handleSubmit, + invalid, selectedLookback, selectedService = '-', services, @@ -322,14 +338,21 @@ export class SearchFormImpl extends React.PureComponent { - + @@ -342,7 +365,11 @@ export class SearchFormImpl extends React.PureComponent { /> - @@ -352,6 +379,7 @@ export class SearchFormImpl extends React.PureComponent { SearchFormImpl.propTypes = { handleSubmit: PropTypes.func.isRequired, + invalid: PropTypes.bool, submitting: PropTypes.bool, services: PropTypes.arrayOf( PropTypes.shape({ @@ -364,6 +392,7 @@ SearchFormImpl.propTypes = { }; SearchFormImpl.defaultProps = { + invalid: false, services: [], submitting: false, selectedService: null, diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.test.js b/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.test.js index e2f3128ca0..3f2edecc03 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.test.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchForm.test.js @@ -29,6 +29,7 @@ import { submitForm, traceIDsToQuery, SearchFormImpl as SearchForm, + validateDurationFields, } from './SearchForm'; import * as markers from './SearchForm.markers'; @@ -275,6 +276,36 @@ describe('', () => { btn = wrapper.find(`[data-test="${markers.SUBMIT_BTN}"]`); expect(btn.prop('disabled')).toBeFalsy(); }); + + it('disables the submit button when the form has invalid data', () => { + wrapper = shallow(); + let btn = wrapper.find(`[data-test="${markers.SUBMIT_BTN}"]`); + // If this test fails on the following expect statement, this may be a false negative caused by a separate + // regression. + expect(btn.prop('disabled')).toBeFalsy(); + wrapper.setProps({ invalid: true }); + btn = wrapper.find(`[data-test="${markers.SUBMIT_BTN}"]`); + expect(btn.prop('disabled')).toBeTruthy(); + }); +}); + +describe('validation', () => { + it('should return `undefined` if the value is falsy', () => { + expect(validateDurationFields('')).toBeUndefined(); + expect(validateDurationFields(null)).toBeUndefined(); + expect(validateDurationFields(undefined)).toBeUndefined(); + }); + + it('should return Popover-compliant error object if the value is a populated string that does not adhere to expected format', () => { + expect(validateDurationFields('100')).toEqual({ + content: 'Please enter a number followed by a duration unit, e.g. 1.2s, 100ms, 500us', + title: 'Please match the requested format.', + }); + }); + + it('should return `undefined` if the value is a populated string that adheres to expected format', () => { + expect(validateDurationFields('100ms')).toBeUndefined(); + }); }); describe('mapStateToProps()', () => { diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.js b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.js index cbdb34aef2..af2aff4d65 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.js @@ -61,7 +61,7 @@ function SelectSortImpl() { return (