Explore: adds QueryRowErrors component, moves error display to QueryRow (#22438)

* Explore: adds QueryRowErrors component

* Explore: updates QueryRow to use QueryRowErrors component

* Explore: updates PromQueryField to remove error render

* Explore: updates Elastic query field  to remove error render

* Explore: updates LokiQueryFieldForm to remove error render

* Explore: updates QueryRow component - brings back passing errors down

* Explore: removes QueryRowErrors component

* Explore: updates ErrorContainer component - moves out data filtering

* Explore: updates QueryRow component - changes QueryRowErrors to ErrorContainer

* Explore: updates Explore component - adds error filtering for ErrorContainer

* Explore: updates ErrorContainer and adds a basic test for it

* Explore: updates Explore component props name and adds a basic render test

* Explore: adds snapshots for Explore and ErrorContainer

* Explore: adds a test for error render

* Explore: adds a comment to Explore component explaining the way we filter non-query-row-specific errors

* Explore: adds getFirstNonQueryRowSpecificError method to explore utilities

* Explore: extracts getFirstNonQueryRowSpecificError method and slightly refactors Explore component

* Explore: updates Explore component tests to cover non-query-row-specific errors
This commit is contained in:
Lukas Siatka 2020-03-11 10:01:58 +01:00 committed by GitHub
parent bbed213115
commit 688283a5cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 439 additions and 61 deletions

View File

@ -541,3 +541,8 @@ export function getIntervals(range: TimeRange, lowLimit: string, resolution: num
export function deduplicateLogRowsById(rows: LogRowModel[]) { export function deduplicateLogRowsById(rows: LogRowModel[]) {
return _.uniqBy(rows, 'uid'); return _.uniqBy(rows, 'uid');
} }
export const getFirstNonQueryRowSpecificError = (queryErrors?: DataQueryError[]) => {
const refId = getValueWithRefId(queryErrors);
return refId ? null : getFirstQueryErrorWithoutRefId(queryErrors);
};

View File

@ -0,0 +1,36 @@
import React from 'react';
import { DataQueryError } from '@grafana/data';
import { shallow } from 'enzyme';
import { ErrorContainer } from './ErrorContainer';
const makeError = (propOverrides?: DataQueryError): DataQueryError => {
const queryError: DataQueryError = {
data: {
message: 'Error data message',
error: 'Error data content',
},
message: 'Error message',
status: 'Error status',
statusText: 'Error status text',
refId: 'A',
cancelled: false,
};
Object.assign(queryError, propOverrides);
return queryError;
};
const setup = (propOverrides?: object) => {
const props = {
queryError: makeError(propOverrides),
};
const wrapper = shallow(<ErrorContainer {...props} />);
return wrapper;
};
describe('ErrorContainer', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,16 +1,13 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import { DataQueryError } from '@grafana/data'; import { DataQueryError } from '@grafana/data';
import { FadeIn } from 'app/core/components/Animations/FadeIn'; import { FadeIn } from 'app/core/components/Animations/FadeIn';
import { getFirstQueryErrorWithoutRefId, getValueWithRefId } from 'app/core/utils/explore';
interface Props { export interface ErrorContainerProps {
queryErrors?: DataQueryError[]; queryError?: DataQueryError;
} }
export const ErrorContainer: FunctionComponent<Props> = props => { export const ErrorContainer: FunctionComponent<ErrorContainerProps> = props => {
const { queryErrors } = props; const { queryError } = props;
const refId = getValueWithRefId(queryErrors);
const queryError = refId ? null : getFirstQueryErrorWithoutRefId(queryErrors);
const showError = queryError ? true : false; const showError = queryError ? true : false;
const duration = showError ? 100 : 10; const duration = showError ? 100 : 10;
const message = queryError ? queryError.message : null; const message = queryError ? queryError.message : null;

View File

@ -0,0 +1,165 @@
import React from 'react';
import {
DataSourceApi,
LoadingState,
ExploreMode,
toUtc,
DataQueryError,
DataQueryRequest,
CoreApp,
} from '@grafana/data';
import { getFirstNonQueryRowSpecificError } from 'app/core/utils/explore';
import { ExploreId } from 'app/types/explore';
import { shallow } from 'enzyme';
import { Explore, ExploreProps } from './Explore';
import { scanStopAction } from './state/actionTypes';
import { toggleGraph } from './state/actions';
import { Provider } from 'react-redux';
import { configureStore } from 'app/store/configureStore';
const setup = (renderMethod: any, propOverrides?: object) => {
const props: ExploreProps = {
changeSize: jest.fn(),
datasourceInstance: {
meta: {
metrics: true,
logs: true,
},
components: {
ExploreStartPage: {},
},
} as DataSourceApi,
datasourceMissing: false,
exploreId: ExploreId.left,
initializeExplore: jest.fn(),
initialized: true,
modifyQueries: jest.fn(),
update: {
datasource: false,
queries: false,
range: false,
mode: false,
ui: false,
},
refreshExplore: jest.fn(),
scanning: false,
scanRange: {
from: '0',
to: '0',
},
scanStart: jest.fn(),
scanStopAction: scanStopAction,
setQueries: jest.fn(),
split: false,
queryKeys: [],
initialDatasource: 'test',
initialQueries: [],
initialRange: {
from: toUtc('2019-01-01 10:00:00'),
to: toUtc('2019-01-01 16:00:00'),
raw: {
from: 'now-6h',
to: 'now',
},
},
mode: ExploreMode.Metrics,
initialUI: {
showingTable: false,
showingGraph: false,
showingLogs: false,
},
isLive: false,
syncedTimes: false,
updateTimeRange: jest.fn(),
graphResult: [],
loading: false,
absoluteRange: {
from: 0,
to: 0,
},
showingGraph: false,
showingTable: false,
timeZone: 'UTC',
onHiddenSeriesChanged: jest.fn(),
toggleGraph: toggleGraph,
queryResponse: {
state: LoadingState.NotStarted,
series: [],
request: ({
requestId: '1',
dashboardId: 0,
interval: '1s',
panelId: 1,
scopedVars: {
apps: {
value: 'value',
},
},
targets: [
{
refId: 'A',
},
],
timezone: 'UTC',
app: CoreApp.Explore,
startTime: 0,
} as unknown) as DataQueryRequest,
error: {} as DataQueryError,
timeRange: {
from: toUtc('2019-01-01 10:00:00'),
to: toUtc('2019-01-01 16:00:00'),
raw: {
from: 'now-6h',
to: 'now',
},
},
},
originPanelId: 1,
addQueryRow: jest.fn(),
};
const store = configureStore();
Object.assign(props, propOverrides);
return renderMethod(
<Provider store={store}>
<Explore {...props} />
</Provider>
);
};
const setupErrors = (hasRefId?: boolean) => {
return [
{
message: 'Error message',
status: '400',
statusText: 'Bad Request',
refId: hasRefId ? 'A' : '',
},
];
};
describe('Explore', () => {
it('should render component', () => {
const wrapper = setup(shallow);
expect(wrapper).toMatchSnapshot();
});
it('should filter out a query-row-specific error when looking for non-query-row-specific errors', async () => {
const queryErrors = setupErrors(true);
const queryError = getFirstNonQueryRowSpecificError(queryErrors);
expect(queryError).toBeNull();
});
it('should not filter out a generic error when looking for non-query-row-specific errors', async () => {
const queryErrors = setupErrors();
const queryError = getFirstNonQueryRowSpecificError(queryErrors);
expect(queryError).not.toBeNull();
expect(queryError).toEqual({
message: 'Error message',
status: '400',
statusText: 'Bad Request',
refId: '',
});
});
});

View File

@ -50,6 +50,7 @@ import {
getTimeRangeFromUrl, getTimeRangeFromUrl,
getTimeRange, getTimeRange,
lastUsedDatasourceKeyForOrgId, lastUsedDatasourceKeyForOrgId,
getFirstNonQueryRowSpecificError,
} from 'app/core/utils/explore'; } from 'app/core/utils/explore';
import { Emitter } from 'app/core/utils/emitter'; import { Emitter } from 'app/core/utils/emitter';
import { ExploreToolbar } from './ExploreToolbar'; import { ExploreToolbar } from './ExploreToolbar';
@ -72,7 +73,7 @@ const getStyles = stylesFactory(() => {
}; };
}); });
interface ExploreProps { export interface ExploreProps {
changeSize: typeof changeSize; changeSize: typeof changeSize;
datasourceInstance: DataSourceApi; datasourceInstance: DataSourceApi;
datasourceMissing: boolean; datasourceMissing: boolean;
@ -294,6 +295,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
const StartPage = datasourceInstance?.components?.ExploreStartPage; const StartPage = datasourceInstance?.components?.ExploreStartPage;
const showStartPage = !queryResponse || queryResponse.state === LoadingState.NotStarted; const showStartPage = !queryResponse || queryResponse.state === LoadingState.NotStarted;
// gets an error without a refID, so non-query-row-related error, like a connection error
const queryErrors = queryResponse.error ? [queryResponse.error] : undefined;
const queryError = getFirstNonQueryRowSpecificError(queryErrors);
return ( return (
<div className={exploreClass} ref={this.getRef}> <div className={exploreClass} ref={this.getRef}>
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} /> <ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} />
@ -323,7 +328,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
<span className="btn-title">{'\xA0' + 'Query history'}</span> <span className="btn-title">{'\xA0' + 'Query history'}</span>
</button> </button>
</div> </div>
<ErrorContainer queryErrors={queryResponse.error ? [queryResponse.error] : undefined} /> <ErrorContainer queryError={queryError} />
<AutoSizer onResize={this.onResize} disableHeight> <AutoSizer onResize={this.onResize} disableHeight>
{({ width }) => { {({ width }) => {
if (width === 0) { if (width === 0) {

View File

@ -26,6 +26,7 @@ import {
import { ExploreItemState, ExploreId } from 'app/types/explore'; import { ExploreItemState, ExploreId } from 'app/types/explore';
import { Emitter } from 'app/core/utils/emitter'; import { Emitter } from 'app/core/utils/emitter';
import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes'; import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes';
import { ErrorContainer } from './ErrorContainer';
interface PropsFromParent { interface PropsFromParent {
exploreId: ExploreId; exploreId: ExploreId;
@ -137,43 +138,46 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
} }
return ( return (
<div className="query-row"> <>
<div className="query-row-field flex-shrink-1"> <div className="query-row">
{QueryField ? ( <div className="query-row-field flex-shrink-1">
<QueryField {QueryField ? (
datasource={datasourceInstance} <QueryField
query={query} datasource={datasourceInstance}
history={history} query={query}
onRunQuery={this.onRunQuery} history={history}
onBlur={noopOnBlur} onRunQuery={this.onRunQuery}
onChange={this.onChange} onBlur={noopOnBlur}
data={queryResponse} onChange={this.onChange}
absoluteRange={absoluteRange} data={queryResponse}
exploreMode={mode} absoluteRange={absoluteRange}
/> exploreMode={mode}
) : ( />
<QueryEditor ) : (
error={queryErrors} <QueryEditor
datasource={datasourceInstance} error={queryErrors}
onQueryChange={this.onChange} datasource={datasourceInstance}
onExecuteQuery={this.onRunQuery} onQueryChange={this.onChange}
initialQuery={query} onExecuteQuery={this.onRunQuery}
exploreEvents={exploreEvents} initialQuery={query}
range={range} exploreEvents={exploreEvents}
textEditModeEnabled={this.state.textEditModeEnabled} range={range}
/> textEditModeEnabled={this.state.textEditModeEnabled}
)} />
)}
</div>
<QueryRowActions
canToggleEditorModes={canToggleEditorModes}
isDisabled={query.hide}
isNotStarted={isNotStarted}
latency={latency}
onClickToggleEditorMode={this.onClickToggleEditorMode}
onClickToggleDisabled={this.onClickToggleDisabled}
onClickRemoveButton={this.onClickRemoveButton}
/>
</div> </div>
<QueryRowActions {queryErrors.length > 0 && <ErrorContainer queryError={queryErrors[0]} />}
canToggleEditorModes={canToggleEditorModes} </>
isDisabled={query.hide}
isNotStarted={isNotStarted}
latency={latency}
onClickToggleEditorMode={this.onClickToggleEditorMode}
onClickToggleDisabled={this.onClickToggleDisabled}
onClickRemoveButton={this.onClickRemoveButton}
/>
</div>
); );
} }
} }

View File

@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ErrorContainer should render component 1`] = `
<Component
duration={100}
in={true}
>
<div
className="alert-container"
>
<div
className="alert-error alert"
>
<div
className="alert-icon"
>
<i
className="fa fa-exclamation-triangle"
/>
</div>
<div
className="alert-body"
>
<div
className="alert-title"
>
Error message
</div>
</div>
</div>
</div>
</Component>
`;

View File

@ -0,0 +1,147 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Explore should render component 1`] = `
<ContextProvider
value={
Object {
"store": Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(observable): [Function],
},
"subscription": Subscription {
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(observable): [Function],
},
"unsubscribe": null,
},
}
}
>
<Explore
absoluteRange={
Object {
"from": 0,
"to": 0,
}
}
addQueryRow={[MockFunction]}
changeSize={[MockFunction]}
datasourceInstance={
Object {
"components": Object {
"ExploreStartPage": Object {},
},
"meta": Object {
"logs": true,
"metrics": true,
},
}
}
datasourceMissing={false}
exploreId="left"
graphResult={Array []}
initialDatasource="test"
initialQueries={Array []}
initialRange={
Object {
"from": "2019-01-01T10:00:00.000Z",
"raw": Object {
"from": "now-6h",
"to": "now",
},
"to": "2019-01-01T16:00:00.000Z",
}
}
initialUI={
Object {
"showingGraph": false,
"showingLogs": false,
"showingTable": false,
}
}
initializeExplore={[MockFunction]}
initialized={true}
isLive={false}
loading={false}
mode="Metrics"
modifyQueries={[MockFunction]}
onHiddenSeriesChanged={[MockFunction]}
originPanelId={1}
queryKeys={Array []}
queryResponse={
Object {
"error": Object {},
"request": Object {
"app": "explore",
"dashboardId": 0,
"interval": "1s",
"panelId": 1,
"requestId": "1",
"scopedVars": Object {
"apps": Object {
"value": "value",
},
},
"startTime": 0,
"targets": Array [
Object {
"refId": "A",
},
],
"timezone": "UTC",
},
"series": Array [],
"state": "NotStarted",
"timeRange": Object {
"from": "2019-01-01T10:00:00.000Z",
"raw": Object {
"from": "now-6h",
"to": "now",
},
"to": "2019-01-01T16:00:00.000Z",
},
}
}
refreshExplore={[MockFunction]}
scanRange={
Object {
"from": "0",
"to": "0",
}
}
scanStart={[MockFunction]}
scanStopAction={[Function]}
scanning={false}
setQueries={[MockFunction]}
showingGraph={false}
showingTable={false}
split={false}
syncedTimes={false}
timeZone="UTC"
toggleGraph={[Function]}
update={
Object {
"datasource": false,
"mode": false,
"queries": false,
"range": false,
"ui": false,
}
}
updateTimeRange={[MockFunction]}
/>
</ContextProvider>
`;

View File

@ -59,7 +59,7 @@ class ElasticsearchQueryField extends React.PureComponent<Props, State> {
}; };
render() { render() {
const { data, query } = this.props; const { query } = this.props;
const { syntaxLoaded } = this.state; const { syntaxLoaded } = this.state;
return ( return (
@ -77,7 +77,6 @@ class ElasticsearchQueryField extends React.PureComponent<Props, State> {
/> />
</div> </div>
</div> </div>
{data && data.error ? <div className="prom-query-field-info text-error">{data.error.message}</div> : null}
</> </>
); );
} }

View File

@ -137,7 +137,6 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
render() { render() {
const { const {
ExtraFieldElement, ExtraFieldElement,
data,
query, query,
syntaxLoaded, syntaxLoaded,
logLabelOptions, logLabelOptions,
@ -150,7 +149,6 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
const hasLogLabels = logLabelOptions && logLabelOptions.length > 0; const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
const chooserText = getChooserText(syntaxLoaded, hasLogLabels); const chooserText = getChooserText(syntaxLoaded, hasLogLabels);
const buttonDisabled = !(syntaxLoaded && hasLogLabels); const buttonDisabled = !(syntaxLoaded && hasLogLabels);
const showError = data && data.error && data.error.refId === query.refId;
return ( return (
<> <>
@ -183,11 +181,6 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
</div> </div>
{ExtraFieldElement} {ExtraFieldElement}
</div> </div>
{showError ? (
<div className="query-row-break">
<div className="prom-query-field-info text-error">{data.error.message}</div>
</div>
) : null}
</> </>
); );
} }

View File

@ -292,12 +292,11 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
}; };
render() { render() {
const { data, query, ExtraFieldElement } = this.props; const { query, ExtraFieldElement } = this.props;
const { metricsOptions, syntaxLoaded, hint } = this.state; const { metricsOptions, syntaxLoaded, hint } = this.state;
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined; const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
const chooserText = getChooserText(syntaxLoaded, metricsOptions); const chooserText = getChooserText(syntaxLoaded, metricsOptions);
const buttonDisabled = !(syntaxLoaded && metricsOptions && metricsOptions.length > 0); const buttonDisabled = !(syntaxLoaded && metricsOptions && metricsOptions.length > 0);
const showError = data && data.error && data.error.refId === query.refId;
return ( return (
<> <>
@ -324,11 +323,6 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
</div> </div>
{ExtraFieldElement} {ExtraFieldElement}
</div> </div>
{showError ? (
<div className="query-row-break">
<div className="prom-query-field-info text-error">{data.error.message}</div>
</div>
) : null}
{hint ? ( {hint ? (
<div className="query-row-break"> <div className="query-row-break">
<div className="prom-query-field-info text-warning"> <div className="prom-query-field-info text-warning">