The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviou...

635 lines
18 KiB

import { map, of } from 'rxjs';
import {
DataQuery,
DataQueryRequest,
DataSourceApi,
DataSourceJsonData,
DataSourceRef,
LoadingState,
PanelData,
} from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { setPluginImportUtils } from '@grafana/runtime';
import { SceneDataTransformer, SceneGridLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes';
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
import { VizPanelManager } from '../panel-edit/VizPanelManager';
import { activateFullSceneTree } from '../utils/test-utils';
import { DashboardDatasourceBehaviour } from './DashboardDatasourceBehaviour';
import { DashboardGridItem } from './DashboardGridItem';
import { DashboardScene } from './DashboardScene';
import { LibraryVizPanel } from './LibraryVizPanel';
const grafanaDs = {
id: 1,
uid: '-- Grafana --',
name: 'grafana',
type: 'grafana',
meta: {
id: 'grafana',
},
getRef: () => {
return { type: 'grafana', uid: '-- Grafana --' };
},
};
const dashboardDs: DataSourceApi = {
meta: {
id: DASHBOARD_DATASOURCE_PLUGIN_ID,
},
name: SHARED_DASHBOARD_QUERY,
type: SHARED_DASHBOARD_QUERY,
uid: SHARED_DASHBOARD_QUERY,
getRef: () => {
return { type: SHARED_DASHBOARD_QUERY, uid: SHARED_DASHBOARD_QUERY };
},
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>;
setPluginImportUtils({
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
getPanelPluginFromCache: (id: string) => undefined,
});
const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => {
const result: PanelData = {
state: LoadingState.Loading,
series: [],
timeRange: request.range,
request,
};
return of([]).pipe(
map(() => {
result.state = LoadingState.Done;
result.series = [];
return result;
})
);
});
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
return runRequestMock(ds, request);
},
getDataSourceSrv: () => {
return {
get: async (ref: DataSourceRef) => {
if (ref.uid === 'grafana') {
return grafanaDs;
}
if (ref.uid === SHARED_DASHBOARD_QUERY) {
return dashboardDs;
}
return null;
},
getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }),
};
},
}));
describe('DashboardDatasourceBehaviour', () => {
describe('Given scene with a dashboard DS panel and a source panel', () => {
let scene: DashboardScene, sourcePanel: VizPanel, dashboardDSPanel: VizPanel, sceneDeactivate: () => void;
beforeEach(async () => {
({ scene, sourcePanel, dashboardDSPanel, sceneDeactivate } = await buildTestScene());
});
it('Should re-run query of dashboardDS panel when source query re-runs', async () => {
// spy on runQueries that will be called by the behaviour
const spy = jest.spyOn(dashboardDSPanel.state.$data!.state.$data as SceneQueryRunner, 'runQueries');
// deactivate scene to mimic going into panel edit
sceneDeactivate();
// run source panel queries and update request ID
(sourcePanel.state.$data!.state.$data as SceneQueryRunner).runQueries();
await new Promise((r) => setTimeout(r, 1));
// activate scene to mimic coming back from panel edit
activateFullSceneTree(scene);
expect(spy).toHaveBeenCalledTimes(1);
});
it('Should not run query of dashboardDS panel when source panel queries do not change', async () => {
// spy on runQueries
const spy = jest.spyOn(dashboardDSPanel.state.$data!.state.$data as SceneQueryRunner, 'runQueries');
// deactivate scene to mimic going into panel edit
sceneDeactivate();
await new Promise((r) => setTimeout(r, 1));
// activate scene to mimic coming back from panel edit
activateFullSceneTree(scene);
expect(spy).not.toHaveBeenCalled();
});
it('Should not re-run queries in behaviour when adding a dashboardDS panel to the scene', async () => {
const sourcePanel = new VizPanel({
title: 'Panel A',
pluginId: 'table',
key: 'panel-1',
$data: new SceneQueryRunner({
datasource: { uid: 'grafana' },
queries: [{ refId: 'A', queryType: 'randomWalk' }],
}),
});
const behaviour = new DashboardDatasourceBehaviour({});
const dashboardDSPanel = new VizPanel({
title: 'Panel B',
pluginId: 'table',
key: 'panel-2',
$data: new SceneQueryRunner({
datasource: { uid: SHARED_DASHBOARD_QUERY },
queries: [{ refId: 'A', panelId: 1 }],
$behaviors: [behaviour],
}),
});
const scene = new DashboardScene({
title: 'hello',
uid: 'dash-1',
meta: {
canEdit: true,
},
body: new SceneGridLayout({
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: sourcePanel,
}),
],
}),
});
activateFullSceneTree(scene);
await new Promise((r) => setTimeout(r, 1));
const spy = jest.spyOn(dashboardDSPanel.state.$data as SceneQueryRunner, 'runQueries');
const layout = scene.state.body as SceneGridLayout;
// we add the new panel, it should run it's query as usual
layout.setState({
children: [
...layout.state.children,
new DashboardGridItem({
key: 'griditem-2',
x: 0,
y: 0,
width: 10,
height: 12,
body: dashboardDSPanel,
}),
],
});
dashboardDSPanel.activate();
expect(spy).toHaveBeenCalledTimes(1);
// since there is no previous request ID on dashboard load, the behaviour should not re-run queries
expect(behaviour['prevRequestId']).toBeUndefined();
});
it('Should not re-run queries in behaviour on scene load', async () => {
const sourcePanel = new VizPanel({
title: 'Panel A',
pluginId: 'table',
key: 'panel-1',
$data: new SceneQueryRunner({
datasource: { uid: 'grafana' },
queries: [{ refId: 'A', queryType: 'randomWalk' }],
}),
});
const behaviour = new DashboardDatasourceBehaviour({});
const dashboardDSPanel = new VizPanel({
title: 'Panel B',
pluginId: 'table',
key: 'panel-2',
$data: new SceneQueryRunner({
datasource: { uid: SHARED_DASHBOARD_QUERY },
queries: [{ refId: 'A', panelId: 1 }],
$behaviors: [behaviour],
}),
});
const scene = new DashboardScene({
title: 'hello',
uid: 'dash-1',
meta: {
canEdit: true,
},
body: new SceneGridLayout({
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: sourcePanel,
}),
new DashboardGridItem({
key: 'griditem-2',
x: 0,
y: 0,
width: 10,
height: 12,
body: dashboardDSPanel,
}),
],
}),
});
const spy = jest.spyOn(dashboardDSPanel.state.$data as SceneQueryRunner, 'runQueries');
activateFullSceneTree(scene);
await new Promise((r) => setTimeout(r, 1));
expect(spy).toHaveBeenCalledTimes(1);
// since there is no previous request ID on dashboard load, the behaviour should not re-run queries
expect(behaviour['prevRequestId']).toBeUndefined();
});
it('Should exit behaviour early if not in a dashboard scene', async () => {
// spy on runQueries
const spy = jest.spyOn(dashboardDSPanel.state.$data!.state.$data as SceneQueryRunner, 'runQueries');
const vizPanelManager = new VizPanelManager({
panel: dashboardDSPanel.clone(),
$data: dashboardDSPanel.state.$data?.clone(),
sourcePanel: dashboardDSPanel.getRef(),
});
vizPanelManager.activate();
expect(spy).not.toHaveBeenCalled();
});
it('Should not re-run queries if dashboard DS panel references an invalid source panel', async () => {
const sourcePanel = new VizPanel({
title: 'Panel A',
pluginId: 'table',
key: 'panel-1',
$data: new SceneQueryRunner({
datasource: { uid: 'grafana' },
queries: [{ refId: 'A', queryType: 'randomWalk' }],
}),
});
// query references inexistent panel
const dashboardDSPanel = new VizPanel({
title: 'Panel B',
pluginId: 'table',
key: 'panel-2',
$data: new SceneQueryRunner({
datasource: { uid: SHARED_DASHBOARD_QUERY },
queries: [{ refId: 'A', panelId: 10 }],
$behaviors: [new DashboardDatasourceBehaviour({})],
}),
});
const scene = new DashboardScene({
title: 'hello',
uid: 'dash-1',
meta: {
canEdit: true,
},
body: new SceneGridLayout({
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: sourcePanel,
}),
new DashboardGridItem({
key: 'griditem-2',
x: 0,
y: 0,
width: 10,
height: 12,
body: dashboardDSPanel,
}),
],
}),
});
const sceneDeactivate = activateFullSceneTree(scene);
await new Promise((r) => setTimeout(r, 1));
// spy on runQueries
const spy = jest.spyOn(dashboardDSPanel.state.$data as SceneQueryRunner, 'runQueries');
// deactivate scene to mimic going into panel edit
sceneDeactivate();
await new Promise((r) => setTimeout(r, 1));
// activate scene to mimic coming back from panel edit
activateFullSceneTree(scene);
expect(spy).not.toHaveBeenCalled();
});
});
describe('Given scene with no DashboardDS panel', () => {
it('Should not re-run queries and exit early in behaviour', async () => {
const sourcePanel = new VizPanel({
title: 'Panel A',
pluginId: 'table',
key: 'panel-1',
$data: new SceneQueryRunner({
datasource: { uid: 'grafana' },
queries: [{ refId: 'A', queryType: 'randomWalk' }],
}),
});
const anotherPanel = new VizPanel({
title: 'Panel B',
pluginId: 'table',
key: 'panel-2',
$data: new SceneQueryRunner({
datasource: { uid: 'grafana' },
queries: [{ refId: 'A', queryType: 'randomWalk' }],
}),
});
const scene = new DashboardScene({
title: 'hello',
uid: 'dash-1',
meta: {
canEdit: true,
},
body: new SceneGridLayout({
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: sourcePanel,
}),
new DashboardGridItem({
key: 'griditem-2',
x: 0,
y: 0,
width: 10,
height: 12,
body: anotherPanel,
}),
],
}),
});
const sceneDeactivate = activateFullSceneTree(scene);
await new Promise((r) => setTimeout(r, 1));
// spy on runQueries
const spy = jest.spyOn(anotherPanel.state.$data as SceneQueryRunner, 'runQueries');
// deactivate scene to mimic going into panel edit
sceneDeactivate();
// run source panel queries and update request ID
(sourcePanel.state.$data as SceneQueryRunner).runQueries();
await new Promise((r) => setTimeout(r, 1));
// activate scene to mimic coming back from panel edit
activateFullSceneTree(scene);
expect(spy).not.toHaveBeenCalled();
});
});
describe('Given an invalid state', () => {
it('Should throw an error if behaviour is not attached to a SceneQueryRunner', () => {
const behaviour = new DashboardDatasourceBehaviour({});
expect(() => behaviour.activate()).toThrow('DashboardDatasourceBehaviour must be attached to a SceneQueryRunner');
});
it('Should throw an error if source panel does not have a SceneQueryRunner', async () => {
const sourcePanel = new VizPanel({
title: 'Panel A',
pluginId: 'table',
key: 'panel-1',
$data: undefined,
});
const dashboardDSPanel = new VizPanel({
title: 'Panel B',
pluginId: 'table',
key: 'panel-2',
$data: new SceneQueryRunner({
datasource: { uid: SHARED_DASHBOARD_QUERY },
queries: [{ refId: 'A', panelId: 1 }],
$behaviors: [new DashboardDatasourceBehaviour({})],
}),
});
const scene = new DashboardScene({
title: 'hello',
uid: 'dash-1',
meta: {
canEdit: true,
},
body: new SceneGridLayout({
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: sourcePanel,
}),
new DashboardGridItem({
key: 'griditem-2',
x: 0,
y: 0,
width: 10,
height: 12,
body: dashboardDSPanel,
}),
],
}),
});
try {
activateFullSceneTree(scene);
} catch (e) {
expect(e).toEqual(new Error('Could not find SceneQueryRunner for panel'));
}
});
});
describe('Library panels', () => {
it('should wait for library panel to be loaded', async () => {
const sourcePanel = new LibraryVizPanel({
name: 'My Library Panel',
title: 'Panel title',
uid: 'fdcvggvfy2qdca',
panelKey: 'lib-panel',
panel: new VizPanel({
key: 'panel-1',
title: 'Panel A',
pluginId: 'table',
}),
});
// query references inexistent panel
const dashboardDSPanel = new VizPanel({
title: 'Panel B',
pluginId: 'table',
key: 'panel-2',
$data: new SceneQueryRunner({
datasource: { uid: SHARED_DASHBOARD_QUERY },
queries: [{ refId: 'A', panelId: 1 }],
$behaviors: [new DashboardDatasourceBehaviour({})],
}),
});
const scene = new DashboardScene({
title: 'hello',
uid: 'dash-1',
meta: {
canEdit: true,
},
body: new SceneGridLayout({
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: sourcePanel,
}),
new DashboardGridItem({
key: 'griditem-2',
x: 0,
y: 0,
width: 10,
height: 12,
body: dashboardDSPanel,
}),
],
}),
});
activateFullSceneTree(scene);
// spy on runQueries
const spy = jest.spyOn(dashboardDSPanel.state.$data as SceneQueryRunner, 'runQueries');
await new Promise((r) => setTimeout(r, 1));
expect(spy).not.toHaveBeenCalled();
// Simulate library panel being loaded
sourcePanel.setState({
isLoaded: true,
panel: new VizPanel({
title: 'Panel A',
pluginId: 'table',
key: 'panel-1',
$data: new SceneQueryRunner({
datasource: { uid: 'grafana' },
queries: [{ refId: 'A', queryType: 'randomWalk' }],
}),
}),
});
expect(spy).toHaveBeenCalledTimes(1);
});
});
});
async function buildTestScene() {
const sourcePanel = new VizPanel({
title: 'Panel A',
pluginId: 'table',
key: 'panel-1',
$data: new SceneDataTransformer({
transformations: [],
$data: new SceneQueryRunner({
datasource: { uid: 'grafana' },
queries: [{ refId: 'A', queryType: 'randomWalk' }],
}),
}),
});
const dashboardDSPanel = new VizPanel({
title: 'Panel B',
pluginId: 'table',
key: 'panel-2',
$data: new SceneDataTransformer({
transformations: [],
$data: new SceneQueryRunner({
datasource: { uid: SHARED_DASHBOARD_QUERY },
queries: [{ refId: 'A', panelId: 1 }],
$behaviors: [new DashboardDatasourceBehaviour({})],
}),
}),
});
const scene = new DashboardScene({
title: 'hello',
uid: 'dash-1',
meta: {
canEdit: true,
},
body: new SceneGridLayout({
children: [
new DashboardGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: sourcePanel,
}),
new DashboardGridItem({
key: 'griditem-2',
x: 0,
y: 0,
width: 10,
height: 12,
body: dashboardDSPanel,
}),
],
}),
});
const sceneDeactivate = activateFullSceneTree(scene);
await new Promise((r) => setTimeout(r, 1));
return { scene, sourcePanel, dashboardDSPanel, sceneDeactivate };
}