From 9b2b9d9c145cab3875eed94835b68df52c480d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 20 Jan 2026 14:44:37 +0100 Subject: [PATCH] Repeating viz panel by query runniner on a viz panel --- packages/scenes-app/src/demos/index.ts | 7 ++ .../src/demos/panelRepeaterByProcessor.tsx | 100 ++++++++++++++++++ .../scenes/src/querying/SceneQueryRunner.ts | 7 +- 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 packages/scenes-app/src/demos/panelRepeaterByProcessor.tsx diff --git a/packages/scenes-app/src/demos/index.ts b/packages/scenes-app/src/demos/index.ts index 2170c9734..031e99295 100644 --- a/packages/scenes-app/src/demos/index.ts +++ b/packages/scenes-app/src/demos/index.ts @@ -43,6 +43,7 @@ import { getSceneGraphEventsDemo } from './sceneGraphEvents'; import { getSeriesLimitTest } from './seriesLimit'; import { getScopesDemo } from './scopesDemo'; import { getVariableWithObjectValuesDemo } from './variableWithObjectValuesDemo'; +import { getPanelRepeaterByProcessorDemo } from './panelRepeaterByProcessor'; export interface DemoDescriptor { title: string; @@ -83,6 +84,12 @@ export function getDemos(): DemoDescriptor[] { getPage: getPanelRepeaterTest, getSourceCodeModule: () => import('!!raw-loader!../demos/panelRepeater'), }, + { + title: 'Repeat layout by series (using data processor)', + description: 'Here we use repeat a panel using a data processor', + getPage: getPanelRepeaterByProcessorDemo, + getSourceCodeModule: () => import('!!raw-loader!../demos/panelRepeaterByProcessor'), + }, { title: 'Repeat layout by variable', description: 'Test of repeating layout by variable', diff --git a/packages/scenes-app/src/demos/panelRepeaterByProcessor.tsx b/packages/scenes-app/src/demos/panelRepeaterByProcessor.tsx new file mode 100644 index 000000000..2223f3889 --- /dev/null +++ b/packages/scenes-app/src/demos/panelRepeaterByProcessor.tsx @@ -0,0 +1,100 @@ +import { + EmbeddedScene, + PanelBuilders, + SceneAppPage, + SceneAppPageState, + SceneCSSGridLayout, + SceneDataNode, + SceneQueryRunner, + SceneToolbarInput, + VizPanel, + sceneGraph, +} from '@grafana/scenes'; +import { getEmbeddedSceneDefaults } from './utils'; +import { DATASOURCE_REF } from '../constants'; +import { LoadingState, PanelData, getFrameDisplayName } from '@grafana/data'; + +export function getPanelRepeaterByProcessorDemo(defaults: SceneAppPageState) { + const queryRunner = new SceneQueryRunner({ + dataProcessor: repeater, + queries: [ + { + refId: 'A', + datasource: DATASOURCE_REF, + scenarioId: 'random_walk', + seriesCount: 4, + alias: '__server_names', + }, + ], + }); + + return new SceneAppPage({ + ...defaults, + getScene: () => { + return new EmbeddedScene({ + ...getEmbeddedSceneDefaults(), + body: new SceneCSSGridLayout({ + children: [ + PanelBuilders.timeseries() + .setTitle('Panel') + .setOption('legend', { showLegend: false }) + .setData(queryRunner) + .build(), + ], + }), + controls: [ + new SceneToolbarInput({ + value: '4', + label: 'Series count', + onChange: (newValue) => { + queryRunner.setState({ + queries: [ + { + ...queryRunner.state.queries[0], + seriesCount: newValue, + }, + ], + }); + queryRunner.runQueries(); + }, + }), + ...getEmbeddedSceneDefaults().controls, + ], + }); + }, + }); +} + +function repeater(queryRunner: SceneQueryRunner, data: PanelData) { + const layout = sceneGraph.getAncestor(queryRunner, SceneCSSGridLayout); + const sourcePanel = queryRunner.parent as VizPanel; + const children: VizPanel[] = [sourcePanel]; + let returnData = data; + + if (data.state === LoadingState.Loading) { + return returnData; + } + + for (let seriesIndex = 0; seriesIndex < data.series.length; seriesIndex++) { + if (seriesIndex === 0) { + sourcePanel.setState({ title: getFrameDisplayName(data.series[seriesIndex], seriesIndex) }); + returnData = { ...data, series: [data.series[seriesIndex]] }; + continue; + } + + const clone = sourcePanel.clone({ + title: getFrameDisplayName(data.series[seriesIndex], seriesIndex), + $data: new SceneDataNode({ + data: { + ...data, + series: [data.series[seriesIndex]], + }, + }), + }); + + children.push(clone); + } + + layout.setState({ children }); + return returnData; +} diff --git a/packages/scenes/src/querying/SceneQueryRunner.ts b/packages/scenes/src/querying/SceneQueryRunner.ts index dc0fb3cf9..ee9d1ab6e 100644 --- a/packages/scenes/src/querying/SceneQueryRunner.ts +++ b/packages/scenes/src/querying/SceneQueryRunner.ts @@ -72,6 +72,7 @@ export interface QueryRunnerState extends SceneObjectState { runQueriesMode?: 'auto' | 'manual'; // Filters to be applied to data layer results before combining them with SQR results dataLayerFilter?: DataLayerFilter; + dataProcessor?: (queryRunner: SceneQueryRunner, data: PanelData) => PanelData; /** * Optional prefix for the requestId. When set, request IDs will be formatted as `{requestIdPrefix}{counter}`. * Useful for identifying requests from specific panels or components. @@ -620,7 +621,7 @@ export class SceneQueryRunner extends SceneObjectBase implemen this._resultAnnotations = data.annotations; // Will combine annotations & alert state from data layer providers - const dataWithLayersApplied = this._combineDataLayers(preProcessedData); + let dataWithLayersApplied = this._combineDataLayers(preProcessedData); let hasFetchedData = this.state._hasFetchedData; @@ -628,6 +629,10 @@ export class SceneQueryRunner extends SceneObjectBase implemen hasFetchedData = true; } + if (this.state.dataProcessor) { + dataWithLayersApplied = this.state.dataProcessor(this, dataWithLayersApplied); + } + this.setState({ data: dataWithLayersApplied, _hasFetchedData: hasFetchedData }); this._results.next({ origin: this, data: dataWithLayersApplied }); };