Allow restored session to run missing searches and show a warning (#101650) (#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>
This commit is contained in:
Kibana Machine 2021-06-23 10:23:04 -04:00 committed by GitHub
parent 6e10c4bfc1
commit 056cf014c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 366 additions and 31 deletions

View file

@ -106,6 +106,7 @@ readonly links: {
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;

File diff suppressed because one or more lines are too long

View file

@ -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;
```

View file

@ -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) |

View file

@ -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) | |

View file

@ -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();
```

View file

@ -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 |

View file

@ -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`,
@ -525,6 +526,7 @@ export interface DocLinksStart {
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;

View file

@ -585,6 +585,7 @@ export interface DocLinksStart {
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;

View file

@ -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)
*/

View file

@ -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;

View file

@ -12,3 +12,4 @@ export * from './timeout_error';
export * from './utils';
export * from './types';
export * from './http_error';
export * from './search_session_incomplete_warning';

View file

@ -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>
</>
);

View file

@ -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>;
@ -508,6 +514,7 @@ describe('SearchInterceptor', () => {
}
: null
);
sessionServiceMock.isRestore.mockReturnValue(!!opts?.isRestore);
fetchMock.mockResolvedValue({ result: 200 });
};
@ -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', () => {

View file

@ -43,6 +43,7 @@ import {
PainlessError,
SearchTimeoutError,
TimeoutErrorMode,
SearchSessionIncompleteWarning,
} from '../errors';
import { toMountPoint } from '../../../../kibana_react/public';
import { AbortError, KibanaServerError } from '../../../../kibana_utils/public';
@ -82,6 +83,7 @@ export class SearchInterceptor {
* @internal
*/
private application!: CoreStart['application'];
private docLinks!: CoreStart['docLinks'];
private batchedFetch!: BatchedFunc<
{ request: IKibanaSearchRequest; options: ISearchOptionsSerializable },
IKibanaSearchResponse
@ -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({
@ -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)) {
@ -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

View file

@ -238,6 +238,7 @@ export {
DataRequestHandlerContext,
AsyncSearchResponse,
AsyncSearchStatusResponse,
NoSearchIdInSessionError,
} from './search';
// Search namespace

View file

@ -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');
}
}

View file

@ -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';

View file

@ -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';
@ -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 };

View file

@ -19,7 +19,7 @@ import {
SharedGlobalConfig,
StartServicesAccessor,
} from 'src/core/server';
import { first, switchMap, tap } from 'rxjs/operators';
import { first, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { BfetchServerSetup } from 'src/plugins/bfetch/server';
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import type {
@ -80,6 +80,7 @@ import { registerBsearchRoute } from './routes/bsearch';
import { getKibanaContext } from './expressions/kibana_context';
import { enhancedEsSearchStrategyProvider } from './strategies/ese_search';
import { eqlSearchStrategyProvider } from './strategies/eql_search';
import { NoSearchIdInSessionError } from './errors/no_search_id_in_session';
type StrategyMap = Record<string, ISearchStrategy<any, any>>;
@ -287,24 +288,48 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
options.strategy
);
const getSearchRequest = async () =>
!options.sessionId || !options.isRestore || request.id
? request
: {
const getSearchRequest = async () => {
if (!options.sessionId || !options.isRestore || request.id) {
return request;
} else {
try {
const id = await deps.searchSessionsClient.getId(request, options);
this.logger.debug(`Found search session id for request ${id}`);
return {
...request,
id: await deps.searchSessionsClient.getId(request, options),
id,
};
} catch (e) {
if (e instanceof NoSearchIdInSessionError) {
this.logger.debug('Ignoring missing search ID');
return request;
} else {
throw e;
}
}
}
};
return from(getSearchRequest()).pipe(
const searchRequest$ = from(getSearchRequest());
const search$ = searchRequest$.pipe(
switchMap((searchRequest) => strategy.search(searchRequest, options, deps)),
tap((response) => {
if (!options.sessionId || !response.id || options.isRestore) return;
withLatestFrom(searchRequest$),
tap(([response, requestWithId]) => {
if (!options.sessionId || !response.id || (options.isRestore && requestWithId.id)) return;
// intentionally swallow tracking error, as it shouldn't fail the search
deps.searchSessionsClient.trackId(request, response.id, options).catch((trackErr) => {
this.logger.error(trackErr);
});
}),
map(([response, requestWithId]) => {
return {
...response,
isRestored: !!requestWithId.id,
};
})
);
return search$;
} catch (e) {
return throwError(e);
}

View file

@ -1211,6 +1211,14 @@ export enum METRIC_TYPES {
TOP_HITS = "top_hits"
}
// Warning: (ae-forgotten-export) The symbol "KbnError" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "NoSearchIdInSessionError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class NoSearchIdInSessionError extends KbnError {
constructor();
}
// Warning: (ae-missing-release-tag) "OptionedParamType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -1543,18 +1551,18 @@ export function usageProvider(core: CoreSetup_2): SearchUsage;
// src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "HistogramFormat" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:246:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:256:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:270:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:248:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:259:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index.ts:272:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/plugin.ts:81:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/search/types.ts:115:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts

View file

@ -27,6 +27,7 @@ describe('Background Search Session management status labels', () => {
id: 'wtywp9u2802hahgp-gsla',
restoreUrl: '/app/great-app-url/#45',
reloadUrl: '/app/great-app-url/#45',
numSearches: 1,
appId: 'security',
status: SearchSessionStatus.IN_PROGRESS,
created: '2020-12-02T00:19:32Z',

View file

@ -70,6 +70,7 @@ describe('Background Search Session Management Table', () => {
status: SearchSessionStatus.IN_PROGRESS,
created: '2020-12-02T00:19:32Z',
expires: '2020-12-07T00:19:32Z',
idMapping: {},
},
},
],
@ -95,10 +96,12 @@ describe('Background Search Session Management Table', () => {
);
});
expect(table.find('thead th').map((node) => node.text())).toMatchInlineSnapshot(`
expect(table.find('thead th .euiTableCellContent__text').map((node) => node.text()))
.toMatchInlineSnapshot(`
Array [
"App",
"Name",
"# Searches",
"Status",
"Created",
"Expiration",
@ -130,6 +133,7 @@ describe('Background Search Session Management Table', () => {
Array [
"App",
"Namevery background search ",
"# Searches0",
"StatusExpired",
"Created2 Dec, 2020, 00:19:32",
"Expiration--",

View file

@ -52,6 +52,7 @@ describe('Search Sessions Management API', () => {
status: 'complete',
initialState: {},
restoreState: {},
idMapping: [],
},
},
],
@ -78,6 +79,7 @@ describe('Search Sessions Management API', () => {
"id": "hello-pizza-123",
"initialState": Object {},
"name": "Veggie",
"numSearches": 0,
"reloadUrl": "hello-cool-undefined-url",
"restoreState": Object {},
"restoreUrl": "hello-cool-undefined-url",
@ -100,6 +102,7 @@ describe('Search Sessions Management API', () => {
expires: moment().subtract(3, 'days'),
initialState: {},
restoreState: {},
idMapping: {},
},
},
],

View file

@ -90,6 +90,7 @@ const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema)
urlGeneratorId,
initialState,
restoreState,
idMapping,
} = savedObject.attributes;
const status = getUIStatus(savedObject.attributes);
@ -113,6 +114,7 @@ const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema)
reloadUrl,
initialState,
restoreState,
numSearches: Object.keys(idMapping).length,
};
};

View file

@ -70,6 +70,7 @@ describe('Search Sessions Management table column factory', () => {
reloadUrl: '/app/great-app-url',
restoreUrl: '/app/great-app-url/#42',
appId: 'discovery',
numSearches: 3,
status: SearchSessionStatus.IN_PROGRESS,
created: '2020-12-02T00:19:32Z',
expires: '2020-12-07T00:19:32Z',
@ -95,6 +96,12 @@ describe('Search Sessions Management table column factory', () => {
"sortable": true,
"width": "20%",
},
Object {
"field": "numSearches",
"name": "# Searches",
"render": [Function],
"sortable": true,
},
Object {
"field": "status",
"name": "Status",
@ -146,10 +153,29 @@ describe('Search Sessions Management table column factory', () => {
});
});
// Num of searches column
describe('num of searches', () => {
test('renders', () => {
const [, , numOfSearches] = getColumns(
mockCoreStart,
mockPluginsSetup,
api,
mockConfig,
tz,
handleAction
) as Array<EuiTableFieldDataColumnType<UISession>>;
const numOfSearchesLine = mount(
numOfSearches.render!(mockSession.numSearches, mockSession) as ReactElement
);
expect(numOfSearchesLine.text()).toMatchInlineSnapshot(`"3"`);
});
});
// Status column
describe('status', () => {
test('render in_progress', () => {
const [, , status] = getColumns(
const [, , , status] = getColumns(
mockCoreStart,
mockPluginsSetup,
api,
@ -165,7 +191,7 @@ describe('Search Sessions Management table column factory', () => {
});
test('error handling', () => {
const [, , status] = getColumns(
const [, , , status] = getColumns(
mockCoreStart,
mockPluginsSetup,
api,
@ -188,7 +214,7 @@ describe('Search Sessions Management table column factory', () => {
test('render using Browser timezone', () => {
tz = 'Browser';
const [, , , createdDateCol] = getColumns(
const [, , , , createdDateCol] = getColumns(
mockCoreStart,
mockPluginsSetup,
api,
@ -205,7 +231,7 @@ describe('Search Sessions Management table column factory', () => {
test('render using AK timezone', () => {
tz = 'US/Alaska';
const [, , , createdDateCol] = getColumns(
const [, , , , createdDateCol] = getColumns(
mockCoreStart,
mockPluginsSetup,
api,
@ -220,7 +246,7 @@ describe('Search Sessions Management table column factory', () => {
});
test('error handling', () => {
const [, , , createdDateCol] = getColumns(
const [, , , , createdDateCol] = getColumns(
mockCoreStart,
mockPluginsSetup,
api,

View file

@ -120,6 +120,20 @@ export const getColumns = (
},
},
// # Searches
{
field: 'numSearches',
name: i18n.translate('xpack.data.mgmt.searchSessions.table.numSearches', {
defaultMessage: '# Searches',
}),
sortable: true,
render: (numSearches: UISession['numSearches'], session) => (
<TableText color="subdued" data-test-subj="sessionManagementNumSearchesCol">
{numSearches}
</TableText>
),
},
// Session status
{
field: 'status',

View file

@ -34,6 +34,7 @@ export interface UISession {
created: string;
expires: string | null;
status: UISearchSessionState;
numSearches: number;
actions?: ACTION[];
reloadUrl: string;
restoreUrl: string;

View file

@ -24,7 +24,11 @@ import {
ENHANCED_ES_SEARCH_STRATEGY,
SEARCH_SESSION_TYPE,
} from '../../../../../../src/plugins/data/common';
import { esKuery, ISearchSessionService } from '../../../../../../src/plugins/data/server';
import {
esKuery,
ISearchSessionService,
NoSearchIdInSessionError,
} from '../../../../../../src/plugins/data/server';
import { AuthenticatedUser, SecurityPluginSetup } from '../../../../security/server';
import {
TaskManagerSetupContract,
@ -436,7 +440,7 @@ export class SearchSessionService
const requestHash = createRequestHash(searchRequest.params);
if (!session.attributes.idMapping.hasOwnProperty(requestHash)) {
this.logger.error(`getId | ${sessionId} | ${requestHash} not found`);
throw new Error('No search ID in this session matching the given search request');
throw new NoSearchIdInSessionError();
}
this.logger.debug(`getId | ${sessionId} | ${requestHash}`);

View file

@ -403,7 +403,12 @@ export default function ({ getService }: FtrProviderContext) {
const { id: id1 } = searchRes1.body;
// it might take the session a moment to be created
await new Promise((resolve) => setTimeout(resolve, 2500));
await retry.waitFor('search session created', async () => {
const response = await supertest
.get(`/internal/session/${sessionId}`)
.set('kbn-xsrf', 'foo');
return response.body.statusCode === undefined;
});
const getSessionFirstTime = await supertest
.get(`/internal/session/${sessionId}`)