Data Links: Allow passing data links post processor via context (#110590)

* Data Links: Allow passing data links post processor via context

* Deprecate data links post processor in PanelContext

* Mark new context as internal
This commit is contained in:
Piotr Jamróz
2025-09-10 22:12:24 +02:00
committed by GitHub
parent 73a241da86
commit 8dd72408b2
11 changed files with 64 additions and 66 deletions
@@ -1,5 +1,5 @@
import { isNumber, set, unset, get, cloneDeep } from 'lodash'; import { isNumber, set, unset, get, cloneDeep } from 'lodash';
import { useMemo, useRef } from 'react'; import { createContext, useContext, useMemo, useRef } from 'react';
import { usePrevious } from 'react-use'; import { usePrevious } from 'react-use';
import { ThresholdsMode, VariableFormatID } from '@grafana/schema'; import { ThresholdsMode, VariableFormatID } from '@grafana/schema';
@@ -583,13 +583,14 @@ export function useFieldOverrides(
data: PanelData | undefined, data: PanelData | undefined,
timeZone: string, timeZone: string,
theme: GrafanaTheme2, theme: GrafanaTheme2,
replace: InterpolateFunction, replace: InterpolateFunction
dataLinkPostProcessor?: DataLinkPostProcessor
): PanelData | undefined { ): PanelData | undefined {
const fieldConfigRegistry = plugin?.fieldConfigRegistry; const fieldConfigRegistry = plugin?.fieldConfigRegistry;
const structureRev = useRef(0); const structureRev = useRef(0);
const prevSeries = usePrevious(data?.series); const prevSeries = usePrevious(data?.series);
const { dataLinkPostProcessor } = useDataLinksContext();
return useMemo(() => { return useMemo(() => {
if (!fieldConfigRegistry || !fieldConfig || !data) { if (!fieldConfigRegistry || !fieldConfig || !data) {
return; return;
@@ -650,3 +651,15 @@ export function getFieldDataContextClone(frame: DataFrame, field: Field, fieldSc
return { value: { frame, field, data: [frame] } }; return { value: { frame, field, data: [frame] } };
} }
/**
* @internal
*/
export const DataLinksContext = createContext<{
dataLinkPostProcessor: DataLinkPostProcessor;
}>({ dataLinkPostProcessor: defaultInternalLinkPostProcessor });
/**
* @internal
*/
export const useDataLinksContext = () => useContext(DataLinksContext);
+2
View File
@@ -150,6 +150,8 @@ export {
applyRawFieldOverrides, applyRawFieldOverrides,
useFieldOverrides, useFieldOverrides,
getFieldDataContextClone, getFieldDataContextClone,
DataLinksContext,
useDataLinksContext,
} from './field/fieldOverrides'; } from './field/fieldOverrides';
export { getFieldDisplayValuesProxy } from './field/getFieldDisplayValuesProxy'; export { getFieldDisplayValuesProxy } from './field/getFieldDisplayValuesProxy';
export { export {
@@ -96,6 +96,7 @@ export interface PanelContext {
/** /**
* Optional supplier for internal data links. If not provided a link pointing to Explore will be generated. * Optional supplier for internal data links. If not provided a link pointing to Explore will be generated.
* @internal * @internal
* @deprecated Please use DataLinksContext instead. This property will be removed in next major.
*/ */
dataLinkPostProcessor?: DataLinkPostProcessor; dataLinkPostProcessor?: DataLinkPostProcessor;
} }
@@ -1,6 +1,6 @@
import { DataFrame, EventBus, LoadingState, SplitOpen, TimeRange } from '@grafana/data'; import { DataFrame, DataLinksContext, EventBus, LoadingState, SplitOpen, TimeRange } from '@grafana/data';
import { PanelRenderer } from '@grafana/runtime'; import { PanelRenderer } from '@grafana/runtime';
import { PanelChrome, PanelContext, PanelContextProvider } from '@grafana/ui'; import { PanelChrome } from '@grafana/ui';
import { getPanelPluginMeta } from '../plugins/importPanelPlugin'; import { getPanelPluginMeta } from '../plugins/importPanelPlugin';
@@ -18,29 +18,13 @@ export interface Props {
eventBus: EventBus; eventBus: EventBus;
} }
export function CustomContainer({ export function CustomContainer({ width, height, timeZone, state, pluginId, frames, timeRange, splitOpenFn }: Props) {
width,
height,
timeZone,
state,
pluginId,
frames,
timeRange,
splitOpenFn,
eventBus,
}: Props) {
const plugin = getPanelPluginMeta(pluginId); const plugin = getPanelPluginMeta(pluginId);
const dataLinkPostProcessor = useExploreDataLinkPostProcessor(splitOpenFn, timeRange); const dataLinkPostProcessor = useExploreDataLinkPostProcessor(splitOpenFn, timeRange);
const panelContext: PanelContext = {
dataLinkPostProcessor,
eventBus,
eventsScope: 'explore',
};
return ( return (
<PanelContextProvider value={panelContext}> <DataLinksContext.Provider value={{ dataLinkPostProcessor }}>
<PanelChrome title={plugin.name} width={width} height={height} loadingState={state}> <PanelChrome title={plugin.name} width={width} height={height} loadingState={state}>
{(innerWidth, innerHeight) => ( {(innerWidth, innerHeight) => (
<PanelRenderer <PanelRenderer
@@ -53,6 +37,6 @@ export function CustomContainer({
/> />
)} )}
</PanelChrome> </PanelChrome>
</PanelContextProvider> </DataLinksContext.Provider>
); );
} }
@@ -8,6 +8,7 @@ import {
createFieldConfigRegistry, createFieldConfigRegistry,
DashboardCursorSync, DashboardCursorSync,
DataFrame, DataFrame,
DataLinksContext,
EventBus, EventBus,
FieldColorModeId, FieldColorModeId,
FieldConfigSource, FieldConfigSource,
@@ -121,9 +122,8 @@ export function ExploreGraph({
replaceVariables: (value) => value, // We don't need proper replace here as it is only used in getLinks and we use getFieldLinks replaceVariables: (value) => value, // We don't need proper replace here as it is only used in getLinks and we use getFieldLinks
theme, theme,
fieldConfigRegistry, fieldConfigRegistry,
dataLinkPostProcessor,
}); });
}, [fieldConfigRegistry, data, timeZone, theme, styledFieldConfig, dataLinkPostProcessor]); }, [fieldConfigRegistry, data, timeZone, theme, styledFieldConfig]);
const annotationsWithConfig = useMemo(() => { const annotationsWithConfig = useMemo(() => {
return applyFieldOverrides({ return applyFieldOverrides({
@@ -171,7 +171,6 @@ export function ExploreGraph({
onToggleSeriesVisibility(label: string, mode: SeriesVisibilityChangeMode) { onToggleSeriesVisibility(label: string, mode: SeriesVisibilityChangeMode) {
setFieldConfig(seriesVisibilityConfigFactory(label, mode, fieldConfig, data)); setFieldConfig(seriesVisibilityConfigFactory(label, mode, fieldConfig, data));
}, },
dataLinkPostProcessor,
}; };
function toggleLegend(name: string | undefined, mode: SeriesVisibilityChangeMode) { function toggleLegend(name: string | undefined, mode: SeriesVisibilityChangeMode) {
@@ -204,23 +203,25 @@ export function ExploreGraph({
); );
return ( return (
<PanelContextProvider value={panelContext}> <DataLinksContext.Provider value={{ dataLinkPostProcessor }}>
<PanelRenderer <PanelContextProvider value={panelContext}>
data={{ <PanelRenderer
series: dataWithConfig, data={{
timeRange, series: dataWithConfig,
state: loadingState, timeRange,
annotations: annotationsWithConfig, state: loadingState,
structureRev, annotations: annotationsWithConfig,
}} structureRev,
pluginId="timeseries" }}
title="" pluginId="timeseries"
width={width} title=""
height={height} width={width}
onChangeTimeRange={onChangeTime} height={height}
timeZone={timeZone} onChangeTimeRange={onChangeTime}
options={panelOptions} timeZone={timeZone}
/> options={panelOptions}
</PanelContextProvider> />
</PanelContextProvider>
</DataLinksContext.Provider>
); );
} }
@@ -11,7 +11,7 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import { Trans } from '@grafana/i18n'; import { Trans } from '@grafana/i18n';
import { getTemplateSrv, PanelRendererProps } from '@grafana/runtime'; import { getTemplateSrv, PanelRendererProps } from '@grafana/runtime';
import { ErrorBoundaryAlert, usePanelContext, useTheme2 } from '@grafana/ui'; import { ErrorBoundaryAlert, useTheme2 } from '@grafana/ui';
import { appEvents } from 'app/core/core'; import { appEvents } from 'app/core/core';
import { importPanelPlugin, syncGetPanelPlugin } from '../../plugins/importPanelPlugin'; import { importPanelPlugin, syncGetPanelPlugin } from '../../plugins/importPanelPlugin';
@@ -39,16 +39,7 @@ export function PanelRenderer<P extends object = {}, F extends object = {}>(prop
const [plugin, setPlugin] = useState(syncGetPanelPlugin(pluginId)); const [plugin, setPlugin] = useState(syncGetPanelPlugin(pluginId));
const [error, setError] = useState<string | undefined>(); const [error, setError] = useState<string | undefined>();
const optionsWithDefaults = useOptionDefaults(plugin, options, fieldConfig); const optionsWithDefaults = useOptionDefaults(plugin, options, fieldConfig);
const { dataLinkPostProcessor } = usePanelContext(); const dataWithOverrides = useFieldOverrides(plugin, optionsWithDefaults?.fieldConfig, data, timeZone, theme, replace);
const dataWithOverrides = useFieldOverrides(
plugin,
optionsWithDefaults?.fieldConfig,
data,
timeZone,
theme,
replace,
dataLinkPostProcessor
);
useEffect(() => { useEffect(() => {
// If we already have a plugin and it's correct one do nothing // If we already have a plugin and it's correct one do nothing
@@ -4,7 +4,7 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import uPlot from 'uplot'; import uPlot from 'uplot';
import { Field, getDisplayProcessor, PanelProps } from '@grafana/data'; import { Field, getDisplayProcessor, PanelProps, useDataLinksContext } from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime'; import { PanelDataErrorView } from '@grafana/runtime';
import { DashboardCursorSync, TooltipDisplayMode } from '@grafana/schema'; import { DashboardCursorSync, TooltipDisplayMode } from '@grafana/schema';
import { import {
@@ -50,11 +50,12 @@ export const CandlestickPanel = ({
onThresholdsChange, onThresholdsChange,
canEditThresholds, canEditThresholds,
showThresholds, showThresholds,
dataLinkPostProcessor,
eventBus, eventBus,
canExecuteActions, canExecuteActions,
} = usePanelContext(); } = usePanelContext();
const { dataLinkPostProcessor } = useDataLinksContext();
const userCanExecuteActions = useMemo(() => canExecuteActions?.() ?? false, [canExecuteActions]); const userCanExecuteActions = useMemo(() => canExecuteActions?.() ?? false, [canExecuteActions]);
const theme = useTheme2(); const theme = useTheme2();
@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { DashboardCursorSync, PanelProps } from '@grafana/data'; import { DashboardCursorSync, PanelProps, useDataLinksContext } from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime'; import { PanelDataErrorView } from '@grafana/runtime';
import { import {
AxisPlacement, AxisPlacement,
@@ -47,8 +47,9 @@ export const StateTimelinePanel = ({
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2 // temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null); const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);
const { sync, eventsScope, canAddAnnotations, dataLinkPostProcessor, eventBus, canExecuteActions } = const { sync, eventsScope, canAddAnnotations, eventBus, canExecuteActions } = usePanelContext();
usePanelContext();
const { dataLinkPostProcessor } = useDataLinksContext();
const userCanExecuteActions = useMemo(() => canExecuteActions?.() ?? false, [canExecuteActions]); const userCanExecuteActions = useMemo(() => canExecuteActions?.() ?? false, [canExecuteActions]);
const cursorSync = sync?.() ?? DashboardCursorSync.Off; const cursorSync = sync?.() ?? DashboardCursorSync.Off;
@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { DashboardCursorSync, PanelProps } from '@grafana/data'; import { DashboardCursorSync, PanelProps, useDataLinksContext } from '@grafana/data';
import { Trans } from '@grafana/i18n'; import { Trans } from '@grafana/i18n';
import { PanelDataErrorView } from '@grafana/runtime'; import { PanelDataErrorView } from '@grafana/runtime';
import { import {
@@ -48,7 +48,8 @@ export const StatusHistoryPanel = ({
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2 // temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null); const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);
const { sync, eventsScope, canAddAnnotations, dataLinkPostProcessor, eventBus } = usePanelContext(); const { sync, eventsScope, canAddAnnotations, eventBus } = usePanelContext();
const { dataLinkPostProcessor } = useDataLinksContext();
const cursorSync = sync?.() ?? DashboardCursorSync.Off; const cursorSync = sync?.() ?? DashboardCursorSync.Off;
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
@@ -7,6 +7,7 @@ import {
DataFrame, DataFrame,
alignTimeRangeCompareData, alignTimeRangeCompareData,
shouldAlignTimeCompare, shouldAlignTimeCompare,
useDataLinksContext,
FieldType, FieldType,
} from '@grafana/data'; } from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime'; import { PanelDataErrorView } from '@grafana/runtime';
@@ -46,11 +47,12 @@ export const TimeSeriesPanel = ({
onThresholdsChange, onThresholdsChange,
canEditThresholds, canEditThresholds,
showThresholds, showThresholds,
dataLinkPostProcessor,
eventBus, eventBus,
canExecuteActions, canExecuteActions,
} = usePanelContext(); } = usePanelContext();
const { dataLinkPostProcessor } = useDataLinksContext();
const userCanExecuteActions = useMemo(() => canExecuteActions?.() ?? false, [canExecuteActions]); const userCanExecuteActions = useMemo(() => canExecuteActions?.() ?? false, [canExecuteActions]);
// Vertical orientation is not available for users through config. // Vertical orientation is not available for users through config.
// It is simplified version of horizontal time series panel and it does not support all plugins. // It is simplified version of horizontal time series panel and it does not support all plugins.
@@ -8,9 +8,10 @@ import {
FieldType, FieldType,
PanelProps, PanelProps,
TimeRange, TimeRange,
useDataLinksContext,
} from '@grafana/data'; } from '@grafana/data';
import { config, PanelDataErrorView } from '@grafana/runtime'; import { config, PanelDataErrorView } from '@grafana/runtime';
import { KeyboardPlugin, TooltipDisplayMode, usePanelContext, TooltipPlugin2 } from '@grafana/ui'; import { KeyboardPlugin, TooltipDisplayMode, TooltipPlugin2 } from '@grafana/ui';
import { TooltipHoverMode } from '@grafana/ui/internal'; import { TooltipHoverMode } from '@grafana/ui/internal';
import { XYFieldMatchers } from 'app/core/components/GraphNG/types'; import { XYFieldMatchers } from 'app/core/components/GraphNG/types';
import { preparePlotFrame } from 'app/core/components/GraphNG/utils'; import { preparePlotFrame } from 'app/core/components/GraphNG/utils';
@@ -33,7 +34,7 @@ export const TrendPanel = ({
replaceVariables, replaceVariables,
id, id,
}: PanelProps<Options>) => { }: PanelProps<Options>) => {
const { dataLinkPostProcessor } = usePanelContext(); const { dataLinkPostProcessor } = useDataLinksContext();
// Need to fallback to first number field if no xField is set in options otherwise panel crashes 😬 // Need to fallback to first number field if no xField is set in options otherwise panel crashes 😬
const trendXFieldName = const trendXFieldName =
options.xField ?? data.series[0]?.fields.find((field) => field.type === FieldType.number)?.name; options.xField ?? data.series[0]?.fields.find((field) => field.type === FieldType.number)?.name;