Skip to content

Commit

Permalink
Allow restored session to run missing searches and show a warning (#1…
Browse files Browse the repository at this point in the history
…01650) (#103078)

* Allow restored session to run missing searches and show a warning

* tests and docs

* improve warning

* tests for new functionality
NoSearchIdInSessionError type

* managmeent tests

* Update texts

* fix search service pus

* link to docs

* imports

* format import

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Liza Katz <lizka.k@gmail.com>
  • Loading branch information
kibanamachine and lizozom authored Jun 23, 2021
1 parent 6e10c4b commit 056cf01
Show file tree
Hide file tree
Showing 30 changed files with 366 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ readonly links: {
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) &gt; [isRestored](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md)

## IKibanaSearchResponse.isRestored property

Indicates whether the results returned are from the async-search index

<b>Signature:</b>

```typescript
isRestored?: boolean;
```
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface IKibanaSearchResponse<RawResponse = any>
| --- | --- | --- |
| [id](./kibana-plugin-plugins-data-public.ikibanasearchresponse.id.md) | <code>string</code> | Some responses may contain a unique id to identify the request this response came from. |
| [isPartial](./kibana-plugin-plugins-data-public.ikibanasearchresponse.ispartial.md) | <code>boolean</code> | Indicates whether the results returned are complete or partial |
| [isRestored](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md) | <code>boolean</code> | Indicates whether the results returned are from the async-search index |
| [isRunning](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrunning.md) | <code>boolean</code> | Indicates whether search is still in flight |
| [loaded](./kibana-plugin-plugins-data-public.ikibanasearchresponse.loaded.md) | <code>number</code> | If relevant to the search strategy, return a loaded number that represents how progress is indicated. |
| [rawResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.rawresponse.md) | <code>RawResponse</code> | The raw response returned by the internal search method (usually the raw ES response) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
| [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) | |
| [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | |
| [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) | |
| [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) | |
| [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | |
| [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | |

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) &gt; [(constructor)](./kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md)

## NoSearchIdInSessionError.(constructor)

Constructs a new instance of the `NoSearchIdInSessionError` class

<b>Signature:</b>

```typescript
constructor();
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md)

## NoSearchIdInSessionError class

<b>Signature:</b>

```typescript
export declare class NoSearchIdInSessionError extends KbnError
```
## Constructors
| Constructor | Modifiers | Description |
| --- | --- | --- |
| [(constructor)()](./kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md) | | Constructs a new instance of the <code>NoSearchIdInSessionError</code> class |
2 changes: 2 additions & 0 deletions src/core/public/doc_links/doc_links_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export class DocLinksService {
},
search: {
sessions: `${KIBANA_DOCS}search-sessions.html`,
sessionLimits: `${KIBANA_DOCS}search-sessions.html#_limitations`,
},
date: {
dateMath: `${ELASTICSEARCH_DOCS}common-options.html#date-math`,
Expand Down Expand Up @@ -525,6 +526,7 @@ export interface DocLinksStart {
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
Expand Down
1 change: 1 addition & 0 deletions src/core/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ export interface DocLinksStart {
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/data/common/search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ export interface IKibanaSearchResponse<RawResponse = any> {
*/
isPartial?: boolean;

/**
* Indicates whether the results returned are from the async-search index
*/
isRestored?: boolean;

/**
* The raw response returned by the internal search method (usually the raw ES response)
*/
Expand Down
1 change: 1 addition & 0 deletions src/plugins/data/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,7 @@ export interface IKibanaSearchRequest<Params = any> {
export interface IKibanaSearchResponse<RawResponse = any> {
id?: string;
isPartial?: boolean;
isRestored?: boolean;
isRunning?: boolean;
loaded?: number;
rawResponse: RawResponse;
Expand Down
1 change: 1 addition & 0 deletions src/plugins/data/public/search/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './timeout_error';
export * from './utils';
export * from './types';
export * from './http_error';
export * from './search_session_incomplete_warning';
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
import { CoreStart } from 'kibana/public';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';

export const SearchSessionIncompleteWarning = (docLinks: CoreStart['docLinks']) => (
<>
<EuiSpacer size="s" />
It needs more time to fully render. You can wait here or come back to it later.
<EuiSpacer size="m" />
<EuiText textAlign="right">
<EuiLink
href={docLinks.links.search.sessionLimits}
color="warning"
target="_blank"
data-test-subj="searchSessionIncompleteWarning"
external
>
<FormattedMessage id="data.searchSession.warning.readDocs" defaultMessage="Read More" />
</EuiLink>
</EuiText>
</>
);
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ jest.mock('./utils', () => ({
}),
}));

jest.mock('../errors/search_session_incomplete_warning', () => ({
SearchSessionIncompleteWarning: jest.fn(),
}));

import { SearchSessionIncompleteWarning } from '../errors/search_session_incomplete_warning';

let searchInterceptor: SearchInterceptor;
let mockCoreSetup: MockedKeys<CoreSetup>;
let bfetchSetup: jest.Mocked<BfetchPublicSetup>;
Expand Down Expand Up @@ -508,6 +514,7 @@ describe('SearchInterceptor', () => {
}
: null
);
sessionServiceMock.isRestore.mockReturnValue(!!opts?.isRestore);
fetchMock.mockResolvedValue({ result: 200 });
};

Expand Down Expand Up @@ -562,6 +569,92 @@ describe('SearchInterceptor', () => {
(sessionService as jest.Mocked<ISessionService>).getSearchOptions
).toHaveBeenCalledWith(sessionId);
});

test('should not show warning if a search is available during restore', async () => {
setup({
isRestore: true,
isStored: true,
sessionId: '123',
});

const responses = [
{
time: 10,
value: {
isPartial: false,
isRunning: false,
isRestored: true,
id: 1,
rawResponse: {
took: 1,
},
},
},
];
mockFetchImplementation(responses);

const response = searchInterceptor.search(
{},
{
sessionId: '123',
}
);
response.subscribe({ next, error, complete });

await timeTravel(10);

expect(SearchSessionIncompleteWarning).toBeCalledTimes(0);
});

test('should show warning once if a search is not available during restore', async () => {
setup({
isRestore: true,
isStored: true,
sessionId: '123',
});

const responses = [
{
time: 10,
value: {
isPartial: false,
isRunning: false,
isRestored: false,
id: 1,
rawResponse: {
took: 1,
},
},
},
];
mockFetchImplementation(responses);

searchInterceptor
.search(
{},
{
sessionId: '123',
}
)
.subscribe({ next, error, complete });

await timeTravel(10);

expect(SearchSessionIncompleteWarning).toBeCalledTimes(1);

searchInterceptor
.search(
{},
{
sessionId: '123',
}
)
.subscribe({ next, error, complete });

await timeTravel(10);

expect(SearchSessionIncompleteWarning).toBeCalledTimes(1);
});
});

describe('Session tracking', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
PainlessError,
SearchTimeoutError,
TimeoutErrorMode,
SearchSessionIncompleteWarning,
} from '../errors';
import { toMountPoint } from '../../../../kibana_react/public';
import { AbortError, KibanaServerError } from '../../../../kibana_utils/public';
Expand Down Expand Up @@ -82,6 +83,7 @@ export class SearchInterceptor {
* @internal
*/
private application!: CoreStart['application'];
private docLinks!: CoreStart['docLinks'];
private batchedFetch!: BatchedFunc<
{ request: IKibanaSearchRequest; options: ISearchOptionsSerializable },
IKibanaSearchResponse
Expand All @@ -95,6 +97,7 @@ export class SearchInterceptor {

this.deps.startServices.then(([coreStart]) => {
this.application = coreStart.application;
this.docLinks = coreStart.docLinks;
});

this.batchedFetch = deps.bfetch.batchedFunction({
Expand Down Expand Up @@ -345,6 +348,11 @@ export class SearchInterceptor {
this.handleSearchError(e, searchOptions, searchAbortController.isTimeout())
);
}),
tap((response) => {
if (this.deps.session.isRestore() && response.isRestored === false) {
this.showRestoreWarning(this.deps.session.getSessionId());
}
}),
finalize(() => {
this.pendingCount$.next(this.pendingCount$.getValue() - 1);
if (untrackSearch && this.deps.session.isCurrentSession(sessionId)) {
Expand All @@ -371,6 +379,25 @@ export class SearchInterceptor {
}
);

private showRestoreWarningToast = (sessionId?: string) => {
this.deps.toasts.addWarning(
{
title: 'Your search session is still running',
text: toMountPoint(SearchSessionIncompleteWarning(this.docLinks)),
},
{
toastLifeTimeMs: 60000,
}
);
};

private showRestoreWarning = memoize(
this.showRestoreWarningToast,
(_: SearchTimeoutError, sessionId: string) => {
return sessionId;
}
);

/**
* Show one error notification per session.
* @internal
Expand Down
1 change: 1 addition & 0 deletions src/plugins/data/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export {
DataRequestHandlerContext,
AsyncSearchResponse,
AsyncSearchStatusResponse,
NoSearchIdInSessionError,
} from './search';

// Search namespace
Expand Down
15 changes: 15 additions & 0 deletions src/plugins/data/server/search/errors/no_search_id_in_session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { KbnError } from '../../../../kibana_utils/common';

export class NoSearchIdInSessionError extends KbnError {
constructor() {
super('No search ID in this session matching the given search request');
}
}
1 change: 1 addition & 0 deletions src/plugins/data/server/search/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export * from './strategies/eql_search';
export { usageProvider, SearchUsage, searchUsageObserver } from './collectors';
export * from './aggs';
export * from './session';
export * from './errors/no_search_id_in_session';
17 changes: 17 additions & 0 deletions src/plugins/data/server/search/search_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
ISearchSessionService,
ISearchStart,
ISearchStrategy,
NoSearchIdInSessionError,
} from '.';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { expressionsPluginMock } from '../../../expressions/public/mocks';
Expand Down Expand Up @@ -175,6 +176,22 @@ describe('Search service', () => {
expect(request).toStrictEqual({ ...searchRequest, id: 'my_id' });
});

it('searches even if id is not found in session during restore', async () => {
const searchRequest = { params: {} };
const options = { sessionId, isStored: true, isRestore: true };

mockSessionClient.getId = jest.fn().mockImplementation(() => {
throw new NoSearchIdInSessionError();
});

const res = await mockScopedClient.search(searchRequest, options).toPromise();

const [request, callOptions] = mockStrategy.search.mock.calls[0];
expect(callOptions).toBe(options);
expect(request).toStrictEqual({ ...searchRequest });
expect(res.isRestored).toBe(false);
});

it('does not fail if `trackId` throws', async () => {
const searchRequest = { params: {} };
const options = { sessionId, isStored: false, isRestore: false };
Expand Down
Loading

0 comments on commit 056cf01

Please sign in to comment.