From 688283a5cc74ed48e81414f27484d9b7be073780 Mon Sep 17 00:00:00 2001 From: Lukas Siatka Date: Wed, 11 Mar 2020 10:01:58 +0100 Subject: [PATCH] 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 --- public/app/core/utils/explore.ts | 5 + .../features/explore/ErrorContainer.test.tsx | 36 ++++ .../app/features/explore/ErrorContainer.tsx | 11 +- public/app/features/explore/Explore.test.tsx | 165 ++++++++++++++++++ public/app/features/explore/Explore.tsx | 9 +- public/app/features/explore/QueryRow.tsx | 76 ++++---- .../ErrorContainer.test.tsx.snap | 33 ++++ .../__snapshots__/Explore.test.tsx.snap | 147 ++++++++++++++++ .../components/ElasticsearchQueryField.tsx | 3 +- .../loki/components/LokiQueryFieldForm.tsx | 7 - .../prometheus/components/PromQueryField.tsx | 8 +- 11 files changed, 439 insertions(+), 61 deletions(-) create mode 100644 public/app/features/explore/ErrorContainer.test.tsx create mode 100644 public/app/features/explore/Explore.test.tsx create mode 100644 public/app/features/explore/__snapshots__/ErrorContainer.test.tsx.snap create mode 100644 public/app/features/explore/__snapshots__/Explore.test.tsx.snap diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 8a44a585908..4d5962babc5 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -541,3 +541,8 @@ export function getIntervals(range: TimeRange, lowLimit: string, resolution: num export function deduplicateLogRowsById(rows: LogRowModel[]) { return _.uniqBy(rows, 'uid'); } + +export const getFirstNonQueryRowSpecificError = (queryErrors?: DataQueryError[]) => { + const refId = getValueWithRefId(queryErrors); + return refId ? null : getFirstQueryErrorWithoutRefId(queryErrors); +}; diff --git a/public/app/features/explore/ErrorContainer.test.tsx b/public/app/features/explore/ErrorContainer.test.tsx new file mode 100644 index 00000000000..d55ae14e099 --- /dev/null +++ b/public/app/features/explore/ErrorContainer.test.tsx @@ -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(); + return wrapper; +}; + +describe('ErrorContainer', () => { + it('should render component', () => { + const wrapper = setup(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/explore/ErrorContainer.tsx b/public/app/features/explore/ErrorContainer.tsx index 1e2c98138ec..57d5dcf24c1 100644 --- a/public/app/features/explore/ErrorContainer.tsx +++ b/public/app/features/explore/ErrorContainer.tsx @@ -1,16 +1,13 @@ import React, { FunctionComponent } from 'react'; import { DataQueryError } from '@grafana/data'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; -import { getFirstQueryErrorWithoutRefId, getValueWithRefId } from 'app/core/utils/explore'; -interface Props { - queryErrors?: DataQueryError[]; +export interface ErrorContainerProps { + queryError?: DataQueryError; } -export const ErrorContainer: FunctionComponent = props => { - const { queryErrors } = props; - const refId = getValueWithRefId(queryErrors); - const queryError = refId ? null : getFirstQueryErrorWithoutRefId(queryErrors); +export const ErrorContainer: FunctionComponent = props => { + const { queryError } = props; const showError = queryError ? true : false; const duration = showError ? 100 : 10; const message = queryError ? queryError.message : null; diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx new file mode 100644 index 00000000000..a4c963c6ce6 --- /dev/null +++ b/public/app/features/explore/Explore.test.tsx @@ -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( + + + + ); +}; + +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: '', + }); + }); +}); diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 58004b67cfa..65de938cf59 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -50,6 +50,7 @@ import { getTimeRangeFromUrl, getTimeRange, lastUsedDatasourceKeyForOrgId, + getFirstNonQueryRowSpecificError, } from 'app/core/utils/explore'; import { Emitter } from 'app/core/utils/emitter'; import { ExploreToolbar } from './ExploreToolbar'; @@ -72,7 +73,7 @@ const getStyles = stylesFactory(() => { }; }); -interface ExploreProps { +export interface ExploreProps { changeSize: typeof changeSize; datasourceInstance: DataSourceApi; datasourceMissing: boolean; @@ -294,6 +295,10 @@ export class Explore extends React.PureComponent { const StartPage = datasourceInstance?.components?.ExploreStartPage; 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 (
@@ -323,7 +328,7 @@ export class Explore extends React.PureComponent { {'\xA0' + 'Query history'}
- + {({ width }) => { if (width === 0) { diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx index cf9a311ed3e..a758459f21e 100644 --- a/public/app/features/explore/QueryRow.tsx +++ b/public/app/features/explore/QueryRow.tsx @@ -26,6 +26,7 @@ import { import { ExploreItemState, ExploreId } from 'app/types/explore'; import { Emitter } from 'app/core/utils/emitter'; import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes'; +import { ErrorContainer } from './ErrorContainer'; interface PropsFromParent { exploreId: ExploreId; @@ -137,43 +138,46 @@ export class QueryRow extends PureComponent { } return ( -
-
- {QueryField ? ( - - ) : ( - - )} + <> +
+
+ {QueryField ? ( + + ) : ( + + )} +
+
- -
+ {queryErrors.length > 0 && } + ); } } diff --git a/public/app/features/explore/__snapshots__/ErrorContainer.test.tsx.snap b/public/app/features/explore/__snapshots__/ErrorContainer.test.tsx.snap new file mode 100644 index 00000000000..4fe09accde1 --- /dev/null +++ b/public/app/features/explore/__snapshots__/ErrorContainer.test.tsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ErrorContainer should render component 1`] = ` + +
+
+
+ +
+
+
+ Error message +
+
+
+
+
+`; diff --git a/public/app/features/explore/__snapshots__/Explore.test.tsx.snap b/public/app/features/explore/__snapshots__/Explore.test.tsx.snap new file mode 100644 index 00000000000..1ce9e949c29 --- /dev/null +++ b/public/app/features/explore/__snapshots__/Explore.test.tsx.snap @@ -0,0 +1,147 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Explore should render component 1`] = ` + + + +`; diff --git a/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx b/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx index 79f066fed84..aca6286dc54 100644 --- a/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx @@ -59,7 +59,7 @@ class ElasticsearchQueryField extends React.PureComponent { }; render() { - const { data, query } = this.props; + const { query } = this.props; const { syntaxLoaded } = this.state; return ( @@ -77,7 +77,6 @@ class ElasticsearchQueryField extends React.PureComponent { />
- {data && data.error ?
{data.error.message}
: null} ); } diff --git a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx b/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx index 5b2db1dd0c4..878ce7541df 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx @@ -137,7 +137,6 @@ export class LokiQueryFieldForm extends React.PureComponent 0; const chooserText = getChooserText(syntaxLoaded, hasLogLabels); const buttonDisabled = !(syntaxLoaded && hasLogLabels); - const showError = data && data.error && data.error.refId === query.refId; return ( <> @@ -183,11 +181,6 @@ export class LokiQueryFieldForm extends React.PureComponent {ExtraFieldElement} - {showError ? ( -
-
{data.error.message}
-
- ) : null} ); } diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 144b8e6c8d8..f0390639d91 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -292,12 +292,11 @@ class PromQueryField extends React.PureComponent 0); - const showError = data && data.error && data.error.refId === query.refId; return ( <> @@ -324,11 +323,6 @@ class PromQueryField extends React.PureComponent {ExtraFieldElement} - {showError ? ( -
-
{data.error.message}
-
- ) : null} {hint ? (