From cb1de743a9f27df54743556f9a38f913c740fa74 Mon Sep 17 00:00:00 2001
From: Liza K
Date: Thu, 28 Jan 2021 16:17:02 +0200
Subject: [PATCH 01/85] Save all search sessions and then manage them based on
their persisted state
---
x-pack/plugins/data_enhanced/common/index.ts | 1 +
.../common/search/session/types.ts | 22 +-
x-pack/plugins/data_enhanced/config.ts | 3 +-
.../public/search/sessions_mgmt/lib/api.ts | 13 +-
.../public/search/sessions_mgmt/types.ts | 15 +-
.../server/saved_objects/search_session.ts | 9 +-
.../server/search/eql_search_strategy.test.ts | 2 -
.../server/search/request_utils.ts | 2 +-
.../session/check_running_sessions.test.ts | 436 ++++++++++----
.../search/session/check_running_sessions.ts | 152 +++--
.../server/search/session/monitoring_task.ts | 16 +-
.../search/session/session_service.test.ts | 569 ++++++++----------
.../server/search/session/session_service.ts | 294 +++------
.../server/search/session/types.ts | 4 +
14 files changed, 839 insertions(+), 699 deletions(-)
diff --git a/x-pack/plugins/data_enhanced/common/index.ts b/x-pack/plugins/data_enhanced/common/index.ts
index 669c33230a34c..8c500ef21ffcf 100644
--- a/x-pack/plugins/data_enhanced/common/index.ts
+++ b/x-pack/plugins/data_enhanced/common/index.ts
@@ -5,6 +5,7 @@
*/
export {
+ SEARCH_SESSION_TYPE,
ENHANCED_ES_SEARCH_STRATEGY,
EQL_SEARCH_STRATEGY,
EqlRequestParams,
diff --git a/x-pack/plugins/data_enhanced/common/search/session/types.ts b/x-pack/plugins/data_enhanced/common/search/session/types.ts
index 9eefdf43aa245..6d07f4b731fae 100644
--- a/x-pack/plugins/data_enhanced/common/search/session/types.ts
+++ b/x-pack/plugins/data_enhanced/common/search/session/types.ts
@@ -6,19 +6,25 @@
import { SearchSessionStatus } from './';
+export const SEARCH_SESSION_TYPE = 'search-session';
export interface SearchSessionSavedObjectAttributes {
+ sessionId: string;
/**
* User-facing session name to be displayed in session management
*/
- name: string;
+ name?: string;
/**
* App that created the session. e.g 'discover'
*/
- appId: string;
+ appId?: string;
/**
* Creation time of the session
*/
created: string;
+ /**
+ * Last touch time of the session
+ */
+ touched: string;
/**
* Expiration time of the session. Expiration itself is managed by Elasticsearch.
*/
@@ -30,22 +36,28 @@ export interface SearchSessionSavedObjectAttributes {
/**
* urlGeneratorId
*/
- urlGeneratorId: string;
+ urlGeneratorId?: string;
/**
* The application state that was used to create the session.
* Should be used, for example, to re-load an expired search session.
*/
- initialState: Record;
+ initialState?: Record;
/**
* Application state that should be used to restore the session.
* For example, relative dates are conveted to absolute ones.
*/
- restoreState: Record;
+ restoreState?: Record;
/**
* Mapping of search request hashes to their corresponsing info (async search id, etc.)
*/
idMapping: Record;
+
+ /**
+ * This value is true if the session was actively stored by the user. If it is false, the session may be purged by the system.
+ */
+ persisted: boolean;
}
+
export interface SearchSessionRequestInfo {
/**
* ID of the async search request
diff --git a/x-pack/plugins/data_enhanced/config.ts b/x-pack/plugins/data_enhanced/config.ts
index 981c398019832..ca737d461e63d 100644
--- a/x-pack/plugins/data_enhanced/config.ts
+++ b/x-pack/plugins/data_enhanced/config.ts
@@ -12,7 +12,8 @@ export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: false }),
pageSize: schema.number({ defaultValue: 10000 }),
trackingInterval: schema.duration({ defaultValue: '10s' }),
- inMemTimeout: schema.duration({ defaultValue: '1m' }),
+ onScreenTimeout: schema.duration({ defaultValue: '10m' }),
+ notTouchedTimeout: schema.duration({ defaultValue: '1m' }),
maxUpdateRetries: schema.number({ defaultValue: 3 }),
defaultExpiration: schema.duration({ defaultValue: '7d' }),
management: schema.object({
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts
index c6a3d088b3cda..a1bbda5dc7cb3 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts
@@ -10,12 +10,12 @@ import moment from 'moment';
import { from, race, timer } from 'rxjs';
import { mapTo, tap } from 'rxjs/operators';
import type { SharePluginStart } from 'src/plugins/share/public';
-import { SessionsConfigSchema } from '../';
import type { ISessionsClient } from '../../../../../../../src/plugins/data/public';
-import type { SearchSessionSavedObjectAttributes } from '../../../../common';
-import { SearchSessionStatus } from '../../../../common/search';
+import { nodeBuilder } from '../../../../../../../src/plugins/data/common';
+import { SearchSessionStatus, SEARCH_SESSION_TYPE } from '../../../../common/search';
import { ACTION } from '../components/actions';
-import { UISession } from '../types';
+import { PersistedSearchSessionSavedObjectAttributes, UISession } from '../types';
+import { SessionsConfigSchema } from '..';
type UrlGeneratorsStart = SharePluginStart['urlGenerators'];
@@ -48,7 +48,7 @@ async function getUrlFromState(
// Helper: factory for a function to map server objects to UI objects
const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema) => async (
- savedObject: SavedObject
+ savedObject: SavedObject
): Promise => {
const {
name,
@@ -110,6 +110,7 @@ export class SearchSessionsMgmtAPI {
perPage: mgmtConfig.maxSessions,
sortField: 'created',
sortOrder: 'asc',
+ filter: nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'true'),
})
);
const timeout$ = timer(refreshTimeout.asMilliseconds()).pipe(
@@ -129,7 +130,7 @@ export class SearchSessionsMgmtAPI {
const result = await race(fetch$, timeout$).toPromise();
if (result && result.saved_objects) {
const savedObjects = result.saved_objects as Array<
- SavedObject
+ SavedObject
>;
return await Promise.all(savedObjects.map(mapToUISession(this.deps.urls, this.config)));
}
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts
index 78b91f7ca8ac2..472cb6d91cb52 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts
@@ -4,11 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SearchSessionStatus } from '../../../common';
+import { SearchSessionSavedObjectAttributes, SearchSessionStatus } from '../../../common';
import { ACTION } from './components/actions';
export const DATE_STRING_FORMAT = 'D MMM, YYYY, HH:mm:ss';
+/**
+ * Some properties are optional for a non-persisted Search Session.
+ * This interface makes them mandatory, because management only shows persisted search sessions.
+ */
+export interface PersistedSearchSessionSavedObjectAttributes
+ extends SearchSessionSavedObjectAttributes {
+ name: string;
+ appId: string;
+ urlGeneratorId: string;
+ initialState: Record;
+ restoreState: Record;
+}
+
export interface UISession {
id: string;
name: string;
diff --git a/x-pack/plugins/data_enhanced/server/saved_objects/search_session.ts b/x-pack/plugins/data_enhanced/server/saved_objects/search_session.ts
index 4e75ffaeec69a..16472199de4d9 100644
--- a/x-pack/plugins/data_enhanced/server/saved_objects/search_session.ts
+++ b/x-pack/plugins/data_enhanced/server/saved_objects/search_session.ts
@@ -5,8 +5,7 @@
*/
import { SavedObjectsType } from 'kibana/server';
-
-export const SEARCH_SESSION_TYPE = 'search-session';
+import { SEARCH_SESSION_TYPE } from '../../common';
export const searchSessionMapping: SavedObjectsType = {
name: SEARCH_SESSION_TYPE,
@@ -14,6 +13,9 @@ export const searchSessionMapping: SavedObjectsType = {
hidden: true,
mappings: {
properties: {
+ persisted: {
+ type: 'boolean',
+ },
sessionId: {
type: 'keyword',
},
@@ -26,6 +28,9 @@ export const searchSessionMapping: SavedObjectsType = {
expires: {
type: 'date',
},
+ touched: {
+ type: 'date',
+ },
status: {
type: 'keyword',
},
diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts
index f2d7725954a26..1670b1116eedb 100644
--- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts
+++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts
@@ -117,7 +117,6 @@ describe('EQL search strategy', () => {
expect(request).toEqual(
expect.objectContaining({
wait_for_completion_timeout: '100ms',
- keep_alive: '1m',
})
);
});
@@ -156,7 +155,6 @@ describe('EQL search strategy', () => {
expect(request).toEqual(
expect.objectContaining({
wait_for_completion_timeout: '5ms',
- keep_alive: '1m',
keep_on_completion: false,
})
);
diff --git a/x-pack/plugins/data_enhanced/server/search/request_utils.ts b/x-pack/plugins/data_enhanced/server/search/request_utils.ts
index f54ab2199c905..12e9c1e35ed52 100644
--- a/x-pack/plugins/data_enhanced/server/search/request_utils.ts
+++ b/x-pack/plugins/data_enhanced/server/search/request_utils.ts
@@ -42,6 +42,7 @@ export async function getDefaultAsyncSubmitParams(
>
> {
return {
+ keep_alive: '7d',
batched_reduce_size: 64,
keep_on_completion: !!options.sessionId, // Always return an ID, even if the request completes quickly
...getDefaultAsyncGetParams(),
@@ -58,7 +59,6 @@ export function getDefaultAsyncGetParams(): Pick<
'keep_alive' | 'wait_for_completion_timeout'
> {
return {
- keep_alive: '1m', // Extend the TTL for this search request by one minute
wait_for_completion_timeout: '100ms', // Wait up to 100ms for the response to return
};
}
diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts
index 4334ab3bc2903..917663252bc8b 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts
@@ -8,11 +8,22 @@ import { checkRunningSessions } from './check_running_sessions';
import { SearchSessionStatus, SearchSessionSavedObjectAttributes } from '../../../common';
import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks';
import type { SavedObjectsClientContract } from 'kibana/server';
-import { SearchStatus } from './types';
+import { SearchSessionsConfig, SearchStatus } from './types';
+import moment from 'moment';
describe('getSearchStatus', () => {
let mockClient: any;
let savedObjectsClient: jest.Mocked;
+ const config: SearchSessionsConfig = {
+ enabled: true,
+ pageSize: 10000,
+ notTouchedTimeout: moment.duration(1, 'm'),
+ onScreenTimeout: moment.duration(5, 'm'),
+ maxUpdateRetries: 3,
+ defaultExpiration: moment.duration(7, 'd'),
+ trackingInterval: moment.duration(10, 's'),
+ management: {} as any,
+ };
const mockLogger: any = {
debug: jest.fn(),
warn: jest.fn(),
@@ -29,163 +40,350 @@ describe('getSearchStatus', () => {
});
test('does nothing if there are no open sessions', async () => {
- savedObjectsClient.bulkUpdate = jest.fn();
savedObjectsClient.find.mockResolvedValue({
saved_objects: [],
total: 0,
} as any);
- await checkRunningSessions(savedObjectsClient, mockClient, mockLogger);
+ await checkRunningSessions(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config
+ );
expect(savedObjectsClient.bulkUpdate).not.toBeCalled();
+ expect(savedObjectsClient.delete).not.toBeCalled();
});
- test('does nothing if there are no searchIds in the saved object', async () => {
- savedObjectsClient.bulkUpdate = jest.fn();
- savedObjectsClient.find.mockResolvedValue({
- saved_objects: [
+ describe('delete', () => {
+ test('doesnt delete a persisted session', async () => {
+ savedObjectsClient.find.mockResolvedValue({
+ saved_objects: [
+ {
+ id: '123',
+ attributes: {
+ persisted: true,
+ status: SearchSessionStatus.IN_PROGRESS,
+ created: moment().subtract(moment.duration(30, 'm')),
+ touched: moment().subtract(moment.duration(10, 'm')),
+ idMapping: {},
+ },
+ },
+ ],
+ total: 1,
+ } as any);
+ await checkRunningSessions(
{
- attributes: {
- idMapping: {},
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config
+ );
+
+ expect(savedObjectsClient.bulkUpdate).not.toBeCalled();
+ expect(savedObjectsClient.delete).not.toBeCalled();
+ });
+
+ test('doesnt delete a non persisted, recently touched session', async () => {
+ savedObjectsClient.find.mockResolvedValue({
+ saved_objects: [
+ {
+ id: '123',
+ attributes: {
+ persisted: false,
+ status: SearchSessionStatus.IN_PROGRESS,
+ created: moment().subtract(moment.duration(3, 'm')),
+ touched: moment().subtract(moment.duration(10, 's')),
+ idMapping: {},
+ },
},
+ ],
+ total: 1,
+ } as any);
+ await checkRunningSessions(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
},
- ],
- total: 1,
- } as any);
+ config
+ );
- await checkRunningSessions(savedObjectsClient, mockClient, mockLogger);
+ expect(savedObjectsClient.bulkUpdate).not.toBeCalled();
+ expect(savedObjectsClient.delete).not.toBeCalled();
+ });
- expect(savedObjectsClient.bulkUpdate).not.toBeCalled();
- });
+ test('doesnt delete a non persisted, completed session, within on screen time frame', async () => {
+ savedObjectsClient.find.mockResolvedValue({
+ saved_objects: [
+ {
+ id: '123',
+ attributes: {
+ persisted: false,
+ status: SearchSessionStatus.COMPLETE,
+ created: moment().subtract(moment.duration(3, 'm')),
+ touched: moment().subtract(moment.duration(1, 'm')),
+ idMapping: {
+ 'search-hash': {
+ id: 'search-id',
+ strategy: 'cool',
+ status: SearchStatus.COMPLETE,
+ },
+ },
+ },
+ },
+ ],
+ total: 1,
+ } as any);
+ await checkRunningSessions(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config
+ );
+
+ expect(savedObjectsClient.bulkUpdate).not.toBeCalled();
+ expect(savedObjectsClient.delete).not.toBeCalled();
+ });
- test('does nothing if the search is still running', async () => {
- savedObjectsClient.bulkUpdate = jest.fn();
- const so = {
- attributes: {
- idMapping: {
- 'search-hash': {
- id: 'search-id',
- strategy: 'cool',
- status: SearchStatus.IN_PROGRESS,
+ test('deletes a non persisted, abandoned session', async () => {
+ savedObjectsClient.find.mockResolvedValue({
+ saved_objects: [
+ {
+ id: '123',
+ attributes: {
+ persisted: false,
+ status: SearchSessionStatus.IN_PROGRESS,
+ created: moment().subtract(moment.duration(3, 'm')),
+ touched: moment().subtract(moment.duration(2, 'm')),
+ idMapping: {},
+ },
},
+ ],
+ total: 1,
+ } as any);
+
+ await checkRunningSessions(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
},
- },
- };
- savedObjectsClient.find.mockResolvedValue({
- saved_objects: [so],
- total: 1,
- } as any);
+ config
+ );
- mockClient.asyncSearch.status.mockResolvedValue({
- body: {
- is_partial: true,
- is_running: true,
- },
+ expect(savedObjectsClient.bulkUpdate).not.toBeCalled();
+ expect(savedObjectsClient.delete).toBeCalled();
});
- await checkRunningSessions(savedObjectsClient, mockClient, mockLogger);
+ test('deletes a completed, not persisted session', async () => {
+ savedObjectsClient.find.mockResolvedValue({
+ saved_objects: [
+ {
+ id: '123',
+ attributes: {
+ persisted: false,
+ status: SearchSessionStatus.COMPLETE,
+ created: moment().subtract(moment.duration(30, 'm')),
+ touched: moment().subtract(moment.duration(5, 'm')),
+ idMapping: {},
+ },
+ },
+ ],
+ total: 1,
+ } as any);
+
+ await checkRunningSessions(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config
+ );
- expect(savedObjectsClient.bulkUpdate).not.toBeCalled();
+ expect(savedObjectsClient.bulkUpdate).not.toBeCalled();
+ expect(savedObjectsClient.delete).toBeCalled();
+ });
});
- test("doesn't re-check completed or errored searches", async () => {
- savedObjectsClient.bulkUpdate = jest.fn();
- const so = {
- attributes: {
- idMapping: {
- 'search-hash': {
- id: 'search-id',
- strategy: 'cool',
- status: SearchStatus.COMPLETE,
- },
- 'another-search-hash': {
- id: 'search-id',
- strategy: 'cool',
- status: SearchStatus.ERROR,
+ describe('update', () => {
+ test('does nothing if the search is still running', async () => {
+ const so = {
+ id: '123',
+ attributes: {
+ persisted: false,
+ status: SearchSessionStatus.IN_PROGRESS,
+ created: moment().subtract(moment.duration(3, 'm')),
+ touched: moment().subtract(moment.duration(10, 's')),
+ idMapping: {
+ 'search-hash': {
+ id: 'search-id',
+ strategy: 'cool',
+ status: SearchStatus.IN_PROGRESS,
+ },
},
},
- },
- };
- savedObjectsClient.find.mockResolvedValue({
- saved_objects: [so],
- total: 1,
- } as any);
+ };
+ savedObjectsClient.find.mockResolvedValue({
+ saved_objects: [so],
+ total: 1,
+ } as any);
- await checkRunningSessions(savedObjectsClient, mockClient, mockLogger);
+ mockClient.asyncSearch.status.mockResolvedValue({
+ body: {
+ is_partial: true,
+ is_running: true,
+ },
+ });
- expect(mockClient.asyncSearch.status).not.toBeCalled();
- });
+ await checkRunningSessions(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config
+ );
- test('updates to complete if the search is done', async () => {
- savedObjectsClient.bulkUpdate = jest.fn();
- const so = {
- attributes: {
- idMapping: {
- 'search-hash': {
- id: 'search-id',
- strategy: 'cool',
- status: SearchStatus.IN_PROGRESS,
+ expect(savedObjectsClient.bulkUpdate).not.toBeCalled();
+ expect(savedObjectsClient.delete).not.toBeCalled();
+ });
+
+ test("doesn't re-check completed or errored searches", async () => {
+ savedObjectsClient.bulkUpdate = jest.fn();
+ savedObjectsClient.delete = jest.fn();
+ const so = {
+ id: '123',
+ attributes: {
+ status: SearchSessionStatus.ERROR,
+ idMapping: {
+ 'search-hash': {
+ id: 'search-id',
+ strategy: 'cool',
+ status: SearchStatus.COMPLETE,
+ },
+ 'another-search-hash': {
+ id: 'search-id',
+ strategy: 'cool',
+ status: SearchStatus.ERROR,
+ },
},
},
- },
- };
- savedObjectsClient.find.mockResolvedValue({
- saved_objects: [so],
- total: 1,
- } as any);
+ };
+ savedObjectsClient.find.mockResolvedValue({
+ saved_objects: [so],
+ total: 1,
+ } as any);
- mockClient.asyncSearch.status.mockResolvedValue({
- body: {
- is_partial: false,
- is_running: false,
- completion_status: 200,
- },
+ await checkRunningSessions(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config
+ );
+
+ expect(mockClient.asyncSearch.status).not.toBeCalled();
+ expect(savedObjectsClient.bulkUpdate).not.toBeCalled();
+ expect(savedObjectsClient.delete).not.toBeCalled();
});
- await checkRunningSessions(savedObjectsClient, mockClient, mockLogger);
+ test('updates to complete if the search is done', async () => {
+ savedObjectsClient.bulkUpdate = jest.fn();
+ const so = {
+ attributes: {
+ status: SearchSessionStatus.IN_PROGRESS,
+ idMapping: {
+ 'search-hash': {
+ id: 'search-id',
+ strategy: 'cool',
+ status: SearchStatus.IN_PROGRESS,
+ },
+ },
+ },
+ };
+ savedObjectsClient.find.mockResolvedValue({
+ saved_objects: [so],
+ total: 1,
+ } as any);
- expect(mockClient.asyncSearch.status).toBeCalledWith({ id: 'search-id' });
- const [updateInput] = savedObjectsClient.bulkUpdate.mock.calls[0];
- const updatedAttributes = updateInput[0].attributes as SearchSessionSavedObjectAttributes;
- expect(updatedAttributes.status).toBe(SearchSessionStatus.COMPLETE);
- expect(updatedAttributes.idMapping['search-hash'].status).toBe(SearchStatus.COMPLETE);
- expect(updatedAttributes.idMapping['search-hash'].error).toBeUndefined();
- });
+ mockClient.asyncSearch.status.mockResolvedValue({
+ body: {
+ is_partial: false,
+ is_running: false,
+ completion_status: 200,
+ },
+ });
- test('updates to error if the search is errored', async () => {
- savedObjectsClient.bulkUpdate = jest.fn();
- const so = {
- attributes: {
- idMapping: {
- 'search-hash': {
- id: 'search-id',
- strategy: 'cool',
- status: SearchStatus.IN_PROGRESS,
- },
+ await checkRunningSessions(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
},
- },
- };
- savedObjectsClient.find.mockResolvedValue({
- saved_objects: [so],
- total: 1,
- } as any);
+ config
+ );
- mockClient.asyncSearch.status.mockResolvedValue({
- body: {
- is_partial: false,
- is_running: false,
- completion_status: 500,
- },
+ expect(mockClient.asyncSearch.status).toBeCalledWith({ id: 'search-id' });
+ const [updateInput] = savedObjectsClient.bulkUpdate.mock.calls[0];
+ const updatedAttributes = updateInput[0].attributes as SearchSessionSavedObjectAttributes;
+ expect(updatedAttributes.status).toBe(SearchSessionStatus.COMPLETE);
+ expect(updatedAttributes.idMapping['search-hash'].status).toBe(SearchStatus.COMPLETE);
+ expect(updatedAttributes.idMapping['search-hash'].error).toBeUndefined();
+
+ expect(savedObjectsClient.delete).not.toBeCalled();
});
- await checkRunningSessions(savedObjectsClient, mockClient, mockLogger);
- const [updateInput] = savedObjectsClient.bulkUpdate.mock.calls[0];
+ test('updates to error if the search is errored', async () => {
+ savedObjectsClient.bulkUpdate = jest.fn();
+ const so = {
+ attributes: {
+ idMapping: {
+ 'search-hash': {
+ id: 'search-id',
+ strategy: 'cool',
+ status: SearchStatus.IN_PROGRESS,
+ },
+ },
+ },
+ };
+ savedObjectsClient.find.mockResolvedValue({
+ saved_objects: [so],
+ total: 1,
+ } as any);
- const updatedAttributes = updateInput[0].attributes as SearchSessionSavedObjectAttributes;
- expect(updatedAttributes.status).toBe(SearchSessionStatus.ERROR);
- expect(updatedAttributes.idMapping['search-hash'].status).toBe(SearchStatus.ERROR);
- expect(updatedAttributes.idMapping['search-hash'].error).toBe(
- 'Search completed with a 500 status'
- );
+ mockClient.asyncSearch.status.mockResolvedValue({
+ body: {
+ is_partial: false,
+ is_running: false,
+ completion_status: 500,
+ },
+ });
+
+ await checkRunningSessions(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config
+ );
+ const [updateInput] = savedObjectsClient.bulkUpdate.mock.calls[0];
+
+ const updatedAttributes = updateInput[0].attributes as SearchSessionSavedObjectAttributes;
+ expect(updatedAttributes.status).toBe(SearchSessionStatus.ERROR);
+ expect(updatedAttributes.idMapping['search-hash'].status).toBe(SearchStatus.ERROR);
+ expect(updatedAttributes.idMapping['search-hash'].error).toBe(
+ 'Search completed with a 500 status'
+ );
+ });
});
});
diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
index 71274e15e284d..3aba7af7b153e 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
@@ -10,28 +10,110 @@ import {
SavedObjectsFindResult,
SavedObjectsClientContract,
} from 'kibana/server';
+import moment from 'moment';
+import { nodeBuilder } from '../../../../../../src/plugins/data/common';
import {
SearchSessionStatus,
SearchSessionSavedObjectAttributes,
SearchSessionRequestInfo,
+ SEARCH_SESSION_TYPE,
} from '../../../common';
-import { SEARCH_SESSION_TYPE } from '../../saved_objects';
import { getSearchStatus } from './get_search_status';
import { getSessionStatus } from './get_session_status';
-import { SearchStatus } from './types';
+import { SearchSessionsConfig, SearchStatus } from './types';
-export async function checkRunningSessions(
- savedObjectsClient: SavedObjectsClientContract,
+export interface CheckRunningSessionsDeps {
+ savedObjectsClient: SavedObjectsClientContract;
+ client: ElasticsearchClient;
+ logger: Logger;
+}
+
+function checkNotPersistedSession(
+ session: SavedObjectsFindResult,
+ config: SearchSessionsConfig,
+ logger: Logger
+) {
+ const curTime = moment();
+ // Delete if a running session wasn't polled for in the last notTouchedTimeout OR if a completed session wasn't saved for within onScreenTimeout
+ return (
+ (session.attributes.status === SearchSessionStatus.IN_PROGRESS &&
+ curTime.diff(moment(session.attributes.touched), 'ms') >
+ config.notTouchedTimeout.asMilliseconds()) ||
+ curTime.diff(moment(session.attributes.created), 'ms') > config.onScreenTimeout.asMilliseconds()
+ );
+}
+
+async function updateSessionStatus(
+ session: SavedObjectsFindResult,
client: ElasticsearchClient,
logger: Logger
+) {
+ let sessionUpdated = false;
+
+ // Check statuses of all running searches
+ await Promise.all(
+ Object.keys(session.attributes.idMapping).map(async (searchKey: string) => {
+ const updateSearchRequest = (
+ currentStatus: Pick
+ ) => {
+ sessionUpdated = true;
+ session.attributes.idMapping[searchKey] = {
+ ...session.attributes.idMapping[searchKey],
+ ...currentStatus,
+ };
+ };
+
+ const searchInfo = session.attributes.idMapping[searchKey];
+ if (searchInfo.status === SearchStatus.IN_PROGRESS) {
+ try {
+ const currentStatus = await getSearchStatus(client, searchInfo.id);
+
+ if (currentStatus.status !== searchInfo.status) {
+ logger.warn(`${currentStatus.status}`);
+ updateSearchRequest(currentStatus);
+ }
+ } catch (e) {
+ logger.error(e);
+ updateSearchRequest({
+ status: SearchStatus.ERROR,
+ error: e.message || e.meta.error?.caused_by?.reason,
+ });
+ }
+ }
+ })
+ );
+
+ // And only then derive the session's status
+ const sessionStatus = getSessionStatus(session.attributes);
+ if (sessionStatus !== session.attributes.status) {
+ session.attributes.status = sessionStatus;
+ sessionUpdated = true;
+ }
+
+ return sessionUpdated;
+}
+
+export async function checkRunningSessions(
+ { savedObjectsClient, client, logger }: CheckRunningSessionsDeps,
+ config: SearchSessionsConfig
): Promise {
try {
+ // TODO: take care of pagination
const runningSearchSessionsResponse = await savedObjectsClient.find(
{
+ perPage: config.pageSize,
type: SEARCH_SESSION_TYPE,
- search: SearchSessionStatus.IN_PROGRESS.toString(),
- searchFields: ['status'],
namespaces: ['*'],
+ filter: nodeBuilder.or([
+ nodeBuilder.and([
+ nodeBuilder.is(
+ `${SEARCH_SESSION_TYPE}.attributes.status`,
+ SearchSessionStatus.IN_PROGRESS.toString()
+ ),
+ nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'true'),
+ ]),
+ nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'false'),
+ ]),
}
);
@@ -41,61 +123,37 @@ export async function checkRunningSessions(
const updatedSessions = new Array>();
- let sessionUpdated = false;
-
await Promise.all(
runningSearchSessionsResponse.saved_objects.map(async (session) => {
- // Check statuses of all running searches
- await Promise.all(
- Object.keys(session.attributes.idMapping).map(async (searchKey: string) => {
- const updateSearchRequest = (
- currentStatus: Pick
- ) => {
- sessionUpdated = true;
- session.attributes.idMapping[searchKey] = {
- ...session.attributes.idMapping[searchKey],
- ...currentStatus,
- };
- };
-
- const searchInfo = session.attributes.idMapping[searchKey];
- if (searchInfo.status === SearchStatus.IN_PROGRESS) {
- try {
- const currentStatus = await getSearchStatus(client, searchInfo.id);
-
- if (currentStatus.status !== SearchStatus.IN_PROGRESS) {
- updateSearchRequest(currentStatus);
- }
- } catch (e) {
- logger.error(e);
- updateSearchRequest({
- status: SearchStatus.ERROR,
- error: e.message || e.meta.error?.caused_by?.reason,
- });
- }
- }
- })
- );
-
- // And only then derive the session's status
- const sessionStatus = getSessionStatus(session.attributes);
- if (sessionStatus !== SearchSessionStatus.IN_PROGRESS) {
- session.attributes.status = sessionStatus;
- sessionUpdated = true;
+ const updated = await updateSessionStatus(session, client, logger);
+ let deleted = false;
+
+ if (!session.attributes.persisted) {
+ if (checkNotPersistedSession(session, config, logger)) {
+ deleted = true;
+ // delete saved object to free up memory
+ // TODO: there's a potential rare edge case of deleting an object and then receiving a new trackId for that same session!
+ // Maybe we want to change state to deleted and cleanup later?
+ logger.debug(`Deleting stale session | ${session.id}`);
+ await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id);
+
+ // TODO cancel running search requests
+ }
}
- if (sessionUpdated) {
+ if (updated && !deleted) {
updatedSessions.push(session);
}
})
);
+ // Do a bulk update
if (updatedSessions.length) {
// If there's an error, we'll try again in the next iteration, so there's no need to check the output.
const updatedResponse = await savedObjectsClient.bulkUpdate(
updatedSessions
);
- logger.debug(`Updated ${updatedResponse.saved_objects.length} background sessions`);
+ logger.debug(`Updated ${updatedResponse.saved_objects.length} search sessions`);
}
} catch (err) {
logger.error(err);
diff --git a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts
index 332e69b119bb6..7883086513f1b 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts
@@ -14,10 +14,10 @@ import {
} from '../../../../task_manager/server';
import { checkRunningSessions } from './check_running_sessions';
import { CoreSetup, SavedObjectsClient, Logger } from '../../../../../../src/core/server';
-import { SEARCH_SESSION_TYPE } from '../../saved_objects';
import { ConfigSchema } from '../../../config';
+import { SEARCH_SESSION_TYPE } from '../../../common';
-export const SEARCH_SESSIONS_TASK_TYPE = 'bg_monitor';
+export const SEARCH_SESSIONS_TASK_TYPE = 'search_sessions_monitor';
export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYPE}`;
interface SearchSessionTaskDeps {
@@ -31,17 +31,21 @@ function searchSessionRunner(core: CoreSetup, { logger, config$ }: SearchSession
return {
async run() {
const config = await config$.pipe(first()).toPromise();
+ const sessionConfig = config.search.sessions;
const [coreStart] = await core.getStartServices();
const internalRepo = coreStart.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]);
const internalSavedObjectsClient = new SavedObjectsClient(internalRepo);
await checkRunningSessions(
- internalSavedObjectsClient,
- coreStart.elasticsearch.client.asInternalUser,
- logger
+ {
+ savedObjectsClient: internalSavedObjectsClient,
+ client: coreStart.elasticsearch.client.asInternalUser,
+ logger,
+ },
+ sessionConfig
);
return {
- runAt: new Date(Date.now() + config.search.sessions.trackingInterval.asMilliseconds()),
+ runAt: new Date(Date.now() + sessionConfig.trackingInterval.asMilliseconds()),
state: {},
};
},
diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts
index 1107ed8155080..10549b18f521a 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts
@@ -5,21 +5,22 @@
*/
import { BehaviorSubject, of } from 'rxjs';
-import type { SavedObject, SavedObjectsClientContract } from 'kibana/server';
+import {
+ SavedObject,
+ SavedObjectsClientContract,
+ SavedObjectsErrorHelpers,
+} from '../../../../../../src/core/server';
import type { SearchStrategyDependencies } from '../../../../../../src/plugins/data/server';
import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks';
-import { SearchSessionStatus } from '../../../common';
-import { SEARCH_SESSION_TYPE } from '../../saved_objects';
-import { SearchSessionDependencies, SearchSessionService, SessionInfo } from './session_service';
+import { SearchSessionStatus, SEARCH_SESSION_TYPE } from '../../../common';
+import { SearchSessionDependencies, SearchSessionService } from './session_service';
import { createRequestHash } from './utils';
import moment from 'moment';
import { coreMock } from 'src/core/server/mocks';
import { ConfigSchema } from '../../../config';
// @ts-ignore
import { taskManagerMock } from '../../../../task_manager/server/mocks';
-import { SearchStatus } from './types';
-const INMEM_TRACKING_INTERVAL = 10000;
const MAX_UPDATE_RETRIES = 3;
const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
@@ -28,67 +29,7 @@ describe('SearchSessionService', () => {
let savedObjectsClient: jest.Mocked;
let service: SearchSessionService;
- const MOCK_SESSION_ID = 'session-id-mock';
- const MOCK_ASYNC_ID = '123456';
const MOCK_STRATEGY = 'ese';
- const MOCK_KEY_HASH = '608de49a4600dbb5b173492759792e4a';
-
- const createMockInternalSavedObjectClient = (
- findSpy?: jest.SpyInstance,
- bulkUpdateSpy?: jest.SpyInstance
- ) => {
- Object.defineProperty(service, 'internalSavedObjectsClient', {
- get: () => {
- const find =
- findSpy ||
- (() => {
- return {
- saved_objects: [
- {
- attributes: {
- sessionId: MOCK_SESSION_ID,
- idMapping: {
- 'another-key': {
- id: 'another-async-id',
- strategy: 'another-strategy',
- },
- },
- },
- id: MOCK_SESSION_ID,
- version: '1',
- },
- ],
- };
- });
-
- const bulkUpdate =
- bulkUpdateSpy ||
- (() => {
- return {
- saved_objects: [],
- };
- });
- return {
- find,
- bulkUpdate,
- };
- },
- });
- };
-
- const createMockIdMapping = (
- mapValues: any[],
- insertTime?: moment.Moment,
- retryCount?: number
- ): Map => {
- const fakeMap = new Map();
- fakeMap.set(MOCK_SESSION_ID, {
- ids: new Map(mapValues),
- insertTime: insertTime || moment(),
- retryCount: retryCount || 0,
- });
- return fakeMap;
- };
const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
const mockSavedObject: SavedObject = {
@@ -110,8 +51,9 @@ describe('SearchSessionService', () => {
sessions: {
enabled: true,
pageSize: 10000,
- inMemTimeout: moment.duration(1, 'm'),
- maxUpdateRetries: 3,
+ notTouchedTimeout: moment.duration(1, 'm'),
+ onScreenTimeout: moment.duration(2, 'm'),
+ maxUpdateRetries: MAX_UPDATE_RETRIES,
defaultExpiration: moment.duration(7, 'd'),
trackingInterval: moment.duration(10, 's'),
management: {} as any,
@@ -126,7 +68,6 @@ describe('SearchSessionService', () => {
service = new SearchSessionService(mockLogger, config$);
const coreStart = coreMock.createStart();
const mockTaskManager = taskManagerMock.createStart();
- jest.useFakeTimers();
await flushPromises();
await service.start(coreStart, {
taskManager: mockTaskManager,
@@ -135,19 +76,6 @@ describe('SearchSessionService', () => {
afterEach(() => {
service.stop();
- jest.useRealTimers();
- });
-
- it('search throws if `name` is not provided', () => {
- expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot(
- `[Error: Name is required]`
- );
- });
-
- it('save throws if `name` is not provided', () => {
- expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot(
- `[Error: Name is required]`
- );
});
it('get calls saved objects client', async () => {
@@ -182,7 +110,7 @@ describe('SearchSessionService', () => {
});
});
- it('update calls saved objects client', async () => {
+ it('update calls saved objects client with added touch time', async () => {
const mockUpdateSavedObject = {
...mockSavedObject,
attributes: {},
@@ -193,11 +121,13 @@ describe('SearchSessionService', () => {
const response = await service.update(sessionId, attributes, { savedObjectsClient });
expect(response).toBe(mockUpdateSavedObject);
- expect(savedObjectsClient.update).toHaveBeenCalledWith(
- SEARCH_SESSION_TYPE,
- sessionId,
- attributes
- );
+
+ const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0];
+
+ expect(type).toBe(SEARCH_SESSION_TYPE);
+ expect(id).toBe(sessionId);
+ expect(callAttributes).toHaveProperty('name', attributes.name);
+ expect(callAttributes).toHaveProperty('touched');
});
it('delete calls saved objects client', async () => {
@@ -256,17 +186,17 @@ describe('SearchSessionService', () => {
spyGetId.mockRestore();
});
- it('calls `trackId` once if the response contains an `id` and not restoring', async () => {
+ it('calls `trackId` for every response, if the response contains an `id` and not restoring', async () => {
const searchRequest = { params: {} };
const options = { sessionId, isStored: false, isRestore: false };
- const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue();
+ const spyTrackId = jest.spyOn(service, 'trackId');
mockSearch.mockReturnValueOnce(of({ id: 'my_id' }, { id: 'my_id' }));
await service
.search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps)
.toPromise();
- expect(spyTrackId).toBeCalledTimes(1);
+ expect(spyTrackId).toBeCalledTimes(2);
expect(spyTrackId).toBeCalledWith(searchRequest, 'my_id', options, {});
spyTrackId.mockRestore();
@@ -276,7 +206,7 @@ describe('SearchSessionService', () => {
const searchRequest = { params: {} };
const options = { sessionId, isStored: true, isRestore: true };
const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id');
- const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue();
+ const spyTrackId = jest.spyOn(service, 'trackId');
mockSearch.mockReturnValueOnce(of({ id: 'my_id' }));
await service
@@ -291,84 +221,194 @@ describe('SearchSessionService', () => {
});
describe('trackId', () => {
- it('stores hash in memory when `isStored` is `false` for when `save` is called', async () => {
+ it('updates the saved object if search session already exists', async () => {
const searchRequest = { params: {} };
const requestHash = createRequestHash(searchRequest.params);
const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';
- const isStored = false;
- const name = 'my saved background search session';
- const appId = 'my_app_id';
- const urlGeneratorId = 'my_url_generator_id';
- const created = new Date().toISOString();
- const expires = new Date().toISOString();
-
- const mockIdMapping = createMockIdMapping([]);
- const setSpy = jest.fn();
- mockIdMapping.set = setSpy;
- Object.defineProperty(service, 'sessionSearchMap', {
- get: () => mockIdMapping,
- });
+
+ const mockUpdateSavedObject = {
+ ...mockSavedObject,
+ attributes: {},
+ };
+ savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject);
await service.trackId(
searchRequest,
searchId,
- { sessionId, isStored, strategy: MOCK_STRATEGY },
+ { sessionId, strategy: MOCK_STRATEGY },
{ savedObjectsClient }
);
- expect(savedObjectsClient.update).not.toHaveBeenCalled();
+ expect(savedObjectsClient.update).toHaveBeenCalled();
+ expect(savedObjectsClient.create).not.toHaveBeenCalled();
- await service.save(
- sessionId,
- { name, created, expires, appId, urlGeneratorId },
+ const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0];
+ expect(type).toBe(SEARCH_SESSION_TYPE);
+ expect(id).toBe(sessionId);
+ expect(callAttributes).toHaveProperty('idMapping', {
+ [requestHash]: {
+ id: searchId,
+ status: SearchSessionStatus.IN_PROGRESS,
+ strategy: MOCK_STRATEGY,
+ },
+ });
+ expect(callAttributes).toHaveProperty('touched');
+ });
+
+ it('retries updating the saved object if there was a ES conflict 409', async () => {
+ const searchRequest = { params: {} };
+ const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';
+
+ const mockUpdateSavedObject = {
+ ...mockSavedObject,
+ attributes: {},
+ };
+
+ let counter = 0;
+
+ savedObjectsClient.update.mockImplementation(() => {
+ return new Promise((resolve, reject) => {
+ if (counter === 0) {
+ counter++;
+ reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId));
+ } else {
+ resolve(mockUpdateSavedObject);
+ }
+ });
+ });
+
+ await service.trackId(
+ searchRequest,
+ searchId,
+ { sessionId, strategy: MOCK_STRATEGY },
{ savedObjectsClient }
);
- expect(savedObjectsClient.create).toHaveBeenCalledWith(
- SEARCH_SESSION_TYPE,
- {
- name,
- created,
- expires,
- initialState: {},
- restoreState: {},
- status: SearchSessionStatus.IN_PROGRESS,
- idMapping: {},
- appId,
- urlGeneratorId,
- sessionId,
- },
- { id: sessionId }
+ expect(savedObjectsClient.update).toHaveBeenCalledTimes(2);
+ expect(savedObjectsClient.create).not.toHaveBeenCalled();
+ });
+
+ it('retries updating the saved object if theres a ES conflict 409, but stops after MAX_RETRIES times', async () => {
+ const searchRequest = { params: {} };
+ const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';
+
+ savedObjectsClient.update.mockImplementation(() => {
+ return new Promise((resolve, reject) => {
+ reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId));
+ });
+ });
+
+ await service.trackId(
+ searchRequest,
+ searchId,
+ { sessionId, strategy: MOCK_STRATEGY },
+ { savedObjectsClient }
);
- const [setSessionId, setParams] = setSpy.mock.calls[0];
- expect(setParams.ids.get(requestHash).id).toBe(searchId);
- expect(setParams.ids.get(requestHash).strategy).toBe(MOCK_STRATEGY);
- expect(setSessionId).toBe(sessionId);
+ // Track ID doesn't throw errors even in cases of failure!
+ expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES);
+ expect(savedObjectsClient.create).not.toHaveBeenCalled();
});
- it('updates saved object when `isStored` is `true`', async () => {
+ it('creates the saved object in non persisted state, if search session doesnt exists', async () => {
const searchRequest = { params: {} };
const requestHash = createRequestHash(searchRequest.params);
const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';
- const isStored = true;
+
+ const mockCreatedSavedObject = {
+ ...mockSavedObject,
+ attributes: {},
+ };
+
+ savedObjectsClient.update.mockRejectedValue(
+ SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId)
+ );
+ savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject);
await service.trackId(
searchRequest,
searchId,
- { sessionId, isStored, strategy: MOCK_STRATEGY },
+ { sessionId, strategy: MOCK_STRATEGY },
{ savedObjectsClient }
);
- expect(savedObjectsClient.update).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId, {
- idMapping: {
- [requestHash]: {
- id: searchId,
- strategy: MOCK_STRATEGY,
- status: SearchStatus.IN_PROGRESS,
- },
+ expect(savedObjectsClient.update).toHaveBeenCalled();
+ expect(savedObjectsClient.create).toHaveBeenCalled();
+
+ const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0];
+ expect(type).toBe(SEARCH_SESSION_TYPE);
+ expect(options).toStrictEqual({ id: sessionId });
+ expect(callAttributes).toHaveProperty('idMapping', {
+ [requestHash]: {
+ id: searchId,
+ status: SearchSessionStatus.IN_PROGRESS,
+ strategy: MOCK_STRATEGY,
},
});
+ expect(callAttributes).toHaveProperty('expires');
+ expect(callAttributes).toHaveProperty('created');
+ expect(callAttributes).toHaveProperty('touched');
+ expect(callAttributes).toHaveProperty('sessionId', sessionId);
+ expect(callAttributes).toHaveProperty('persisted', false);
+ });
+
+ it('retries updating if update returned 404 and then update returned conflict 409 (first create race condition)', async () => {
+ const searchRequest = { params: {} };
+ const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';
+
+ const mockUpdateSavedObject = {
+ ...mockSavedObject,
+ attributes: {},
+ };
+
+ let counter = 0;
+
+ savedObjectsClient.update.mockImplementation(() => {
+ return new Promise((resolve, reject) => {
+ if (counter === 0) {
+ counter++;
+ reject(SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId));
+ } else {
+ resolve(mockUpdateSavedObject);
+ }
+ });
+ });
+
+ savedObjectsClient.create.mockRejectedValue(
+ SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)
+ );
+
+ await service.trackId(
+ searchRequest,
+ searchId,
+ { sessionId, strategy: MOCK_STRATEGY },
+ { savedObjectsClient }
+ );
+
+ expect(savedObjectsClient.update).toHaveBeenCalledTimes(2);
+ expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
+ });
+
+ it('retries everything at most MAX_RETRIES times', async () => {
+ const searchRequest = { params: {} };
+ const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';
+
+ savedObjectsClient.update.mockRejectedValue(
+ SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId)
+ );
+ savedObjectsClient.create.mockRejectedValue(
+ SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)
+ );
+
+ await service.trackId(
+ searchRequest,
+ searchId,
+ { sessionId, strategy: MOCK_STRATEGY },
+ { savedObjectsClient }
+ );
+
+ expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES);
+ expect(savedObjectsClient.create).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES);
});
});
@@ -437,194 +477,95 @@ describe('SearchSessionService', () => {
});
});
- describe('Monitor', () => {
- it('schedules the next iteration', async () => {
- const findSpy = jest.fn().mockResolvedValue({ saved_objects: [] });
- createMockInternalSavedObjectClient(findSpy);
-
- const mockIdMapping = createMockIdMapping(
- [[MOCK_KEY_HASH, { id: MOCK_ASYNC_ID, strategy: MOCK_STRATEGY }]],
- moment()
+ describe('save', () => {
+ it('save throws if `name` is not provided', () => {
+ expect(service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot(
+ `[Error: Name is required]`
);
-
- Object.defineProperty(service, 'sessionSearchMap', {
- get: () => mockIdMapping,
- });
-
- jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL);
- expect(findSpy).toHaveBeenCalledTimes(1);
- await flushPromises();
-
- jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL);
- expect(findSpy).toHaveBeenCalledTimes(2);
- });
-
- it('should delete expired IDs', async () => {
- const findSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] });
- createMockInternalSavedObjectClient(findSpy);
-
- const mockIdMapping = createMockIdMapping(
- [[MOCK_KEY_HASH, { id: MOCK_ASYNC_ID, strategy: MOCK_STRATEGY }]],
- moment().subtract(2, 'm')
- );
-
- const deleteSpy = jest.spyOn(mockIdMapping, 'delete');
- Object.defineProperty(service, 'sessionSearchMap', {
- get: () => mockIdMapping,
- });
-
- // Get setInterval to fire
- jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL);
-
- expect(findSpy).not.toHaveBeenCalled();
- expect(deleteSpy).toHaveBeenCalledTimes(1);
- });
-
- it('should delete IDs that passed max retries', async () => {
- const findSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] });
- createMockInternalSavedObjectClient(findSpy);
-
- const mockIdMapping = createMockIdMapping(
- [[MOCK_KEY_HASH, { id: MOCK_ASYNC_ID, strategy: MOCK_STRATEGY }]],
- moment(),
- MAX_UPDATE_RETRIES
- );
-
- const deleteSpy = jest.spyOn(mockIdMapping, 'delete');
- Object.defineProperty(service, 'sessionSearchMap', {
- get: () => mockIdMapping,
- });
-
- // Get setInterval to fire
- jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL);
-
- expect(findSpy).not.toHaveBeenCalled();
- expect(deleteSpy).toHaveBeenCalledTimes(1);
});
- it('should not fetch when no IDs are mapped', async () => {
- const findSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] });
- createMockInternalSavedObjectClient(findSpy);
-
- jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL);
- expect(findSpy).not.toHaveBeenCalled();
+ it('save throws if `appId` is not provided', () => {
+ expect(
+ service.save(sessionId, { name: 'banana' }, { savedObjectsClient })
+ ).rejects.toMatchInlineSnapshot(`[Error: AppId is required]`);
});
- it('should try to fetch saved objects if some ids are mapped', async () => {
- const mockIdMapping = createMockIdMapping([[MOCK_KEY_HASH, MOCK_ASYNC_ID]]);
- Object.defineProperty(service, 'sessionSearchMap', {
- get: () => mockIdMapping,
- });
-
- const findSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] });
- const bulkUpdateSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] });
- createMockInternalSavedObjectClient(findSpy, bulkUpdateSpy);
-
- jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL);
- expect(findSpy).toHaveBeenCalledTimes(1);
- expect(bulkUpdateSpy).not.toHaveBeenCalled();
+ it('save throws if `generator id` is not provided', () => {
+ expect(
+ service.save(sessionId, { name: 'banana', appId: 'nanana' }, { savedObjectsClient })
+ ).rejects.toMatchInlineSnapshot(`[Error: UrlGeneratorId is required]`);
});
- it('should update saved objects if they are found, and delete session on success', async () => {
- const mockIdMapping = createMockIdMapping([[MOCK_KEY_HASH, MOCK_ASYNC_ID]], undefined, 1);
- const mockMapDeleteSpy = jest.fn();
- const mockSessionDeleteSpy = jest.fn();
- mockIdMapping.delete = mockMapDeleteSpy;
- mockIdMapping.get(MOCK_SESSION_ID)!.ids.delete = mockSessionDeleteSpy;
- Object.defineProperty(service, 'sessionSearchMap', {
- get: () => mockIdMapping,
- });
-
- const findSpy = jest.fn().mockResolvedValueOnce({
- saved_objects: [
- {
- id: MOCK_SESSION_ID,
- attributes: {
- idMapping: {
- b: 'c',
- },
- },
- },
- ],
- });
- const bulkUpdateSpy = jest.fn().mockResolvedValueOnce({
- saved_objects: [
- {
- id: MOCK_SESSION_ID,
- attributes: {
- idMapping: {
- b: 'c',
- [MOCK_KEY_HASH]: {
- id: MOCK_ASYNC_ID,
- strategy: MOCK_STRATEGY,
- },
- },
- },
- },
- ],
- });
- createMockInternalSavedObjectClient(findSpy, bulkUpdateSpy);
-
- jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL);
+ it('saving updates an existing saved object and persists it', async () => {
+ const mockUpdateSavedObject = {
+ ...mockSavedObject,
+ attributes: {},
+ };
+ savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject);
- // Release timers to call check after test actions are done.
- jest.useRealTimers();
- await new Promise((r) => setTimeout(r, 15));
+ await service.save(
+ sessionId,
+ {
+ name: 'banana',
+ appId: 'nanana',
+ urlGeneratorId: 'panama',
+ },
+ { savedObjectsClient }
+ );
- expect(findSpy).toHaveBeenCalledTimes(1);
- expect(bulkUpdateSpy).toHaveBeenCalledTimes(1);
- expect(mockSessionDeleteSpy).toHaveBeenCalledTimes(2);
- expect(mockSessionDeleteSpy).toBeCalledWith('b');
- expect(mockSessionDeleteSpy).toBeCalledWith(MOCK_KEY_HASH);
- expect(mockMapDeleteSpy).toBeCalledTimes(1);
+ expect(savedObjectsClient.update).toHaveBeenCalled();
+ expect(savedObjectsClient.create).not.toHaveBeenCalled();
+
+ const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0];
+ expect(type).toBe(SEARCH_SESSION_TYPE);
+ expect(id).toBe(sessionId);
+ expect(callAttributes).not.toHaveProperty('idMapping');
+ expect(callAttributes).toHaveProperty('touched');
+ expect(callAttributes).toHaveProperty('persisted', true);
+ expect(callAttributes).toHaveProperty('name', 'banana');
+ expect(callAttributes).toHaveProperty('appId', 'nanana');
+ expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama');
+ expect(callAttributes).toHaveProperty('initialState', {});
+ expect(callAttributes).toHaveProperty('restoreState', {});
});
- it('should update saved objects if they are found, and increase retryCount on error', async () => {
- const mockIdMapping = createMockIdMapping([[MOCK_KEY_HASH, MOCK_ASYNC_ID]]);
- const mockMapDeleteSpy = jest.fn();
- const mockSessionDeleteSpy = jest.fn();
- mockIdMapping.delete = mockMapDeleteSpy;
- mockIdMapping.get(MOCK_SESSION_ID)!.ids.delete = mockSessionDeleteSpy;
- Object.defineProperty(service, 'sessionSearchMap', {
- get: () => mockIdMapping,
- });
-
- const findSpy = jest.fn().mockResolvedValueOnce({
- saved_objects: [
- {
- id: MOCK_SESSION_ID,
- attributes: {
- idMapping: {
- b: {
- id: 'c',
- strategy: MOCK_STRATEGY,
- },
- },
- },
- },
- ],
- });
- const bulkUpdateSpy = jest.fn().mockResolvedValueOnce({
- saved_objects: [
- {
- id: MOCK_SESSION_ID,
- error: 'not ok',
- },
- ],
- });
- createMockInternalSavedObjectClient(findSpy, bulkUpdateSpy);
+ it('saving creates a new persisted saved object, if it did not exist', async () => {
+ const mockCreatedSavedObject = {
+ ...mockSavedObject,
+ attributes: {},
+ };
- jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL);
+ savedObjectsClient.update.mockRejectedValue(
+ SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId)
+ );
+ savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject);
- // Release timers to call check after test actions are done.
- jest.useRealTimers();
- await new Promise((r) => setTimeout(r, 15));
+ await service.save(
+ sessionId,
+ {
+ name: 'banana',
+ appId: 'nanana',
+ urlGeneratorId: 'panama',
+ },
+ { savedObjectsClient }
+ );
- expect(findSpy).toHaveBeenCalledTimes(1);
- expect(bulkUpdateSpy).toHaveBeenCalledTimes(1);
- expect(mockSessionDeleteSpy).not.toHaveBeenCalled();
- expect(mockMapDeleteSpy).not.toHaveBeenCalled();
- expect(mockIdMapping.get(MOCK_SESSION_ID)!.retryCount).toBe(1);
+ expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
+ expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
+
+ const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0];
+ expect(type).toBe(SEARCH_SESSION_TYPE);
+ expect(options?.id).toBe(sessionId);
+ expect(callAttributes).toHaveProperty('idMapping', {});
+ expect(callAttributes).toHaveProperty('touched');
+ expect(callAttributes).toHaveProperty('expires');
+ expect(callAttributes).toHaveProperty('created');
+ expect(callAttributes).toHaveProperty('persisted', true);
+ expect(callAttributes).toHaveProperty('name', 'banana');
+ expect(callAttributes).toHaveProperty('appId', 'nanana');
+ expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama');
+ expect(callAttributes).toHaveProperty('initialState', {});
+ expect(callAttributes).toHaveProperty('restoreState', {});
});
});
});
diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
index 794baa21e2f4c..1c5a857a630f6 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
@@ -4,27 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import moment, { Moment } from 'moment';
import { from, Observable } from 'rxjs';
-import { first, switchMap } from 'rxjs/operators';
+import { first, switchMap, tap } from 'rxjs/operators';
import {
CoreStart,
KibanaRequest,
- SavedObjectsClient,
SavedObjectsClientContract,
Logger,
- SavedObject,
CoreSetup,
- SavedObjectsBulkUpdateObject,
SavedObjectsFindOptions,
+ SavedObjectsErrorHelpers,
+ SavedObjectsUpdateResponse,
+ SavedObject,
} from '../../../../../../src/core/server';
import {
IKibanaSearchRequest,
IKibanaSearchResponse,
ISearchOptions,
- KueryNode,
- nodeBuilder,
- tapFirst,
} from '../../../../../../src/plugins/data/common';
import {
ISearchStrategy,
@@ -37,25 +33,17 @@ import {
} from '../../../../task_manager/server';
import {
SearchSessionSavedObjectAttributes,
- SearchSessionRequestInfo,
SearchSessionStatus,
+ SEARCH_SESSION_TYPE,
} from '../../../common';
-import { SEARCH_SESSION_TYPE } from '../../saved_objects';
import { createRequestHash } from './utils';
import { ConfigSchema } from '../../../config';
import { registerSearchSessionsTask, scheduleSearchSessionsTasks } from './monitoring_task';
-import { SearchStatus } from './types';
+import { SearchSessionsConfig, SearchStatus } from './types';
export interface SearchSessionDependencies {
savedObjectsClient: SavedObjectsClientContract;
}
-
-export interface SessionInfo {
- insertTime: Moment;
- retryCount: number;
- ids: Map;
-}
-
interface SetupDependencies {
taskManager: TaskManagerSetupContract;
}
@@ -64,16 +52,10 @@ interface StartDependencies {
taskManager: TaskManagerStartContract;
}
-type SearchSessionsConfig = ConfigSchema['search']['sessions'];
-
+function sleep(ms: number) {
+ return new Promise((r) => setTimeout(r, ms));
+}
export class SearchSessionService implements ISessionService {
- /**
- * Map of sessionId to { [requestHash]: searchId }
- * @private
- */
- private sessionSearchMap = new Map();
- private internalSavedObjectsClient!: SavedObjectsClientContract;
- private monitorTimer!: NodeJS.Timeout;
private config!: SearchSessionsConfig;
constructor(
@@ -95,139 +77,14 @@ export class SearchSessionService implements ISessionService {
return this.setupMonitoring(core, deps);
}
- public stop() {
- this.sessionSearchMap.clear();
- clearTimeout(this.monitorTimer);
- }
+ public stop() {}
private setupMonitoring = async (core: CoreStart, deps: StartDependencies) => {
if (this.config.enabled) {
scheduleSearchSessionsTasks(deps.taskManager, this.logger, this.config.trackingInterval);
- this.logger.debug(`setupMonitoring | Enabling monitoring`);
- const internalRepo = core.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]);
- this.internalSavedObjectsClient = new SavedObjectsClient(internalRepo);
- this.monitorMappedIds();
}
};
- /**
- * Compiles a KQL Query to fetch sessions by ID.
- * Done as a performance optimization workaround.
- */
- private sessionIdsAsFilters(sessionIds: string[]): KueryNode {
- return nodeBuilder.or(
- sessionIds.map((id) => {
- return nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.sessionId`, id);
- })
- );
- }
-
- /**
- * Gets all {@link SessionSavedObjectAttributes | Background Searches} that
- * currently being tracked by the service.
- *
- * @remarks
- * Uses `internalSavedObjectsClient` as this is called asynchronously, not within the
- * context of a user's session.
- */
- private async getAllMappedSavedObjects() {
- const filter = this.sessionIdsAsFilters(Array.from(this.sessionSearchMap.keys()));
- const res = await this.internalSavedObjectsClient.find({
- perPage: this.config.pageSize, // If there are more sessions in memory, they will be synced when some items are cleared out.
- type: SEARCH_SESSION_TYPE,
- filter,
- namespaces: ['*'],
- });
- this.logger.debug(`getAllMappedSavedObjects | Got ${res.saved_objects.length} items`);
- return res.saved_objects;
- }
-
- private clearSessions = async () => {
- const curTime = moment();
-
- this.sessionSearchMap.forEach((sessionInfo, sessionId) => {
- if (
- moment.duration(curTime.diff(sessionInfo.insertTime)).asMilliseconds() >
- this.config.inMemTimeout.asMilliseconds()
- ) {
- this.logger.debug(`clearSessions | Deleting expired session ${sessionId}`);
- this.sessionSearchMap.delete(sessionId);
- } else if (sessionInfo.retryCount >= this.config.maxUpdateRetries) {
- this.logger.warn(`clearSessions | Deleting failed session ${sessionId}`);
- this.sessionSearchMap.delete(sessionId);
- }
- });
- };
-
- private async monitorMappedIds() {
- this.monitorTimer = setTimeout(async () => {
- try {
- this.clearSessions();
-
- if (!this.sessionSearchMap.size) return;
- this.logger.debug(`monitorMappedIds | Map contains ${this.sessionSearchMap.size} items`);
-
- const savedSessions = await this.getAllMappedSavedObjects();
- const updatedSessions = await this.updateAllSavedObjects(savedSessions);
-
- updatedSessions.forEach((updatedSavedObject) => {
- const sessionInfo = this.sessionSearchMap.get(updatedSavedObject.id)!;
- if (updatedSavedObject.error) {
- this.logger.warn(
- `monitorMappedIds | update error ${JSON.stringify(updatedSavedObject.error) || ''}`
- );
- // Retry next time
- sessionInfo.retryCount++;
- } else if (updatedSavedObject.attributes.idMapping) {
- // Delete the ids that we just saved, avoiding a potential new ids being lost.
- Object.keys(updatedSavedObject.attributes.idMapping).forEach((key) => {
- sessionInfo.ids.delete(key);
- });
- // If the session object is empty, delete it as well
- if (!sessionInfo.ids.entries.length) {
- this.sessionSearchMap.delete(updatedSavedObject.id);
- } else {
- sessionInfo.retryCount = 0;
- }
- }
- });
- } catch (e) {
- this.logger.error(`monitorMappedIds | Error while updating sessions. ${e}`);
- } finally {
- this.monitorMappedIds();
- }
- }, this.config.trackingInterval.asMilliseconds());
- }
-
- private async updateAllSavedObjects(
- activeMappingObjects: Array>
- ) {
- if (!activeMappingObjects.length) return [];
-
- this.logger.debug(`updateAllSavedObjects | Updating ${activeMappingObjects.length} items`);
- const updatedSessions: Array<
- SavedObjectsBulkUpdateObject
- > = activeMappingObjects
- .filter((so) => !so.error)
- .map((sessionSavedObject) => {
- const sessionInfo = this.sessionSearchMap.get(sessionSavedObject.id);
- const idMapping = sessionInfo ? Object.fromEntries(sessionInfo.ids.entries()) : {};
- sessionSavedObject.attributes.idMapping = {
- ...sessionSavedObject.attributes.idMapping,
- ...idMapping,
- };
- return {
- ...sessionSavedObject,
- namespace: sessionSavedObject.namespaces?.[0],
- };
- });
-
- const updateResults = await this.internalSavedObjectsClient.bulkUpdate(
- updatedSessions
- );
- return updateResults.saved_objects;
- }
-
public search(
strategy: ISearchStrategy,
searchRequest: Request,
@@ -246,53 +103,110 @@ export class SearchSessionService implements ISessionService {
return from(getSearchRequest()).pipe(
switchMap((request) => strategy.search(request, options, searchDeps)),
- tapFirst((response) => {
- if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return;
+ tap((response) => {
+ if (!options.sessionId || !response.id || options.isRestore) return;
this.trackId(searchRequest, response.id, options, deps);
})
);
}
- // TODO: Generate the `userId` from the realm type/realm name/username
+ private updateOrCreate = async (
+ sessionId: string,
+ attributes: Partial,
+ deps: SearchSessionDependencies,
+ retry: number = 1
+ ): Promise<
+ | SavedObjectsUpdateResponse
+ | SavedObject
+ | undefined
+ > => {
+ this.logger.debug(`updateOrCreate | ${sessionId} | ${retry}`);
+ try {
+ return await this.update(sessionId, attributes, deps);
+ } catch (e) {
+ if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
+ try {
+ this.logger.debug(`Object not found | ${sessionId}`);
+ return await this.create(sessionId, attributes, deps);
+ } catch (createError) {
+ if (
+ SavedObjectsErrorHelpers.isConflictError(createError) &&
+ retry < this.config.maxUpdateRetries
+ ) {
+ this.logger.debug(`Conflict error | ${sessionId}`);
+ // TODO: Randomize sleep
+ await sleep(100);
+ return await this.updateOrCreate(sessionId, attributes, deps, retry + 1);
+ } else {
+ this.logger.error(createError);
+ }
+ }
+ } else if (
+ SavedObjectsErrorHelpers.isConflictError(e) &&
+ retry < this.config.maxUpdateRetries
+ ) {
+ this.logger.debug(`Conflict error | ${sessionId}`);
+ await sleep(100);
+ return await this.updateOrCreate(sessionId, attributes, deps, retry + 1);
+ } else {
+ this.logger.error(e);
+ }
+ }
+
+ return undefined;
+ };
+
public save = async (
sessionId: string,
{
name,
appId,
- created = new Date().toISOString(),
- expires = new Date(Date.now() + this.config.defaultExpiration.asMilliseconds()).toISOString(),
- status = SearchSessionStatus.IN_PROGRESS,
urlGeneratorId,
initialState = {},
restoreState = {},
}: Partial,
- { savedObjectsClient }: SearchSessionDependencies
+ deps: SearchSessionDependencies
) => {
if (!name) throw new Error('Name is required');
if (!appId) throw new Error('AppId is required');
if (!urlGeneratorId) throw new Error('UrlGeneratorId is required');
- this.logger.debug(`save | ${sessionId}`);
-
- const attributes = {
- name,
- created,
- expires,
- status,
- initialState,
- restoreState,
- idMapping: {},
- urlGeneratorId,
- appId,
+ return this.updateOrCreate(
sessionId,
- };
- const session = await savedObjectsClient.create(
+ {
+ name,
+ appId,
+ urlGeneratorId,
+ initialState,
+ restoreState,
+ persisted: true,
+ },
+ deps
+ );
+ };
+
+ private create = (
+ sessionId: string,
+ attributes: Partial,
+ { savedObjectsClient }: SearchSessionDependencies
+ ) => {
+ this.logger.debug(`create | ${sessionId}`);
+ return savedObjectsClient.create(
SEARCH_SESSION_TYPE,
- attributes,
+ {
+ sessionId,
+ status: SearchSessionStatus.IN_PROGRESS,
+ expires: new Date(
+ Date.now() + this.config.defaultExpiration.asMilliseconds()
+ ).toISOString(),
+ created: new Date().toISOString(),
+ touched: new Date().toISOString(),
+ idMapping: {},
+ persisted: false,
+ ...attributes,
+ },
{ id: sessionId }
);
-
- return session;
};
// TODO: Throw an error if this session doesn't belong to this user
@@ -325,7 +239,10 @@ export class SearchSessionService implements ISessionService {
return savedObjectsClient.update(
SEARCH_SESSION_TYPE,
sessionId,
- attributes
+ {
+ ...attributes,
+ touched: new Date().toISOString(),
+ }
);
};
@@ -342,7 +259,7 @@ export class SearchSessionService implements ISessionService {
public trackId = async (
searchRequest: IKibanaSearchRequest,
searchId: string,
- { sessionId, isStored, strategy }: ISearchOptions,
+ { sessionId, strategy }: ISearchOptions,
deps: SearchSessionDependencies
) => {
if (!sessionId || !searchId) return;
@@ -354,22 +271,9 @@ export class SearchSessionService implements ISessionService {
status: SearchStatus.IN_PROGRESS,
};
- // If there is already a saved object for this session, update it to include this request/ID.
- // Otherwise, just update the in-memory mapping for this session for when the session is saved.
- if (isStored) {
- const attributes = {
- idMapping: { [requestHash]: searchInfo },
- };
- await this.update(sessionId, attributes, deps);
- } else {
- const map = this.sessionSearchMap.get(sessionId) ?? {
- insertTime: moment(),
- retryCount: 0,
- ids: new Map(),
- };
- map.ids.set(requestHash, searchInfo);
- this.sessionSearchMap.set(sessionId, map);
- }
+ const idMapping = { [requestHash]: searchInfo };
+
+ return this.updateOrCreate(sessionId, { idMapping }, deps);
};
/**
diff --git a/x-pack/plugins/data_enhanced/server/search/session/types.ts b/x-pack/plugins/data_enhanced/server/search/session/types.ts
index c30e03f70d2dc..136c37942cb2e 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/types.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/types.ts
@@ -4,8 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { ConfigSchema } from '../../../config';
+
export enum SearchStatus {
IN_PROGRESS = 'in_progress',
ERROR = 'error',
COMPLETE = 'complete',
}
+
+export type SearchSessionsConfig = ConfigSchema['search']['sessions'];
From 2b3b90e454ce56fa7a11250dfc5a44d69c351cf1 Mon Sep 17 00:00:00 2001
From: Liza K
Date: Thu, 28 Jan 2021 16:38:33 +0200
Subject: [PATCH 02/85] Get default search session expiration from config
---
x-pack/plugins/data_enhanced/config.ts | 2 +-
x-pack/plugins/data_enhanced/server/plugin.ts | 7 ++++++-
.../server/search/es_search_strategy.ts | 14 ++++++++++----
.../data_enhanced/server/search/request_utils.ts | 4 +++-
.../search/session/check_running_sessions.ts | 2 +-
5 files changed, 21 insertions(+), 8 deletions(-)
diff --git a/x-pack/plugins/data_enhanced/config.ts b/x-pack/plugins/data_enhanced/config.ts
index ca737d461e63d..7207d7e5131f9 100644
--- a/x-pack/plugins/data_enhanced/config.ts
+++ b/x-pack/plugins/data_enhanced/config.ts
@@ -12,7 +12,7 @@ export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: false }),
pageSize: schema.number({ defaultValue: 10000 }),
trackingInterval: schema.duration({ defaultValue: '10s' }),
- onScreenTimeout: schema.duration({ defaultValue: '10m' }),
+ onScreenTimeout: schema.duration({ defaultValue: '5m' }),
notTouchedTimeout: schema.duration({ defaultValue: '1m' }),
maxUpdateRetries: schema.number({ defaultValue: 3 }),
defaultExpiration: schema.duration({ defaultValue: '7d' }),
diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts
index cff0ee3efd738..834f1669e2d7e 100644
--- a/x-pack/plugins/data_enhanced/server/plugin.ts
+++ b/x-pack/plugins/data_enhanced/server/plugin.ts
@@ -5,6 +5,7 @@
*/
import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server';
+import { Observable } from 'rxjs';
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
import {
PluginSetup as DataPluginSetup,
@@ -22,6 +23,7 @@ import {
} from './search';
import { getUiSettings } from './ui_settings';
import type { DataEnhancedRequestHandlerContext } from './type';
+import { ConfigSchema } from '../config';
interface SetupDependencies {
data: DataPluginSetup;
@@ -37,9 +39,11 @@ export class EnhancedDataServerPlugin
implements Plugin {
private readonly logger: Logger;
private sessionService!: SearchSessionService;
+ private config$: Observable;
- constructor(private initializerContext: PluginInitializerContext) {
+ constructor(private initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get('data_enhanced');
+ this.config$ = this.initializerContext.config.create();
}
public setup(core: CoreSetup, deps: SetupDependencies) {
@@ -51,6 +55,7 @@ export class EnhancedDataServerPlugin
deps.data.search.registerSearchStrategy(
ENHANCED_ES_SEARCH_STRATEGY,
enhancedEsSearchStrategyProvider(
+ this.config$,
this.initializerContext.config.legacy.globalConfig$,
this.logger,
usage
diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
index 54ed59b30952a..d02a028d803df 100644
--- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
+++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
@@ -34,9 +34,11 @@ import {
import { toAsyncKibanaSearchResponse } from './response_utils';
import { AsyncSearchResponse } from './types';
import { KbnServerError } from '../../../../../src/plugins/kibana_utils/server';
+import { ConfigSchema } from '../../config';
export const enhancedEsSearchStrategyProvider = (
- config$: Observable,
+ config$: Observable,
+ legacyConfig$: Observable,
logger: Logger,
usage?: SearchUsage
): ISearchStrategy => {
@@ -52,9 +54,13 @@ export const enhancedEsSearchStrategyProvider = (
const client = esClient.asCurrentUser.asyncSearch;
const search = async () => {
+ const config = await config$.pipe(first()).toPromise();
const params = id
? getDefaultAsyncGetParams()
- : { ...(await getDefaultAsyncSubmitParams(uiSettingsClient, options)), ...request.params };
+ : {
+ ...(await getDefaultAsyncSubmitParams(uiSettingsClient, config, options)),
+ ...request.params,
+ };
const promise = id
? client.get({ ...params, id })
: client.submit(params);
@@ -79,12 +85,12 @@ export const enhancedEsSearchStrategyProvider = (
options: ISearchOptions,
{ esClient, uiSettingsClient }: SearchStrategyDependencies
): Promise {
- const config = await config$.pipe(first()).toPromise();
+ const legacyConfig = await legacyConfig$.pipe(first()).toPromise();
const { body, index, ...params } = request.params!;
const method = 'POST';
const path = encodeURI(`/${index}/_rollup_search`);
const querystring = {
- ...getShardTimeout(config),
+ ...getShardTimeout(legacyConfig),
...(await getIgnoreThrottled(uiSettingsClient)),
...(await getDefaultSearchParams(uiSettingsClient)),
...params,
diff --git a/x-pack/plugins/data_enhanced/server/search/request_utils.ts b/x-pack/plugins/data_enhanced/server/search/request_utils.ts
index 12e9c1e35ed52..6f241bc863010 100644
--- a/x-pack/plugins/data_enhanced/server/search/request_utils.ts
+++ b/x-pack/plugins/data_enhanced/server/search/request_utils.ts
@@ -11,6 +11,7 @@ import {
} from '@elastic/elasticsearch/api/requestParams';
import { ISearchOptions, UI_SETTINGS } from '../../../../../src/plugins/data/common';
import { getDefaultSearchParams } from '../../../../../src/plugins/data/server';
+import { ConfigSchema } from '../../config';
/**
* @internal
@@ -27,6 +28,7 @@ export async function getIgnoreThrottled(
*/
export async function getDefaultAsyncSubmitParams(
uiSettingsClient: IUiSettingsClient,
+ config: ConfigSchema,
options: ISearchOptions
): Promise<
Pick<
@@ -42,7 +44,7 @@ export async function getDefaultAsyncSubmitParams(
>
> {
return {
- keep_alive: '7d',
+ keep_alive: `${config.search.sessions.defaultExpiration.asMilliseconds()}ms`,
batched_reduce_size: 64,
keep_on_completion: !!options.sessionId, // Always return an ID, even if the request completes quickly
...getDefaultAsyncGetParams(),
diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
index 3aba7af7b153e..2f59deffc313d 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
@@ -69,7 +69,7 @@ async function updateSessionStatus(
const currentStatus = await getSearchStatus(client, searchInfo.id);
if (currentStatus.status !== searchInfo.status) {
- logger.warn(`${currentStatus.status}`);
+ logger.debug(`search ${searchInfo.id} | status changed to ${currentStatus.status}`);
updateSearchRequest(currentStatus);
}
} catch (e) {
From cd23cef302c25ea1306a0509e3b8359a549bc1aa Mon Sep 17 00:00:00 2001
From: Liza K
Date: Thu, 28 Jan 2021 16:55:26 +0200
Subject: [PATCH 03/85] randomize sleep time
---
.../server/search/session/session_service.ts | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
index 1c5a857a630f6..33a557393e268 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
@@ -120,6 +120,13 @@ export class SearchSessionService implements ISessionService {
| SavedObject
| undefined
> => {
+ const retryOnConflict = async (e: any) => {
+ this.logger.debug(`Conflict error | ${sessionId}`);
+ // Randomize sleep to spread updates out in case of conflicts
+ await sleep(100 + Math.random() * 50);
+ return await this.updateOrCreate(sessionId, attributes, deps, retry + 1);
+ };
+
this.logger.debug(`updateOrCreate | ${sessionId} | ${retry}`);
try {
return await this.update(sessionId, attributes, deps);
@@ -133,10 +140,7 @@ export class SearchSessionService implements ISessionService {
SavedObjectsErrorHelpers.isConflictError(createError) &&
retry < this.config.maxUpdateRetries
) {
- this.logger.debug(`Conflict error | ${sessionId}`);
- // TODO: Randomize sleep
- await sleep(100);
- return await this.updateOrCreate(sessionId, attributes, deps, retry + 1);
+ return await retryOnConflict(createError);
} else {
this.logger.error(createError);
}
@@ -145,9 +149,7 @@ export class SearchSessionService implements ISessionService {
SavedObjectsErrorHelpers.isConflictError(e) &&
retry < this.config.maxUpdateRetries
) {
- this.logger.debug(`Conflict error | ${sessionId}`);
- await sleep(100);
- return await this.updateOrCreate(sessionId, attributes, deps, retry + 1);
+ return await retryOnConflict(e);
} else {
this.logger.error(e);
}
From 5f21181afd8cf62ff98e91b773b27c45017bf99a Mon Sep 17 00:00:00 2001
From: Liza K
Date: Thu, 28 Jan 2021 17:29:50 +0200
Subject: [PATCH 04/85] fix test
---
.../server/search/es_search_strategy.test.ts | 55 ++++++++++++++++---
1 file changed, 47 insertions(+), 8 deletions(-)
diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
index 3230895da7705..483cc61cbaa3e 100644
--- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
+++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
@@ -7,6 +7,7 @@
import { enhancedEsSearchStrategyProvider } from './es_search_strategy';
import { BehaviorSubject } from 'rxjs';
import { SearchStrategyDependencies } from '../../../../../src/plugins/data/server/search';
+import moment from 'moment';
const mockAsyncResponse = {
body: {
@@ -56,7 +57,7 @@ describe('ES search strategy', () => {
},
},
} as unknown) as SearchStrategyDependencies;
- const mockConfig$ = new BehaviorSubject({
+ const mockLegacyConfig$ = new BehaviorSubject({
elasticsearch: {
shardTimeout: {
asMilliseconds: () => {
@@ -66,6 +67,16 @@ describe('ES search strategy', () => {
},
});
+ const mockConfig$ = new BehaviorSubject({
+ search: {
+ sessions: {
+ defaultExpiration: () => {
+ return moment.duration('1', 'm');
+ },
+ },
+ },
+ });
+
beforeEach(() => {
mockApiCaller.mockClear();
mockGetCaller.mockClear();
@@ -74,7 +85,11 @@ describe('ES search strategy', () => {
});
it('returns a strategy with `search and `cancel`', async () => {
- const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger);
+ const esSearch = await enhancedEsSearchStrategyProvider(
+ mockConfig$,
+ mockLegacyConfig$,
+ mockLogger
+ );
expect(typeof esSearch.search).toBe('function');
});
@@ -84,7 +99,11 @@ describe('ES search strategy', () => {
mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse);
const params = { index: 'logstash-*', body: { query: {} } };
- const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger);
+ const esSearch = await enhancedEsSearchStrategyProvider(
+ mockConfig$,
+ mockLegacyConfig$,
+ mockLogger
+ );
await esSearch.search({ params }, {}, mockDeps).toPromise();
@@ -98,7 +117,11 @@ describe('ES search strategy', () => {
mockGetCaller.mockResolvedValueOnce(mockAsyncResponse);
const params = { index: 'logstash-*', body: { query: {} } };
- const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger);
+ const esSearch = await enhancedEsSearchStrategyProvider(
+ mockConfig$,
+ mockLegacyConfig$,
+ mockLogger
+ );
await esSearch.search({ id: 'foo', params }, {}, mockDeps).toPromise();
@@ -113,7 +136,11 @@ describe('ES search strategy', () => {
mockApiCaller.mockResolvedValueOnce(mockRollupResponse);
const params = { index: 'foo-程', body: {} };
- const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger);
+ const esSearch = await enhancedEsSearchStrategyProvider(
+ mockConfig$,
+ mockLegacyConfig$,
+ mockLogger
+ );
await esSearch
.search(
@@ -136,7 +163,11 @@ describe('ES search strategy', () => {
mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse);
const params = { index: 'foo-*', body: {} };
- const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger);
+ const esSearch = await enhancedEsSearchStrategyProvider(
+ mockConfig$,
+ mockLegacyConfig$,
+ mockLogger
+ );
await esSearch.search({ params }, {}, mockDeps).toPromise();
@@ -152,7 +183,11 @@ describe('ES search strategy', () => {
mockDeleteCaller.mockResolvedValueOnce(200);
const id = 'some_id';
- const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger);
+ const esSearch = await enhancedEsSearchStrategyProvider(
+ mockConfig$,
+ mockLegacyConfig$,
+ mockLogger
+ );
await esSearch.cancel!(id, {}, mockDeps);
@@ -168,7 +203,11 @@ describe('ES search strategy', () => {
const id = 'some_other_id';
const keepAlive = '1d';
- const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger);
+ const esSearch = await enhancedEsSearchStrategyProvider(
+ mockConfig$,
+ mockLegacyConfig$,
+ mockLogger
+ );
await esSearch.extend!(id, keepAlive, {}, mockDeps);
From cdcf56276d6503cbe8b0355a91250537f4b9bdf5 Mon Sep 17 00:00:00 2001
From: Liza K
Date: Thu, 28 Jan 2021 18:42:42 +0200
Subject: [PATCH 05/85] fix test
---
.../data_enhanced/server/search/es_search_strategy.test.ts | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
index 483cc61cbaa3e..56baea0f1e23d 100644
--- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
+++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
@@ -70,9 +70,7 @@ describe('ES search strategy', () => {
const mockConfig$ = new BehaviorSubject({
search: {
sessions: {
- defaultExpiration: () => {
- return moment.duration('1', 'm');
- },
+ defaultExpiration: moment.duration('1', 'm'),
},
},
});
@@ -129,7 +127,6 @@ describe('ES search strategy', () => {
const request = mockGetCaller.mock.calls[0][0];
expect(request.id).toEqual('foo');
expect(request).toHaveProperty('wait_for_completion_timeout');
- expect(request).toHaveProperty('keep_alive');
});
it('calls the rollup API if the index is a rollup type', async () => {
From 667c9d3c5dd3e6168673bdfd081c1cad99c29b7a Mon Sep 17 00:00:00 2001
From: Liza K
Date: Thu, 28 Jan 2021 20:42:10 +0200
Subject: [PATCH 06/85] Make sure we poll, and dont persist, searches not in
the context of a session
---
.../server/search/es_search_strategy.ts | 2 +-
.../server/search/request_utils.ts | 22 ++++++++++++++-----
2 files changed, 17 insertions(+), 7 deletions(-)
diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
index d02a028d803df..e393ed84a271b 100644
--- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
+++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
@@ -56,7 +56,7 @@ export const enhancedEsSearchStrategyProvider = (
const search = async () => {
const config = await config$.pipe(first()).toPromise();
const params = id
- ? getDefaultAsyncGetParams()
+ ? getDefaultAsyncGetParams(options)
: {
...(await getDefaultAsyncSubmitParams(uiSettingsClient, config, options)),
...request.params,
diff --git a/x-pack/plugins/data_enhanced/server/search/request_utils.ts b/x-pack/plugins/data_enhanced/server/search/request_utils.ts
index 6f241bc863010..da742f84629dc 100644
--- a/x-pack/plugins/data_enhanced/server/search/request_utils.ts
+++ b/x-pack/plugins/data_enhanced/server/search/request_utils.ts
@@ -33,6 +33,7 @@ export async function getDefaultAsyncSubmitParams(
): Promise<
Pick<
AsyncSearchSubmit,
+ | 'keep_alive'
| 'batched_reduce_size'
| 'keep_alive'
| 'wait_for_completion_timeout'
@@ -44,23 +45,32 @@ export async function getDefaultAsyncSubmitParams(
>
> {
return {
- keep_alive: `${config.search.sessions.defaultExpiration.asMilliseconds()}ms`,
batched_reduce_size: 64,
keep_on_completion: !!options.sessionId, // Always return an ID, even if the request completes quickly
- ...getDefaultAsyncGetParams(),
+ ...getDefaultAsyncGetParams(options),
...(await getIgnoreThrottled(uiSettingsClient)),
...(await getDefaultSearchParams(uiSettingsClient)),
+ ...(options.sessionId
+ ? {
+ keep_alive: `${config.search.sessions.defaultExpiration.asMilliseconds()}ms`,
+ }
+ : {}),
};
}
/**
@internal
*/
-export function getDefaultAsyncGetParams(): Pick<
- AsyncSearchGet,
- 'keep_alive' | 'wait_for_completion_timeout'
-> {
+export function getDefaultAsyncGetParams(
+ options: ISearchOptions
+): Pick {
return {
wait_for_completion_timeout: '100ms', // Wait up to 100ms for the response to return
+ ...(options.sessionId
+ ? undefined
+ : {
+ keep_alive: '1m',
+ // We still need to do polling for searches not within the context of a search session
+ }),
};
}
From 763746b208a237457aacc9fff88f51b06d698beb Mon Sep 17 00:00:00 2001
From: Liza K
Date: Thu, 28 Jan 2021 22:38:24 +0200
Subject: [PATCH 07/85] Added keepalive unit tests
---
.../server/search/es_search_strategy.test.ts | 43 ++++++++++++++++++-
1 file changed, 42 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
index 56baea0f1e23d..f3e8189ca97a4 100644
--- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
+++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
@@ -93,7 +93,7 @@ describe('ES search strategy', () => {
});
describe('search', () => {
- it('makes a POST request to async search with params when no ID is provided', async () => {
+ it('makes a POST request with params when no ID is provided', async () => {
mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse);
const params = { index: 'logstash-*', body: { query: {} } };
@@ -109,6 +109,27 @@ describe('ES search strategy', () => {
const request = mockSubmitCaller.mock.calls[0][0];
expect(request.index).toEqual(params.index);
expect(request.body).toEqual(params.body);
+ expect(request).toHaveProperty('keep_alive', '1m');
+ });
+
+ it('makes a POST request with params when sessionId is provided, with long keepalive', async () => {
+ mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse);
+
+ const params = { index: 'logstash-*', body: { query: {} } };
+ const esSearch = await enhancedEsSearchStrategyProvider(
+ mockConfig$,
+ mockLegacyConfig$,
+ mockLogger
+ );
+
+ await esSearch.search({ params }, { sessionId: '1' }, mockDeps).toPromise();
+
+ expect(mockSubmitCaller).toBeCalled();
+ const request = mockSubmitCaller.mock.calls[0][0];
+ expect(request.index).toEqual(params.index);
+ expect(request.body).toEqual(params.body);
+
+ expect(request).toHaveProperty('keep_alive', '60000ms');
});
it('makes a GET request to async search with ID when ID is provided', async () => {
@@ -127,6 +148,26 @@ describe('ES search strategy', () => {
const request = mockGetCaller.mock.calls[0][0];
expect(request.id).toEqual('foo');
expect(request).toHaveProperty('wait_for_completion_timeout');
+ expect(request).toHaveProperty('keep_alive', '1m');
+ });
+
+ it('makes a GET request to async search without keepalive when session ID is provided', async () => {
+ mockGetCaller.mockResolvedValueOnce(mockAsyncResponse);
+
+ const params = { index: 'logstash-*', body: { query: {} } };
+ const esSearch = await enhancedEsSearchStrategyProvider(
+ mockConfig$,
+ mockLegacyConfig$,
+ mockLogger
+ );
+
+ await esSearch.search({ id: 'foo', params }, { sessionId: '1' }, mockDeps).toPromise();
+
+ expect(mockGetCaller).toBeCalled();
+ const request = mockGetCaller.mock.calls[0][0];
+ expect(request.id).toEqual('foo');
+ expect(request).toHaveProperty('wait_for_completion_timeout');
+ expect(request).not.toHaveProperty('keep_alive');
});
it('calls the rollup API if the index is a rollup type', async () => {
From 01f292deaadbecf3d7b3047a68360534b39067a7 Mon Sep 17 00:00:00 2001
From: Anton Dosov
Date: Fri, 29 Jan 2021 10:06:51 +0100
Subject: [PATCH 08/85] fix ts
---
.../data_enhanced/server/search/eql_search_strategy.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts
index a0d4e9dcd19b9..f86cb1a0a349f 100644
--- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts
+++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts
@@ -41,11 +41,11 @@ export const eqlSearchStrategyProvider = (
uiSettingsClient
);
const params = id
- ? getDefaultAsyncGetParams()
+ ? getDefaultAsyncGetParams(options)
: {
...(await getIgnoreThrottled(uiSettingsClient)),
...defaultParams,
- ...getDefaultAsyncGetParams(),
+ ...getDefaultAsyncGetParams(options),
...request.params,
};
const promise = id
From 2d75b963bc3a4b33c34400e6470fb7a6f2ad250e Mon Sep 17 00:00:00 2001
From: Liza K
Date: Sun, 31 Jan 2021 12:46:08 +0200
Subject: [PATCH 09/85] code review @lukasolson
---
.../data_enhanced/public/search/sessions_mgmt/lib/api.ts | 5 +++--
x-pack/plugins/data_enhanced/server/routes/session.ts | 6 +++++-
x-pack/plugins/data_enhanced/server/search/request_utils.ts | 1 -
.../server/search/session/check_running_sessions.ts | 4 ++--
4 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts
index a1bbda5dc7cb3..6f1836ef98b11 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts
@@ -10,7 +10,7 @@ import moment from 'moment';
import { from, race, timer } from 'rxjs';
import { mapTo, tap } from 'rxjs/operators';
import type { SharePluginStart } from 'src/plugins/share/public';
-import type { ISessionsClient } from '../../../../../../../src/plugins/data/public';
+import { esKuery, ISessionsClient } from '../../../../../../../src/plugins/data/public';
import { nodeBuilder } from '../../../../../../../src/plugins/data/common';
import { SearchSessionStatus, SEARCH_SESSION_TYPE } from '../../../../common/search';
import { ACTION } from '../components/actions';
@@ -110,7 +110,8 @@ export class SearchSessionsMgmtAPI {
perPage: mgmtConfig.maxSessions,
sortField: 'created',
sortOrder: 'asc',
- filter: nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'true'),
+ searchFields: ['persisted'],
+ search: 'true',
})
);
const timeout$ = timer(refreshTimeout.asMilliseconds()).pipe(
diff --git a/x-pack/plugins/data_enhanced/server/routes/session.ts b/x-pack/plugins/data_enhanced/server/routes/session.ts
index cbf683bd18fd2..1b9f65aab728d 100644
--- a/x-pack/plugins/data_enhanced/server/routes/session.ts
+++ b/x-pack/plugins/data_enhanced/server/routes/session.ts
@@ -91,11 +91,13 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger:
sortField: schema.maybe(schema.string()),
sortOrder: schema.maybe(schema.string()),
filter: schema.maybe(schema.string()),
+ searchFields: schema.arrayOf(schema.string()),
+ search: schema.string(),
}),
},
},
async (context, request, res) => {
- const { page, perPage, sortField, sortOrder, filter } = request.body;
+ const { page, perPage, sortField, sortOrder, filter, searchFields, search } = request.body;
try {
const response = await context.search!.session.find({
page,
@@ -103,6 +105,8 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger:
sortField,
sortOrder,
filter,
+ searchFields,
+ search,
});
return res.ok({
diff --git a/x-pack/plugins/data_enhanced/server/search/request_utils.ts b/x-pack/plugins/data_enhanced/server/search/request_utils.ts
index da742f84629dc..d9ef3ab3292c3 100644
--- a/x-pack/plugins/data_enhanced/server/search/request_utils.ts
+++ b/x-pack/plugins/data_enhanced/server/search/request_utils.ts
@@ -33,7 +33,6 @@ export async function getDefaultAsyncSubmitParams(
): Promise<
Pick<
AsyncSearchSubmit,
- | 'keep_alive'
| 'batched_reduce_size'
| 'keep_alive'
| 'wait_for_completion_timeout'
diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
index 2f59deffc313d..07249fbba3336 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
@@ -28,7 +28,7 @@ export interface CheckRunningSessionsDeps {
logger: Logger;
}
-function checkNotPersistedSession(
+function isSessionStale(
session: SavedObjectsFindResult,
config: SearchSessionsConfig,
logger: Logger
@@ -129,7 +129,7 @@ export async function checkRunningSessions(
let deleted = false;
if (!session.attributes.persisted) {
- if (checkNotPersistedSession(session, config, logger)) {
+ if (isSessionStale(session, config, logger)) {
deleted = true;
// delete saved object to free up memory
// TODO: there's a potential rare edge case of deleting an object and then receiving a new trackId for that same session!
From 1cd21fd4a100594cdcfe915bfb6505fd1c750d15 Mon Sep 17 00:00:00 2001
From: Liza K
Date: Sun, 31 Jan 2021 14:03:56 +0200
Subject: [PATCH 10/85] ts
---
.../data_enhanced/public/search/sessions_mgmt/lib/api.ts | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts
index 6f1836ef98b11..25c06d1d2e278 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts
@@ -10,9 +10,8 @@ import moment from 'moment';
import { from, race, timer } from 'rxjs';
import { mapTo, tap } from 'rxjs/operators';
import type { SharePluginStart } from 'src/plugins/share/public';
-import { esKuery, ISessionsClient } from '../../../../../../../src/plugins/data/public';
-import { nodeBuilder } from '../../../../../../../src/plugins/data/common';
-import { SearchSessionStatus, SEARCH_SESSION_TYPE } from '../../../../common/search';
+import { ISessionsClient } from '../../../../../../../src/plugins/data/public';
+import { SearchSessionStatus } from '../../../../common/search';
import { ACTION } from '../components/actions';
import { PersistedSearchSessionSavedObjectAttributes, UISession } from '../types';
import { SessionsConfigSchema } from '..';
From a9cc263ec825d0b6cdf9453cbca95d83182d7346 Mon Sep 17 00:00:00 2001
From: Liza K
Date: Sun, 31 Jan 2021 18:10:34 +0200
Subject: [PATCH 11/85] More tests, rename onScreenTimeout to completedTimeout
---
x-pack/plugins/data_enhanced/config.ts | 2 +-
.../data_enhanced/server/routes/session.ts | 4 +-
.../session/check_running_sessions.test.ts | 2 +-
.../search/session/check_running_sessions.ts | 5 +-
.../search/session/session_service.test.ts | 2 +-
.../server/search/session/session_service.ts | 19 +-
.../api_integration/apis/search/session.ts | 177 +++++++++++++++++-
x-pack/test/api_integration/config.ts | 2 +
.../async_search/send_to_background.ts | 1 +
.../send_to_background_relative_time.ts | 31 +--
10 files changed, 202 insertions(+), 43 deletions(-)
diff --git a/x-pack/plugins/data_enhanced/config.ts b/x-pack/plugins/data_enhanced/config.ts
index 7207d7e5131f9..27413b64e474b 100644
--- a/x-pack/plugins/data_enhanced/config.ts
+++ b/x-pack/plugins/data_enhanced/config.ts
@@ -12,7 +12,7 @@ export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: false }),
pageSize: schema.number({ defaultValue: 10000 }),
trackingInterval: schema.duration({ defaultValue: '10s' }),
- onScreenTimeout: schema.duration({ defaultValue: '5m' }),
+ completedTimeout: schema.duration({ defaultValue: '5m' }),
notTouchedTimeout: schema.duration({ defaultValue: '1m' }),
maxUpdateRetries: schema.number({ defaultValue: 3 }),
defaultExpiration: schema.duration({ defaultValue: '7d' }),
diff --git a/x-pack/plugins/data_enhanced/server/routes/session.ts b/x-pack/plugins/data_enhanced/server/routes/session.ts
index 1b9f65aab728d..622d4d68413ca 100644
--- a/x-pack/plugins/data_enhanced/server/routes/session.ts
+++ b/x-pack/plugins/data_enhanced/server/routes/session.ts
@@ -91,8 +91,8 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger:
sortField: schema.maybe(schema.string()),
sortOrder: schema.maybe(schema.string()),
filter: schema.maybe(schema.string()),
- searchFields: schema.arrayOf(schema.string()),
- search: schema.string(),
+ searchFields: schema.maybe(schema.arrayOf(schema.string())),
+ search: schema.maybe(schema.string()),
}),
},
},
diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts
index 917663252bc8b..ac00668ca7b69 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts
@@ -18,7 +18,7 @@ describe('getSearchStatus', () => {
enabled: true,
pageSize: 10000,
notTouchedTimeout: moment.duration(1, 'm'),
- onScreenTimeout: moment.duration(5, 'm'),
+ completedTimeout: moment.duration(5, 'm'),
maxUpdateRetries: 3,
defaultExpiration: moment.duration(7, 'd'),
trackingInterval: moment.duration(10, 's'),
diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
index 07249fbba3336..4d5cd0a3231b7 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
@@ -34,12 +34,13 @@ function isSessionStale(
logger: Logger
) {
const curTime = moment();
- // Delete if a running session wasn't polled for in the last notTouchedTimeout OR if a completed session wasn't saved for within onScreenTimeout
+ // Delete if a running session wasn't polled for in the last notTouchedTimeout OR if a completed session wasn't saved for within onScreenTicompletedTimeoutmeout
return (
(session.attributes.status === SearchSessionStatus.IN_PROGRESS &&
curTime.diff(moment(session.attributes.touched), 'ms') >
config.notTouchedTimeout.asMilliseconds()) ||
- curTime.diff(moment(session.attributes.created), 'ms') > config.onScreenTimeout.asMilliseconds()
+ curTime.diff(moment(session.attributes.created), 'ms') >
+ config.completedTimeout.asMilliseconds()
);
}
diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts
index 10549b18f521a..8671c5c42a54c 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts
@@ -52,7 +52,7 @@ describe('SearchSessionService', () => {
enabled: true,
pageSize: 10000,
notTouchedTimeout: moment.duration(1, 'm'),
- onScreenTimeout: moment.duration(2, 'm'),
+ completedTimeout: moment.duration(2, 'm'),
maxUpdateRetries: MAX_UPDATE_RETRIES,
defaultExpiration: moment.duration(7, 'd'),
trackingInterval: moment.duration(10, 's'),
diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
index 33a557393e268..e6c2b60e4a79d 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
@@ -32,6 +32,7 @@ import {
TaskManagerStartContract,
} from '../../../../task_manager/server';
import {
+ SearchSessionRequestInfo,
SearchSessionSavedObjectAttributes,
SearchSessionStatus,
SEARCH_SESSION_TYPE,
@@ -266,14 +267,18 @@ export class SearchSessionService implements ISessionService {
) => {
if (!sessionId || !searchId) return;
this.logger.debug(`trackId | ${sessionId} | ${searchId}`);
- const requestHash = createRequestHash(searchRequest.params);
- const searchInfo = {
- id: searchId,
- strategy: strategy!,
- status: SearchStatus.IN_PROGRESS,
- };
- const idMapping = { [requestHash]: searchInfo };
+ let idMapping: Record = {};
+
+ if (searchRequest.params) {
+ const requestHash = createRequestHash(searchRequest.params);
+ const searchInfo = {
+ id: searchId,
+ strategy: strategy!,
+ status: SearchStatus.IN_PROGRESS,
+ };
+ idMapping = { [requestHash]: searchInfo };
+ }
return this.updateOrCreate(sessionId, { idMapping }, deps);
};
diff --git a/x-pack/test/api_integration/apis/search/session.ts b/x-pack/test/api_integration/apis/search/session.ts
index ee2e4337adc95..fed8b649becc6 100644
--- a/x-pack/test/api_integration/apis/search/session.ts
+++ b/x-pack/test/api_integration/apis/search/session.ts
@@ -12,6 +12,20 @@ export default function ({ getService }: FtrProviderContext) {
describe('search session', () => {
describe('session management', () => {
+ it('should fail to create a session with no name', async () => {
+ const sessionId = `my-session-${Math.random()}`;
+ await supertest
+ .post(`/internal/session`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ sessionId,
+ appId: 'discover',
+ expires: '123',
+ urlGeneratorId: 'discover',
+ })
+ .expect(400);
+ });
+
it('should create and get a session', async () => {
const sessionId = `my-session-${Math.random()}`;
await supertest
@@ -52,7 +66,7 @@ export default function ({ getService }: FtrProviderContext) {
await supertest.get(`/internal/session/${sessionId}`).set('kbn-xsrf', 'foo').expect(404);
});
- it('should sync search ids into session', async () => {
+ it('should sync search ids into persisted session', async () => {
const sessionId = `my-session-${Math.random()}`;
// run search
@@ -76,7 +90,7 @@ export default function ({ getService }: FtrProviderContext) {
const { id: id1 } = searchRes1.body;
- // create session
+ // persist session
await supertest
.post(`/internal/session`)
.set('kbn-xsrf', 'foo')
@@ -108,21 +122,170 @@ export default function ({ getService }: FtrProviderContext) {
const { id: id2 } = searchRes2.body;
- // wait 10 seconds for ids to be synced
- // TODO: make the refresh interval dynamic, so we can speed it up!
- await new Promise((resolve) => setTimeout(resolve, 10000));
-
const resp = await supertest
.get(`/internal/session/${sessionId}`)
.set('kbn-xsrf', 'foo')
.expect(200);
- const { idMapping } = resp.body.attributes;
+ const { name, touched, created, persisted, idMapping } = resp.body.attributes;
+ expect(persisted).to.be(true);
+ expect(name).to.be('My Session');
+ expect(touched).not.to.be(undefined);
+ expect(created).not.to.be(undefined);
const idMappings = Object.values(idMapping).map((value: any) => value.id);
expect(idMappings).to.contain(id1);
expect(idMappings).to.contain(id2);
});
});
+
+ it('should sync search ids into not persisted session', async () => {
+ const sessionId = `my-session-${Math.random()}`;
+
+ // run search
+ const searchRes1 = await supertest
+ .post(`/internal/search/ese`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ sessionId,
+ params: {
+ body: {
+ query: {
+ term: {
+ agent: '1',
+ },
+ },
+ },
+ wait_for_completion_timeout: '1ms',
+ },
+ })
+ .expect(200);
+
+ const { id: id1 } = searchRes1.body;
+
+ // run search
+ const searchRes2 = await supertest
+ .post(`/internal/search/ese`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ sessionId,
+ params: {
+ body: {
+ query: {
+ match_all: {},
+ },
+ },
+ wait_for_completion_timeout: '1ms',
+ },
+ })
+ .expect(200);
+
+ const { id: id2 } = searchRes2.body;
+
+ const resp = await supertest
+ .get(`/internal/session/${sessionId}`)
+ .set('kbn-xsrf', 'foo')
+ .expect(200);
+
+ const { appId, name, touched, created, persisted, idMapping } = resp.body.attributes;
+ expect(persisted).to.be(false);
+ expect(name).to.be(undefined);
+ expect(appId).to.be(undefined);
+ expect(touched).not.to.be(undefined);
+ expect(created).not.to.be(undefined);
+
+ const idMappings = Object.values(idMapping).map((value: any) => value.id);
+ expect(idMappings).to.contain(id1);
+ expect(idMappings).to.contain(id2);
+ });
+
+ it('touched time updates when you poll on an search', async () => {
+ const sessionId = `my-session-${Math.random()}`;
+
+ // run search
+ const searchRes1 = await supertest
+ .post(`/internal/search/ese`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ sessionId,
+ params: {
+ body: {
+ query: {
+ term: {
+ agent: '1',
+ },
+ },
+ },
+ wait_for_completion_timeout: '1ms',
+ },
+ })
+ .expect(200);
+
+ const { id: id1 } = searchRes1.body;
+
+ // it might take the session a moment to be created
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ const getSessionFirstTime = await supertest
+ .get(`/internal/session/${sessionId}`)
+ .set('kbn-xsrf', 'foo')
+ .expect(200);
+
+ // poll on search
+ await supertest
+ .post(`/internal/search/ese/${id1}`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ sessionId,
+ })
+ .expect(200);
+
+ const getSessionSecondTime = await supertest
+ .get(`/internal/session/${sessionId}`)
+ .set('kbn-xsrf', 'foo')
+ .expect(200);
+
+ expect(getSessionFirstTime.body.attributes.sessionId).to.be.equal(
+ getSessionSecondTime.body.attributes.sessionId
+ );
+ expect(getSessionFirstTime.body.attributes.touched).to.be.lessThan(
+ getSessionSecondTime.body.attributes.touched
+ );
+ });
+
+ it('session is cleared by monitoring task if notTouchedTimeout expires', async () => {
+ const sessionId = `my-session-${Math.random()}`;
+
+ // run search
+ const searchRes1 = await supertest
+ .post(`/internal/search/ese`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ sessionId,
+ params: {
+ body: {
+ query: {
+ term: {
+ agent: '1',
+ },
+ },
+ },
+ wait_for_completion_timeout: '1ms',
+ },
+ })
+ .expect(200);
+
+ const { id: id1 } = searchRes1.body;
+
+ // it might take the session a moment to be created
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ await supertest.get(`/internal/session/${sessionId}`).set('kbn-xsrf', 'foo').expect(200);
+
+ // wait for session to expire due to completedTimeout (as if user stayed on screen and didn't save the search session)
+ await new Promise((resolve) => setTimeout(resolve, 20000));
+
+ await supertest.get(`/internal/session/${sessionId}`).set('kbn-xsrf', 'foo').expect(404);
+ });
});
}
diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts
index 546b23ab4f26c..03d0634aada69 100644
--- a/x-pack/test/api_integration/config.ts
+++ b/x-pack/test/api_integration/config.ts
@@ -31,6 +31,8 @@ export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProvi
'--xpack.fleet.enabled=true',
'--xpack.fleet.agents.pollingRequestTimeout=5000', // 5 seconds
'--xpack.data_enhanced.search.sessions.enabled=true', // enable WIP send to background UI
+ '--xpack.data_enhanced.search.sessions.completedTimeout=15s', // shorten completedTimeout for quicker testing
+ '--xpack.data_enhanced.search.sessions.trackingInterval=2s', // shorten trackingInterval for quicker testing
],
},
esTestCluster: {
diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts
index 7e878e763bfc1..6a4cfba513539 100644
--- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts
+++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts
@@ -80,6 +80,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
// load URL to restore a saved session
+ // TODO: replace with clicking on "Re-run link"
const url = await browser.getCurrentUrl();
const savedSessionURL = `${url}&searchSessionId=${savedSessionId}`;
await browser.get(savedSessionURL);
diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts
index 25291fd74b322..5d5cdb29523bd 100644
--- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts
+++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts
@@ -19,13 +19,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'home',
'timePicker',
'maps',
+ 'searchSessionsManagement',
]);
const dashboardPanelActions = getService('dashboardPanelActions');
const inspector = getService('inspector');
const pieChart = getService('pieChart');
const find = getService('find');
const dashboardExpect = getService('dashboardExpect');
- const browser = getService('browser');
const searchSessions = getService('searchSessions');
describe('send to background with relative time', () => {
@@ -59,23 +59,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.timePicker.pauseAutoRefresh(); // sample data has auto-refresh on
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.dashboard.waitForRenderComplete();
- await checkSampleDashboardLoaded();
await searchSessions.expectState('completed');
await searchSessions.save();
await searchSessions.expectState('backgroundCompleted');
- const savedSessionId = await dashboardPanelActions.getSearchSessionIdByTitle(
- '[Flights] Airline Carrier'
- );
- const resolvedTimeRange = await getResolvedTimeRangeFromPanel('[Flights] Airline Carrier');
+
+ await checkSampleDashboardLoaded();
// load URL to restore a saved session
- const url = await browser.getCurrentUrl();
- const savedSessionURL = `${url}&searchSessionId=${savedSessionId}`
- .replace('now-24h', `'${resolvedTimeRange.gte}'`)
- .replace('now', `'${resolvedTimeRange.lte}'`);
- log.debug('Trying to restore session by URL:', savedSessionId);
- await browser.get(savedSessionURL);
+ await PageObjects.searchSessionsManagement.goTo();
+ const searchSessionList = await PageObjects.searchSessionsManagement.getList();
+
+ // navigate to dashboard
+ await searchSessionList[0].view();
+
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.dashboard.waitForRenderComplete();
await checkSampleDashboardLoaded();
@@ -87,16 +84,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// HELPERS
- async function getResolvedTimeRangeFromPanel(
- panelTitle: string
- ): Promise<{ gte: string; lte: string }> {
- await dashboardPanelActions.openInspectorByTitle(panelTitle);
- await inspector.openInspectorRequestsView();
- await (await inspector.getOpenRequestDetailRequestButton()).click();
- const request = JSON.parse(await inspector.getCodeEditorValue());
- return request.query.bool.filter.find((f: any) => f.range).range.timestamp;
- }
-
async function checkSampleDashboardLoaded() {
log.debug('Checking no error labels');
await testSubjects.missingOrFail('embeddableErrorLabel');
From bfcbdb23c79ffa97f10f2145562a52fb0e752b33 Mon Sep 17 00:00:00 2001
From: Liza K
Date: Sun, 31 Jan 2021 18:17:56 +0200
Subject: [PATCH 12/85] lint
---
x-pack/test/api_integration/apis/search/session.ts | 2 --
1 file changed, 2 deletions(-)
diff --git a/x-pack/test/api_integration/apis/search/session.ts b/x-pack/test/api_integration/apis/search/session.ts
index fed8b649becc6..d5649bacb1cb0 100644
--- a/x-pack/test/api_integration/apis/search/session.ts
+++ b/x-pack/test/api_integration/apis/search/session.ts
@@ -275,8 +275,6 @@ export default function ({ getService }: FtrProviderContext) {
})
.expect(200);
- const { id: id1 } = searchRes1.body;
-
// it might take the session a moment to be created
await new Promise((resolve) => setTimeout(resolve, 1000));
From 9b914f6c22a2f7338766efd61c91a3584576a7a0 Mon Sep 17 00:00:00 2001
From: Liza K
Date: Sun, 31 Jan 2021 18:20:06 +0200
Subject: [PATCH 13/85] lint
---
x-pack/test/api_integration/apis/search/session.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/test/api_integration/apis/search/session.ts b/x-pack/test/api_integration/apis/search/session.ts
index d5649bacb1cb0..691382222597a 100644
--- a/x-pack/test/api_integration/apis/search/session.ts
+++ b/x-pack/test/api_integration/apis/search/session.ts
@@ -257,7 +257,7 @@ export default function ({ getService }: FtrProviderContext) {
const sessionId = `my-session-${Math.random()}`;
// run search
- const searchRes1 = await supertest
+ await supertest
.post(`/internal/search/ese`)
.set('kbn-xsrf', 'foo')
.send({
From 7ab767d93cc16ac2ad70693e1aea6960d709a2b2 Mon Sep 17 00:00:00 2001
From: Liza K
Date: Sun, 31 Jan 2021 19:56:34 +0200
Subject: [PATCH 14/85] Delete async seaches
---
.../server/search/eql_search_strategy.ts | 3 +-
.../session/check_running_sessions.test.ts | 85 ++++++++++++++++++-
.../search/session/check_running_sessions.ts | 15 +++-
.../server/search/session/session_service.ts | 3 +-
4 files changed, 99 insertions(+), 7 deletions(-)
diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts
index f86cb1a0a349f..65ce5bdf5255c 100644
--- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts
+++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts
@@ -22,7 +22,8 @@ export const eqlSearchStrategyProvider = (
logger: Logger
): ISearchStrategy => {
async function cancelAsyncSearch(id: string, esClient: IScopedClusterClient) {
- await esClient.asCurrentUser.asyncSearch.delete({ id });
+ const client = esClient.asCurrentUser.eql;
+ await client.delete({ id });
}
return {
diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts
index ac00668ca7b69..ebfa3575417b7 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts
@@ -5,7 +5,12 @@
*/
import { checkRunningSessions } from './check_running_sessions';
-import { SearchSessionStatus, SearchSessionSavedObjectAttributes } from '../../../common';
+import {
+ SearchSessionStatus,
+ SearchSessionSavedObjectAttributes,
+ ENHANCED_ES_SEARCH_STRATEGY,
+ EQL_SEARCH_STRATEGY,
+} from '../../../common';
import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks';
import type { SavedObjectsClientContract } from 'kibana/server';
import { SearchSessionsConfig, SearchStatus } from './types';
@@ -35,6 +40,11 @@ describe('getSearchStatus', () => {
mockClient = {
asyncSearch: {
status: jest.fn(),
+ delete: jest.fn(),
+ },
+ eql: {
+ status: jest.fn(),
+ delete: jest.fn(),
},
};
});
@@ -162,7 +172,12 @@ describe('getSearchStatus', () => {
status: SearchSessionStatus.IN_PROGRESS,
created: moment().subtract(moment.duration(3, 'm')),
touched: moment().subtract(moment.duration(2, 'm')),
- idMapping: {},
+ idMapping: {
+ 'map-key': {
+ strategy: ENHANCED_ES_SEARCH_STRATEGY,
+ id: 'async-id',
+ },
+ },
},
},
],
@@ -180,9 +195,16 @@ describe('getSearchStatus', () => {
expect(savedObjectsClient.bulkUpdate).not.toBeCalled();
expect(savedObjectsClient.delete).toBeCalled();
+
+ expect(mockClient.asyncSearch.delete).toBeCalled();
+
+ const { id } = mockClient.asyncSearch.delete.mock.calls[0][0];
+ expect(id).toBe('async-id');
});
test('deletes a completed, not persisted session', async () => {
+ mockClient.asyncSearch.delete = jest.fn().mockResolvedValue(true);
+
savedObjectsClient.find.mockResolvedValue({
saved_objects: [
{
@@ -192,7 +214,59 @@ describe('getSearchStatus', () => {
status: SearchSessionStatus.COMPLETE,
created: moment().subtract(moment.duration(30, 'm')),
touched: moment().subtract(moment.duration(5, 'm')),
- idMapping: {},
+ idMapping: {
+ 'map-key': {
+ strategy: ENHANCED_ES_SEARCH_STRATEGY,
+ id: 'async-id',
+ },
+ 'eql-map-key': {
+ strategy: EQL_SEARCH_STRATEGY,
+ id: 'eql-async-id',
+ },
+ },
+ },
+ },
+ ],
+ total: 1,
+ } as any);
+
+ await checkRunningSessions(
+ {
+ savedObjectsClient,
+ client: mockClient,
+ logger: mockLogger,
+ },
+ config
+ );
+
+ expect(savedObjectsClient.bulkUpdate).not.toBeCalled();
+ expect(savedObjectsClient.delete).toBeCalled();
+
+ expect(mockClient.asyncSearch.delete).toBeCalled();
+ expect(mockClient.eql.delete).not.toBeCalled();
+
+ const { id } = mockClient.asyncSearch.delete.mock.calls[0][0];
+ expect(id).toBe('async-id');
+ });
+
+ test('ignores errors thrown while deleting async searches', async () => {
+ mockClient.asyncSearch.delete = jest.fn().mockRejectedValueOnce(false);
+
+ savedObjectsClient.find.mockResolvedValue({
+ saved_objects: [
+ {
+ id: '123',
+ attributes: {
+ persisted: false,
+ status: SearchSessionStatus.COMPLETE,
+ created: moment().subtract(moment.duration(30, 'm')),
+ touched: moment().subtract(moment.duration(5, 'm')),
+ idMapping: {
+ 'map-key': {
+ strategy: ENHANCED_ES_SEARCH_STRATEGY,
+ id: 'async-id',
+ },
+ },
},
},
],
@@ -210,6 +284,11 @@ describe('getSearchStatus', () => {
expect(savedObjectsClient.bulkUpdate).not.toBeCalled();
expect(savedObjectsClient.delete).toBeCalled();
+
+ expect(mockClient.asyncSearch.delete).toBeCalled();
+
+ const { id } = mockClient.asyncSearch.delete.mock.calls[0][0];
+ expect(id).toBe('async-id');
});
});
diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
index 4d5cd0a3231b7..5aea9c86d956c 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts
@@ -17,6 +17,7 @@ import {
SearchSessionSavedObjectAttributes,
SearchSessionRequestInfo,
SEARCH_SESSION_TYPE,
+ ENHANCED_ES_SEARCH_STRATEGY,
} from '../../../common';
import { getSearchStatus } from './get_search_status';
import { getSessionStatus } from './get_session_status';
@@ -138,7 +139,19 @@ export async function checkRunningSessions(
logger.debug(`Deleting stale session | ${session.id}`);
await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id);
- // TODO cancel running search requests
+ // Send a delete request for each async search to ES
+ Object.keys(session.attributes.idMapping).map(async (searchKey: string) => {
+ const searchInfo = session.attributes.idMapping[searchKey];
+ if (searchInfo.strategy === ENHANCED_ES_SEARCH_STRATEGY) {
+ try {
+ await client.asyncSearch.delete({ id: searchInfo.id });
+ } catch (e) {
+ logger.debug(
+ `Error ignored while deleting async_search ${searchInfo.id}: ${e.message}`
+ );
+ }
+ }
+ });
}
}
diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
index e6c2b60e4a79d..c8a8b58de25bb 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts
@@ -255,8 +255,7 @@ export class SearchSessionService implements ISessionService {
};
/**
- * Tracks the given search request/search ID in the saved session (if it exists). Otherwise, just
- * store it in memory until a saved session exists.
+ * Tracks the given search request/search ID in the saved session.
* @internal
*/
public trackId = async (
From 4f43096c64c4b27205ecd8fd3aecfd1426da6892 Mon Sep 17 00:00:00 2001
From: Nicolas Ruflin
Date: Mon, 1 Feb 2021 10:28:49 +0100
Subject: [PATCH 15/85] [Fleet] Remove comments around experimental registry
(#89830)
The experimental registry was used for the 7.8 release but since then was not touched anymore. Because of this it should not show up in the code anymore even if it is commented out.
---
.../plugins/fleet/server/services/epm/registry/registry_url.ts | 2 --
1 file changed, 2 deletions(-)
diff --git a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts
index efc25cc2efb5d..4f17a2b88670a 100644
--- a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts
+++ b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts
@@ -11,12 +11,10 @@ import { appContextService, licenseService } from '../../';
const PRODUCTION_REGISTRY_URL_CDN = 'https://epr.elastic.co';
// const STAGING_REGISTRY_URL_CDN = 'https://epr-staging.elastic.co';
-// const EXPERIMENTAL_REGISTRY_URL_CDN = 'https://epr-experimental.elastic.co/';
const SNAPSHOT_REGISTRY_URL_CDN = 'https://epr-snapshot.elastic.co';
// const PRODUCTION_REGISTRY_URL_NO_CDN = 'https://epr.ea-web.elastic.dev';
// const STAGING_REGISTRY_URL_NO_CDN = 'https://epr-staging.ea-web.elastic.dev';
-// const EXPERIMENTAL_REGISTRY_URL_NO_CDN = 'https://epr-experimental.ea-web.elastic.dev/';
// const SNAPSHOT_REGISTRY_URL_NO_CDN = 'https://epr-snapshot.ea-web.elastic.dev';
const getDefaultRegistryUrl = (): string => {
From c2f53a96ebb6e4a5a9a8e4dcbbcde33aa4d1f20d Mon Sep 17 00:00:00 2001
From: Anton Dosov
Date: Mon, 1 Feb 2021 10:40:38 +0100
Subject: [PATCH 16/85] [Search Sessions][Dashboard] Clear search session when
navigating from dashboard route (#89749)
---
src/plugins/dashboard/public/application/dashboard_app.tsx | 7 +++++++
.../apps/dashboard/async_search/send_to_background.ts | 4 ++++
2 files changed, 11 insertions(+)
diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx
index 7ea181715717b..6955365ebca3f 100644
--- a/src/plugins/dashboard/public/application/dashboard_app.tsx
+++ b/src/plugins/dashboard/public/application/dashboard_app.tsx
@@ -265,6 +265,13 @@ export function DashboardApp({
};
}, [dashboardStateManager, dashboardContainer, onAppLeave, embeddable]);
+ // clear search session when leaving dashboard route
+ useEffect(() => {
+ return () => {
+ data.search.session.clear();
+ };
+ }, [data.search.session]);
+
return (
{savedDashboard && dashboardStateManager && dashboardContainer && viewMode && (
diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts
index 7e878e763bfc1..3e417551c3cb9 100644
--- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts
+++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts
@@ -96,6 +96,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// should leave session state untouched
await PageObjects.dashboard.switchToEditMode();
await searchSessions.expectState('restored');
+
+ // navigating to a listing page clears the session
+ await PageObjects.dashboard.gotoDashboardLandingPage();
+ await searchSessions.missingOrFail();
});
});
}
From f0717a0a79d8cb1c772a9039aa7796691aa78ea0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Casper=20H=C3=BCbertz?=
Date: Mon, 1 Feb 2021 10:54:08 +0100
Subject: [PATCH 17/85] [Observability] `ActionMenu` style fixes (#89547)
* [Observability] Reduced space between title and subtitle
* [Observability] Reduce margin between sections
* [Observability] Reduce list item font size
* [Observability] Remove spacer
* [APM] Changes button style and label
* [Logs] Changes the actions button label and style
* [Logs] Fixes the overlap of actions button and close
* Updated test and snapshot
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../CustomLinkMenuSection/index.tsx | 1 -
.../TransactionActionMenu.test.tsx | 12 ++++++------
.../TransactionActionMenu/TransactionActionMenu.tsx | 8 ++++----
.../TransactionActionMenu.test.tsx.snap | 8 ++++----
.../log_entry_flyout/log_entry_actions_menu.tsx | 8 ++++----
.../logging/log_entry_flyout/log_entry_flyout.tsx | 2 +-
.../public/components/shared/action_menu/index.tsx | 6 +++---
7 files changed, 22 insertions(+), 23 deletions(-)
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx
index ae22718af8b57..43f566a93a89d 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx
@@ -107,7 +107,6 @@ export function CustomLinkMenuSection({
-
{i18n.translate(
'xpack.apm.transactionActionMenu.customLink.subtitle',
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx
index 48c863b460482..3141dc7a5f3c6 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.test.tsx
@@ -52,7 +52,7 @@ const renderTransaction = async (transaction: Record) => {
}
);
- fireEvent.click(rendered.getByText('Actions'));
+ fireEvent.click(rendered.getByText('Investigate'));
return rendered;
};
@@ -289,7 +289,7 @@ describe('TransactionActionMenu component', () => {
});
const component = renderTransactionActionMenuWithLicense(license);
act(() => {
- fireEvent.click(component.getByText('Actions'));
+ fireEvent.click(component.getByText('Investigate'));
});
expectTextsNotInDocument(component, ['Custom Links']);
});
@@ -313,7 +313,7 @@ describe('TransactionActionMenu component', () => {
{ wrapper: Wrapper }
);
act(() => {
- fireEvent.click(component.getByText('Actions'));
+ fireEvent.click(component.getByText('Investigate'));
});
expectTextsNotInDocument(component, ['Custom Links']);
});
@@ -330,7 +330,7 @@ describe('TransactionActionMenu component', () => {
});
const component = renderTransactionActionMenuWithLicense(license);
act(() => {
- fireEvent.click(component.getByText('Actions'));
+ fireEvent.click(component.getByText('Investigate'));
});
expectTextsInDocument(component, ['Custom Links']);
});
@@ -347,7 +347,7 @@ describe('TransactionActionMenu component', () => {
});
const component = renderTransactionActionMenuWithLicense(license);
act(() => {
- fireEvent.click(component.getByText('Actions'));
+ fireEvent.click(component.getByText('Investigate'));
});
expectTextsInDocument(component, ['Custom Links']);
});
@@ -364,7 +364,7 @@ describe('TransactionActionMenu component', () => {
});
const component = renderTransactionActionMenuWithLicense(license);
act(() => {
- fireEvent.click(component.getByText('Actions'));
+ fireEvent.click(component.getByText('Investigate'));
});
expectTextsInDocument(component, ['Custom Links']);
act(() => {
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx
index 312513db80886..22fa25f93b212 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiButtonEmpty } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { useLocation } from 'react-router-dom';
@@ -30,11 +30,11 @@ interface Props {
function ActionMenuButton({ onClick }: { onClick: () => void }) {
return (
-
+
{i18n.translate('xpack.apm.transactionActionMenu.actionsButtonLabel', {
- defaultMessage: 'Actions',
+ defaultMessage: 'Investigate',
})}
-
+
);
}
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap
index fa6db645d28a8..ea33fb3c3df08 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap
+++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__snapshots__/TransactionActionMenu.test.tsx.snap
@@ -10,20 +10,20 @@ exports[`TransactionActionMenu component matches the snapshot 1`] = `
class="euiPopover__anchor"
>
diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx
index aa3b4532e878e..9fef939733432 100644
--- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx
+++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
+import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useMemo } from 'react';
import { useVisibilityState } from '../../../utils/use_visibility_state';
@@ -67,7 +67,7 @@ export const LogEntryActionsMenu: React.FunctionComponent<{
-
+
}
closePopover={hide}
id="logEntryActionsMenu"
diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx
index 5684d4068f3be..7d8ca95f9b93b 100644
--- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx
+++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx
@@ -88,7 +88,7 @@ export const LogEntryFlyout = ({
>
) : null}
-
+
{logEntry ? : null}
diff --git a/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx b/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx
index 4819a0760d88a..af61f618a89b2 100644
--- a/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx
+++ b/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx
@@ -26,7 +26,7 @@ export function SectionTitle({ children }: { children?: ReactNode }) {
{children}
-
+
>
);
}
@@ -55,7 +55,7 @@ export function SectionSpacer() {
}
export const Section = styled.div`
- margin-bottom: 24px;
+ margin-bottom: 16px;
&:last-of-type {
margin-bottom: 0;
}
@@ -63,7 +63,7 @@ export const Section = styled.div`
export type SectionLinkProps = EuiListGroupItemProps;
export function SectionLink(props: SectionLinkProps) {
- return ;
+ return ;
}
export function ActionMenuDivider() {
From 84d49f11238c76c806b360a28f1d579dde38ab16 Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Mon, 1 Feb 2021 11:03:44 +0100
Subject: [PATCH 18/85] [SOM] display invalid references in the relationship
flyout (#88814)
* return invalid relations and display them in SOM
* add FTR test
---
.../saved_objects_management/common/index.ts | 9 +-
.../saved_objects_management/common/types.ts | 16 +-
.../public/lib/get_relationships.test.ts | 9 +-
.../public/lib/get_relationships.ts | 6 +-
.../__snapshots__/relationships.test.tsx.snap | 1097 ++++++++++-------
.../components/relationships.test.tsx | 265 ++--
.../components/relationships.tsx | 179 ++-
.../saved_objects_management/public/types.ts | 9 +-
.../server/lib/find_relationships.test.ts | 227 +++-
.../server/lib/find_relationships.ts | 73 +-
.../server/routes/relationships.ts | 4 +-
.../saved_objects_management/server/types.ts | 9 +-
.../saved_objects_management/relationships.ts | 106 +-
.../saved_objects/relationships/data.json | 190 +++
.../saved_objects/relationships/data.json.gz | Bin 1385 -> 0 bytes
.../saved_objects/relationships/mappings.json | 16 +-
.../apps/saved_objects_management/index.ts | 1 +
.../show_relationships.ts | 52 +
.../show_relationships/data.json | 36 +
.../show_relationships/mappings.json | 473 +++++++
.../management/saved_objects_page.ts | 16 +
21 files changed, 2058 insertions(+), 735 deletions(-)
create mode 100644 test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json
delete mode 100644 test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json.gz
create mode 100644 test/functional/apps/saved_objects_management/show_relationships.ts
create mode 100644 test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/data.json
create mode 100644 test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json
diff --git a/src/plugins/saved_objects_management/common/index.ts b/src/plugins/saved_objects_management/common/index.ts
index a8395e602979c..8850899e38958 100644
--- a/src/plugins/saved_objects_management/common/index.ts
+++ b/src/plugins/saved_objects_management/common/index.ts
@@ -6,4 +6,11 @@
* Public License, v 1.
*/
-export { SavedObjectRelation, SavedObjectWithMetadata, SavedObjectMetadata } from './types';
+export {
+ SavedObjectWithMetadata,
+ SavedObjectMetadata,
+ SavedObjectRelation,
+ SavedObjectRelationKind,
+ SavedObjectInvalidRelation,
+ SavedObjectGetRelationshipsResponse,
+} from './types';
diff --git a/src/plugins/saved_objects_management/common/types.ts b/src/plugins/saved_objects_management/common/types.ts
index 8618cf4332acf..e100dfc6b23e6 100644
--- a/src/plugins/saved_objects_management/common/types.ts
+++ b/src/plugins/saved_objects_management/common/types.ts
@@ -28,12 +28,26 @@ export type SavedObjectWithMetadata = SavedObject & {
meta: SavedObjectMetadata;
};
+export type SavedObjectRelationKind = 'child' | 'parent';
+
/**
* Represents a relation between two {@link SavedObject | saved object}
*/
export interface SavedObjectRelation {
id: string;
type: string;
- relationship: 'child' | 'parent';
+ relationship: SavedObjectRelationKind;
meta: SavedObjectMetadata;
}
+
+export interface SavedObjectInvalidRelation {
+ id: string;
+ type: string;
+ relationship: SavedObjectRelationKind;
+ error: string;
+}
+
+export interface SavedObjectGetRelationshipsResponse {
+ relations: SavedObjectRelation[];
+ invalidRelations: SavedObjectInvalidRelation[];
+}
diff --git a/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts b/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts
index b609fac67dac1..4454907f530fe 100644
--- a/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts
+++ b/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts
@@ -6,6 +6,7 @@
* Public License, v 1.
*/
+import { SavedObjectGetRelationshipsResponse } from '../types';
import { httpServiceMock } from '../../../../core/public/mocks';
import { getRelationships } from './get_relationships';
@@ -22,13 +23,17 @@ describe('getRelationships', () => {
});
it('should handle successful responses', async () => {
- httpMock.get.mockResolvedValue([1, 2]);
+ const serverResponse: SavedObjectGetRelationshipsResponse = {
+ relations: [],
+ invalidRelations: [],
+ };
+ httpMock.get.mockResolvedValue(serverResponse);
const response = await getRelationships(httpMock, 'dashboard', '1', [
'search',
'index-pattern',
]);
- expect(response).toEqual([1, 2]);
+ expect(response).toEqual(serverResponse);
});
it('should handle errors', async () => {
diff --git a/src/plugins/saved_objects_management/public/lib/get_relationships.ts b/src/plugins/saved_objects_management/public/lib/get_relationships.ts
index 0eb97e1052fa4..69aeb6fbf580b 100644
--- a/src/plugins/saved_objects_management/public/lib/get_relationships.ts
+++ b/src/plugins/saved_objects_management/public/lib/get_relationships.ts
@@ -8,19 +8,19 @@
import { HttpStart } from 'src/core/public';
import { get } from 'lodash';
-import { SavedObjectRelation } from '../types';
+import { SavedObjectGetRelationshipsResponse } from '../types';
export async function getRelationships(
http: HttpStart,
type: string,
id: string,
savedObjectTypes: string[]
-): Promise {
+): Promise {
const url = `/api/kibana/management/saved_objects/relationships/${encodeURIComponent(
type
)}/${encodeURIComponent(id)}`;
try {
- return await http.get(url, {
+ return await http.get(url, {
query: {
savedObjectTypes,
},
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap
index 15e5cb89b622c..c39263f304249 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap
@@ -28,133 +28,131 @@ exports[`Relationships should render dashboards normally 1`] = `
-
-
-
- Here are the saved objects related to MyDashboard. Deleting this dashboard affects its parent objects, but not its children.
-
-
-
-
+
+ Here are the saved objects related to MyDashboard. Deleting this dashboard affects its parent objects, but not its children.
+
+
+
+
-
+ }
+ tableLayout="fixed"
+ />
`;
@@ -231,138 +229,315 @@ exports[`Relationships should render index patterns normally 1`] = `
-
-
-
- Here are the saved objects related to MyIndexPattern*. Deleting this index-pattern affects its parent objects, but not its children.
-
-
-
-
+
+ Here are the saved objects related to MyIndexPattern*. Deleting this index-pattern affects its parent objects, but not its children.
+
+
+
+
+
+
+`;
+
+exports[`Relationships should render invalid relations 1`] = `
+
+
+
+
+
+
+
+
+ MyIndexPattern*
+
+
+
+
+
+
+
+
+
+
+ Here are the saved objects related to MyIndexPattern*. Deleting this index-pattern affects its parent objects, but not its children.
+
+
+
+
-
+ }
+ tableLayout="fixed"
+ />
`;
@@ -395,138 +570,136 @@ exports[`Relationships should render searches normally 1`] = `
-
-
-
- Here are the saved objects related to MySearch. Deleting this search affects its parent objects, but not its children.
-
-
-
-
+
+ Here are the saved objects related to MySearch. Deleting this search affects its parent objects, but not its children.
+
+
+
+
-
+ }
+ tableLayout="fixed"
+ />
`;
@@ -559,133 +732,131 @@ exports[`Relationships should render visualizations normally 1`] = `
-
-
-
- Here are the saved objects related to MyViz. Deleting this visualization affects its parent objects, but not its children.
-
-
-
-
+
+ Here are the saved objects related to MyViz. Deleting this visualization affects its parent objects, but not its children.
+
+
+
+
-
+ }
+ tableLayout="fixed"
+ />
`;
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx
index 72a4b0f2788fa..e590520193bba 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx
@@ -25,36 +25,39 @@ describe('Relationships', () => {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
- getRelationships: jest.fn().mockImplementation(() => [
- {
- type: 'search',
- id: '1',
- relationship: 'parent',
- meta: {
- editUrl: '/management/kibana/objects/savedSearches/1',
- icon: 'search',
- inAppUrl: {
- path: '/app/discover#//1',
- uiCapabilitiesPath: 'discover.show',
+ getRelationships: jest.fn().mockImplementation(() => ({
+ relations: [
+ {
+ type: 'search',
+ id: '1',
+ relationship: 'parent',
+ meta: {
+ editUrl: '/management/kibana/objects/savedSearches/1',
+ icon: 'search',
+ inAppUrl: {
+ path: '/app/discover#//1',
+ uiCapabilitiesPath: 'discover.show',
+ },
+ title: 'My Search Title',
},
- title: 'My Search Title',
},
- },
- {
- type: 'visualization',
- id: '2',
- relationship: 'parent',
- meta: {
- editUrl: '/management/kibana/objects/savedVisualizations/2',
- icon: 'visualizeApp',
- inAppUrl: {
- path: '/app/visualize#/edit/2',
- uiCapabilitiesPath: 'visualize.show',
+ {
+ type: 'visualization',
+ id: '2',
+ relationship: 'parent',
+ meta: {
+ editUrl: '/management/kibana/objects/savedVisualizations/2',
+ icon: 'visualizeApp',
+ inAppUrl: {
+ path: '/app/visualize#/edit/2',
+ uiCapabilitiesPath: 'visualize.show',
+ },
+ title: 'My Visualization Title',
},
- title: 'My Visualization Title',
},
- },
- ]),
+ ],
+ invalidRelations: [],
+ })),
savedObject: {
id: '1',
type: 'index-pattern',
@@ -92,36 +95,39 @@ describe('Relationships', () => {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
- getRelationships: jest.fn().mockImplementation(() => [
- {
- type: 'index-pattern',
- id: '1',
- relationship: 'child',
- meta: {
- editUrl: '/management/kibana/indexPatterns/patterns/1',
- icon: 'indexPatternApp',
- inAppUrl: {
- path: '/app/management/kibana/indexPatterns/patterns/1',
- uiCapabilitiesPath: 'management.kibana.indexPatterns',
+ getRelationships: jest.fn().mockImplementation(() => ({
+ relations: [
+ {
+ type: 'index-pattern',
+ id: '1',
+ relationship: 'child',
+ meta: {
+ editUrl: '/management/kibana/indexPatterns/patterns/1',
+ icon: 'indexPatternApp',
+ inAppUrl: {
+ path: '/app/management/kibana/indexPatterns/patterns/1',
+ uiCapabilitiesPath: 'management.kibana.indexPatterns',
+ },
+ title: 'My Index Pattern',
},
- title: 'My Index Pattern',
},
- },
- {
- type: 'visualization',
- id: '2',
- relationship: 'parent',
- meta: {
- editUrl: '/management/kibana/objects/savedVisualizations/2',
- icon: 'visualizeApp',
- inAppUrl: {
- path: '/app/visualize#/edit/2',
- uiCapabilitiesPath: 'visualize.show',
+ {
+ type: 'visualization',
+ id: '2',
+ relationship: 'parent',
+ meta: {
+ editUrl: '/management/kibana/objects/savedVisualizations/2',
+ icon: 'visualizeApp',
+ inAppUrl: {
+ path: '/app/visualize#/edit/2',
+ uiCapabilitiesPath: 'visualize.show',
+ },
+ title: 'My Visualization Title',
},
- title: 'My Visualization Title',
},
- },
- ]),
+ ],
+ invalidRelations: [],
+ })),
savedObject: {
id: '1',
type: 'search',
@@ -159,36 +165,39 @@ describe('Relationships', () => {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
- getRelationships: jest.fn().mockImplementation(() => [
- {
- type: 'dashboard',
- id: '1',
- relationship: 'parent',
- meta: {
- editUrl: '/management/kibana/objects/savedDashboards/1',
- icon: 'dashboardApp',
- inAppUrl: {
- path: '/app/kibana#/dashboard/1',
- uiCapabilitiesPath: 'dashboard.show',
+ getRelationships: jest.fn().mockImplementation(() => ({
+ relations: [
+ {
+ type: 'dashboard',
+ id: '1',
+ relationship: 'parent',
+ meta: {
+ editUrl: '/management/kibana/objects/savedDashboards/1',
+ icon: 'dashboardApp',
+ inAppUrl: {
+ path: '/app/kibana#/dashboard/1',
+ uiCapabilitiesPath: 'dashboard.show',
+ },
+ title: 'My Dashboard 1',
},
- title: 'My Dashboard 1',
},
- },
- {
- type: 'dashboard',
- id: '2',
- relationship: 'parent',
- meta: {
- editUrl: '/management/kibana/objects/savedDashboards/2',
- icon: 'dashboardApp',
- inAppUrl: {
- path: '/app/kibana#/dashboard/2',
- uiCapabilitiesPath: 'dashboard.show',
+ {
+ type: 'dashboard',
+ id: '2',
+ relationship: 'parent',
+ meta: {
+ editUrl: '/management/kibana/objects/savedDashboards/2',
+ icon: 'dashboardApp',
+ inAppUrl: {
+ path: '/app/kibana#/dashboard/2',
+ uiCapabilitiesPath: 'dashboard.show',
+ },
+ title: 'My Dashboard 2',
},
- title: 'My Dashboard 2',
},
- },
- ]),
+ ],
+ invalidRelations: [],
+ })),
savedObject: {
id: '1',
type: 'visualization',
@@ -226,36 +235,39 @@ describe('Relationships', () => {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
- getRelationships: jest.fn().mockImplementation(() => [
- {
- type: 'visualization',
- id: '1',
- relationship: 'child',
- meta: {
- editUrl: '/management/kibana/objects/savedVisualizations/1',
- icon: 'visualizeApp',
- inAppUrl: {
- path: '/app/visualize#/edit/1',
- uiCapabilitiesPath: 'visualize.show',
+ getRelationships: jest.fn().mockImplementation(() => ({
+ relations: [
+ {
+ type: 'visualization',
+ id: '1',
+ relationship: 'child',
+ meta: {
+ editUrl: '/management/kibana/objects/savedVisualizations/1',
+ icon: 'visualizeApp',
+ inAppUrl: {
+ path: '/app/visualize#/edit/1',
+ uiCapabilitiesPath: 'visualize.show',
+ },
+ title: 'My Visualization Title 1',
},
- title: 'My Visualization Title 1',
},
- },
- {
- type: 'visualization',
- id: '2',
- relationship: 'child',
- meta: {
- editUrl: '/management/kibana/objects/savedVisualizations/2',
- icon: 'visualizeApp',
- inAppUrl: {
- path: '/app/visualize#/edit/2',
- uiCapabilitiesPath: 'visualize.show',
+ {
+ type: 'visualization',
+ id: '2',
+ relationship: 'child',
+ meta: {
+ editUrl: '/management/kibana/objects/savedVisualizations/2',
+ icon: 'visualizeApp',
+ inAppUrl: {
+ path: '/app/visualize#/edit/2',
+ uiCapabilitiesPath: 'visualize.show',
+ },
+ title: 'My Visualization Title 2',
},
- title: 'My Visualization Title 2',
},
- },
- ]),
+ ],
+ invalidRelations: [],
+ })),
savedObject: {
id: '1',
type: 'dashboard',
@@ -324,4 +336,49 @@ describe('Relationships', () => {
expect(props.getRelationships).toHaveBeenCalled();
expect(component).toMatchSnapshot();
});
+
+ it('should render invalid relations', async () => {
+ const props: RelationshipsProps = {
+ goInspectObject: () => {},
+ canGoInApp: () => true,
+ basePath: httpServiceMock.createSetupContract().basePath,
+ getRelationships: jest.fn().mockImplementation(() => ({
+ relations: [],
+ invalidRelations: [
+ {
+ id: '1',
+ type: 'dashboard',
+ relationship: 'child',
+ error: 'Saved object [dashboard/1] not found',
+ },
+ ],
+ })),
+ savedObject: {
+ id: '1',
+ type: 'index-pattern',
+ attributes: {},
+ references: [],
+ meta: {
+ title: 'MyIndexPattern*',
+ icon: 'indexPatternApp',
+ editUrl: '#/management/kibana/indexPatterns/patterns/1',
+ inAppUrl: {
+ path: '/management/kibana/indexPatterns/patterns/1',
+ uiCapabilitiesPath: 'management.kibana.indexPatterns',
+ },
+ },
+ },
+ close: jest.fn(),
+ };
+
+ const component = shallowWithI18nProvider();
+
+ // Ensure all promises resolve
+ await new Promise((resolve) => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(props.getRelationships).toHaveBeenCalled();
+ expect(component).toMatchSnapshot();
+ });
});
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx
index 2d62699b6f1f2..aee61f7bc9c7a 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx
@@ -26,11 +26,17 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { IBasePath } from 'src/core/public';
import { getDefaultTitle, getSavedObjectLabel } from '../../../lib';
-import { SavedObjectWithMetadata, SavedObjectRelation } from '../../../types';
+import {
+ SavedObjectWithMetadata,
+ SavedObjectRelationKind,
+ SavedObjectRelation,
+ SavedObjectInvalidRelation,
+ SavedObjectGetRelationshipsResponse,
+} from '../../../types';
export interface RelationshipsProps {
basePath: IBasePath;
- getRelationships: (type: string, id: string) => Promise;
+ getRelationships: (type: string, id: string) => Promise;
savedObject: SavedObjectWithMetadata;
close: () => void;
goInspectObject: (obj: SavedObjectWithMetadata) => void;
@@ -38,17 +44,47 @@ export interface RelationshipsProps {
}
export interface RelationshipsState {
- relationships: SavedObjectRelation[];
+ relations: SavedObjectRelation[];
+ invalidRelations: SavedObjectInvalidRelation[];
isLoading: boolean;
error?: string;
}
+const relationshipColumn = {
+ field: 'relationship',
+ name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnRelationshipName', {
+ defaultMessage: 'Direct relationship',
+ }),
+ dataType: 'string',
+ sortable: false,
+ width: '125px',
+ 'data-test-subj': 'directRelationship',
+ render: (relationship: SavedObjectRelationKind) => {
+ return (
+
+ {relationship === 'parent' ? (
+
+ ) : (
+
+ )}
+
+ );
+ },
+};
+
export class Relationships extends Component {
constructor(props: RelationshipsProps) {
super(props);
this.state = {
- relationships: [],
+ relations: [],
+ invalidRelations: [],
isLoading: false,
error: undefined,
};
@@ -70,8 +106,11 @@ export class Relationships extends Component
+
+
+ ({
+ 'data-test-subj': `invalidRelationshipsTableRow`,
+ })}
+ />
+
+ >
+ );
+ }
+
+ renderRelationshipsTable() {
+ const { goInspectObject, basePath, savedObject } = this.props;
+ const { relations, isLoading, error } = this.state;
if (error) {
return this.renderError();
@@ -137,39 +250,7 @@ export class Relationships extends Component {
- if (relationship === 'parent') {
- return (
-
-
-
- );
- }
- if (relationship === 'child') {
- return (
-
-
-
- );
- }
- },
- },
+ relationshipColumn,
{
field: 'meta.title',
name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnTitleName', {
@@ -224,7 +305,7 @@ export class Relationships extends Component [
+ relations.map((relationship) => [
relationship.type,
{
value: relationship.type,
@@ -277,7 +358,7 @@ export class Relationships extends Component
+ <>
{i18n.translate(
@@ -296,7 +377,7 @@ export class Relationships extends Component
-
+ >
);
}
@@ -328,8 +409,10 @@ export class Relationships extends Component
-
- {this.renderRelationships()}
+
+ {this.renderInvalidRelationship()}
+ {this.renderRelationshipsTable()}
+
);
}
diff --git a/src/plugins/saved_objects_management/public/types.ts b/src/plugins/saved_objects_management/public/types.ts
index 37f239227475d..cdfa3c43e5af2 100644
--- a/src/plugins/saved_objects_management/public/types.ts
+++ b/src/plugins/saved_objects_management/public/types.ts
@@ -6,4 +6,11 @@
* Public License, v 1.
*/
-export { SavedObjectMetadata, SavedObjectWithMetadata, SavedObjectRelation } from '../common';
+export {
+ SavedObjectMetadata,
+ SavedObjectWithMetadata,
+ SavedObjectRelationKind,
+ SavedObjectRelation,
+ SavedObjectInvalidRelation,
+ SavedObjectGetRelationshipsResponse,
+} from '../common';
diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts
index 631faf0c23c98..416be7d7e7426 100644
--- a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts
+++ b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts
@@ -6,10 +6,35 @@
* Public License, v 1.
*/
+import type { SavedObject, SavedObjectError } from 'src/core/types';
+import type { SavedObjectsFindResponse } from 'src/core/server';
import { findRelationships } from './find_relationships';
import { managementMock } from '../services/management.mock';
import { savedObjectsClientMock } from '../../../../core/server/mocks';
+const createObj = (parts: Partial>): SavedObject => ({
+ id: 'id',
+ type: 'type',
+ attributes: {},
+ references: [],
+ ...parts,
+});
+
+const createFindResponse = (objs: SavedObject[]): SavedObjectsFindResponse => ({
+ saved_objects: objs.map((obj) => ({ ...obj, score: 1 })),
+ total: objs.length,
+ per_page: 20,
+ page: 1,
+});
+
+const createError = (parts: Partial): SavedObjectError => ({
+ error: 'error',
+ message: 'message',
+ metadata: {},
+ statusCode: 404,
+ ...parts,
+});
+
describe('findRelationships', () => {
let savedObjectsClient: ReturnType;
let managementService: ReturnType;
@@ -19,7 +44,7 @@ describe('findRelationships', () => {
managementService = managementMock.create();
});
- it('returns the child and parent references of the object', async () => {
+ it('calls the savedObjectClient APIs with the correct parameters', async () => {
const type = 'dashboard';
const id = 'some-id';
const references = [
@@ -36,46 +61,35 @@ describe('findRelationships', () => {
];
const referenceTypes = ['some-type', 'another-type'];
- savedObjectsClient.get.mockResolvedValue({
- id,
- type,
- attributes: {},
- references,
- });
-
+ savedObjectsClient.get.mockResolvedValue(
+ createObj({
+ id,
+ type,
+ references,
+ })
+ );
savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: [
- {
+ createObj({
type: 'some-type',
id: 'ref-1',
- attributes: {},
- references: [],
- },
- {
+ }),
+ createObj({
type: 'another-type',
id: 'ref-2',
- attributes: {},
- references: [],
- },
+ }),
],
});
-
- savedObjectsClient.find.mockResolvedValue({
- saved_objects: [
- {
+ savedObjectsClient.find.mockResolvedValue(
+ createFindResponse([
+ createObj({
type: 'parent-type',
id: 'parent-id',
- attributes: {},
- score: 1,
- references: [],
- },
- ],
- total: 1,
- per_page: 20,
- page: 1,
- });
+ }),
+ ])
+ );
- const relationships = await findRelationships({
+ await findRelationships({
type,
id,
size: 20,
@@ -101,8 +115,63 @@ describe('findRelationships', () => {
perPage: 20,
type: referenceTypes,
});
+ });
+
+ it('returns the child and parent references of the object', async () => {
+ const type = 'dashboard';
+ const id = 'some-id';
+ const references = [
+ {
+ type: 'some-type',
+ id: 'ref-1',
+ name: 'ref 1',
+ },
+ {
+ type: 'another-type',
+ id: 'ref-2',
+ name: 'ref 2',
+ },
+ ];
+ const referenceTypes = ['some-type', 'another-type'];
+
+ savedObjectsClient.get.mockResolvedValue(
+ createObj({
+ id,
+ type,
+ references,
+ })
+ );
+ savedObjectsClient.bulkGet.mockResolvedValue({
+ saved_objects: [
+ createObj({
+ type: 'some-type',
+ id: 'ref-1',
+ }),
+ createObj({
+ type: 'another-type',
+ id: 'ref-2',
+ }),
+ ],
+ });
+ savedObjectsClient.find.mockResolvedValue(
+ createFindResponse([
+ createObj({
+ type: 'parent-type',
+ id: 'parent-id',
+ }),
+ ])
+ );
+
+ const { relations, invalidRelations } = await findRelationships({
+ type,
+ id,
+ size: 20,
+ client: savedObjectsClient,
+ referenceTypes,
+ savedObjectsManagement: managementService,
+ });
- expect(relationships).toEqual([
+ expect(relations).toEqual([
{
id: 'ref-1',
relationship: 'child',
@@ -122,6 +191,70 @@ describe('findRelationships', () => {
meta: expect.any(Object),
},
]);
+ expect(invalidRelations).toHaveLength(0);
+ });
+
+ it('returns the invalid relations', async () => {
+ const type = 'dashboard';
+ const id = 'some-id';
+ const references = [
+ {
+ type: 'some-type',
+ id: 'ref-1',
+ name: 'ref 1',
+ },
+ {
+ type: 'another-type',
+ id: 'ref-2',
+ name: 'ref 2',
+ },
+ ];
+ const referenceTypes = ['some-type', 'another-type'];
+
+ savedObjectsClient.get.mockResolvedValue(
+ createObj({
+ id,
+ type,
+ references,
+ })
+ );
+ const ref1Error = createError({ message: 'Not found' });
+ savedObjectsClient.bulkGet.mockResolvedValue({
+ saved_objects: [
+ createObj({
+ type: 'some-type',
+ id: 'ref-1',
+ error: ref1Error,
+ }),
+ createObj({
+ type: 'another-type',
+ id: 'ref-2',
+ }),
+ ],
+ });
+ savedObjectsClient.find.mockResolvedValue(createFindResponse([]));
+
+ const { relations, invalidRelations } = await findRelationships({
+ type,
+ id,
+ size: 20,
+ client: savedObjectsClient,
+ referenceTypes,
+ savedObjectsManagement: managementService,
+ });
+
+ expect(relations).toEqual([
+ {
+ id: 'ref-2',
+ relationship: 'child',
+ type: 'another-type',
+ meta: expect.any(Object),
+ },
+ ]);
+
+ expect(invalidRelations).toEqual([
+ { type: 'some-type', id: 'ref-1', relationship: 'child', error: ref1Error.message },
+ ]);
});
it('uses the management service to consolidate the relationship objects', async () => {
@@ -144,32 +277,24 @@ describe('findRelationships', () => {
uiCapabilitiesPath: 'uiCapabilitiesPath',
});
- savedObjectsClient.get.mockResolvedValue({
- id,
- type,
- attributes: {},
- references,
- });
-
+ savedObjectsClient.get.mockResolvedValue(
+ createObj({
+ id,
+ type,
+ references,
+ })
+ );
savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: [
- {
+ createObj({
type: 'some-type',
id: 'ref-1',
- attributes: {},
- references: [],
- },
+ }),
],
});
+ savedObjectsClient.find.mockResolvedValue(createFindResponse([]));
- savedObjectsClient.find.mockResolvedValue({
- saved_objects: [],
- total: 0,
- per_page: 20,
- page: 1,
- });
-
- const relationships = await findRelationships({
+ const { relations } = await findRelationships({
type,
id,
size: 20,
@@ -183,7 +308,7 @@ describe('findRelationships', () => {
expect(managementService.getEditUrl).toHaveBeenCalledTimes(1);
expect(managementService.getInAppUrl).toHaveBeenCalledTimes(1);
- expect(relationships).toEqual([
+ expect(relations).toEqual([
{
id: 'ref-1',
relationship: 'child',
diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.ts
index 0ceef484196a3..bc6568e73c4e2 100644
--- a/src/plugins/saved_objects_management/server/lib/find_relationships.ts
+++ b/src/plugins/saved_objects_management/server/lib/find_relationships.ts
@@ -9,7 +9,11 @@
import { SavedObjectsClientContract } from 'src/core/server';
import { injectMetaAttributes } from './inject_meta_attributes';
import { ISavedObjectsManagement } from '../services';
-import { SavedObjectRelation, SavedObjectWithMetadata } from '../types';
+import {
+ SavedObjectInvalidRelation,
+ SavedObjectWithMetadata,
+ SavedObjectGetRelationshipsResponse,
+} from '../types';
export async function findRelationships({
type,
@@ -25,17 +29,19 @@ export async function findRelationships({
client: SavedObjectsClientContract;
referenceTypes: string[];
savedObjectsManagement: ISavedObjectsManagement;
-}): Promise {
+}): Promise {
const { references = [] } = await client.get(type, id);
// Use a map to avoid duplicates, it does happen but have a different "name" in the reference
- const referencedToBulkGetOpts = new Map(
- references.map((ref) => [`${ref.type}:${ref.id}`, { id: ref.id, type: ref.type }])
- );
+ const childrenReferences = [
+ ...new Map(
+ references.map((ref) => [`${ref.type}:${ref.id}`, { id: ref.id, type: ref.type }])
+ ).values(),
+ ];
const [childReferencesResponse, parentReferencesResponse] = await Promise.all([
- referencedToBulkGetOpts.size > 0
- ? client.bulkGet([...referencedToBulkGetOpts.values()])
+ childrenReferences.length > 0
+ ? client.bulkGet(childrenReferences)
: Promise.resolve({ saved_objects: [] }),
client.find({
hasReference: { type, id },
@@ -44,28 +50,37 @@ export async function findRelationships({
}),
]);
- return childReferencesResponse.saved_objects
- .map((obj) => injectMetaAttributes(obj, savedObjectsManagement))
- .map(extractCommonProperties)
- .map(
- (obj) =>
- ({
- ...obj,
- relationship: 'child',
- } as SavedObjectRelation)
- )
- .concat(
- parentReferencesResponse.saved_objects
- .map((obj) => injectMetaAttributes(obj, savedObjectsManagement))
- .map(extractCommonProperties)
- .map(
- (obj) =>
- ({
- ...obj,
- relationship: 'parent',
- } as SavedObjectRelation)
- )
- );
+ const invalidRelations: SavedObjectInvalidRelation[] = childReferencesResponse.saved_objects
+ .filter((obj) => Boolean(obj.error))
+ .map((obj) => ({
+ id: obj.id,
+ type: obj.type,
+ relationship: 'child',
+ error: obj.error!.message,
+ }));
+
+ const relations = [
+ ...childReferencesResponse.saved_objects
+ .filter((obj) => !obj.error)
+ .map((obj) => injectMetaAttributes(obj, savedObjectsManagement))
+ .map(extractCommonProperties)
+ .map((obj) => ({
+ ...obj,
+ relationship: 'child' as const,
+ })),
+ ...parentReferencesResponse.saved_objects
+ .map((obj) => injectMetaAttributes(obj, savedObjectsManagement))
+ .map(extractCommonProperties)
+ .map((obj) => ({
+ ...obj,
+ relationship: 'parent' as const,
+ })),
+ ];
+
+ return {
+ relations,
+ invalidRelations,
+ };
}
function extractCommonProperties(savedObject: SavedObjectWithMetadata) {
diff --git a/src/plugins/saved_objects_management/server/routes/relationships.ts b/src/plugins/saved_objects_management/server/routes/relationships.ts
index 3a52c973fde8d..5417ff2926120 100644
--- a/src/plugins/saved_objects_management/server/routes/relationships.ts
+++ b/src/plugins/saved_objects_management/server/routes/relationships.ts
@@ -38,7 +38,7 @@ export const registerRelationshipsRoute = (
? req.query.savedObjectTypes
: [req.query.savedObjectTypes];
- const relations = await findRelationships({
+ const findRelationsResponse = await findRelationships({
type,
id,
client,
@@ -48,7 +48,7 @@ export const registerRelationshipsRoute = (
});
return res.ok({
- body: relations,
+ body: findRelationsResponse,
});
})
);
diff --git a/src/plugins/saved_objects_management/server/types.ts b/src/plugins/saved_objects_management/server/types.ts
index 710bb5db7d1cb..562970d2d2dcd 100644
--- a/src/plugins/saved_objects_management/server/types.ts
+++ b/src/plugins/saved_objects_management/server/types.ts
@@ -12,4 +12,11 @@ export interface SavedObjectsManagementPluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SavedObjectsManagementPluginStart {}
-export { SavedObjectMetadata, SavedObjectWithMetadata, SavedObjectRelation } from '../common';
+export {
+ SavedObjectMetadata,
+ SavedObjectWithMetadata,
+ SavedObjectRelationKind,
+ SavedObjectRelation,
+ SavedObjectInvalidRelation,
+ SavedObjectGetRelationshipsResponse,
+} from '../common';
diff --git a/test/api_integration/apis/saved_objects_management/relationships.ts b/test/api_integration/apis/saved_objects_management/relationships.ts
index 185c6ded01de4..6dea461f790e8 100644
--- a/test/api_integration/apis/saved_objects_management/relationships.ts
+++ b/test/api_integration/apis/saved_objects_management/relationships.ts
@@ -14,23 +14,32 @@ export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
- const responseSchema = schema.arrayOf(
- schema.object({
- id: schema.string(),
- type: schema.string(),
- relationship: schema.oneOf([schema.literal('parent'), schema.literal('child')]),
- meta: schema.object({
- title: schema.string(),
- icon: schema.string(),
- editUrl: schema.string(),
- inAppUrl: schema.object({
- path: schema.string(),
- uiCapabilitiesPath: schema.string(),
- }),
- namespaceType: schema.string(),
+ const relationSchema = schema.object({
+ id: schema.string(),
+ type: schema.string(),
+ relationship: schema.oneOf([schema.literal('parent'), schema.literal('child')]),
+ meta: schema.object({
+ title: schema.string(),
+ icon: schema.string(),
+ editUrl: schema.string(),
+ inAppUrl: schema.object({
+ path: schema.string(),
+ uiCapabilitiesPath: schema.string(),
}),
- })
- );
+ namespaceType: schema.string(),
+ }),
+ });
+ const invalidRelationSchema = schema.object({
+ id: schema.string(),
+ type: schema.string(),
+ relationship: schema.oneOf([schema.literal('parent'), schema.literal('child')]),
+ error: schema.string(),
+ });
+
+ const responseSchema = schema.object({
+ relations: schema.arrayOf(relationSchema),
+ invalidRelations: schema.arrayOf(invalidRelationSchema),
+ });
describe('relationships', () => {
before(async () => {
@@ -64,7 +73,7 @@ export default function ({ getService }: FtrProviderContext) {
.get(relationshipsUrl('search', '960372e0-3224-11e8-a572-ffca06da1357'))
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: '8963ca30-3224-11e8-a572-ffca06da1357',
type: 'index-pattern',
@@ -108,7 +117,7 @@ export default function ({ getService }: FtrProviderContext) {
)
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: '8963ca30-3224-11e8-a572-ffca06da1357',
type: 'index-pattern',
@@ -145,8 +154,7 @@ export default function ({ getService }: FtrProviderContext) {
]);
});
- // TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail.
- it.skip('should return 404 if search finds no results', async () => {
+ it('should return 404 if search finds no results', async () => {
await supertest
.get(relationshipsUrl('search', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'))
.expect(404);
@@ -169,7 +177,7 @@ export default function ({ getService }: FtrProviderContext) {
.get(relationshipsUrl('dashboard', 'b70c7ae0-3224-11e8-a572-ffca06da1357'))
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: 'add810b0-3224-11e8-a572-ffca06da1357',
type: 'visualization',
@@ -210,7 +218,7 @@ export default function ({ getService }: FtrProviderContext) {
.get(relationshipsUrl('dashboard', 'b70c7ae0-3224-11e8-a572-ffca06da1357', ['search']))
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: 'add810b0-3224-11e8-a572-ffca06da1357',
type: 'visualization',
@@ -246,8 +254,7 @@ export default function ({ getService }: FtrProviderContext) {
]);
});
- // TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail.
- it.skip('should return 404 if dashboard finds no results', async () => {
+ it('should return 404 if dashboard finds no results', async () => {
await supertest
.get(relationshipsUrl('dashboard', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'))
.expect(404);
@@ -270,7 +277,7 @@ export default function ({ getService }: FtrProviderContext) {
.get(relationshipsUrl('visualization', 'a42c0580-3224-11e8-a572-ffca06da1357'))
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: '960372e0-3224-11e8-a572-ffca06da1357',
type: 'search',
@@ -313,7 +320,7 @@ export default function ({ getService }: FtrProviderContext) {
)
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: '960372e0-3224-11e8-a572-ffca06da1357',
type: 'search',
@@ -356,7 +363,7 @@ export default function ({ getService }: FtrProviderContext) {
.get(relationshipsUrl('index-pattern', '8963ca30-3224-11e8-a572-ffca06da1357'))
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: '960372e0-3224-11e8-a572-ffca06da1357',
type: 'search',
@@ -399,7 +406,7 @@ export default function ({ getService }: FtrProviderContext) {
)
.expect(200);
- expect(resp.body).to.eql([
+ expect(resp.body.relations).to.eql([
{
id: '960372e0-3224-11e8-a572-ffca06da1357',
type: 'search',
@@ -425,5 +432,48 @@ export default function ({ getService }: FtrProviderContext) {
.expect(404);
});
});
+
+ describe('invalid references', () => {
+ it('should validate the response schema', async () => {
+ const resp = await supertest.get(relationshipsUrl('dashboard', 'invalid-refs')).expect(200);
+
+ expect(() => {
+ responseSchema.validate(resp.body);
+ }).not.to.throwError();
+ });
+
+ it('should return the invalid relations', async () => {
+ const resp = await supertest.get(relationshipsUrl('dashboard', 'invalid-refs')).expect(200);
+
+ expect(resp.body).to.eql({
+ invalidRelations: [
+ {
+ error: 'Saved object [visualization/invalid-vis] not found',
+ id: 'invalid-vis',
+ relationship: 'child',
+ type: 'visualization',
+ },
+ ],
+ relations: [
+ {
+ id: 'add810b0-3224-11e8-a572-ffca06da1357',
+ meta: {
+ editUrl:
+ '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357',
+ icon: 'visualizeApp',
+ inAppUrl: {
+ path: '/app/visualize#/edit/add810b0-3224-11e8-a572-ffca06da1357',
+ uiCapabilitiesPath: 'visualize.show',
+ },
+ namespaceType: 'single',
+ title: 'Visualization',
+ },
+ relationship: 'child',
+ type: 'visualization',
+ },
+ ],
+ });
+ });
+ });
});
}
diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json
new file mode 100644
index 0000000000000..21d84c4b55e55
--- /dev/null
+++ b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json
@@ -0,0 +1,190 @@
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "timelion-sheet:190f3e90-2ec3-11e8-ba48-69fc4e41e1f6",
+ "source": {
+ "type": "timelion-sheet",
+ "updated_at": "2018-03-23T17:53:30.872Z",
+ "timelion-sheet": {
+ "title": "New TimeLion Sheet",
+ "hits": 0,
+ "description": "",
+ "timelion_sheet": [
+ ".es(*)"
+ ],
+ "timelion_interval": "auto",
+ "timelion_chart_height": 275,
+ "timelion_columns": 2,
+ "timelion_rows": 2,
+ "version": 1
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "index-pattern:8963ca30-3224-11e8-a572-ffca06da1357",
+ "source": {
+ "type": "index-pattern",
+ "updated_at": "2018-03-28T01:08:34.290Z",
+ "index-pattern": {
+ "title": "saved_objects*",
+ "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]"
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "config:7.0.0-alpha1",
+ "source": {
+ "type": "config",
+ "updated_at": "2018-03-28T01:08:39.248Z",
+ "config": {
+ "buildNum": 8467,
+ "telemetry:optIn": false,
+ "defaultIndex": "8963ca30-3224-11e8-a572-ffca06da1357"
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "search:960372e0-3224-11e8-a572-ffca06da1357",
+ "source": {
+ "type": "search",
+ "updated_at": "2018-03-28T01:08:55.182Z",
+ "search": {
+ "title": "OneRecord",
+ "description": "",
+ "hits": 0,
+ "columns": [
+ "_source"
+ ],
+ "sort": [
+ "_score",
+ "desc"
+ ],
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"index\":\"8963ca30-3224-11e8-a572-ffca06da1357\",\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"id:3\",\"language\":\"lucene\"},\"filter\":[]}"
+ }
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "visualization:a42c0580-3224-11e8-a572-ffca06da1357",
+ "source": {
+ "type": "visualization",
+ "updated_at": "2018-03-28T01:09:18.936Z",
+ "visualization": {
+ "title": "VisualizationFromSavedSearch",
+ "visState": "{\"title\":\"VisualizationFromSavedSearch\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMeticsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}",
+ "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}",
+ "description": "",
+ "savedSearchId": "960372e0-3224-11e8-a572-ffca06da1357",
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ }
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "visualization:add810b0-3224-11e8-a572-ffca06da1357",
+ "source": {
+ "type": "visualization",
+ "updated_at": "2018-03-28T01:09:35.163Z",
+ "visualization": {
+ "title": "Visualization",
+ "visState": "{\"title\":\"Visualization\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}",
+ "uiStateJSON": "{}",
+ "description": "",
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"index\":\"8963ca30-3224-11e8-a572-ffca06da1357\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ }
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "dashboard:b70c7ae0-3224-11e8-a572-ffca06da1357",
+ "source": {
+ "type": "dashboard",
+ "updated_at": "2018-03-28T01:09:50.606Z",
+ "dashboard": {
+ "title": "Dashboard",
+ "hits": 0,
+ "description": "",
+ "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0-alpha1\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"add810b0-3224-11e8-a572-ffca06da1357\",\"embeddableConfig\":{}},{\"gridData\":{\"w\":24,\"h\":15,\"x\":24,\"y\":0,\"i\":\"2\"},\"version\":\"7.0.0-alpha1\",\"panelIndex\":\"2\",\"type\":\"visualization\",\"id\":\"a42c0580-3224-11e8-a572-ffca06da1357\",\"embeddableConfig\":{}}]",
+ "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}",
+ "version": 1,
+ "timeRestore": false,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}"
+ }
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "id": "dashboard:invalid-refs",
+ "source": {
+ "type": "dashboard",
+ "updated_at": "2018-03-28T01:09:50.606Z",
+ "dashboard": {
+ "title": "Dashboard",
+ "hits": 0,
+ "description": "",
+ "panelsJSON": "[]",
+ "optionsJSON": "{}",
+ "version": 1,
+ "timeRestore": false,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{}"
+ }
+ },
+ "references": [
+ {
+ "type":"visualization",
+ "id": "add810b0-3224-11e8-a572-ffca06da1357",
+ "name": "valid-ref"
+ },
+ {
+ "type":"visualization",
+ "id": "invalid-vis",
+ "name": "missing-ref"
+ }
+ ]
+ }
+ }
+}
diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json.gz b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json.gz
deleted file mode 100644
index 0834567abb66b663079894089ed4edd91f1cf0b3..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 1385
zcmV-v1(y0BiwFP!000026V+JVZ`(Eyf6rfGXfIn48~X5vthb^?hYW2R#6}+$2LUCX
zWv;U5QB=~@(Eq+8C0mrECGvtGnI97Qcs$m8BGZD22gy7Lt@`B_*dyDA^hk#?yYb0+4|-wU-`D?Y;|<*LNK7`ym-mNf3G{|YrRCa=-?zQK>&=}>F!BP=9{3aY&szV$
zPJNPIlZig;9PWB^RQ!yJy;cWFiU@hL0v4~-Deh#{s>PFhohtX;wq?QZ4%co$WMx=R
zB`i*Me~Xji3UJ=YzUfFYxa+g~mtVvi|tywT)oz%*<=
zi5GuvJAv&7-f-YfZ38b&GwpE6$Sqpr;a?ER?46mNC4+>j8?~;s3o9jSSXjZrx?yx-
zoi4PmT98S>(pbwPo~IIpHa?f20#pu`B*{RDfCx-=n5d0XmXn12Br5R%8M=`@
z@}CLbhRu!`o(7ITn0jLa!%Z{oQ2u7>C=%5$m^G`Xv^A4>dX;v)U*G*>2Ab4gu{Me}
zM38k>=5_<(qRgYC8^Ma-UEr+BNOFnerr8g01%b(;?7c$HXSju=v5wVInk;MUtU_j*
zCkZZ7CJ@;r!j!0}OwPF^iD5>n@1OECDm!PsE@6e8M;)dHHPzB^$?-
z?M*kg6|9LCDn4e>!6g(0Le;qIoaw7Jstj+xx-H}8jtv+;9r-G&Q+TF9egr4K5YHH8
z{cqgx2rs+_6Hw|qcK9kx;9)l#d(UBl<4eC;>#aD)Dx!4Gc_P`ynCU3}3^AnU?AK;z
z_gIkzW>#XJzi?{K$aw~rhyXB&0jqKX
zJa<^$+w06QBYQBm%~_*1(atU(9~^P?Z)F>jVs-73tAHE}MnB@)V3{9PZthSGn5rm8
z`0%4D)BEZ_+u^=w44ezge2uHHjc1+h!Q(X9?e+ojRVCGh^vkNlw_r+D<$ciabY&Ht
zc8p0&nnAh82jzARs>4kCNKn^i4!O>3W>hF8;`(&bg?DMtAR@76`}lotR1KQp@|
diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/mappings.json b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/mappings.json
index c670508247b1a..6dd4d198e0f67 100644
--- a/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/mappings.json
+++ b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/mappings.json
@@ -12,6 +12,20 @@
"mappings": {
"dynamic": "strict",
"properties": {
+ "references": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
+ },
"config": {
"dynamic": "true",
"properties": {
@@ -280,4 +294,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/test/functional/apps/saved_objects_management/index.ts b/test/functional/apps/saved_objects_management/index.ts
index 9491661de73ef..5e4eaefb7e9d1 100644
--- a/test/functional/apps/saved_objects_management/index.ts
+++ b/test/functional/apps/saved_objects_management/index.ts
@@ -12,5 +12,6 @@ export default function savedObjectsManagementApp({ loadTestFile }: FtrProviderC
describe('saved objects management', function savedObjectsManagementAppTestSuite() {
this.tags('ciGroup7');
loadTestFile(require.resolve('./edit_saved_object'));
+ loadTestFile(require.resolve('./show_relationships'));
});
}
diff --git a/test/functional/apps/saved_objects_management/show_relationships.ts b/test/functional/apps/saved_objects_management/show_relationships.ts
new file mode 100644
index 0000000000000..6f3fb5a4973e2
--- /dev/null
+++ b/test/functional/apps/saved_objects_management/show_relationships.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function ({ getPageObjects, getService }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const PageObjects = getPageObjects(['common', 'settings', 'savedObjects']);
+
+ describe('saved objects relationships flyout', () => {
+ beforeEach(async () => {
+ await esArchiver.load('saved_objects_management/show_relationships');
+ });
+
+ afterEach(async () => {
+ await esArchiver.unload('saved_objects_management/show_relationships');
+ });
+
+ it('displays the invalid references', async () => {
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaSavedObjects();
+
+ const objects = await PageObjects.savedObjects.getRowTitles();
+ expect(objects.includes('Dashboard with missing refs')).to.be(true);
+
+ await PageObjects.savedObjects.clickRelationshipsByTitle('Dashboard with missing refs');
+
+ const invalidRelations = await PageObjects.savedObjects.getInvalidRelations();
+
+ expect(invalidRelations).to.eql([
+ {
+ error: 'Saved object [visualization/missing-vis-ref] not found',
+ id: 'missing-vis-ref',
+ relationship: 'Child',
+ type: 'visualization',
+ },
+ {
+ error: 'Saved object [dashboard/missing-dashboard-ref] not found',
+ id: 'missing-dashboard-ref',
+ relationship: 'Child',
+ type: 'dashboard',
+ },
+ ]);
+ });
+ });
+}
diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/data.json
new file mode 100644
index 0000000000000..4d5b969a3c931
--- /dev/null
+++ b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/data.json
@@ -0,0 +1,36 @@
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "type": "doc",
+ "id": "dashboard:dash-with-missing-refs",
+ "source": {
+ "dashboard": {
+ "title": "Dashboard with missing refs",
+ "hits": 0,
+ "description": "",
+ "panelsJSON": "[]",
+ "optionsJSON": "{}",
+ "version": 1,
+ "timeRestore": false,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{}"
+ }
+ },
+ "type": "dashboard",
+ "references": [
+ {
+ "type": "visualization",
+ "id": "missing-vis-ref",
+ "name": "some missing ref"
+ },
+ {
+ "type": "dashboard",
+ "id": "missing-dashboard-ref",
+ "name": "some other missing ref"
+ }
+ ],
+ "updated_at": "2019-01-22T19:32:47.232Z"
+ }
+ }
+}
diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json
new file mode 100644
index 0000000000000..d53e6c96e883e
--- /dev/null
+++ b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json
@@ -0,0 +1,473 @@
+{
+ "type": "index",
+ "value": {
+ "index": ".kibana",
+ "settings": {
+ "index": {
+ "number_of_shards": "1",
+ "auto_expand_replicas": "0-1",
+ "number_of_replicas": "0"
+ }
+ },
+ "mappings": {
+ "dynamic": "strict",
+ "properties": {
+ "references": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
+ },
+ "apm-telemetry": {
+ "properties": {
+ "has_any_services": {
+ "type": "boolean"
+ },
+ "services_per_agent": {
+ "properties": {
+ "go": {
+ "type": "long",
+ "null_value": 0
+ },
+ "java": {
+ "type": "long",
+ "null_value": 0
+ },
+ "js-base": {
+ "type": "long",
+ "null_value": 0
+ },
+ "nodejs": {
+ "type": "long",
+ "null_value": 0
+ },
+ "python": {
+ "type": "long",
+ "null_value": 0
+ },
+ "ruby": {
+ "type": "long",
+ "null_value": 0
+ }
+ }
+ }
+ }
+ },
+ "canvas-workpad": {
+ "dynamic": "false",
+ "properties": {
+ "@created": {
+ "type": "date"
+ },
+ "@timestamp": {
+ "type": "date"
+ },
+ "id": {
+ "type": "text",
+ "index": false
+ },
+ "name": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "config": {
+ "dynamic": "true",
+ "properties": {
+ "accessibility:disableAnimations": {
+ "type": "boolean"
+ },
+ "buildNum": {
+ "type": "keyword"
+ },
+ "dateFormat:tz": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ },
+ "defaultIndex": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ },
+ "telemetry:optIn": {
+ "type": "boolean"
+ }
+ }
+ },
+ "dashboard": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "optionsJSON": {
+ "type": "text"
+ },
+ "panelsJSON": {
+ "type": "text"
+ },
+ "refreshInterval": {
+ "properties": {
+ "display": {
+ "type": "keyword"
+ },
+ "pause": {
+ "type": "boolean"
+ },
+ "section": {
+ "type": "integer"
+ },
+ "value": {
+ "type": "integer"
+ }
+ }
+ },
+ "timeFrom": {
+ "type": "keyword"
+ },
+ "timeRestore": {
+ "type": "boolean"
+ },
+ "timeTo": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "map": {
+ "properties": {
+ "bounds": {
+ "type": "geo_shape",
+ "tree": "quadtree"
+ },
+ "description": {
+ "type": "text"
+ },
+ "layerListJSON": {
+ "type": "text"
+ },
+ "mapStateJSON": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "graph-workspace": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "numLinks": {
+ "type": "integer"
+ },
+ "numVertices": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ },
+ "wsState": {
+ "type": "text"
+ }
+ }
+ },
+ "index-pattern": {
+ "properties": {
+ "fieldFormatMap": {
+ "type": "text"
+ },
+ "fields": {
+ "type": "text"
+ },
+ "intervalName": {
+ "type": "keyword"
+ },
+ "notExpandable": {
+ "type": "boolean"
+ },
+ "sourceFilters": {
+ "type": "text"
+ },
+ "timeFieldName": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "typeMeta": {
+ "type": "keyword"
+ }
+ }
+ },
+ "kql-telemetry": {
+ "properties": {
+ "optInCount": {
+ "type": "long"
+ },
+ "optOutCount": {
+ "type": "long"
+ }
+ }
+ },
+ "migrationVersion": {
+ "dynamic": "true",
+ "properties": {
+ "index-pattern": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ },
+ "space": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ }
+ }
+ },
+ "namespace": {
+ "type": "keyword"
+ },
+ "search": {
+ "properties": {
+ "columns": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "sort": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "server": {
+ "properties": {
+ "uuid": {
+ "type": "keyword"
+ }
+ }
+ },
+ "space": {
+ "properties": {
+ "_reserved": {
+ "type": "boolean"
+ },
+ "color": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "disabledFeatures": {
+ "type": "keyword"
+ },
+ "initials": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 2048
+ }
+ }
+ }
+ }
+ },
+ "spaceId": {
+ "type": "keyword"
+ },
+ "telemetry": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ }
+ }
+ },
+ "timelion-sheet": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "timelion_chart_height": {
+ "type": "integer"
+ },
+ "timelion_columns": {
+ "type": "integer"
+ },
+ "timelion_interval": {
+ "type": "keyword"
+ },
+ "timelion_other_interval": {
+ "type": "keyword"
+ },
+ "timelion_rows": {
+ "type": "integer"
+ },
+ "timelion_sheet": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "url": {
+ "properties": {
+ "accessCount": {
+ "type": "long"
+ },
+ "accessDate": {
+ "type": "date"
+ },
+ "createDate": {
+ "type": "date"
+ },
+ "url": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 2048
+ }
+ }
+ }
+ }
+ },
+ "visualization": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "savedSearchId": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ },
+ "visState": {
+ "type": "text"
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts
index 1cdf76ad58ef0..cf162f12df9d9 100644
--- a/test/functional/page_objects/management/saved_objects_page.ts
+++ b/test/functional/page_objects/management/saved_objects_page.ts
@@ -257,6 +257,22 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv
});
}
+ async getInvalidRelations() {
+ const rows = await testSubjects.findAll('invalidRelationshipsTableRow');
+ return mapAsync(rows, async (row) => {
+ const objectType = await row.findByTestSubject('relationshipsObjectType');
+ const objectId = await row.findByTestSubject('relationshipsObjectId');
+ const relationship = await row.findByTestSubject('directRelationship');
+ const error = await row.findByTestSubject('relationshipsError');
+ return {
+ type: await objectType.getVisibleText(),
+ id: await objectId.getVisibleText(),
+ relationship: await relationship.getVisibleText(),
+ error: await error.getVisibleText(),
+ };
+ });
+ }
+
async getTableSummary() {
const table = await testSubjects.find('savedObjectsTable');
const $ = await table.parseDomContent();
From 61a51b568481abfba41f71781d24acfd4f65c7ee Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Mon, 1 Feb 2021 11:14:46 +0100
Subject: [PATCH 19/85] [ILM] New copy for rollover and small refactor for
timeline (#89422)
* refactor timeline and relative ms calculation logic for easier use outside of edit_policy section
* further refactor, move child component to own file in timeline, and clean up public API for relative timing calculation
* added copy to call out variation in timing (slop) introduced by rollover
* use separate copy for timeline
* remove unused import
* fix unresolved merge
* implement copy feedback
* added component integration for showing/hiding hot phase icon on timeline
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../edit_policy/edit_policy.helpers.tsx | 1 +
.../edit_policy/edit_policy.test.ts | 8 +
.../components/phases/hot_phase/hot_phase.tsx | 5 +
.../components/timeline/components/index.ts | 7 +
.../components/timeline_phase_text.tsx | 28 ++
.../edit_policy/components/timeline/index.ts | 2 +-
.../timeline/timeline.container.tsx | 33 +++
.../components/timeline/timeline.scss | 4 +
.../components/timeline/timeline.tsx | 252 ++++++++++--------
.../sections/edit_policy/i18n_texts.ts | 7 +
...absolute_timing_to_relative_timing.test.ts | 9 +-
.../lib/absolute_timing_to_relative_timing.ts | 78 +++---
.../sections/edit_policy/lib/index.ts | 5 +-
13 files changed, 288 insertions(+), 151 deletions(-)
create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/index.ts
create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx
create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
index 64b654b030236..d9256ec916ec8 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
@@ -251,6 +251,7 @@ export const setup = async (arg?: { appServicesContext: Partial exists('timelineHotPhaseRolloverToolTip'),
hasHotPhase: () => exists('ilmTimelineHotPhase'),
hasWarmPhase: () => exists('ilmTimelineWarmPhase'),
hasColdPhase: () => exists('ilmTimelineColdPhase'),
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
index bb96e8b4df239..05793a4bed581 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
@@ -843,5 +843,13 @@ describe('', () => {
expect(actions.timeline.hasColdPhase()).toBe(true);
expect(actions.timeline.hasDeletePhase()).toBe(true);
});
+
+ test('show and hide rollover indicator on timeline', async () => {
+ const { actions } = testBed;
+ expect(actions.timeline.hasRolloverIndicator()).toBe(true);
+ await actions.hot.toggleDefaultRollover(false);
+ await actions.hot.toggleRollover(false);
+ expect(actions.timeline.hasRolloverIndicator()).toBe(false);
+ });
});
});
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
index fb7c9a80acba0..02de47f8c56ef 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
@@ -16,6 +16,7 @@ import {
EuiTextColor,
EuiSwitch,
EuiIconTip,
+ EuiIcon,
} from '@elastic/eui';
import { useFormData, UseField, SelectField, NumericField } from '../../../../../../shared_imports';
@@ -80,6 +81,10 @@ export const HotPhase: FunctionComponent = () => {
+
+
+ {i18nTexts.editPolicy.rolloverOffsetsHotPhaseTiming}
+
path={isUsingDefaultRolloverPath}>
{(field) => (
<>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/index.ts
new file mode 100644
index 0000000000000..1c9d5e1abc316
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/index.ts
@@ -0,0 +1,7 @@
+/*
+ * 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.
+ */
+
+export { TimelinePhaseText } from './timeline_phase_text';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx
new file mode 100644
index 0000000000000..a44e0f2407c52
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx
@@ -0,0 +1,28 @@
+/*
+ * 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 React, { FunctionComponent, ReactNode } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
+
+export const TimelinePhaseText: FunctionComponent<{
+ phaseName: ReactNode | string;
+ durationInPhase?: ReactNode | string;
+}> = ({ phaseName, durationInPhase }) => (
+
+
+
+ {phaseName}
+
+
+
+ {typeof durationInPhase === 'string' ? (
+ {durationInPhase}
+ ) : (
+ durationInPhase
+ )}
+
+
+);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts
index 4664429db37d7..7bcaa6584edf0 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/index.ts
@@ -3,4 +3,4 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-export { Timeline } from './timeline';
+export { Timeline } from './timeline.container';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx
new file mode 100644
index 0000000000000..75f53fcb25091
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx
@@ -0,0 +1,33 @@
+/*
+ * 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 React, { FunctionComponent } from 'react';
+
+import { useFormData } from '../../../../../shared_imports';
+
+import { formDataToAbsoluteTimings } from '../../lib';
+
+import { useConfigurationIssues } from '../../form';
+
+import { FormInternal } from '../../types';
+
+import { Timeline as ViewComponent } from './timeline';
+
+export const Timeline: FunctionComponent = () => {
+ const [formData] = useFormData();
+ const timings = formDataToAbsoluteTimings(formData);
+ const { isUsingRollover } = useConfigurationIssues();
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss
index 452221a29a991..7d65d2cd6b212 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss
@@ -84,4 +84,8 @@ $ilmDeletePhaseColor: shadeOrTint($euiColorVis5, 40%, 40%);
background-color: $euiColorVis1;
}
}
+
+ &__rolloverIcon {
+ display: inline-block;
+ }
}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx
index 40bab9c676de2..2e2db88e1384d 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx
@@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
-import React, { FunctionComponent, useMemo } from 'react';
+import React, { FunctionComponent, memo } from 'react';
import {
- EuiText,
EuiIcon,
EuiIconProps,
EuiFlexGroup,
@@ -16,18 +15,19 @@ import {
} from '@elastic/eui';
import { PhasesExceptDelete } from '../../../../../../common/types';
-import { useFormData } from '../../../../../shared_imports';
-
-import { FormInternal } from '../../types';
import {
- calculateRelativeTimingMs,
+ calculateRelativeFromAbsoluteMilliseconds,
normalizeTimingsToHumanReadable,
PhaseAgeInMilliseconds,
+ AbsoluteTimings,
} from '../../lib';
import './timeline.scss';
import { InfinityIconSvg } from './infinity_icon.svg';
+import { TimelinePhaseText } from './components';
+
+const exists = (v: unknown) => v != null;
const InfinityIcon: FunctionComponent> = (props) => (
@@ -56,6 +56,13 @@ const i18nTexts = {
hotPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.hotPhaseSectionTitle', {
defaultMessage: 'Hot phase',
}),
+ rolloverTooltip: i18n.translate(
+ 'xpack.indexLifecycleMgmt.timeline.hotPhaseRolloverToolTipContent',
+ {
+ defaultMessage:
+ 'How long it takes to reach the rollover criteria in the hot phase can vary. Data moves to the next phase when the time since rollover reaches the minimum age.',
+ }
+ ),
warmPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.warmPhaseSectionTitle', {
defaultMessage: 'Warm phase',
}),
@@ -88,121 +95,136 @@ const calculateWidths = (inputs: PhaseAgeInMilliseconds) => {
};
};
-const TimelinePhaseText: FunctionComponent<{
- phaseName: string;
- durationInPhase?: React.ReactNode | string;
-}> = ({ phaseName, durationInPhase }) => (
-
-
-
- {phaseName}
-
-
-
- {typeof durationInPhase === 'string' ? (
- {durationInPhase}
- ) : (
- durationInPhase
- )}
-
-
-);
-
-export const Timeline: FunctionComponent = () => {
- const [formData] = useFormData();
-
- const phaseTimingInMs = useMemo(() => {
- return calculateRelativeTimingMs(formData);
- }, [formData]);
+interface Props {
+ hasDeletePhase: boolean;
+ /**
+ * For now we assume the hot phase does not have a min age
+ */
+ hotPhaseMinAge: undefined;
+ isUsingRollover: boolean;
+ warmPhaseMinAge?: string;
+ coldPhaseMinAge?: string;
+ deletePhaseMinAge?: string;
+}
- const humanReadableTimings = useMemo(() => normalizeTimingsToHumanReadable(phaseTimingInMs), [
- phaseTimingInMs,
- ]);
-
- const widths = calculateWidths(phaseTimingInMs);
-
- const getDurationInPhaseContent = (phase: PhasesExceptDelete): string | React.ReactNode =>
- phaseTimingInMs.phases[phase] === Infinity ? (
-
- ) : (
- humanReadableTimings[phase]
- );
-
- return (
-
-
-
-
- {i18n.translate('xpack.indexLifecycleMgmt.timeline.title', {
- defaultMessage: 'Policy Timeline',
- })}
-
-
-
-
- {
- if (el) {
- el.style.setProperty('--ilm-timeline-hot-phase-width', widths.hot);
- el.style.setProperty('--ilm-timeline-warm-phase-width', widths.warm ?? null);
- el.style.setProperty('--ilm-timeline-cold-phase-width', widths.cold ?? null);
- }
- }}
- >
-
-
-
- {/* These are the actual color bars for the timeline */}
-
- {formData._meta?.warm.enabled && (
+/**
+ * Display a timeline given ILM policy phase information. This component is re-usable and memo-ized
+ * and should not rely directly on any application-specific context.
+ */
+export const Timeline: FunctionComponent
= memo(
+ ({ hasDeletePhase, isUsingRollover, ...phasesMinAge }) => {
+ const absoluteTimings: AbsoluteTimings = {
+ hot: { min_age: phasesMinAge.hotPhaseMinAge },
+ warm: phasesMinAge.warmPhaseMinAge ? { min_age: phasesMinAge.warmPhaseMinAge } : undefined,
+ cold: phasesMinAge.coldPhaseMinAge ? { min_age: phasesMinAge.coldPhaseMinAge } : undefined,
+ delete: phasesMinAge.deletePhaseMinAge
+ ? { min_age: phasesMinAge.deletePhaseMinAge }
+ : undefined,
+ };
+
+ const phaseAgeInMilliseconds = calculateRelativeFromAbsoluteMilliseconds(absoluteTimings);
+ const humanReadableTimings = normalizeTimingsToHumanReadable(phaseAgeInMilliseconds);
+
+ const widths = calculateWidths(phaseAgeInMilliseconds);
+
+ const getDurationInPhaseContent = (phase: PhasesExceptDelete): string | React.ReactNode =>
+ phaseAgeInMilliseconds.phases[phase] === Infinity ? (
+
+ ) : (
+ humanReadableTimings[phase]
+ );
+
+ return (
+
+
+
+
+ {i18n.translate('xpack.indexLifecycleMgmt.timeline.title', {
+ defaultMessage: 'Policy Timeline',
+ })}
+
+
+
+
+ {
+ if (el) {
+ el.style.setProperty('--ilm-timeline-hot-phase-width', widths.hot);
+ el.style.setProperty('--ilm-timeline-warm-phase-width', widths.warm ?? null);
+ el.style.setProperty('--ilm-timeline-cold-phase-width', widths.cold ?? null);
+ }
+ }}
+ >
+
+
+
+ {/* These are the actual color bars for the timeline */}
-
+
+ {i18nTexts.hotPhase}
+
+
+
+
+ >
+ ) : (
+ i18nTexts.hotPhase
+ )
+ }
+ durationInPhase={getDurationInPhaseContent('hot')}
/>
- )}
- {formData._meta?.cold.enabled && (
+ {exists(phaseAgeInMilliseconds.phases.warm) && (
+
+ )}
+ {exists(phaseAgeInMilliseconds.phases.cold) && (
+
+ )}
+
+
+ {hasDeletePhase && (
+
- )}
-
-
- {formData._meta?.delete.enabled && (
-
-
-
-
-
- )}
-
-
-
-
- );
-};
+
+ )}
+
+
+
+
+ );
+ }
+);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
index 71085a6d7a2b8..cf8c92b8333d0 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
@@ -11,6 +11,13 @@ export const i18nTexts = {
shrinkLabel: i18n.translate('xpack.indexLifecycleMgmt.shrink.indexFieldLabel', {
defaultMessage: 'Shrink index',
}),
+ rolloverOffsetsHotPhaseTiming: i18n.translate(
+ 'xpack.indexLifecycleMgmt.rollover.rolloverOffsetsPhaseTimingDescription',
+ {
+ defaultMessage:
+ 'How long it takes to reach the rollover criteria in the hot phase can vary. Data moves to the next phase when the time since rollover reaches the minimum age.',
+ }
+ ),
searchableSnapshotInHotPhase: {
searchableSnapshotDisallowed: {
calloutTitle: i18n.translate(
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts
index 28910871fa33b..405de2b55a2f7 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts
@@ -4,13 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { flow } from 'fp-ts/function';
import { deserializer } from '../form';
import {
+ formDataToAbsoluteTimings,
+ calculateRelativeFromAbsoluteMilliseconds,
absoluteTimingToRelativeTiming,
- calculateRelativeTimingMs,
} from './absolute_timing_to_relative_timing';
+export const calculateRelativeTimingMs = flow(
+ formDataToAbsoluteTimings,
+ calculateRelativeFromAbsoluteMilliseconds
+);
+
describe('Conversion of absolute policy timing to relative timing', () => {
describe('calculateRelativeTimingMs', () => {
describe('policy that never deletes data (keep forever)', () => {
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts
index 2f37608b2d7ae..a44863b2f1ce2 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts
@@ -14,16 +14,21 @@
*
* This code converts the absolute timings to _relative_ timings of the form: 30 days in hot phase,
* 40 days in warm phase then forever in cold phase.
+ *
+ * All functions exported from this file can be viewed as utilities for working with form data and
+ * other defined interfaces to calculate the relative amount of time data will spend in a phase.
*/
import moment from 'moment';
-import { flow } from 'fp-ts/lib/function';
import { i18n } from '@kbn/i18n';
+import { flow } from 'fp-ts/function';
import { splitSizeAndUnits } from '../../../lib/policies';
import { FormInternal } from '../types';
+/* -===- Private functions and types -===- */
+
type MinAgePhase = 'warm' | 'cold' | 'delete';
type Phase = 'hot' | MinAgePhase;
@@ -43,7 +48,34 @@ const i18nTexts = {
}),
};
-interface AbsoluteTimings {
+const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'delete'];
+
+const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({
+ min_age: formData.phases?.[phase]?.min_age
+ ? formData.phases[phase]!.min_age! + formData._meta[phase].minAgeUnit
+ : '0ms',
+});
+
+/**
+ * See https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#date-math
+ * for all date math values. ILM policies also support "micros" and "nanos".
+ */
+const getPhaseMinAgeInMilliseconds = (phase: { min_age: string }): number => {
+ let milliseconds: number;
+ const { units, size } = splitSizeAndUnits(phase.min_age);
+ if (units === 'micros') {
+ milliseconds = parseInt(size, 10) / 1e3;
+ } else if (units === 'nanos') {
+ milliseconds = parseInt(size, 10) / 1e6;
+ } else {
+ milliseconds = moment.duration(size, units as any).asMilliseconds();
+ }
+ return milliseconds;
+};
+
+/* -===- Public functions and types -===- */
+
+export interface AbsoluteTimings {
hot: {
min_age: undefined;
};
@@ -67,16 +99,7 @@ export interface PhaseAgeInMilliseconds {
};
}
-const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'delete'];
-
-const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({
- min_age:
- formData.phases && formData.phases[phase]?.min_age
- ? formData.phases[phase]!.min_age! + formData._meta[phase].minAgeUnit
- : '0ms',
-});
-
-const formDataToAbsoluteTimings = (formData: FormInternal): AbsoluteTimings => {
+export const formDataToAbsoluteTimings = (formData: FormInternal): AbsoluteTimings => {
const { _meta } = formData;
if (!_meta) {
return { hot: { min_age: undefined } };
@@ -89,28 +112,13 @@ const formDataToAbsoluteTimings = (formData: FormInternal): AbsoluteTimings => {
};
};
-/**
- * See https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#date-math
- * for all date math values. ILM policies also support "micros" and "nanos".
- */
-const getPhaseMinAgeInMilliseconds = (phase: { min_age: string }): number => {
- let milliseconds: number;
- const { units, size } = splitSizeAndUnits(phase.min_age);
- if (units === 'micros') {
- milliseconds = parseInt(size, 10) / 1e3;
- } else if (units === 'nanos') {
- milliseconds = parseInt(size, 10) / 1e6;
- } else {
- milliseconds = moment.duration(size, units as any).asMilliseconds();
- }
- return milliseconds;
-};
-
/**
* Given a set of phase minimum age absolute timings, like hot phase 0ms and warm phase 3d, work out
* the number of milliseconds data will reside in phase.
*/
-const calculateMilliseconds = (inputs: AbsoluteTimings): PhaseAgeInMilliseconds => {
+export const calculateRelativeFromAbsoluteMilliseconds = (
+ inputs: AbsoluteTimings
+): PhaseAgeInMilliseconds => {
return phaseOrder.reduce(
(acc, phaseName, idx) => {
// Delete does not have an age associated with it
@@ -152,6 +160,8 @@ const calculateMilliseconds = (inputs: AbsoluteTimings): PhaseAgeInMilliseconds
);
};
+export type RelativePhaseTimingInMs = ReturnType;
+
const millisecondsToDays = (milliseconds?: number): string | undefined => {
if (milliseconds == null) {
return;
@@ -177,10 +187,12 @@ export const normalizeTimingsToHumanReadable = ({
};
};
-export const calculateRelativeTimingMs = flow(formDataToAbsoluteTimings, calculateMilliseconds);
-
+/**
+ * Given {@link FormInternal}, extract the min_age values for each phase and calculate
+ * human readable strings for communicating how long data will remain in a phase.
+ */
export const absoluteTimingToRelativeTiming = flow(
formDataToAbsoluteTimings,
- calculateMilliseconds,
+ calculateRelativeFromAbsoluteMilliseconds,
normalizeTimingsToHumanReadable
);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts
index 9593fcc810a6f..a9372c99a72fc 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts
@@ -6,7 +6,10 @@
export {
absoluteTimingToRelativeTiming,
- calculateRelativeTimingMs,
+ calculateRelativeFromAbsoluteMilliseconds,
normalizeTimingsToHumanReadable,
+ formDataToAbsoluteTimings,
+ AbsoluteTimings,
PhaseAgeInMilliseconds,
+ RelativePhaseTimingInMs,
} from './absolute_timing_to_relative_timing';
From 19b1f46611d05a9e494b1fe107bf103d417a0456 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Mon, 1 Feb 2021 12:43:06 +0200
Subject: [PATCH 20/85] Fixes flakiness on timelion suggestions (#89538)
* Fixes flakiness on timelion suggestions
* Improvements
* Remove flakiness
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
test/functional/apps/timelion/_expression_typeahead.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/test/functional/apps/timelion/_expression_typeahead.js b/test/functional/apps/timelion/_expression_typeahead.js
index 744f8de15e767..3db5cb48dd38b 100644
--- a/test/functional/apps/timelion/_expression_typeahead.js
+++ b/test/functional/apps/timelion/_expression_typeahead.js
@@ -75,18 +75,18 @@ export default function ({ getPageObjects }) {
await PageObjects.timelion.updateExpression(',split');
await PageObjects.timelion.clickSuggestion();
const suggestions = await PageObjects.timelion.getSuggestionItemsText();
- expect(suggestions.length).to.eql(51);
+ expect(suggestions.length).not.to.eql(0);
expect(suggestions[0].includes('@message.raw')).to.eql(true);
- await PageObjects.timelion.clickSuggestion(10, 2000);
+ await PageObjects.timelion.clickSuggestion(10);
});
it('should show field suggestions for metric argument when index pattern set', async () => {
await PageObjects.timelion.updateExpression(',metric');
await PageObjects.timelion.clickSuggestion();
await PageObjects.timelion.updateExpression('avg:');
- await PageObjects.timelion.clickSuggestion(0, 2000);
+ await PageObjects.timelion.clickSuggestion(0);
const suggestions = await PageObjects.timelion.getSuggestionItemsText();
- expect(suggestions.length).to.eql(2);
+ expect(suggestions.length).not.to.eql(0);
expect(suggestions[0].includes('avg:bytes')).to.eql(true);
});
});
From e31b6a8c91e88741238bddc90147a825444640eb Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 1 Feb 2021 11:52:57 +0100
Subject: [PATCH 21/85] [Lens] Add smoke test for lens in canvas (#88657)
---
x-pack/test/functional/apps/canvas/index.js | 1 +
x-pack/test/functional/apps/canvas/lens.ts | 30 ++
x-pack/test/functional/config.js | 1 +
.../es_archives/canvas/lens/data.json | 190 ++++++++
.../es_archives/canvas/lens/mappings.json | 409 ++++++++++++++++++
5 files changed, 631 insertions(+)
create mode 100644 x-pack/test/functional/apps/canvas/lens.ts
create mode 100644 x-pack/test/functional/es_archives/canvas/lens/data.json
create mode 100644 x-pack/test/functional/es_archives/canvas/lens/mappings.json
diff --git a/x-pack/test/functional/apps/canvas/index.js b/x-pack/test/functional/apps/canvas/index.js
index b7031cf0e55da..d5f7540f48c83 100644
--- a/x-pack/test/functional/apps/canvas/index.js
+++ b/x-pack/test/functional/apps/canvas/index.js
@@ -26,6 +26,7 @@ export default function canvasApp({ loadTestFile, getService }) {
loadTestFile(require.resolve('./custom_elements'));
loadTestFile(require.resolve('./feature_controls/canvas_security'));
loadTestFile(require.resolve('./feature_controls/canvas_spaces'));
+ loadTestFile(require.resolve('./lens'));
loadTestFile(require.resolve('./reports'));
});
}
diff --git a/x-pack/test/functional/apps/canvas/lens.ts b/x-pack/test/functional/apps/canvas/lens.ts
new file mode 100644
index 0000000000000..e74795de6c7ea
--- /dev/null
+++ b/x-pack/test/functional/apps/canvas/lens.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function canvasLensTest({ getService, getPageObjects }: FtrProviderContext) {
+ const PageObjects = getPageObjects(['canvas', 'common', 'header', 'lens']);
+ const esArchiver = getService('esArchiver');
+
+ describe('lens in canvas', function () {
+ before(async () => {
+ await esArchiver.load('canvas/lens');
+ // open canvas home
+ await PageObjects.common.navigateToApp('canvas');
+ // load test workpad
+ await PageObjects.common.navigateToApp('canvas', {
+ hash: '/workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31/page/1',
+ });
+ });
+
+ it('renders lens visualization', async () => {
+ await PageObjects.header.waitUntilLoadingHasFinished();
+
+ await PageObjects.lens.assertMetric('Maximum of bytes', '16,788');
+ });
+ });
+}
diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js
index 1815942a06a9a..fc508f8477ebe 100644
--- a/x-pack/test/functional/config.js
+++ b/x-pack/test/functional/config.js
@@ -232,6 +232,7 @@ export default async function ({ readConfigFile }) {
{
feature: {
canvas: ['all'],
+ visualize: ['all'],
},
spaces: ['*'],
},
diff --git a/x-pack/test/functional/es_archives/canvas/lens/data.json b/x-pack/test/functional/es_archives/canvas/lens/data.json
new file mode 100644
index 0000000000000..dca7d31d71082
--- /dev/null
+++ b/x-pack/test/functional/es_archives/canvas/lens/data.json
@@ -0,0 +1,190 @@
+{
+ "type": "doc",
+ "value": {
+ "id": "space:default",
+ "index": ".kibana_1",
+ "source": {
+ "space": {
+ "_reserved": true,
+ "color": "#00bfb3",
+ "description": "This is your default space!",
+ "name": "Default"
+ },
+ "type": "space",
+ "updated_at": "2018-11-06T18:20:26.703Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "canvas-workpad:workpad-1705f884-6224-47de-ba49-ca224fe6ec31",
+ "index": ".kibana_1",
+ "source": {
+ "canvas-workpad": {
+ "@created": "2018-11-19T19:17:12.646Z",
+ "@timestamp": "2018-11-19T19:36:28.499Z",
+ "assets": {
+ },
+ "colors": [
+ "#37988d",
+ "#c19628",
+ "#b83c6f",
+ "#3f9939",
+ "#1785b0",
+ "#ca5f35",
+ "#45bdb0",
+ "#f2bc33",
+ "#e74b8b",
+ "#4fbf48",
+ "#1ea6dc",
+ "#fd7643",
+ "#72cec3",
+ "#f5cc5d",
+ "#ec77a8",
+ "#7acf74",
+ "#4cbce4",
+ "#fd986f",
+ "#a1ded7",
+ "#f8dd91",
+ "#f2a4c5",
+ "#a6dfa2",
+ "#86d2ed",
+ "#fdba9f",
+ "#000000",
+ "#444444",
+ "#777777",
+ "#BBBBBB",
+ "#FFFFFF",
+ "rgba(255,255,255,0)"
+ ],
+ "height": 920,
+ "id": "workpad-1705f884-6224-47de-ba49-ca224fe6ec31",
+ "isWriteable": true,
+ "name": "Test Workpad",
+ "page": 0,
+ "pages": [
+ {
+ "elements": [
+ {
+ "expression": "savedLens id=\"my-lens-vis\" timerange={timerange from=\"2014-01-01\" to=\"2018-01-01\"}",
+ "id": "element-8f64a10a-01f3-4a71-a682-5b627cbe4d0e",
+ "position": {
+ "angle": 0,
+ "height": 238,
+ "left": 33.5,
+ "top": 20,
+ "width": 338
+ }
+ }
+ ],
+ "id": "page-c38cd459-10fe-45f9-847b-2cbd7ec74319",
+ "style": {
+ "background": "#fff"
+ },
+ "transition": {
+ }
+ }
+ ],
+ "width": 840
+ },
+ "type": "canvas-workpad",
+ "updated_at": "2018-11-19T19:36:28.511Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "lens:my-lens-vis",
+ "index": ".kibana_1",
+ "source": {
+ "lens": {
+ "expression": "",
+ "state": {
+ "datasourceMetaData": {
+ "filterableIndexPatterns": [
+ {
+ "id": "logstash-lens",
+ "title": "logstash-lens"
+ }
+ ]
+ },
+ "datasourceStates": {
+ "indexpattern": {
+ "currentIndexPatternId": "logstash-lens",
+ "layers": {
+ "c61a8afb-a185-4fae-a064-fb3846f6c451": {
+ "columnOrder": [
+ "2cd09808-3915-49f4-b3b0-82767eba23f7"
+ ],
+ "columns": {
+ "2cd09808-3915-49f4-b3b0-82767eba23f7": {
+ "dataType": "number",
+ "isBucketed": false,
+ "label": "Maximum of bytes",
+ "operationType": "max",
+ "scale": "ratio",
+ "sourceField": "bytes"
+ }
+ },
+ "indexPatternId": "logstash-lens"
+ }
+ }
+ }
+ },
+ "filters": [],
+ "query": {
+ "language": "kuery",
+ "query": ""
+ },
+ "visualization": {
+ "accessor": "2cd09808-3915-49f4-b3b0-82767eba23f7",
+ "layerId": "c61a8afb-a185-4fae-a064-fb3846f6c451"
+ }
+ },
+ "title": "Artistpreviouslyknownaslens",
+ "visualizationType": "lnsMetric"
+ },
+ "references": [],
+ "type": "lens",
+ "updated_at": "2019-10-16T00:28:08.979Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": "logstash-lens",
+ "id": "1",
+ "source": {
+ "@timestamp": "2015-09-20T02:00:00.000Z",
+ "bytes": 16788
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "index-pattern:logstash-lens",
+ "index": ".kibana_1",
+ "source": {
+ "index-pattern" : {
+ "title" : "logstash-lens",
+ "timeFieldName" : "@timestamp",
+ "fields" : "[]"
+ },
+ "type" : "index-pattern",
+ "references" : [ ],
+ "migrationVersion" : {
+ "index-pattern" : "7.6.0"
+ },
+ "updated_at" : "2020-08-19T08:39:09.998Z"
+ },
+ "type": "_doc"
+ }
+}
\ No newline at end of file
diff --git a/x-pack/test/functional/es_archives/canvas/lens/mappings.json b/x-pack/test/functional/es_archives/canvas/lens/mappings.json
new file mode 100644
index 0000000000000..811bfaaae0d2c
--- /dev/null
+++ b/x-pack/test/functional/es_archives/canvas/lens/mappings.json
@@ -0,0 +1,409 @@
+{
+ "type": "index",
+ "value": {
+ "aliases": {
+ ".kibana": {
+ }
+ },
+ "index": ".kibana_1",
+ "mappings": {
+ "dynamic": "strict",
+ "properties": {
+ "canvas-workpad": {
+ "dynamic": "false",
+ "properties": {
+ "@created": {
+ "type": "date"
+ },
+ "@timestamp": {
+ "type": "date"
+ },
+ "id": {
+ "index": false,
+ "type": "text"
+ },
+ "name": {
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
+ },
+ "config": {
+ "dynamic": "true",
+ "properties": {
+ "buildNum": {
+ "type": "keyword"
+ }
+ }
+ },
+ "references": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
+ },
+ "dashboard": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "optionsJSON": {
+ "type": "text"
+ },
+ "panelsJSON": {
+ "type": "text"
+ },
+ "refreshInterval": {
+ "properties": {
+ "display": {
+ "type": "keyword"
+ },
+ "pause": {
+ "type": "boolean"
+ },
+ "section": {
+ "type": "integer"
+ },
+ "value": {
+ "type": "integer"
+ }
+ }
+ },
+ "timeFrom": {
+ "type": "keyword"
+ },
+ "timeRestore": {
+ "type": "boolean"
+ },
+ "timeTo": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "graph-workspace": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "numLinks": {
+ "type": "integer"
+ },
+ "numVertices": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ },
+ "wsState": {
+ "type": "text"
+ }
+ }
+ },
+ "index-pattern": {
+ "properties": {
+ "fieldFormatMap": {
+ "type": "text"
+ },
+ "fields": {
+ "type": "text"
+ },
+ "intervalName": {
+ "type": "keyword"
+ },
+ "notExpandable": {
+ "type": "boolean"
+ },
+ "sourceFilters": {
+ "type": "text"
+ },
+ "timeFieldName": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "typeMeta": {
+ "type": "keyword"
+ }
+ }
+ },
+ "lens": {
+ "properties": {
+ "expression": {
+ "index": false,
+ "type": "keyword"
+ },
+ "state": {
+ "type": "flattened"
+ },
+ "title": {
+ "type": "text"
+ },
+ "visualizationType": {
+ "type": "keyword"
+ }
+ }
+ },
+ "kql-telemetry": {
+ "properties": {
+ "optInCount": {
+ "type": "long"
+ },
+ "optOutCount": {
+ "type": "long"
+ }
+ }
+ },
+ "migrationVersion": {
+ "dynamic": "true",
+ "type": "object"
+ },
+ "namespace": {
+ "type": "keyword"
+ },
+ "search": {
+ "properties": {
+ "columns": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "sort": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "server": {
+ "properties": {
+ "uuid": {
+ "type": "keyword"
+ }
+ }
+ },
+ "space": {
+ "properties": {
+ "_reserved": {
+ "type": "boolean"
+ },
+ "color": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "initials": {
+ "type": "keyword"
+ },
+ "disabledFeatures": {
+ "type": "keyword"
+ },
+ "name": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 2048,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
+ },
+ "telemetry": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ }
+ }
+ },
+ "timelion-sheet": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "timelion_chart_height": {
+ "type": "integer"
+ },
+ "timelion_columns": {
+ "type": "integer"
+ },
+ "timelion_interval": {
+ "type": "keyword"
+ },
+ "timelion_other_interval": {
+ "type": "keyword"
+ },
+ "timelion_rows": {
+ "type": "integer"
+ },
+ "timelion_sheet": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "url": {
+ "properties": {
+ "accessCount": {
+ "type": "long"
+ },
+ "accessDate": {
+ "type": "date"
+ },
+ "createDate": {
+ "type": "date"
+ },
+ "url": {
+ "fields": {
+ "keyword": {
+ "ignore_above": 2048,
+ "type": "keyword"
+ }
+ },
+ "type": "text"
+ }
+ }
+ },
+ "visualization": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "savedSearchId": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ },
+ "visState": {
+ "type": "text"
+ }
+ }
+ }
+ }
+ },
+ "settings": {
+ "index": {
+ "number_of_replicas": "1",
+ "number_of_shards": "1"
+ }
+ }
+ }
+}
+
+{
+ "type": "index",
+ "value": {
+ "index": "logstash-lens",
+ "mappings": {
+ "properties": {
+ "@timestamp": {
+ "type": "date"
+ },
+ "bytes": {
+ "type": "float"
+ }
+ }
+ },
+ "settings": {
+ "index": {
+ "number_of_shards": "1",
+ "number_of_replicas": "0"
+ }
+ }
+ }
+}
\ No newline at end of file
From 1b8c3c1dcc5c077a61d3c66511a525157374f4f7 Mon Sep 17 00:00:00 2001
From: Marta Bondyra
Date: Mon, 1 Feb 2021 11:54:16 +0100
Subject: [PATCH 22/85] [Lens] Refactor reorder drag and drop (#88578)
---
test/functional/services/common/browser.ts | 2 +-
.../__snapshots__/drag_drop.test.tsx.snap | 24 +-
.../lens/public/drag_drop/drag_drop.scss | 11 +-
.../lens/public/drag_drop/drag_drop.test.tsx | 443 ++++++---
.../lens/public/drag_drop/drag_drop.tsx | 892 +++++++++++-------
.../lens/public/drag_drop/providers.tsx | 222 ++++-
.../plugins/lens/public/drag_drop/readme.md | 4 +-
.../config_panel/config_panel.test.tsx | 8 +
.../config_panel/config_panel.tsx | 63 +-
.../config_panel/dimension_button.tsx | 66 ++
.../draggable_dimension_button.tsx | 110 +++
.../config_panel/empty_dimension_button.tsx | 97 ++
.../config_panel/layer_panel.scss | 8 +-
.../config_panel/layer_panel.test.tsx | 199 ++--
.../editor_frame/config_panel/layer_panel.tsx | 511 ++++------
.../config_panel/remove_layer_button.tsx | 60 ++
.../editor_frame/config_panel/types.ts | 26 +-
.../editor_frame/data_panel_wrapper.tsx | 6 +-
.../editor_frame/editor_frame.test.tsx | 24 +-
.../editor_frame/editor_frame.tsx | 12 +-
.../editor_frame/suggestion_helpers.ts | 4 +-
.../workspace_panel/workspace_panel.test.tsx | 12 +-
.../workspace_panel/workspace_panel.tsx | 16 +-
.../datapanel.test.tsx | 15 +-
.../indexpattern_datasource/datapanel.tsx | 27 +-
.../dimension_panel/droppable.test.ts | 7 +
.../dimension_panel/droppable.ts | 189 ++--
.../field_item.test.tsx | 2 +
.../indexpattern_datasource/field_item.tsx | 18 +-
.../indexpattern_datasource/field_list.tsx | 11 +-
.../fields_accordion.test.tsx | 1 +
.../fields_accordion.tsx | 105 ++-
.../indexpattern_datasource/indexpattern.tsx | 4 +-
.../public/indexpattern_datasource/mocks.ts | 5 +
x-pack/plugins/lens/public/types.ts | 8 +-
.../xy_visualization/xy_config_panel.tsx | 10 +-
.../translations/translations/ja-JP.json | 1 -
.../translations/translations/zh-CN.json | 1 -
.../test/functional/page_objects/lens_page.ts | 2 +-
39 files changed, 2074 insertions(+), 1152 deletions(-)
create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_button.tsx
create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx
create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx
create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/remove_layer_button.tsx
diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts
index 635fde6dad720..4a7e82d5b42c0 100644
--- a/test/functional/services/common/browser.ts
+++ b/test/functional/services/common/browser.ts
@@ -289,13 +289,13 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
}
const origin = document.querySelector(arguments[0]);
- const target = document.querySelector(arguments[1]);
const dragStartEvent = createEvent('dragstart');
dispatchEvent(origin, dragStartEvent);
setTimeout(() => {
const dropEvent = createEvent('drop');
+ const target = document.querySelector(arguments[1]);
dispatchEvent(target, dropEvent, dragStartEvent.dataTransfer);
const dragEndEvent = createEvent('dragend');
dispatchEvent(origin, dragEndEvent, dropEvent.dataTransfer);
diff --git a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
index dc53f3a2bc2a7..6423a9f6190a7 100644
--- a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
+++ b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
@@ -13,10 +13,8 @@ exports[`DragDrop items that have droppable=false get special styling when anoth