diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/ReferencesButton.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/ReferencesButton.css new file mode 100644 index 0000000000..10c17cbf36 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/ReferencesButton.css @@ -0,0 +1,36 @@ +/* +Copyright (c) 2019 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. +*/ + +.ReferencesButton-MultiParent { + padding: 0 5px; + color: #000; +} +.ReferencesButton-MultiParent ~ .ReferencesButton-MultiParent { + margin-left: 5px; +} + +a.ReferencesButton--TraceRefLink { + display: flex; + justify-content: space-between; +} + +a.ReferencesButton--TraceRefLink > .NewWindowIcon { + margin: 0.2em 0 0; +} + +.ReferencesButton-tooltip { + max-width: none; +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/ReferencesButton.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/ReferencesButton.test.js new file mode 100644 index 0000000000..bf85d9b19b --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/ReferencesButton.test.js @@ -0,0 +1,83 @@ +// Copyright (c) 2019 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 { Menu, Dropdown, Tooltip } from 'antd'; + +import ReferencesButton from './ReferencesButton'; +import transformTraceData from '../../../model/transform-trace-data'; +import traceGenerator from '../../../demo/trace-generators'; +import ReferenceLink from '../url/ReferenceLink'; + +describe(ReferencesButton, () => { + const trace = transformTraceData(traceGenerator.trace({ numberOfSpans: 10 })); + const oneReference = trace.spans[1].references; + + const moreReferences = oneReference.slice(); + const externalSpanID = 'extSpan'; + + moreReferences.push( + { + refType: 'CHILD_OF', + traceID: trace.traceID, + spanID: trace.spans[2].spanID, + span: trace.spans[2], + }, + { + refType: 'CHILD_OF', + traceID: 'otherTrace', + spanID: externalSpanID, + } + ); + + const baseProps = { + focusSpan: () => {}, + }; + + it('renders single reference', () => { + const props = { ...baseProps, references: oneReference }; + const wrapper = shallow(); + const dropdown = wrapper.find(Dropdown); + const refLink = wrapper.find(ReferenceLink); + const tooltip = wrapper.find(Tooltip); + + expect(dropdown.length).toBe(0); + expect(refLink.length).toBe(1); + expect(refLink.prop('reference')).toBe(oneReference[0]); + expect(refLink.first().props().className).toBe('ReferencesButton-MultiParent'); + expect(tooltip.length).toBe(1); + expect(tooltip.prop('title')).toBe(props.tooltipText); + }); + + it('renders multiple references', () => { + const props = { ...baseProps, references: moreReferences }; + const wrapper = shallow(); + const dropdown = wrapper.find(Dropdown); + expect(dropdown.length).toBe(1); + const menuInstance = shallow(dropdown.first().props().overlay); + const submenuItems = menuInstance.find(Menu.Item); + expect(submenuItems.length).toBe(3); + submenuItems.forEach((submenuItem, i) => { + expect(submenuItem.find(ReferenceLink).prop('reference')).toBe(moreReferences[i]); + }); + expect( + submenuItems + .at(2) + .find(ReferenceLink) + .childAt(0) + .text() + ).toBe(`(another trace) - ${moreReferences[2].spanID}`); + }); +}); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/ReferencesButton.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/ReferencesButton.tsx new file mode 100644 index 0000000000..7525ebef48 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/ReferencesButton.tsx @@ -0,0 +1,83 @@ +// Copyright (c) 2019 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 { Dropdown, Menu, Tooltip } from 'antd'; +import { TooltipPlacement } from 'antd/lib/tooltip'; +import NewWindowIcon from '../../common/NewWindowIcon'; +import { SpanReference } from '../../../types/trace'; + +import './ReferencesButton.css'; +import ReferenceLink from '../url/ReferenceLink'; + +type TReferencesButtonProps = { + references: SpanReference[]; + children: React.ReactNode; + tooltipText: string; + focusSpan: (spanID: string) => void; +}; + +export default class ReferencesButton extends React.PureComponent { + referencesList = (references: SpanReference[]) => ( + + {references.map(ref => { + const { span, spanID } = ref; + return ( + + + {span + ? `${span.process.serviceName}:${span.operationName} - ${ref.spanID}` + : `(another trace) - ${ref.spanID}`} + {!span && } + + + ); + })} + + ); + + render() { + const { references, children, tooltipText, focusSpan } = this.props; + + const tooltipProps = { + arrowPointAtCenter: true, + mouseLeaveDelay: 0.5, + placement: 'bottom' as TooltipPlacement, + title: tooltipText, + overlayClassName: 'ReferencesButton--tooltip', + }; + + if (references.length > 1) { + return ( + + + {children} + + + ); + } + const ref = references[0]; + return ( + + + {children} + + + ); + } +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.test.js index 39cd5af596..f339653df8 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.test.js @@ -13,10 +13,11 @@ // limitations under the License. import React from 'react'; -import { mount } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import SpanBarRow from './SpanBarRow'; import SpanTreeOffset from './SpanTreeOffset'; +import ReferencesButton from './ReferencesButton'; jest.mock('./SpanTreeOffset'); @@ -78,4 +79,87 @@ describe('', () => { wrapper.find(SpanTreeOffset).prop('onClick')(); expect(onChildrenToggled.mock.calls).toEqual([[spanID]]); }); + + it('render references button', () => { + const span = Object.assign( + { + references: [ + { + refType: 'CHILD_OF', + traceID: 'trace1', + spanID: 'span0', + span: { + spanID: 'span0', + }, + }, + { + refType: 'CHILD_OF', + traceID: 'otherTrace', + spanID: 'span1', + span: { + spanID: 'span1', + }, + }, + ], + }, + props.span + ); + + const spanRow = shallow(); + const refButton = spanRow.find(ReferencesButton); + expect(refButton.length).toEqual(1); + expect(refButton.at(0).props().tooltipText).toEqual('Contains multiple references'); + }); + + it('render referenced to by single span', () => { + const span = Object.assign( + { + subsidiarilyReferencedBy: [ + { + refType: 'CHILD_OF', + traceID: 'trace1', + spanID: 'span0', + span: { + spanID: 'span0', + }, + }, + ], + }, + props.span + ); + const spanRow = shallow(); + const refButton = spanRow.find(ReferencesButton); + expect(refButton.length).toEqual(1); + expect(refButton.at(0).props().tooltipText).toEqual('This span is referenced by another span'); + }); + + it('render referenced to by multiple span', () => { + const span = Object.assign( + { + subsidiarilyReferencedBy: [ + { + refType: 'CHILD_OF', + traceID: 'trace1', + spanID: 'span0', + span: { + spanID: 'span0', + }, + }, + { + refType: 'CHILD_OF', + traceID: 'trace1', + spanID: 'span1', + span: { + spanID: 'span1', + }, + }, + ], + }, + props.span + ); + const spanRow = shallow(); + const refButton = spanRow.find(ReferencesButton); + expect(refButton.length).toEqual(1); + expect(refButton.at(0).props().tooltipText).toEqual('This span is referenced by multiple other spans'); + }); }); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.tsx index f69c0f4eb8..d9189c9123 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.tsx @@ -15,7 +15,9 @@ import * as React from 'react'; import IoAlert from 'react-icons/lib/io/alert'; import IoArrowRightA from 'react-icons/lib/io/arrow-right-a'; - +import IoNetwork from 'react-icons/lib/io/network'; +import MdFileUpload from 'react-icons/lib/md/file-upload'; +import ReferencesButton from './ReferencesButton'; import TimelineRow from './TimelineRow'; import { formatDuration, ViewedBoundsFunctionType } from './utils'; import SpanTreeOffset from './SpanTreeOffset'; @@ -50,6 +52,7 @@ type SpanBarRowProps = { getViewedBounds: ViewedBoundsFunctionType; traceStartTime: number; span: Span; + focusSpan: (spanID: string) => void; }; /** @@ -88,6 +91,7 @@ export default class SpanBarRow extends React.PureComponent { getViewedBounds, traceStartTime, span, + focusSpan, } = this.props; const { duration, @@ -150,6 +154,26 @@ export default class SpanBarRow extends React.PureComponent { {rpc ? rpc.operationName : operationName} + {span.references && span.references.length > 1 && ( + + + + )} + {span.subsidiarilyReferencedBy && span.subsidiarilyReferencedBy.length > 0 && ( + + + + )} a { + width: 100%; + display: inline-block; +} + +.ReferencesList--Item:nth-child(2n) { + background: #f5f5f5; +} + +.SpanReference--debugInfo { + letter-spacing: 0.25px; + margin: 0.5em 0 0; +} + +.SpanReference--debugLabel::before { + color: #bbb; + content: attr(data-label); +} + +.SpanReference--debugLabel { + margin: 0 5px 0 5px; +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js new file mode 100644 index 0000000000..774b0d57a0 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js @@ -0,0 +1,111 @@ +// Copyright (c) 2019 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 AccordianReferences, { References } from './AccordianReferences'; +import ReferenceLink from '../../url/ReferenceLink'; + +const traceID = 'trace1'; +const references = [ + { + refType: 'CHILD_OF', + span: { + spanID: 'span1', + traceID, + operationName: 'op1', + process: { + serviceName: 'service1', + }, + }, + spanID: 'span1', + traceID, + }, + { + refType: 'CHILD_OF', + span: { + spanID: 'span3', + traceID, + operationName: 'op2', + process: { + serviceName: 'service2', + }, + }, + spanID: 'span3', + traceID, + }, + { + refType: 'CHILD_OF', + spanID: 'span5', + traceID: 'trace2', + }, +]; + +describe('', () => { + let wrapper; + + const props = { + compact: false, + data: references, + highContrast: false, + isOpen: false, + onToggle: jest.fn(), + focusSpan: jest.fn(), + }; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('renders without exploding', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.exists()).toBe(true); + }); + + it('renders the content when it is expanded', () => { + wrapper.setProps({ isOpen: true }); + const content = wrapper.find(References); + expect(content.length).toBe(1); + expect(content.prop('data')).toBe(references); + }); +}); + +describe('', () => { + let wrapper; + + const props = { + data: references, + focusSpan: jest.fn(), + }; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('render references list', () => { + const refLinks = wrapper.find(ReferenceLink); + expect(refLinks.length).toBe(references.length); + refLinks.forEach((refLink, i) => { + const span = references[i].span; + const serviceName = refLink.find('span.span-svc-name').text(); + if (span && span.traceID === traceID) { + const endpointName = refLink.find('small.endpoint-name').text(); + expect(serviceName).toBe(span.process.serviceName); + expect(endpointName).toBe(span.operationName); + } else { + expect(serviceName).toBe('< span in another trace >'); + } + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx new file mode 100644 index 0000000000..b4b2423b86 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx @@ -0,0 +1,116 @@ +// Copyright (c) 2019 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 cx from 'classnames'; +import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down'; +import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right'; +import './AccordianReferences.css'; +import { SpanReference } from '../../../../types/trace'; +import ReferenceLink from '../../url/ReferenceLink'; + +type AccordianReferencesProps = { + data: SpanReference[]; + highContrast?: boolean; + interactive?: boolean; + isOpen: boolean; + onToggle?: null | (() => void); + focusSpan: (uiFind: string) => void; +}; + +type ReferenceItemProps = { + data: SpanReference[]; + focusSpan: (uiFind: string) => void; +}; + +// export for test +export function References(props: ReferenceItemProps) { + const { data, focusSpan } = props; + + return ( +
+
    + {data.map(reference => { + return ( +
  • + + + {reference.span ? ( + + {reference.span.process.serviceName} + {reference.span.operationName} + + ) : ( + < span in another trace > + )} + + + {reference.refType} + + + {reference.spanID} + + + + +
  • + ); + })} +
+
+ ); +} + +export default class AccordianReferences extends React.PureComponent { + static defaultProps = { + highContrast: false, + interactive: true, + onToggle: null, + }; + + render() { + const { data, highContrast, interactive, isOpen, onToggle, focusSpan } = this.props; + const isEmpty = !Array.isArray(data) || !data.length; + const iconCls = cx('u-align-icon', { 'AccordianKReferences--emptyIcon': isEmpty }); + let arrow: React.ReactNode | null = null; + let headerProps: Object | null = null; + if (interactive) { + arrow = isOpen ? : ; + headerProps = { + 'aria-checked': isOpen, + onClick: isEmpty ? null : onToggle, + role: 'switch', + }; + } + return ( +
+
+ {arrow} + + References + {' '} + ({data.length}) +
+ {isOpen && } +
+ ); + } +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/DetailState.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/DetailState.tsx index ed822aea1d..3e856dbd01 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/DetailState.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/DetailState.tsx @@ -22,12 +22,19 @@ export default class DetailState { isProcessOpen: boolean; logs: { isOpen: boolean; openedItems: Set }; isWarningsOpen: boolean; + isReferencesOpen: boolean; constructor(oldState?: DetailState) { - const { isTagsOpen, isProcessOpen, isWarningsOpen, logs }: DetailState | Record = - oldState || {}; + const { + isTagsOpen, + isProcessOpen, + isReferencesOpen, + isWarningsOpen, + logs, + }: DetailState | Record = oldState || {}; this.isTagsOpen = Boolean(isTagsOpen); this.isProcessOpen = Boolean(isProcessOpen); + this.isReferencesOpen = Boolean(isReferencesOpen); this.isWarningsOpen = Boolean(isWarningsOpen); this.logs = { isOpen: Boolean(logs && logs.isOpen), @@ -47,6 +54,12 @@ export default class DetailState { return next; } + toggleReferences() { + const next = new DetailState(this); + next.isReferencesOpen = !this.isReferencesOpen; + return next; + } + toggleWarnings() { const next = new DetailState(this); next.isWarningsOpen = !this.isWarningsOpen; diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js index 485ddb9294..50f08f6857 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js @@ -36,6 +36,7 @@ describe('', () => { const detailState = new DetailState() .toggleLogs() .toggleProcess() + .toggleReferences() .toggleTags(); const traceStartTime = 5; const props = { @@ -47,6 +48,7 @@ describe('', () => { processToggle: jest.fn(), tagsToggle: jest.fn(), warningsToggle: jest.fn(), + referencesToggle: jest.fn(), }; span.logs = [ { @@ -61,6 +63,48 @@ describe('', () => { span.warnings = ['Warning 1', 'Warning 2']; + span.references = [ + { + refType: 'CHILD_OF', + span: { + spanID: 'span2', + traceID: 'trace1', + operationName: 'op1', + process: { + serviceName: 'service1', + }, + }, + spanID: 'span1', + traceID: 'trace1', + }, + { + refType: 'CHILD_OF', + span: { + spanID: 'span3', + traceID: 'trace1', + operationName: 'op2', + process: { + serviceName: 'service2', + }, + }, + spanID: 'span4', + traceID: 'trace1', + }, + { + refType: 'CHILD_OF', + span: { + spanID: 'span6', + traceID: 'trace2', + operationName: 'op2', + process: { + serviceName: 'service2', + }, + }, + spanID: 'span5', + traceID: 'trace2', + }, + ]; + beforeEach(() => { formatDuration.mockReset(); props.tagsToggle.mockReset(); @@ -130,6 +174,13 @@ describe('', () => { expect(props.warningsToggle).toHaveBeenLastCalledWith(span.spanID); }); + it('renders the references', () => { + const refElem = wrapper.find({ data: span.references }); + expect(refElem.length).toBe(1); + refElem.simulate('toggle'); + expect(props.referencesToggle).toHaveBeenLastCalledWith(span.spanID); + }); + it('renders CopyIcon with deep link URL', () => { expect( wrapper diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.tsx index 19d92dd261..f61282f680 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.tsx @@ -27,6 +27,7 @@ import { TNil } from '../../../../types'; import { KeyValuePair, Link, Log, Span } from '../../../../types/trace'; import './index.css'; +import AccordianReferences from './AccordianReferences'; type SpanDetailProps = { detailState: DetailState; @@ -38,6 +39,8 @@ type SpanDetailProps = { tagsToggle: (spanID: string) => void; traceStartTime: number; warningsToggle: (spanID: string) => void; + referencesToggle: (spanID: string) => void; + focusSpan: (uiFind: string) => void; }; export default function SpanDetail(props: SpanDetailProps) { @@ -51,9 +54,21 @@ export default function SpanDetail(props: SpanDetailProps) { tagsToggle, traceStartTime, warningsToggle, + referencesToggle, + focusSpan, } = props; - const { isTagsOpen, isProcessOpen, logs: logsState, isWarningsOpen } = detailState; - const { operationName, process, duration, relativeStartTime, spanID, logs, tags, warnings } = span; + const { isTagsOpen, isProcessOpen, logs: logsState, isWarningsOpen, isReferencesOpen } = detailState; + const { + operationName, + process, + duration, + relativeStartTime, + spanID, + logs, + tags, + warnings, + references, + } = span; const overviewItems = [ { key: 'svc', @@ -125,6 +140,14 @@ export default function SpanDetail(props: SpanDetailProps) { onToggle={() => warningsToggle(spanID)} /> )} + {references && references.length > 1 && ( + referencesToggle(spanID)} + focusSpan={focusSpan} + /> + )} {spanID} void; logsToggle: (spanID: string) => void; processToggle: (spanID: string) => void; + referencesToggle: (spanID: string) => void; warningsToggle: (spanID: string) => void; span: Span; tagsToggle: (spanID: string) => void; traceStartTime: number; + focusSpan: (uiFind: string) => void; }; export default class SpanDetailRow extends React.PureComponent { @@ -56,10 +58,12 @@ export default class SpanDetailRow extends React.PureComponent @@ -83,10 +87,12 @@ export default class SpanDetailRow extends React.PureComponent
diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.test.js index d2f84783d2..d789c48b46 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.test.js @@ -21,12 +21,15 @@ import SpanDetailRow from './SpanDetailRow'; import { DEFAULT_HEIGHTS, VirtualizedTraceViewImpl } from './VirtualizedTraceView'; import traceGenerator from '../../../demo/trace-generators'; import transformTraceData from '../../../model/transform-trace-data'; +import updateUiFindSpy from '../../../utils/update-ui-find'; jest.mock('./SpanTreeOffset'); +jest.mock('../../../utils/update-ui-find'); describe('', () => { let wrapper; let instance; + const focusUiFindMatchesMock = jest.fn(); const trace = transformTraceData(traceGenerator.trace({ numberOfSpans: 10 })); const props = { @@ -44,11 +47,18 @@ describe('', () => { registerAccessors: jest.fn(), scrollToFirstVisibleSpan: jest.fn(), setSpanNameColumnWidth: jest.fn(), + focusUiFindMatches: focusUiFindMatchesMock, setTrace: jest.fn(), shouldScrollToFirstUiFindMatch: false, spanNameColumnWidth: 0.5, trace, uiFind: 'uiFind', + history: { + replace: () => {}, + }, + location: { + search: null, + }, }; function expandRow(rowIndex) { @@ -390,4 +400,17 @@ describe('', () => { }); }); }); + + describe('focusSpan', () => { + it('calls updateUiFind and focusUiFindMatches', () => { + const spanName = 'span1'; + instance.focusSpan(spanName); + expect(updateUiFindSpy).toHaveBeenLastCalledWith({ + history: props.history, + location: props.location, + uiFind: spanName, + }); + expect(focusUiFindMatchesMock).toHaveBeenLastCalledWith(trace, spanName, false); + }); + }); }); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.tsx index 2b129fa264..ddc65e2fe9 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.tsx @@ -41,6 +41,7 @@ import { Log, Span, Trace, KeyValuePair } from '../../../types/trace'; import TTraceTimeline from '../../../types/TTraceTimeline'; import './VirtualizedTraceView.css'; +import updateUiFind from '../../../utils/update-ui-find'; type RowState = { isDetail: boolean; @@ -62,11 +63,13 @@ type TDispatchProps = { detailLogItemToggle: (spanID: string, log: Log) => void; detailLogsToggle: (spanID: string) => void; detailWarningsToggle: (spanID: string) => void; + detailReferencesToggle: (spanID: string) => void; detailProcessToggle: (spanID: string) => void; detailTagsToggle: (spanID: string) => void; detailToggle: (spanID: string) => void; setSpanNameColumnWidth: (width: number) => void; setTrace: (trace: Trace | TNil, uiFind: string | TNil) => void; + focusUiFindMatches: (trace: Trace, uiFind: string | TNil, allowHide?: boolean) => void; }; type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & @@ -220,6 +223,18 @@ export class VirtualizedTraceViewImpl extends React.Component { + const { trace, focusUiFindMatches, location, history } = this.props; + if (trace) { + updateUiFind({ + location, + history, + uiFind, + }); + focusUiFindMatches(trace, uiFind, false); + } + }; + getAccessors() { const lv = this.listView; if (!lv) { @@ -360,6 +375,7 @@ export class VirtualizedTraceViewImpl extends React.Component ); @@ -372,6 +388,7 @@ export class VirtualizedTraceViewImpl extends React.Component ); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.test.js index 6af5d32e7f..5820f35f11 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.test.js @@ -387,6 +387,13 @@ describe('TraceTimelineViewer/duck', () => { unchecked: new DetailState(), checked: baseDetail.toggleWarnings(), }, + { + msg: 'toggles references', + action: actions.detailReferencesToggle(id), + get: state => state.detailStates.get(id), + unchecked: new DetailState(), + checked: baseDetail.toggleReferences(), + }, ]; beforeEach(() => { diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.tsx index 528c2a282f..cb31245fc9 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.tsx @@ -27,7 +27,7 @@ import spanAncestorIds from '../../../utils/span-ancestor-ids'; export type TSpanIdLogValue = { logItem: Log; spanID: string }; export type TSpanIdValue = { spanID: string }; type TSpansValue = { spans: Span[] }; -type TTraceUiFindValue = { trace: Trace; uiFind: string | TNil }; +type TTraceUiFindValue = { trace: Trace; uiFind: string | TNil; allowHide?: boolean }; export type TWidthValue = { width: number }; export type TActionTypes = | TSpanIdLogValue @@ -69,6 +69,7 @@ export const actionTypes = generateActionTypes('@jaeger-ui/trace-timeline-viewer 'DETAIL_LOGS_TOGGLE', 'DETAIL_LOG_ITEM_TOGGLE', 'DETAIL_WARNINGS_TOGGLE', + 'DETAIL_REFERENCES_TOGGLE', 'EXPAND_ALL', 'EXPAND_ONE', 'FOCUS_UI_FIND_MATCHES', @@ -89,9 +90,14 @@ const fullActions = createActions({ [actionTypes.EXPAND_ONE]: (spans: Span[]) => ({ spans }), [actionTypes.DETAIL_PROCESS_TOGGLE]: (spanID: string) => ({ spanID }), [actionTypes.DETAIL_WARNINGS_TOGGLE]: (spanID: string) => ({ spanID }), + [actionTypes.DETAIL_REFERENCES_TOGGLE]: (spanID: string) => ({ spanID }), [actionTypes.DETAIL_TAGS_TOGGLE]: (spanID: string) => ({ spanID }), [actionTypes.DETAIL_TOGGLE]: (spanID: string) => ({ spanID }), - [actionTypes.FOCUS_UI_FIND_MATCHES]: (trace: Trace, uiFind: string | TNil) => ({ trace, uiFind }), + [actionTypes.FOCUS_UI_FIND_MATCHES]: (trace: Trace, uiFind: string | TNil, allowHide?: boolean) => ({ + trace, + uiFind, + allowHide, + }), [actionTypes.REMOVE_HOVER_INDENT_GUIDE_ID]: (spanID: string) => ({ spanID }), [actionTypes.SET_SPAN_NAME_COLUMN_WIDTH]: (width: number) => ({ width }), [actionTypes.SET_TRACE]: (trace: Trace, uiFind: string | TNil) => ({ trace, uiFind }), @@ -99,7 +105,7 @@ const fullActions = createActions({ export const actions = (fullActions as any).jaegerUi.traceTimelineViewer as TTimelineViewerActions; -function calculateFocusedFindRowStates(uiFind: string, spans: Span[]) { +function calculateFocusedFindRowStates(uiFind: string, spans: Span[], allowHide: boolean = true) { const spansMap = new Map(); const childrenHiddenIDs: Set = new Set(); const detailStates: Map = new Map(); @@ -107,7 +113,9 @@ function calculateFocusedFindRowStates(uiFind: string, spans: Span[]) { spans.forEach(span => { spansMap.set(span.spanID, span); - childrenHiddenIDs.add(span.spanID); + if (allowHide) { + childrenHiddenIDs.add(span.spanID); + } }); const matchedSpanIds = filterSpans(uiFind, spans); if (matchedSpanIds && matchedSpanIds.size) { @@ -125,11 +133,11 @@ function calculateFocusedFindRowStates(uiFind: string, spans: Span[]) { }; } -function focusUiFindMatches(state: TTraceTimeline, { uiFind, trace }: TTraceUiFindValue) { +function focusUiFindMatches(state: TTraceTimeline, { uiFind, trace, allowHide }: TTraceUiFindValue) { if (!uiFind) return state; return { ...state, - ...calculateFocusedFindRowStates(uiFind, trace.spans), + ...calculateFocusedFindRowStates(uiFind, trace.spans, allowHide), }; } @@ -239,7 +247,7 @@ function detailToggle(state: TTraceTimeline, { spanID }: TSpanIdValue) { } function detailSubsectionToggle( - subSection: 'tags' | 'process' | 'logs' | 'warnings', + subSection: 'tags' | 'process' | 'logs' | 'warnings' | 'references', state: TTraceTimeline, { spanID }: TSpanIdValue ) { @@ -254,6 +262,8 @@ function detailSubsectionToggle( detailState = old.toggleProcess(); } else if (subSection === 'warnings') { detailState = old.toggleWarnings(); + } else if (subSection === 'references') { + detailState = old.toggleReferences(); } else { detailState = old.toggleLogs(); } @@ -266,6 +276,7 @@ const detailTagsToggle = detailSubsectionToggle.bind(null, 'tags'); const detailProcessToggle = detailSubsectionToggle.bind(null, 'process'); const detailLogsToggle = detailSubsectionToggle.bind(null, 'logs'); const detailWarningsToggle = detailSubsectionToggle.bind(null, 'warnings'); +const detailReferencesToggle = detailSubsectionToggle.bind(null, 'references'); function detailLogItemToggle(state: TTraceTimeline, { spanID, logItem }: TSpanIdLogValue) { const old = state.detailStates.get(spanID); @@ -305,6 +316,7 @@ export default handleActions( [actionTypes.DETAIL_LOG_ITEM_TOGGLE]: guardReducer(detailLogItemToggle), [actionTypes.DETAIL_PROCESS_TOGGLE]: guardReducer(detailProcessToggle), [actionTypes.DETAIL_WARNINGS_TOGGLE]: guardReducer(detailWarningsToggle), + [actionTypes.DETAIL_REFERENCES_TOGGLE]: guardReducer(detailReferencesToggle), [actionTypes.DETAIL_TAGS_TOGGLE]: guardReducer(detailTagsToggle), [actionTypes.DETAIL_TOGGLE]: guardReducer(detailToggle), [actionTypes.EXPAND_ALL]: guardReducer(expandAll), diff --git a/packages/jaeger-ui/src/components/TracePage/url/ReferenceLink.test.js b/packages/jaeger-ui/src/components/TracePage/url/ReferenceLink.test.js new file mode 100644 index 0000000000..64e9980ef2 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/url/ReferenceLink.test.js @@ -0,0 +1,61 @@ +// Copyright (c) 2019 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 ReferenceLink from './ReferenceLink'; + +describe(ReferenceLink, () => { + const focusMock = jest.fn(); + + const sameTraceRef = { + refType: 'CHILD_OF', + traceID: 'trace1', + spanID: 'span1', + span: { + // not null or undefined is an indicator of an internal reference + }, + }; + + const externalRef = { + refType: 'CHILD_OF', + traceID: 'trace2', + spanID: 'span2', + }; + + describe('rendering', () => { + it('render for this trace', () => { + const component = shallow(); + const link = component.find('a'); + expect(link.length).toBe(1); + expect(link.props().role).toBe('button'); + }); + + it('render for external trace', () => { + const component = shallow(); + const link = component.find('a[href="/trace/trace2/uiFind?=span2"]'); + expect(link.length).toBe(1); + }); + }); + describe('focus span', () => { + it('call focusSpan', () => { + focusMock.mockReset(); + const component = shallow(); + const link = component.find('a'); + link.simulate('click'); + expect(focusMock).toHaveBeenLastCalledWith('span1'); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/TracePage/url/ReferenceLink.tsx b/packages/jaeger-ui/src/components/TracePage/url/ReferenceLink.tsx new file mode 100644 index 0000000000..93cb551f09 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/url/ReferenceLink.tsx @@ -0,0 +1,50 @@ +// Copyright (c) 2019 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 { SpanReference } from '../../../types/trace'; +import { getUrl } from '.'; + +type ReferenceLinkProps = { + reference: SpanReference; + children: React.ReactNode; + className?: string; + focusSpan: (spanID: string) => void; + onClick?: () => void; +}; + +const linkToExternalSpan = (traceID: string, spanID: string) => `${getUrl(traceID)}/uiFind?=${spanID}`; + +export default function ReferenceLink(props: ReferenceLinkProps) { + const { reference, children, className, focusSpan, ...otherProps } = props; + delete otherProps.onClick; + if (reference.span) { + return ( + focusSpan(reference.spanID)} className={className} {...otherProps}> + {children} + + ); + } + return ( + + {children} + + ); +} diff --git a/packages/jaeger-ui/src/components/TracePage/url.tsx b/packages/jaeger-ui/src/components/TracePage/url/index.tsx similarity index 90% rename from packages/jaeger-ui/src/components/TracePage/url.tsx rename to packages/jaeger-ui/src/components/TracePage/url/index.tsx index 6f9b51929e..689d23167c 100644 --- a/packages/jaeger-ui/src/components/TracePage/url.tsx +++ b/packages/jaeger-ui/src/components/TracePage/url/index.tsx @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import prefixUrl from '../../utils/prefix-url'; +import prefixUrl from '../../../utils/prefix-url'; -import { TNil } from '../../types'; +import { TNil } from '../../../types'; export const ROUTE_PATH = prefixUrl('/trace/:id'); diff --git a/packages/jaeger-ui/src/demo/trace-generators.js b/packages/jaeger-ui/src/demo/trace-generators.js index c384163be6..5cd1782ca5 100644 --- a/packages/jaeger-ui/src/demo/trace-generators.js +++ b/packages/jaeger-ui/src/demo/trace-generators.js @@ -49,18 +49,18 @@ function getParentSpanId(span, levels) { } /* this simulates the hierarchy created by CHILD_OF tags */ -function attachReferences(spans) { - const depth = chance.integer({ min: 1, max: 10 }); +function attachReferences(spans, depth, spansPerLevel) { let levels = [[getSpanId(spans[0])]]; - const duplicateLevelFilter = currentLevels => spanID => - !currentLevels.find(level => level.indexOf(spanID) >= 0); + const duplicateLevelFilter = currentLevels => span => + !currentLevels.find(level => level.indexOf(span.spanID) >= 0); while (levels.length < depth) { + const remainingSpans = spans.filter(duplicateLevelFilter(levels)); + if (remainingSpans.length <= 0) break; const newLevel = chance - .pickset(spans, chance.integer({ min: 4, max: 8 })) - .map(getSpanId) - .filter(duplicateLevelFilter(levels)); + .pickset(remainingSpans, spansPerLevel || chance.integer({ min: 4, max: 8 })) + .map(getSpanId); levels.push(newLevel); } @@ -95,6 +95,8 @@ export default chance.mixin({ Math.ceil(chance.normal({ mean: 45, dev: 15 })) + 1, ]), numberOfProcesses = chance.integer({ min: 1, max: 10 }), + maxDepth = chance.integer({ min: 1, max: 10 }), + spansPerLevel = null, }) { const traceID = chance.guid(); const duration = chance.integer({ min: 10000, max: 5000000 }); @@ -109,7 +111,7 @@ export default chance.mixin({ traceStartTime: timestamp, traceEndTime: timestamp + duration, }); - spans = attachReferences(spans); + spans = attachReferences(spans, maxDepth, spansPerLevel); if (spans.length > 1) { spans = setupParentSpan(spans, { startTime: timestamp, duration }); } diff --git a/packages/jaeger-ui/src/model/transform-trace-data.tsx b/packages/jaeger-ui/src/model/transform-trace-data.tsx index 6fc358a7cf..04e0615515 100644 --- a/packages/jaeger-ui/src/model/transform-trace-data.tsx +++ b/packages/jaeger-ui/src/model/transform-trace-data.tsx @@ -144,11 +144,21 @@ export default function transformTraceData(data: TraceData & { spans: SpanData[] const tagsInfo = deduplicateTags(span.tags); span.tags = orderTags(tagsInfo.tags, getConfigValue('topTagPrefixes')); span.warnings = span.warnings.concat(tagsInfo.warnings); - span.references.forEach(ref => { + span.references.forEach((ref, index) => { const refSpan = spanMap.get(ref.spanID) as Span; if (refSpan) { // eslint-disable-next-line no-param-reassign ref.span = refSpan; + if (index > 0) { + // Don't take into account the parent, just other references. + refSpan.subsidiarilyReferencedBy = refSpan.subsidiarilyReferencedBy || []; + refSpan.subsidiarilyReferencedBy.push({ + spanID, + traceID, + span, + refType: ref.refType, + }); + } } }); spans.push(span); diff --git a/packages/jaeger-ui/src/reducers/trace.test.js b/packages/jaeger-ui/src/reducers/trace.test.js index 8e4135d79c..352645dd04 100644 --- a/packages/jaeger-ui/src/reducers/trace.test.js +++ b/packages/jaeger-ui/src/reducers/trace.test.js @@ -113,6 +113,38 @@ describe('fetch multiple traces', () => { }; expect(state.traces).toEqual(outcome); }); + + it('process multiple references', () => { + const multiRefTrace = traceGenerator.trace({ numberOfSpans: 7, maxDepth: 3, spansPerLevel: 4 }); + const { traceID, spanID: rootSpanId } = multiRefTrace.spans[0]; + const [willGainRef, willNotChange] = multiRefTrace.spans.filter( + span => span.references.length > 0 && span.references[0].spanID !== rootSpanId + ); + const { spanID: existingRefID } = willGainRef.references[0]; + const { spanID: willBeReferencedID } = willNotChange.references[0]; + + willGainRef.references.push({ + refType: 'CHILD_OF', + traceID, + spanID: willBeReferencedID, + }); + const tTrace = transformTraceData(multiRefTrace); + const multiReference = tTrace.spans.filter(span => span.references && span.references.length > 1); + + expect(multiReference.length).toEqual(1); + expect(new Set(multiReference[0].references)).toEqual( + new Set([ + expect.objectContaining({ spanID: willBeReferencedID }), + expect.objectContaining({ spanID: existingRefID }), + ]) + ); + const hasReferral = tTrace.spans.filter( + span => span.subsidiarilyReferencedBy && span.subsidiarilyReferencedBy.length > 0 + ); + expect(new Set(hasReferral[0].subsidiarilyReferencedBy)).toEqual( + new Set([expect.objectContaining({ spanID: willGainRef.spanID })]) + ); + }); }); it('handles a failed request', () => { diff --git a/packages/jaeger-ui/src/types/trace.tsx b/packages/jaeger-ui/src/types/trace.tsx index c06b1f4933..2b30b9b6f6 100644 --- a/packages/jaeger-ui/src/types/trace.tsx +++ b/packages/jaeger-ui/src/types/trace.tsx @@ -66,6 +66,7 @@ export type Span = SpanData & { tags: NonNullable; references: NonNullable; warnings: NonNullable; + subsidiarilyReferencedBy: Array; }; export type TraceData = {