Browse Source

Merge pull request #15368 from grafana/15217-panels-without-queries

POC: Panels without queries can skip DataPanel
Torkel Ödegaard 7 years ago
parent
commit
3b0ae4bd0a

+ 1 - 0
pkg/api/frontendsettings.go

@@ -145,6 +145,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
 			"info":         panel.Info,
 			"hideFromList": panel.HideFromList,
 			"sort":         getPanelSort(panel.Id),
+			"dataFormats":  panel.DataFormats,
 		}
 	}
 

+ 5 - 0
pkg/plugins/panel_plugin.go

@@ -4,6 +4,7 @@ import "encoding/json"
 
 type PanelPlugin struct {
 	FrontendPluginBase
+	DataFormats []string `json:"dataFormats"`
 }
 
 func (p *PanelPlugin) Load(decoder *json.Decoder, pluginDir string) error {
@@ -15,6 +16,10 @@ func (p *PanelPlugin) Load(decoder *json.Decoder, pluginDir string) error {
 		return err
 	}
 
+	if p.DataFormats == nil {
+		p.DataFormats = []string{"time_series", "table"}
+	}
+
 	Panels[p.Id] = p
 	return nil
 }

+ 48 - 24
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -10,14 +10,14 @@ import { PanelHeader } from './PanelHeader/PanelHeader';
 import { DataPanel } from './DataPanel';
 
 // Utils
-import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
+import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel';
 import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
 import { profiler } from 'app/core/profiler';
 
 // Types
 import { DashboardModel, PanelModel } from '../state';
 import { PanelPlugin } from 'app/types';
-import { TimeRange, LoadingState } from '@grafana/ui';
+import { TimeRange, LoadingState, PanelData } from '@grafana/ui';
 
 import variables from 'sass/_variables.scss';
 import templateSrv from 'app/features/templating/template_srv';
@@ -94,7 +94,20 @@ export class PanelChrome extends PureComponent<Props, State> {
     return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
   }
 
