diff --git a/src/ui/public/courier/fetch/call_client.js b/src/ui/public/courier/fetch/call_client.js index 6d59a631c53bb..f830d52a2dcb3 100644 --- a/src/ui/public/courier/fetch/call_client.js +++ b/src/ui/public/courier/fetch/call_client.js @@ -161,7 +161,7 @@ export function CallClientProvider(Private, Promise, es, config) { return; } - const segregatedResponses = await Promise.all(abortableSearches.map(({ searching }) => searching)); + const segregatedResponses = await Promise.all(abortableSearches.map(({ searching }) => searching.catch((e) => [{ error: e }]))); // Assigning searchRequests to strategies means that the responses come back in a different // order than the original searchRequests. So we'll put them back in order so that we can diff --git a/src/ui/public/courier/fetch/call_response_handlers.js b/src/ui/public/courier/fetch/call_response_handlers.js index 4554608680c4a..766ea8af3a0e8 100644 --- a/src/ui/public/courier/fetch/call_response_handlers.js +++ b/src/ui/public/courier/fetch/call_response_handlers.js @@ -20,6 +20,7 @@ import { toastNotifications } from '../../notify'; import { RequestFailure } from '../../errors'; import { RequestStatus } from './req_status'; +import { SearchError } from '../search_strategy/search_error'; export function CallResponseHandlersProvider(Private, Promise) { const ABORTED = RequestStatus.ABORTED; @@ -58,7 +59,7 @@ export function CallResponseHandlersProvider(Private, Promise) { if (searchRequest.filterError(response)) { return progress(); } else { - return searchRequest.handleFailure(new RequestFailure(null, response)); + return searchRequest.handleFailure(response.error instanceof SearchError ? response.error : new RequestFailure(null, response)); } } diff --git a/src/ui/public/courier/fetch/request/search_request/search_request.js b/src/ui/public/courier/fetch/request/search_request/search_request.js index b1c075f4f1b97..1471df8d35223 100644 --- a/src/ui/public/courier/fetch/request/search_request/search_request.js +++ b/src/ui/public/courier/fetch/request/search_request/search_request.js @@ -123,7 +123,8 @@ export function SearchRequestProvider(Promise) { handleFailure(error) { this.success = false; - this.resp = error && error.resp; + this.resp = error; + this.resp = (error && error.resp) || error; return this.errorHandler(this, error); } diff --git a/src/ui/public/courier/index.d.ts b/src/ui/public/courier/index.d.ts index 72170adc2b129..c06518697422b 100644 --- a/src/ui/public/courier/index.d.ts +++ b/src/ui/public/courier/index.d.ts @@ -18,3 +18,4 @@ */ export * from './search_source'; +export * from './search_strategy'; diff --git a/src/ui/public/courier/index.js b/src/ui/public/courier/index.js index 583172f1731cd..244042317ebf2 100644 --- a/src/ui/public/courier/index.js +++ b/src/ui/public/courier/index.js @@ -34,4 +34,5 @@ export { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern, SearchError, + getSearchErrorType, } from './search_strategy'; diff --git a/src/ui/public/courier/search_strategy/index.d.ts b/src/ui/public/courier/search_strategy/index.d.ts new file mode 100644 index 0000000000000..dc98484655d00 --- /dev/null +++ b/src/ui/public/courier/search_strategy/index.d.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { SearchError, getSearchErrorType } from './search_error'; diff --git a/src/ui/public/courier/search_strategy/index.js b/src/ui/public/courier/search_strategy/index.js index 102610538df98..3f6d172426d0d 100644 --- a/src/ui/public/courier/search_strategy/index.js +++ b/src/ui/public/courier/search_strategy/index.js @@ -25,4 +25,4 @@ export { export { isDefaultTypeIndexPattern } from './is_default_type_index_pattern'; -export { SearchError } from './search_error'; +export { SearchError, getSearchErrorType } from './search_error'; diff --git a/src/ui/public/courier/search_strategy/search_error.d.ts b/src/ui/public/courier/search_strategy/search_error.d.ts new file mode 100644 index 0000000000000..bf49853957c75 --- /dev/null +++ b/src/ui/public/courier/search_strategy/search_error.d.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export type SearchError = any; +export type getSearchErrorType = any; diff --git a/src/ui/public/courier/search_strategy/search_error.js b/src/ui/public/courier/search_strategy/search_error.js index 9406ceb4beaeb..9c35d11a6abf4 100644 --- a/src/ui/public/courier/search_strategy/search_error.js +++ b/src/ui/public/courier/search_strategy/search_error.js @@ -18,13 +18,14 @@ */ export class SearchError extends Error { - constructor({ status, title, message, path }) { + constructor({ status, title, message, path, type }) { super(message); this.name = 'SearchError'; this.status = status; this.title = title; this.message = message; this.path = path; + this.type = type; // captureStackTrace is only available in the V8 engine, so any browser using // a different JS engine won't have access to this method. @@ -37,3 +38,10 @@ export class SearchError extends Error { Object.setPrototypeOf(this, SearchError.prototype); } } + +export function getSearchErrorType({ message }) { + const msg = message.toLowerCase(); + if(msg.indexOf('unsupported query') > -1) { + return 'UNSUPPORTED_QUERY'; + } +} diff --git a/src/ui/public/visualize/components/__snapshots__/visualization_requesterror.test.js.snap b/src/ui/public/visualize/components/__snapshots__/visualization_requesterror.test.js.snap new file mode 100644 index 0000000000000..7eabc9118f74b --- /dev/null +++ b/src/ui/public/visualize/components/__snapshots__/visualization_requesterror.test.js.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VisualizationRequestError should render according to snapshot 1`] = ` +
+
+
+ Request error +
+
+
+`; diff --git a/src/ui/public/visualize/components/visualization.less b/src/ui/public/visualize/components/visualization.less index fbd1ba39cd00c..5ff9117a3313e 100644 --- a/src/ui/public/visualize/components/visualization.less +++ b/src/ui/public/visualize/components/visualization.less @@ -65,7 +65,15 @@ display: flex; align-items: center; justify-content: center; - .top { align-self: flext-start; } + .top { align-self: flex-start; } .item { } - .bottom { align-self: flext-end; } + .bottom { align-self: flex-end; } +} + +/** + * 1. Prevent large request errors from overflowing the container + */ +.visualize-request-error { + max-width: 100%; + max-height: 100%; } diff --git a/src/ui/public/visualize/components/visualization.test.js b/src/ui/public/visualize/components/visualization.test.js index 1d768787ce193..cdeec28712850 100644 --- a/src/ui/public/visualize/components/visualization.test.js +++ b/src/ui/public/visualize/components/visualization.test.js @@ -79,6 +79,13 @@ describe('', () => { expect(wrapper.text()).toBe('No results found'); }); + it('should display error message when there is a request error that should be shown and no data', () => { + const errorVis = { ...vis, requestError: { message: 'Request error' }, showRequestError: true }; + const data = null; + const wrapper = render(); + expect(wrapper.text()).toBe('Request error'); + }); + it('should render chart when data is present', () => { const wrapper = render(); expect(wrapper.text()).not.toBe('No results found'); diff --git a/src/ui/public/visualize/components/visualization.tsx b/src/ui/public/visualize/components/visualization.tsx index 04dcd7851a560..ccf9f2d9a5cd2 100644 --- a/src/ui/public/visualize/components/visualization.tsx +++ b/src/ui/public/visualize/components/visualization.tsx @@ -25,6 +25,7 @@ import { memoizeLast } from '../../utils/memoize'; import { Vis } from '../../vis'; import { VisualizationChart } from './visualization_chart'; import { VisualizationNoResults } from './visualization_noresults'; +import { VisualizationRequestError } from './visualization_requesterror'; import './visualization.less'; @@ -37,6 +38,12 @@ function shouldShowNoResultsMessage(vis: Vis, visData: any): boolean { return Boolean(requiresSearch && isZeroHits && shouldShowMessage); } +function shouldShowRequestErrorMessage(vis: Vis, visData: any): boolean { + const requestError = get(vis, 'requestError'); + const showRequestError = get(vis, 'showRequestError'); + return Boolean(!visData && requestError && showRequestError); +} + interface VisualizationProps { listenOnChange: boolean; onInit?: () => void; @@ -63,10 +70,13 @@ export class Visualization extends React.Component { const { vis, visData, onInit, uiState } = this.props; const noResults = this.showNoResultsMessage(vis, visData); + const requestError = shouldShowRequestErrorMessage(vis, visData); return (
- {noResults ? ( + {requestError ? ( + + ) : noResults ? ( ) : ( diff --git a/src/ui/public/visualize/components/visualization_requesterror.test.js b/src/ui/public/visualize/components/visualization_requesterror.test.js new file mode 100644 index 0000000000000..6a07c9ec4e145 --- /dev/null +++ b/src/ui/public/visualize/components/visualization_requesterror.test.js @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { render } from 'enzyme'; +import { VisualizationRequestError } from './visualization_requesterror'; + +describe('VisualizationRequestError', () => { + it('should render according to snapshot', () => { + const wrapper = render(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should set html when error is an object', () => { + const wrapper = render(); + expect(wrapper.text()).toBe('Request error'); + }); + + it('should set html when error is a string', () => { + const wrapper = render(); + expect(wrapper.text()).toBe('Request error'); + }); +}); diff --git a/src/ui/public/visualize/components/visualization_requesterror.tsx b/src/ui/public/visualize/components/visualization_requesterror.tsx new file mode 100644 index 0000000000000..8dbec10973d50 --- /dev/null +++ b/src/ui/public/visualize/components/visualization_requesterror.tsx @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiText } from '@elastic/eui'; +import React from 'react'; +import { SearchError } from 'ui/courier'; +import { dispatchRenderComplete } from '../../render_complete'; + +interface VisualizationRequestErrorProps { + onInit?: () => void; + error: SearchError | string; +} + +export class VisualizationRequestError extends React.Component { + private containerDiv = React.createRef(); + + public render() { + const { error } = this.props; + const errorMessage = (error && error.message) || error; + + return ( +
+ + {errorMessage} + +
+ ); + } + + public componentDidMount() { + this.afterRender(); + } + + public componentDidUpdate() { + this.afterRender(); + } + + private afterRender() { + if (this.props.onInit) { + this.props.onInit(); + } + if (this.containerDiv.current) { + dispatchRenderComplete(this.containerDiv.current); + } + } +} diff --git a/src/ui/public/visualize/loader/visualize_data_loader.ts b/src/ui/public/visualize/loader/visualize_data_loader.ts index 780a4d21efc7d..6b6fbf5ba46bd 100644 --- a/src/ui/public/visualize/loader/visualize_data_loader.ts +++ b/src/ui/public/visualize/loader/visualize_data_loader.ts @@ -67,8 +67,10 @@ export class VisualizeDataLoader { this.responseHandler = getHandler(responseHandlers, responseHandler); } - public async fetch(params: RequestHandlerParams): Promise { + public fetch = async (params: RequestHandlerParams): Promise => { this.vis.filters = { timeRange: params.timeRange }; + this.vis.requestError = undefined; + this.vis.showRequestError = false; try { // searchSource is only there for courier request handler @@ -95,6 +97,7 @@ export class VisualizeDataLoader { } catch (e) { params.searchSource.cancelQueued(); this.vis.requestError = e; + this.vis.showRequestError = e.type && e.type === 'UNSUPPORTED_QUERY'; if (isTermSizeZeroError(e)) { return toastNotifications.addDanger( `Your visualization ('${this.vis.title}') has an error: it has a term ` + @@ -107,5 +110,5 @@ export class VisualizeDataLoader { text: e.message, }); } - } + }; } diff --git a/x-pack/plugins/rollup/public/search/rollup_search_strategy.js b/x-pack/plugins/rollup/public/search/rollup_search_strategy.js index 0b4f446981508..ea5d7f9bd5b4e 100644 --- a/x-pack/plugins/rollup/public/search/rollup_search_strategy.js +++ b/x-pack/plugins/rollup/public/search/rollup_search_strategy.js @@ -5,7 +5,7 @@ */ import { kfetchAbortable } from 'ui/kfetch'; -import { SearchError } from 'ui/courier'; +import { SearchError, getSearchErrorType } from 'ui/courier'; function getAllFetchParams(searchRequests, Promise) { return Promise.map(searchRequests, (searchRequest) => { @@ -118,8 +118,9 @@ export const rollupSearchStrategy = { const searchError = new SearchError({ status: statusText, title, - message, + message: `Rollup search error: ${message}`, path: url, + type: getSearchErrorType({ message }), }); reject(searchError);