Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8ae09bc
feat: add system and url provided transformations to SceneDataTransfo…
dprokop Aug 18, 2026
7d1014e
chore: run prettier
gtk-grafana Aug 18, 2026
78eb437
fix(SceneDataTransformer): prevent custom transforms from rewriting t…
gtk-grafana Aug 18, 2026
fdb7af0
fix(useDataTransformer): prevent destruction of system transformation…
gtk-grafana Aug 18, 2026
10c9c43
test(SceneDataTransformer): cover interoplation coverage gap
gtk-grafana Aug 18, 2026
d177c34
chore(SceneDataTransformer): clean up types
gtk-grafana Aug 18, 2026
3074fbb
fix: setUserTransformations change bug
gtk-grafana Aug 18, 2026
00e5b54
chore: remove `url` and rename `system` to `plugin`
gtk-grafana Aug 20, 2026
456fa65
Update packages/scenes/src/querying/SceneDataTransformer.ts
gtk-grafana Aug 21, 2026
a79dc4d
chore: prettier
gtk-grafana Aug 21, 2026
34b01c8
feat(system-transforms): add optional transform supplier
gtk-grafana Aug 24, 2026
df8275d
chore(TransformationOrigin): remove convention type literal
gtk-grafana Aug 24, 2026
05090da
chore(SceneDataTransformer): clone _suppliers and _originOrder
gtk-grafana Aug 24, 2026
f01074c
chore(SceneDataTransformer): set default getResolvedSystemTransformat…
gtk-grafana Aug 24, 2026
ed73af9
fix(SceneDataTransformer): default empty array reference changes on e…
gtk-grafana Aug 24, 2026
fc5d474
fix(SceneDataTransformer): unsubscribe _transformSub
gtk-grafana Aug 24, 2026
d7ff11c
chore(SceneDataTransformer): skip forced re-rerun when not needed
gtk-grafana Aug 24, 2026
40da41a
chore(SceneDataTransformer): no when the data is unchanged
gtk-grafana Aug 24, 2026
46f9ae8
feat(system-transformations): rewrite behavior implementation to inst…
gtk-grafana Sep 8, 2026
5f2af2d
docs(system-transformations): clean up todos, docs
gtk-grafana Sep 8, 2026
9a4b9d1
refactor(system-transformations): isolate, implememnt interface in Vi…
gtk-grafana Sep 8, 2026
dbcd44a
fix: ensure no provider transform interpolation
gtk-grafana Sep 8, 2026
cd44d49
fix: return no series when panel missing
gtk-grafana Sep 8, 2026
d8a6f89
fix(system-transformations): freeze constants, clear provider
gtk-grafana Sep 8, 2026
ac9f862
fix(discoverProvider): detect when reactivation changed which provide…
gtk-grafana Sep 8, 2026
679a135
Merge remote-tracking branch 'origin/main' into panel-system-transfor…
gtk-grafana Sep 11, 2026
0bf93cc
refactor: fix bad rename
gtk-grafana Sep 11, 2026
141d0ce
docs(SceneDataTransformer): update getResolvedSystemTransformations d…
gtk-grafana Sep 11, 2026
39c9828
refactor(VizPanel): add getPluginAsync, _importPlugin
gtk-grafana Sep 11, 2026
1c55fd9
fix(SceneDataTransformer): _discoverProvider not always reprocessing …
gtk-grafana Sep 11, 2026
05aad61
fix(SceneDataTransformer): last pass recorded before transform settles
gtk-grafana Sep 11, 2026
7170dbf
chore: remove
gtk-grafana Sep 11, 2026
77c6cec
fix: remove unused, update docs, check if provider exists before repr…
gtk-grafana Sep 14, 2026
7d30c21
chore(system-transforms): remove unused getSystemTransformations tran…
gtk-grafana Sep 14, 2026
900c38e
fix(system-transformations): clear transforms from stale provider aft…
gtk-grafana Sep 14, 2026
846df9f
chore: simplify narrowing with isSystemTransformationsProvider flag
gtk-grafana Sep 16, 2026
c710747
chore(SystemTransformProvider): mark flags readonly
gtk-grafana Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
384 changes: 383 additions & 1 deletion packages/scenes/src/components/VizPanel/VizPanel.test.tsx

