Просмотр исходного кода

Merge pull request #15424 from grafana/move-error-boundry

Move error boundry from DataPanel to PanelChrome
Torkel Ödegaard 6 лет назад
Родитель
Сommit
7fd89ff77a

+ 1 - 3
packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.tsx

@@ -5,9 +5,7 @@ export interface Props {
 }
 }
 
 
 const EmptySearchResult: FC<Props> = ({ children }) => {
 const EmptySearchResult: FC<Props> = ({ children }) => {
-  return (
-    <div className="empty-search-result">{children}</div>
-  );
+  return <div className="empty-search-result">{children}</div>;
 };
 };
 
 
 export { EmptySearchResult };
 export { EmptySearchResult };

+ 12 - 8
packages/grafana-ui/src/components/Tooltip/Popper.tsx

@@ -35,8 +35,16 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
 
 
 class Popper extends PureComponent<Props> {
 class Popper extends PureComponent<Props> {
   render() {
   render() {
-    const { show, placement, onMouseEnter, onMouseLeave, className, wrapperClassName, renderArrow } = this.props;
-    const { content } = this.props;
+    const {
+      content,
+      show,
+      placement,
+      onMouseEnter,
+      onMouseLeave,
+      className,
+      wrapperClassName,
+      renderArrow,
+    } = this.props;
 
 
     return (
     return (
       <Manager>
       <Manager>
@@ -50,7 +58,7 @@ class Popper extends PureComponent<Props> {
                   // TODO: move modifiers config to popper controller
                   // TODO: move modifiers config to popper controller
                   modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }}
                   modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }}
                 >
                 >
-                  {({ ref, style, placement, arrowProps, scheduleUpdate }) => {
+                  {({ ref, style, placement, arrowProps }) => {
                     return (
                     return (
                       <div
                       <div
                         onMouseEnter={onMouseEnter}
                         onMouseEnter={onMouseEnter}
@@ -65,11 +73,7 @@ class Popper extends PureComponent<Props> {
                         className={`${wrapperClassName}`}
                         className={`${wrapperClassName}`}
                       >
                       >
                         <div className={className}>
                         <div className={className}>
-                          {typeof content === 'string'
-                            ? content
-                            : React.cloneElement(content, {
-                                updatePopperPosition: scheduleUpdate,
-                              })}
+                          {typeof content === 'string' ? content : React.cloneElement(content)}
                           {renderArrow &&
                           {renderArrow &&
                             renderArrow({
                             renderArrow({
                               arrowProps,
                               arrowProps,

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

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

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

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

+ 24 - 49
public/app/features/dashboard/dashgrid/DataPanel.tsx

@@ -1,8 +1,6 @@
 // Library
 // Library
 import React, { Component } from 'react';
 import React, { Component } from 'react';
-import { Tooltip } from '@grafana/ui';
 
 
-import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary';
 // Services
 // Services
 import { DatasourceSrv, getDatasourceSrv } from 'app/features/plugins/datasource_srv';
 import { DatasourceSrv, getDatasourceSrv } from 'app/features/plugins/datasource_srv';
 // Utils
 // Utils
@@ -11,6 +9,7 @@ import kbn from 'app/core/utils/kbn';
 import {
 import {
   DataQueryOptions,
   DataQueryOptions,
   DataQueryResponse,
   DataQueryResponse,
+  DataQueryError,
   LoadingState,
   LoadingState,
   PanelData,
   PanelData,
   TableData,
   TableData,
@@ -18,8 +17,6 @@ import {
   TimeSeries,
   TimeSeries,
 } from '@grafana/ui';
 } from '@grafana/ui';
 
 
-const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
-
 interface RenderProps {
 interface RenderProps {
   loading: LoadingState;
   loading: LoadingState;
   panelData: PanelData;
   panelData: PanelData;
@@ -38,12 +35,12 @@ export interface Props {
   maxDataPoints?: number;
   maxDataPoints?: number;
   children: (r: RenderProps) => JSX.Element;
   children: (r: RenderProps) => JSX.Element;
   onDataResponse?: (data: DataQueryResponse) => void;
   onDataResponse?: (data: DataQueryResponse) => void;
+  onError: (message: string, error: DataQueryError) => void;
 }
 }
 
 
 export interface State {
 export interface State {
   isFirstLoad: boolean;
   isFirstLoad: boolean;
   loading: LoadingState;
   loading: LoadingState;
-  errorMessage: string;
   response: DataQueryResponse;
   response: DataQueryResponse;
 }
 }
 
 
@@ -61,7 +58,6 @@ export class DataPanel extends Component<Props, State> {
 
 
     this.state = {
     this.state = {
       loading: LoadingState.NotStarted,
       loading: LoadingState.NotStarted,
-      errorMessage: '',
       response: {
       response: {
         data: [],
         data: [],
       },
       },
@@ -100,6 +96,7 @@ export class DataPanel extends Component<Props, State> {
       widthPixels,
       widthPixels,
       maxDataPoints,
       maxDataPoints,
       onDataResponse,
       onDataResponse,
+      onError,
     } = this.props;
     } = this.props;
 
 
     if (!isVisible) {
     if (!isVisible) {
@@ -111,7 +108,7 @@ export class DataPanel extends Component<Props, State> {
       return;
       return;
     }
     }
 
 
-    this.setState({ loading: LoadingState.Loading, errorMessage: '' });
+    this.setState({ loading: LoadingState.Loading });
 
 
     try {
     try {
       const ds = await this.dataSourceSrv.get(datasource);
       const ds = await this.dataSourceSrv.get(datasource);
@@ -150,18 +147,22 @@ export class DataPanel extends Component<Props, State> {
         isFirstLoad: false,
         isFirstLoad: false,
       });
       });
     } catch (err) {
     } catch (err) {
-      console.log('Loading error', err);
-      this.onError('Request Error');
-    }
-  };
+      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;
+      } else if (err.status) {
+        message = `Query error: ${err.status} ${err.statusText}`;
+      }
 
 
-  onError = (errorMessage: string) => {
-    if (this.state.loading !== LoadingState.Error || this.state.errorMessage !== errorMessage) {
-      this.setState({
-        loading: LoadingState.Error,
-        isFirstLoad: false,
-        errorMessage: errorMessage,
-      });
+      onError(message, err);
+      this.setState({ isFirstLoad: false });
     }
     }
   };
   };
 
 
@@ -184,11 +185,10 @@ export class DataPanel extends Component<Props, State> {
   render() {
   render() {
     const { queries } = this.props;
     const { queries } = this.props;
     const { loading, isFirstLoad } = this.state;
     const { loading, isFirstLoad } = this.state;
-
     const panelData = this.getPanelData();
     const panelData = this.getPanelData();
 
 
     if (isFirstLoad && loading === LoadingState.Loading) {
     if (isFirstLoad && loading === LoadingState.Loading) {
-      return this.renderLoadingStates();
+      return this.renderLoadingState();
     }
     }
 
 
     if (!queries.length) {
     if (!queries.length) {
@@ -201,46 +201,21 @@ export class DataPanel extends Component<Props, State> {
 
 
     return (
     return (
       <>
       <>
-        {this.renderLoadingStates()}
-        <ErrorBoundary>
-          {({ error, errorInfo }) => {
-            if (errorInfo) {
-              this.onError(error.message || DEFAULT_PLUGIN_ERROR);
-              return null;
-            }
-            return (
-              <>
-                {this.props.children({
-                  loading,
-                  panelData,
-                })}
-              </>
-            );
-          }}
-        </ErrorBoundary>
+        {this.renderLoadingState()}
+        {this.props.children({ loading, panelData })}
       </>
       </>
     );
     );
   }
   }
 
 
-  private renderLoadingStates(): JSX.Element {
-    const { loading, errorMessage } = this.state;
+  private renderLoadingState(): JSX.Element {
+    const { loading } = this.state;
     if (loading === LoadingState.Loading) {
     if (loading === LoadingState.Loading) {
       return (
       return (
         <div className="panel-loading">
         <div className="panel-loading">
           <i className="fa fa-spinner fa-spin" />
           <i className="fa fa-spinner fa-spin" />
         </div>
         </div>
       );
       );
-    } else if (loading === LoadingState.Error) {
-      return (
-        <Tooltip content={errorMessage} placement="bottom-start" theme="error">
-          <div className="panel-info-corner panel-info-corner--error">
-            <i className="fa" />
-            <span className="panel-info-corner-inner" />
-          </div>
-        </Tooltip>
-      );
     }
     }
-
     return null;
     return null;
   }
   }
 }
 }

+ 43 - 4
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -8,6 +8,7 @@ import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
 // Components
 // Components
 import { PanelHeader } from './PanelHeader/PanelHeader';
 import { PanelHeader } from './PanelHeader/PanelHeader';
 import { DataPanel } from './DataPanel';
 import { DataPanel } from './DataPanel';
+import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary';
 
 
 // Utils
 // Utils
 import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel';
 import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel';
@@ -17,11 +18,12 @@ import { profiler } from 'app/core/profiler';
 // Types
 // Types
 import { DashboardModel, PanelModel } from '../state';
 import { DashboardModel, PanelModel } from '../state';
 import { PanelPlugin } from 'app/types';
 import { PanelPlugin } from 'app/types';
-import { TimeRange, LoadingState, PanelData } from '@grafana/ui';
+import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui';
 
 
 import variables from 'sass/_variables.scss';
 import variables from 'sass/_variables.scss';
 import templateSrv from 'app/features/templating/template_srv';
 import templateSrv from 'app/features/templating/template_srv';
-import { DataQueryResponse } from '@grafana/ui/src';
+
+const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
 
 
 export interface Props {
 export interface Props {
   panel: PanelModel;
   panel: PanelModel;
@@ -34,6 +36,7 @@ export interface State {
   renderCounter: number;
   renderCounter: number;
   timeInfo?: string;
   timeInfo?: string;
   timeRange?: TimeRange;
   timeRange?: TimeRange;
+  errorMessage: string | null;
 }
 }
 
 
 export class PanelChrome extends PureComponent<Props, State> {
 export class PanelChrome extends PureComponent<Props, State> {
@@ -45,6 +48,7 @@ export class PanelChrome extends PureComponent<Props, State> {
     this.state = {
     this.state = {
       refreshCounter: 0,
       refreshCounter: 0,
       renderCounter: 0,
       renderCounter: 0,
+      errorMessage: null,
     };
     };
   }
   }
 
 
@@ -88,8 +92,33 @@ export class PanelChrome extends PureComponent<Props, State> {
     if (this.props.dashboard.isSnapshot()) {
     if (this.props.dashboard.isSnapshot()) {
       this.props.panel.snapshotData = dataQueryResponse.data;
       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() {
   get isVisible() {
     return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
     return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
   }
   }
@@ -150,6 +179,7 @@ export class PanelChrome extends PureComponent<Props, State> {
             widthPixels={width}
             widthPixels={width}
             refreshCounter={refreshCounter}
             refreshCounter={refreshCounter}
             onDataResponse={this.onDataResponse}
             onDataResponse={this.onDataResponse}
+            onError={this.onDataError}
           >
           >
             {({ loading, panelData }) => {
             {({ loading, panelData }) => {
               return this.renderPanelPlugin(loading, panelData, width, height);
               return this.renderPanelPlugin(loading, panelData, width, height);
@@ -164,7 +194,7 @@ export class PanelChrome extends PureComponent<Props, State> {
 
 
   render() {
   render() {
     const { dashboard, panel } = this.props;
     const { dashboard, panel } = this.props;
-    const { timeInfo } = this.state;
+    const { errorMessage, timeInfo } = this.state;
     const { transparent } = panel;
     const { transparent } = panel;
 
 
     const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`;
     const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`;
@@ -185,8 +215,17 @@ export class PanelChrome extends PureComponent<Props, State> {
                 description={panel.description}
                 description={panel.description}
                 scopedVars={panel.scopedVars}
                 scopedVars={panel.scopedVars}
                 links={panel.links}
                 links={panel.links}
+                error={errorMessage}
               />
               />
-              {this.renderPanelBody(width, height)}
+              <ErrorBoundary>
+                {({ error, errorInfo }) => {
+                  if (errorInfo) {
+                    this.onPanelError(error.message || DEFAULT_PLUGIN_ERROR);
+                    return null;
+                  }
+                  return this.renderPanelBody(width, height);
+                }}
+              </ErrorBoundary>
             </div>
             </div>
           );
           );
         }}
         }}

+ 3 - 1
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx

@@ -18,6 +18,7 @@ export interface Props {
   description?: string;
   description?: string;
   scopedVars?: string;
   scopedVars?: string;
   links?: [];
   links?: [];
+  error?: string;
 }
 }
 
 
 interface ClickCoordinates {
 interface ClickCoordinates {
@@ -71,7 +72,7 @@ export class PanelHeader extends Component<Props, State> {
     const isFullscreen = false;
     const isFullscreen = false;
     const isLoading = false;
     const isLoading = false;
     const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
     const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
-    const { panel, dashboard, timeInfo, scopedVars } = this.props;
+    const { panel, dashboard, timeInfo, scopedVars, error } = this.props;
     const title = templateSrv.replaceWithText(panel.title, scopedVars);
     const title = templateSrv.replaceWithText(panel.title, scopedVars);
 
 
     return (
     return (
@@ -82,6 +83,7 @@ export class PanelHeader extends Component<Props, State> {
           description={panel.description}
           description={panel.description}
           scopedVars={panel.scopedVars}
           scopedVars={panel.scopedVars}
           links={panel.links}
           links={panel.links}
+          error={error}
         />
         />
         <div className={panelHeaderClass}>
         <div className={panelHeaderClass}>
           {isLoading && (
           {isLoading && (

+ 31 - 20
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 { LinkSrv } from 'app/features/panel/panellinks/link_srv';
 import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
 import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
 
 
-enum InfoModes {
+enum InfoMode {
   Error = 'Error',
   Error = 'Error',
   Info = 'Info',
   Info = 'Info',
   Links = 'Links',
   Links = 'Links',
@@ -18,18 +18,22 @@ interface Props {
   description?: string;
   description?: string;
   scopedVars?: string;
   scopedVars?: string;
   links?: [];
   links?: [];
+  error?: string;
 }
 }
 
 
 export class PanelHeaderCorner extends Component<Props> {
 export class PanelHeaderCorner extends Component<Props> {
   timeSrv: TimeSrv = getTimeSrv();
   timeSrv: TimeSrv = getTimeSrv();
 
 
   getInfoMode = () => {
   getInfoMode = () => {
-    const { panel } = this.props;
+    const { panel, error } = this.props;
+    if (error) {
+      return InfoMode.Error;
+    }
     if (!!panel.description) {
     if (!!panel.description) {
-      return InfoModes.Info;
+      return InfoMode.Info;
     }
     }
     if (panel.links && panel.links.length) {
     if (panel.links && panel.links.length) {
-      return InfoModes.Links;
+      return InfoMode.Links;
     }
     }
 
 
     return undefined;
     return undefined;
@@ -42,7 +46,7 @@ export class PanelHeaderCorner extends Component<Props> {
     const interpolatedMarkdown = templateSrv.replace(markdown, panel.scopedVars);
     const interpolatedMarkdown = templateSrv.replace(markdown, panel.scopedVars);
     const remarkableInterpolatedMarkdown = new Remarkable().render(interpolatedMarkdown);
     const remarkableInterpolatedMarkdown = new Remarkable().render(interpolatedMarkdown);
 
 
-    const html = (
+    return (
       <div className="markdown-html">
       <div className="markdown-html">
         <div dangerouslySetInnerHTML={{ __html: remarkableInterpolatedMarkdown }} />
         <div dangerouslySetInnerHTML={{ __html: remarkableInterpolatedMarkdown }} />
         {panel.links &&
         {panel.links &&
@@ -62,29 +66,36 @@ export class PanelHeaderCorner extends Component<Props> {
           )}
           )}
       </div>
       </div>
     );
     );
-
-    return html;
   };
   };
 
 
+  renderCornerType(infoMode: InfoMode, content: string | JSX.Element) {
+    const theme = infoMode === InfoMode.Error ? 'error' : 'info';
+    return (
+      <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" />
+        </div>
+      </Tooltip>
+    );
+  }
+
   render() {
   render() {
-    const infoMode: InfoModes | undefined = this.getInfoMode();
+    const infoMode: InfoMode | undefined = this.getInfoMode();
 
 
     if (!infoMode) {
     if (!infoMode) {
       return null;
       return null;
     }
     }
 
 
-    return (
-      <>
-        {infoMode === InfoModes.Info || infoMode === InfoModes.Links ? (
-          <Tooltip content={this.getInfoContent()} placement="bottom-start">
-            <div className={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}>
-              <i className="fa" />
-              <span className="panel-info-corner-inner" />
-            </div>
-          </Tooltip>
-        ) : null}
-      </>
-    );
+    if (infoMode === InfoMode.Error) {
+      return this.renderCornerType(infoMode, this.props.error);
+    }
+
+    if (infoMode === InfoMode.Info) {
+      return this.renderCornerType(infoMode, this.getInfoContent());
+    }
+
+    return null;
   }
   }
 }
 }
 
 

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

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