Explorar o código

QueryRunner: Move queryRunner to panelModel (#16656)

* move queryRunner to panelModel

* remove isEditing from PanelChrome

* move listener to QueriesTab

* Fixed issue with isFirstLoad set to false before loading state is Done

* QueryRunner: Fixed issue with error and delayed loading state indication

* Anoter fix to issues with multiple setState calls in observable callbacks
Ryan McKinley %!s(int64=6) %!d(string=hai) anos
pai
achega
0643dff2f6

+ 2 - 3
public/app/features/dashboard/dashgrid/DashboardPanel.tsx

@@ -37,7 +37,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
   element: HTMLElement;
   specialPanels = {};
 
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
 
     this.state = {
@@ -150,7 +150,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
   };
 
   renderReactPanel() {
-    const { dashboard, panel, isFullscreen, isEditing } = this.props;
+    const { dashboard, panel, isFullscreen } = this.props;
     const { plugin } = this.state;
 
     return (
@@ -165,7 +165,6 @@ export class DashboardPanel extends PureComponent<Props, State> {
               panel={panel}
               dashboard={dashboard}
               isFullscreen={isFullscreen}
-              isEditing={isEditing}
               width={width}
               height={height}
             />

+ 30 - 45
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -17,7 +17,7 @@ import config from 'app/core/config';
 // Types
 import { DashboardModel, PanelModel } from '../state';
 import { PanelPlugin } from 'app/types';
-import { TimeRange, LoadingState, PanelData, toLegacyResponseData } from '@grafana/ui';
+import { TimeRange, LoadingState, PanelData } from '@grafana/ui';
 import { ScopedVars } from '@grafana/ui';
 
 import templateSrv from 'app/features/templating/template_srv';
@@ -32,7 +32,6 @@ export interface Props {
   dashboard: DashboardModel;
   plugin: PanelPlugin;
   isFullscreen: boolean;
-  isEditing: boolean;
   width: number;
   height: number;
 }
@@ -50,7 +49,6 @@ export interface State {
 
 export class PanelChrome extends PureComponent<Props, State> {
   timeSrv: TimeSrv = getTimeSrv();
-  queryRunner = new PanelQueryRunner();
   querySubscription: Unsubscribable;
 
   constructor(props: Props) {
@@ -64,9 +62,6 @@ export class PanelChrome extends PureComponent<Props, State> {
         series: [],
       },
     };
-
-    // Listen for changes to the query results
-    this.querySubscription = this.queryRunner.subscribe(this.panelDataObserver);
   }
 
   componentDidMount() {
@@ -91,50 +86,41 @@ export class PanelChrome extends PureComponent<Props, State> {
 
   componentWillUnmount() {
     this.props.panel.events.off('refresh', this.onRefresh);
+    if (this.querySubscription) {
+      this.querySubscription.unsubscribe();
+      this.querySubscription = null;
+    }
   }
 
   // Updates the response with information from the stream
+  // The next is outside a react synthetic event so setState is not batched
+  // So in this context we can only do a single call to setState
   panelDataObserver = {
     next: (data: PanelData) => {
+      let { errorMessage, isFirstLoad } = this.state;
+
       if (data.state === LoadingState.Error) {
         const { error } = data;
         if (error) {
-          let message = error.message;
-          if (!message) {
-            message = 'Query error';
-          }
-
-          if (this.state.errorMessage !== message) {
-            this.setState({ errorMessage: message });
+          if (this.state.errorMessage !== error.message) {
+            errorMessage = error.message;
           }
-          // this event is used by old query editors
-          this.props.panel.events.emit('data-error', error);
         }
       } else {
-        this.clearErrorState();
+        errorMessage = null;
       }
 
-      // Save the query response into the panel
-      if (data.state === LoadingState.Done && this.props.dashboard.snapshot) {
-        this.props.panel.snapshotData = data.series;
-      }
-
-      this.setState({ data, isFirstLoad: false });
-
-      // Notify query editors that the results have changed
-      if (this.props.isEditing) {
-        const events = this.props.panel.events;
-        let legacy = data.legacy;
-        if (!legacy) {
-          legacy = data.series.map(v => toLegacyResponseData(v));
+      if (data.state === LoadingState.Done) {
+        // If we are doing a snapshot save data in panel model
+        if (this.props.dashboard.snapshot) {
+          this.props.panel.snapshotData = data.series;
+        }
+        if (this.state.isFirstLoad) {
+          isFirstLoad = false;
         }
-
-        // Angular query editors expect TimeSeries|TableData
-        events.emit('data-received', legacy);
-
-        // Notify react query editors
-        events.emit('series-data-received', data);
       }
+
+      this.setState({ isFirstLoad, errorMessage, data });
     },
   };
 
@@ -153,13 +139,18 @@ export class PanelChrome extends PureComponent<Props, State> {
     });
 
     // Issue Query
-    if (this.wantsQueryExecution && !this.hasPanelSnapshot) {
+    if (this.wantsQueryExecution) {
       if (width < 0) {
         console.log('No width yet... wait till we know');
         return;
       }
-
-      this.queryRunner.run({
+      if (!panel.queryRunner) {
+        panel.queryRunner = new PanelQueryRunner();
+      }
+      if (!this.querySubscription) {
+        this.querySubscription = panel.queryRunner.subscribe(this.panelDataObserver);
+      }
+      panel.queryRunner.run({
         datasource: panel.datasource,
         queries: panel.targets,
         panelId: panel.id,
@@ -195,12 +186,6 @@ export class PanelChrome extends PureComponent<Props, State> {
     }
   };
 
-  clearErrorState() {
-    if (this.state.errorMessage) {
-      this.setState({ errorMessage: null });
-    }
-  }
-
   get isVisible() {
     return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
   }
@@ -211,7 +196,7 @@ export class PanelChrome extends PureComponent<Props, State> {
   }
 
   get wantsQueryExecution() {
-    return this.props.plugin.dataFormats.length > 0;
+    return this.props.plugin.dataFormats.length > 0 && !this.hasPanelSnapshot;
   }
 
   renderPanel(width: number, height: number): JSX.Element {

+ 43 - 4
public/app/features/dashboard/panel_editor/QueriesTab.tsx

@@ -12,14 +12,16 @@ import { QueryEditorRow } from './QueryEditorRow';
 
 // Services
 import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
-import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv';
+import { getBackendSrv } from 'app/core/services/backend_srv';
 import config from 'app/core/config';
 
 // Types
 import { PanelModel } from '../state/PanelModel';
 import { DashboardModel } from '../state/DashboardModel';
-import { DataQuery, DataSourceSelectItem } from '@grafana/ui/src/types';
+import { DataQuery, DataSourceSelectItem, PanelData, LoadingState } from '@grafana/ui/src/types';
 import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
+import { PanelQueryRunner, PanelQueryRunnerFormat } from '../state/PanelQueryRunner';
+import { Unsubscribable } from 'rxjs';
 
 interface Props {
   panel: PanelModel;
@@ -33,11 +35,13 @@ interface State {
   isPickerOpen: boolean;
   isAddingMixed: boolean;
   scrollTop: number;
+  data: PanelData;
 }
 
 export class QueriesTab extends PureComponent<Props, State> {
   datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
-  backendSrv: BackendSrv = getBackendSrv();
+  backendSrv = getBackendSrv();
+  querySubscription: Unsubscribable;
 
   state: State = {
     isLoadingHelp: false,
@@ -46,6 +50,40 @@ export class QueriesTab extends PureComponent<Props, State> {
     isPickerOpen: false,
     isAddingMixed: false,
     scrollTop: 0,
+    data: {
+      state: LoadingState.NotStarted,
+      series: [],
+    },
+  };
+
+  componentDidMount() {
+    const { panel } = this.props;
+
+    if (!panel.queryRunner) {
+      panel.queryRunner = new PanelQueryRunner();
+    }
+
+    this.querySubscription = panel.queryRunner.subscribe(this.panelDataObserver, PanelQueryRunnerFormat.both);
+  }
+
+  componentWillUnmount() {
+    if (this.querySubscription) {
+      this.querySubscription.unsubscribe();
+      this.querySubscription = null;
+    }
+  }
+
+  // Updates the response with information from the stream
+  panelDataObserver = {
+    next: (data: PanelData) => {
+      const { panel } = this.props;
+      if (data.state === LoadingState.Error) {
+        panel.events.emit('data-error', data.error);
+      } else if (data.state === LoadingState.Done) {
+        panel.events.emit('data-received', data.legacy);
+      }
+      this.setState({ data });
+    },
   };
 
   findCurrentDataSource(): DataSourceSelectItem {
@@ -179,7 +217,7 @@ export class QueriesTab extends PureComponent<Props, State> {
 
   render() {
     const { panel, dashboard } = this.props;
-    const { currentDS, scrollTop } = this.state;
+    const { currentDS, scrollTop, data } = this.state;
 
     const queryInspector: EditorToolbarView = {
       title: 'Query Inspector',
@@ -208,6 +246,7 @@ export class QueriesTab extends PureComponent<Props, State> {
                 key={query.refId}
                 panel={panel}
                 dashboard={dashboard}
+                data={data}
                 query={query}
                 onChange={query => this.onQueryChange(query, index)}
                 onRemoveQuery={this.onRemoveQuery}

+ 20 - 49
public/app/features/dashboard/panel_editor/QueryEditorRow.tsx

@@ -11,11 +11,12 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
 
 // Types
 import { PanelModel } from '../state/PanelModel';
-import { DataQuery, DataSourceApi, TimeRange, DataQueryError, SeriesData } from '@grafana/ui';
+import { DataQuery, DataSourceApi, TimeRange, DataQueryError, SeriesData, PanelData } from '@grafana/ui';
 import { DashboardModel } from '../state/DashboardModel';
 
 interface Props {
   panel: PanelModel;
+  data: PanelData;
   query: DataQuery;
   dashboard: DashboardModel;
   onAddQuery: (query?: DataQuery) => void;
@@ -51,61 +52,14 @@ export class QueryEditorRow extends PureComponent<Props, State> {
 
   componentDidMount() {
     this.loadDatasource();
-    this.props.panel.events.on('refresh', this.onPanelRefresh);
-    this.props.panel.events.on('data-error', this.onPanelDataError);
-    this.props.panel.events.on('data-received', this.onPanelDataReceived);
-    this.props.panel.events.on('series-data-received', this.onSeriesDataReceived);
   }
 
   componentWillUnmount() {
-    this.props.panel.events.off('refresh', this.onPanelRefresh);
-    this.props.panel.events.off('data-error', this.onPanelDataError);
-    this.props.panel.events.off('data-received', this.onPanelDataReceived);
-    this.props.panel.events.off('series-data-received', this.onSeriesDataReceived);
-
     if (this.angularQueryEditor) {
       this.angularQueryEditor.destroy();
     }
   }
 
-  onPanelDataError = (error: DataQueryError) => {
-    // Some query controllers listen to data error events and need a digest
-    if (this.angularQueryEditor) {
-      // for some reason this needs to be done in next tick
-      setTimeout(this.angularQueryEditor.digest);
-      return;
-    }
-
-    // if error relates to this query store it in state and pass it on to query editor
-    if (error.refId === this.props.query.refId) {
-      this.setState({ queryError: error });
-    }
-  };
-
-  // Only used by angular plugins
-  onPanelDataReceived = () => {
-    // Some query controllers listen to data error events and need a digest
-    if (this.angularQueryEditor) {
-      // for some reason this needs to be done in next tick
-      setTimeout(this.angularQueryEditor.digest);
-    }
-  };
-
-  // Only used by the React Query Editors
-  onSeriesDataReceived = (data: SeriesData[]) => {
-    if (!this.angularQueryEditor) {
-      // only pass series related to this query to query editor
-      const filterByRefId = data.filter(series => series.refId === this.props.query.refId);
-      this.setState({ queryResponse: filterByRefId, queryError: null });
-    }
-  };
-
-  onPanelRefresh = () => {
-    if (this.angularScope) {
-      this.angularScope.range = getTimeSrv().timeRange();
-    }
-  };
-
   getAngularQueryComponentScope(): AngularQueryComponentScope {
     const { panel, query, dashboard } = this.props;
     const { datasource } = this.state;
@@ -134,8 +88,25 @@ export class QueryEditorRow extends PureComponent<Props, State> {
     });
   }
 
-  componentDidUpdate() {
+  componentDidUpdate(prevProps: Props) {
     const { loadedDataSourceValue } = this.state;
+    const { data, query } = this.props;
+
+    if (data !== prevProps.data) {
+      const queryError = data.error && data.error.refId === query.refId ? data.error : null;
+      const queryResponse = data.series.filter(series => series.refId === query.refId);
+      this.setState({ queryResponse, queryError });
+
+      if (this.angularScope) {
+        this.angularScope.range = getTimeSrv().timeRange();
+      }
+
+      if (this.angularQueryEditor) {
+        // Some query controllers listen to data error events and need a digest
+        // for some reason this needs to be done in next tick
+        setTimeout(this.angularQueryEditor.digest);
+      }
+    }
 
     // check if we need to load another datasource
     if (loadedDataSourceValue !== this.props.dataSourceValue) {

+ 4 - 0
public/app/features/dashboard/state/PanelModel.ts

@@ -10,6 +10,8 @@ import { DataQuery, Threshold, ScopedVars, DataQueryResponseData } from '@grafan
 import { PanelPlugin } from 'app/types';
 import config from 'app/core/config';
 
+import { PanelQueryRunner } from './PanelQueryRunner';
+
 export interface GridPos {
   x: number;
   y: number;
@@ -25,6 +27,7 @@ const notPersistedProperties: { [str: string]: boolean } = {
   hasRefreshed: true,
   cachedPluginOptions: true,
   plugin: true,
+  queryRunner: true,
 };
 
 // For angular panels we need to clean up properties when changing type
@@ -115,6 +118,7 @@ export class PanelModel {
   cachedPluginOptions?: any;
   legend?: { show: boolean };
   plugin?: PanelPlugin;
+  queryRunner?: PanelQueryRunner;
 
   constructor(model: any) {
     this.events = new Emitter();

+ 32 - 20
public/app/features/dashboard/state/PanelQueryRunner.ts

@@ -39,6 +39,7 @@ export interface QueryRunnerOptions<TQuery extends DataQuery = DataQuery> {
 export enum PanelQueryRunnerFormat {
   series = 'series',
   legacy = 'legacy',
+  both = 'both',
 }
 
 export class PanelQueryRunner {
@@ -63,6 +64,9 @@ export class PanelQueryRunner {
 
     if (format === PanelQueryRunnerFormat.legacy) {
       this.sendLegacy = true;
+    } else if (format === PanelQueryRunnerFormat.both) {
+      this.sendSeries = true;
+      this.sendLegacy = true;
     } else {
       this.sendSeries = true;
     }
@@ -121,6 +125,8 @@ export class PanelQueryRunner {
       return this.data;
     }
 
+    let loadingStateTimeoutId = 0;
+
     try {
       const ds = options.ds ? options.ds : await getDatasourceSrv().get(datasource, request.scopedVars);
 
@@ -137,18 +143,12 @@ export class PanelQueryRunner {
       request.intervalMs = norm.intervalMs;
 
       // Send a loading status event on slower queries
-      setTimeout(() => {
-        if (!request.endTime) {
-          this.data = {
-            ...this.data,
-            state: LoadingState.Loading,
-            request,
-          };
-          this.subject.next(this.data);
-        }
-      }, delayStateNotification || 100);
+      loadingStateTimeoutId = window.setTimeout(() => {
+        this.publishUpdate({ state: LoadingState.Loading });
+      }, delayStateNotification || 500);
 
       const resp = await ds.query(request);
+
       request.endTime = Date.now();
 
       // Make sure the response is in a supported format
@@ -162,15 +162,16 @@ export class PanelQueryRunner {
           })
         : undefined;
 
-      // The Result
-      this.data = {
+      // Make sure the delayed loading state timeout is cleared
+      clearTimeout(loadingStateTimeoutId);
+
+      // Publish the result
+      return this.publishUpdate({
         state: LoadingState.Done,
         series,
         legacy,
         request,
-      };
-      this.subject.next(this.data);
-      return this.data;
+      });
     } catch (err) {
       const error = err as DataQueryError;
       if (!error.message) {
@@ -187,15 +188,26 @@ export class PanelQueryRunner {
         error.message = message;
       }
 
-      this.data = {
-        ...this.data, // ?? Should we keep existing data, or clear it ???
+      // Make sure the delayed loading state timeout is cleared
+      clearTimeout(loadingStateTimeoutId);
+
+      return this.publishUpdate({
         state: LoadingState.Error,
         error: error,
-      };
-      this.subject.next(this.data);
-      return this.data;
+      });
     }
   }
+
+  publishUpdate(update: Partial<PanelData>): PanelData {
+    this.data = {
+      ...this.data,
+      ...update,
+    };
+
+    this.subject.next(this.data);
+
+    return this.data;
+  }
 }
 
 /**