mirror of https://github.com/grafana/grafana
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 dashboardpull/18751/head
parent
8ce509f3b4
commit
e1924608a2
@ -0,0 +1,322 @@ |
||||
{ |
||||
"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 |
||||
} |
@ -0,0 +1,193 @@ |
||||
// 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} |
||||
|
||||
<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> |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,4 @@ |
||||
# Dashboard Datasource - Native Plugin |
||||
|
||||
This is a **built in** datasource that lets you reuse the query from other panels in the |
||||
same dashboard. |
@ -0,0 +1,22 @@ |
||||
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); |
||||
}); |
||||
}); |
@ -0,0 +1,115 @@ |
||||
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), |
||||
}); |
||||
} |
@ -0,0 +1,23 @@ |
||||
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({}); |
||||
} |
||||
} |
@ -0,0 +1,4 @@ |
||||
import { DashboardDatasource } from './datasource'; |
||||
import { DataSourcePlugin } from '@grafana/ui'; |
||||
|
||||
export const plugin = new DataSourcePlugin(DashboardDatasource); |
@ -0,0 +1,9 @@ |
||||
{ |
||||
"type": "datasource", |
||||
"name": "-- Dashboard --", |
||||
"id": "dashboard", |
||||
"state": "alpha", |
||||
|
||||
"builtIn": true, |
||||
"metrics": true |
||||
} |
@ -0,0 +1,5 @@ |
||||
import { DataQuery } from '@grafana/ui/src/types'; |
||||
|
||||
export interface DashboardQuery extends DataQuery { |
||||
panelId?: number; |
||||
} |
Loading…
Reference in new issue