mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Tracing: Adds header and minimap (#23315)
* Add integration with Jeager Add Jaeger datasource and modify derived fields in loki to allow for opening a trace in Jager in separate split. Modifies build so that this branch docker images are pushed to docker hub Add a traceui dir with docker-compose and provision files for demoing.:wq * Enable docker logger plugin to send logs to loki * Add placeholder zipkin datasource * Fixed rebase issues, added enhanceDataFrame to non-legacy code path * Trace selector for jaeger query field * Fix logs default mode for Loki * Fix loading jaeger query field services on split * Updated grafana image in traceui/compose file * Fix prettier error * Hide behind feature flag, clean up unused code. * Fix tests * Fix tests * Cleanup code and review feedback * Remove traceui directory * Remove circle build changes * Fix feature toggles object * Fix merge issues * Add trace ui in Explore * WIP * WIP * WIP * Make jaeger datasource return trace data instead of link * Allow js in jest tests * Return data from Jaeger datasource * Take yarn.lock from master * Fix missing component * Update yarn lock * Fix some ts and lint errors * Fix merge * Fix type errors * Make tests pass again * Add tests * Fix es5 compatibility * Add header with minimap * Fix sizing issue due to column resizer handle * Fix issues with sizing, search functionality, duplicate react, tests * Refactor TraceView component, fix tests * Fix type errors * Add tests for hooks Co-authored-by: David Kaltschmidt <david.kaltschmidt@gmail.com>
This commit is contained in:
@@ -19,7 +19,7 @@ import { toggleGraph } from './state/actions';
|
||||
import { Provider } from 'react-redux';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { SecondaryActions } from './SecondaryActions';
|
||||
import { TraceView } from './TraceView';
|
||||
import { TraceView } from './TraceView/TraceView';
|
||||
|
||||
const dummyProps: ExploreProps = {
|
||||
changeSize: jest.fn(),
|
||||
|
||||
@@ -59,7 +59,7 @@ import { getTimeZone } from '../profile/state/selectors';
|
||||
import { ErrorContainer } from './ErrorContainer';
|
||||
import { scanStopAction } from './state/actionTypes';
|
||||
import { ExploreGraphPanel } from './ExploreGraphPanel';
|
||||
import { TraceView } from './TraceView';
|
||||
import { TraceView } from './TraceView/TraceView';
|
||||
import { SecondaryActions } from './SecondaryActions';
|
||||
|
||||
const getStyles = stylesFactory(() => {
|
||||
@@ -73,18 +73,6 @@ const getStyles = stylesFactory(() => {
|
||||
button: css`
|
||||
margin: 1em 4px 0 0;
|
||||
`,
|
||||
// Utility class for iframe parents so that we can show iframe content with reasonable height instead of squished
|
||||
// or some random explicit height.
|
||||
fullHeight: css`
|
||||
label: fullHeight;
|
||||
height: 100%;
|
||||
`,
|
||||
iframe: css`
|
||||
label: iframe;
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -330,22 +318,14 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
onClickRichHistoryButton={this.toggleShowRichHistory}
|
||||
/>
|
||||
<ErrorContainer queryError={queryError} />
|
||||
<AutoSizer className={styles.fullHeight} onResize={this.onResize} disableHeight>
|
||||
<AutoSizer onResize={this.onResize} disableHeight>
|
||||
{({ width }) => {
|
||||
if (width === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<main
|
||||
className={cx(
|
||||
styles.logsMain,
|
||||
// We need height to be 100% for tracing iframe to look good but in case of metrics mode
|
||||
// it makes graph and table also full page high when they do not need to be.
|
||||
mode === ExploreMode.Tracing && styles.fullHeight
|
||||
)}
|
||||
style={{ width }}
|
||||
>
|
||||
<main className={cx(styles.logsMain)} style={{ width }}>
|
||||
<ErrorBoundaryAlert>
|
||||
{showStartPage && StartPage && (
|
||||
<div className={'grafana-info-box grafana-info-box--max-lg'}>
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { TraceView } from './TraceView';
|
||||
import { SpanData, TraceData, TraceTimelineViewer } from '@jaegertracing/jaeger-ui-components';
|
||||
|
||||
describe('TraceView', () => {
|
||||
it('renders TraceTimelineViewer', () => {
|
||||
const wrapper = shallow(<TraceView trace={response} />);
|
||||
expect(wrapper.find(TraceTimelineViewer)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('toggles detailState', () => {
|
||||
const wrapper = shallow(<TraceView trace={response} />);
|
||||
let viewer = wrapper.find(TraceTimelineViewer);
|
||||
expect(viewer.props().traceTimeline.detailStates.size).toBe(0);
|
||||
|
||||
viewer.props().detailToggle('1');
|
||||
viewer = wrapper.find(TraceTimelineViewer);
|
||||
expect(viewer.props().traceTimeline.detailStates.size).toBe(1);
|
||||
expect(viewer.props().traceTimeline.detailStates.get('1')).not.toBeUndefined();
|
||||
|
||||
viewer.props().detailToggle('1');
|
||||
viewer = wrapper.find(TraceTimelineViewer);
|
||||
expect(viewer.props().traceTimeline.detailStates.size).toBe(0);
|
||||
});
|
||||
|
||||
it('toggles children visibility', () => {
|
||||
const wrapper = shallow(<TraceView trace={response} />);
|
||||
let viewer = wrapper.find(TraceTimelineViewer);
|
||||
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
|
||||
viewer.props().childrenToggle('1');
|
||||
viewer = wrapper.find(TraceTimelineViewer);
|
||||
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(1);
|
||||
expect(viewer.props().traceTimeline.childrenHiddenIDs.has('1')).toBeTruthy();
|
||||
|
||||
viewer.props().childrenToggle('1');
|
||||
viewer = wrapper.find(TraceTimelineViewer);
|
||||
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
});
|
||||
|
||||
it('toggles adds and removes hover indent guides', () => {
|
||||
const wrapper = shallow(<TraceView trace={response} />);
|
||||
let viewer = wrapper.find(TraceTimelineViewer);
|
||||
expect(viewer.props().traceTimeline.hoverIndentGuideIds.size).toBe(0);
|
||||
|
||||
viewer.props().addHoverIndentGuideId('1');
|
||||
viewer = wrapper.find(TraceTimelineViewer);
|
||||
expect(viewer.props().traceTimeline.hoverIndentGuideIds.size).toBe(1);
|
||||
expect(viewer.props().traceTimeline.hoverIndentGuideIds.has('1')).toBeTruthy();
|
||||
|
||||
viewer.props().removeHoverIndentGuideId('1');
|
||||
viewer = wrapper.find(TraceTimelineViewer);
|
||||
expect(viewer.props().traceTimeline.hoverIndentGuideIds.size).toBe(0);
|
||||
});
|
||||
|
||||
it('toggles collapses and expands one level of spans', () => {
|
||||
const wrapper = shallow(<TraceView trace={response} />);
|
||||
let viewer = wrapper.find(TraceTimelineViewer);
|
||||
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
const spans = viewer.props().trace.spans;
|
||||
|
||||
viewer.props().collapseOne(spans);
|
||||
viewer = wrapper.find(TraceTimelineViewer);
|
||||
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(1);
|
||||
expect(viewer.props().traceTimeline.childrenHiddenIDs.has('3fb050342773d333')).toBeTruthy();
|
||||
|
||||
viewer.props().expandOne(spans);
|
||||
viewer = wrapper.find(TraceTimelineViewer);
|
||||
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
});
|
||||
|
||||
it('toggles collapses and expands all levels', () => {
|
||||
const wrapper = shallow(<TraceView trace={response} />);
|
||||
let viewer = wrapper.find(TraceTimelineViewer);
|
||||
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
const spans = viewer.props().trace.spans;
|
||||
|
||||
viewer.props().collapseAll(spans);
|
||||
viewer = wrapper.find(TraceTimelineViewer);
|
||||
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(2);
|
||||
expect(viewer.props().traceTimeline.childrenHiddenIDs.has('3fb050342773d333')).toBeTruthy();
|
||||
expect(viewer.props().traceTimeline.childrenHiddenIDs.has('1ed38015486087ca')).toBeTruthy();
|
||||
|
||||
viewer.props().expandAll();
|
||||
viewer = wrapper.find(TraceTimelineViewer);
|
||||
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
const response: TraceData & { spans: SpanData[] } = {
|
||||
traceID: '1ed38015486087ca',
|
||||
spans: [
|
||||
{
|
||||
traceID: '1ed38015486087ca',
|
||||
spanID: '1ed38015486087ca',
|
||||
flags: 1,
|
||||
operationName: 'HTTP POST - api_prom_push',
|
||||
references: [] as any,
|
||||
startTime: 1585244579835187,
|
||||
duration: 1098,
|
||||
tags: [
|
||||
{ key: 'sampler.type', type: 'string', value: 'const' },
|
||||
{ key: 'sampler.param', type: 'bool', value: true },
|
||||
{ key: 'span.kind', type: 'string', value: 'server' },
|
||||
{ key: 'http.method', type: 'string', value: 'POST' },
|
||||
{ key: 'http.url', type: 'string', value: '/api/prom/push' },
|
||||
{ key: 'component', type: 'string', value: 'net/http' },
|
||||
{ key: 'http.status_code', type: 'int64', value: 204 },
|
||||
{ key: 'internal.span.format', type: 'string', value: 'proto' },
|
||||
],
|
||||
logs: [
|
||||
{
|
||||
timestamp: 1585244579835229,
|
||||
fields: [{ key: 'event', type: 'string', value: 'util.ParseProtoRequest[start reading]' }],
|
||||
},
|
||||
{
|
||||
timestamp: 1585244579835241,
|
||||
fields: [
|
||||
{ key: 'event', type: 'string', value: 'util.ParseProtoRequest[decompress]' },
|
||||
{ key: 'size', type: 'int64', value: 315 },
|
||||
],
|
||||
},
|
||||
{
|
||||
timestamp: 1585244579835245,
|
||||
fields: [
|
||||
{ key: 'event', type: 'string', value: 'util.ParseProtoRequest[unmarshal]' },
|
||||
{ key: 'size', type: 'int64', value: 446 },
|
||||
],
|
||||
},
|
||||
],
|
||||
processID: 'p1',
|
||||
warnings: null as any,
|
||||
},
|
||||
{
|
||||
traceID: '1ed38015486087ca',
|
||||
spanID: '3fb050342773d333',
|
||||
flags: 1,
|
||||
operationName: '/logproto.Pusher/Push',
|
||||
references: [{ refType: 'CHILD_OF', traceID: '1ed38015486087ca', spanID: '1ed38015486087ca' }],
|
||||
startTime: 1585244579835341,
|
||||
duration: 921,
|
||||
tags: [
|
||||
{ key: 'span.kind', type: 'string', value: 'client' },
|
||||
{ key: 'component', type: 'string', value: 'gRPC' },
|
||||
{ key: 'internal.span.format', type: 'string', value: 'proto' },
|
||||
],
|
||||
logs: [],
|
||||
processID: 'p1',
|
||||
warnings: null,
|
||||
},
|
||||
{
|
||||
traceID: '1ed38015486087ca',
|
||||
spanID: '35118c298fc91f68',
|
||||
flags: 1,
|
||||
operationName: '/logproto.Pusher/Push',
|
||||
references: [{ refType: 'CHILD_OF', traceID: '1ed38015486087ca', spanID: '3fb050342773d333' }],
|
||||
startTime: 1585244579836040,
|
||||
duration: 36,
|
||||
tags: [
|
||||
{ key: 'span.kind', type: 'string', value: 'server' },
|
||||
{ key: 'component', type: 'string', value: 'gRPC' },
|
||||
{ key: 'internal.span.format', type: 'string', value: 'proto' },
|
||||
],
|
||||
logs: [] as any,
|
||||
processID: 'p1',
|
||||
warnings: null as any,
|
||||
},
|
||||
],
|
||||
processes: {
|
||||
p1: {
|
||||
serviceName: 'loki-all',
|
||||
tags: [
|
||||
{ key: 'client-uuid', type: 'string', value: '2a59d08899ef6a8a' },
|
||||
{ key: 'hostname', type: 'string', value: '0080b530fae3' },
|
||||
{ key: 'ip', type: 'string', value: '172.18.0.6' },
|
||||
{ key: 'jaeger.version', type: 'string', value: 'Go-2.20.1' },
|
||||
],
|
||||
},
|
||||
},
|
||||
warnings: null as any,
|
||||
};
|
||||
@@ -1,247 +0,0 @@
|
||||
import {
|
||||
DetailState,
|
||||
KeyValuePair,
|
||||
Link,
|
||||
Log,
|
||||
Span,
|
||||
// SpanData,
|
||||
// SpanReference,
|
||||
Trace,
|
||||
TraceTimelineViewer,
|
||||
TTraceTimeline,
|
||||
UIElementsContext,
|
||||
ViewRangeTimeUpdate,
|
||||
transformTraceData,
|
||||
SpanData,
|
||||
TraceData,
|
||||
} from '@jaegertracing/jaeger-ui-components';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
trace: TraceData & { spans: SpanData[] };
|
||||
};
|
||||
|
||||
export function TraceView(props: Props) {
|
||||
/**
|
||||
* Track whether details are open per span.
|
||||
*/
|
||||
const [detailStates, setDetailStates] = useState(new Map<string, DetailState>());
|
||||
|
||||
/**
|
||||
* Track whether span is collapsed, meaning its children spans are hidden.
|
||||
*/
|
||||
const [childrenHiddenIDs, setChildrenHiddenIDs] = useState(new Set<string>());
|
||||
|
||||
/**
|
||||
* For some reason this is used internally to handle hover state of indent guide. As indent guides are separate
|
||||
* components per each row/span and you need to highlight all in multiple rows to make the effect of single line
|
||||
* they need this kind of common imperative state changes.
|
||||
*
|
||||
* Ideally would be changed to trace view internal state.
|
||||
*/
|
||||
const [hoverIndentGuideIds, setHoverIndentGuideIds] = useState(new Set<string>());
|
||||
|
||||
/**
|
||||
* Keeps state of resizable name column
|
||||
*/
|
||||
const [spanNameColumnWidth, setSpanNameColumnWidth] = useState(0.25);
|
||||
|
||||
function toggleDetail(spanID: string) {
|
||||
const newDetailStates = new Map(detailStates);
|
||||
if (newDetailStates.has(spanID)) {
|
||||
newDetailStates.delete(spanID);
|
||||
} else {
|
||||
newDetailStates.set(spanID, new DetailState());
|
||||
}
|
||||
setDetailStates(newDetailStates);
|
||||
}
|
||||
|
||||
function expandOne(spans: Span[]) {
|
||||
if (childrenHiddenIDs.size === 0) {
|
||||
return;
|
||||
}
|
||||
let prevExpandedDepth = -1;
|
||||
let expandNextHiddenSpan = true;
|
||||
const newChildrenHiddenIDs = spans.reduce((res, s) => {
|
||||
if (s.depth <= prevExpandedDepth) {
|
||||
expandNextHiddenSpan = true;
|
||||
}
|
||||
if (expandNextHiddenSpan && res.has(s.spanID)) {
|
||||
res.delete(s.spanID);
|
||||
expandNextHiddenSpan = false;
|
||||
prevExpandedDepth = s.depth;
|
||||
}
|
||||
return res;
|
||||
}, new Set(childrenHiddenIDs));
|
||||
setChildrenHiddenIDs(newChildrenHiddenIDs);
|
||||
}
|
||||
|
||||
function collapseOne(spans: Span[]) {
|
||||
if (shouldDisableCollapse(spans, childrenHiddenIDs)) {
|
||||
return;
|
||||
}
|
||||
let nearestCollapsedAncestor: Span | undefined;
|
||||
const newChildrenHiddenIDs = spans.reduce((res, curSpan) => {
|
||||
if (nearestCollapsedAncestor && curSpan.depth <= nearestCollapsedAncestor.depth) {
|
||||
res.add(nearestCollapsedAncestor.spanID);
|
||||
if (curSpan.hasChildren) {
|
||||
nearestCollapsedAncestor = curSpan;
|
||||
}
|
||||
} else if (curSpan.hasChildren && !res.has(curSpan.spanID)) {
|
||||
nearestCollapsedAncestor = curSpan;
|
||||
}
|
||||
return res;
|
||||
}, new Set(childrenHiddenIDs));
|
||||
// The last one
|
||||
if (nearestCollapsedAncestor) {
|
||||
newChildrenHiddenIDs.add(nearestCollapsedAncestor.spanID);
|
||||
}
|
||||
setChildrenHiddenIDs(newChildrenHiddenIDs);
|
||||
}
|
||||
|
||||
function expandAll() {
|
||||
setChildrenHiddenIDs(new Set<string>());
|
||||
}
|
||||
|
||||
function collapseAll(spans: Span[]) {
|
||||
if (shouldDisableCollapse(spans, childrenHiddenIDs)) {
|
||||
return;
|
||||
}
|
||||
const newChildrenHiddenIDs = spans.reduce((res, s) => {
|
||||
if (s.hasChildren) {
|
||||
res.add(s.spanID);
|
||||
}
|
||||
return res;
|
||||
}, new Set<string>());
|
||||
|
||||
setChildrenHiddenIDs(newChildrenHiddenIDs);
|
||||
}
|
||||
|
||||
function childrenToggle(spanID: string) {
|
||||
const newChildrenHiddenIDs = new Set(childrenHiddenIDs);
|
||||
if (childrenHiddenIDs.has(spanID)) {
|
||||
newChildrenHiddenIDs.delete(spanID);
|
||||
} else {
|
||||
newChildrenHiddenIDs.add(spanID);
|
||||
}
|
||||
setChildrenHiddenIDs(newChildrenHiddenIDs);
|
||||
}
|
||||
|
||||
function detailLogItemToggle(spanID: string, log: Log) {
|
||||
const old = detailStates.get(spanID);
|
||||
if (!old) {
|
||||
return;
|
||||
}
|
||||
const detailState = old.toggleLogItem(log);
|
||||
const newDetailStates = new Map(detailStates);
|
||||
newDetailStates.set(spanID, detailState);
|
||||
return setDetailStates(newDetailStates);
|
||||
}
|
||||
|
||||
function addHoverIndentGuideId(spanID: string) {
|
||||
setHoverIndentGuideIds(prevState => {
|
||||
const newHoverIndentGuideIds = new Set(prevState);
|
||||
newHoverIndentGuideIds.add(spanID);
|
||||
return newHoverIndentGuideIds;
|
||||
});
|
||||
}
|
||||
|
||||
function removeHoverIndentGuideId(spanID: string) {
|
||||
setHoverIndentGuideIds(prevState => {
|
||||
const newHoverIndentGuideIds = new Set(prevState);
|
||||
newHoverIndentGuideIds.delete(spanID);
|
||||
return newHoverIndentGuideIds;
|
||||
});
|
||||
}
|
||||
|
||||
const traceProp = transformTraceData(props.trace);
|
||||
|
||||
return (
|
||||
<UIElementsContext.Provider
|
||||
value={{
|
||||
Popover: (() => null as any) as any,
|
||||
Tooltip: (() => null as any) as any,
|
||||
Icon: (() => null as any) as any,
|
||||
Dropdown: (() => null as any) as any,
|
||||
Menu: (() => null as any) as any,
|
||||
MenuItem: (() => null as any) as any,
|
||||
Button: (() => null as any) as any,
|
||||
Divider: (() => null as any) as any,
|
||||
}}
|
||||
>
|
||||
<TraceTimelineViewer
|
||||
registerAccessors={() => {}}
|
||||
scrollToFirstVisibleSpan={() => {}}
|
||||
findMatchesIDs={null}
|
||||
trace={traceProp}
|
||||
traceTimeline={
|
||||
{
|
||||
childrenHiddenIDs,
|
||||
detailStates,
|
||||
hoverIndentGuideIds,
|
||||
shouldScrollToFirstUiFindMatch: false,
|
||||
spanNameColumnWidth,
|
||||
traceID: '50b96206cf81dd64',
|
||||
} as TTraceTimeline
|
||||
}
|
||||
updateNextViewRangeTime={(update: ViewRangeTimeUpdate) => {}}
|
||||
updateViewRangeTime={() => {}}
|
||||
viewRange={{ time: { current: [0, 1], cursor: null } }}
|
||||
focusSpan={() => {}}
|
||||
createLinkToExternalSpan={() => ''}
|
||||
setSpanNameColumnWidth={setSpanNameColumnWidth}
|
||||
collapseAll={collapseAll}
|
||||
collapseOne={collapseOne}
|
||||
expandAll={expandAll}
|
||||
expandOne={expandOne}
|
||||
childrenToggle={childrenToggle}
|
||||
clearShouldScrollToFirstUiFindMatch={() => {}}
|
||||
detailLogItemToggle={detailLogItemToggle}
|
||||
detailLogsToggle={makeDetailSubsectionToggle('logs', detailStates, setDetailStates)}
|
||||
detailWarningsToggle={makeDetailSubsectionToggle('warnings', detailStates, setDetailStates)}
|
||||
detailReferencesToggle={makeDetailSubsectionToggle('references', detailStates, setDetailStates)}
|
||||
detailProcessToggle={makeDetailSubsectionToggle('process', detailStates, setDetailStates)}
|
||||
detailTagsToggle={makeDetailSubsectionToggle('tags', detailStates, setDetailStates)}
|
||||
detailToggle={toggleDetail}
|
||||
setTrace={(trace: Trace | null, uiFind: string | null) => {}}
|
||||
addHoverIndentGuideId={addHoverIndentGuideId}
|
||||
removeHoverIndentGuideId={removeHoverIndentGuideId}
|
||||
linksGetter={(span: Span, items: KeyValuePair[], itemIndex: number) => [] as Link[]}
|
||||
uiFind={undefined}
|
||||
/>
|
||||
</UIElementsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function shouldDisableCollapse(allSpans: Span[], hiddenSpansIds: Set<string>) {
|
||||
const allParentSpans = allSpans.filter(s => s.hasChildren);
|
||||
return allParentSpans.length === hiddenSpansIds.size;
|
||||
}
|
||||
|
||||
function makeDetailSubsectionToggle(
|
||||
subSection: 'tags' | 'process' | 'logs' | 'warnings' | 'references',
|
||||
detailStates: Map<string, DetailState>,
|
||||
setDetailStates: (detailStates: Map<string, DetailState>) => void
|
||||
) {
|
||||
return (spanID: string) => {
|
||||
const old = detailStates.get(spanID);
|
||||
if (!old) {
|
||||
return;
|
||||
}
|
||||
let detailState;
|
||||
if (subSection === 'tags') {
|
||||
detailState = old.toggleTags();
|
||||
} else if (subSection === 'process') {
|
||||
detailState = old.toggleProcess();
|
||||
} else if (subSection === 'warnings') {
|
||||
detailState = old.toggleWarnings();
|
||||
} else if (subSection === 'references') {
|
||||
detailState = old.toggleReferences();
|
||||
} else {
|
||||
detailState = old.toggleLogs();
|
||||
}
|
||||
const newDetailStates = new Map(detailStates);
|
||||
newDetailStates.set(spanID, detailState);
|
||||
setDetailStates(newDetailStates);
|
||||
};
|
||||
}
|
||||
216
public/app/features/explore/TraceView/TraceView.test.tsx
Normal file
216
public/app/features/explore/TraceView/TraceView.test.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { TraceView } from './TraceView';
|
||||
import { SpanData, TraceData, TracePageHeader, TraceTimelineViewer } from '@jaegertracing/jaeger-ui-components';
|
||||
|
||||
function renderTraceView() {
|
||||
const wrapper = shallow(<TraceView trace={response} />);
|
||||
return {
|
||||
timeline: wrapper.find(TraceTimelineViewer),
|
||||
header: wrapper.find(TracePageHeader),
|
||||
wrapper,
|
||||
};
|
||||
}
|
||||
|
||||
describe('TraceView', () => {
|
||||
it('renders TraceTimelineViewer', () => {
|
||||
const { timeline, header } = renderTraceView();
|
||||
expect(timeline).toHaveLength(1);
|
||||
expect(header).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('toggles detailState', () => {
|
||||
let { timeline, wrapper } = renderTraceView();
|
||||
expect(timeline.props().traceTimeline.detailStates.size).toBe(0);
|
||||
|
||||
timeline.props().detailToggle('1');
|
||||
timeline = wrapper.find(TraceTimelineViewer);
|
||||
expect(timeline.props().traceTimeline.detailStates.size).toBe(1);
|
||||
expect(timeline.props().traceTimeline.detailStates.get('1')).not.toBeUndefined();
|
||||
|
||||
timeline.props().detailToggle('1');
|
||||
timeline = wrapper.find(TraceTimelineViewer);
|
||||
expect(timeline.props().traceTimeline.detailStates.size).toBe(0);
|
||||
});
|
||||
|
||||
it('toggles children visibility', () => {
|
||||
let { timeline, wrapper } = renderTraceView();
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
|
||||
timeline.props().childrenToggle('1');
|
||||
timeline = wrapper.find(TraceTimelineViewer);
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(1);
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.has('1')).toBeTruthy();
|
||||
|
||||
timeline.props().childrenToggle('1');
|
||||
timeline = wrapper.find(TraceTimelineViewer);
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
});
|
||||
|
||||
it('toggles adds and removes hover indent guides', () => {
|
||||
let { timeline, wrapper } = renderTraceView();
|
||||
expect(timeline.props().traceTimeline.hoverIndentGuideIds.size).toBe(0);
|
||||
|
||||
timeline.props().addHoverIndentGuideId('1');
|
||||
timeline = wrapper.find(TraceTimelineViewer);
|
||||
expect(timeline.props().traceTimeline.hoverIndentGuideIds.size).toBe(1);
|
||||
expect(timeline.props().traceTimeline.hoverIndentGuideIds.has('1')).toBeTruthy();
|
||||
|
||||
timeline.props().removeHoverIndentGuideId('1');
|
||||
timeline = wrapper.find(TraceTimelineViewer);
|
||||
expect(timeline.props().traceTimeline.hoverIndentGuideIds.size).toBe(0);
|
||||
});
|
||||
|
||||
it('toggles collapses and expands one level of spans', () => {
|
||||
let { timeline, wrapper } = renderTraceView();
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
const spans = timeline.props().trace.spans;
|
||||
|
||||
timeline.props().collapseOne(spans);
|
||||
timeline = wrapper.find(TraceTimelineViewer);
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(1);
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.has('3fb050342773d333')).toBeTruthy();
|
||||
|
||||
timeline.props().expandOne(spans);
|
||||
timeline = wrapper.find(TraceTimelineViewer);
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
});
|
||||
|
||||
it('toggles collapses and expands all levels', () => {
|
||||
let { timeline, wrapper } = renderTraceView();
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
const spans = timeline.props().trace.spans;
|
||||
|
||||
timeline.props().collapseAll(spans);
|
||||
timeline = wrapper.find(TraceTimelineViewer);
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(2);
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.has('3fb050342773d333')).toBeTruthy();
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.has('1ed38015486087ca')).toBeTruthy();
|
||||
|
||||
timeline.props().expandAll();
|
||||
timeline = wrapper.find(TraceTimelineViewer);
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
});
|
||||
|
||||
it('searches for spans', () => {
|
||||
let { wrapper, header } = renderTraceView();
|
||||
header.props().onSearchValueChange('HTTP POST - api_prom_push');
|
||||
|
||||
const timeline = wrapper.find(TraceTimelineViewer);
|
||||
expect(timeline.props().findMatchesIDs.has('1ed38015486087ca')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('change viewRange', () => {
|
||||
let { header, timeline, wrapper } = renderTraceView();
|
||||
const defaultRange = { time: { current: [0, 1] } };
|
||||
expect(timeline.props().viewRange).toEqual(defaultRange);
|
||||
expect(header.props().viewRange).toEqual(defaultRange);
|
||||
header.props().updateViewRangeTime(0.2, 0.8);
|
||||
|
||||
let newRange = { time: { current: [0.2, 0.8] } };
|
||||
timeline = wrapper.find(TraceTimelineViewer);
|
||||
header = wrapper.find(TracePageHeader);
|
||||
expect(timeline.props().viewRange).toEqual(newRange);
|
||||
expect(header.props().viewRange).toEqual(newRange);
|
||||
|
||||
newRange = { time: { current: [0.3, 0.7] } };
|
||||
timeline.props().updateViewRangeTime(0.3, 0.7);
|
||||
timeline = wrapper.find(TraceTimelineViewer);
|
||||
header = wrapper.find(TracePageHeader);
|
||||
expect(timeline.props().viewRange).toEqual(newRange);
|
||||
expect(header.props().viewRange).toEqual(newRange);
|
||||
});
|
||||
});
|
||||
|
||||
const response: TraceData & { spans: SpanData[] } = {
|
||||
traceID: '1ed38015486087ca',
|
||||
spans: [
|
||||
{
|
||||
traceID: '1ed38015486087ca',
|
||||
spanID: '1ed38015486087ca',
|
||||
flags: 1,
|
||||
operationName: 'HTTP POST - api_prom_push',
|
||||
references: [] as any,
|
||||
startTime: 1585244579835187,
|
||||
duration: 1098,
|
||||
tags: [
|
||||
{ key: 'sampler.type', type: 'string', value: 'const' },
|
||||
{ key: 'sampler.param', type: 'bool', value: true },
|
||||
{ key: 'span.kind', type: 'string', value: 'server' },
|
||||
{ key: 'http.method', type: 'string', value: 'POST' },
|
||||
{ key: 'http.url', type: 'string', value: '/api/prom/push' },
|
||||
{ key: 'component', type: 'string', value: 'net/http' },
|
||||
{ key: 'http.status_code', type: 'int64', value: 204 },
|
||||
{ key: 'internal.span.format', type: 'string', value: 'proto' },
|
||||
],
|
||||
logs: [
|
||||
{
|
||||
timestamp: 1585244579835229,
|
||||
fields: [{ key: 'event', type: 'string', value: 'util.ParseProtoRequest[start reading]' }],
|
||||
},
|
||||
{
|
||||
timestamp: 1585244579835241,
|
||||
fields: [
|
||||
{ key: 'event', type: 'string', value: 'util.ParseProtoRequest[decompress]' },
|
||||
{ key: 'size', type: 'int64', value: 315 },
|
||||
],
|
||||
},
|
||||
{
|
||||
timestamp: 1585244579835245,
|
||||
fields: [
|
||||
{ key: 'event', type: 'string', value: 'util.ParseProtoRequest[unmarshal]' },
|
||||
{ key: 'size', type: 'int64', value: 446 },
|
||||
],
|
||||
},
|
||||
],
|
||||
processID: 'p1',
|
||||
warnings: null as any,
|
||||
},
|
||||
{
|
||||
traceID: '1ed38015486087ca',
|
||||
spanID: '3fb050342773d333',
|
||||
flags: 1,
|
||||
operationName: '/logproto.Pusher/Push',
|
||||
references: [{ refType: 'CHILD_OF', traceID: '1ed38015486087ca', spanID: '1ed38015486087ca' }],
|
||||
startTime: 1585244579835341,
|
||||
duration: 921,
|
||||
tags: [
|
||||
{ key: 'span.kind', type: 'string', value: 'client' },
|
||||
{ key: 'component', type: 'string', value: 'gRPC' },
|
||||
{ key: 'internal.span.format', type: 'string', value: 'proto' },
|
||||
],
|
||||
logs: [],
|
||||
processID: 'p1',
|
||||
warnings: null,
|
||||
},
|
||||
{
|
||||
traceID: '1ed38015486087ca',
|
||||
spanID: '35118c298fc91f68',
|
||||
flags: 1,
|
||||
operationName: '/logproto.Pusher/Push',
|
||||
references: [{ refType: 'CHILD_OF', traceID: '1ed38015486087ca', spanID: '3fb050342773d333' }],
|
||||
startTime: 1585244579836040,
|
||||
duration: 36,
|
||||
tags: [
|
||||
{ key: 'span.kind', type: 'string', value: 'server' },
|
||||
{ key: 'component', type: 'string', value: 'gRPC' },
|
||||
{ key: 'internal.span.format', type: 'string', value: 'proto' },
|
||||
],
|
||||
logs: [] as any,
|
||||
processID: 'p1',
|
||||
warnings: null as any,
|
||||
},
|
||||
],
|
||||
processes: {
|
||||
p1: {
|
||||
serviceName: 'loki-all',
|
||||
tags: [
|
||||
{ key: 'client-uuid', type: 'string', value: '2a59d08899ef6a8a' },
|
||||
{ key: 'hostname', type: 'string', value: '0080b530fae3' },
|
||||
{ key: 'ip', type: 'string', value: '172.18.0.6' },
|
||||
{ key: 'jaeger.version', type: 'string', value: 'Go-2.20.1' },
|
||||
],
|
||||
},
|
||||
},
|
||||
warnings: null as any,
|
||||
};
|
||||
119
public/app/features/explore/TraceView/TraceView.tsx
Normal file
119
public/app/features/explore/TraceView/TraceView.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
KeyValuePair,
|
||||
Link,
|
||||
Span,
|
||||
Trace,
|
||||
TraceTimelineViewer,
|
||||
TTraceTimeline,
|
||||
UIElementsContext,
|
||||
transformTraceData,
|
||||
SpanData,
|
||||
TraceData,
|
||||
TracePageHeader,
|
||||
} from '@jaegertracing/jaeger-ui-components';
|
||||
import { UIElements } from './uiElements';
|
||||
import { useViewRange } from './useViewRange';
|
||||
import { useSearch } from './useSearch';
|
||||
import { useChildrenState } from './useChildrenState';
|
||||
import { useDetailState } from './useDetailState';
|
||||
import { useHoverIndentGuide } from './useHoverIndentGuide';
|
||||
|
||||
type Props = {
|
||||
trace: TraceData & { spans: SpanData[] };
|
||||
};
|
||||
|
||||
export function TraceView(props: Props) {
|
||||
const { expandOne, collapseOne, childrenToggle, collapseAll, childrenHiddenIDs, expandAll } = useChildrenState();
|
||||
const {
|
||||
detailStates,
|
||||
toggleDetail,
|
||||
detailLogItemToggle,
|
||||
detailLogsToggle,
|
||||
detailProcessToggle,
|
||||
detailReferencesToggle,
|
||||
detailTagsToggle,
|
||||
detailWarningsToggle,
|
||||
} = useDetailState();
|
||||
const { removeHoverIndentGuideId, addHoverIndentGuideId, hoverIndentGuideIds } = useHoverIndentGuide();
|
||||
const { viewRange, updateViewRangeTime, updateNextViewRangeTime } = useViewRange();
|
||||
|
||||
/**
|
||||
* Keeps state of resizable name column width
|
||||
*/
|
||||
const [spanNameColumnWidth, setSpanNameColumnWidth] = useState(0.25);
|
||||
/**
|
||||
* State of the top minimap, slim means it is collapsed.
|
||||
*/
|
||||
const [slim, setSlim] = useState(false);
|
||||
|
||||
const traceProp = transformTraceData(props.trace);
|
||||
const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans);
|
||||
|
||||
return (
|
||||
<UIElementsContext.Provider value={UIElements}>
|
||||
<TracePageHeader
|
||||
canCollapse={true}
|
||||
clearSearch={() => {}}
|
||||
focusUiFindMatches={() => {}}
|
||||
hideMap={false}
|
||||
hideSummary={false}
|
||||
nextResult={() => {}}
|
||||
onSlimViewClicked={() => setSlim(!slim)}
|
||||
onTraceGraphViewClicked={() => {}}
|
||||
prevResult={() => {}}
|
||||
resultCount={0}
|
||||
slimView={slim}
|
||||
textFilter={null}
|
||||
trace={traceProp}
|
||||
traceGraphView={false}
|
||||
updateNextViewRangeTime={updateNextViewRangeTime}
|
||||
updateViewRangeTime={updateViewRangeTime}
|
||||
viewRange={viewRange}
|
||||
searchValue={search}
|
||||
onSearchValueChange={setSearch}
|
||||
hideSearchButtons={true}
|
||||
/>
|
||||
<TraceTimelineViewer
|
||||
registerAccessors={() => {}}
|
||||
scrollToFirstVisibleSpan={() => {}}
|
||||
findMatchesIDs={spanFindMatches}
|
||||
trace={traceProp}
|
||||
traceTimeline={
|
||||
{
|
||||
childrenHiddenIDs,
|
||||
detailStates,
|
||||
hoverIndentGuideIds,
|
||||
shouldScrollToFirstUiFindMatch: false,
|
||||
spanNameColumnWidth,
|
||||
traceID: '50b96206cf81dd64',
|
||||
} as TTraceTimeline
|
||||
}
|
||||
updateNextViewRangeTime={updateNextViewRangeTime}
|
||||
updateViewRangeTime={updateViewRangeTime}
|
||||
viewRange={viewRange}
|
||||
focusSpan={() => {}}
|
||||
createLinkToExternalSpan={() => ''}
|
||||
setSpanNameColumnWidth={setSpanNameColumnWidth}
|
||||
collapseAll={collapseAll}
|
||||
collapseOne={collapseOne}
|
||||
expandAll={expandAll}
|
||||
expandOne={expandOne}
|
||||
childrenToggle={childrenToggle}
|
||||
clearShouldScrollToFirstUiFindMatch={() => {}}
|
||||
detailLogItemToggle={detailLogItemToggle}
|
||||
detailLogsToggle={detailLogsToggle}
|
||||
detailWarningsToggle={detailWarningsToggle}
|
||||
detailReferencesToggle={detailReferencesToggle}
|
||||
detailProcessToggle={detailProcessToggle}
|
||||
detailTagsToggle={detailTagsToggle}
|
||||
detailToggle={toggleDetail}
|
||||
setTrace={(trace: Trace | null, uiFind: string | null) => {}}
|
||||
addHoverIndentGuideId={addHoverIndentGuideId}
|
||||
removeHoverIndentGuideId={removeHoverIndentGuideId}
|
||||
linksGetter={(span: Span, items: KeyValuePair[], itemIndex: number) => [] as Link[]}
|
||||
uiFind={search}
|
||||
/>
|
||||
</UIElementsContext.Provider>
|
||||
);
|
||||
}
|
||||
45
public/app/features/explore/TraceView/uiElements.tsx
Normal file
45
public/app/features/explore/TraceView/uiElements.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { ButtonProps, Elements } from '@jaegertracing/jaeger-ui-components';
|
||||
import { Button, Input } from '@grafana/ui';
|
||||
|
||||
/**
|
||||
* Right now Jaeger components need some UI elements to be injected. This is to get rid of AntD UI library that was
|
||||
* used by default.
|
||||
*/
|
||||
|
||||
// This needs to be static to prevent remounting on every render.
|
||||
export const UIElements: Elements = {
|
||||
Popover: (() => null as any) as any,
|
||||
Tooltip: (() => null as any) as any,
|
||||
Icon: (() => null as any) as any,
|
||||
Dropdown: (() => null as any) as any,
|
||||
Menu: (() => null as any) as any,
|
||||
MenuItem: (() => null as any) as any,
|
||||
Button: ({ onClick, children, className }: ButtonProps) => (
|
||||
<Button variant={'secondary'} onClick={onClick} className={className}>
|
||||
{children}
|
||||
</Button>
|
||||
),
|
||||
Divider,
|
||||
Input: props => <Input {...props} />,
|
||||
InputGroup: ({ children, className, style }) => (
|
||||
<span className={className} style={style}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
};
|
||||
|
||||
function Divider({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
background: '#e8e8e8',
|
||||
width: '1px',
|
||||
height: '0.9em',
|
||||
margin: '0 8px',
|
||||
}}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useChildrenState } from './useChildrenState';
|
||||
import { Span } from '@jaegertracing/jaeger-ui-components';
|
||||
|
||||
describe('useChildrenState', () => {
|
||||
describe('childrenToggle', () => {
|
||||
it('toggles children state', async () => {
|
||||
const { result } = renderHook(() => useChildrenState());
|
||||
expect(result.current.childrenHiddenIDs.size).toBe(0);
|
||||
act(() => result.current.childrenToggle('testId'));
|
||||
|
||||
expect(result.current.childrenHiddenIDs.size).toBe(1);
|
||||
expect(result.current.childrenHiddenIDs.has('testId')).toBe(true);
|
||||
|
||||
act(() => result.current.childrenToggle('testId'));
|
||||
|
||||
expect(result.current.childrenHiddenIDs.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expandAll', () => {
|
||||
it('expands all', async () => {
|
||||
const { result } = renderHook(() => useChildrenState());
|
||||
act(() => result.current.childrenToggle('testId1'));
|
||||
act(() => result.current.childrenToggle('testId2'));
|
||||
|
||||
expect(result.current.childrenHiddenIDs.size).toBe(2);
|
||||
|
||||
act(() => result.current.expandAll());
|
||||
|
||||
expect(result.current.childrenHiddenIDs.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collapseAll', () => {
|
||||
it('hides spans that have children', async () => {
|
||||
const { result } = renderHook(() => useChildrenState());
|
||||
act(() =>
|
||||
result.current.collapseAll([
|
||||
{ spanID: 'span1', hasChildren: true } as Span,
|
||||
{ spanID: 'span2', hasChildren: false } as Span,
|
||||
])
|
||||
);
|
||||
|
||||
expect(result.current.childrenHiddenIDs.size).toBe(1);
|
||||
expect(result.current.childrenHiddenIDs.has('span1')).toBe(true);
|
||||
});
|
||||
|
||||
it('does nothing if already collapsed', async () => {
|
||||
const { result } = renderHook(() => useChildrenState());
|
||||
act(() => result.current.childrenToggle('span1'));
|
||||
act(() =>
|
||||
result.current.collapseAll([
|
||||
{ spanID: 'span1', hasChildren: true } as Span,
|
||||
{ spanID: 'span2', hasChildren: false } as Span,
|
||||
])
|
||||
);
|
||||
|
||||
expect(result.current.childrenHiddenIDs.size).toBe(1);
|
||||
expect(result.current.childrenHiddenIDs.has('span1')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// Other function are not yet used.
|
||||
});
|
||||
94
public/app/features/explore/TraceView/useChildrenState.ts
Normal file
94
public/app/features/explore/TraceView/useChildrenState.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useState } from 'react';
|
||||
import { Span } from '@jaegertracing/jaeger-ui-components';
|
||||
|
||||
/**
|
||||
* Children state means whether spans are collapsed or not. Also provides some functions to manipulate that state.
|
||||
*/
|
||||
export function useChildrenState() {
|
||||
const [childrenHiddenIDs, setChildrenHiddenIDs] = useState(new Set<string>());
|
||||
|
||||
function expandOne(spans: Span[]) {
|
||||
if (childrenHiddenIDs.size === 0) {
|
||||
return;
|
||||
}
|
||||
let prevExpandedDepth = -1;
|
||||
let expandNextHiddenSpan = true;
|
||||
const newChildrenHiddenIDs = spans.reduce((res, s) => {
|
||||
if (s.depth <= prevExpandedDepth) {
|
||||
expandNextHiddenSpan = true;
|
||||
}
|
||||
if (expandNextHiddenSpan && res.has(s.spanID)) {
|
||||
res.delete(s.spanID);
|
||||
expandNextHiddenSpan = false;
|
||||
prevExpandedDepth = s.depth;
|
||||
}
|
||||
return res;
|
||||
}, new Set(childrenHiddenIDs));
|
||||
setChildrenHiddenIDs(newChildrenHiddenIDs);
|
||||
}
|
||||
|
||||
function collapseOne(spans: Span[]) {
|
||||
if (shouldDisableCollapse(spans, childrenHiddenIDs)) {
|
||||
return;
|
||||
}
|
||||
let nearestCollapsedAncestor: Span | undefined;
|
||||
const newChildrenHiddenIDs = spans.reduce((res, curSpan) => {
|
||||
if (nearestCollapsedAncestor && curSpan.depth <= nearestCollapsedAncestor.depth) {
|
||||
res.add(nearestCollapsedAncestor.spanID);
|
||||
if (curSpan.hasChildren) {
|
||||
nearestCollapsedAncestor = curSpan;
|
||||
}
|
||||
} else if (curSpan.hasChildren && !res.has(curSpan.spanID)) {
|
||||
nearestCollapsedAncestor = curSpan;
|
||||
}
|
||||
return res;
|
||||
}, new Set(childrenHiddenIDs));
|
||||
// The last one
|
||||
if (nearestCollapsedAncestor) {
|
||||
newChildrenHiddenIDs.add(nearestCollapsedAncestor.spanID);
|
||||
}
|
||||
setChildrenHiddenIDs(newChildrenHiddenIDs);
|
||||
}
|
||||
|
||||
function expandAll() {
|
||||
setChildrenHiddenIDs(new Set<string>());
|
||||
}
|
||||
|
||||
function collapseAll(spans: Span[]) {
|
||||
if (shouldDisableCollapse(spans, childrenHiddenIDs)) {
|
||||
return;
|
||||
}
|
||||
const newChildrenHiddenIDs = spans.reduce((res, s) => {
|
||||
if (s.hasChildren) {
|
||||
res.add(s.spanID);
|
||||
}
|
||||
return res;
|
||||
}, new Set<string>());
|
||||
|
||||
setChildrenHiddenIDs(newChildrenHiddenIDs);
|
||||
}
|
||||
|
||||
function childrenToggle(spanID: string) {
|
||||
const newChildrenHiddenIDs = new Set(childrenHiddenIDs);
|
||||
if (childrenHiddenIDs.has(spanID)) {
|
||||
newChildrenHiddenIDs.delete(spanID);
|
||||
} else {
|
||||
newChildrenHiddenIDs.add(spanID);
|
||||
}
|
||||
setChildrenHiddenIDs(newChildrenHiddenIDs);
|
||||
}
|
||||
|
||||
return {
|
||||
childrenHiddenIDs,
|
||||
expandOne,
|
||||
collapseOne,
|
||||
expandAll,
|
||||
collapseAll,
|
||||
childrenToggle,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldDisableCollapse(allSpans: Span[], hiddenSpansIds: Set<string>) {
|
||||
const allParentSpans = allSpans.filter(s => s.hasChildren);
|
||||
return allParentSpans.length === hiddenSpansIds.size;
|
||||
}
|
||||
56
public/app/features/explore/TraceView/useDetailState.test.ts
Normal file
56
public/app/features/explore/TraceView/useDetailState.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { Log } from '@jaegertracing/jaeger-ui-components';
|
||||
import { useDetailState } from './useDetailState';
|
||||
|
||||
describe('useDetailState', () => {
|
||||
it('toggles detail', async () => {
|
||||
const { result } = renderHook(() => useDetailState());
|
||||
expect(result.current.detailStates.size).toBe(0);
|
||||
|
||||
act(() => result.current.toggleDetail('span1'));
|
||||
expect(result.current.detailStates.size).toBe(1);
|
||||
expect(result.current.detailStates.has('span1')).toBe(true);
|
||||
|
||||
act(() => result.current.toggleDetail('span1'));
|
||||
expect(result.current.detailStates.size).toBe(0);
|
||||
});
|
||||
|
||||
it('toggles logs and logs items', async () => {
|
||||
const { result } = renderHook(() => useDetailState());
|
||||
act(() => result.current.toggleDetail('span1'));
|
||||
act(() => result.current.detailLogsToggle('span1'));
|
||||
expect(result.current.detailStates.get('span1').logs.isOpen).toBe(true);
|
||||
|
||||
const log = { timestamp: 1 } as Log;
|
||||
act(() => result.current.detailLogItemToggle('span1', log));
|
||||
expect(result.current.detailStates.get('span1').logs.openedItems.has(log)).toBe(true);
|
||||
});
|
||||
|
||||
it('toggles warnings', async () => {
|
||||
const { result } = renderHook(() => useDetailState());
|
||||
act(() => result.current.toggleDetail('span1'));
|
||||
act(() => result.current.detailWarningsToggle('span1'));
|
||||
expect(result.current.detailStates.get('span1').isWarningsOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('toggles references', async () => {
|
||||
const { result } = renderHook(() => useDetailState());
|
||||
act(() => result.current.toggleDetail('span1'));
|
||||
act(() => result.current.detailReferencesToggle('span1'));
|
||||
expect(result.current.detailStates.get('span1').isReferencesOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('toggles processes', async () => {
|
||||
const { result } = renderHook(() => useDetailState());
|
||||
act(() => result.current.toggleDetail('span1'));
|
||||
act(() => result.current.detailProcessToggle('span1'));
|
||||
expect(result.current.detailStates.get('span1').isProcessOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('toggles tags', async () => {
|
||||
const { result } = renderHook(() => useDetailState());
|
||||
act(() => result.current.toggleDetail('span1'));
|
||||
act(() => result.current.detailTagsToggle('span1'));
|
||||
expect(result.current.detailStates.get('span1').isTagsOpen).toBe(true);
|
||||
});
|
||||
});
|
||||
70
public/app/features/explore/TraceView/useDetailState.ts
Normal file
70
public/app/features/explore/TraceView/useDetailState.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useState } from 'react';
|
||||
import { DetailState, Log } from '@jaegertracing/jaeger-ui-components';
|
||||
|
||||
/**
|
||||
* Keeps state of the span detail. This means whether span details are open but also state of each detail subitem
|
||||
* like logs or tags.
|
||||
*/
|
||||
export function useDetailState() {
|
||||
const [detailStates, setDetailStates] = useState(new Map<string, DetailState>());
|
||||
|
||||
function toggleDetail(spanID: string) {
|
||||
const newDetailStates = new Map(detailStates);
|
||||
if (newDetailStates.has(spanID)) {
|
||||
newDetailStates.delete(spanID);
|
||||
} else {
|
||||
newDetailStates.set(spanID, new DetailState());
|
||||
}
|
||||
setDetailStates(newDetailStates);
|
||||
}
|
||||
|
||||
function detailLogItemToggle(spanID: string, log: Log) {
|
||||
const old = detailStates.get(spanID);
|
||||
if (!old) {
|
||||
return;
|
||||
}
|
||||
const detailState = old.toggleLogItem(log);
|
||||
const newDetailStates = new Map(detailStates);
|
||||
newDetailStates.set(spanID, detailState);
|
||||
return setDetailStates(newDetailStates);
|
||||
}
|
||||
|
||||
return {
|
||||
detailStates,
|
||||
toggleDetail,
|
||||
detailLogItemToggle,
|
||||
detailLogsToggle: makeDetailSubsectionToggle('logs', detailStates, setDetailStates),
|
||||
detailWarningsToggle: makeDetailSubsectionToggle('warnings', detailStates, setDetailStates),
|
||||
detailReferencesToggle: makeDetailSubsectionToggle('references', detailStates, setDetailStates),
|
||||
detailProcessToggle: makeDetailSubsectionToggle('process', detailStates, setDetailStates),
|
||||
detailTagsToggle: makeDetailSubsectionToggle('tags', detailStates, setDetailStates),
|
||||
};
|
||||
}
|
||||
|
||||
function makeDetailSubsectionToggle(
|
||||
subSection: 'tags' | 'process' | 'logs' | 'warnings' | 'references',
|
||||
detailStates: Map<string, DetailState>,
|
||||
setDetailStates: (detailStates: Map<string, DetailState>) => void
|
||||
) {
|
||||
return (spanID: string) => {
|
||||
const old = detailStates.get(spanID);
|
||||
if (!old) {
|
||||
return;
|
||||
}
|
||||
let detailState;
|
||||
if (subSection === 'tags') {
|
||||
detailState = old.toggleTags();
|
||||
} else if (subSection === 'process') {
|
||||
detailState = old.toggleProcess();
|
||||
} else if (subSection === 'warnings') {
|
||||
detailState = old.toggleWarnings();
|
||||
} else if (subSection === 'references') {
|
||||
detailState = old.toggleReferences();
|
||||
} else {
|
||||
detailState = old.toggleLogs();
|
||||
}
|
||||
const newDetailStates = new Map(detailStates);
|
||||
newDetailStates.set(spanID, detailState);
|
||||
setDetailStates(newDetailStates);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useHoverIndentGuide } from './useHoverIndentGuide';
|
||||
|
||||
describe('useHoverIndentGuide', () => {
|
||||
it('adds and removes indent guide ids', async () => {
|
||||
const { result } = renderHook(() => useHoverIndentGuide());
|
||||
expect(result.current.hoverIndentGuideIds.size).toBe(0);
|
||||
|
||||
act(() => result.current.addHoverIndentGuideId('span1'));
|
||||
expect(result.current.hoverIndentGuideIds.size).toBe(1);
|
||||
expect(result.current.hoverIndentGuideIds.has('span1')).toBe(true);
|
||||
|
||||
act(() => result.current.removeHoverIndentGuideId('span1'));
|
||||
expect(result.current.hoverIndentGuideIds.size).toBe(0);
|
||||
});
|
||||
});
|
||||
30
public/app/features/explore/TraceView/useHoverIndentGuide.ts
Normal file
30
public/app/features/explore/TraceView/useHoverIndentGuide.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
/**
|
||||
* This is used internally to handle hover state of indent guide. As indent guides are separate
|
||||
* components per each row/span and you need to highlight all in multiple rows to make the effect of single line
|
||||
* they need this kind of common imperative state changes.
|
||||
*
|
||||
* Ideally would be changed to trace view internal state.
|
||||
*/
|
||||
export function useHoverIndentGuide() {
|
||||
const [hoverIndentGuideIds, setHoverIndentGuideIds] = useState(new Set<string>());
|
||||
|
||||
function addHoverIndentGuideId(spanID: string) {
|
||||
setHoverIndentGuideIds(prevState => {
|
||||
const newHoverIndentGuideIds = new Set(prevState);
|
||||
newHoverIndentGuideIds.add(spanID);
|
||||
return newHoverIndentGuideIds;
|
||||
});
|
||||
}
|
||||
|
||||
function removeHoverIndentGuideId(spanID: string) {
|
||||
setHoverIndentGuideIds(prevState => {
|
||||
const newHoverIndentGuideIds = new Set(prevState);
|
||||
newHoverIndentGuideIds.delete(spanID);
|
||||
return newHoverIndentGuideIds;
|
||||
});
|
||||
}
|
||||
|
||||
return { hoverIndentGuideIds, addHoverIndentGuideId, removeHoverIndentGuideId };
|
||||
}
|
||||
44
public/app/features/explore/TraceView/useSearch.test.ts
Normal file
44
public/app/features/explore/TraceView/useSearch.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useSearch } from './useSearch';
|
||||
import { Span } from '@jaegertracing/jaeger-ui-components';
|
||||
|
||||
describe('useSearch', () => {
|
||||
it('returns matching span IDs', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSearch([
|
||||
{
|
||||
spanID: 'span1',
|
||||
operationName: 'operation1',
|
||||
process: {
|
||||
serviceName: 'service1',
|
||||
tags: [],
|
||||
},
|
||||
tags: [],
|
||||
logs: [],
|
||||
} as Span,
|
||||
|
||||
{
|
||||
spanID: 'span2',
|
||||
operationName: 'operation2',
|
||||
process: {
|
||||
serviceName: 'service2',
|
||||
tags: [],
|
||||
},
|
||||
tags: [],
|
||||
logs: [],
|
||||
} as Span,
|
||||
])
|
||||
);
|
||||
|
||||
act(() => result.current.setSearch('service1'));
|
||||
expect(result.current.spanFindMatches.size).toBe(1);
|
||||
expect(result.current.spanFindMatches.has('span1')).toBe(true);
|
||||
});
|
||||
|
||||
it('works without spans', async () => {
|
||||
const { result } = renderHook(() => useSearch());
|
||||
|
||||
act(() => result.current.setSearch('service1'));
|
||||
expect(result.current.spanFindMatches).toBe(undefined);
|
||||
});
|
||||
});
|
||||
15
public/app/features/explore/TraceView/useSearch.ts
Normal file
15
public/app/features/explore/TraceView/useSearch.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useState } from 'react';
|
||||
import { Span, filterSpans } from '@jaegertracing/jaeger-ui-components';
|
||||
|
||||
/**
|
||||
* Controls the state of search input that highlights spans if they match the search string.
|
||||
* @param spans
|
||||
*/
|
||||
export function useSearch(spans?: Span[]) {
|
||||
const [search, setSearch] = useState('');
|
||||
let spanFindMatches: Set<string> | undefined;
|
||||
if (search && spans) {
|
||||
spanFindMatches = filterSpans(search, spans);
|
||||
}
|
||||
return { search, setSearch, spanFindMatches };
|
||||
}
|
||||
25
public/app/features/explore/TraceView/useViewRange.test.ts
Normal file
25
public/app/features/explore/TraceView/useViewRange.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useViewRange } from './useViewRange';
|
||||
|
||||
describe('useViewRange', () => {
|
||||
it('defaults to full range', async () => {
|
||||
const { result } = renderHook(() => useViewRange());
|
||||
expect(result.current.viewRange).toEqual({ time: { current: [0, 1] } });
|
||||
});
|
||||
|
||||
describe('updateNextViewRangeTime', () => {
|
||||
it('updates time', async () => {
|
||||
const { result } = renderHook(() => useViewRange());
|
||||
act(() => result.current.updateNextViewRangeTime({ cursor: 0.5 }));
|
||||
expect(result.current.viewRange).toEqual({ time: { current: [0, 1], cursor: 0.5 } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateViewRangeTime', () => {
|
||||
it('updates time', async () => {
|
||||
const { result } = renderHook(() => useViewRange());
|
||||
act(() => result.current.updateViewRangeTime(0.1, 0.2));
|
||||
expect(result.current.viewRange).toEqual({ time: { current: [0.1, 0.2] } });
|
||||
});
|
||||
});
|
||||
});
|
||||
35
public/app/features/explore/TraceView/useViewRange.ts
Normal file
35
public/app/features/explore/TraceView/useViewRange.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useState } from 'react';
|
||||
import { ViewRangeTimeUpdate, ViewRange } from '@jaegertracing/jaeger-ui-components';
|
||||
|
||||
/**
|
||||
* Controls state of the zoom function that can be used through minimap in header or on the timeline. ViewRange contains
|
||||
* state not only for current range that is showing but range that is currently being selected by the user.
|
||||
*/
|
||||
export function useViewRange() {
|
||||
const [viewRange, setViewRange] = useState<ViewRange>({
|
||||
time: {
|
||||
current: [0, 1],
|
||||
},
|
||||
});
|
||||
|
||||
function updateNextViewRangeTime(update: ViewRangeTimeUpdate) {
|
||||
setViewRange(
|
||||
(prevRange): ViewRange => {
|
||||
const time = { ...prevRange.time, ...update };
|
||||
return { ...prevRange, time };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function updateViewRangeTime(start: number, end: number) {
|
||||
const current: [number, number] = [start, end];
|
||||
const time = { current };
|
||||
setViewRange(
|
||||
(prevRange): ViewRange => {
|
||||
return { ...prevRange, time };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return { viewRange, updateViewRangeTime, updateNextViewRangeTime };
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export class Wrapper extends Component<WrapperProps> {
|
||||
return (
|
||||
<div className="page-scrollbar-wrapper">
|
||||
<CustomScrollbar autoHeightMin={'100%'} autoHeightMax={''} className="custom-scrollbar--page">
|
||||
<div style={{ height: '100%' }} className="explore-wrapper">
|
||||
<div className="explore-wrapper">
|
||||
<ErrorBoundaryAlert style="page">
|
||||
<Explore exploreId={ExploreId.left} />
|
||||
</ErrorBoundaryAlert>
|
||||
|
||||
Reference in New Issue
Block a user