From 2bc26a01f98cd77e5dc8159ce841cea93a317b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 14 Feb 2019 15:18:55 +0100 Subject: [PATCH] Fixes to error handling and clearing, also publishing of legacy events so old query editors work with react panels fully --- packages/grafana-ui/src/types/datasource.ts | 10 +++ public/app/core/services/AngularLoader.ts | 4 +- .../features/dashboard/dashgrid/DataPanel.tsx | 20 +++-- .../dashboard/dashgrid/PanelChrome.tsx | 46 ++++++++---- .../PanelHeader/PanelHeaderCorner.tsx | 19 ++--- .../dashboard/panel_editor/QueryEditorRow.tsx | 75 ++++++++++++------- 6 files changed, 114 insertions(+), 60 deletions(-) diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index e34cf25dc01..a34f39b59c6 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/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 { timezone: string; range: TimeRange; diff --git a/public/app/core/services/AngularLoader.ts b/public/app/core/services/AngularLoader.ts index 54dd9a35767..d9b78e66cba 100644 --- a/public/app/core/services/AngularLoader.ts +++ b/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; diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 75c931ec14e..c8d1947f892 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/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 { 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}`); } }; diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index db5a6c62483..5a993293946 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/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 { @@ -49,8 +48,7 @@ export class PanelChrome extends PureComponent { this.state = { refreshCounter: 0, renderCounter: 0, - loading: LoadingState.NotStarted, - errorMessage: '', + errorMessage: null, }; } @@ -94,8 +92,33 @@ export class PanelChrome extends PureComponent { 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 { 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 { 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 { {({ 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); diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx index ad686713afc..ef2fb69358d 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx +++ b/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 { 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 { ); }; - renderCornerType(infoMode: InfoModes, content: string | JSX.Element) { + renderCornerType(infoMode: InfoMode, content: string | JSX.Element) { + const theme = infoMode === InfoMode.Error ? 'error' : 'info'; return ( - +
@@ -80,17 +81,17 @@ export class PanelHeaderCorner extends Component { } 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()); } diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx index cf89175a5ee..0b8d2c39908 100644 --- a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx +++ b/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 { 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 { 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 { 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 { } 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 { } }; - 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 { }; 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 { 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 { {isCollapsed &&
{this.renderCollapsedText()}
}
- {this.hasTextEditMode && ( + {hasTextEditMode && (