Explore: Add trace UI to show traces from tracing datasources (#23047)

* 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

Co-authored-by: David Kaltschmidt <david.kaltschmidt@gmail.com>
This commit is contained in:
Andrej Ocenas
2020-04-02 13:34:16 +02:00
committed by GitHub
parent a40c258544
commit a4d4dd325f
149 changed files with 16275 additions and 193 deletions

View File

@@ -7,54 +7,108 @@ import {
DataQueryError,
DataQueryRequest,
CoreApp,
MutableDataFrame,
} from '@grafana/data';
import { getFirstNonQueryRowSpecificError } from 'app/core/utils/explore';
import { ExploreId } from 'app/types/explore';
import { shallow } from 'enzyme';
import AutoSizer from 'react-virtualized-auto-sizer';
import { Explore, ExploreProps } from './Explore';
import { scanStopAction } from './state/actionTypes';
import { toggleGraph } from './state/actions';
import { Provider } from 'react-redux';
import { configureStore } from 'app/store/configureStore';
import { SecondaryActions } from './SecondaryActions';
import { TraceView } from './TraceView';
const setup = (renderMethod: any, propOverrides?: object) => {
const props: ExploreProps = {
changeSize: jest.fn(),
datasourceInstance: {
meta: {
metrics: true,
logs: true,
},
components: {
ExploreStartPage: {},
},
} as DataSourceApi,
datasourceMissing: false,
exploreId: ExploreId.left,
initializeExplore: jest.fn(),
initialized: true,
modifyQueries: jest.fn(),
update: {
datasource: false,
queries: false,
range: false,
mode: false,
ui: false,
const dummyProps: ExploreProps = {
changeSize: jest.fn(),
datasourceInstance: {
meta: {
metrics: true,
logs: true,
},
refreshExplore: jest.fn(),
scanning: false,
scanRange: {
from: '0',
to: '0',
components: {
ExploreStartPage: {},
},
scanStart: jest.fn(),
scanStopAction: scanStopAction,
setQueries: jest.fn(),
split: false,
queryKeys: [],
initialDatasource: 'test',
initialQueries: [],
initialRange: {
} as DataSourceApi,
datasourceMissing: false,
exploreId: ExploreId.left,
initializeExplore: jest.fn(),
initialized: true,
modifyQueries: jest.fn(),
update: {
datasource: false,
queries: false,
range: false,
mode: false,
ui: false,
},
refreshExplore: jest.fn(),
scanning: false,
scanRange: {
from: '0',
to: '0',
},
scanStart: jest.fn(),
scanStopAction: scanStopAction,
setQueries: jest.fn(),
split: false,
queryKeys: [],
initialDatasource: 'test',
initialQueries: [],
initialRange: {
from: toUtc('2019-01-01 10:00:00'),
to: toUtc('2019-01-01 16:00:00'),
raw: {
from: 'now-6h',
to: 'now',
},
},
mode: ExploreMode.Metrics,
initialUI: {
showingTable: false,
showingGraph: false,
showingLogs: false,
},
isLive: false,
syncedTimes: false,
updateTimeRange: jest.fn(),
graphResult: [],
loading: false,
absoluteRange: {
from: 0,
to: 0,
},
showingGraph: false,
showingTable: false,
timeZone: 'UTC',
onHiddenSeriesChanged: jest.fn(),
toggleGraph: toggleGraph,
queryResponse: {
state: LoadingState.NotStarted,
series: [],
request: ({
requestId: '1',
dashboardId: 0,
interval: '1s',
panelId: 1,
scopedVars: {
apps: {
value: 'value',
},
},
targets: [
{
refId: 'A',
},
],
timezone: 'UTC',
app: CoreApp.Explore,
startTime: 0,
} as unknown) as DataQueryRequest,
error: {} as DataQueryError,
timeRange: {
from: toUtc('2019-01-01 10:00:00'),
to: toUtc('2019-01-01 16:00:00'),
raw: {
@@ -62,68 +116,21 @@ const setup = (renderMethod: any, propOverrides?: object) => {
to: 'now',
},
},
mode: ExploreMode.Metrics,
initialUI: {
showingTable: false,
showingGraph: false,
showingLogs: false,
},
isLive: false,
syncedTimes: false,
updateTimeRange: jest.fn(),
graphResult: [],
loading: false,
absoluteRange: {
from: 0,
to: 0,
},
showingGraph: false,
showingTable: false,
timeZone: 'UTC',
onHiddenSeriesChanged: jest.fn(),
toggleGraph: toggleGraph,
queryResponse: {
state: LoadingState.NotStarted,
series: [],
request: ({
requestId: '1',
dashboardId: 0,
interval: '1s',
panelId: 1,
scopedVars: {
apps: {
value: 'value',
},
},
targets: [
{
refId: 'A',
},
],
timezone: 'UTC',
app: CoreApp.Explore,
startTime: 0,
} as unknown) as DataQueryRequest,
error: {} as DataQueryError,
timeRange: {
from: toUtc('2019-01-01 10:00:00'),
to: toUtc('2019-01-01 16:00:00'),
raw: {
from: 'now-6h',
to: 'now',
},
},
},
originPanelId: 1,
addQueryRow: jest.fn(),
};
},
originPanelId: 1,
addQueryRow: jest.fn(),
};
const setup = (renderMethod: any, propOverrides?: Partial<ExploreProps>) => {
const store = configureStore();
Object.assign(props, propOverrides);
return renderMethod(
<Provider store={store}>
<Explore {...props} />
<Explore
{...{
...dummyProps,
...propOverrides,
}}
/>
</Provider>
);
};
@@ -145,6 +152,40 @@ describe('Explore', () => {
expect(wrapper).toMatchSnapshot();
});
it('renders SecondaryActions and add row button', () => {
const wrapper = shallow(<Explore {...dummyProps} />);
expect(wrapper.find(SecondaryActions)).toHaveLength(1);
expect(wrapper.find(SecondaryActions).props().addQueryRowButtonHidden).toBe(false);
});
it('does not show add row button if mode is tracing', () => {
const wrapper = shallow(<Explore {...{ ...dummyProps, mode: ExploreMode.Tracing }} />);
expect(wrapper.find(SecondaryActions).props().addQueryRowButtonHidden).toBe(true);
});
it('renders TraceView if tracing mode', () => {
const wrapper = shallow(
<Explore
{...{
...dummyProps,
mode: ExploreMode.Tracing,
queryResponse: {
...dummyProps.queryResponse,
state: LoadingState.Done,
series: [new MutableDataFrame({ fields: [{ name: 'trace', values: [{}] }] })],
},
}}
/>
);
const autoSizer = shallow(
wrapper
.find(AutoSizer)
.props()
.children({ width: 100, height: 100 }) as React.ReactElement
);
expect(autoSizer.find(TraceView).length).toBe(1);
});
it('should filter out a query-row-specific error when looking for non-query-row-specific errors', async () => {
const queryErrors = setupErrors(true);
const queryError = getFirstNonQueryRowSpecificError(queryErrors);

View File

@@ -59,6 +59,8 @@ import { getTimeZone } from '../profile/state/selectors';
import { ErrorContainer } from './ErrorContainer';
import { scanStopAction } from './state/actionTypes';
import { ExploreGraphPanel } from './ExploreGraphPanel';
import { TraceView } from './TraceView';
import { SecondaryActions } from './SecondaryActions';
const getStyles = stylesFactory(() => {
return {
@@ -319,27 +321,14 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
{datasourceInstance && (
<div className="explore-container">
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
<div className="gf-form">
<button
aria-label="Add row button"
className={`gf-form-label gf-form-label--btn ${styles.button}`}
onClick={this.onClickAddQueryRowButton}
disabled={isLive}
>
<i className={'fa fa-fw fa-plus icon-margin-right'} />
<span className="btn-title">{'\xA0' + 'Add query'}</span>
</button>
<button
aria-label="Rich history button"
className={cx(`gf-form-label gf-form-label--btn ${styles.button}`, {
['explore-active-button']: showRichHistory,
})}
onClick={this.toggleShowRichHistory}
>
<i className={'fa fa-fw fa-history icon-margin-right '} />
<span className="btn-title">{'\xA0' + 'Query history'}</span>
</button>
</div>
<SecondaryActions
addQueryRowButtonDisabled={isLive}
// We cannot show multiple traces at the same time right now so we do not show add query button.
addQueryRowButtonHidden={mode === ExploreMode.Tracing}
richHistoryButtonActive={showRichHistory}
onClickAddQueryRowButton={this.onClickAddQueryRowButton}
onClickRichHistoryButton={this.toggleShowRichHistory}
/>
<ErrorContainer queryError={queryError} />
<AutoSizer className={styles.fullHeight} onResize={this.onResize} disableHeight>
{({ width }) => {
@@ -400,18 +389,12 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
onStopScanning={this.onStopScanning}
/>
)}
{mode === ExploreMode.Tracing && (
<div className={styles.fullHeight}>
{queryResponse &&
!!queryResponse.series.length &&
queryResponse.series[0].fields[0].values.get(0) && (
<iframe
className={styles.iframe}
src={queryResponse.series[0].fields[0].values.get(0)}
/>
)}
</div>
)}
{mode === ExploreMode.Tracing &&
// We expect only one trace at the moment to be in the dataframe
// If there is not data (like 404) we show a separate error so no need to show anything here
queryResponse.series[0] && (
<TraceView trace={queryResponse.series[0].fields[0].values.get(0) as any} />
)}
</>
)}
{showRichHistory && (

View File

@@ -0,0 +1,51 @@
import React from 'react';
import { noop } from 'lodash';
import { shallow } from 'enzyme';
import { SecondaryActions } from './SecondaryActions';
const addQueryRowButtonSelector = '[aria-label="Add row button"]';
const richHistoryButtonSelector = '[aria-label="Rich history button"]';
describe('SecondaryActions', () => {
it('should render component two buttons', () => {
const wrapper = shallow(<SecondaryActions onClickAddQueryRowButton={noop} onClickRichHistoryButton={noop} />);
expect(wrapper.find(addQueryRowButtonSelector)).toHaveLength(1);
expect(wrapper.find(richHistoryButtonSelector)).toHaveLength(1);
});
it('should not render add row button if addQueryRowButtonHidden=true', () => {
const wrapper = shallow(
<SecondaryActions
addQueryRowButtonHidden={true}
onClickAddQueryRowButton={noop}
onClickRichHistoryButton={noop}
/>
);
expect(wrapper.find(addQueryRowButtonSelector)).toHaveLength(0);
expect(wrapper.find(richHistoryButtonSelector)).toHaveLength(1);
});
it('should disable add row button if addQueryRowButtonDisabled=true', () => {
const wrapper = shallow(
<SecondaryActions
addQueryRowButtonDisabled={true}
onClickAddQueryRowButton={noop}
onClickRichHistoryButton={noop}
/>
);
expect(wrapper.find(addQueryRowButtonSelector).props().disabled).toBe(true);
});
it('should map click handlers correctly', () => {
const onClickAddRow = jest.fn();
const onClickHistory = jest.fn();
const wrapper = shallow(
<SecondaryActions onClickAddQueryRowButton={onClickAddRow} onClickRichHistoryButton={onClickHistory} />
);
wrapper.find(addQueryRowButtonSelector).simulate('click');
expect(onClickAddRow).toBeCalled();
wrapper.find(richHistoryButtonSelector).simulate('click');
expect(onClickHistory).toBeCalled();
});
});

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { css, cx } from 'emotion';
import { stylesFactory } from '@grafana/ui';
type Props = {
addQueryRowButtonDisabled?: boolean;
richHistoryButtonActive?: boolean;
addQueryRowButtonHidden?: boolean;
onClickAddQueryRowButton: () => void;
onClickRichHistoryButton: () => void;
};
const getStyles = stylesFactory(() => {
return {
button: css`
margin: 1em 4px 0 0;
`,
};
});
export function SecondaryActions(props: Props) {
const styles = getStyles();
return (
<div className="gf-form">
{!props.addQueryRowButtonHidden && (
<button
aria-label="Add row button"
className={`gf-form-label gf-form-label--btn ${styles.button}`}
onClick={props.onClickAddQueryRowButton}
disabled={props.addQueryRowButtonDisabled}
>
<i className={'fa fa-fw fa-plus icon-margin-right'} />
<span className="btn-title">{'\xA0' + 'Add query'}</span>
</button>
)}
<button
aria-label="Rich history button"
className={cx(`gf-form-label gf-form-label--btn ${styles.button}`, {
['explore-active-button']: props.richHistoryButtonActive,
})}
onClick={props.onClickRichHistoryButton}
>
<i className={'fa fa-fw fa-history icon-margin-right '} />
<span className="btn-title">{'\xA0' + 'Query history'}</span>
</button>
</div>
);
}

View File

@@ -0,0 +1,182 @@
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,
};

View File

@@ -0,0 +1,247 @@
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);
};
}