Scenes: Add support for shared query results of other panel (#65413)

* Scene: Add support for shared query results of other panel

* Update

* Fixing dashboard
pull/65324/head
Torkel Ödegaard 3 years ago committed by GitHub
parent 4ded937c79
commit 3af8f3246a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 446
      devenv/dev-dashboards/panel-common/shared_queries.json
  2. 18
      public/app/features/scenes/dashboard/DashboardsLoader.test.ts
  3. 44
      public/app/features/scenes/dashboard/DashboardsLoader.ts
  4. 54
      public/app/features/scenes/dashboard/ShareQueryDataProvider.test.ts
  5. 103
      public/app/features/scenes/dashboard/ShareQueryDataProvider.ts
  6. 26
      public/app/features/scenes/dashboard/utils.ts
  7. 1
      public/app/plugins/datasource/dashboard/types.ts
  8. 4
      yarn.lock

@ -3,7 +3,10 @@
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
@ -13,17 +16,69 @@
]
},
"editable": true,
"gnetId": null,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"liveNow": false,
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"fill": 0,
"fillGradient": 6,
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 60,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 6,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "always",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"links": [],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 15,
"w": 12,
@ -31,200 +86,191 @@
"y": 0
},
"id": 2,
"options": {
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"dataLinks": []
},
"percentage": false,
"pointradius": 2,
"points": true,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.5.0-pre",
"targets": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0,100"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,-100,200"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "2.5,3.5,4.5,10.5,20.5,21.5,19.5"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Raw Data Graph",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
"type": "timeseries"
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
{
"datasource": "-- Dashboard --",
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 0
},
"id": 4,
"options": {
"fieldOptions": {
"calcs": ["lastNotNull"],
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"max": 100,
"min": 0,
"thresholds": [
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"override": {},
"values": false
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 0
},
"id": 4,
"options": {
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"pluginVersion": "6.4.0-pre",
"pluginVersion": "9.5.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Last non-null",
"type": "gauge"
},
{
"datasource": "-- Dashboard --",
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 5
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"id": 6,
"options": {
"fieldOptions": {
"calcs": ["min"],
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"max": 100,
"min": 0,
"thresholds": [
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"override": {},
"values": false
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 5
},
"id": 6,
"options": {
"orientation": "auto",
"reduceOptions": {
"calcs": [
"min"
],
"fields": "",
"values": false
},
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"pluginVersion": "6.4.0-pre",
"pluginVersion": "9.5.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "min",
"type": "gauge"
},
{
"datasource": "-- Dashboard --",
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 10
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"id": 5,
"options": {
"displayMode": "basic",
"fieldOptions": {
"calcs": ["max"],
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"max": 200,
"min": 0,
"thresholds": [
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
"color": "green"
},
{
"color": "blue",
@ -235,28 +281,102 @@
"value": 120
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 10
},
"override": {},
"id": 5,
"options": {
"displayMode": "basic",
"minVizHeight": 10,
"minVizWidth": 0,
"orientation": "vertical",
"reduceOptions": {
"calcs": [
"max"
],
"fields": "",
"values": false
},
"orientation": "vertical"
"showUnfilled": true,
"valueMode": "color"
},
"pluginVersion": "6.4.0-pre",
"pluginVersion": "9.5.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Max",
"type": "bargauge"
},
{
"columns": [],
"datasource": "-- Dashboard --",
"fontSize": "100%",
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"decimals": 2,
"displayName": "",
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Time"
},
"properties": [
{
"id": "displayName",
"value": "Time"
},
{
"id": "unit",
"value": "time: YYYY-MM-DD HH:mm:ss"
},
{
"id": "custom.align"
}
]
}
]
},
"gridPos": {
"h": 10,
"w": 24,
@ -264,47 +384,49 @@
"y": 15
},
"id": 8,
"options": {},
"pageSize": null,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sort": {
"col": 0,
"desc": true
"showRowNums": false
},
"styles": [
"pluginVersion": "9.5.0-pre",
"targets": [
{
"alias": "Time",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"pattern": "Time",
"type": "date"
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
{
"alias": "",
"colorMode": null,
"colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
"decimals": 2,
"pattern": "/.*/",
"thresholds": [],
"type": "number",
"unit": "short"
"panelId": 2,
"refId": "A"
}
],
"targets": [
"title": "The data from graph above with seriesToColumns transform",
"transformations": [
{
"panelId": 2,
"refId": "A"
"id": "seriesToColumns",
"options": {
"reducers": []
}
}
],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"transform": "timeseries_to_columns",
"type": "table"
}
],
"schemaVersion": 19,
"refresh": "",
"schemaVersion": 38,
"style": "dark",
"tags": ["gdev", "datasource-test"],
"tags": [
"gdev",
"datasource-test"
],
"templating": {
"list": []
},
@ -313,10 +435,22 @@
"to": "now"
},
"timepicker": {
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "",
"title": "Datasource tests - Shared Queries",
"uid": "ZqZnVvFZz",
"version": 10
"version": 8,
"weekStart": ""
}

@ -13,6 +13,8 @@ import { defaultDashboard, LoadingState, Panel, RowPanel, VariableType } from '@
import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { createPanelJSONFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures';
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
import { DashboardScene } from './DashboardScene';
import {
@ -21,6 +23,7 @@ import {
createSceneVariableFromVariableModel,
DashboardLoader,
} from './DashboardsLoader';
import { ShareQueryDataProvider } from './ShareQueryDataProvider';
describe('DashboardLoader', () => {
describe('when fetching/loading a dashboard', () => {
@ -328,6 +331,21 @@ describe('DashboardLoader', () => {
expect((vizPanelSceneObject.state.body as VizPanel)?.state.displayMode).toEqual('transparent');
expect((vizPanelSceneObject.state.body as VizPanel)?.state.hoverHeader).toEqual(true);
});
it('should handle a dashboard query data source', () => {
const panel = {
title: '',
type: 'test-plugin',
datasource: { uid: SHARED_DASHBOARD_QUERY, type: DASHBOARD_DATASOURCE_PLUGIN_ID },
gridPos: { x: 0, y: 0, w: 12, h: 8 },
transparent: true,
targets: [{ refId: 'A', panelId: 10 }],
};
const vizPanel = createVizPanelFromPanelModel(new PanelModel(panel)).state.body as VizPanel;
expect(vizPanel.state.$data).toBeInstanceOf(ShareQueryDataProvider);
});
});
describe('when creating variables objects', () => {

@ -22,13 +22,17 @@ import {
SceneRefreshPicker,
SceneDataTransformer,
SceneGridItem,
SceneDataProvider,
} from '@grafana/scenes';
import { StateManagerBase } from 'app/core/services/StateManagerBase';
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/types';
import { DashboardDTO } from 'app/types';
import { DashboardScene } from './DashboardScene';
import { ShareQueryDataProvider } from './ShareQueryDataProvider';
import { getVizPanelKeyForPanelId } from './utils';
export interface DashboardLoaderState {
dashboard?: DashboardScene;
@ -259,11 +263,6 @@ export function createSceneVariableFromVariableModel(variable: VariableModel): S
}
export function createVizPanelFromPanelModel(panel: PanelModel) {
const queryRunner = new SceneQueryRunner({
queries: panel.targets,
maxDataPoints: panel.maxDataPoints ?? undefined,
});
return new SceneGridItem({
x: panel.gridPos.x,
y: panel.gridPos.y,
@ -272,6 +271,7 @@ export function createVizPanelFromPanelModel(panel: PanelModel) {
isDraggable: true,
isResizable: true,
body: new VizPanel({
key: getVizPanelKeyForPanelId(panel.id),
title: panel.title,
pluginId: panel.type,
options: panel.options ?? {},
@ -280,16 +280,38 @@ export function createVizPanelFromPanelModel(panel: PanelModel) {
displayMode: panel.transparent ? 'transparent' : undefined,
// To be replaced with it's own option persited option instead derived
hoverHeader: !panel.title && !panel.timeFrom && !panel.timeShift,
$data: panel.transformations?.length
? new SceneDataTransformer({
$data: queryRunner,
transformations: panel.transformations,
})
: queryRunner,
$data: createPanelDataProvider(panel),
}),
});
}
export function createPanelDataProvider(panel: PanelModel): SceneDataProvider | undefined {
if (!panel.targets?.length) {
return undefined;
}
let dataProvider: SceneDataProvider | undefined = undefined;
if (panel.datasource?.uid === SHARED_DASHBOARD_QUERY) {
dataProvider = new ShareQueryDataProvider({ query: panel.targets[0] });
} else {
dataProvider = new SceneQueryRunner({
queries: panel.targets,
maxDataPoints: panel.maxDataPoints ?? undefined,
});
}
// Wrap inner data provider in a data transformer
if (panel.transformations?.length) {
dataProvider = new SceneDataTransformer({
$data: dataProvider,
transformations: panel.transformations,
});
}
return dataProvider;
}
let loader: DashboardLoader | null = null;
export function getDashboardLoader(): DashboardLoader {

@ -0,0 +1,54 @@
import { getDefaultTimeRange, LoadingState } from '@grafana/data';
import {
SceneDataNode,
SceneFlexItem,
SceneFlexLayout,
sceneGraph,
SceneObjectBase,
SceneObjectState,
} from '@grafana/scenes';
import { ShareQueryDataProvider } from './ShareQueryDataProvider';
import { activateFullSceneTree, getVizPanelKeyForPanelId } from './utils';
export class SceneDummyPanel extends SceneObjectBase<SceneObjectState> {}
describe('ShareQueryDataProvider', () => {
it('Should find and subscribe to another VizPanels data provider', () => {
const panel = new SceneDummyPanel({
key: getVizPanelKeyForPanelId(2),
$data: new ShareQueryDataProvider({
query: { refId: 'A', panelId: 1 },
}),
});
const sourceData = new SceneDataNode({
data: {
series: [],
state: LoadingState.Done,
timeRange: getDefaultTimeRange(),
structureRev: 11,
},
});
const scene = new SceneFlexLayout({
children: [
new SceneFlexItem({
body: new SceneDummyPanel({
key: getVizPanelKeyForPanelId(1),
$data: sourceData,
}),
}),
new SceneFlexItem({ body: panel }),
],
});
activateFullSceneTree(scene);
expect(sceneGraph.getData(panel).state.data?.structureRev).toBe(11);
sourceData.setState({ data: { ...sourceData.state.data!, structureRev: 12 } });
expect(sceneGraph.getData(panel).state.data?.structureRev).toBe(12);
});
});

@ -0,0 +1,103 @@
import { Unsubscribable } from 'rxjs';
import {
SceneDataProvider,
SceneDataState,
SceneDataTransformer,
SceneDeactivationHandler,
SceneObject,
SceneObjectBase,
} from '@grafana/scenes';
import { DashboardQuery } from 'app/plugins/datasource/dashboard/types';
import { getVizPanelKeyForPanelId } from './utils';
export interface ShareQueryDataProviderState extends SceneDataState {
query: DashboardQuery;
}
export class ShareQueryDataProvider extends SceneObjectBase<ShareQueryDataProviderState> implements SceneDataProvider {
private _querySub: Unsubscribable | undefined;
private _sourceDataDeactivationHandler?: SceneDeactivationHandler;
public constructor(state: ShareQueryDataProviderState) {
super(state);
this.addActivationHandler(() => {
// TODO handle changes to query model (changed panelId / withTransforms)
//this.subscribeToState(this._onStateChanged);
this._subscribeToSource();
return () => {
if (this._querySub) {
this._querySub.unsubscribe();
}
if (this._sourceDataDeactivationHandler) {
this._sourceDataDeactivationHandler();
}
};
});
}
private _subscribeToSource() {
const { query } = this.state;
if (this._querySub) {
this._querySub.unsubscribe();
}
if (!query.panelId) {
return;
}
const keyToFind = getVizPanelKeyForPanelId(query.panelId);
const source = findObjectInScene(this.getRoot(), (scene: SceneObject) => scene.state.key === keyToFind);
if (!source) {
console.log('Shared dashboard query refers to a panel that does not exist in the scene');
return;
}
let sourceData = source.state.$data;
if (!sourceData) {
console.log('No source data found for shared dashboard query');
return;
}
// This will activate if sourceData is part of hidden panel
// Also make sure the sourceData is not deactivated if hidden later
this._sourceDataDeactivationHandler = sourceData.activate();
if (sourceData instanceof SceneDataTransformer) {
if (!query.withTransforms) {
if (!sourceData.state.$data) {
throw new Error('No source inner query runner found in data transformer');
}
sourceData = sourceData.state.$data;
}
}
this._querySub = sourceData.subscribeToState((state) => this.setState({ data: state.data }));
// Copy the initial state
this.setState({ data: sourceData.state.data });
}
}
export function findObjectInScene(scene: SceneObject, check: (scene: SceneObject) => boolean): SceneObject | null {
if (check(scene)) {
return scene;
}
let found: SceneObject | null = null;
scene.forEachChild((child) => {
let maybe = findObjectInScene(child, check);
if (maybe) {
found = maybe;
}
});
return found;
}

@ -0,0 +1,26 @@
import { SceneDeactivationHandler, SceneObject } from '@grafana/scenes';
export function getVizPanelKeyForPanelId(panelId: number) {
return `panel-${panelId}`;
}
/**
* Useful from tests to simulate mounting a full scene. Children are activated before parents to simulate the real order
* of React mount order and useEffect ordering.
*
*/
export function activateFullSceneTree(scene: SceneObject): SceneDeactivationHandler {
const deactivationHandlers: SceneDeactivationHandler[] = [];
scene.forEachChild((child) => {
deactivationHandlers.push(activateFullSceneTree(child));
});
deactivationHandlers.push(scene.activate());
return () => {
for (const handler of deactivationHandlers) {
handler();
}
};
}

@ -1,6 +1,7 @@
import { DataFrame, DataQuery, DataQueryError, DataTopic } from '@grafana/data';
export const SHARED_DASHBOARD_QUERY = '-- Dashboard --';
export const DASHBOARD_DATASOURCE_PLUGIN_ID = 'dashboard';
export interface DashboardQuery extends DataQuery {
panelId?: number;

@ -3327,7 +3327,7 @@ __metadata:
linkType: soft
"@grafana/scenes@npm:^0.3.0":
version: 0.3.0
version: 0.0.0-use.local
resolution: "@grafana/scenes@npm:0.3.0"
dependencies:
"@grafana/e2e-selectors": canary
@ -3338,7 +3338,7 @@ __metadata:
uuid: ^9.0.0
checksum: 3610cedcc150b9d6e3d6948056bb1bbbfe58d7fa0ff6e762eec6619bb0940504db867ea87e56160f27e4d93772f6203493bf87d353a1b18d2664e74a03f03a05
languageName: node
linkType: hard
linkType: soft
"@grafana/schema@10.0.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema":
version: 0.0.0-use.local

Loading…
Cancel
Save