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

This commit is contained in:
Torkel Ödegaard 2019-02-14 15:18:55 +01:00
parent cc61d9c5cc
commit 2bc26a01f9
6 changed files with 114 additions and 60 deletions

View File

@ -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;

View File

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

View File

@ -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}`);
}
};

View File

@ -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);

View File

@ -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());
}

View File

@ -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}