瀏覽代碼

Merge pull request #14763 from grafana/14528-panel-errors

Panel errors
Torkel Ödegaard 7 年之前
父節點
當前提交
a9808ef518

+ 1 - 0
packages/grafana-ui/src/visualizations/Graph/Graph.tsx

@@ -98,6 +98,7 @@ export class Graph extends PureComponent<GraphProps> {
       $.plot(this.element, timeSeries, flotOptions);
     } catch (err) {
       console.log('Graph rendering error', err, flotOptions, timeSeries);
+      throw new Error('Error rendering panel');
     }
   }
 

+ 44 - 0
public/app/core/components/ErrorBoundary/ErrorBoundary.tsx

@@ -0,0 +1,44 @@
+import { Component } from 'react';
+
+interface ErrorInfo {
+  componentStack: string;
+}
+
+interface RenderProps {
+  error: Error;
+  errorInfo: ErrorInfo;
+}
+
+interface Props {
+  children: (r: RenderProps) => JSX.Element;
+}
+
+interface State {
+  error: Error;
+  errorInfo: ErrorInfo;
+}
+
+class ErrorBoundary extends Component<Props, State> {
+  readonly state: State = {
+    error: null,
+    errorInfo: null,
+  };
+
+  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+    this.setState({
+      error: error,
+      errorInfo: errorInfo
+    });
+  }
+
+  render() {
+    const { children } = this.props;
+    const { error, errorInfo } = this.state;
+    return children({
+      error,
+      errorInfo,
+    });
+  }
+}
+
+export default ErrorBoundary;

+ 10 - 2
public/app/core/components/Tooltip/Popper.tsx

@@ -3,6 +3,11 @@ import Portal from 'app/core/components/Portal/Portal';
 import { Manager, Popper as ReactPopper, Reference } from 'react-popper';
 import Transition from 'react-transition-group/Transition';
 
