Quellcode durchsuchen

Fixes to error handling and clearing, also publishing of legacy events so old query editors work with react panels fully

Torkel Ödegaard vor 6 Jahren
Ursprung
Commit
2bc26a01f9

+ 10 - 0
packages/grafana-ui/src/types/datasource.ts

@@ -29,6 +29,16 @@ export interface DataQuery {
   datasource?: string | null;
 }
 
+export interface DataQueryError {
+  data?: {
+    message?: string;
+    error?: string;
+  };
+  message?: string;
+  status?: string;
+  statusText?: string;
+}
+
 export interface DataQueryOptions<TQuery extends DataQuery = DataQuery> {
   timezone: string;
   range: TimeRange;

+ 3 - 1
public/app/core/services/AngularLoader.ts

@@ -27,7 +27,9 @@ export class AngularLoader {
         compiledElem.remove();
       },
       digest: () => {
-        scope.$digest();
+        if (!scope.$$phase) {
+          scope.$digest();
+        }
       },
       getScope: () => {
         return scope;

+ 15 - 5
public/app/features/dashboard/dashgrid/DataPanel.tsx

@@ -9,6 +9,7 @@ import kbn from 'app/core/utils/kbn';
 import {
   DataQueryOptions,
   DataQueryResponse,
+  DataQueryError,
   LoadingState,
   PanelData,
   TableData,
@@ -34,7 +35,7 @@ export interface Props {
   maxDataPoints?: number;
   children: (r: RenderProps) => JSX.Element;
   onDataResponse?: (data: DataQueryResponse) => void;
-  onError: (errorMessage: string) => void;
+  onError: (message: string, error: DataQueryError) => void;
 }
 
 export interface State {
@@ -146,11 +147,20 @@ export class DataPanel extends Component<Props, State> {
         isFirstLoad: false,
       });
     } catch (err) {
-      console.log('Loading error', err);
+      console.log('DataPanel error', err);
+
+      let message = 'Query error';
+
+      if (err.message) {
+        message = err.message;
+      } else if (err.data && err.data.message) {
+        message = err.data.message;
+      } else if (err.data && err.data.error) {
+        message = err.data.error;
+      }
+
+      onError(message, err);
       this.setState({ isFirstLoad: false });
-      onError(`Query error
-      status: ${err.status}
-      message: ${err.statusText}`);
     }
   };
 

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

@@ -18,7 +18,7 @@ import { profiler } from 'app/core/profiler';
 // Types
 import { DashboardModel, PanelModel } from '../state';
 import { PanelPlugin } from 'app/types';
-import { DataQueryResponse, TimeRange, LoadingState, PanelData } from '@grafana/ui';
+import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui';
 
 import variables from 'sass/_variables.scss';
 import templateSrv from 'app/features/templating/template_srv';
@@ -36,8 +36,7 @@ export interface State {
   renderCounter: number;
   timeInfo?: string;
   timeRange?: TimeRange;
-  loading: LoadingState;
-  errorMessage: string;
+  errorMessage: string | null;
 }
 
 export class PanelChrome extends PureComponent<Props, State> {
@@ -49,8 +48,7 @@ export class PanelChrome extends PureComponent<Props, State> {
     this.state = {
       refreshCounter: 0,
       renderCounter: 0,
-      loading: LoadingState.NotStarted,
-      errorMessage: '',
+      errorMessage: null,
     };
   }
 
@@ -94,8 +92,33 @@ export class PanelChrome extends PureComponent<Props, State> {
     if (this.props.dashboard.isSnapshot()) {
       this.props.panel.snapshotData = dataQueryResponse.data;
     }
+    // clear error state (if any)
+    this.clearErrorState();
+
+    // This event is used by old query editors and panel editor options
+    this.props.panel.events.emit('data-received', dataQueryResponse.data);
+  };
+
+  onDataError = (message: string, error: DataQueryError) => {
+    if (this.state.errorMessage !== message) {
+      this.setState({ errorMessage: message });
+    }
+    // this event is used by old query editors
+    this.props.panel.events.emit('data-error', error);
+  };
+
+  onPanelError = (message: string) => {
+    if (this.state.errorMessage !== message) {
+      this.setState({ errorMessage: message });
+    }
   };
 
+  clearErrorState() {
+    if (this.state.errorMessage) {
+      this.setState({ errorMessage: null });
+    }
+  }
+
   get isVisible() {
     return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
   }
@@ -113,15 +136,6 @@ export class PanelChrome extends PureComponent<Props, State> {
     return this.hasPanelSnapshot ? snapshotDataToPanelData(this.props.panel) : null;
   }
 
-  onError = (errorMessage: string) => {
-    if (this.state.loading !== LoadingState.Error || this.state.errorMessage !== errorMessage) {
-      this.setState({
-        loading: LoadingState.Error,
-        errorMessage: errorMessage,
-      });
-    }
-  };
-
   renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element {
     const { panel, plugin } = this.props;
     const { timeRange, renderCounter } = this.state;
@@ -165,7 +179,7 @@ export class PanelChrome extends PureComponent<Props, State> {
             widthPixels={width}
             refreshCounter={refreshCounter}
             onDataResponse={this.onDataResponse}
-            onError={this.onError}
+            onError={this.onDataError}
           >
             {({ loading, panelData }) => {
               return this.renderPanelPlugin(loading, panelData, width, height);
@@ -206,7 +220,7 @@ export class PanelChrome extends PureComponent<Props, State> {
               <ErrorBoundary>
                 {({ error, errorInfo }) => {
                   if (errorInfo) {
-                    this.onError(error.message || DEFAULT_PLUGIN_ERROR);
+                    this.onPanelError(error.message || DEFAULT_PLUGIN_ERROR);
                     return null;
                   }
                   return this.renderPanelBody(width, height);

+ 10 - 9
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx

@@ -6,7 +6,7 @@ import templateSrv from 'app/features/templating/template_srv';
 import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
 import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
 
-enum InfoModes {
+enum InfoMode {
   Error = 'Error',
   Info = 'Info',
   Links = 'Links',
@@ -27,13 +27,13 @@ export class PanelHeaderCorner extends Component<Props> {
   getInfoMode = () => {
     const { panel, error } = this.props;
     if (error) {
-      return InfoModes.Error;
+      return InfoMode.Error;
     }
     if (!!panel.description) {
-      return InfoModes.Info;
+      return InfoMode.Info;
     }
     if (panel.links && panel.links.length) {
-      return InfoModes.Links;
+      return InfoMode.Links;
     }
 
     return undefined;
@@ -68,9 +68,10 @@ export class PanelHeaderCorner extends Component<Props> {
     );
   };
 
-  renderCornerType(infoMode: InfoModes, content: string | JSX.Element) {
+  renderCornerType(infoMode: InfoMode, content: string | JSX.Element) {
+    const theme = infoMode === InfoMode.Error ? 'error' : 'info';
     return (
-      <Tooltip content={content} placement="bottom-start">
+      <Tooltip content={content} placement="bottom-start" theme={theme}>
         <div className={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}>
           <i className="fa" />
           <span className="panel-info-corner-inner" />
@@ -80,17 +81,17 @@ export class PanelHeaderCorner extends Component<Props> {
   }
 
   render() {
-    const infoMode: InfoModes | undefined = this.getInfoMode();
+    const infoMode: InfoMode | undefined = this.getInfoMode();
 
     if (!infoMode) {
       return null;
     }
 
-    if (infoMode === InfoModes.Error) {
+    if (infoMode === InfoMode.Error) {
       return this.renderCornerType(infoMode, this.props.error);
     }
 
-    if (infoMode === InfoModes.Info) {
+    if (infoMode === InfoMode.Info) {
       return this.renderCornerType(infoMode, this.getInfoContent());
     }
 

+ 46 - 29
public/app/features/dashboard/panel_editor/QueryEditorRow.tsx

@@ -28,28 +28,57 @@ interface State {
   loadedDataSourceValue: string | null | undefined;
   datasource: DataSourceApi | null;
   isCollapsed: boolean;
-  angularScope: AngularQueryComponentScope | null;
+  hasTextEditMode: boolean;
 }
 
 export class QueryEditorRow extends PureComponent<Props, State> {
   element: HTMLElement | null = null;
+  angularScope: AngularQueryComponentScope | null;
   angularQueryEditor: AngularComponent | null = null;
 
   state: State = {
     datasource: null,
     isCollapsed: false,
-    angularScope: null,
     loadedDataSourceValue: undefined,
+    hasTextEditMode: false,
   };
 
   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);
+  }
+
+  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);
+
+    if (this.angularQueryEditor) {
+      this.angularQueryEditor.destroy();
+    }
   }
 
+  onPanelDataError = () => {
+    // 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);
+    }
+  };
+
+  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);
+    }
+  };
+
   onPanelRefresh = () => {
-    if (this.state.angularScope) {
-      this.state.angularScope.range = getTimeSrv().timeRange();
+    if (this.angularScope) {
+      this.angularScope.range = getTimeSrv().timeRange();
     }
   };
 
@@ -73,7 +102,11 @@ export class QueryEditorRow extends PureComponent<Props, State> {
     const dataSourceSrv = getDatasourceSrv();
     const datasource = await dataSourceSrv.get(query.datasource || panel.datasource);
 
-    this.setState({ datasource, loadedDataSourceValue: this.props.dataSourceValue });
+    this.setState({
+      datasource,
+      loadedDataSourceValue: this.props.dataSourceValue,
+      hasTextEditMode: false,
+    });
   }
 
   componentDidUpdate() {
@@ -98,21 +131,14 @@ export class QueryEditorRow extends PureComponent<Props, State> {
     const scopeProps = { ctrl: this.getAngularQueryComponentScope() };
 
     this.angularQueryEditor = loader.load(this.element, scopeProps, template);
+    this.angularScope = scopeProps.ctrl;
 
     // give angular time to compile
     setTimeout(() => {
-      this.setState({ angularScope: scopeProps.ctrl });
+      this.setState({ hasTextEditMode: !!this.angularScope.toggleEditorMode });
     }, 10);
   }
 
-  componentWillUnmount() {
-    this.props.panel.events.off('refresh', this.onPanelRefresh);
-
-    if (this.angularQueryEditor) {
-      this.angularQueryEditor.destroy();
-    }
-  }
-
   onToggleCollapse = () => {
     this.setState({ isCollapsed: !this.state.isCollapsed });
   };
@@ -138,10 +164,8 @@ export class QueryEditorRow extends PureComponent<Props, State> {
   }
 
   onToggleEditMode = () => {
-    const { angularScope } = this.state;
-
-    if (angularScope && angularScope.toggleEditorMode) {
-      angularScope.toggleEditorMode();
+    if (this.angularScope && this.angularScope.toggleEditorMode) {
+      this.angularScope.toggleEditorMode();
       this.angularQueryEditor.digest();
     }
 
@@ -150,11 +174,6 @@ export class QueryEditorRow extends PureComponent<Props, State> {
     }
   };
 
-  get hasTextEditMode() {
-    const { angularScope } = this.state;
-    return angularScope && angularScope.toggleEditorMode;
-  }
-
   onRemoveQuery = () => {
     this.props.onRemoveQuery(this.props.query);
   };
@@ -171,10 +190,8 @@ export class QueryEditorRow extends PureComponent<Props, State> {
   };
 
   renderCollapsedText(): string | null {
-    const { angularScope } = this.state;
-
-    if (angularScope && angularScope.getCollapsedText) {
-      return angularScope.getCollapsedText();
+    if (this.angularScope && this.angularScope.getCollapsedText) {
+      return this.angularScope.getCollapsedText();
     }
 
     return null;
@@ -182,7 +199,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
 
   render() {
     const { query, inMixedMode } = this.props;
-    const { datasource, isCollapsed } = this.state;
+    const { datasource, isCollapsed, hasTextEditMode } = this.state;
     const isDisabled = query.hide;
 
     const bodyClasses = classNames('query-editor-row__body gf-form-query', {
@@ -212,7 +229,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
             {isCollapsed && <div>{this.renderCollapsedText()}</div>}
           </div>
           <div className="query-editor-row__actions">
-            {this.hasTextEditMode && (
+            {hasTextEditMode && (
               <button
                 className="query-editor-row__action"
                 onClick={this.onToggleEditMode}