Tracing: Fix links to traces in Explore (#50113)

* Tracing: Fix links to traces in Explore

* Fix links in dashboard

* Fix references and tracetimelineviewer tests

* Remove hard-coded references to fix tests

* Add noopener
This commit is contained in:
Connor Lindsey 2022-06-07 07:21:01 -06:00 committed by GitHub
parent f9ddb8bf86
commit c65d62c95b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 108 additions and 170 deletions

View File

@ -59,7 +59,7 @@ exports[`no enzyme tests`] = {
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianLogs.test.js:3960703835": [
[14, 19, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js:2429764318": [
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js:2025513694": [
[14, 19, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.js:3813002651": [
@ -89,10 +89,10 @@ exports[`no enzyme tests`] = {
"packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.test.js:551014442": [
[13, 26, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/index.test.js:381298544": [
"packages/jaeger-ui-components/src/TraceTimelineViewer/index.test.js:276996587": [
[14, 19, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/url/ReferenceLink.test.js:3249503373": [
"packages/jaeger-ui-components/src/url/ReferenceLink.test.js:261377050": [
[14, 26, 13, "RegExp match", "2409514259"]
],
"public/app/core/components/PageActionBar/PageActionBar.test.tsx:1251504193": [

View File

@ -63,7 +63,7 @@ describe('<AccordianReferences>', () => {
highContrast: false,
isOpen: false,
onToggle: jest.fn(),
focusSpan: jest.fn(),
createFocusSpanLink: jest.fn(),
};
beforeEach(() => {
@ -88,7 +88,7 @@ describe('<References>', () => {
const props = {
data: references,
focusSpan: jest.fn(),
createFocusSpanLink: jest.fn(),
};
beforeEach(() => {

View File

@ -17,7 +17,7 @@ import * as React from 'react';
import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down';
import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right';
import { GrafanaTheme2 } from '@grafana/data';
import { Field, GrafanaTheme2, LinkModel } from '@grafana/data';
import { Icon, useStyles2 } from '@grafana/ui';
import { autoColor } from '../../Theme';
@ -103,6 +103,11 @@ const getStyles = (theme: GrafanaTheme2) => {
serviceName: css`
margin-right: 8px;
`,
title: css`
display: flex;
align-items: center;
gap: 4px;
`,
};
};
@ -114,7 +119,7 @@ type AccordianReferencesProps = {
openedItems?: Set<TraceSpanReference>;
onItemToggle?: (reference: TraceSpanReference) => void;
onToggle?: null | (() => void);
focusSpan: (uiFind: string) => void;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel<Field>;
};
type ReferenceItemProps = {
@ -122,20 +127,20 @@ type ReferenceItemProps = {
interactive?: boolean;
openedItems?: Set<TraceSpanReference>;
onItemToggle?: (reference: TraceSpanReference) => void;
focusSpan: (uiFind: string) => void;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel<Field>;
};
// export for test
export function References(props: ReferenceItemProps) {
const { data, focusSpan, openedItems, onItemToggle, interactive } = props;
const { data, createFocusSpanLink, openedItems, onItemToggle, interactive } = props;
const styles = useStyles2(getStyles);
return (
<div className={styles.AccordianReferencesContent}>
{data.map((reference, i) => (
<div className={i < data.length - 1 ? styles.AccordianReferenceItem : undefined} key={reference.spanID}>
<div className={i < data.length - 1 ? styles.AccordianReferenceItem : undefined} key={i}>
<div className={styles.item} key={`${reference.spanID}`}>
<ReferenceLink reference={reference} focusSpan={focusSpan}>
<ReferenceLink reference={reference} createFocusSpanLink={createFocusSpanLink}>
<span className={styles.itemContent}>
{reference.span ? (
<span>
@ -145,7 +150,7 @@ export function References(props: ReferenceItemProps) {
<small className="endpoint-name">{reference.span.operationName}</small>
</span>
) : (
<span className="span-svc-name">
<span className={cx('span-svc-name', styles.title)}>
View Linked Span <Icon name="external-link-alt" />
</span>
)}
@ -187,7 +192,7 @@ const AccordianReferences: React.FC<AccordianReferencesProps> = ({
onToggle,
onItemToggle,
openedItems,
focusSpan,
createFocusSpanLink,
}) => {
const isEmpty = !Array.isArray(data) || !data.length;
let arrow: React.ReactNode | null = null;
@ -217,7 +222,7 @@ const AccordianReferences: React.FC<AccordianReferencesProps> = ({
<References
data={data}
openedItems={openedItems}
focusSpan={focusSpan}
createFocusSpanLink={createFocusSpanLink}
onItemToggle={onItemToggle}
interactive={interactive}
/>

View File

@ -116,7 +116,6 @@ type SpanDetailProps = {
stackTracesToggle: (spanID: string) => void;
referenceItemToggle: (spanID: string, reference: TraceSpanReference) => void;
referencesToggle: (spanID: string) => void;
focusSpan: (uiFind: string) => void;
createSpanLink?: SpanLinkFunc;
focusedSpanId?: string;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
@ -137,7 +136,6 @@ export default function SpanDetail(props: SpanDetailProps) {
stackTracesToggle,
referencesToggle,
referenceItemToggle,
focusSpan,
createSpanLink,
createFocusSpanLink,
topOfViewRefType,
@ -284,7 +282,7 @@ export default function SpanDetail(props: SpanDetailProps) {
openedItems={referencesState.openedItems}
onToggle={() => referencesToggle(spanID)}
onItemToggle={(reference) => referenceItemToggle(spanID, reference)}
focusSpan={focusSpan}
createFocusSpanLink={createFocusSpanLink}
/>
)}
{topOfViewRefType === TopOfViewRefType.Explore && (

View File

@ -86,7 +86,6 @@ type SpanDetailRowProps = {
span: TraceSpan;
tagsToggle: (spanID: string) => void;
traceStartTime: number;
focusSpan: (uiFind: string) => void;
hoverIndentGuideIds: Set<string>;
addHoverIndentGuideId: (spanID: string) => void;
removeHoverIndentGuideId: (spanID: string) => void;
@ -122,7 +121,6 @@ export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProp
span,
tagsToggle,
traceStartTime,
focusSpan,
hoverIndentGuideIds,
addHoverIndentGuideId,
removeHoverIndentGuideId,
@ -169,7 +167,6 @@ export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProp
span={span}
tagsToggle={tagsToggle}
traceStartTime={traceStartTime}
focusSpan={focusSpan}
createSpanLink={createSpanLink}
focusedSpanId={focusedSpanId}
createFocusSpanLink={createFocusSpanLink}

View File

@ -88,7 +88,6 @@ type TVirtualizedTraceViewOwnProps = {
scrollToFirstVisibleSpan: () => void;
registerAccessors: (accesors: Accessors) => void;
trace: Trace;
focusSpan: (uiFind: string) => void;
linksGetter: (span: TraceSpan, items: TraceKeyValuePair[], itemIndex: number) => TraceLink[];
childrenToggle: (spanID: string) => void;
clearShouldScrollToFirstUiFindMatch: () => void;
@ -479,7 +478,6 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
detailToggle,
spanNameColumnWidth,
trace,
focusSpan,
hoverIndentGuideIds,
addHoverIndentGuideId,
removeHoverIndentGuideId,
@ -514,7 +512,6 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
span={span}
tagsToggle={detailTagsToggle}
traceStartTime={trace.startTime}
focusSpan={focusSpan}
hoverIndentGuideIds={hoverIndentGuideIds}
addHoverIndentGuideId={addHoverIndentGuideId}
removeHoverIndentGuideId={removeHoverIndentGuideId}

View File

@ -53,7 +53,6 @@ describe('<TraceTimelineViewer>', () => {
beforeEach(() => {
wrapper = shallow(<TraceTimelineViewer {...props} />)
.dive()
.dive()
.dive();
});

View File

@ -24,7 +24,6 @@ import { merge as mergeShortcuts } from '../keyboard-shortcuts';
import { SpanLinkFunc, TNil } from '../types';
import TTraceTimeline from '../types/TTraceTimeline';
import { TraceSpan, Trace, TraceLog, TraceKeyValuePair, TraceLink, TraceSpanReference } from '../types/trace';
import ExternalLinkContext from '../url/externalLinkContext';
import TimelineHeaderRow from './TimelineHeaderRow';
import VirtualizedTraceView, { TopOfViewRefType } from './VirtualizedTraceView';
@ -79,8 +78,6 @@ type TProps = TExtractUiFindFromStateReturn & {
updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void;
updateViewRangeTime: TUpdateViewRangeTimeFunction;
viewRange: ViewRange;
focusSpan: (uiFind: string) => void;
createLinkToExternalSpan: (traceID: string, spanID: string) => string;
setSpanNameColumnWidth: (width: number) => void;
collapseAll: (spans: TraceSpan[]) => void;
@ -163,7 +160,6 @@ export class UnthemedTraceTimelineViewer extends React.PureComponent<TProps, Sta
updateNextViewRangeTime,
updateViewRangeTime,
viewRange,
createLinkToExternalSpan,
traceTimeline,
theme,
topOfViewRef,
@ -174,35 +170,33 @@ export class UnthemedTraceTimelineViewer extends React.PureComponent<TProps, Sta
const styles = getStyles(theme);
return (
<ExternalLinkContext.Provider value={createLinkToExternalSpan}>
<div
className={styles.TraceTimelineViewer}
ref={(ref: HTMLDivElement | null) => ref && this.setState({ height: ref.getBoundingClientRect().height })}
>
<TimelineHeaderRow
duration={trace.duration}
nameColumnWidth={traceTimeline.spanNameColumnWidth}
numTicks={NUM_TICKS}
onCollapseAll={this.collapseAll}
onCollapseOne={this.collapseOne}
onColummWidthChange={setSpanNameColumnWidth}
onExpandAll={this.expandAll}
onExpandOne={this.expandOne}
viewRangeTime={viewRange.time}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
columnResizeHandleHeight={this.state.height}
/>
<VirtualizedTraceView
{...rest}
{...traceTimeline}
setSpanNameColumnWidth={setSpanNameColumnWidth}
currentViewRangeTime={viewRange.time.current}
topOfViewRef={topOfViewRef}
focusedSpanIdForSearch={focusedSpanIdForSearch}
/>
</div>
</ExternalLinkContext.Provider>
<div
className={styles.TraceTimelineViewer}
ref={(ref: HTMLDivElement | null) => ref && this.setState({ height: ref.getBoundingClientRect().height })}
>
<TimelineHeaderRow
duration={trace.duration}
nameColumnWidth={traceTimeline.spanNameColumnWidth}
numTicks={NUM_TICKS}
onCollapseAll={this.collapseAll}
onCollapseOne={this.collapseOne}
onColummWidthChange={setSpanNameColumnWidth}
onExpandAll={this.expandAll}
onExpandOne={this.expandOne}
viewRangeTime={viewRange.time}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
columnResizeHandleHeight={this.state.height}
/>
<VirtualizedTraceView
{...rest}
{...traceTimeline}
setSpanNameColumnWidth={setSpanNameColumnWidth}
currentViewRangeTime={viewRange.time.current}
topOfViewRef={topOfViewRef}
focusedSpanIdForSearch={focusedSpanIdForSearch}
/>
</div>
);
}
}

View File

@ -16,62 +16,26 @@ import { shallow, mount } from 'enzyme';
import React from 'react';
import ReferenceLink from './ReferenceLink';
import ExternalLinkContext from './externalLinkContext';
describe(ReferenceLink, () => {
const focusMock = jest.fn();
const createFocusSpanLinkMock = jest.fn((traceId, spanId) => {
return {
href: `${traceId}-${spanId}`,
};
});
const sameTraceRef = {
refType: 'CHILD_OF',
const ref = {
refType: 'FOLLOWS_FROM',
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(<ReferenceLink reference={sameTraceRef} focusSpan={focusMock} />);
it('renders reference with correct href', () => {
const component = shallow(<ReferenceLink reference={ref} createFocusSpanLink={createFocusSpanLinkMock} />);
const link = component.find('a');
expect(link.length).toBe(1);
expect(link.props().role).toBe('button');
});
it('render for external trace', () => {
const component = mount(
<ExternalLinkContext.Provider value={(trace, span) => `${trace}/${span}`}>
<ReferenceLink reference={externalRef} focusSpan={focusMock} />
</ExternalLinkContext.Provider>
);
const link = component.find('a[href="trace2/span2"]');
expect(link.length).toBe(1);
});
it('throws if ExternalLinkContext is not set', () => {
// Prevent writing to stderr during this render.
const err = console.error;
console.error = jest.fn();
expect(() => mount(<ReferenceLink reference={externalRef} focusSpan={focusMock} />)).toThrow(
'ExternalLinkContext'
);
// Restore writing to stderr.
console.error = err;
});
});
describe('focus span', () => {
it('call focusSpan', () => {
focusMock.mockReset();
const component = shallow(<ReferenceLink reference={sameTraceRef} focusSpan={focusMock} />);
const link = component.find('a');
link.simulate('click');
expect(focusMock).toHaveBeenLastCalledWith('span1');
expect(link.prop('href')).toBe('trace1-span1');
});
});
});

View File

@ -14,48 +14,36 @@
import React from 'react';
import { TraceSpanReference } from '../types/trace';
import { Field, LinkModel } from '@grafana/data';
import ExternalLinkContext from './externalLinkContext';
import { TraceSpanReference } from '../types/trace';
type ReferenceLinkProps = {
reference: TraceSpanReference;
children: React.ReactNode;
className?: string;
focusSpan: (spanID: string) => void;
onClick?: () => void;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel<Field>;
};
export default function ReferenceLink(props: ReferenceLinkProps) {
const { reference, children, className, focusSpan, ...otherProps } = props;
delete otherProps.onClick;
if (reference.span) {
return (
<a role="button" onClick={() => focusSpan(reference.spanID)} className={className} {...otherProps}>
{children}
</a>
);
}
const { reference, children, createFocusSpanLink } = props;
const link = createFocusSpanLink(reference.traceID, reference.spanID);
return (
<ExternalLinkContext.Consumer>
{(createLinkToExternalSpan) => {
if (!createLinkToExternalSpan) {
throw new Error("ExternalLinkContext does not have a value, you probably forgot to setup it's provider");
}
return (
<a
href={createLinkToExternalSpan(reference.traceID, reference.spanID)}
target="_blank"
rel="noopener noreferrer"
className={className}
{...otherProps}
>
{children}
</a>
);
}}
</ExternalLinkContext.Consumer>
<a
href={link.href}
target={link.target}
rel="noopener noreferrer"
onClick={
link.onClick
? (event) => {
event.preventDefault();
link.onClick!(event);
}
: undefined
}
>
{children}
</a>
);
}

View File

@ -1,24 +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';
/**
* There are several places where external links to spans are created. The url layout though is something
* that should be decided on the application level and not on the component level but at the same time
* propagating the factory function everywhere could be cumbersome so we use this context for that.
*/
const ExternalLinkContext = React.createContext<((traceID: string, spanID: string) => string) | undefined>(undefined);
ExternalLinkContext.displayName = 'ExternalLinkContext';
export default ExternalLinkContext;

View File

@ -23,6 +23,7 @@ import { TraceToLogsData } from 'app/core/components/TraceToLogs/TraceToLogsSett
import { TraceToMetricsData } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { getTimeZone } from 'app/features/profile/state/selectors';
import { TempoQuery } from 'app/plugins/datasource/tempo/datasource';
import { StoreState } from 'app/types';
import { ExploreId } from 'app/types/explore';
@ -99,13 +100,9 @@ export function TraceView(props: Props) {
refId: props.dataFrames[0]?.refId,
exploreId: props.exploreId!,
datasource,
splitOpenFn: props.splitOpenFn!,
});
const createLinkToExternalSpan = (traceId: string, spanId: string) => {
const link = createFocusSpanLink(traceId, spanId);
return link.href;
};
const traceTimeline: TTraceTimeline = useMemo(
() => ({
childrenHiddenIDs,
@ -162,8 +159,6 @@ export function TraceView(props: Props) {
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
viewRange={viewRange}
focusSpan={noop}
createLinkToExternalSpan={createLinkToExternalSpan}
setSpanNameColumnWidth={setSpanNameColumnWidth}
collapseAll={collapseAll}
collapseOne={collapseOne}
@ -208,6 +203,7 @@ export function TraceView(props: Props) {
*/
function useFocusSpanLink(options: {
exploreId: ExploreId;
splitOpenFn: SplitOpen;
refId?: string;
datasource?: DataSourceApi;
}): [string | undefined, (traceId: string, spanId: string) => LinkModel<Field>] {
@ -234,7 +230,10 @@ function useFocusSpanLink(options: {
internal: {
datasourceUid: options.datasource?.uid!,
datasourceName: options.datasource?.name!,
query: query,
query: {
...query,
query: traceId,
},
panelsState: {
trace: {
spanId,
@ -243,13 +242,34 @@ function useFocusSpanLink(options: {
},
};
// Check if the link is to a different trace or not.
// If it's the same trace, only update panel state with setFocusedSpanId (no navigation).
// If it's a different trace, use splitOpenFn to open a new explore panel
const sameTrace = query?.queryType === 'traceId' && (query as TempoQuery).query === traceId;
return mapInternalLinkToExplore({
link,
internalLink: link.internal!,
scopedVars: {},
range: {} as any,
field: {} as Field,
onClickFn: () => setFocusedSpanId(focusedSpanId === spanId ? undefined : spanId),
onClickFn: sameTrace
? () => setFocusedSpanId(focusedSpanId === spanId ? undefined : spanId)
: options.splitOpenFn
? () =>
options.splitOpenFn({
datasourceUid: options.datasource?.uid!,
query: {
...query!,
query: traceId,
},
panelsState: {
trace: {
spanId,
},
},
})
: undefined,
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
});
};