+export enum Themes {
+  Default = 'popper__background--default',
+  Error = 'popper__background--error',
+}
+
 const defaultTransitionStyles = {
   transition: 'opacity 200ms linear',
   opacity: 0,
@@ -21,13 +26,16 @@ interface Props {
   placement?: any;
   content: string | ((props: any) => JSX.Element);
   refClassName?: string;
+  theme?: Themes;
 }
 
 class Popper extends PureComponent<Props> {
   render() {
-    const { children, renderContent, show, placement, refClassName } = this.props;
+    const { children, renderContent, show, placement, refClassName, theme } = this.props;
     const { content } = this.props;
 
+    const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : '');
+
     return (
       <Manager>
         <Reference>
@@ -53,7 +61,7 @@ class Popper extends PureComponent<Props> {
                       data-placement={placement}
                       className="popper"
                     >
-                      <div className="popper__background">
+                      <div className={popperBackgroundClassName}>
                         {renderContent(content)}
                         <div ref={arrowProps.ref} data-placement={placement} className="popper__arrow" />
                       </div>

+ 3 - 2
public/app/core/components/Tooltip/withPopper.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-
+import { Themes } from './Popper';
 export interface UsingPopperProps {
   showPopper: (prevState: object) => void;
   hidePopper: (prevState: object) => void;
@@ -9,6 +9,7 @@ export interface UsingPopperProps {
   content: string | ((props: any) => JSX.Element);
   className?: string;
   refClassName?: string;
+  theme?: Themes;
 }
 
 interface Props {
@@ -16,6 +17,7 @@ interface Props {
   className?: string;
   refClassName?: string;
   content: string | ((props: any) => JSX.Element);
+  theme?: Themes;
 }
 
 interface State {
@@ -71,7 +73,6 @@ export default function withPopper(WrappedComponent) {
     render() {
       const { show, placement } = this.state;
       const className = this.props.className || '';
-
       return (
         <WrappedComponent
           {...this.props}

+ 52 - 11
public/app/features/dashboard/dashgrid/DataPanel.tsx

@@ -1,5 +1,7 @@
 // Library
 import React, { Component } from 'react';
+import Tooltip from 'app/core/components/Tooltip/Tooltip';
+import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary';
 
 // Services
 import { getDatasourceSrv, DatasourceSrv } from 'app/features/plugins/datasource_srv';
@@ -10,6 +12,9 @@ import kbn from 'app/core/utils/kbn';
 // Types
 import { DataQueryOptions, DataQueryResponse } from 'app/types';
 import { TimeRange, TimeSeries, LoadingState } from '@grafana/ui';
+import { Themes } from 'app/core/components/Tooltip/Popper';
+
+const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
 
 interface RenderProps {
   loading: LoadingState;
@@ -33,6 +38,7 @@ export interface Props {
 export interface State {
   isFirstLoad: boolean;
   loading: LoadingState;
+  errorMessage: string;
   response: DataQueryResponse;
 }
 
@@ -51,6 +57,7 @@ export class DataPanel extends Component<Props, State> {
 
     this.state = {
       loading: LoadingState.NotStarted,
+      errorMessage: '',
       response: {
         data: [],
       },
@@ -90,7 +97,7 @@ export class DataPanel extends Component<Props, State> {
       return;
     }
 
-    this.setState({ loading: LoadingState.Loading });
+    this.setState({ loading: LoadingState.Loading, errorMessage: '' });
 
     try {
       const ds = await this.dataSourceSrv.get(datasource);
@@ -128,10 +135,20 @@ export class DataPanel extends Component<Props, State> {
       });
     } catch (err) {
       console.log('Loading error', err);
-      this.setState({ loading: LoadingState.Error, isFirstLoad: false });
+      this.onError('Request Error');
     }
   };
 
+  onError = (errorMessage: string) => {
+    if (this.state.loading !== LoadingState.Error || this.state.errorMessage !== errorMessage) {
+      this.setState({
+        loading: LoadingState.Error,
+        isFirstLoad: false,
+        errorMessage: errorMessage
+      });
+    }
+  }
+
   render() {
     const { queries } = this.props;
     const { response, loading, isFirstLoad } = this.state;
@@ -139,7 +156,7 @@ export class DataPanel extends Component<Props, State> {
     const timeSeries = response.data;
 
     if (isFirstLoad && loading === LoadingState.Loading) {
-      return this.renderLoadingSpinner();
+      return this.renderLoadingStates();
     }
 
     if (!queries.length) {
@@ -152,24 +169,48 @@ export class DataPanel extends Component<Props, State> {
 
     return (
       <>
-        {this.renderLoadingSpinner()}
-        {this.props.children({
-          timeSeries,
-          loading,
-        })}
+        {this.renderLoadingStates()}
+        <ErrorBoundary>
+          {({error, errorInfo}) => {
+            if (errorInfo) {
+              this.onError(error.message || DEFAULT_PLUGIN_ERROR);
+              return null;
+            }
+            return (
+              <>
+                {this.props.children({
+                  timeSeries,
+                  loading,
+                })}
+              </>
+            );
+          }}
+        </ErrorBoundary>
       </>
     );
   }
 
-  private renderLoadingSpinner(): JSX.Element {
-    const { loading } = this.state;
-
+  private renderLoadingStates(): JSX.Element {
+    const { loading, errorMessage } = this.state;
     if (loading === LoadingState.Loading) {
       return (
         <div className="panel-loading">
           <i className="fa fa-spinner fa-spin" />
         </div>
       );
+    } else if (loading === LoadingState.Error) {
+      return (
+        <Tooltip
+          content={errorMessage}
+          className="popper__manager--block"
+          refClassName={`panel-info-corner panel-info-corner--error`}
+          placement="bottom-start"
+          theme={Themes.Error}
+        >
+          <i className="fa" />
+          <span className="panel-info-corner-inner" />
+        </Tooltip>
+      );
     }
 
     return null;

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

@@ -87,7 +87,6 @@ export class PanelChrome extends PureComponent<Props, State> {
     const { datasource, targets, transparent } = panel;
     const PanelComponent = plugin.exports.Panel;
     const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`;
-
     return (
       <AutoSizer>
         {({ width, height }) => {

+ 0 - 4
public/app/plugins/panel/graph2/GraphPanel.tsx

@@ -10,10 +10,6 @@ import { Options } from './types';
 interface Props extends PanelProps<Options> {}
 
 export class GraphPanel extends PureComponent<Props> {
-  constructor(props) {
-    super(props);
-  }
-
   render() {
     const { timeSeries, timeRange, width, height } = this.props;
     const { showLines, showBars, showPoints } = this.props.options;

+ 5 - 2
public/sass/_variables.dark.scss

@@ -103,6 +103,7 @@ $panel-bg: #212124;
 $panel-border-color: $dark-1;
 $panel-border: solid 1px $panel-border-color;
 $panel-header-hover-bg: $dark-4;
+$panel-corner: $panel-bg;
 
 // page header
 $page-header-bg: linear-gradient(90deg, #292a2d, black);
@@ -302,12 +303,14 @@ $popover-error-bg: $btn-danger-bg;
 // Tooltips and popovers
 // -------------------------
 $tooltipColor: $popover-help-color;
-$tooltipBackground: $popover-help-bg;
 $tooltipArrowWidth: 5px;
-$tooltipArrowColor: $tooltipBackground;
 $tooltipLinkColor: $link-color;
 $graph-tooltip-bg: $dark-1;
 
+$tooltipBackground: $popover-help-bg;
+$tooltipArrowColor: $tooltipBackground;
+$tooltipBackgroundError: $brand-danger;
+
 // images
 $checkboxImageUrl: '../img/checkbox.png';
 

+ 5 - 2
public/sass/_variables.light.scss

@@ -102,6 +102,7 @@ $panel-bg: $white;
 $panel-border-color: $gray-5;
 $panel-border: solid 1px $panel-border-color;
 $panel-header-hover-bg: $gray-6;
+$panel-corner: $gray-4;
 
 // Page header
 $page-header-bg: linear-gradient(90deg, $white, $gray-7);
@@ -307,12 +308,14 @@ $popover-error-bg: $btn-danger-bg;
 // Tooltips and popovers
 // -------------------------
 $tooltipColor: $popover-help-color;
-$tooltipBackground: $popover-help-bg;
 $tooltipArrowWidth: 5px;
-$tooltipArrowColor: $tooltipBackground;
 $tooltipLinkColor: lighten($popover-help-color, 5%);
 $graph-tooltip-bg: $gray-5;
 
+$tooltipBackground: $popover-help-bg;
+$tooltipArrowColor: $tooltipBackground; // Used by Angular tooltip
+$tooltipBackgroundError: $brand-danger;
+
 // images
 $checkboxImageUrl: '../img/checkbox_white.png';
 

+ 17 - 9
public/sass/components/_popper.scss

@@ -8,7 +8,22 @@ $popper-margin-from-ref: 5px;
   text-align: center;
 }
 
-.popper .popper__arrow {
+.popper__background {
+  background: $tooltipBackground;
+  border-radius: $border-radius;
+  box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
+  padding: 10px;
+
+  // Themes
+  &.popper__background--error {
+    background: $tooltipBackgroundError;
+    .popper__arrow {
+      border-color: $tooltipBackgroundError;
+    }
+  }
+}
+
+.popper__arrow {
   width: 0;
   height: 0;
   border-style: solid;
@@ -16,17 +31,10 @@ $popper-margin-from-ref: 5px;
   margin: 0px;
 }
 
-.popper .popper__arrow {
+.popper__arrow {
   border-color: $tooltipBackground;
 }
 
-.popper__background {
-  background: $tooltipBackground;
-  border-radius: $border-radius;
-  box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
-  padding: 10px;
-}
-
 // Top
 .popper[data-placement^='top'] {
   padding-bottom: $popper-margin-from-ref;

+ 3 - 3
public/sass/pages/_dashboard.scss

@@ -214,7 +214,7 @@ div.flot-text {
 
   &--info {
     display: block;
-    @include panel-corner-color(lighten($panel-bg, 4%));
+    @include panel-corner-color(lighten($panel-corner, 4%));
     .fa:before {
       content: '\f129';
     }
@@ -222,7 +222,7 @@ div.flot-text {
 
   &--links {
     display: block;
-    @include panel-corner-color(lighten($panel-bg, 4%));
+    @include panel-corner-color(lighten($panel-corner, 4%));
     .fa {
       left: 4px;
     }
@@ -233,7 +233,7 @@ div.flot-text {
 
   &--error {
     display: block;
-    color: $text-color;
+    color: $white;
     @include panel-corner-color($popover-error-bg);
     .fa:before {
       content: '\f12a';