Large diffs are not rendered by default.

191 changes: 175 additions & 16 deletions packages/scenes/src/components/VizPanel/VizPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { t } from '@grafana/i18n';
import {
AbsoluteTimeRange,
DataFrame,
DataTopic,
DataTransformerConfig,
FieldConfigSource,
PanelModel,
PanelPlugin,
Expand All @@ -22,7 +25,7 @@ import { PanelContext, SeriesVisibilityChangeMode, VizLegendOptions } from '@gra
import { config, getAppEvents, getPluginImportUtils } from '@grafana/runtime';
import { SceneObjectBase } from '../../core/SceneObjectBase';
import { sceneGraph } from '../../core/sceneGraph';
import { DeepPartial, SceneObject, SceneObjectState } from '../../core/types';
import { CustomTransformerDefinition, DeepPartial, SceneObject, SceneObjectState } from '../../core/types';

import { VizPanelRenderer } from './VizPanelRenderer';
import { VizPanelMenu } from './VizPanelMenu';
Expand All @@ -42,6 +45,10 @@ import { registerQueryWithController, wrapPromiseInStateObservable } from '../..
import { SceneDataTransformer } from '../../querying/SceneDataTransformer';
import { SceneQueryRunner } from '../../querying/SceneQueryRunner';
import { buildPathIdFor } from '../../utils/pathId';
import {
SystemTransformationsProvider,
TransformationOrigin,
} from '../../querying/systemTransformations/systemTransformationTypes';

export interface VizPanelState<TOptions = {}, TFieldConfig = {}> extends SceneObjectState {
/**
Expand Down Expand Up @@ -106,6 +113,11 @@ export interface VizPanelState<TOptions = {}, TFieldConfig = {}> extends SceneOb
**/
_UNSAFE_clearPreviousFieldValues?: boolean;

/**
* Whether to check the panel plugin for system transformations. Set by host to gate rollout (feature toggle)
*/
applyPluginTransformations?: boolean;
Comment thread
gtk-grafana marked this conversation as resolved.

/**
* Sets panel chrome collapsed state
*/
Expand All @@ -125,9 +137,10 @@ export interface VizPanelState<TOptions = {}, TFieldConfig = {}> extends SceneOb
_renderCounter?: number;
}

export class VizPanel<TOptions = {}, TFieldConfig extends {} = {}> extends SceneObjectBase<
VizPanelState<TOptions, TFieldConfig>
> {
export class VizPanel<TOptions = {}, TFieldConfig extends {} = {}>
extends SceneObjectBase<VizPanelState<TOptions, TFieldConfig>>
implements SystemTransformationsProvider
{
public static Component = VizPanelRenderer;

protected _variableDependency = new VariableDependencyConfig(this, {
Expand All @@ -140,7 +153,13 @@ export class VizPanel<TOptions = {}, TFieldConfig extends {} = {}> extends Scene

// Not part of state as this is not serializable
protected _panelContext?: PanelContext;
/**
* The plugin this panel has adopted: options have been migrated and defaulted against it, so it
* deliberately lags state.pluginId while a viz type change is still loading.
*/
private _plugin?: PanelPlugin;
/** The in-flight import, so concurrent callers share one. */
private _pluginImport?: { pluginId: string; promise: Promise<PanelPlugin> };
private _prevData?: PanelData;
private _dataWithFieldConfig?: PanelData;
private _structureRev = 0;
Expand Down Expand Up @@ -207,22 +226,11 @@ export class VizPanel<TOptions = {}, TFieldConfig extends {} = {}> extends Scene
endPluginLoadCallback?.(plugin, true);
this._pluginLoaded(plugin, overwriteOptions, overwriteFieldConfig, isAfterPluginChange);
} else {
const { importPanelPlugin } = getPluginImportUtils();

try {
// Start profiling plugin load - get end callback
const endPluginLoadCallback = profiler?.onPluginLoadStart(pluginId);

const panelPromise = importPanelPlugin(pluginId);

const queryControler = sceneGraph.getQueryController(this);
if (queryControler && queryControler.state.enableProfiling) {
wrapPromiseInStateObservable(panelPromise)
.pipe(registerQueryWithController({ type: `VizPanel/loadPlugin/${pluginId}`, origin: this }))
.subscribe(() => {});
}

const result = await panelPromise;
const result = await this._importPlugin(pluginId);

// End profiling plugin load (not from cache)
endPluginLoadCallback?.(result, false);
Expand All @@ -238,6 +246,29 @@ export class VizPanel<TOptions = {}, TFieldConfig extends {} = {}> extends Scene
}
}

/**
* The only place the plugin is imported. Concurrent callers (_loadPlugin, getPluginAsync) share one import,
* and a rejection stays cached so a plugin that fails to load is not retried on every call.
*/
private _importPlugin(pluginId: string): Promise<PanelPlugin> {
if (this._pluginImport?.pluginId === pluginId) {
return this._pluginImport.promise;
}

const promise = getPluginImportUtils().importPanelPlugin(pluginId);

const queryControler = sceneGraph.getQueryController(this);
if (queryControler && queryControler.state.enableProfiling) {
wrapPromiseInStateObservable(promise)
.pipe(registerQueryWithController({ type: `VizPanel/loadPlugin/${pluginId}`, origin: this }))
.subscribe(() => {});
}

this._pluginImport = { pluginId, promise };

return promise;
}

public getLegacyPanelId() {
/**
* The `/` part is here because a panel key can be in a clone chain
Expand Down Expand Up @@ -336,6 +367,18 @@ export class VizPanel<TOptions = {}, TFieldConfig extends {} = {}> extends Scene
pluginId: plugin.meta.id,
});

// The plugin arrives async so the pipeline may have already run a pass without its system
// transformations.
if (
this.state.applyPluginTransformations && // does panel support system transformations?
$data instanceof SceneDataTransformer &&
$data.isActive &&
// the plugin supplies a provider method, or a previous one did and its transformations must come back out
(pluginProvidesSystemTransformations(plugin) || $data.hasAppliedSystemTransformations())
) {
Comment thread
cursor[bot] marked this conversation as resolved.
$data.reprocessTransformations();
}
Comment thread
gtk-grafana marked this conversation as resolved.
Comment thread
gtk-grafana marked this conversation as resolved.

// Non data panels needs to be re-rendered when time range change
if (plugin.meta.skipDataQuery) {
const sceneTimeRange = sceneGraph.getTimeRange(this);
Expand All @@ -347,10 +390,73 @@ export class VizPanel<TOptions = {}, TFieldConfig extends {} = {}> extends Scene
return plugin && plugin.meta.info.version ? plugin.meta.info.version : config.buildInfo.version;
}

/**
* The plugin this panel has adopted, or undefined while it is still loading.
*/
public getPlugin(): PanelPlugin | undefined {
return this._plugin;
}

/**
* The plugin for the current state.pluginId, importing it if it is not loaded yet. Rejects when
* the import does, like the host's importPanelPlugin.
*
* Unlike getPlugin this does not return undefined merely because the import has not finished, and
* it does not wait on the panel being activated. It does not adopt what it resolves: option
* migrations and the state write stay with _loadPlugin, on the render and viz change paths.
*/
public getPluginAsync(): Promise<PanelPlugin> {
const resolved = this._resolvePluginSync();

return resolved ? Promise.resolve(resolved) : this._importPlugin(this.state.pluginId);
}

/**
* The plugin for the current state.pluginId when it is available without importing: the one this
* panel adopted, or one already in the host's cache.
*/
private _resolvePluginSync(): PanelPlugin | undefined {
const { pluginId } = this.state;

return this._plugin?.meta.id === pluginId ? this._plugin : loadPanelPluginSync(pluginId);
}

/**
* @internal
* Marks this panel as a SystemTransformationsProvider; not for app code.
*/
public readonly isSystemTransformationsProvider: true = true;

/**
* @internal
* Identifies this panel's tier in its data pipeline; not for app code.
*/
public readonly origin: TransformationOrigin = 'plugin';

/**
* @internal
* Called by the SceneDataTransformer that discovered this panel with the frames about to enter the pipeline.
*/
public getSystemTransformations({ series }: { series: DataFrame[] }): {
prepend?: Array<DataTransformerConfig | CustomTransformerDefinition>;
append?: Array<DataTransformerConfig | CustomTransformerDefinition>;
} {
if (series.length === 0) {
return {};
}

const plugin = this._pluginForTransformations();

return plugin ? getPluginSystemTransformations(plugin, { series }) : {};
}

/**
* The plugin that contributes the transformations for this panel.
*/
private _pluginForTransformations(): PanelPlugin | undefined {
return this.state.applyPluginTransformations ? this._resolvePluginSync() : undefined;
}

public getPanelContext(): PanelContext {
this._panelContext ??= this.buildPanelContext();

Expand Down Expand Up @@ -730,3 +836,56 @@ function getPanelPluginNotFound(id: string): PanelPlugin {

return plugin;
}

/**
* The subset of PanelPlugin that carries panel plugin defined transformations.
* This is a Grafana 13 addition to PanelPlugin, while scenes builds against
* @grafana/data >= 11.6, so this file has to compile and behave against versions that lack it.
*/
interface PluginWithSystemTransformations {
getSystemTransformations(ctx: { series: DataFrame[] }): {
prepend?: Array<DataTransformerConfig | CustomTransformerDefinition>;
append?: Array<DataTransformerConfig | CustomTransformerDefinition>;
};
}

/**
* Whether the plugin implements the contract
*/
function pluginProvidesSystemTransformations(
plugin: PanelPlugin
): plugin is PanelPlugin & PluginWithSystemTransformations {
// @todo remove type assertion after scenes builds against updated PanelPlugin in Grafana 13
return typeof (plugin as Partial<PluginWithSystemTransformations>).getSystemTransformations === 'function';
}

/**
* Internal helper - resolves system transformations from the plugin
*/
function getPluginSystemTransformations(
plugin: PanelPlugin,
ctx: { series: DataFrame[] }
): {
prepend?: Array<DataTransformerConfig | CustomTransformerDefinition>;
append?: Array<DataTransformerConfig | CustomTransformerDefinition>;
} {
if (!pluginProvidesSystemTransformations(plugin)) {
return {};
}

const { prepend = [], append = [] } = plugin.getSystemTransformations(ctx) ?? {};

// The contract supports the series topic only.
return { prepend: prepend.filter(appliesToSeriesTopic), append: append.filter(appliesToSeriesTopic) };
}

/**
* Internal helper - checks if the transformation applies to series
*/
function appliesToSeriesTopic(transformation: DataTransformerConfig | CustomTransformerDefinition): boolean {
if (typeof transformation === 'function') {
return true;
}

return transformation.topic == null || transformation.topic === DataTopic.Series;
}
16 changes: 11 additions & 5 deletions packages/scenes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ import {
escapeLabelValueInExactSelector,
escapeLabelValueInRegexSelector,
escapeURLDelimiters,
renderPrometheusLabelFilters,
getVariableControlId,
renderPrometheusLabelFilters,
} from './variables/utils';
import {
isAdHocVariable,
isQueryVariable,
isTextBoxVariable,
isConstantVariable,
isCustomVariable,
isDataSourceVariable,
isConstantVariable,
isIntervalVariable,
isGroupByVariable,
isIntervalVariable,
isQueryVariable,
isSwitchVariable,
isTextBoxVariable,
} from './variables/variants/guards';

export * from './core/types';
Expand Down Expand Up @@ -202,3 +202,9 @@ export { getCompareSeriesRefId } from './utils/getCompareSeriesRefId';
export { timeShiftAlignmentProcessor } from './utils/timeShiftAlignmentProcessor';
export { loadResources } from './utils/loadResources';
export { PATH_ID_SEPARATOR } from './utils/pathId';
export type { SystemTransformationsProvider } from './querying/systemTransformations/systemTransformationTypes';
export type { SystemTransformation } from './querying/systemTransformations/systemTransformationTypes';
export type { SystemTransformationPosition } from './querying/systemTransformations/systemTransformationTypes';
export type { TransformationOrigin } from './querying/systemTransformations/systemTransformationTypes';
export type { ResolvedSystemTransformations } from './querying/systemTransformations/systemTransformationTypes';
export { isSystemTransformationsProvider } from './querying/systemTransformations/systemTransformationProvider';
Loading
Loading