Browse Source

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
Ryan McKinley 6 năm trước cách đây
mục cha
commit
e1924608a2

+ 322 - 0
devenv/dev-dashboards/panel-common/shared_queries.json

@@ -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
+}

+ 30 - 23
public/app/features/dashboard/panel_editor/QueriesTab.tsx

@@ -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>
     );
   }

+ 1 - 1
public/app/features/dashboard/state/PanelModel.ts

@@ -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;
   }

+ 93 - 6
public/app/features/dashboard/state/PanelQueryRunner.test.ts

@@ -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);
+    });
+  });
 });

+ 38 - 8
public/app/features/dashboard/state/PanelQueryRunner.ts

@@ -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,

+ 2 - 0
public/app/features/plugins/built_in_plugins.ts

@@ -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,

+ 3 - 1
public/app/features/plugins/datasource_srv.ts

@@ -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 - 0
public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx

@@ -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}
+                  &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>
+    );
+  }
+}

+ 4 - 0
public/app/plugins/datasource/dashboard/README.md

@@ -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.

+ 22 - 0
public/app/plugins/datasource/dashboard/SharedQueryRunner.test.ts

@@ -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 - 0
public/app/plugins/datasource/dashboard/SharedQueryRunner.ts

@@ -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 - 0
public/app/plugins/datasource/dashboard/datasource.ts

@@ -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 - 0
public/app/plugins/datasource/dashboard/module.ts

@@ -0,0 +1,4 @@
+import { DashboardDatasource } from './datasource';
+import { DataSourcePlugin } from '@grafana/ui';
+
+export const plugin = new DataSourcePlugin(DashboardDatasource);

+ 9 - 0
public/app/plugins/datasource/dashboard/plugin.json

@@ -0,0 +1,9 @@
+{
+  "type": "datasource",
+  "name": "-- Dashboard --",
+  "id": "dashboard",
+  "state": "alpha",
+
+  "builtIn": true,
+  "metrics": true
+}

+ 5 - 0
public/app/plugins/datasource/dashboard/types.ts

@@ -0,0 +1,5 @@
+import { DataQuery } from '@grafana/ui/src/types';
+
+export interface DashboardQuery extends DataQuery {
+  panelId?: number;
+}