Commit e1924608 by Ryan McKinley Committed by Torkel Ödegaard

DashboardDatasource: reuse query results within a dashboard (#16660)

* move queryRunner to panelModel

* remove isEditing from PanelChrome

* move listener to QueriesTab

* add shared query datasource

* expose getDashboardSrv to react

* no changes to panel chrome

* issue queries when in fullscreen

* moved to regular QueryEditor interface

* moved to regular QueryEditor interface

* lower limit

* add dashboard query

* no changes to editor row

* fix sort order

* fix sort order

* make it an alpha panel

* make panelId a getter

* fix angular constructor

* rename SeriesData to DataFrame

* merge with master

* use series

* add simple tests

* check unsubscribe

* Minor code cleanup, creating Subjects look cheap and does not need to be lazy, simplifies code

* minor refactor

* Minor refacforing, renames

* added test dashboard
parent 8ce509f3
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"fill": 0,
"fillGradient": 6,
"gridPos": {
"h": 15,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"dataLinks": []
},
"percentage": false,
"pointradius": 2,
"points": true,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0,100"
},
{
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,-100,200"
},
{
"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
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"datasource": "-- Dashboard --",
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 0
},
"id": 4,
"options": {
"fieldOptions": {
"calcs": ["lastNotNull"],
"defaults": {
"mappings": [],
"max": 100,
"min": 0,
"thresholds": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"override": {},
"values": false
},
"orientation": "auto",
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"pluginVersion": "6.4.0-pre",
"targets": [
{
"panelId": 2,
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Last non nulll",
"type": "gauge"
},
{
"datasource": "-- Dashboard --",
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 5
},
"id": 6,
"options": {
"fieldOptions": {
"calcs": ["min"],
"defaults": {
"mappings": [],
"max": 100,
"min": 0,
"thresholds": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"override": {},
"values": false
},
"orientation": "auto",
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"pluginVersion": "6.4.0-pre",
"targets": [
{
"panelId": 2,
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "min",
"type": "gauge"
},
{
"datasource": "-- Dashboard --",
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 10
},
"id": 5,
"options": {
"displayMode": "basic",
"fieldOptions": {
"calcs": ["max"],
"defaults": {
"mappings": [],
"max": 200,
"min": 0,
"thresholds": [
{
"color": "green",
"value": null
},
{
"color": "blue",
"value": 40
},
{
"color": "red",
"value": 120
}
]
},
"override": {},
"values": false
},
"orientation": "vertical"
},
"pluginVersion": "6.4.0-pre",
"targets": [
{
"panelId": 2,
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Max",
"type": "bargauge"
},
{
"columns": [],
"datasource": "-- Dashboard --",
"fontSize": "100%",
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 15
},
"id": 8,
"options": {},
"pageSize": null,
"showHeader": true,
"sort": {
"col": 0,
"desc": true
},
"styles": [
{
"alias": "Time",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"pattern": "Time",
"type": "date"
},
{
"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"
}
],
"targets": [
{
"panelId": 2,
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"transform": "timeseries_to_columns",
"type": "table"
}
],
"schemaVersion": 19,
"style": "dark",
"tags": ["gdev", "datasource-test"],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
},
"timezone": "",
"title": "Datasource tests - Shared Queries",
"uid": "ZqZnVvFZz",
"version": 10
}
......@@ -23,6 +23,8 @@ import { LoadingState } from '@grafana/data';
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
import { PanelQueryRunnerFormat } from '../state/PanelQueryRunner';
import { Unsubscribable } from 'rxjs';
import { isSharedDashboardQuery } from 'app/plugins/datasource/dashboard/SharedQueryRunner';
import { DashboardQueryEditor } from 'app/plugins/datasource/dashboard/DashboardQueryEditor';
interface Props {
panel: PanelModel;
......@@ -166,12 +168,13 @@ export class QueriesTab extends PureComponent<Props, State> {
renderToolbar = () => {
const { currentDS, isAddingMixed } = this.state;
const showAddButton = !(isAddingMixed || isSharedDashboardQuery(currentDS.name));
return (
<>
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
<div className="flex-grow-1" />
{!isAddingMixed && (
{showAddButton && (
<button className="btn navbar-button" onClick={this.onAddQueryClick}>
Add Query
</button>
......@@ -236,28 +239,32 @@ export class QueriesTab extends PureComponent<Props, State> {
setScrollTop={this.setScrollTop}
scrollTop={scrollTop}
>
<>
<div className="query-editor-rows">
{panel.targets.map((query, index) => (
<QueryEditorRow
dataSourceValue={query.datasource || panel.datasource}
key={query.refId}
panel={panel}
dashboard={dashboard}
data={data}
query={query}
onChange={query => this.onQueryChange(query, index)}
onRemoveQuery={this.onRemoveQuery}
onAddQuery={this.onAddQuery}
onMoveQuery={this.onMoveQuery}
inMixedMode={currentDS.meta.mixed}
/>
))}
</div>
<PanelOptionsGroup>
<QueryOptions panel={panel} datasource={currentDS} />
</PanelOptionsGroup>
</>
{isSharedDashboardQuery(currentDS.name) ? (
<DashboardQueryEditor panel={panel} panelData={data} onChange={query => this.onQueryChange(query, 0)} />
) : (
<>
<div className="query-editor-rows">
{panel.targets.map((query, index) => (
<QueryEditorRow
dataSourceValue={query.datasource || panel.datasource}
key={query.refId}
panel={panel}
dashboard={dashboard}
data={data}
query={query}
onChange={query => this.onQueryChange(query, index)}
onRemoveQuery={this.onRemoveQuery}
onAddQuery={this.onAddQuery}
onMoveQuery={this.onMoveQuery}
inMixedMode={currentDS.meta.mixed}
/>
))}
</div>
<PanelOptionsGroup>
<QueryOptions panel={panel} datasource={currentDS} />
</PanelOptionsGroup>
</>
)}
</EditorTabBody>
);
}
......
......@@ -326,7 +326,7 @@ export class PanelModel {
getQueryRunner(): PanelQueryRunner {
if (!this.queryRunner) {
this.queryRunner = new PanelQueryRunner();
this.queryRunner = new PanelQueryRunner(this.id);
}
return this.queryRunner;
}
......
import { PanelQueryRunner } from './PanelQueryRunner';
import { PanelQueryRunner, QueryRunnerOptions } from './PanelQueryRunner';
import { PanelData, DataQueryRequest, DataStreamObserver, DataStreamState, ScopedVars } from '@grafana/ui';
import { LoadingState, DataFrameHelper } from '@grafana/data';
import { dateTime } from '@grafana/data';
import { SHARED_DASHBODARD_QUERY } from 'app/plugins/datasource/dashboard/SharedQueryRunner';
import { DashboardQuery } from 'app/plugins/datasource/dashboard/types';
import { PanelModel } from './PanelModel';
import { Subject } from 'rxjs';
jest.mock('app/core/services/backend_srv');
// Defined within setup functions
const panelsForCurrentDashboardMock: { [key: number]: PanelModel } = {};
jest.mock('app/features/dashboard/services/DashboardSrv', () => ({
getDashboardSrv: () => {
return {
getCurrent: () => {
return {
getPanelById: (id: number) => {
return panelsForCurrentDashboardMock[id];
},
};
},
};
},
}));
interface ScenarioContext {
setup: (fn: () => void) => void;
// Options used in setup
maxDataPoints?: number | null;
widthPixels: number;
dsInterval?: string;
minInterval?: string;
scopedVars: ScopedVars;
// Filled in by the Scenario runner
events?: PanelData[];
res?: PanelData;
queryCalledWith?: DataQueryRequest;
observer: DataStreamObserver;
runner: PanelQueryRunner;
scopedVars: ScopedVars;
}
type ScenarioFn = (ctx: ScenarioContext) => void;
......@@ -31,7 +55,7 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
scopedVars: {
server: { text: 'Server1', value: 'server-1' },
},
runner: new PanelQueryRunner(),
runner: new PanelQueryRunner(1),
observer: (args: any) => {},
setup: (fn: () => void) => {
setupFn = fn;
......@@ -39,7 +63,7 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
};
const response: any = {
data: [{ target: 'hello', datapoints: [] }],
data: [{ target: 'hello', datapoints: [[1, 1000], [2, 2000]] }],
};
beforeEach(async () => {
......@@ -67,17 +91,24 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
to: dateTime(),
raw: { from: '1h', to: 'now' },
},
panelId: 0,
panelId: 1,
queries: [{ refId: 'A', test: 1 }],
};
ctx.runner = new PanelQueryRunner();
ctx.runner = new PanelQueryRunner(1);
ctx.runner.subscribe({
next: (data: PanelData) => {
ctx.events.push(data);
},
});
panelsForCurrentDashboardMock[1] = {
id: 1,
getQueryRunner: () => {
return ctx.runner;
},
} as PanelModel;
ctx.events = [];
ctx.res = await ctx.runner.run(args);
});
......@@ -201,4 +232,60 @@ describe('PanelQueryRunner', () => {
expect(isUnsubbed).toBe(true);
});
});
describeQueryRunnerScenario('Shared query request', ctx => {
ctx.setup(() => {});
it('should get the same results as the original', async () => {
// Get the results from
const q: DashboardQuery = { refId: 'Z', panelId: 1 };
const myPanelId = 7;
const runnerWantingSharedResults = new PanelQueryRunner(myPanelId);
panelsForCurrentDashboardMock[myPanelId] = {
id: myPanelId,
getQueryRunner: () => {
return runnerWantingSharedResults;
},
} as PanelModel;
const res = await runnerWantingSharedResults.run({
datasource: SHARED_DASHBODARD_QUERY,
queries: [q],
// Same query setup
scopedVars: ctx.scopedVars,
minInterval: ctx.minInterval,
widthPixels: ctx.widthPixels,
maxDataPoints: ctx.maxDataPoints,
timeRange: {
from: dateTime().subtract(1, 'days'),
to: dateTime(),
raw: { from: '1h', to: 'now' },
},
panelId: myPanelId, // Not 1
});
const req = res.request;
expect(req.panelId).toBe(1); // The source panel
expect(req.targets[0].datasource).toBe('TestDB');
expect(res.series.length).toBe(1);
expect(res.series[0].length).toBe(2);
// Get the private subject and check that someone is listening
const subject = (ctx.runner as any).subject as Subject<PanelData>;
expect(subject.observers.length).toBe(2);
// Now change the query and we should stop listening
try {
runnerWantingSharedResults.run({
datasource: 'unknown-datasource',
panelId: myPanelId, // Not 1
} as QueryRunnerOptions);
} catch {}
// runnerWantingSharedResults subject is now unsubscribed
// the test listener is still subscribed
expect(subject.observers.length).toBe(1);
});
});
});
......@@ -8,6 +8,7 @@ import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import kbn from 'app/core/utils/kbn';
import templateSrv from 'app/features/templating/template_srv';
import { PanelQueryState } from './PanelQueryState';
import { isSharedDashboardQuery, SharedQueryRunner } from 'app/plugins/datasource/dashboard/SharedQueryRunner';
// Types
import { PanelData, DataQuery, ScopedVars, DataQueryRequest, DataSourceApi, DataSourceJsonData } from '@grafana/ui';
......@@ -49,8 +50,16 @@ export class PanelQueryRunner {
private state = new PanelQueryState();
constructor() {
// Listen to another panel for changes
private sharedQueryRunner: SharedQueryRunner;
constructor(private panelId: number) {
this.state.onStreamingDataUpdated = this.onStreamingDataUpdated;
this.subject = new Subject();
}
getPanelId() {
return this.panelId;
}
/**
......@@ -58,10 +67,6 @@ export class PanelQueryRunner {
* the results will be immediatly passed to the observer
*/
subscribe(observer: PartialObserver<PanelData>, format = PanelQueryRunnerFormat.frames): Unsubscribable {
if (!this.subject) {
this.subject = new Subject(); // Delay creating a subject until someone is listening
}
if (format === PanelQueryRunnerFormat.legacy) {
this.state.sendLegacy = true;
} else if (format === PanelQueryRunnerFormat.both) {
......@@ -79,11 +84,25 @@ export class PanelQueryRunner {
return this.subject.subscribe(observer);
}
async run(options: QueryRunnerOptions): Promise<PanelData> {
if (!this.subject) {
this.subject = new Subject();
/**
* Subscribe one runner to another
*/
chain(runner: PanelQueryRunner): Unsubscribable {
const { sendLegacy, sendFrames } = runner.state;
let format = sendFrames ? PanelQueryRunnerFormat.frames : PanelQueryRunnerFormat.legacy;
if (sendLegacy) {
format = PanelQueryRunnerFormat.both;
}
return this.subscribe(runner.subject, format);
}
getCurrentData(): PanelData {
return this.state.validateStreamsAndGetPanelData();
}
async run(options: QueryRunnerOptions): Promise<PanelData> {
const { state } = this;
const {
......@@ -102,6 +121,17 @@ export class PanelQueryRunner {
delayStateNotification,
} = options;
// Support shared queries
if (isSharedDashboardQuery(datasource)) {
if (!this.sharedQueryRunner) {
this.sharedQueryRunner = new SharedQueryRunner(this);
}
return this.sharedQueryRunner.process(options);
} else if (this.sharedQueryRunner) {
this.sharedQueryRunner.disconnect();
this.sharedQueryRunner = null;
}
const request: DataQueryRequest = {
requestId: getNextRequestId(),
timezone,
......
import * as graphitePlugin from 'app/plugins/datasource/graphite/module';
import * as cloudwatchPlugin from 'app/plugins/datasource/cloudwatch/module';
import * as dashboardDSPlugin from 'app/plugins/datasource/dashboard/module';
import * as elasticsearchPlugin from 'app/plugins/datasource/elasticsearch/module';
import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module';
import * as grafanaPlugin from 'app/plugins/datasource/grafana/module';
......@@ -39,6 +40,7 @@ import * as exampleApp from 'app/plugins/app/example-app/module';
const builtInPlugins: any = {
'app/plugins/datasource/graphite/module': graphitePlugin,
'app/plugins/datasource/cloudwatch/module': cloudwatchPlugin,
'app/plugins/datasource/dashboard/module': dashboardDSPlugin,
'app/plugins/datasource/elasticsearch/module': elasticsearchPlugin,
'app/plugins/datasource/opentsdb/module': opentsdbPlugin,
'app/plugins/datasource/grafana/module': grafanaPlugin,
......
......@@ -125,8 +125,10 @@ export class DatasourceSrv implements DataSourceService {
//Make sure grafana and mixed are sorted at the bottom
if (value.meta.id === 'grafana') {
metricSource.sort = String.fromCharCode(253);
} else if (value.meta.id === 'mixed') {
} else if (value.meta.id === 'dashboard') {
metricSource.sort = String.fromCharCode(254);
} else if (value.meta.id === 'mixed') {
metricSource.sort = String.fromCharCode(255);
}
metricSources.push(metricSource);
......
// Libraries
import React, { PureComponent } from 'react';
// Types
import { Select, DataQuery, DataQueryError, PanelData } from '@grafana/ui';
import { DataFrame, SelectableValue } from '@grafana/data';
import { DashboardQuery } from './types';
import config from 'app/core/config';
import { css } from 'emotion';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { PanelModel } from 'app/features/dashboard/state';
import { SHARED_DASHBODARD_QUERY } from './SharedQueryRunner';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { filterPanelDataToQuery } from 'app/features/dashboard/panel_editor/QueryEditorRow';
type ResultInfo = {
img: string; // The Datasource
refId: string;
query: string; // As text
data: DataFrame[];
error?: DataQueryError;
};
function getQueryDisplayText(query: DataQuery): string {
return JSON.stringify(query);
}
interface Props {
panel: PanelModel;
panelData: PanelData;
onChange: (query: DashboardQuery) => void;
}
type State = {
defaultDatasource: string;
results: ResultInfo[];
};
export class DashboardQueryEditor extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
defaultDatasource: '',
results: [],
};
}
getQuery(): DashboardQuery {
const { panel } = this.props;
return panel.targets[0] as DashboardQuery;
}
async componentDidMount() {
this.componentDidUpdate(null);
}
async componentDidUpdate(prevProps: Props) {
const { panelData } = this.props;
if (!prevProps || prevProps.panelData !== panelData) {
const query = this.props.panel.targets[0] as DashboardQuery;
const defaultDS = await getDatasourceSrv().get(null);
const dashboard = getDashboardSrv().getCurrent();
const panel = dashboard.getPanelById(query.panelId);
if (!panel) {
this.setState({ defaultDatasource: defaultDS.name });
return;
}
const mainDS = await getDatasourceSrv().get(panel.datasource);
const info: ResultInfo[] = [];
for (const query of panel.targets) {
const ds = query.datasource ? await getDatasourceSrv().get(query.datasource) : mainDS;
const fmt = ds.getQueryDisplayText ? ds.getQueryDisplayText : getQueryDisplayText;
const qData = filterPanelDataToQuery(panelData, query.refId);
const queryData = qData ? qData : panelData;
info.push({
refId: query.refId,
query: fmt(query),
img: ds.meta.info.logos.small,
data: queryData.series,
error: queryData.error,
});
}
this.setState({ defaultDatasource: defaultDS.name, results: info });
}
}
onPanelChanged = (id: number) => {
const { onChange } = this.props;
const query = this.getQuery();
query.panelId = id;
onChange(query);
// Update the
this.props.panel.refresh();
};
renderQueryData(editURL: string) {
const { results } = this.state;
return (
<div>
{results.map((target, index) => {
return (
<div className="query-editor-row__header" key={index}>
<div className="query-editor-row__ref-id">
<img src={target.img} width={16} className={css({ marginRight: '8px' })} />
{target.refId}:
</div>
<div className="query-editor-row__collapsed-text">
<a href={editURL}>
{target.query}
&nbsp;
<i className="fa fa-external-link" />
</a>
</div>
</div>
);
})}
</div>
);
}
getPanelDescription = (panel: PanelModel): string => {
const { defaultDatasource } = this.state;
const dsname = panel.datasource ? panel.datasource : defaultDatasource;
if (panel.targets.length === 1) {
return '1 query to ' + dsname;
}
return panel.targets.length + ' queries to ' + dsname;
};
render() {
const dashboard = getDashboardSrv().getCurrent();
const query = this.getQuery();
let selected: SelectableValue<number>;
const panels: Array<SelectableValue<number>> = [];
for (const panel of dashboard.panels) {
if (panel.targets && panel.datasource !== SHARED_DASHBODARD_QUERY) {
const plugin = config.panels[panel.type];
const item = {
value: panel.id,
label: panel.title ? panel.title : 'Panel ' + panel.id,
description: this.getPanelDescription(panel),
imgUrl: plugin.info.logos.small,
};
panels.push(item);
if (query.panelId === panel.id) {
selected = item;
}
}
}
if (panels.length < 1) {
return (
<div className={css({ padding: '10px' })}>
This dashboard does not have other panels. Add queries to other panels and try again
</div>
);
}
// Same as current URL, but different panelId
const editURL = `d/${dashboard.uid}/${dashboard.title}?&fullscreen&edit&panelId=${query.panelId}`;
return (
<div>
<div className="gf-form">
<div className="gf-form-label">Use results from panel</div>
<Select
placeholder="Choose Panel"
isSearchable={true}
options={panels}
value={selected}
onChange={item => this.onPanelChanged(item.value)}
/>
</div>
<div className={css({ padding: '16px' })}>{query.panelId && this.renderQueryData(editURL)}</div>
</div>
);
}
}
# Dashboard Datasource - Native Plugin
This is a **built in** datasource that lets you reuse the query from other panels in the
same dashboard.
\ No newline at end of file
import { isSharedDashboardQuery } from './SharedQueryRunner';
import { DataSourceApi } from '@grafana/ui';
describe('SharedQueryRunner', () => {
it('should identify shared queries', () => {
expect(isSharedDashboardQuery('-- Dashboard --')).toBe(true);
expect(isSharedDashboardQuery('')).toBe(false);
expect(isSharedDashboardQuery(undefined)).toBe(false);
expect(isSharedDashboardQuery(null)).toBe(false);
const ds = {
meta: {
name: '-- Dashboard --',
},
} as DataSourceApi;
expect(isSharedDashboardQuery(ds)).toBe(true);
ds.meta.name = 'something else';
expect(isSharedDashboardQuery(ds)).toBe(false);
});
});
import { DataSourceApi, DataQuery, PanelData } from '@grafana/ui';
import { PanelQueryRunner, QueryRunnerOptions } from 'app/features/dashboard/state/PanelQueryRunner';
import { toDataQueryError } from 'app/features/dashboard/state/PanelQueryState';
import { DashboardQuery } from './types';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { Unsubscribable } from 'rxjs';
import { PanelModel } from 'app/features/dashboard/state';
import { LoadingState } from '@grafana/data';
export const SHARED_DASHBODARD_QUERY = '-- Dashboard --';
export function isSharedDashboardQuery(datasource: string | DataSourceApi) {
if (!datasource) {
// default datasource
return false;
}
if (datasource === SHARED_DASHBODARD_QUERY) {
return true;
}
const ds = datasource as DataSourceApi;
return ds.meta && ds.meta.name === SHARED_DASHBODARD_QUERY;
}
export class SharedQueryRunner {
private containerPanel: PanelModel;
private listenToPanelId: number;
private listenToPanel: PanelModel;
private listenToRunner: PanelQueryRunner;
private subscription: Unsubscribable;
constructor(private runner: PanelQueryRunner) {
this.containerPanel = getDashboardSrv()
.getCurrent()
.getPanelById(runner.getPanelId());
}
process(options: QueryRunnerOptions): Promise<PanelData> {
const panelId = getPanelIdFromQuery(options.queries);
if (!panelId) {
this.disconnect();
return getQueryError('Missing panel reference ID');
}
// The requested panel changed
if (this.listenToPanelId !== panelId) {
this.disconnect();
this.listenToPanel = getDashboardSrv()
.getCurrent()
.getPanelById(panelId);
if (!this.listenToPanel) {
return getQueryError('Unknown Panel: ' + panelId);
}
this.listenToPanelId = panelId;
this.listenToRunner = this.listenToPanel.getQueryRunner();
this.subscription = this.listenToRunner.chain(this.runner);
console.log('Connecting panel: ', this.containerPanel.id, 'to:', this.listenToPanelId);
}
// If the target has refreshed recently, use the exising data
const data = this.listenToRunner.getCurrentData();
if (data.request && data.request.startTime) {
const elapsed = Date.now() - data.request.startTime;
if (elapsed < 150) {
return Promise.resolve(data);
}
}
// When fullscreen run with the current panel settings
if (this.containerPanel.fullscreen) {
const { datasource, targets } = this.listenToPanel;
const modified = {
...options,
panelId,
datasource,
queries: targets,
};
return this.listenToRunner.run(modified);
} else {
this.listenToPanel.refresh();
}
return Promise.resolve(data);
}
disconnect() {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = null;
}
if (this.listenToPanel) {
this.listenToPanel = null;
}
this.listenToPanelId = undefined;
}
}
function getPanelIdFromQuery(queries: DataQuery[]): number | undefined {
if (!queries || !queries.length) {
return undefined;
}
return (queries[0] as DashboardQuery).panelId;
}
function getQueryError(msg: string): Promise<PanelData> {
return Promise.resolve({
state: LoadingState.Error,
series: [],
legacy: [],
error: toDataQueryError(msg),
});
}
import { DataSourceApi, DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings } from '@grafana/ui';
import { DashboardQuery } from './types';
/**
* This should not really be called
*/
export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
constructor(instanceSettings: DataSourceInstanceSettings) {
super(instanceSettings);
}
getCollapsedText(query: DashboardQuery) {
return `Dashboard Reference: ${query.panelId}`;
}
query(options: DataQueryRequest<DashboardQuery>): Promise<DataQueryResponse> {
return Promise.reject('This should not be called directly');
}
testDatasource() {
return Promise.resolve({});
}
}
import { DashboardDatasource } from './datasource';
import { DataSourcePlugin } from '@grafana/ui';
export const plugin = new DataSourcePlugin(DashboardDatasource);
{
"type": "datasource",
"name": "-- Dashboard --",
"id": "dashboard",
"state": "alpha",
"builtIn": true,
"metrics": true
}
import { DataQuery } from '@grafana/ui/src/types';
export interface DashboardQuery extends DataQuery {
panelId?: number;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment