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:
Joey
2025-03-05 15:47:44 +01:00
committed by GitHub
co-authored by Piotr Jamróz
parent c23bb36956
commit 31e92ba720
16 changed files with 490 additions and 72 deletions
@@ -185,6 +185,7 @@ export enum PluginExtensionPoints {
DataSourceConfig = 'grafana/datasources/config',
ExploreToolbarAction = 'grafana/explore/toolbar/action',
UserProfileTab = 'grafana/user/profile/tab',
TraceViewDetails = 'grafana/traceview/details',
}
export type PluginExtensionPanelContext = {
+1
View File
@@ -514,6 +514,7 @@ export class Explore extends PureComponent<Props, ExploreState> {
dataFrames={dataFrames}
splitOpenFn={this.onSplitOpen('traceView')}
scrollElement={this.scrollElement}
timeRange={queryResponse.timeRange}
/>
</ContentOutlineItem>
)
@@ -3,8 +3,8 @@ import userEvent from '@testing-library/user-event';
import { createRef } from 'react';
import { Provider } from 'react-redux';
import { DataFrame, MutableDataFrame } from '@grafana/data';
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
import { DataFrame, MutableDataFrame, TimeRange } from '@grafana/data';
import { DataSourceSrv, setDataSourceSrv, setPluginLinksHook } from '@grafana/runtime';
import { configureStore } from '../../../store/configureStore';
@@ -25,6 +25,7 @@ function getTraceView(frames: DataFrame[]) {
traceProp={transformDataFrames(frames[0])!}
datasource={undefined}
topOfViewRef={topOfViewRef}
timeRange={{} as TimeRange}
/>
</Provider>
);
@@ -47,6 +48,11 @@ function renderTraceViewNew() {
describe('TraceView', () => {
beforeAll(() => {
setPluginLinksHook(() => ({
isLoading: false,
links: [],
}));
setDataSourceSrv({
getInstanceSettings() {
return undefined;
@@ -13,6 +13,7 @@ import {
LinkModel,
mapInternalLinkToExplore,
SplitOpen,
TimeRange,
} from '@grafana/data';
import { getTraceToLogsOptions, TraceToMetricsData, TraceToProfilesData } from '@grafana/o11y-ds-frontend';
import { getTemplateSrv } from '@grafana/runtime';
@@ -67,6 +68,7 @@ type Props = {
focusedSpanId?: string;
createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>;
spanFilters?: SearchProps;
timeRange: TimeRange;
};
export function TraceView(props: Props) {
@@ -238,6 +240,8 @@ export function TraceView(props: Props) {
setTraceFlameGraphs={setTraceFlameGraphs}
redrawListView={redrawListView}
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 { Provider } from 'react-redux';
import { TimeRange } from '@grafana/data';
import { configureStore } from '../../../store/configureStore';
import { frameOld } from './TraceView.test';
@@ -11,6 +13,7 @@ jest.mock('@grafana/runtime', () => {
return {
...jest.requireActual('@grafana/runtime'),
reportInteraction: jest.fn(),
usePluginLinks: jest.fn().mockReturnValue({ isLoading: false, links: [] }),
};
});
@@ -19,7 +22,7 @@ function renderTraceViewContainer(frames = [frameOld]) {
const { container, baseElement } = render(
<Provider store={store}>
<TraceViewContainer exploreId="left" dataFrames={frames} splitOpenFn={() => {}} />
<TraceViewContainer exploreId="left" dataFrames={frames} splitOpenFn={() => {}} timeRange={{} as TimeRange} />
</Provider>
);
return {
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { DataFrame, SplitOpen } from '@grafana/data';
import { DataFrame, SplitOpen, TimeRange } from '@grafana/data';
import { PanelChrome } from '@grafana/ui';
import { StoreState, useSelector } from 'app/types';
@@ -12,12 +12,13 @@ interface Props {
splitOpenFn: SplitOpen;
exploreId: string;
scrollElement?: Element;
timeRange: TimeRange;
}
export function TraceViewContainer(props: Props) {
// At this point we only show single trace
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 datasource = useSelector(
(state: StoreState) => state.explore.panes[props.exploreId]?.datasourceInstance ?? undefined
@@ -36,6 +37,7 @@ export function TraceViewContainer(props: Props) {
scrollElement={scrollElement}
traceProp={traceProp}
datasource={datasource}
timeRange={timeRange}
/>
</PanelChrome>
);
@@ -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('');
});
});
@@ -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 }}
/>
);
};
@@ -19,7 +19,7 @@ import userEvent from '@testing-library/user-event';
import { createDataFrame, DataSourceInstanceSettings } from '@grafana/data';
import { data } from '@grafana/flamegraph';
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
import { DataSourceSrv, setDataSourceSrv, setPluginLinksHook } from '@grafana/runtime';
import { pyroscopeProfileIdTagKey } from '../../../createSpanLink';
import traceGenerator from '../../demo/trace-generators';
@@ -70,6 +70,12 @@ describe('<SpanDetail>', () => {
createFocusSpanLink: jest.fn().mockReturnValue({}),
traceFlameGraphs: { [span.spanID]: createDataFrame(data) },
setRedrawListView: jest.fn(),
timeRange: {
raw: {
from: 0,
to: 1000000000000,
},
},
};
span.tags = [
@@ -156,6 +162,11 @@ describe('<SpanDetail>', () => {
props.logsToggle.mockReset();
props.logItemToggle.mockReset();
setPluginLinksHook(() => ({
isLoading: false,
links: [],
}));
setDataSourceSrv({
getList() {
return [pyroSettings];
@@ -15,29 +15,26 @@
import { css } from '@emotion/css';
import { SpanStatusCode } from '@opentelemetry/api';
import cx from 'classnames';
import * as React from 'react';
import {
CoreApp,
DataFrame,
dateTimeFormat,
GrafanaTheme2,
IconName,
LinkModel,
TimeRange,
TraceKeyValuePair,
TraceLog,
} from '@grafana/data';
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
import { config, locationService, reportInteraction } from '@grafana/runtime';
import { TimeZone } from '@grafana/schema';
import { DataLinkButton, Divider, Icon, TextArea, useStyles2 } from '@grafana/ui';
import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer';
import { Divider, Icon, TextArea, useStyles2 } from '@grafana/ui';
import { pyroscopeProfileIdTagKey } from '../../../createSpanLink';
import { autoColor } from '../../Theme';
import LabeledList from '../../common/LabeledList';
import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE } from '../../constants/span';
import { SpanLinkFunc, TNil } from '../../types';
import { SpanLinkDef, SpanLinkType } from '../../types/links';
import { TraceLink, TraceSpan, TraceSpanReference } from '../../types/trace';
import { formatDuration } from '../utils';
@@ -46,6 +43,7 @@ import AccordianLogs from './AccordianLogs';
import AccordianReferences from './AccordianReferences';
import AccordianText from './AccordianText';
import DetailState from './DetailState';
import { getSpanDetailLinkButtons } from './SpanDetailLinkButtons';
import SpanFlameGraph from './SpanFlameGraph';
const getStyles = (theme: GrafanaTheme2) => {
@@ -168,6 +166,8 @@ export type SpanDetailProps = {
traceFlameGraphs: TraceFlameGraphs;
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
setRedrawListView: (redraw: {}) => void;
timeRange: TimeRange;
app: CoreApp;
};
export default function SpanDetail(props: SpanDetailProps) {
@@ -193,6 +193,8 @@ export default function SpanDetail(props: SpanDetailProps) {
setTraceFlameGraphs,
traceToProfilesOptions,
setRedrawListView,
timeRange,
app,
} = props;
const {
isTagsOpen,
@@ -289,62 +291,14 @@ export default function SpanDetail(props: SpanDetailProps) {
});
}
const createLinkButton = (link: SpanLinkDef, type: SpanLinkType, title: string, icon: IconName) => {
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: 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 { profileLinkButtons, logLinkButton, sessionLinkButton } = getSpanDetailLinkButtons({
span,
createSpanLink,
datasourceType,
traceToProfilesOptions,
timeRange,
app,
});
const focusSpanLink = createFocusSpanLink(traceID, spanID);
return (
@@ -359,7 +313,7 @@ export default function SpanDetail(props: SpanDetailProps) {
</div>
<div className={styles.linkList}>
{logLinkButton}
{profileLinkButton}
{profileLinkButtons}
{sessionLinkButton}
</div>
<Divider spacing={1} />
@@ -16,6 +16,7 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createTheme } from '@grafana/data';
import { setPluginLinksHook } from '@grafana/runtime';
import DetailState from './SpanDetail/DetailState';
import { UnthemedSpanDetailRow, SpanDetailRowProps } from './SpanDetailRow';
@@ -47,12 +48,25 @@ const setup = (propOverrides?: SpanDetailRowProps) => {
traceStartTime: 1000,
theme: createTheme(),
traceFlameGraphs: {},
timeRange: {
raw: {
from: 0,
to: 1000000000000,
},
},
...propOverrides,
};
return render(<UnthemedSpanDetailRow {...(props as SpanDetailRowProps)} />);
};
describe('SpanDetailRow tests', () => {
beforeEach(() => {
setPluginLinksHook(() => ({
isLoading: false,
links: [],
}));
});
it('renders without exploding', () => {
expect(() => setup()).not.toThrow();
});
@@ -16,7 +16,7 @@ import { css } from '@emotion/css';
import classNames from 'classnames';
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 { TimeZone } from '@grafana/schema';
import { Button, clearButtonStyles, stylesFactory, withTheme2 } from '@grafana/ui';
@@ -103,6 +103,8 @@ export type SpanDetailRowProps = {
traceFlameGraphs: TraceFlameGraphs;
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
setRedrawListView: (redraw: {}) => void;
timeRange: TimeRange;
app: CoreApp;
};
export class UnthemedSpanDetailRow extends PureComponent<SpanDetailRowProps> {
@@ -146,6 +148,8 @@ export class UnthemedSpanDetailRow extends PureComponent<SpanDetailRowProps> {
traceFlameGraphs,
setTraceFlameGraphs,
setRedrawListView,
timeRange,
app,
} = this.props;
const styles = getStyles(theme);
return (
@@ -193,6 +197,8 @@ export class UnthemedSpanDetailRow extends PureComponent<SpanDetailRowProps> {
traceFlameGraphs={traceFlameGraphs}
setTraceFlameGraphs={setTraceFlameGraphs}
setRedrawListView={setRedrawListView}
timeRange={timeRange}
app={app}
/>
</div>
</TimelineRow.Cell>
@@ -18,7 +18,7 @@ import memoizeOne from 'memoize-one';
import * as React 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 { config, reportInteraction } from '@grafana/runtime';
import { TimeZone } from '@grafana/schema';
@@ -109,6 +109,8 @@ type TVirtualizedTraceViewOwnProps = {
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
redrawListView: {};
setRedrawListView: (redraw: {}) => void;
timeRange: TimeRange;
app: CoreApp;
};
export type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TTraceTimeline;
@@ -557,6 +559,8 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
traceFlameGraphs,
setTraceFlameGraphs,
setRedrawListView,
timeRange,
app,
} = this.props;
const detailState = detailStates.get(spanID);
if (!trace || !detailState) {
@@ -598,6 +602,8 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
traceFlameGraphs={traceFlameGraphs}
setTraceFlameGraphs={setTraceFlameGraphs}
setRedrawListView={setRedrawListView}
timeRange={timeRange}
app={app}
/>
</div>
);
@@ -15,7 +15,7 @@
import { css } from '@emotion/css';
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 { config, reportInteraction } from '@grafana/runtime';
import { TimeZone } from '@grafana/schema';
@@ -112,6 +112,8 @@ export type TProps = {
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
redrawListView: {};
setRedrawListView: (redraw: {}) => void;
timeRange: TimeRange;
app: CoreApp;
};
type State = {
@@ -9,6 +9,7 @@ export enum SpanLinkType {
Traces = 'trace',
Metrics = 'metric',
Profiles = 'profile',
ProfilesDrilldown = 'profile-drilldown',
Session = 'session',
Unknown = 'unknown',
}
@@ -52,6 +52,7 @@ export const TracesPanel = ({ data, options, replaceVariables }: PanelProps<Trac
focusedSpanId={options.focusedSpanId}
createFocusSpanLink={options.createFocusSpanLink}
spanFilters={replaceSearchVariables(replaceVariables, options.spanFilters)}
timeRange={data.timeRange}
/>
</div>
);