-  renderPanel(loading, panelData, width, height): JSX.Element {
+  get hasPanelSnapshot() {
+    const { panel } = this.props;
+    return panel.snapshotData && panel.snapshotData.length;
+  }
+
+  get needsQueryExecution() {
+    return this.hasPanelSnapshot || this.props.plugin.dataFormats.length > 0;
+  }
+
+  get getDataForPanel() {
+    return this.hasPanelSnapshot ? snapshotDataToPanelData(this.props.panel) : null;
+  }
+
+  renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element {
     const { panel, plugin } = this.props;
     const { timeRange, renderCounter } = this.state;
     const PanelComponent = plugin.exports.Panel;
@@ -121,11 +134,39 @@ export class PanelChrome extends PureComponent<Props, State> {
     );
   }
 
+  renderPanelBody = (width: number, height: number): JSX.Element => {
+    const { panel } = this.props;
+    const { refreshCounter, timeRange } = this.state;
+    const { datasource, targets } = panel;
+    return (
+      <>
+        {this.needsQueryExecution ? (
+          <DataPanel
+            panelId={panel.id}
+            datasource={datasource}
+            queries={targets}
+            timeRange={timeRange}
+            isVisible={this.isVisible}
+            widthPixels={width}
+            refreshCounter={refreshCounter}
+            onDataResponse={this.onDataResponse}
+          >
+            {({ loading, panelData }) => {
+              return this.renderPanelPlugin(loading, panelData, width, height);
+            }}
+          </DataPanel>
+        ) : (
+          this.renderPanelPlugin(LoadingState.Done, this.getDataForPanel, width, height)
+        )}
+      </>
+    );
+  };
+
   render() {
-    const { panel, dashboard } = this.props;
-    const { refreshCounter, timeRange, timeInfo } = this.state;
+    const { dashboard, panel } = this.props;
+    const { timeInfo } = this.state;
+    const { transparent } = panel;
 
-    const { datasource, targets, transparent } = panel;
     const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`;
     return (
       <AutoSizer>
@@ -145,24 +186,7 @@ export class PanelChrome extends PureComponent<Props, State> {
                 scopedVars={panel.scopedVars}
                 links={panel.links}
               />
-              {panel.snapshotData ? (
-                this.renderPanel(false, panel.snapshotData, width, height)
-              ) : (
-                <DataPanel
-                  panelId={panel.id}
-                  datasource={datasource}
-                  queries={targets}
-                  timeRange={timeRange}
-                  isVisible={this.isVisible}
-                  widthPixels={width}
-                  refreshCounter={refreshCounter}
-                  onDataResponse={this.onDataResponse}
-                >
-                  {({ loading, panelData }) => {
-                    return this.renderPanel(loading, panelData, width, height);
-                  }}
-                </DataPanel>
-              )}
+              {this.renderPanelBody(width, height)}
             </div>
           );
         }}

+ 1 - 0
public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx

@@ -46,6 +46,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin {
     sort: 100,
     module: '',
     baseUrl: '',
+    dataFormats: [],
     info: {
       author: {
         name: '',

+ 37 - 16
public/app/features/dashboard/panel_editor/PanelEditor.tsx

@@ -30,6 +30,32 @@ interface PanelEditorTab {
   text: string;
 }
 
+enum PanelEditorTabIds {
+  Queries = 'queries',
+  Visualization = 'visualization',
+  Advanced = 'advanced',
+  Alert = 'alert',
+}
+
+interface PanelEditorTab {
+  id: string;
+  text: string;
+}
+
+const panelEditorTabTexts = {
+  [PanelEditorTabIds.Queries]: 'Queries',
+  [PanelEditorTabIds.Visualization]: 'Visualization',
+  [PanelEditorTabIds.Advanced]: 'Panel Options',
+  [PanelEditorTabIds.Alert]: 'Alert',
+};
+
+const getPanelEditorTab = (tabId: PanelEditorTabIds): PanelEditorTab => {
+  return {
+    id: tabId,
+    text: panelEditorTabTexts[tabId],
+  };
+};
+
 export class PanelEditor extends PureComponent<PanelEditorProps> {
   constructor(props) {
     super(props);
@@ -72,31 +98,26 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
 
   render() {
     const { plugin } = this.props;
-    let activeTab = store.getState().location.query.tab || 'queries';
+    let activeTab: PanelEditorTabIds = store.getState().location.query.tab || PanelEditorTabIds.Queries;
 
     const tabs: PanelEditorTab[] = [
-      { id: 'queries', text: 'Queries' },
-      { id: 'visualization', text: 'Visualization' },
-      { id: 'advanced', text: 'Panel Options' },
+      getPanelEditorTab(PanelEditorTabIds.Queries),
+      getPanelEditorTab(PanelEditorTabIds.Visualization),
+      getPanelEditorTab(PanelEditorTabIds.Advanced),
     ];
 
     // handle panels that do not have queries tab
-    if (plugin.exports.PanelCtrl) {
-      if (!plugin.exports.PanelCtrl.prototype.onDataReceived) {
-        // remove queries tab
-        tabs.shift();
-        // switch tab
-        if (activeTab === 'queries') {
-          activeTab = 'visualization';
-        }
+    if (plugin.dataFormats.length === 0) {
+      // remove queries tab
+      tabs.shift();
+      // switch tab
+      if (activeTab === PanelEditorTabIds.Queries) {
+        activeTab = PanelEditorTabIds.Visualization;
       }
     }
 
     if (config.alertingEnabled && plugin.id === 'graph') {
-      tabs.push({
-        id: 'alert',
-        text: 'Alert',
-      });
+      tabs.push(getPanelEditorTab(PanelEditorTabIds.Alert));
     }
 
     return (

+ 18 - 1
public/app/features/dashboard/utils/panel.ts

@@ -4,7 +4,8 @@ import store from 'app/core/store';
 // Models
 import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
 import { PanelModel } from 'app/features/dashboard/state/PanelModel';
-import { TimeRange } from '@grafana/ui';
+import { PanelData, TimeRange, TimeSeries } from '@grafana/ui';
+import { TableData } from '@grafana/ui/src';
 
 // Utils
 import { isString as _isString } from 'lodash';
@@ -168,3 +169,19 @@ export function getResolution(panel: PanelModel): number {
 
   return panel.maxDataPoints ? panel.maxDataPoints : Math.ceil(width * (panel.gridPos.w / 24));
 }
+
+const isTimeSeries = (data: any): data is TimeSeries => data && data.hasOwnProperty('datapoints');
+const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
+export const snapshotDataToPanelData = (panel: PanelModel): PanelData => {
+  const snapshotData = panel.snapshotData;
+  if (isTimeSeries(snapshotData[0])) {
+    return {
+      timeSeries: snapshotData
+    } as PanelData;
+  } else if (isTableData(snapshotData[0])) {
+    return {
+      tableData: snapshotData[0]
+    } as PanelData;
+  }
+  throw new Error('snapshotData is invalid:' + snapshotData.toString());
+};

+ 2 - 1
public/app/features/plugins/__mocks__/pluginMocks.ts

@@ -1,4 +1,4 @@
-import { Plugin, PanelPlugin } from 'app/types';
+import { Plugin, PanelPlugin, PanelDataFormat } from 'app/types';
 
 export const getMockPlugins = (amount: number): Plugin[] => {
   const plugins = [];
@@ -38,6 +38,7 @@ export const getPanelPlugin = (options: { id: string; sort?: number; hideFromLis
     id: options.id,
     name: options.id,
     sort: options.sort || 1,
+    dataFormats: [PanelDataFormat.TimeSeries],
     info: {
       author: {
         name: options.id + 'name',

+ 2 - 0
public/app/plugins/panel/alertlist/plugin.json

@@ -3,6 +3,8 @@
   "name": "Alert List",
   "id": "alertlist",
 
+  "dataFormats": [],
+
   "info": {
     "description": "Shows list of alerts and their current status",
     "author": {

+ 3 - 1
public/app/plugins/panel/dashlist/plugin.json

@@ -3,12 +3,14 @@
   "name": "Dashboard list",
   "id": "dashlist",
 
+  "dataFormats": [],
+
   "info": {
     "description": "List of dynamic links to other dashboards",
     "author": {
       "name": "Grafana Project",
       "url": "https://grafana.com"
-},
+    },
     "logos": {
       "small": "img/icn-dashlist-panel.svg",
       "large": "img/icn-dashlist-panel.svg"

+ 2 - 0
public/app/plugins/panel/gauge/plugin.json

@@ -3,6 +3,8 @@
   "name": "Gauge",
   "id": "gauge",
 
+  "dataFormats": ["time_series"],
+
   "info": {
     "author": {
       "name": "Grafana Project",

+ 3 - 2
public/app/plugins/panel/graph/plugin.json

@@ -3,12 +3,14 @@
   "name": "Graph",
   "id": "graph",
 
+  "dataFormats": ["time_series", "table"],
+
   "info": {
     "description": "Graph Panel for Grafana",
     "author": {
       "name": "Grafana Project",
       "url": "https://grafana.com"
-},
+    },
     "logos": {
       "small": "img/icn-graph-panel.svg",
       "large": "img/icn-graph-panel.svg"
@@ -16,4 +18,3 @@
     "version": "5.0.0"
   }
 }
-

+ 2 - 2
public/app/plugins/panel/graph2/GraphPanel.tsx

@@ -9,7 +9,7 @@ import { processTimeSeries } from '@grafana/ui/src/utils';
 import { Graph } from '@grafana/ui';
 
 // Types
-import { PanelProps, NullValueMode } from '@grafana/ui/src/types';
+import { PanelProps, NullValueMode, TimeSeriesVMs } from '@grafana/ui/src/types';
 import { Options } from './types';
 
 interface Props extends PanelProps<Options> {}
@@ -19,7 +19,7 @@ export class GraphPanel extends PureComponent<Props> {
     const { panelData, timeRange, width, height } = this.props;
     const { showLines, showBars, showPoints } = this.props.options;
 
-    let vmSeries;
+    let vmSeries: TimeSeriesVMs;
     if (panelData.timeSeries) {
       vmSeries = processTimeSeries({
         timeSeries: panelData.timeSeries,

+ 0 - 1
public/app/plugins/panel/graph2/plugin.json

@@ -2,7 +2,6 @@
   "type": "panel",
   "name": "React Graph",
   "id": "graph2",
-
   "state": "alpha",
 
   "info": {

+ 2 - 0
public/app/plugins/panel/heatmap/plugin.json

@@ -3,6 +3,8 @@
   "name": "Heatmap",
   "id": "heatmap",
 
+  "dataFormats": ["time_series"],
+
   "info": {
     "description": "Heatmap Panel for Grafana",
     "author": {

+ 2 - 0
public/app/plugins/panel/pluginlist/plugin.json

@@ -3,6 +3,8 @@
   "name": "Plugin list",
   "id": "pluginlist",
 
+  "dataFormats": [],
+
   "info": {
     "description": "Plugin List for Grafana",
     "author": {

+ 3 - 2
public/app/plugins/panel/singlestat/plugin.json

@@ -3,12 +3,14 @@
   "name": "Singlestat",
   "id": "singlestat",
 
+  "dataFormats": ["time_series", "table"],
+
   "info": {
     "description": "Singlestat Panel for Grafana",
     "author": {
       "name": "Grafana Project",
       "url": "https://grafana.com"
-},
+    },
     "logos": {
       "small": "img/icn-singlestat-panel.svg",
       "large": "img/icn-singlestat-panel.svg"
@@ -16,4 +18,3 @@
     "version": "5.0.0"
   }
 }
-

+ 3 - 2
public/app/plugins/panel/table/plugin.json

@@ -3,12 +3,14 @@
   "name": "Table",
   "id": "table",
 
+  "dataFormats": ["table", "time_series"],
+
   "info": {
     "description": "Table Panel for Grafana",
     "author": {
       "name": "Grafana Project",
       "url": "https://grafana.com"
-},
+    },
     "logos": {
       "small": "img/icn-table-panel.svg",
       "large": "img/icn-table-panel.svg"
@@ -16,4 +18,3 @@
     "version": "5.0.0"
   }
 }
-

+ 3 - 1
public/app/plugins/panel/text/plugin.json

@@ -3,11 +3,13 @@
   "name": "Text",
   "id": "text",
 
+  "dataFormats": [],
+
   "info": {
     "author": {
       "name": "Grafana Project",
       "url": "https://grafana.com"
-},
+    },
     "logos": {
       "small": "img/icn-text-panel.svg",
       "large": "img/icn-text-panel.svg"

+ 1 - 1
public/app/plugins/panel/text2/module.tsx

@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
 import { PanelProps } from '@grafana/ui';
 
 export class Text2 extends PureComponent<PanelProps> {
-  constructor(props) {
+  constructor(props: PanelProps) {
     super(props);
   }
 

+ 2 - 1
public/app/plugins/panel/text2/plugin.json

@@ -2,9 +2,10 @@
   "type": "panel",
   "name": "Text v2",
   "id": "text2",
-
   "state": "alpha",
 
+  "dataFormats": [],
+
   "info": {
     "author": {
       "name": "Grafana Project",

+ 6 - 0
public/app/types/plugins.ts

@@ -9,6 +9,12 @@ export interface PanelPlugin {
   info: any;
   sort: number;
   exports?: PluginExports;
+  dataFormats: PanelDataFormat[];
+}
+
+export enum PanelDataFormat {
+  Table = 'table',
+  TimeSeries = 'time_series',
 }
 
 export interface Plugin {