Trace View: Add link from the Trace View to the Profiles Drilldown (#101422)
* Add link from the Trace View to the Profiles App via Profiles for this span button dropdown * Fix tests * Lint * Fix test * Lint * Fallback if plugin extension does not exist * Move span detail link buttons to their own file * Add tests * Update type name * Update naming * Prettier * Move button * Remove fallback * Use proper time range * Betterer * Update test * Use CoreApp * Betterer * Simplify context * Update tests --------- Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
This commit is contained in:
@@ -185,6 +185,7 @@ export enum PluginExtensionPoints {
|
|||||||
DataSourceConfig = 'grafana/datasources/config',
|
DataSourceConfig = 'grafana/datasources/config',
|
||||||
ExploreToolbarAction = 'grafana/explore/toolbar/action',
|
ExploreToolbarAction = 'grafana/explore/toolbar/action',
|
||||||
UserProfileTab = 'grafana/user/profile/tab',
|
UserProfileTab = 'grafana/user/profile/tab',
|
||||||
|
TraceViewDetails = 'grafana/traceview/details',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PluginExtensionPanelContext = {
|
export type PluginExtensionPanelContext = {
|
||||||
|
|||||||
@@ -514,6 +514,7 @@ export class Explore extends PureComponent<Props, ExploreState> {
|
|||||||
dataFrames={dataFrames}
|
dataFrames={dataFrames}
|
||||||
splitOpenFn={this.onSplitOpen('traceView')}
|
splitOpenFn={this.onSplitOpen('traceView')}
|
||||||
scrollElement={this.scrollElement}
|
scrollElement={this.scrollElement}
|
||||||
|
timeRange={queryResponse.timeRange}
|
||||||
/>
|
/>
|
||||||
</ContentOutlineItem>
|
</ContentOutlineItem>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import userEvent from '@testing-library/user-event';
|
|||||||
import { createRef } from 'react';
|
import { createRef } from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import { DataFrame, MutableDataFrame } from '@grafana/data';
|
import { DataFrame, MutableDataFrame, TimeRange } from '@grafana/data';
|
||||||
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
import { DataSourceSrv, setDataSourceSrv, setPluginLinksHook } from '@grafana/runtime';
|
||||||
|
|
||||||
import { configureStore } from '../../../store/configureStore';
|
import { configureStore } from '../../../store/configureStore';
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ function getTraceView(frames: DataFrame[]) {
|
|||||||
traceProp={transformDataFrames(frames[0])!}
|
traceProp={transformDataFrames(frames[0])!}
|
||||||
datasource={undefined}
|
datasource={undefined}
|
||||||
topOfViewRef={topOfViewRef}
|
topOfViewRef={topOfViewRef}
|
||||||
|
timeRange={{} as TimeRange}
|
||||||
/>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
@@ -47,6 +48,11 @@ function renderTraceViewNew() {
|
|||||||
|
|
||||||
describe('TraceView', () => {
|
describe('TraceView', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
setPluginLinksHook(() => ({
|
||||||
|
isLoading: false,
|
||||||
|
links: [],
|
||||||
|
}));
|
||||||
|
|
||||||
setDataSourceSrv({
|
setDataSourceSrv({
|
||||||
getInstanceSettings() {
|
getInstanceSettings() {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
LinkModel,
|
LinkModel,
|
||||||
mapInternalLinkToExplore,
|
mapInternalLinkToExplore,
|
||||||
SplitOpen,
|
SplitOpen,
|
||||||
|
TimeRange,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getTraceToLogsOptions, TraceToMetricsData, TraceToProfilesData } from '@grafana/o11y-ds-frontend';
|
import { getTraceToLogsOptions, TraceToMetricsData, TraceToProfilesData } from '@grafana/o11y-ds-frontend';
|
||||||
import { getTemplateSrv } from '@grafana/runtime';
|
import { getTemplateSrv } from '@grafana/runtime';
|
||||||
@@ -67,6 +68,7 @@ type Props = {
|
|||||||
focusedSpanId?: string;
|
focusedSpanId?: string;
|
||||||
createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>;
|
createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>;
|
||||||
spanFilters?: SearchProps;
|
spanFilters?: SearchProps;
|
||||||
|
timeRange: TimeRange;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TraceView(props: Props) {
|
export function TraceView(props: Props) {
|
||||||
@@ -238,6 +240,8 @@ export function TraceView(props: Props) {
|
|||||||
setTraceFlameGraphs={setTraceFlameGraphs}
|
setTraceFlameGraphs={setTraceFlameGraphs}
|
||||||
redrawListView={redrawListView}
|
redrawListView={redrawListView}
|
||||||
setRedrawListView={setRedrawListView}
|
setRedrawListView={setRedrawListView}
|
||||||
|
timeRange={props.timeRange}
|
||||||
|
app={exploreId ? CoreApp.Explore : CoreApp.Unknown}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { TimeRange } from '@grafana/data';
|
||||||
|
|
||||||
import { configureStore } from '../../../store/configureStore';
|
import { configureStore } from '../../../store/configureStore';
|
||||||
|
|
||||||
import { frameOld } from './TraceView.test';
|
import { frameOld } from './TraceView.test';
|
||||||
@@ -11,6 +13,7 @@ jest.mock('@grafana/runtime', () => {
|
|||||||
return {
|
return {
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
reportInteraction: jest.fn(),
|
reportInteraction: jest.fn(),
|
||||||
|
usePluginLinks: jest.fn().mockReturnValue({ isLoading: false, links: [] }),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -19,7 +22,7 @@ function renderTraceViewContainer(frames = [frameOld]) {
|
|||||||
|
|
||||||
const { container, baseElement } = render(
|
const { container, baseElement } = render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<TraceViewContainer exploreId="left" dataFrames={frames} splitOpenFn={() => {}} />
|
<TraceViewContainer exploreId="left" dataFrames={frames} splitOpenFn={() => {}} timeRange={{} as TimeRange} />
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { DataFrame, SplitOpen } from '@grafana/data';
|
import { DataFrame, SplitOpen, TimeRange } from '@grafana/data';
|
||||||
import { PanelChrome } from '@grafana/ui';
|
import { PanelChrome } from '@grafana/ui';
|
||||||
import { StoreState, useSelector } from 'app/types';
|
import { StoreState, useSelector } from 'app/types';
|
||||||
|
|
||||||
@@ -12,12 +12,13 @@ interface Props {
|
|||||||
splitOpenFn: SplitOpen;
|
splitOpenFn: SplitOpen;
|
||||||
exploreId: string;
|
exploreId: string;
|
||||||
scrollElement?: Element;
|
scrollElement?: Element;
|
||||||
|
timeRange: TimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TraceViewContainer(props: Props) {
|
export function TraceViewContainer(props: Props) {
|
||||||
// At this point we only show single trace
|
// At this point we only show single trace
|
||||||
const frame = props.dataFrames[0];
|
const frame = props.dataFrames[0];
|
||||||
const { dataFrames, splitOpenFn, exploreId, scrollElement } = props;
|
const { dataFrames, splitOpenFn, exploreId, scrollElement, timeRange } = props;
|
||||||
const traceProp = useMemo(() => transformDataFrames(frame), [frame]);
|
const traceProp = useMemo(() => transformDataFrames(frame), [frame]);
|
||||||
const datasource = useSelector(
|
const datasource = useSelector(
|
||||||
(state: StoreState) => state.explore.panes[props.exploreId]?.datasourceInstance ?? undefined
|
(state: StoreState) => state.explore.panes[props.exploreId]?.datasourceInstance ?? undefined
|
||||||
@@ -36,6 +37,7 @@ export function TraceViewContainer(props: Props) {
|
|||||||
scrollElement={scrollElement}
|
scrollElement={scrollElement}
|
||||||
traceProp={traceProp}
|
traceProp={traceProp}
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
|
timeRange={timeRange}
|
||||||
/>
|
/>
|
||||||
</PanelChrome>
|
</PanelChrome>
|
||||||
);
|
);
|
||||||
|
|||||||
+230
@@ -0,0 +1,230 @@
|
|||||||
|
import { CoreApp, TimeRange } from '@grafana/data';
|
||||||
|
import { usePluginLinks } from '@grafana/runtime';
|
||||||
|
import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer';
|
||||||
|
|
||||||
|
import { SpanLinkType } from '../../types/links';
|
||||||
|
import { TraceSpan } from '../../types/trace';
|
||||||
|
|
||||||
|
import { getSpanDetailLinkButtons, getProfileLinkButtonsContext } from './SpanDetailLinkButtons';
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...jest.requireActual('@grafana/runtime'),
|
||||||
|
usePluginLinks: jest.fn().mockReturnValue({ isLoading: false, links: [] }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const span = {
|
||||||
|
process: {
|
||||||
|
serviceName: 'test-service',
|
||||||
|
},
|
||||||
|
tags: [{ key: 'pyroscope.profile.id', value: 'test-profile' }],
|
||||||
|
} as TraceSpan;
|
||||||
|
|
||||||
|
const createSpanLink = jest.fn();
|
||||||
|
const timeRange = {
|
||||||
|
from: new Date(0),
|
||||||
|
to: new Date(1000),
|
||||||
|
} as unknown as TimeRange;
|
||||||
|
|
||||||
|
describe('getSpanDetailLinkButtons', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty buttons when createSpanLink is not provided', () => {
|
||||||
|
const result = getSpanDetailLinkButtons({
|
||||||
|
span,
|
||||||
|
createSpanLink: undefined,
|
||||||
|
datasourceType: 'test',
|
||||||
|
traceToProfilesOptions: undefined,
|
||||||
|
timeRange,
|
||||||
|
app: CoreApp.Explore,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.logLinkButton).toBeNull();
|
||||||
|
expect(result.profileLinkButtons).toBeNull();
|
||||||
|
expect(result.sessionLinkButton).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create log link button when logs link exists', () => {
|
||||||
|
createSpanLink.mockReturnValue([{ type: SpanLinkType.Logs, href: '/logs', title: 'Logs' }]);
|
||||||
|
|
||||||
|
const result = getSpanDetailLinkButtons({
|
||||||
|
span,
|
||||||
|
createSpanLink,
|
||||||
|
datasourceType: 'test',
|
||||||
|
traceToProfilesOptions: undefined,
|
||||||
|
timeRange,
|
||||||
|
app: CoreApp.Explore,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.logLinkButton).toBeDefined();
|
||||||
|
expect(result.profileLinkButtons).toBeNull();
|
||||||
|
expect(result.sessionLinkButton).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create profile link button when profiles link exists', () => {
|
||||||
|
createSpanLink.mockReturnValue([{ type: SpanLinkType.Profiles, href: '/profiles', title: RelatedProfilesTitle }]);
|
||||||
|
|
||||||
|
const result = getSpanDetailLinkButtons({
|
||||||
|
span,
|
||||||
|
createSpanLink,
|
||||||
|
datasourceType: 'test',
|
||||||
|
traceToProfilesOptions: {
|
||||||
|
datasourceUid: 'test-uid',
|
||||||
|
profileTypeId: 'test-type',
|
||||||
|
customQuery: false,
|
||||||
|
},
|
||||||
|
timeRange,
|
||||||
|
app: CoreApp.Explore,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.logLinkButton).toBeNull();
|
||||||
|
expect(result.profileLinkButtons).toBeDefined();
|
||||||
|
expect(result.sessionLinkButton).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create session link button when session link exists', () => {
|
||||||
|
createSpanLink.mockReturnValue([{ type: SpanLinkType.Session, href: '/session', title: 'Session' }]);
|
||||||
|
|
||||||
|
const result = getSpanDetailLinkButtons({
|
||||||
|
span,
|
||||||
|
createSpanLink,
|
||||||
|
datasourceType: 'test',
|
||||||
|
traceToProfilesOptions: undefined,
|
||||||
|
timeRange,
|
||||||
|
app: CoreApp.Explore,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.logLinkButton).toBeNull();
|
||||||
|
expect(result.profileLinkButtons).toBeNull();
|
||||||
|
expect(result.sessionLinkButton).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create profile drilldown button when plugin link exists', () => {
|
||||||
|
createSpanLink.mockReturnValue([{ type: SpanLinkType.Profiles, href: '/profiles', title: RelatedProfilesTitle }]);
|
||||||
|
(usePluginLinks as jest.Mock).mockReturnValue({
|
||||||
|
isLoading: false,
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
pluginId: 'grafana-pyroscope-app',
|
||||||
|
title: 'Open in Profiles Drilldown',
|
||||||
|
onClick: jest.fn(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getSpanDetailLinkButtons({
|
||||||
|
span,
|
||||||
|
createSpanLink,
|
||||||
|
datasourceType: 'test',
|
||||||
|
traceToProfilesOptions: {
|
||||||
|
datasourceUid: 'test-uid',
|
||||||
|
profileTypeId: 'test-type',
|
||||||
|
customQuery: false,
|
||||||
|
},
|
||||||
|
timeRange,
|
||||||
|
app: CoreApp.Explore,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.profileLinkButtons).toBeDefined();
|
||||||
|
// Should render both the original profile link and the drilldown button
|
||||||
|
expect(result.profileLinkButtons?.props.children).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not create profile drilldown button when not in Explore', () => {
|
||||||
|
createSpanLink.mockReturnValue([{ type: SpanLinkType.Profiles, href: '/profiles', title: RelatedProfilesTitle }]);
|
||||||
|
(usePluginLinks as jest.Mock).mockReturnValue({
|
||||||
|
isLoading: false,
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
pluginId: 'grafana-pyroscope-app',
|
||||||
|
title: 'Open in Profiles Drilldown',
|
||||||
|
onClick: jest.fn(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getSpanDetailLinkButtons({
|
||||||
|
span,
|
||||||
|
createSpanLink,
|
||||||
|
datasourceType: 'test',
|
||||||
|
traceToProfilesOptions: {
|
||||||
|
datasourceUid: 'test-uid',
|
||||||
|
profileTypeId: 'test-type',
|
||||||
|
customQuery: false,
|
||||||
|
},
|
||||||
|
timeRange,
|
||||||
|
app: CoreApp.Dashboard,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.profileLinkButtons).toBeDefined();
|
||||||
|
// Should only render the original profile link
|
||||||
|
expect(result.profileLinkButtons?.props.children).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getProfileLinkButtonsContext', () => {
|
||||||
|
const traceToProfilesOptions = {
|
||||||
|
datasourceUid: 'test-uid',
|
||||||
|
profileTypeId: 'test-type',
|
||||||
|
customQuery: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should create context with all properties', () => {
|
||||||
|
const context = getProfileLinkButtonsContext(span, traceToProfilesOptions, timeRange);
|
||||||
|
|
||||||
|
expect(context).toEqual({
|
||||||
|
serviceName: 'test-service',
|
||||||
|
profileTypeId: 'test-type',
|
||||||
|
spanSelector: 'test-profile',
|
||||||
|
explorationType: 'flame-graph',
|
||||||
|
timeRange: {
|
||||||
|
from: new Date(0).toISOString(),
|
||||||
|
to: new Date(1000).toISOString(),
|
||||||
|
},
|
||||||
|
datasource: {
|
||||||
|
uid: 'test-uid',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing traceToProfilesOptions', () => {
|
||||||
|
const context = getProfileLinkButtonsContext(span, undefined, timeRange);
|
||||||
|
|
||||||
|
expect(context).toEqual({
|
||||||
|
serviceName: 'test-service',
|
||||||
|
profileTypeId: '',
|
||||||
|
spanSelector: 'test-profile',
|
||||||
|
explorationType: 'flame-graph',
|
||||||
|
timeRange: {
|
||||||
|
from: new Date(0).toISOString(),
|
||||||
|
to: new Date(1000).toISOString(),
|
||||||
|
},
|
||||||
|
datasource: {
|
||||||
|
uid: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing service name', () => {
|
||||||
|
const spanWithoutService = {
|
||||||
|
process: {},
|
||||||
|
tags: [{ key: 'pyroscope.profile.id', value: 'test-profile' }],
|
||||||
|
} as TraceSpan;
|
||||||
|
|
||||||
|
const context = getProfileLinkButtonsContext(spanWithoutService, traceToProfilesOptions, timeRange);
|
||||||
|
|
||||||
|
expect(context.serviceName).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing profile ID tag', () => {
|
||||||
|
const spanWithoutProfileId = {
|
||||||
|
process: { serviceName: 'test-service' },
|
||||||
|
tags: [],
|
||||||
|
} as unknown as TraceSpan;
|
||||||
|
|
||||||
|
const context = getProfileLinkButtonsContext(spanWithoutProfileId, traceToProfilesOptions, timeRange);
|
||||||
|
|
||||||
|
expect(context.spanSelector).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { CoreApp, IconName, PluginExtensionPoints, RawTimeRange, TimeRange } from '@grafana/data';
|
||||||
|
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||||
|
import { config, locationService, reportInteraction, usePluginLinks } from '@grafana/runtime';
|
||||||
|
import { DataSourceRef } from '@grafana/schema';
|
||||||
|
import { Button, DataLinkButton } from '@grafana/ui';
|
||||||
|
import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer';
|
||||||
|
|
||||||
|
import { pyroscopeProfileIdTagKey } from '../../../createSpanLink';
|
||||||
|
import { SpanLinkFunc } from '../../types';
|
||||||
|
import { SpanLinkDef, SpanLinkType } from '../../types/links';
|
||||||
|
import { TraceSpan } from '../../types/trace';
|
||||||
|
|
||||||
|
export type ProfilesButtonContext = {
|
||||||
|
serviceName: string;
|
||||||
|
profileTypeId: string;
|
||||||
|
spanSelector: string;
|
||||||
|
explorationType: string;
|
||||||
|
timeRange: RawTimeRange;
|
||||||
|
datasource: DataSourceRef;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
span: TraceSpan;
|
||||||
|
traceToProfilesOptions?: TraceToProfilesOptions;
|
||||||
|
datasourceType: string;
|
||||||
|
timeRange: TimeRange;
|
||||||
|
createSpanLink?: SpanLinkFunc;
|
||||||
|
app: CoreApp;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSpanDetailLinkButtons = (props: Props) => {
|
||||||
|
const { span, createSpanLink, traceToProfilesOptions, timeRange, datasourceType, app } = props;
|
||||||
|
|
||||||
|
let logLinkButton: JSX.Element | null = null;
|
||||||
|
let profileLinkButton: JSX.Element | null = null;
|
||||||
|
let sessionLinkButton: JSX.Element | null = null;
|
||||||
|
if (createSpanLink) {
|
||||||
|
const links = createSpanLink(span);
|
||||||
|
const logsLink = links?.filter((link) => link.type === SpanLinkType.Logs);
|
||||||
|
if (links && logsLink && logsLink.length > 0) {
|
||||||
|
logLinkButton = createLinkButton(logsLink[0], SpanLinkType.Logs, 'Logs for this span', 'gf-logs', datasourceType);
|
||||||
|
}
|
||||||
|
const profilesLink = links?.filter(
|
||||||
|
(link) => link.type === SpanLinkType.Profiles && link.title === RelatedProfilesTitle
|
||||||
|
);
|
||||||
|
if (links && profilesLink && profilesLink.length > 0) {
|
||||||
|
profileLinkButton = createLinkButton(
|
||||||
|
profilesLink[0],
|
||||||
|
SpanLinkType.Profiles,
|
||||||
|
'Profiles for this span',
|
||||||
|
'link',
|
||||||
|
datasourceType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const sessionLink = links?.filter((link) => link.type === SpanLinkType.Session);
|
||||||
|
if (links && sessionLink && sessionLink.length > 0) {
|
||||||
|
sessionLinkButton = createLinkButton(
|
||||||
|
sessionLink[0],
|
||||||
|
SpanLinkType.Session,
|
||||||
|
'Session for this span',
|
||||||
|
'frontend-observability',
|
||||||
|
datasourceType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let profileLinkButtons = profileLinkButton;
|
||||||
|
if (profileLinkButton) {
|
||||||
|
// ensure we have a profile link
|
||||||
|
const profilesDrilldownPluginId = 'grafana-pyroscope-app';
|
||||||
|
const context = getProfileLinkButtonsContext(span, traceToProfilesOptions, timeRange);
|
||||||
|
|
||||||
|
// if in explore, use the plugin extension point to get the link
|
||||||
|
// note: plugin extension point links are not currently supported in panel plugins
|
||||||
|
if (app === CoreApp.Explore) {
|
||||||
|
const extensionPointId = PluginExtensionPoints.TraceViewDetails;
|
||||||
|
const { links } = usePluginLinks({ extensionPointId, context, limitPerPlugin: 1 });
|
||||||
|
const link = links && links.length > 0 ? links.find((link) => link.pluginId === profilesDrilldownPluginId) : null;
|
||||||
|
const label = 'Open in Profiles Drilldown';
|
||||||
|
|
||||||
|
// if we have a plugin link, add a button to open in Grafana Profiles Drilldown
|
||||||
|
if (link) {
|
||||||
|
const profileDrilldownLinkButton = (
|
||||||
|
<Button
|
||||||
|
icon="link"
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (link && link.onClick) {
|
||||||
|
reportInteraction('grafana_traces_trace_view_span_link_clicked', {
|
||||||
|
datasourceType,
|
||||||
|
grafana_version: config.buildInfo.version,
|
||||||
|
type: SpanLinkType.ProfilesDrilldown,
|
||||||
|
location: 'spanDetails',
|
||||||
|
});
|
||||||
|
|
||||||
|
link.onClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
profileLinkButtons = (
|
||||||
|
<>
|
||||||
|
{profileLinkButton}
|
||||||
|
{profileDrilldownLinkButton}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { profileLinkButtons, logLinkButton, sessionLinkButton };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProfileLinkButtonsContext = (
|
||||||
|
span: TraceSpan,
|
||||||
|
traceToProfilesOptions: TraceToProfilesOptions | undefined,
|
||||||
|
timeRange: TimeRange
|
||||||
|
) => {
|
||||||
|
const spanSelector = span.tags.filter((tag) => tag.key === pyroscopeProfileIdTagKey);
|
||||||
|
const context: ProfilesButtonContext = {
|
||||||
|
serviceName: span.process.serviceName ?? '',
|
||||||
|
profileTypeId: traceToProfilesOptions?.profileTypeId ?? '',
|
||||||
|
spanSelector: spanSelector.length === 1 && spanSelector[0].value ? spanSelector[0].value : '',
|
||||||
|
explorationType: 'flame-graph',
|
||||||
|
timeRange: {
|
||||||
|
from: timeRange.from.toISOString(),
|
||||||
|
to: timeRange.to.toISOString(),
|
||||||
|
},
|
||||||
|
datasource: { uid: traceToProfilesOptions?.datasourceUid },
|
||||||
|
};
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createLinkButton = (
|
||||||
|
link: SpanLinkDef,
|
||||||
|
type: SpanLinkType,
|
||||||
|
title: string,
|
||||||
|
icon: IconName,
|
||||||
|
datasourceType: string,
|
||||||
|
className?: string
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<DataLinkButton
|
||||||
|
link={{
|
||||||
|
...link,
|
||||||
|
title: title,
|
||||||
|
target: '_blank',
|
||||||
|
origin: link.field,
|
||||||
|
onClick: (event: React.MouseEvent) => {
|
||||||
|
// DataLinkButton assumes if you provide an onClick event you would want to prevent default behavior like navigation
|
||||||
|
// In this case, if an onClick is not defined, restore navigation to the provided href while keeping the tracking
|
||||||
|
// this interaction will not be tracked with link right clicks
|
||||||
|
reportInteraction('grafana_traces_trace_view_span_link_clicked', {
|
||||||
|
datasourceType,
|
||||||
|
grafana_version: config.buildInfo.version,
|
||||||
|
type,
|
||||||
|
location: 'spanDetails',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (link.onClick) {
|
||||||
|
link.onClick?.(event);
|
||||||
|
} else {
|
||||||
|
locationService.push(link.href);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
buttonProps={{ icon, className }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
+12
-1
@@ -19,7 +19,7 @@ import userEvent from '@testing-library/user-event';
|
|||||||
|
|
||||||
import { createDataFrame, DataSourceInstanceSettings } from '@grafana/data';
|
import { createDataFrame, DataSourceInstanceSettings } from '@grafana/data';
|
||||||
import { data } from '@grafana/flamegraph';
|
import { data } from '@grafana/flamegraph';
|
||||||
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
import { DataSourceSrv, setDataSourceSrv, setPluginLinksHook } from '@grafana/runtime';
|
||||||
|
|
||||||
import { pyroscopeProfileIdTagKey } from '../../../createSpanLink';
|
import { pyroscopeProfileIdTagKey } from '../../../createSpanLink';
|
||||||
import traceGenerator from '../../demo/trace-generators';
|
import traceGenerator from '../../demo/trace-generators';
|
||||||
@@ -70,6 +70,12 @@ describe('<SpanDetail>', () => {
|
|||||||
createFocusSpanLink: jest.fn().mockReturnValue({}),
|
createFocusSpanLink: jest.fn().mockReturnValue({}),
|
||||||
traceFlameGraphs: { [span.spanID]: createDataFrame(data) },
|
traceFlameGraphs: { [span.spanID]: createDataFrame(data) },
|
||||||
setRedrawListView: jest.fn(),
|
setRedrawListView: jest.fn(),
|
||||||
|
timeRange: {
|
||||||
|
raw: {
|
||||||
|
from: 0,
|
||||||
|
to: 1000000000000,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
span.tags = [
|
span.tags = [
|
||||||
@@ -156,6 +162,11 @@ describe('<SpanDetail>', () => {
|
|||||||
props.logsToggle.mockReset();
|
props.logsToggle.mockReset();
|
||||||
props.logItemToggle.mockReset();
|
props.logItemToggle.mockReset();
|
||||||
|
|
||||||
|
setPluginLinksHook(() => ({
|
||||||
|
isLoading: false,
|
||||||
|
links: [],
|
||||||
|
}));
|
||||||
|
|
||||||
setDataSourceSrv({
|
setDataSourceSrv({
|
||||||
getList() {
|
getList() {
|
||||||
return [pyroSettings];
|
return [pyroSettings];
|
||||||
|
|||||||
+17
-63
@@ -15,29 +15,26 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { SpanStatusCode } from '@opentelemetry/api';
|
import { SpanStatusCode } from '@opentelemetry/api';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CoreApp,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
dateTimeFormat,
|
dateTimeFormat,
|
||||||
GrafanaTheme2,
|
GrafanaTheme2,
|
||||||
IconName,
|
|
||||||
LinkModel,
|
LinkModel,
|
||||||
|
TimeRange,
|
||||||
TraceKeyValuePair,
|
TraceKeyValuePair,
|
||||||
TraceLog,
|
TraceLog,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||||
import { config, locationService, reportInteraction } from '@grafana/runtime';
|
|
||||||
import { TimeZone } from '@grafana/schema';
|
import { TimeZone } from '@grafana/schema';
|
||||||
import { DataLinkButton, Divider, Icon, TextArea, useStyles2 } from '@grafana/ui';
|
import { Divider, Icon, TextArea, useStyles2 } from '@grafana/ui';
|
||||||
import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer';
|
|
||||||
|
|
||||||
import { pyroscopeProfileIdTagKey } from '../../../createSpanLink';
|
import { pyroscopeProfileIdTagKey } from '../../../createSpanLink';
|
||||||
import { autoColor } from '../../Theme';
|
import { autoColor } from '../../Theme';
|
||||||
import LabeledList from '../../common/LabeledList';
|
import LabeledList from '../../common/LabeledList';
|
||||||
import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE } from '../../constants/span';
|
import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE } from '../../constants/span';
|
||||||
import { SpanLinkFunc, TNil } from '../../types';
|
import { SpanLinkFunc, TNil } from '../../types';
|
||||||
import { SpanLinkDef, SpanLinkType } from '../../types/links';
|
|
||||||
import { TraceLink, TraceSpan, TraceSpanReference } from '../../types/trace';
|
import { TraceLink, TraceSpan, TraceSpanReference } from '../../types/trace';
|
||||||
import { formatDuration } from '../utils';
|
import { formatDuration } from '../utils';
|
||||||
|
|
||||||
@@ -46,6 +43,7 @@ import AccordianLogs from './AccordianLogs';
|
|||||||
import AccordianReferences from './AccordianReferences';
|
import AccordianReferences from './AccordianReferences';
|
||||||
import AccordianText from './AccordianText';
|
import AccordianText from './AccordianText';
|
||||||
import DetailState from './DetailState';
|
import DetailState from './DetailState';
|
||||||
|
import { getSpanDetailLinkButtons } from './SpanDetailLinkButtons';
|
||||||
import SpanFlameGraph from './SpanFlameGraph';
|
import SpanFlameGraph from './SpanFlameGraph';
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
@@ -168,6 +166,8 @@ export type SpanDetailProps = {
|
|||||||
traceFlameGraphs: TraceFlameGraphs;
|
traceFlameGraphs: TraceFlameGraphs;
|
||||||
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
|
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
|
||||||
setRedrawListView: (redraw: {}) => void;
|
setRedrawListView: (redraw: {}) => void;
|
||||||
|
timeRange: TimeRange;
|
||||||
|
app: CoreApp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SpanDetail(props: SpanDetailProps) {
|
export default function SpanDetail(props: SpanDetailProps) {
|
||||||
@@ -193,6 +193,8 @@ export default function SpanDetail(props: SpanDetailProps) {
|
|||||||
setTraceFlameGraphs,
|
setTraceFlameGraphs,
|
||||||
traceToProfilesOptions,
|
traceToProfilesOptions,
|
||||||
setRedrawListView,
|
setRedrawListView,
|
||||||
|
timeRange,
|
||||||
|
app,
|
||||||
} = props;
|
} = props;
|
||||||
const {
|
const {
|
||||||
isTagsOpen,
|
isTagsOpen,
|
||||||
@@ -289,62 +291,14 @@ export default function SpanDetail(props: SpanDetailProps) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const createLinkButton = (link: SpanLinkDef, type: SpanLinkType, title: string, icon: IconName) => {
|
const { profileLinkButtons, logLinkButton, sessionLinkButton } = getSpanDetailLinkButtons({
|
||||||
return (
|
span,
|
||||||
<DataLinkButton
|
createSpanLink,
|
||||||
link={{
|
datasourceType,
|
||||||
...link,
|
traceToProfilesOptions,
|
||||||
title: title,
|
timeRange,
|
||||||
target: '_blank',
|
app,
|
||||||
origin: link.field,
|
});
|
||||||
onClick: (event: React.MouseEvent) => {
|
|
||||||
// DataLinkButton assumes if you provide an onClick event you would want to prevent default behavior like navigation
|
|
||||||
// In this case, if an onClick is not defined, restore navigation to the provided href while keeping the tracking
|
|
||||||
// this interaction will not be tracked with link right clicks
|
|
||||||
reportInteraction('grafana_traces_trace_view_span_link_clicked', {
|
|
||||||
datasourceType: datasourceType,
|
|
||||||
grafana_version: config.buildInfo.version,
|
|
||||||
type,
|
|
||||||
location: 'spanDetails',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (link.onClick) {
|
|
||||||
link.onClick?.(event);
|
|
||||||
} else {
|
|
||||||
locationService.push(link.href);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
buttonProps={{ icon }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
let logLinkButton: JSX.Element | null = null;
|
|
||||||
let profileLinkButton: JSX.Element | null = null;
|
|
||||||
let sessionLinkButton: JSX.Element | null = null;
|
|
||||||
if (createSpanLink) {
|
|
||||||
const links = createSpanLink(span);
|
|
||||||
const logsLink = links?.filter((link) => link.type === SpanLinkType.Logs);
|
|
||||||
if (links && logsLink && logsLink.length > 0) {
|
|
||||||
logLinkButton = createLinkButton(logsLink[0], SpanLinkType.Logs, 'Logs for this span', 'gf-logs');
|
|
||||||
}
|
|
||||||
const profilesLink = links?.filter(
|
|
||||||
(link) => link.type === SpanLinkType.Profiles && link.title === RelatedProfilesTitle
|
|
||||||
);
|
|
||||||
if (links && profilesLink && profilesLink.length > 0) {
|
|
||||||
profileLinkButton = createLinkButton(profilesLink[0], SpanLinkType.Profiles, 'Profiles for this span', 'link');
|
|
||||||
}
|
|
||||||
const sessionLink = links?.filter((link) => link.type === SpanLinkType.Session);
|
|
||||||
if (links && sessionLink && sessionLink.length > 0) {
|
|
||||||
sessionLinkButton = createLinkButton(
|
|
||||||
sessionLink[0],
|
|
||||||
SpanLinkType.Session,
|
|
||||||
'Session for this span',
|
|
||||||
'frontend-observability'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const focusSpanLink = createFocusSpanLink(traceID, spanID);
|
const focusSpanLink = createFocusSpanLink(traceID, spanID);
|
||||||
return (
|
return (
|
||||||
@@ -359,7 +313,7 @@ export default function SpanDetail(props: SpanDetailProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.linkList}>
|
<div className={styles.linkList}>
|
||||||
{logLinkButton}
|
{logLinkButton}
|
||||||
{profileLinkButton}
|
{profileLinkButtons}
|
||||||
{sessionLinkButton}
|
{sessionLinkButton}
|
||||||
</div>
|
</div>
|
||||||
<Divider spacing={1} />
|
<Divider spacing={1} />
|
||||||
|
|||||||
+14
@@ -16,6 +16,7 @@ import { render, screen } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
import { createTheme } from '@grafana/data';
|
import { createTheme } from '@grafana/data';
|
||||||
|
import { setPluginLinksHook } from '@grafana/runtime';
|
||||||
|
|
||||||
import DetailState from './SpanDetail/DetailState';
|
import DetailState from './SpanDetail/DetailState';
|
||||||
import { UnthemedSpanDetailRow, SpanDetailRowProps } from './SpanDetailRow';
|
import { UnthemedSpanDetailRow, SpanDetailRowProps } from './SpanDetailRow';
|
||||||
@@ -47,12 +48,25 @@ const setup = (propOverrides?: SpanDetailRowProps) => {
|
|||||||
traceStartTime: 1000,
|
traceStartTime: 1000,
|
||||||
theme: createTheme(),
|
theme: createTheme(),
|
||||||
traceFlameGraphs: {},
|
traceFlameGraphs: {},
|
||||||
|
timeRange: {
|
||||||
|
raw: {
|
||||||
|
from: 0,
|
||||||
|
to: 1000000000000,
|
||||||
|
},
|
||||||
|
},
|
||||||
...propOverrides,
|
...propOverrides,
|
||||||
};
|
};
|
||||||
return render(<UnthemedSpanDetailRow {...(props as SpanDetailRowProps)} />);
|
return render(<UnthemedSpanDetailRow {...(props as SpanDetailRowProps)} />);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('SpanDetailRow tests', () => {
|
describe('SpanDetailRow tests', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setPluginLinksHook(() => ({
|
||||||
|
isLoading: false,
|
||||||
|
links: [],
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
it('renders without exploding', () => {
|
it('renders without exploding', () => {
|
||||||
expect(() => setup()).not.toThrow();
|
expect(() => setup()).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|||||||
+7
-1
@@ -16,7 +16,7 @@ import { css } from '@emotion/css';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, LinkModel, TraceKeyValuePair, TraceLog } from '@grafana/data';
|
import { CoreApp, GrafanaTheme2, LinkModel, TimeRange, TraceKeyValuePair, TraceLog } from '@grafana/data';
|
||||||
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||||
import { TimeZone } from '@grafana/schema';
|
import { TimeZone } from '@grafana/schema';
|
||||||
import { Button, clearButtonStyles, stylesFactory, withTheme2 } from '@grafana/ui';
|
import { Button, clearButtonStyles, stylesFactory, withTheme2 } from '@grafana/ui';
|
||||||
@@ -103,6 +103,8 @@ export type SpanDetailRowProps = {
|
|||||||
traceFlameGraphs: TraceFlameGraphs;
|
traceFlameGraphs: TraceFlameGraphs;
|
||||||
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
|
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
|
||||||
setRedrawListView: (redraw: {}) => void;
|
setRedrawListView: (redraw: {}) => void;
|
||||||
|
timeRange: TimeRange;
|
||||||
|
app: CoreApp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class UnthemedSpanDetailRow extends PureComponent<SpanDetailRowProps> {
|
export class UnthemedSpanDetailRow extends PureComponent<SpanDetailRowProps> {
|
||||||
@@ -146,6 +148,8 @@ export class UnthemedSpanDetailRow extends PureComponent<SpanDetailRowProps> {
|
|||||||
traceFlameGraphs,
|
traceFlameGraphs,
|
||||||
setTraceFlameGraphs,
|
setTraceFlameGraphs,
|
||||||
setRedrawListView,
|
setRedrawListView,
|
||||||
|
timeRange,
|
||||||
|
app,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
return (
|
return (
|
||||||
@@ -193,6 +197,8 @@ export class UnthemedSpanDetailRow extends PureComponent<SpanDetailRowProps> {
|
|||||||
traceFlameGraphs={traceFlameGraphs}
|
traceFlameGraphs={traceFlameGraphs}
|
||||||
setTraceFlameGraphs={setTraceFlameGraphs}
|
setTraceFlameGraphs={setTraceFlameGraphs}
|
||||||
setRedrawListView={setRedrawListView}
|
setRedrawListView={setRedrawListView}
|
||||||
|
timeRange={timeRange}
|
||||||
|
app={app}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TimelineRow.Cell>
|
</TimelineRow.Cell>
|
||||||
|
|||||||
+7
-1
@@ -18,7 +18,7 @@ import memoizeOne from 'memoize-one';
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { RefObject } from 'react';
|
import { RefObject } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, LinkModel, TraceKeyValuePair, TraceLog } from '@grafana/data';
|
import { CoreApp, GrafanaTheme2, LinkModel, TimeRange, TraceKeyValuePair, TraceLog } from '@grafana/data';
|
||||||
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||||
import { config, reportInteraction } from '@grafana/runtime';
|
import { config, reportInteraction } from '@grafana/runtime';
|
||||||
import { TimeZone } from '@grafana/schema';
|
import { TimeZone } from '@grafana/schema';
|
||||||
@@ -109,6 +109,8 @@ type TVirtualizedTraceViewOwnProps = {
|
|||||||
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
|
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
|
||||||
redrawListView: {};
|
redrawListView: {};
|
||||||
setRedrawListView: (redraw: {}) => void;
|
setRedrawListView: (redraw: {}) => void;
|
||||||
|
timeRange: TimeRange;
|
||||||
|
app: CoreApp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TTraceTimeline;
|
export type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TTraceTimeline;
|
||||||
@@ -557,6 +559,8 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
|||||||
traceFlameGraphs,
|
traceFlameGraphs,
|
||||||
setTraceFlameGraphs,
|
setTraceFlameGraphs,
|
||||||
setRedrawListView,
|
setRedrawListView,
|
||||||
|
timeRange,
|
||||||
|
app,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const detailState = detailStates.get(spanID);
|
const detailState = detailStates.get(spanID);
|
||||||
if (!trace || !detailState) {
|
if (!trace || !detailState) {
|
||||||
@@ -598,6 +602,8 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
|||||||
traceFlameGraphs={traceFlameGraphs}
|
traceFlameGraphs={traceFlameGraphs}
|
||||||
setTraceFlameGraphs={setTraceFlameGraphs}
|
setTraceFlameGraphs={setTraceFlameGraphs}
|
||||||
setRedrawListView={setRedrawListView}
|
setRedrawListView={setRedrawListView}
|
||||||
|
timeRange={timeRange}
|
||||||
|
app={app}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { PureComponent, RefObject } from 'react';
|
import { PureComponent, RefObject } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, LinkModel, TraceKeyValuePair, TraceLog } from '@grafana/data';
|
import { CoreApp, GrafanaTheme2, LinkModel, TimeRange, TraceKeyValuePair, TraceLog } from '@grafana/data';
|
||||||
import { SpanBarOptions, TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
import { SpanBarOptions, TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||||
import { config, reportInteraction } from '@grafana/runtime';
|
import { config, reportInteraction } from '@grafana/runtime';
|
||||||
import { TimeZone } from '@grafana/schema';
|
import { TimeZone } from '@grafana/schema';
|
||||||
@@ -112,6 +112,8 @@ export type TProps = {
|
|||||||
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
|
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
|
||||||
redrawListView: {};
|
redrawListView: {};
|
||||||
setRedrawListView: (redraw: {}) => void;
|
setRedrawListView: (redraw: {}) => void;
|
||||||
|
timeRange: TimeRange;
|
||||||
|
app: CoreApp;
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export enum SpanLinkType {
|
|||||||
Traces = 'trace',
|
Traces = 'trace',
|
||||||
Metrics = 'metric',
|
Metrics = 'metric',
|
||||||
Profiles = 'profile',
|
Profiles = 'profile',
|
||||||
|
ProfilesDrilldown = 'profile-drilldown',
|
||||||
Session = 'session',
|
Session = 'session',
|
||||||
Unknown = 'unknown',
|
Unknown = 'unknown',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export const TracesPanel = ({ data, options, replaceVariables }: PanelProps<Trac
|
|||||||
focusedSpanId={options.focusedSpanId}
|
focusedSpanId={options.focusedSpanId}
|
||||||
createFocusSpanLink={options.createFocusSpanLink}
|
createFocusSpanLink={options.createFocusSpanLink}
|
||||||
spanFilters={replaceSearchVariables(replaceVariables, options.spanFilters)}
|
spanFilters={replaceSearchVariables(replaceVariables, options.spanFilters)}
|
||||||
|
timeRange={data.timeRange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user