;
}
diff --git a/src/plugins/data/public/search/long_query_notification.tsx b/src/plugins/data/public/search/long_query_notification.tsx
deleted file mode 100644
index 1db298618fae81..00000000000000
--- a/src/plugins/data/public/search/long_query_notification.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-import React from 'react';
-import { ApplicationStart } from 'kibana/public';
-import { toMountPoint } from '../../../kibana_react/public';
-
-interface Props {
- application: ApplicationStart;
-}
-
-export function getLongQueryNotification(props: Props) {
- return toMountPoint();
-}
-
-export function LongQueryNotification(props: Props) {
- return (
-
-
-
-
-
- {
- await props.application.navigateToApp('management/stack/license_management');
- }}
- >
-
-
-
-
-
- );
-}
diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts
index 84db69a83a005d..7bfa6f0ab1bc57 100644
--- a/src/plugins/data/public/search/search_interceptor.test.ts
+++ b/src/plugins/data/public/search/search_interceptor.test.ts
@@ -95,6 +95,39 @@ describe('SearchInterceptor', () => {
await flushPromises();
});
+ test('Should not timeout if requestTimeout is undefined', async () => {
+ searchInterceptor = new SearchInterceptor({
+ startServices: mockCoreSetup.getStartServices(),
+ uiSettings: mockCoreSetup.uiSettings,
+ http: mockCoreSetup.http,
+ toasts: mockCoreSetup.notifications.toasts,
+ });
+ mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => {
+ return new Promise((resolve, reject) => {
+ options.signal.addEventListener('abort', () => {
+ reject(new AbortError());
+ });
+
+ setTimeout(resolve, 5000);
+ });
+ });
+ const mockRequest: IEsSearchRequest = {
+ params: {},
+ };
+ const response = searchInterceptor.search(mockRequest);
+
+ expect.assertions(1);
+ const next = jest.fn();
+ const complete = () => {
+ expect(next).toBeCalled();
+ };
+ response.subscribe({ next, complete });
+
+ jest.advanceTimersByTime(5000);
+
+ await flushPromises();
+ });
+
test('Observable should fail if user aborts (test merged signal)', async () => {
const abortController = new AbortController();
mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => {
@@ -125,7 +158,7 @@ describe('SearchInterceptor', () => {
await flushPromises();
});
- test('Immediatelly aborts if passed an aborted abort signal', async (done) => {
+ test('Immediately aborts if passed an aborted abort signal', async (done) => {
const abort = new AbortController();
const mockRequest: IEsSearchRequest = {
params: {},
@@ -141,44 +174,4 @@ describe('SearchInterceptor', () => {
response.subscribe({ error });
});
});
-
- describe('getPendingCount$', () => {
- test('should observe the number of pending requests', () => {
- const pendingCount$ = searchInterceptor.getPendingCount$();
- const pendingNext = jest.fn();
- pendingCount$.subscribe(pendingNext);
-
- const mockResponse: any = { result: 200 };
- mockCoreSetup.http.fetch.mockResolvedValue(mockResponse);
- const mockRequest: IEsSearchRequest = {
- params: {},
- };
- const response = searchInterceptor.search(mockRequest);
-
- response.subscribe({
- complete: () => {
- expect(pendingNext.mock.calls).toEqual([[0], [1], [0]]);
- },
- });
- });
-
- test('should observe the number of pending requests on error', () => {
- const pendingCount$ = searchInterceptor.getPendingCount$();
- const pendingNext = jest.fn();
- pendingCount$.subscribe(pendingNext);
-
- const mockResponse: any = { result: 500 };
- mockCoreSetup.http.fetch.mockRejectedValue(mockResponse);
- const mockRequest: IEsSearchRequest = {
- params: {},
- };
- const response = searchInterceptor.search(mockRequest);
-
- response.subscribe({
- complete: () => {
- expect(pendingNext.mock.calls).toEqual([[0], [1], [0]]);
- },
- });
- });
- });
});
diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts
index 0a6d60afed2f73..888e12a4285b1c 100644
--- a/src/plugins/data/public/search/search_interceptor.ts
+++ b/src/plugins/data/public/search/search_interceptor.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { trimEnd } from 'lodash';
+import { trimEnd, debounce } from 'lodash';
import {
BehaviorSubject,
throwError,
@@ -28,25 +28,24 @@ import {
Observable,
NEVER,
} from 'rxjs';
-import { finalize, filter } from 'rxjs/operators';
-import { Toast, CoreStart, ToastsSetup, CoreSetup } from 'kibana/public';
-import { getCombinedSignal, AbortError } from '../../common/utils';
+import { catchError, finalize } from 'rxjs/operators';
+import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public';
+import { i18n } from '@kbn/i18n';
import {
+ getCombinedSignal,
+ AbortError,
IEsSearchRequest,
IEsSearchResponse,
ISearchOptions,
ES_SEARCH_STRATEGY,
-} from '../../common/search';
-import { getLongQueryNotification } from './long_query_notification';
+} from '../../common';
import { SearchUsageCollector } from './collectors';
-const LONG_QUERY_NOTIFICATION_DELAY = 10000;
-
export interface SearchInterceptorDeps {
- toasts: ToastsSetup;
http: CoreSetup['http'];
uiSettings: CoreSetup['uiSettings'];
startServices: Promise<[CoreStart, any, unknown]>;
+ toasts: ToastsSetup;
usageCollector?: SearchUsageCollector;
}
@@ -69,12 +68,6 @@ export class SearchInterceptor {
*/
protected timeoutSubscriptions: Subscription = new Subscription();
- /**
- * The current long-running toast (if there is one).
- * @internal
- */
- protected longRunningToast?: Toast;
-
/**
* @internal
*/
@@ -89,19 +82,6 @@ export class SearchInterceptor {
this.deps.startServices.then(([coreStart]) => {
this.application = coreStart.application;
});
-
- // When search requests go out, a notification is scheduled allowing users to continue the
- // request past the timeout. When all search requests complete, we remove the notification.
- this.getPendingCount$()
- .pipe(filter((count) => count === 0))
- .subscribe(this.hideToast);
- }
- /**
- * Returns an `Observable` over the current number of pending searches. This could mean that one
- * of the search requests is still in flight, or that it has only received partial responses.
- */
- public getPendingCount$() {
- return this.pendingCount$.asObservable();
}
/**
@@ -146,6 +126,12 @@ export class SearchInterceptor {
this.pendingCount$.next(this.pendingCount$.getValue() + 1);
return this.runSearch(request, combinedSignal, options?.strategy).pipe(
+ catchError((e: any) => {
+ if (e.body?.attributes?.error === 'Request timed out') {
+ this.showTimeoutError(e);
+ }
+ return throwError(e);
+ }),
finalize(() => {
this.pendingCount$.next(this.pendingCount$.getValue() - 1);
cleanup();
@@ -170,12 +156,10 @@ export class SearchInterceptor {
const timeout$ = timeout ? timer(timeout) : NEVER;
const subscription = timeout$.subscribe(() => {
timeoutController.abort();
+ this.showTimeoutError(new AbortError());
});
this.timeoutSubscriptions.add(subscription);
- // Schedule the notification to allow users to cancel or wait beyond the timeout
- const notificationSubscription = timer(LONG_QUERY_NOTIFICATION_DELAY).subscribe(this.showToast);
-
// Get a combined `AbortSignal` that will be aborted whenever the first of the following occurs:
// 1. The user manually aborts (via `cancelPending`)
// 2. The request times out
@@ -189,7 +173,6 @@ export class SearchInterceptor {
const combinedSignal = getCombinedSignal(signals);
const cleanup = () => {
this.timeoutSubscriptions.remove(subscription);
- notificationSubscription.unsubscribe();
};
combinedSignal.addEventListener('abort', cleanup);
@@ -200,36 +183,23 @@ export class SearchInterceptor {
};
}
- /**
- * @internal
- */
- protected showToast = () => {
- if (this.longRunningToast) return;
- this.longRunningToast = this.deps.toasts.addInfo(
- {
- title: 'Your query is taking a while',
- text: getLongQueryNotification({
- application: this.application,
+ // Right now we are debouncing but we will hook this up with background sessions to show only one
+ // error notification per session.
+ protected showTimeoutError = debounce(
+ (e: Error) => {
+ this.deps.toasts.addError(e, {
+ title: 'Timed out',
+ toastMessage: i18n.translate('data.search.upgradeLicense', {
+ defaultMessage:
+ 'One or more queries timed out. With our free Basic tier, your queries never time out.',
}),
- },
- {
- toastLifeTimeMs: 1000000,
- }
- );
- };
-
- /**
- * @internal
- */
- protected hideToast = () => {
- if (this.longRunningToast) {
- this.deps.toasts.remove(this.longRunningToast);
- delete this.longRunningToast;
- if (this.deps.usageCollector) {
- this.deps.usageCollector.trackLongQueryDialogDismissed();
- }
+ });
+ },
+ 60000,
+ {
+ leading: true,
}
- };
+ );
}
export type ISearchInterceptor = PublicMethodsOf;
diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts
index b2b958454de48b..aefdac2ab639fb 100644
--- a/src/plugins/data/server/search/types.ts
+++ b/src/plugins/data/server/search/types.ts
@@ -20,7 +20,7 @@
import { RequestHandlerContext } from '../../../../core/server';
import { ISearchOptions } from '../../common/search';
import { AggsSetup, AggsStart } from './aggs';
-import { SearchUsage } from './collectors/usage';
+import { SearchUsage } from './collectors';
import { IEsSearchRequest, IEsSearchResponse } from './es_search';
export interface SearchEnhancements {
diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json
index 637af39339e277..5ded0f8f0dec3a 100644
--- a/x-pack/plugins/data_enhanced/kibana.json
+++ b/x-pack/plugins/data_enhanced/kibana.json
@@ -6,10 +6,11 @@
"xpack", "data_enhanced"
],
"requiredPlugins": [
- "data"
+ "data",
+ "features"
],
- "optionalPlugins": ["kibanaReact", "kibanaUtils", "usageCollection"],
+ "optionalPlugins": ["kibanaUtils", "usageCollection"],
"server": true,
"ui": true,
- "requiredBundles": ["kibanaReact", "kibanaUtils"]
+ "requiredBundles": ["kibanaUtils"]
}
diff --git a/x-pack/plugins/data_enhanced/public/search/long_query_notification.tsx b/x-pack/plugins/data_enhanced/public/search/long_query_notification.tsx
deleted file mode 100644
index 325cf1145fa5f1..00000000000000
--- a/x-pack/plugins/data_enhanced/public/search/long_query_notification.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-import React from 'react';
-import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
-
-interface Props {
- cancel: () => void;
- runBeyondTimeout: () => void;
-}
-
-export function getLongQueryNotification(props: Props) {
- return toMountPoint(
-
- );
-}
-
-export function LongQueryNotification(props: Props) {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts
index 261e03887acdba..af2fc85602541a 100644
--- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts
+++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts
@@ -60,9 +60,6 @@ describe('EnhancedSearchInterceptor', () => {
mockUsageCollector = {
trackQueryTimedOut: jest.fn(),
trackQueriesCancelled: jest.fn(),
- trackLongQueryPopupShown: jest.fn(),
- trackLongQueryDialogDismissed: jest.fn(),
- trackLongQueryRunBeyondTimeout: jest.fn(),
};
const mockPromise = new Promise((resolve) => {
@@ -390,88 +387,4 @@ describe('EnhancedSearchInterceptor', () => {
expect(mockUsageCollector.trackQueriesCancelled).toBeCalledTimes(1);
});
});
-
- describe('runBeyondTimeout', () => {
- const timedResponses = [
- {
- time: 250,
- value: {
- isPartial: true,
- isRunning: true,
- id: 1,
- rawResponse: {
- took: 1,
- },
- },
- },
- {
- time: 2000,
- value: {
- isPartial: false,
- isRunning: false,
- id: 1,
- rawResponse: {
- took: 1,
- },
- },
- },
- ];
-
- test('times out if runBeyondTimeout is not called', async () => {
- mockFetchImplementation(timedResponses);
-
- const response = searchInterceptor.search({});
- response.subscribe({ next, error });
-
- await timeTravel(250);
-
- expect(next).toHaveBeenCalled();
- expect(next.mock.calls[0][0]).toStrictEqual(timedResponses[0].value);
-
- await timeTravel(750);
-
- expect(error).toHaveBeenCalled();
- expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError);
- });
-
- test('times out if runBeyondTimeout is called too late', async () => {
- mockFetchImplementation(timedResponses);
-
- const response = searchInterceptor.search({});
- response.subscribe({ next, error });
- setTimeout(() => searchInterceptor.runBeyondTimeout(), 1100);
-
- await timeTravel(250);
-
- expect(next).toHaveBeenCalled();
- expect(next.mock.calls[0][0]).toStrictEqual(timedResponses[0].value);
-
- await timeTravel(750);
-
- expect(error).toHaveBeenCalled();
- expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError);
- });
-
- test('should prevent the request from timing out', async () => {
- mockFetchImplementation(timedResponses);
-
- const response = searchInterceptor.search({}, { pollInterval: 0 });
- response.subscribe({ next, error, complete });
- setTimeout(() => searchInterceptor.runBeyondTimeout(), 500);
-
- await timeTravel(250);
-
- expect(next).toHaveBeenCalled();
- expect(next.mock.calls[0][0]).toStrictEqual(timedResponses[0].value);
-
- await timeTravel(250); // Run beyond timeout
- await timeTravel(1750); // Final response
-
- expect(next).toHaveBeenCalledTimes(2);
- expect(next.mock.calls[0][0]).toStrictEqual(timedResponses[0].value);
- expect(next.mock.calls[1][0]).toStrictEqual(timedResponses[1].value);
- expect(error).not.toHaveBeenCalled();
- expect(mockUsageCollector.trackLongQueryRunBeyondTimeout).toBeCalledTimes(1);
- });
- });
});
diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts
index 61cf579d3136b6..f7ae9fc6d0f917 100644
--- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts
+++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts
@@ -6,7 +6,8 @@
import { throwError, EMPTY, timer, from, Subscription } from 'rxjs';
import { mergeMap, expand, takeUntil, finalize, tap } from 'rxjs/operators';
-import { getLongQueryNotification } from './long_query_notification';
+import { debounce } from 'lodash';
+import { i18n } from '@kbn/i18n';
import {
SearchInterceptor,
SearchInterceptorDeps,
@@ -42,38 +43,11 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
* Abort our `AbortController`, which in turn aborts any intercepted searches.
*/
public cancelPending = () => {
- this.hideToast();
this.abortController.abort();
this.abortController = new AbortController();
if (this.deps.usageCollector) this.deps.usageCollector.trackQueriesCancelled();
};
- /**
- * Un-schedule timing out all of the searches intercepted.
- */
- public runBeyondTimeout = () => {
- this.hideToast();
- this.timeoutSubscriptions.unsubscribe();
- if (this.deps.usageCollector) this.deps.usageCollector.trackLongQueryRunBeyondTimeout();
- };
-
- protected showToast = () => {
- if (this.longRunningToast) return;
- this.longRunningToast = this.deps.toasts.addInfo(
- {
- title: 'Your query is taking a while',
- text: getLongQueryNotification({
- cancel: this.cancelPending,
- runBeyondTimeout: this.runBeyondTimeout,
- }),
- },
- {
- toastLifeTimeMs: 1000000,
- }
- );
- if (this.deps.usageCollector) this.deps.usageCollector.trackLongQueryPopupShown();
- };
-
public search(
request: IAsyncSearchRequest,
{ pollInterval = 1000, ...options }: IAsyncSearchOptions = {}
@@ -127,4 +101,28 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
})
);
}
+
+ // Right now we are debouncing but we will hook this up with background sessions to show only one
+ // error notification per session.
+ protected showTimeoutError = debounce(
+ (e: Error) => {
+ const message = this.application.capabilities.advancedSettings?.save
+ ? i18n.translate('xpack.data.search.timeoutIncreaseSetting', {
+ defaultMessage:
+ 'One or more queries timed out. Increase run time with the search.timeout advanced setting.',
+ })
+ : i18n.translate('xpack.data.search.timeoutContactAdmin', {
+ defaultMessage:
+ 'One or more queries timed out. Contact your system administrator to increase the run time.',
+ });
+ this.deps.toasts.addError(e, {
+ title: 'Timed out',
+ toastMessage: message,
+ });
+ },
+ 60000,
+ {
+ leading: true,
+ }
+ );
}
diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts
index 3b05e83d208b7b..a1dff00ddfdd39 100644
--- a/x-pack/plugins/data_enhanced/server/plugin.ts
+++ b/x-pack/plugins/data_enhanced/server/plugin.ts
@@ -18,8 +18,8 @@ import {
} from '../../../../src/plugins/data/server';
import { enhancedEsSearchStrategyProvider } from './search';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
-import { ENHANCED_ES_SEARCH_STRATEGY } from '../common';
import { getUiSettings } from './ui_settings';
+import { ENHANCED_ES_SEARCH_STRATEGY } from '../common';
interface SetupDependencies {
data: DataPluginSetup;
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 3106d1e974d130..096af87131c7f7 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -819,8 +819,6 @@
"data.query.queryBar.KQLNestedQuerySyntaxInfoTitle": "KQL ネストされたクエリ構文",
"data.query.queryBar.kqlOffLabel": "オフ",
"data.query.queryBar.kqlOnLabel": "オン",
- "data.query.queryBar.licenseOptions": "ライセンスオプションに進む",
- "data.query.queryBar.longQueryMessage": "ライセンスをアップグレードすれば、リクエストの完了までに十分な時間を確保できます。",
"data.query.queryBar.luceneLanguageName": "Lucene",
"data.query.queryBar.luceneSyntaxWarningMessage": "Lucene クエリ構文を使用しているようですが、Kibana クエリ言語 (KQL) が選択されています。KQL ドキュメント {link} を確認してください。",
"data.query.queryBar.luceneSyntaxWarningOptOutText": "今後表示しない",
@@ -6681,8 +6679,6 @@
"xpack.data.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText": "より小さいまたは等しい",
"xpack.data.kueryAutocomplete.orOperatorDescription": "{oneOrMoreArguments} が true であることを条件とする",
"xpack.data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "1つ以上の引数",
- "xpack.data.query.queryBar.cancelLongQuery": "キャンセル",
- "xpack.data.query.queryBar.runBeyond": "タイムアウトを越えて実行",
"xpack.discover.FlyoutCreateDrilldownAction.displayName": "基本データを調査",
"xpack.embeddableEnhanced.actions.panelNotifications.manyDrilldowns": "パネルには{count}個のドリルダウンがあります",
"xpack.embeddableEnhanced.actions.panelNotifications.oneDrilldown": "パネルには1個のドリルダウンがあります",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 97f62b1144ff5f..8e3fc7ea6ad49f 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -820,8 +820,6 @@
"data.query.queryBar.KQLNestedQuerySyntaxInfoTitle": "KQL 嵌套查询语法",
"data.query.queryBar.kqlOffLabel": "关闭",
"data.query.queryBar.kqlOnLabel": "开启",
- "data.query.queryBar.licenseOptions": "前往许可证选项",
- "data.query.queryBar.longQueryMessage": "使用升级的许可证,您可以确保有足够的时间来完成请求。",
"data.query.queryBar.luceneLanguageName": "Lucene",
"data.query.queryBar.luceneSyntaxWarningMessage": "尽管您选择了 Kibana 查询语言 (KQL),但似乎您正在尝试使用 Lucene 查询语法。请查看 KQL 文档 {link}。",
"data.query.queryBar.luceneSyntaxWarningOptOutText": "不再显示",
@@ -6684,8 +6682,6 @@
"xpack.data.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText": "小于或等于",
"xpack.data.kueryAutocomplete.orOperatorDescription": "需要{oneOrMoreArguments}为 true",
"xpack.data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "一个或多个参数",
- "xpack.data.query.queryBar.cancelLongQuery": "取消",
- "xpack.data.query.queryBar.runBeyond": "运行超时",
"xpack.discover.FlyoutCreateDrilldownAction.displayName": "浏览底层数据",
"xpack.embeddableEnhanced.actions.panelNotifications.manyDrilldowns": "面板有 {count} 个向下钻取",
"xpack.embeddableEnhanced.actions.panelNotifications.oneDrilldown": "面板有 1 个向下钻取",