mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
8ce509f3b4
commit
e1924608a2
322
devenv/dev-dashboards/panel-common/shared_queries.json
Normal file
322
devenv/dev-dashboards/panel-common/shared_queries.json
Normal file
@ -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
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -1,23 +1,47 @@
|
||||
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,
|
||||
|
@ -1,5 +1,6 @@
|
||||
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);
|
||||
|
193
public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx
Normal file
193
public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
4
public/app/plugins/datasource/dashboard/README.md
Normal file
4
public/app/plugins/datasource/dashboard/README.md
Normal file
@ -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);
|
||||
});
|
||||
});
|
115
public/app/plugins/datasource/dashboard/SharedQueryRunner.ts
Normal file
115
public/app/plugins/datasource/dashboard/SharedQueryRunner.ts
Normal file
@ -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),
|
||||
});
|
||||
}
|
23
public/app/plugins/datasource/dashboard/datasource.ts
Normal file
23
public/app/plugins/datasource/dashboard/datasource.ts
Normal file
@ -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({});
|
||||
}
|
||||
}
|
4
public/app/plugins/datasource/dashboard/module.ts
Normal file
4
public/app/plugins/datasource/dashboard/module.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { DashboardDatasource } from './datasource';
|
||||
import { DataSourcePlugin } from '@grafana/ui';
|
||||
|
||||
export const plugin = new DataSourcePlugin(DashboardDatasource);
|
9
public/app/plugins/datasource/dashboard/plugin.json
Normal file
9
public/app/plugins/datasource/dashboard/plugin.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "-- Dashboard --",
|
||||
"id": "dashboard",
|
||||
"state": "alpha",
|
||||
|
||||
"builtIn": true,
|
||||
"metrics": true
|
||||
}
|
5
public/app/plugins/datasource/dashboard/types.ts
Normal file
5
public/app/plugins/datasource/dashboard/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { DataQuery } from '@grafana/ui/src/types';
|
||||
|
||||
export interface DashboardQuery extends DataQuery {
|
||||
panelId?: number;
|
||||
}
|
Loading…
Reference in New Issue
Block a user