From 600137919e756e1dd5c0e32cfc43352fd4302a18 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:58:21 +0100 Subject: [PATCH] TraceView: Add support for plugin components in trace view header (#110524) * Add support for plugin components in trace view header * Fix lint error * Remove as * Fix tests * Prettier * better eslint --- .betterer.results | 3 +- .../explore/TraceView/TraceView.test.tsx | 7 ++- .../TracePageHeader/TracePageHeader.test.tsx | 7 ++- .../TracePageHeader/TracePageHeader.tsx | 50 ++++++++++++------- 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/.betterer.results b/.betterer.results index abc27670fe4..594f2a49e0c 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2662,8 +2662,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "1"] ], "public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx:5381": [ - [0, 0, 0, "React Hook \\"useMemo\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "0"], - [0, 0, 0, "React Hook \\"usePluginLinks\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "1"] + [0, 0, 0, "React Hook \\"useMemo\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "0"] ], "public/app/features/explore/TraceView/components/TracePageHeader/index.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./TracePageHeader\`)", "0"] diff --git a/public/app/features/explore/TraceView/TraceView.test.tsx b/public/app/features/explore/TraceView/TraceView.test.tsx index 69caf06d38f..92ec42b76e2 100644 --- a/public/app/features/explore/TraceView/TraceView.test.tsx +++ b/public/app/features/explore/TraceView/TraceView.test.tsx @@ -4,7 +4,7 @@ import { createRef } from 'react'; import { Provider } from 'react-redux'; import { DataFrame, MutableDataFrame, TimeRange } from '@grafana/data'; -import { DataSourceSrv, setDataSourceSrv, setPluginLinksHook } from '@grafana/runtime'; +import { DataSourceSrv, setDataSourceSrv, setPluginLinksHook, setPluginComponentsHook } from '@grafana/runtime'; import { configureStore } from '../../../store/configureStore'; @@ -52,6 +52,11 @@ describe('TraceView', () => { links: [], })); + setPluginComponentsHook(() => ({ + isLoading: false, + components: [], + })); + setDataSourceSrv({ getInstanceSettings() { return undefined; diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx index 97fc884d659..69c8b23f771 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx @@ -22,7 +22,7 @@ import { PluginExtensionPoints, PluginExtensionTypes, } from '@grafana/data'; -import { usePluginLinks } from '@grafana/runtime'; +import { usePluginLinks, usePluginComponents } from '@grafana/runtime'; import { DEFAULT_SPAN_FILTERS } from 'app/features/explore/state/constants'; import { TraceViewPluginExtensionContext } from '../types/trace'; @@ -34,6 +34,7 @@ import { trace } from './mocks'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), usePluginLinks: jest.fn(), + usePluginComponents: jest.fn(), reportInteraction: jest.fn(), })); @@ -92,6 +93,9 @@ const setup = (pluginLinks: { links: PluginExtensionLink[]; isLoading: boolean } const mockUsePluginLinks = usePluginLinks as jest.MockedFunction; mockUsePluginLinks.mockReturnValue(pluginLinks); + const mockUsePluginComponents = usePluginComponents as jest.MockedFunction; + mockUsePluginComponents.mockReturnValue({ components: [], isLoading: false }); + const defaultProps = { trace, timeZone: '', @@ -115,6 +119,7 @@ const setup = (pluginLinks: { links: PluginExtensionLink[]; isLoading: boolean } return { ...render(), mockUsePluginLinks, + mockUsePluginComponents, }; }; diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx index 579fc610ccd..4d3e15928d3 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx @@ -26,7 +26,7 @@ import { PluginExtensionPoints, } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { reportInteraction, usePluginLinks } from '@grafana/runtime'; +import { reportInteraction, renderLimitedComponents, usePluginComponents, usePluginLinks } from '@grafana/runtime'; import { TimeZone } from '@grafana/schema'; import { Badge, @@ -95,6 +95,28 @@ export const TracePageHeader = memo((props: TracePageHeaderProps) => { setHeaderHeight(document.querySelector('.' + styles.header)?.scrollHeight ?? 0); }, [setHeaderHeight, showSpanFilters, styles.header]); + // Build context for plugin extensions if trace is available + const traceContext: TraceViewPluginExtensionContext | undefined = trace + ? { + ...trace, + datasource: { + name: datasourceName, + uid: datasourceUid, + type: datasourceType, + }, + } + : undefined; + + const { links: extensionLinks } = usePluginLinks({ + extensionPointId: PluginExtensionPoints.TraceViewHeaderActions, + context: traceContext, + limitPerPlugin: 2, + }); + + const { components: extensionComponents } = usePluginComponents({ + extensionPointId: PluginExtensionPoints.TraceViewHeaderActions, + }); + if (!trace) { return null; } @@ -110,22 +132,6 @@ export const TracePageHeader = memo((props: TracePageHeaderProps) => { return new Set(trace.spans.map((span) => span.process?.serviceName)).size; }, [trace.spans]); - // Get plugin extensions for trace view header actions - const traceContext: TraceViewPluginExtensionContext = { - ...trace, - datasource: { - name: datasourceName, - uid: datasourceUid, - type: datasourceType, - }, - }; - - const { links: extensionLinks } = usePluginLinks({ - extensionPointId: PluginExtensionPoints.TraceViewHeaderActions, - context: traceContext, - limitPerPlugin: 2, - }); - let statusColor: BadgeColor = 'green'; if (status && status.length > 0) { if (status[0].value.toString().charAt(0) === '4') { @@ -212,6 +218,16 @@ export const TracePageHeader = memo((props: TracePageHeaderProps) => { )} +
+ {traceContext + ? renderLimitedComponents({ + props: traceContext, + components: extensionComponents, + limit: 2, + }) + : null} +
+ {config.feedbackLinksEnabled && (